Merge branch 'dev' into empty_query

This commit is contained in:
Jack Ye 2025-05-03 08:59:50 +08:00 committed by GitHub
commit 6a4fdcb451
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
352 changed files with 8477 additions and 3578 deletions

91
.github/workflows/dotnet.yml vendored Normal file
View file

@ -0,0 +1,91 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
name: Build
on:
workflow_dispatch:
push:
branches:
- dev
- master
pull_request:
jobs:
build:
runs-on: windows-latest
env:
FlowVersion: 1.19.5
NUGET_CERT_REVOCATION_MODE: offline
BUILD_NUMBER: ${{ github.run_number }}
steps:
- uses: actions/checkout@v4
- name: Set Flow.Launcher.csproj version
id: update
uses: vers-one/dotnet-project-version-updater@v1.7
with:
file: |
"**/SolutionAssemblyInfo.cs"
version: ${{ env.FlowVersion }}.${{ env.BUILD_NUMBER }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
# cache: true
# cache-dependency-path: |
# Flow.Launcher/packages.lock.json
# Flow.Launcher.Core/packages.lock.json
# Flow.Launcher.Infrastructure/packages.lock.json
# Flow.Launcher.Plugin/packages.lock.json
- name: Install vpk
run: dotnet tool install -g vpk
- name: Restore dependencies
run: nuget restore
- name: Build
run: dotnet build --no-restore -c Release
- name: Initialize Service
run: |
sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest
net start WSearch
- name: Test
run: dotnet test --no-build --verbosity normal -c Release
- name: Perform post_build tasks
shell: powershell
run: .\Scripts\post_build.ps1
- name: Upload Plugin Nupkg
uses: actions/upload-artifact@v4
with:
name: Plugin nupkg
path: |
Output\Release\Flow.Launcher.Plugin.*.nupkg
compression-level: 0
- name: Upload Setup
uses: actions/upload-artifact@v4
with:
name: Flow Installer
path: |
Output\Packages\Flow-Launcher-*.exe
compression-level: 0
- name: Upload Portable Version
uses: actions/upload-artifact@v4
with:
name: Portable Version
path: |
Output\Packages\Flow-Launcher-Portable.zip
compression-level: 0
- name: Upload Full Nupkg
uses: actions/upload-artifact@v4
with:
name: Full nupkg
path: |
Output\Packages\FlowLauncher-*-full.nupkg
compression-level: 0
- name: Upload Release Information
uses: actions/upload-artifact@v4
with:
name: RELEASES
path: |
Output\Packages\RELEASES
compression-level: 0

21
.github/workflows/website_deploy.yml vendored Normal file
View file

@ -0,0 +1,21 @@
---
name: Deploy Website On Release
on:
release:
types: [published]
workflow_dispatch:
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Dispatch event
run: |
http_status=$(curl -L -f -s -o /dev/null -w "%{http_code}" \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.DEPLOY_FLOW_WEBSITE }}" \
https://api.github.com/repos/Flow-Launcher/flow-launcher.github.io/dispatches \
-d '{"event_type":"deploy"}')
if [ "$http_status" -ne 204 ]; then echo "Error: Deploy trigger failed, HTTP status code is $http_status"; exit 1; fi

View file

@ -1,28 +1,29 @@
using Microsoft.Win32; using System;
using Squirrel;
using System;
using System.IO; using System.IO;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Windows; 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 CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.Win32;
using Squirrel;
namespace Flow.Launcher.Core.Configuration namespace Flow.Launcher.Core.Configuration
{ {
public class Portable : IPortable public class Portable : IPortable
{ {
private static readonly string ClassName = nameof(Portable);
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>(); private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
/// <summary> /// <summary>
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private UpdateManager NewUpdateManager() private static UpdateManager NewUpdateManager()
{ {
var applicationFolderName = Constant.ApplicationDirectory var applicationFolderName = Constant.ApplicationDirectory
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None) .Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
@ -51,7 +52,7 @@ namespace Flow.Launcher.Core.Configuration
} }
catch (Exception e) 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,26 +76,22 @@ namespace Flow.Launcher.Core.Configuration
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception("|Portable.EnablePortableMode|Error occurred while enabling portable mode", e); API.LogException(ClassName, "Error occurred while enabling portable mode", e);
} }
} }
public void RemoveShortcuts() public void RemoveShortcuts()
{ {
using (var portabilityUpdater = NewUpdateManager()) using var portabilityUpdater = NewUpdateManager();
{ portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
}
} }
public void RemoveUninstallerEntry() public void RemoveUninstallerEntry()
{ {
using (var portabilityUpdater = NewUpdateManager()) using var portabilityUpdater = NewUpdateManager();
{ portabilityUpdater.RemoveUninstallerRegistryEntry();
portabilityUpdater.RemoveUninstallerRegistryEntry();
}
} }
public void MoveUserDataFolder(string fromLocation, string toLocation) public void MoveUserDataFolder(string fromLocation, string toLocation)
@ -110,12 +107,10 @@ namespace Flow.Launcher.Core.Configuration
public void CreateShortcuts() public void CreateShortcuts()
{ {
using (var portabilityUpdater = NewUpdateManager()) using var portabilityUpdater = NewUpdateManager();
{ portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
}
} }
public void CreateUninstallerEntry() public void CreateUninstallerEntry()
@ -129,18 +124,14 @@ namespace Flow.Launcher.Core.Configuration
subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String); subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String);
} }
using (var portabilityUpdater = NewUpdateManager()) using var portabilityUpdater = NewUpdateManager();
{ _ = portabilityUpdater.CreateUninstallerRegistryEntry();
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
}
} }
internal void IndicateDeletion(string filePathTodelete) private static void IndicateDeletion(string filePathTodelete)
{ {
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile); var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);
using (var _ = File.CreateText(deleteFilePath)) using var _ = File.CreateText(deleteFilePath);
{
}
} }
///<summary> ///<summary>

View file

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

View file

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins namespace Flow.Launcher.Core.ExternalPlugins
{ {
@ -39,10 +40,14 @@ namespace Flow.Launcher.Core.ExternalPlugins
var completedTask = await Task.WhenAny(tasks); var completedTask = await Task.WhenAny(tasks);
if (completedTask.IsCompletedSuccessfully) if (completedTask.IsCompletedSuccessfully)
{ {
// one of the requests completed successfully; keep its results var result = await completedTask;
// and cancel the remaining http requests. if (result != null)
pluginResults = await completedTask; {
cts.Cancel(); // one of the requests completed successfully; keep its results
// and cancel the remaining http requests.
pluginResults = result;
cts.Cancel();
}
} }
tasks.Remove(completedTask); tasks.Remove(completedTask);
} }

View file

@ -1,19 +1,20 @@
using Flow.Launcher.Infrastructure.Logger; using System;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Forms; using System.Windows.Forms;
using Flow.Launcher.Core.Resource;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.ExternalPlugins.Environments namespace Flow.Launcher.Core.ExternalPlugins.Environments
{ {
public abstract class AbstractPluginEnvironment public abstract class AbstractPluginEnvironment
{ {
private static readonly string ClassName = nameof(AbstractPluginEnvironment);
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>(); protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
internal abstract string Language { get; } internal abstract string Language { get; }
@ -42,8 +43,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal IEnumerable<PluginPair> Setup() internal IEnumerable<PluginPair> Setup()
{ {
// If no plugin is using the language, return empty list
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase))) if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
{
return new List<PluginPair>(); return new List<PluginPair>();
}
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath)) if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
{ {
@ -55,24 +59,55 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
} }
var noRuntimeMessage = string.Format( var noRuntimeMessage = string.Format(
InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"), API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
Language, Language,
EnvName, EnvName,
Environment.NewLine Environment.NewLine
); );
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{ {
var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName); var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
string selectedFile;
selectedFile = GetFileFromDialog(msg, FileDialogFilter); var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
if (!string.IsNullOrEmpty(selectedFile)) if (!string.IsNullOrEmpty(selectedFile))
{
PluginsSettingsFilePath = selectedFile; PluginsSettingsFilePath = selectedFile;
}
// Nothing selected because user pressed cancel from the file dialog window // Nothing selected because user pressed cancel from the file dialog window
if (string.IsNullOrEmpty(selectedFile)) else
InstallEnvironment(); {
var forceDownloadMessage = string.Format(
API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
Language,
EnvName,
Environment.NewLine
);
// Let users select valid path or choose to download
while (string.IsNullOrEmpty(selectedFile))
{
if (API.ShowMsgBox(forceDownloadMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
// Continue select file
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
}
else
{
// User selected no, break the loop
break;
}
}
if (!string.IsNullOrEmpty(selectedFile))
{
PluginsSettingsFilePath = selectedFile;
}
else
{
InstallEnvironment();
}
}
} }
else else
{ {
@ -85,8 +120,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
} }
else else
{ {
API.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language)); 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.", $"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment"); $"{Language}Environment");
@ -98,13 +133,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath) private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
{ {
if (expectedPath == currentPath) if (expectedPath == currentPath) return;
return;
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s)); FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
InstallEnvironment(); InstallEnvironment();
} }
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata); internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
@ -116,13 +149,16 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
foreach (var metadata in PluginMetadataList) foreach (var metadata in PluginMetadataList)
{ {
if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase)) if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase))
{
metadata.AssemblyName = string.Empty;
pluginPairs.Add(CreatePluginPair(filePath, metadata)); pluginPairs.Add(CreatePluginPair(filePath, metadata));
}
} }
return pluginPairs; return pluginPairs;
} }
private string GetFileFromDialog(string title, string filter = "") private static string GetFileFromDialog(string title, string filter = "")
{ {
var dlg = new OpenFileDialog var dlg = new OpenFileDialog
{ {
@ -136,7 +172,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
var result = dlg.ShowDialog(); var result = dlg.ShowDialog();
return result == DialogResult.OK ? dlg.FileName : string.Empty; return result == DialogResult.OK ? dlg.FileName : string.Empty;
} }
/// <summary> /// <summary>
@ -179,31 +214,33 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
else else
{ {
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName)) if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
{
settings.PluginSettings.PythonExecutablePath settings.PluginSettings.PythonExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath); = GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
}
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName)) if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
{
settings.PluginSettings.NodeExecutablePath settings.PluginSettings.NodeExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath); = GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
}
} }
} }
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName) private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
{ {
if (string.IsNullOrEmpty(filePath)) if (string.IsNullOrEmpty(filePath)) return false;
return false;
// DataLocation.PortableDataPath returns the current portable path, this determines if an out // DataLocation.PortableDataPath returns the current portable path, this determines if an out
// of date path is also a portable path. // of date path is also a portable path.
var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}"; var portableAppEnvLocation = Path.Combine("UserData", DataLocation.PluginEnvironments, pluginEnvironmentName);
return filePath.Contains(portableAppEnvLocation); return filePath.Contains(portableAppEnvLocation);
} }
private static bool IsUsingRoamingPath(string filePath) private static bool IsUsingRoamingPath(string filePath)
{ {
if (string.IsNullOrEmpty(filePath)) if (string.IsNullOrEmpty(filePath)) return false;
return false;
return filePath.StartsWith(DataLocation.RoamingDataPath); return filePath.StartsWith(DataLocation.RoamingDataPath);
} }
@ -213,8 +250,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
var index = filePath.IndexOf(DataLocation.PluginEnvironments); var index = filePath.IndexOf(DataLocation.PluginEnvironments);
// get the substring after "Environments" because we can not determine it dynamically // get the substring after "Environments" because we can not determine it dynamically
var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count()); var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}"; return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
} }
} }
} }

View file

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

View file

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

View file

@ -1,10 +1,11 @@
using Droplex; using System.Collections.Generic;
using System.IO;
using Droplex;
using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using System.Collections.Generic; using Microsoft.VisualStudio.Threading;
using System.IO;
namespace Flow.Launcher.Core.ExternalPlugins.Environments namespace Flow.Launcher.Core.ExternalPlugins.Environments
{ {
@ -22,17 +23,23 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string FileDialogFilter => "Python|pythonw.exe"; internal override string FileDialogFilter => "Python|pythonw.exe";
internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; } internal override string PluginsSettingsFilePath
{
get => PluginSettings.PythonExecutablePath;
set => PluginSettings.PythonExecutablePath = value;
}
internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
internal override void InstallEnvironment() internal override void InstallEnvironment()
{ {
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
// Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and // 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 // 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; PluginsSettingsFilePath = ExecutablePath;
} }

View file

@ -1,10 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using Droplex;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin;
using System.IO; using System.IO;
using Droplex;
using Flow.Launcher.Core.Plugin; 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 namespace Flow.Launcher.Core.ExternalPlugins.Environments
{ {
@ -19,15 +20,21 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } internal override string PluginsSettingsFilePath
{
get => PluginSettings.NodeExecutablePath;
set => PluginSettings.NodeExecutablePath = value;
}
internal TypeScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal TypeScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
internal override void InstallEnvironment() internal override void InstallEnvironment()
{ {
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); 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; PluginsSettingsFilePath = ExecutablePath;
} }

View file

@ -1,10 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using Droplex;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin;
using System.IO; using System.IO;
using Droplex;
using Flow.Launcher.Core.Plugin; 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 namespace Flow.Launcher.Core.ExternalPlugins.Environments
{ {
@ -19,15 +20,21 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } internal override string PluginsSettingsFilePath
{
get => PluginSettings.NodeExecutablePath;
set => PluginSettings.NodeExecutablePath = value;
}
internal TypeScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal TypeScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
internal override void InstallEnvironment() internal override void InstallEnvironment()
{ {
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); 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; PluginsSettingsFilePath = ExecutablePath;
} }

View file

@ -1,13 +1,16 @@
using Flow.Launcher.Infrastructure.Logger; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins namespace Flow.Launcher.Core.ExternalPlugins
{ {
public static class PluginsManifest public static class PluginsManifest
{ {
private static readonly string ClassName = nameof(PluginsManifest);
private static readonly CommunityPluginStore mainPluginStore = private static readonly CommunityPluginStore mainPluginStore =
new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json", 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", "https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json",
@ -17,11 +20,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
private static readonly SemaphoreSlim manifestUpdateLock = new(1); private static readonly SemaphoreSlim manifestUpdateLock = new(1);
private static DateTime lastFetchedAt = DateTime.MinValue; private static DateTime lastFetchedAt = DateTime.MinValue;
private static TimeSpan fetchTimeout = TimeSpan.FromMinutes(2); private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
public static List<UserPlugin> UserPlugins { get; private set; } public static List<UserPlugin> UserPlugins { get; private set; }
public static async Task<bool> UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false) public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
{ {
try try
{ {
@ -43,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e); Ioc.Default.GetRequiredService<IPublicAPI>().LogException(ClassName, "Http request failed", e);
} }
finally finally
{ {

View file

@ -1,23 +0,0 @@
using System;
namespace Flow.Launcher.Core.ExternalPlugins
{
public record UserPlugin
{
public string ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Author { get; set; }
public string Version { get; set; }
public string Language { get; set; }
public string Website { get; set; }
public string UrlDownload { get; set; }
public string UrlSourceCode { get; set; }
public string LocalInstallPath { get; set; }
public string IcoPath { get; set; }
public DateTime? LatestReleaseDate { get; set; }
public DateTime? DateAdded { get; set; }
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
}
}

View file

@ -1,28 +1,14 @@
using Flow.Launcher.Core.Resource; using System;
using Flow.Launcher.Infrastructure;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Microsoft.IO; using Microsoft.IO;
using System.Windows;
using System.Windows.Controls;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using CheckBox = System.Windows.Controls.CheckBox;
using Control = System.Windows.Controls.Control;
using Orientation = System.Windows.Controls.Orientation;
using TextBox = System.Windows.Controls.TextBox;
using UserControl = System.Windows.Controls.UserControl;
using System.Windows.Documents;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
@ -32,7 +18,9 @@ namespace Flow.Launcher.Core.Plugin
/// </summary> /// </summary>
internal abstract class JsonRPCPlugin : JsonRPCPluginBase internal abstract class JsonRPCPlugin : JsonRPCPluginBase
{ {
public const string JsonRPC = "JsonRPC"; public new const string JsonRPC = "JsonRPC";
private static readonly string ClassName = nameof(JsonRPCPlugin);
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default); protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default); protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
@ -41,9 +29,6 @@ namespace Flow.Launcher.Core.Plugin
private int RequestId { get; set; } private int RequestId { get; set; }
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
public override List<Result> LoadContextMenus(Result selectedResult) public override List<Result> LoadContextMenus(Result selectedResult)
{ {
var request = new JsonRPCRequestModel(RequestId++, var request = new JsonRPCRequestModel(RequestId++,
@ -69,13 +54,6 @@ namespace Flow.Launcher.Core.Plugin
} }
}; };
private static readonly JsonSerializerOptions settingSerializeOption = new()
{
WriteIndented = true
};
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
private async Task<List<Result>> DeserializedResultAsync(Stream output) private async Task<List<Result>> DeserializedResultAsync(Stream output)
{ {
await using (output) await using (output)
@ -134,7 +112,6 @@ namespace Flow.Launcher.Core.Plugin
return !result.JsonRPCAction.DontHideAfterAction; return !result.JsonRPCAction.DontHideAfterAction;
} }
/// <summary> /// <summary>
/// Execute external program and return the output /// Execute external program and return the output
/// </summary> /// </summary>
@ -172,11 +149,11 @@ namespace Flow.Launcher.Core.Plugin
var error = standardError.ReadToEnd(); var error = standardError.ReadToEnd();
if (!string.IsNullOrEmpty(error)) if (!string.IsNullOrEmpty(error))
{ {
Log.Error($"|JsonRPCPlugin.Execute|{error}"); Context.API.LogError(ClassName, error);
return string.Empty; return string.Empty;
} }
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error."); Context.API.LogError(ClassName, "Empty standard output and standard error.");
return string.Empty; return string.Empty;
} }
@ -184,8 +161,8 @@ namespace Flow.Launcher.Core.Plugin
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception( Context.API.LogException(ClassName,
$"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", $"Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>",
e); e);
return string.Empty; return string.Empty;
} }
@ -196,7 +173,7 @@ namespace Flow.Launcher.Core.Plugin
using var process = Process.Start(startInfo); using var process = Process.Start(startInfo);
if (process == null) if (process == null)
{ {
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process"); Context.API.LogError(ClassName, "Can't start new process");
return Stream.Null; return Stream.Null;
} }
@ -216,7 +193,7 @@ namespace Flow.Launcher.Core.Plugin
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception("|JsonRPCPlugin.ExecuteAsync|Exception when kill process", e); Context.API.LogException(ClassName, "Exception when kill process", e);
} }
}); });
@ -237,7 +214,7 @@ namespace Flow.Launcher.Core.Plugin
{ {
case (0, 0): case (0, 0):
const string errorMessage = "Empty JSON-RPC Response."; const string errorMessage = "Empty JSON-RPC Response.";
Log.Warn($"|{nameof(JsonRPCPlugin)}.{nameof(ExecuteAsync)}|{errorMessage}"); Context.API.LogWarn(ClassName, errorMessage);
break; break;
case (_, not 0): case (_, not 0):
throw new InvalidDataException(Encoding.UTF8.GetString(errorBuffer.ToArray())); // The process has exited with an error message throw new InvalidDataException(Encoding.UTF8.GetString(errorBuffer.ToArray())); // The process has exited with an error message

View file

@ -1,32 +1,15 @@
using Flow.Launcher.Core.Resource; using System;
using Flow.Launcher.Infrastructure;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Microsoft.IO;
using System.Windows;
using System.Windows.Controls;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
using CheckBox = System.Windows.Controls.CheckBox;
using Control = System.Windows.Controls.Control; using Control = System.Windows.Controls.Control;
using Orientation = System.Windows.Controls.Orientation;
using TextBox = System.Windows.Controls.TextBox;
using UserControl = System.Windows.Controls.UserControl;
using System.Windows.Documents;
using static System.Windows.Forms.LinkLabel;
using Droplex;
using System.Windows.Forms;
using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
@ -36,16 +19,14 @@ namespace Flow.Launcher.Core.Plugin
/// </summary> /// </summary>
public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
{ {
protected PluginInitContext Context;
public const string JsonRPC = "JsonRPC"; public const string JsonRPC = "JsonRPC";
private int RequestId { get; set; } protected PluginInitContext Context;
private string SettingConfigurationPath => private string SettingConfigurationPath =>
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
private string SettingDirectory => Path.Combine(DataLocation.PluginSettingsDirectory, private string SettingDirectory => Context.CurrentPluginMetadata.PluginSettingsDirectoryPath;
Context.CurrentPluginMetadata.Name);
private string SettingPath => Path.Combine(SettingDirectory, "Settings.json"); private string SettingPath => Path.Combine(SettingDirectory, "Settings.json");
@ -125,7 +106,6 @@ namespace Flow.Launcher.Core.Plugin
public abstract Task<List<Result>> QueryAsync(Query query, CancellationToken token); public abstract Task<List<Result>> QueryAsync(Query query, CancellationToken token);
private async Task InitSettingAsync() private async Task InitSettingAsync()
{ {
JsonRpcConfigurationModel configuration = null; JsonRpcConfigurationModel configuration = null;
@ -137,7 +117,6 @@ namespace Flow.Launcher.Core.Plugin
await File.ReadAllTextAsync(SettingConfigurationPath)); await File.ReadAllTextAsync(SettingConfigurationPath));
} }
Settings ??= new JsonRPCPluginSettings Settings ??= new JsonRPCPluginSettings
{ {
Configuration = configuration, SettingPath = SettingPath, API = Context.API Configuration = configuration, SettingPath = SettingPath, API = Context.API
@ -148,7 +127,7 @@ namespace Flow.Launcher.Core.Plugin
public virtual async Task InitAsync(PluginInitContext context) public virtual async Task InitAsync(PluginInitContext context)
{ {
this.Context = context; Context = context;
await InitSettingAsync(); await InitSettingAsync();
} }
@ -166,13 +145,5 @@ namespace Flow.Launcher.Core.Plugin
{ {
return Settings.CreateSettingPanel(); return Settings.CreateSettingPanel();
} }
public void DeletePluginSettingsDirectory()
{
if (Directory.Exists(SettingDirectory))
{
Directory.Delete(SettingDirectory, true);
}
}
} }
} }

View file

@ -23,6 +23,8 @@ namespace Flow.Launcher.Core.Plugin
protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!; protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!;
public required IPublicAPI API { get; init; } public required IPublicAPI API { get; init; }
private static readonly string ClassName = nameof(JsonRPCPluginSettings);
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!; private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin"); private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
@ -111,7 +113,7 @@ namespace Flow.Launcher.Core.Plugin
// If can parse the default value to bool, use it, otherwise use false // If can parse the default value to bool, use it, otherwise use false
: value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString) : value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString)
&& boolValueFromString; && boolValueFromString;
checkBox.Dispatcher.Invoke(() =>checkBox.IsChecked = isChecked); checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = isChecked);
break; break;
} }
} }
@ -122,12 +124,26 @@ namespace Flow.Launcher.Core.Plugin
public async Task SaveAsync() public async Task SaveAsync()
{ {
await _storage.SaveAsync(); try
{
await _storage.SaveAsync();
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
}
} }
public void Save() public void Save()
{ {
_storage.Save(); try
{
_storage.Save();
}
catch (System.Exception e)
{
API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
}
} }
public bool NeedCreateSettingPanel() public bool NeedCreateSettingPanel()
@ -138,8 +154,7 @@ namespace Flow.Launcher.Core.Plugin
public Control CreateSettingPanel() public Control CreateSettingPanel()
{ {
// No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true if (!NeedCreateSettingPanel()) return null!;
// if (!NeedCreateSettingPanel()) return null;
// Create main grid with two columns (Column 1: Auto, Column 2: *) // Create main grid with two columns (Column 1: Auto, Column 2: *)
var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center };

View file

@ -10,20 +10,20 @@ using Microsoft.VisualStudio.Threading;
using StreamJsonRpc; using StreamJsonRpc;
using IAsyncDisposable = System.IAsyncDisposable; using IAsyncDisposable = System.IAsyncDisposable;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated
{ {
public const string JsonRpc = "JsonRPC"; public const string JsonRpc = "JsonRPC";
private static readonly string ClassName = nameof(JsonRPCPluginV2);
protected abstract IDuplexPipe ClientPipe { get; set; } protected abstract IDuplexPipe ClientPipe { get; set; }
protected StreamReader ErrorStream { get; set; } protected StreamReader ErrorStream { get; set; }
private JsonRpc RPC { get; set; } private JsonRpc RPC { get; set; }
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result) protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
{ {
var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method, var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method,
@ -55,7 +55,6 @@ namespace Flow.Launcher.Core.Plugin
return results; return results;
} }
public override async Task InitAsync(PluginInitContext context) public override async Task InitAsync(PluginInitContext context)
{ {
await base.InitAsync(context); await base.InitAsync(context);
@ -88,7 +87,6 @@ namespace Flow.Launcher.Core.Plugin
protected abstract MessageHandlerType MessageHandler { get; } protected abstract MessageHandlerType MessageHandler { get; }
private void SetupJsonRPC() private void SetupJsonRPC()
{ {
var formatter = new SystemTextJsonFormatter { JsonSerializerOptions = RequestSerializeOption }; var formatter = new SystemTextJsonFormatter { JsonSerializerOptions = RequestSerializeOption };
@ -118,8 +116,17 @@ namespace Flow.Launcher.Core.Plugin
{ {
await RPC.InvokeAsync("reload_data", Context); await RPC.InvokeAsync("reload_data", Context);
} }
catch (RemoteMethodNotFoundException e) catch (RemoteMethodNotFoundException)
{ {
// Ignored
}
catch (ConnectionLostException)
{
// Ignored
}
catch (Exception e)
{
Context.API.LogException(ClassName, $"Failed to call reload_data for plugin {Context.CurrentPluginMetadata.Name}", e);
} }
} }
@ -129,8 +136,17 @@ namespace Flow.Launcher.Core.Plugin
{ {
await RPC.InvokeAsync("close"); await RPC.InvokeAsync("close");
} }
catch (RemoteMethodNotFoundException e) catch (RemoteMethodNotFoundException)
{ {
// Ignored
}
catch (ConnectionLostException)
{
// Ignored
}
catch (Exception e)
{
Context.API.LogException(ClassName, $"Failed to call close for plugin {Context.CurrentPluginMetadata.Name}", e);
} }
finally finally
{ {

View file

@ -12,7 +12,7 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
{ {
public class JsonRPCPublicAPI public class JsonRPCPublicAPI
{ {
private IPublicAPI _api; private readonly IPublicAPI _api;
public JsonRPCPublicAPI(IPublicAPI api) public JsonRPCPublicAPI(IPublicAPI api)
{ {
@ -104,7 +104,6 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
return _api.GetAllPlugins(); return _api.GetAllPlugins();
} }
public MatchResult FuzzySearch(string query, string stringToCompare) public MatchResult FuzzySearch(string query, string stringToCompare)
{ {
return _api.FuzzySearch(query, stringToCompare); return _api.FuzzySearch(query, stringToCompare);
@ -156,6 +155,11 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
_api.LogWarn(className, message, methodName); _api.LogWarn(className, message, methodName);
} }
public void LogError(string className, string message, [CallerMemberName] string methodName = "")
{
_api.LogError(className, message, methodName);
}
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null) public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null)
{ {
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath); _api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
@ -185,5 +189,10 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
{ {
_api.StopLoadingBar(); _api.StopLoadingBar();
} }
public void SavePluginCaches()
{
_api.SavePluginCaches();
}
} }
} }

View file

@ -1,17 +1,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.IO; using System.IO;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using System.Text.Json; using System.Text.Json;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
internal abstract class PluginConfig 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> /// <summary>
/// Parse plugin metadata in the given directories /// Parse plugin metadata in the given directories
/// </summary> /// </summary>
@ -33,7 +38,7 @@ namespace Flow.Launcher.Core.Plugin
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception($"|PluginConfig.ParsePLuginConfigs|Can't delete <{directory}>", e); API.LogException(ClassName, $"Can't delete <{directory}>", e);
} }
} }
else else
@ -50,11 +55,11 @@ namespace Flow.Launcher.Core.Plugin
duplicateList duplicateList
.ForEach( .ForEach(
x => Log.Warn("PluginConfig", x => API.LogWarn(ClassName,
string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " + string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " +
"not loaded due to version not the highest of the duplicates", "not loaded due to version not the highest of the duplicates",
x.Name, x.ID, x.Version), x.Name, x.ID, x.Version),
"GetUniqueLatestPluginMetadata")); "GetUniqueLatestPluginMetadata"));
return uniqueList; return uniqueList;
} }
@ -102,7 +107,7 @@ namespace Flow.Launcher.Core.Plugin
string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName); string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName);
if (!File.Exists(configPath)) 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; return null;
} }
@ -112,29 +117,29 @@ namespace Flow.Launcher.Core.Plugin
metadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(configPath)); metadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(configPath));
metadata.PluginDirectory = pluginDirectory; metadata.PluginDirectory = pluginDirectory;
// for plugins which doesn't has ActionKeywords key // for plugins which doesn't has ActionKeywords key
metadata.ActionKeywords = metadata.ActionKeywords ?? new List<string> { metadata.ActionKeyword }; metadata.ActionKeywords ??= new List<string> { metadata.ActionKeyword };
// for plugin still use old ActionKeyword // for plugin still use old ActionKeyword
metadata.ActionKeyword = metadata.ActionKeywords?[0]; metadata.ActionKeyword = metadata.ActionKeywords?[0];
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception($"|PluginConfig.GetPluginMetadata|invalid json for config <{configPath}>", e); API.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
return null; return null;
} }
if (!AllowedLanguage.IsAllowed(metadata.Language)) 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; return null;
} }
if (!File.Exists(metadata.ExecuteFilePath)) 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; return null;
} }
return metadata; return metadata;
} }
} }
} }

View file

@ -1,20 +1,19 @@
using Flow.Launcher.Core.ExternalPlugins; using System;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using ISavable = Flow.Launcher.Plugin.ISavable;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using System.Text.Json; using IRemovable = Flow.Launcher.Core.Storage.IRemovable;
using Flow.Launcher.Core.Resource; using ISavable = Flow.Launcher.Plugin.ISavable;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
@ -23,6 +22,8 @@ namespace Flow.Launcher.Core.Plugin
/// </summary> /// </summary>
public static class PluginManager public static class PluginManager
{ {
private static readonly string ClassName = nameof(PluginManager);
private static IEnumerable<PluginPair> _contextMenuPlugins; private static IEnumerable<PluginPair> _contextMenuPlugins;
public static List<PluginPair> AllPlugins { get; private set; } public static List<PluginPair> AllPlugins { get; private set; }
@ -35,7 +36,7 @@ namespace Flow.Launcher.Core.Plugin
private static PluginsSettings Settings; private static PluginsSettings Settings;
private static List<PluginMetadata> _metadatas; private static List<PluginMetadata> _metadatas;
private static List<string> _modifiedPlugins = new List<string>(); private static readonly List<string> _modifiedPlugins = new();
/// <summary> /// <summary>
/// Directories that will hold Flow Launcher plugin directory /// Directories that will hold Flow Launcher plugin directory
@ -59,18 +60,34 @@ namespace Flow.Launcher.Core.Plugin
/// </summary> /// </summary>
public static void Save() public static void Save()
{ {
foreach (var plugin in AllPlugins) foreach (var pluginPair in AllPlugins)
{ {
var savable = plugin.Plugin as ISavable; var savable = pluginPair.Plugin as ISavable;
savable?.Save(); try
{
savable?.Save();
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
}
} }
API.SavePluginSettings(); API.SavePluginSettings();
API.SavePluginCaches();
} }
public static async ValueTask DisposePluginsAsync() public static async ValueTask DisposePluginsAsync()
{ {
foreach (var pluginPair in AllPlugins) foreach (var pluginPair in AllPlugins)
{
await DisposePluginAsync(pluginPair);
}
}
private static async Task DisposePluginAsync(PluginPair pluginPair)
{
try
{ {
switch (pluginPair.Plugin) switch (pluginPair.Plugin)
{ {
@ -82,6 +99,10 @@ namespace Flow.Launcher.Core.Plugin
break; break;
} }
} }
catch (Exception e)
{
API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
}
} }
public static async Task ReloadDataAsync() public static async Task ReloadDataAsync()
@ -155,6 +176,25 @@ namespace Flow.Launcher.Core.Plugin
Settings = settings; Settings = settings;
Settings.UpdatePluginSettings(_metadatas); Settings.UpdatePluginSettings(_metadatas);
AllPlugins = PluginsLoader.Plugins(_metadatas, Settings); AllPlugins = PluginsLoader.Plugins(_metadatas, Settings);
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
UpdatePluginDirectory(_metadatas);
}
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
{
foreach (var metadata in metadatas)
{
if (AllowedLanguage.IsDotNet(metadata.Language))
{
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName);
metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.AssemblyName);
}
else
{
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name);
metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.Name);
}
}
} }
/// <summary> /// <summary>
@ -169,16 +209,16 @@ namespace Flow.Launcher.Core.Plugin
{ {
try try
{ {
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API))); () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
pair.Metadata.InitTime += milliseconds; pair.Metadata.InitTime += milliseconds;
Log.Info( API.LogInfo(ClassName,
$"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); $"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e); API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
pair.Metadata.Disabled = true; pair.Metadata.Disabled = true;
failedPlugins.Enqueue(pair); failedPlugins.Enqueue(pair);
} }
@ -225,10 +265,9 @@ namespace Flow.Launcher.Core.Plugin
if (query is null) if (query is null)
return Array.Empty<PluginPair>(); return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword)) if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
return GlobalPlugins; return GlobalPlugins;
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair> return new List<PluginPair>
{ {
plugin plugin
@ -242,7 +281,7 @@ namespace Flow.Launcher.Core.Plugin
try try
{ {
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false)); async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
@ -266,7 +305,7 @@ namespace Flow.Launcher.Core.Plugin
{ {
Title = $"{metadata.Name}: Failed to respond!", Title = $"{metadata.Name}: Failed to respond!",
SubTitle = "Select this result for more info", SubTitle = "Select this result for more info",
IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon, IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory, PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword, ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID, PluginID = metadata.ID,
@ -330,8 +369,8 @@ namespace Flow.Launcher.Core.Plugin
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception( API.LogException(ClassName,
$"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>", $"Can't load context menus for plugin <{pluginPair.Metadata.Name}>",
e); e);
} }
} }
@ -343,8 +382,8 @@ namespace Flow.Launcher.Core.Plugin
{ {
// this method is only checking for action keywords (defined as not '*') registration // this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic // hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword); && NonGlobalPlugins.ContainsKey(actionKeyword);
} }
/// <summary> /// <summary>
@ -432,37 +471,26 @@ namespace Flow.Launcher.Core.Plugin
#region Public functions #region Public functions
public static bool PluginModified(string uuid) public static bool PluginModified(string id)
{ {
return _modifiedPlugins.Contains(uuid); return _modifiedPlugins.Contains(id);
} }
public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
/// <summary>
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
/// unless it's a local path installation
/// </summary>
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
{ {
InstallPlugin(newVersion, zipFilePath, checkModified:false); InstallPlugin(newVersion, zipFilePath, checkModified:false);
UninstallPlugin(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false); await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
_modifiedPlugins.Add(existingVersion.ID); _modifiedPlugins.Add(existingVersion.ID);
} }
/// <summary>
/// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
/// </summary>
public static void InstallPlugin(UserPlugin plugin, string zipFilePath) public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
{ {
InstallPlugin(plugin, zipFilePath, checkModified: true); InstallPlugin(plugin, zipFilePath, checkModified: true);
} }
/// <summary> public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
/// Uninstall a plugin.
/// </summary>
public static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
{ {
UninstallPlugin(plugin, removePluginFromSettings, removePluginSettings, true); await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true);
} }
#endregion #endregion
@ -503,20 +531,20 @@ namespace Flow.Launcher.Core.Plugin
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}"; var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
var defaultPluginIDs = new List<string> var defaultPluginIDs = new List<string>
{ {
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
"572be03c74c642baae319fc283e561a8", // Explorer "572be03c74c642baae319fc283e561a8", // Explorer
"6A122269676E40EB86EB543B945932B9", // PluginIndicator "6A122269676E40EB86EB543B945932B9", // PluginIndicator
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
"791FC278BA414111B8D1886DFE447410", // Program "791FC278BA414111B8D1886DFE447410", // Program
"D409510CD0D2481F853690A07E6DC426", // Shell "D409510CD0D2481F853690A07E6DC426", // Shell
"CEA08895D2544B019B2E9C5009600DF4", // Sys "CEA08895D2544B019B2E9C5009600DF4", // Sys
"0308FD86DE0A4DEE8D62B9B535370992", // URL "0308FD86DE0A4DEE8D62B9B535370992", // URL
"565B73353DBF4806919830B9202EE3BF", // WebSearch "565B73353DBF4806919830B9202EE3BF", // WebSearch
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
}; };
// Treat default plugin differently, it needs to be removable along with each flow release // Treat default plugin differently, it needs to be removable along with each flow release
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID) var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
@ -534,7 +562,7 @@ namespace Flow.Launcher.Core.Plugin
} }
catch (Exception e) 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) if (checkModified)
@ -543,64 +571,63 @@ namespace Flow.Launcher.Core.Plugin
} }
} }
internal static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
{ {
if (checkModified && PluginModified(plugin.ID)) if (checkModified && PluginModified(plugin.ID))
{ {
throw new ArgumentException($"Plugin {plugin.Name} has been modified"); throw new ArgumentException($"Plugin {plugin.Name} has been modified");
} }
if (removePluginSettings || removePluginFromSettings)
{
// If we want to remove plugin from AllPlugins,
// we need to dispose them so that they can release file handles
// which can help FL to delete the plugin settings & cache folders successfully
var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID);
foreach (var pluginPair in pluginPairs)
{
await DisposePluginAsync(pluginPair);
}
}
if (removePluginSettings) if (removePluginSettings)
{ {
if (AllowedLanguage.IsDotNet(plugin.Language)) // for the plugin in .NET, we can use assembly loader // For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances
if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable)
{ {
var assemblyLoader = new PluginAssemblyLoader(plugin.ExecuteFilePath); removable.RemovePluginSettings(plugin.AssemblyName);
var assembly = assemblyLoader.LoadAssemblyAndDependencies(); removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath);
var assemblyName = assembly.GetName().Name;
// if user want to remove the plugin settings, we cannot call save method for the plugin json storage instance of this plugin
// so we need to remove it from the api instance
var method = API.GetType().GetMethod("RemovePluginSettings");
var pluginJsonStorage = method?.Invoke(API, new object[] { assemblyName });
// if there exists a json storage for current plugin, we need to delete the directory path
if (pluginJsonStorage != null)
{
var deleteMethod = pluginJsonStorage.GetType().GetMethod("DeleteDirectory");
try
{
deleteMethod?.Invoke(pluginJsonStorage, null);
}
catch (Exception e)
{
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e);
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
}
}
} }
else // the plugin with json prc interface
try
{ {
var pluginPair = AllPlugins.FirstOrDefault(p => p.Metadata.ID == plugin.ID); var pluginSettingsDirectory = plugin.PluginSettingsDirectoryPath;
if (pluginPair != null && pluginPair.Plugin is JsonRPCPlugin jsonRpcPlugin) if (Directory.Exists(pluginSettingsDirectory))
{ Directory.Delete(pluginSettingsDirectory, true);
try }
{ catch (Exception e)
jsonRpcPlugin.DeletePluginSettingsDirectory(); {
} API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
catch (Exception e) API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
{ string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e);
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
}
}
} }
} }
if (removePluginFromSettings) if (removePluginFromSettings)
{ {
Settings.Plugins.Remove(plugin.ID); try
{
var pluginCacheDirectory = plugin.PluginCacheDirectoryPath;
if (Directory.Exists(pluginCacheDirectory))
Directory.Delete(pluginCacheDirectory, true);
}
catch (Exception 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));
}
Settings.RemovePluginSettings(plugin.ID);
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
} }

View file

@ -11,12 +11,17 @@ using Flow.Launcher.Infrastructure.Logger;
#pragma warning restore IDE0005 #pragma warning restore IDE0005
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {
public static class PluginsLoader public static class PluginsLoader
{ {
private static readonly string ClassName = nameof(PluginsLoader);
// 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 static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings) public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
{ {
var dotnetPlugins = DotNetPlugins(metadatas); var dotnetPlugins = DotNetPlugins(metadatas);
@ -50,7 +55,7 @@ namespace Flow.Launcher.Core.Plugin
return plugins; return plugins;
} }
public static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source) private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
{ {
var erroredPlugins = new List<string>(); var erroredPlugins = new List<string>();
@ -59,8 +64,7 @@ namespace Flow.Launcher.Core.Plugin
foreach (var metadata in metadatas) foreach (var metadata in metadatas)
{ {
var milliseconds = Stopwatch.Debug( var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
$"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () =>
{ {
Assembly assembly = null; Assembly assembly = null;
IAsyncPlugin plugin = null; IAsyncPlugin plugin = null;
@ -74,28 +78,30 @@ namespace Flow.Launcher.Core.Plugin
typeof(IAsyncPlugin)); typeof(IAsyncPlugin));
plugin = Activator.CreateInstance(type) as IAsyncPlugin; plugin = Activator.CreateInstance(type) as IAsyncPlugin;
metadata.AssemblyName = assembly.GetName().Name;
} }
#if DEBUG #if DEBUG
catch (Exception e) catch (Exception)
{ {
throw; throw;
} }
#else #else
catch (Exception e) when (assembly == null) 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) 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) 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) 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 #endif
@ -112,7 +118,7 @@ namespace Flow.Launcher.Core.Plugin
if (erroredPlugins.Count > 0) if (erroredPlugins.Count > 0)
{ {
var errorPluginString = String.Join(Environment.NewLine, erroredPlugins); var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
var errorMessage = "The following " var errorMessage = "The following "
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
@ -130,23 +136,31 @@ namespace Flow.Launcher.Core.Plugin
return plugins; return plugins;
} }
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source) private static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
{ {
return source return source
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
.Select(metadata => new PluginPair .Select(metadata =>
{ {
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata return new PluginPair
{
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
Metadata = metadata
};
}); });
} }
public static IEnumerable<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source) private static IEnumerable<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source)
{ {
return source return source
.Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase)) .Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase))
.Select(metadata => new PluginPair .Select(metadata =>
{ {
Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata return new PluginPair
{
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
Metadata = metadata
};
}); });
} }
} }

View file

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

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
@ -33,7 +33,7 @@ namespace Flow.Launcher.Core.Plugin
searchTerms = terms; searchTerms = terms;
} }
return new Query () return new Query()
{ {
Search = search, Search = search,
RawQuery = rawQuery, RawQuery = rawQuery,
@ -42,4 +42,4 @@ namespace Flow.Launcher.Core.Plugin
}; };
} }
} }
} }

View file

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

View file

@ -1,12 +0,0 @@
using System;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Resource
{
[Obsolete("InternationalizationManager.Instance is obsolete. Use Ioc.Default.GetRequiredService<Internationalization>() instead.")]
public static class InternationalizationManager
{
public static Internationalization Instance
=> Ioc.Default.GetRequiredService<Internationalization>();
}
}

View file

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

View file

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

View file

@ -13,9 +13,9 @@ using System.Windows.Media.Effects;
using System.Windows.Shell; using System.Windows.Shell;
using System.Windows.Threading; using System.Windows.Threading;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Microsoft.Win32; using Microsoft.Win32;
namespace Flow.Launcher.Core.Resource namespace Flow.Launcher.Core.Resource
@ -24,6 +24,8 @@ namespace Flow.Launcher.Core.Resource
{ {
#region Properties & Fields #region Properties & Fields
private readonly string ClassName = nameof(Theme);
public bool BlurEnabled { get; private set; } public bool BlurEnabled { get; private set; }
private const string ThemeMetadataNamePrefix = "Name:"; private const string ThemeMetadataNamePrefix = "Name:";
@ -72,20 +74,15 @@ namespace Flow.Launcher.Core.Resource
} }
else 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; _oldTheme = Constant.DefaultTheme;
}; }
} }
#endregion #endregion
#region Theme Resources #region Theme Resources
public string GetCurrentTheme()
{
return _settings.Theme;
}
private void MakeSureThemeDirectoriesExist() private void MakeSureThemeDirectoriesExist()
{ {
foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir))) foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir)))
@ -96,7 +93,7 @@ namespace Flow.Launcher.Core.Resource
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e); _api.LogException(ClassName, $"Exception when create directory <{dir}>", e);
} }
} }
} }
@ -127,9 +124,9 @@ namespace Flow.Launcher.Core.Resource
try try
{ {
// Load a ResourceDictionary for the specified theme. // Load a ResourceDictionary for the specified theme.
var themeName = GetCurrentTheme(); var themeName = _settings.Theme;
var dict = GetThemeResourceDictionary(themeName); var dict = GetThemeResourceDictionary(themeName);
// Apply font settings to the theme resource. // Apply font settings to the theme resource.
ApplyFontSettings(dict); ApplyFontSettings(dict);
UpdateResourceDictionary(dict); UpdateResourceDictionary(dict);
@ -139,7 +136,7 @@ namespace Flow.Launcher.Core.Resource
} }
catch (Exception e) catch (Exception e)
{ {
Log.Exception("Error occurred while updating theme fonts", e); _api.LogException(ClassName, "Error occurred while updating theme fonts", e);
} }
} }
@ -155,11 +152,11 @@ namespace Flow.Launcher.Core.Resource
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle);
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true); SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true);
SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
} }
if (dict["ItemTitleStyle"] is Style resultItemStyle && if (dict["ItemTitleStyle"] is Style resultItemStyle &&
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
@ -175,7 +172,7 @@ namespace Flow.Launcher.Core.Resource
SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
} }
if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle && if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
{ {
@ -200,7 +197,7 @@ namespace Flow.Launcher.Core.Resource
// First, find the setters to remove and store them in a list // First, find the setters to remove and store them in a list
var settersToRemove = style.Setters var settersToRemove = style.Setters
.OfType<Setter>() .OfType<Setter>()
.Where(setter => .Where(setter =>
setter.Property == Control.FontFamilyProperty || setter.Property == Control.FontFamilyProperty ||
setter.Property == Control.FontStyleProperty || setter.Property == Control.FontStyleProperty ||
setter.Property == Control.FontWeightProperty || setter.Property == Control.FontWeightProperty ||
@ -230,18 +227,18 @@ namespace Flow.Launcher.Core.Resource
{ {
var settersToRemove = style.Setters var settersToRemove = style.Setters
.OfType<Setter>() .OfType<Setter>()
.Where(setter => .Where(setter =>
setter.Property == TextBlock.FontFamilyProperty || setter.Property == TextBlock.FontFamilyProperty ||
setter.Property == TextBlock.FontStyleProperty || setter.Property == TextBlock.FontStyleProperty ||
setter.Property == TextBlock.FontWeightProperty || setter.Property == TextBlock.FontWeightProperty ||
setter.Property == TextBlock.FontStretchProperty) setter.Property == TextBlock.FontStretchProperty)
.ToList(); .ToList();
foreach (var setter in settersToRemove) foreach (var setter in settersToRemove)
{ {
style.Setters.Remove(setter); style.Setters.Remove(setter);
} }
style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily)); style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily));
style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle)); style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle));
style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight)); style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight));
@ -328,9 +325,9 @@ namespace Flow.Launcher.Core.Resource
return dict; return dict;
} }
private ResourceDictionary GetCurrentResourceDictionary() public ResourceDictionary GetCurrentResourceDictionary()
{ {
return GetResourceDictionary(GetCurrentTheme()); return GetResourceDictionary(_settings.Theme);
} }
private ThemeData GetThemeDataFromPath(string path) private ThemeData GetThemeDataFromPath(string path)
@ -383,9 +380,20 @@ namespace Flow.Launcher.Core.Resource
#endregion #endregion
#region Load & Change #region Get & Change Theme
public List<ThemeData> LoadAvailableThemes() public ThemeData GetCurrentTheme()
{
var themes = GetAvailableThemes();
var matchingTheme = themes.FirstOrDefault(t => t.FileNameWithoutExtension == _settings.Theme);
if (matchingTheme == null)
{
_api.LogWarn(ClassName, $"No matching theme found for '{_settings.Theme}'. Falling back to the first available theme.");
}
return matchingTheme ?? themes.FirstOrDefault();
}
public List<ThemeData> GetAvailableThemes()
{ {
List<ThemeData> themes = new List<ThemeData>(); List<ThemeData> themes = new List<ThemeData>();
foreach (var themeDirectory in _themeDirectories) foreach (var themeDirectory in _themeDirectories)
@ -403,7 +411,7 @@ namespace Flow.Launcher.Core.Resource
public bool ChangeTheme(string theme = null) public bool ChangeTheme(string theme = null)
{ {
if (string.IsNullOrEmpty(theme)) if (string.IsNullOrEmpty(theme))
theme = GetCurrentTheme(); theme = _settings.Theme;
string path = GetThemePath(theme); string path = GetThemePath(theme);
try try
@ -413,7 +421,7 @@ namespace Flow.Launcher.Core.Resource
// Retrieve theme resource always use the resource with font settings applied. // Retrieve theme resource always use the resource with font settings applied.
var resourceDict = GetResourceDictionary(theme); var resourceDict = GetResourceDictionary(theme);
UpdateResourceDictionary(resourceDict); UpdateResourceDictionary(resourceDict);
_settings.Theme = theme; _settings.Theme = theme;
@ -426,14 +434,14 @@ namespace Flow.Launcher.Core.Resource
BlurEnabled = IsBlurTheme(); BlurEnabled = IsBlurTheme();
// Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues // Apply blur and drop shadow effect so that we do not need to call it again
_ = RefreshFrameAsync(); _ = RefreshFrameAsync();
return true; return true;
} }
catch (DirectoryNotFoundException) 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) if (theme != Constant.DefaultTheme)
{ {
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme)); _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme));
@ -443,7 +451,7 @@ namespace Flow.Launcher.Core.Resource
} }
catch (XamlParseException) catch (XamlParseException)
{ {
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); _api.LogError(ClassName, $"Theme <{theme}> fail to parse");
if (theme != Constant.DefaultTheme) if (theme != Constant.DefaultTheme)
{ {
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme)); _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));
@ -591,7 +599,7 @@ namespace Flow.Launcher.Core.Resource
{ {
AutoDropShadow(useDropShadowEffect); AutoDropShadow(useDropShadowEffect);
} }
SetBlurForWindow(GetCurrentTheme(), backdropType); SetBlurForWindow(_settings.Theme, backdropType);
if (!BlurEnabled) if (!BlurEnabled)
{ {
@ -610,7 +618,7 @@ namespace Flow.Launcher.Core.Resource
// Get the actual backdrop type and drop shadow effect settings // Get the actual backdrop type and drop shadow effect settings
var (backdropType, _) = GetActualValue(); var (backdropType, _) = GetActualValue();
SetBlurForWindow(GetCurrentTheme(), backdropType); SetBlurForWindow(_settings.Theme, backdropType);
}, DispatcherPriority.Render); }, DispatcherPriority.Render);
} }
@ -764,22 +772,18 @@ namespace Flow.Launcher.Core.Resource
{ {
if (bgColor == null) return; if (bgColor == null) return;
// Copy the existing WindowBorderStyle // Create a new Style for the preview
var previewStyle = new Style(typeof(Border)); var previewStyle = new Style(typeof(Border));
if (Application.Current.Resources.Contains("WindowBorderStyle"))
// Get the original WindowBorderStyle
if (Application.Current.Resources.Contains("WindowBorderStyle") &&
Application.Current.Resources["WindowBorderStyle"] is Style originalStyle)
{ {
if (Application.Current.Resources["WindowBorderStyle"] is Style originalStyle) // Copy the original style, including the base style if it exists
{ CopyStyle(originalStyle, previewStyle);
foreach (var setter in originalStyle.Setters.OfType<Setter>())
{
previewStyle.Setters.Add(new Setter(setter.Property, setter.Value));
}
}
} }
// Apply background color (remove transparency in color) // Apply background color (remove transparency in color)
// WPF does not allow the use of an acrylic brush within the window's internal area,
// so transparency effects are not applied to the preview.
Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B); Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B);
previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor))); previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor)));
@ -790,9 +794,26 @@ namespace Flow.Launcher.Core.Resource
previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5))); previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5)));
previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1))); previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1)));
} }
// Set the new style to the resource
Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle; Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle;
} }
private void CopyStyle(Style originalStyle, Style targetStyle)
{
// If the style is based on another style, copy the base style first
if (originalStyle.BasedOn != null)
{
CopyStyle(originalStyle.BasedOn, targetStyle);
}
// Copy the setters from the original style
foreach (var setter in originalStyle.Setters.OfType<Setter>())
{
targetStyle.Setters.Add(new Setter(setter.Property, setter.Value));
}
}
private void ColorizeWindow(string theme, BackdropTypes backdropType) private void ColorizeWindow(string theme, BackdropTypes backdropType)
{ {
var dict = GetThemeResourceDictionary(theme); var dict = GetThemeResourceDictionary(theme);
@ -898,11 +919,5 @@ namespace Flow.Launcher.Core.Resource
} }
#endregion #endregion
#region Classes
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
#endregion
} }
} }

View file

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

View file

@ -0,0 +1,19 @@
namespace Flow.Launcher.Core.Storage;
/// <summary>
/// Remove storage instances from <see cref="Launcher.Plugin.IPublicAPI"/> instance
/// </summary>
public interface IRemovable
{
/// <summary>
/// Remove all <see cref="Infrastructure.Storage.PluginJsonStorage{T}"/> instances of one plugin
/// </summary>
/// <param name="assemblyName"></param>
public void RemovePluginSettings(string assemblyName);
/// <summary>
/// Remove all <see cref="Infrastructure.Storage.PluginBinaryStorage{T}"/> instances of one plugin
/// </summary>
/// <param name="cacheDirectory"></param>
public void RemovePluginCaches(string cacheDirectory);
}

View file

@ -4,19 +4,18 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using JetBrains.Annotations;
using Squirrel;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using System.Text.Json.Serialization; using JetBrains.Annotations;
using System.Threading; using Squirrel;
namespace Flow.Launcher.Core namespace Flow.Launcher.Core
{ {
@ -24,6 +23,8 @@ namespace Flow.Launcher.Core
{ {
public string GitHubRepository { get; init; } public string GitHubRepository { get; init; }
private static readonly string ClassName = nameof(Updater);
private readonly IPublicAPI _api; private readonly IPublicAPI _api;
public Updater(IPublicAPI publicAPI, string gitHubRepository) public Updater(IPublicAPI publicAPI, string gitHubRepository)
@ -51,7 +52,7 @@ namespace Flow.Launcher.Core
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = Version.Parse(Constant.Version); var currentVersion = Version.Parse(Constant.Version);
Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>"); _api.LogInfo(ClassName, $"Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
if (newReleaseVersion <= currentVersion) if (newReleaseVersion <= currentVersion)
{ {
@ -70,7 +71,7 @@ namespace Flow.Launcher.Core
if (DataLocation.PortableDataLocationInUse()) if (DataLocation.PortableDataLocationInUse())
{ {
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)); FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s))) if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), _api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
@ -84,7 +85,7 @@ namespace Flow.Launcher.Core
var newVersionTips = NewVersionTips(newReleaseVersion.ToString()); 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) 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) catch (Exception e)
{ {
if ((e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)) 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); {
_api.LogException(ClassName, $"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
}
else else
Log.Exception($"|Updater.UpdateApp|Error Occurred", e); {
_api.LogException(ClassName, $"Error Occurred", e);
}
if (!silentUpdate) if (!silentUpdate)
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"), _api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
@ -122,14 +127,14 @@ namespace Flow.Launcher.Core
} }
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs // https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository) private static async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
{ {
var uri = new Uri(repository); var uri = new Uri(repository);
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
await using var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false); await using var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false);
var releases = await System.Text.Json.JsonSerializer.DeserializeAsync<List<GithubRelease>>(jsonStream).ConfigureAwait(false); var releases = await JsonSerializer.DeserializeAsync<List<GithubRelease>>(jsonStream).ConfigureAwait(false);
var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First(); var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First();
var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/"); var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/");
@ -144,12 +149,21 @@ namespace Flow.Launcher.Core
return manager; return manager;
} }
public string NewVersionTips(string version) private string NewVersionTips(string version)
{ {
var translator = InternationalizationManager.Instance; var tips = string.Format(_api.GetTranslation("newVersionTips"), version);
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
return tips; return tips;
} }
private static string Formatted<T>(T t)
{
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
{
WriteIndented = true
});
return formatted;
}
} }
} }

View file

@ -48,6 +48,7 @@ namespace Flow.Launcher.Infrastructure
public const string Themes = "Themes"; public const string Themes = "Themes";
public const string Settings = "Settings"; public const string Settings = "Settings";
public const string Logs = "Logs"; public const string Logs = "Logs";
public const string Cache = "Cache";
public const string Website = "https://flowlauncher.com"; public const string Website = "https://flowlauncher.com";
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher"; public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";

View file

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

View file

@ -1,19 +1,11 @@
#nullable enable #nullable enable
using System; using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Infrastructure namespace Flow.Launcher.Infrastructure
{ {
public static class Helper public static class Helper
{ {
static Helper()
{
jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter());
}
/// <summary> /// <summary>
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy /// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
/// </summary> /// </summary>
@ -36,55 +28,5 @@ namespace Flow.Launcher.Infrastructure
throw new NullReferenceException(); throw new NullReferenceException();
} }
} }
public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
{
if (!Directory.Exists(dataDirectory))
{
Directory.CreateDirectory(dataDirectory);
}
foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
{
var data = Path.GetFileName(bundledDataPath);
var dataPath = Path.Combine(dataDirectory, data.NonNull());
if (!File.Exists(dataPath))
{
File.Copy(bundledDataPath, dataPath);
}
else
{
var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
if (time1 != time2)
{
File.Copy(bundledDataPath, dataPath, true);
}
}
}
}
public static void ValidateDirectory(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public static string Formatted<T>(this T t)
{
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
{
WriteIndented = true
});
return formatted;
}
} }
} }

View file

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

View file

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

View file

@ -5,17 +5,19 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Documents;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.Storage;
using static Flow.Launcher.Infrastructure.Http.Http; using SharpVectors.Converters;
using SharpVectors.Renderers.Wpf;
namespace Flow.Launcher.Infrastructure.Image namespace Flow.Launcher.Infrastructure.Image
{ {
public static class ImageLoader public static class ImageLoader
{ {
private static readonly string ClassName = nameof(ImageLoader);
private static readonly ImageCache ImageCache = new(); private static readonly ImageCache ImageCache = new();
private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1); private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
private static BinaryStorage<List<(string, bool)>> _storage; private static BinaryStorage<List<(string, bool)>> _storage;
@ -27,9 +29,10 @@ namespace Flow.Launcher.Infrastructure.Image
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon)); public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
public const int SmallIconSize = 64; public const int SmallIconSize = 64;
public const int FullIconSize = 256; public const int FullIconSize = 256;
public const int FullImageSize = 320;
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" }; private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
private static readonly string SvgExtension = ".svg";
public static async Task InitializeAsync() public static async Task InitializeAsync()
{ {
@ -37,6 +40,7 @@ namespace Flow.Launcher.Infrastructure.Image
_hashGenerator = new ImageHashGenerator(); _hashGenerator = new ImageHashGenerator();
var usage = await LoadStorageToConcurrentDictionaryAsync(); var usage = await LoadStorageToConcurrentDictionaryAsync();
_storage.ClearData();
ImageCache.Initialize(usage); ImageCache.Initialize(usage);
@ -49,19 +53,18 @@ namespace Flow.Launcher.Infrastructure.Image
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () => await Stopwatch.InfoAsync(ClassName, "Preload images cost", async () =>
{ {
foreach (var (path, isFullImage) in usage) foreach (var (path, isFullImage) in usage)
{ {
await LoadAsync(path, isFullImage); await LoadAsync(path, isFullImage);
} }
}); });
Log.Info( Log.Info(ClassName, $"Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
$"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
}); });
} }
public static async Task Save() public static async Task SaveAsync()
{ {
await storageLock.WaitAsync(); await storageLock.WaitAsync();
@ -71,12 +74,22 @@ namespace Flow.Launcher.Infrastructure.Image
.Select(x => x.Key) .Select(x => x.Key)
.ToList()); .ToList());
} }
catch (System.Exception e)
{
Log.Exception(ClassName, "Failed to save image cache to file", e);
}
finally finally
{ {
storageLock.Release(); storageLock.Release();
} }
} }
public static async Task WaitSaveAsync()
{
await storageLock.WaitAsync();
storageLock.Release();
}
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync() private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
{ {
await storageLock.WaitAsync(); await storageLock.WaitAsync();
@ -158,8 +171,8 @@ namespace Flow.Launcher.Infrastructure.Image
} }
catch (System.Exception e2) catch (System.Exception e2)
{ {
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e); Log.Exception(ClassName, $"Failed to get thumbnail for {path} on first try", e);
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2); Log.Exception(ClassName, $"Failed to get thumbnail for {path} on second try", e2);
ImageSource image = ImageCache[Constant.MissingImgIcon, false]; ImageSource image = ImageCache[Constant.MissingImgIcon, false];
ImageCache[path, false] = image; ImageCache[path, false] = image;
@ -173,7 +186,7 @@ namespace Flow.Launcher.Infrastructure.Image
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult) private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
{ {
// Download image from url // Download image from url
await using var resp = await GetStreamAsync(uriResult); await using var resp = await Http.Http.GetStreamAsync(uriResult);
await using var buffer = new MemoryStream(); await using var buffer = new MemoryStream();
await resp.CopyToAsync(buffer); await resp.CopyToAsync(buffer);
buffer.Seek(0, SeekOrigin.Begin); buffer.Seek(0, SeekOrigin.Begin);
@ -221,10 +234,11 @@ namespace Flow.Launcher.Infrastructure.Image
image = LoadFullImage(path); image = LoadFullImage(path);
type = ImageType.FullImageFile; type = ImageType.FullImageFile;
} }
catch (NotSupportedException) catch (NotSupportedException ex)
{ {
image = Image; image = Image;
type = ImageType.Error; type = ImageType.Error;
Log.Exception(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex);
} }
} }
else else
@ -237,6 +251,20 @@ namespace Flow.Launcher.Infrastructure.Image
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly); image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
} }
} }
else if (extension == SvgExtension)
{
try
{
image = LoadSvgImage(path, loadFullImage);
type = ImageType.FullImageFile;
}
catch (System.Exception ex)
{
image = Image;
type = ImageType.Error;
Log.Exception(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex);
}
}
else else
{ {
type = ImageType.File; type = ImageType.File;
@ -277,7 +305,7 @@ namespace Flow.Launcher.Infrastructure.Image
return ImageCache.TryGetValue(path, loadFullImage, out image); return ImageCache.TryGetValue(path, loadFullImage, out image);
} }
public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false) public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false, bool cacheImage = true)
{ {
var imageResult = await LoadInternalAsync(path, loadFullImage); var imageResult = await LoadInternalAsync(path, loadFullImage);
@ -293,22 +321,24 @@ namespace Flow.Launcher.Infrastructure.Image
// image already exists // image already exists
img = ImageCache[key, loadFullImage] ?? img; img = ImageCache[key, loadFullImage] ?? img;
} }
else else if (cacheImage)
{ {
// new guid // save guid key
GuidToKey[hash] = path; GuidToKey[hash] = path;
} }
} }
// update cache if (cacheImage)
ImageCache[path, loadFullImage] = img; {
// update cache
ImageCache[path, loadFullImage] = img;
}
} }
return img; return img;
} }
private static BitmapImage LoadFullImage(string path) private static ImageSource LoadFullImage(string path)
{ {
BitmapImage image = new BitmapImage(); BitmapImage image = new BitmapImage();
image.BeginInit(); image.BeginInit();
@ -317,24 +347,24 @@ namespace Flow.Launcher.Infrastructure.Image
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
image.EndInit(); image.EndInit();
if (image.PixelWidth > 320) if (image.PixelWidth > FullImageSize)
{ {
BitmapImage resizedWidth = new BitmapImage(); BitmapImage resizedWidth = new BitmapImage();
resizedWidth.BeginInit(); resizedWidth.BeginInit();
resizedWidth.CacheOption = BitmapCacheOption.OnLoad; resizedWidth.CacheOption = BitmapCacheOption.OnLoad;
resizedWidth.UriSource = new Uri(path); resizedWidth.UriSource = new Uri(path);
resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
resizedWidth.DecodePixelWidth = 320; resizedWidth.DecodePixelWidth = FullImageSize;
resizedWidth.EndInit(); resizedWidth.EndInit();
if (resizedWidth.PixelHeight > 320) if (resizedWidth.PixelHeight > FullImageSize)
{ {
BitmapImage resizedHeight = new BitmapImage(); BitmapImage resizedHeight = new BitmapImage();
resizedHeight.BeginInit(); resizedHeight.BeginInit();
resizedHeight.CacheOption = BitmapCacheOption.OnLoad; resizedHeight.CacheOption = BitmapCacheOption.OnLoad;
resizedHeight.UriSource = new Uri(path); resizedHeight.UriSource = new Uri(path);
resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
resizedHeight.DecodePixelHeight = 320; resizedHeight.DecodePixelHeight = FullImageSize;
resizedHeight.EndInit(); resizedHeight.EndInit();
return resizedHeight; return resizedHeight;
} }
@ -344,5 +374,50 @@ namespace Flow.Launcher.Infrastructure.Image
return image; return image;
} }
private static ImageSource LoadSvgImage(string path, bool loadFullImage = false)
{
// Set up drawing settings
var desiredHeight = loadFullImage ? FullImageSize : SmallIconSize;
var drawingSettings = new WpfDrawingSettings
{
IncludeRuntime = true,
// Set IgnoreRootViewbox to false to respect the SVG's viewBox
IgnoreRootViewbox = false
};
// Load and render the SVG
var converter = new FileSvgReader(drawingSettings);
var drawing = converter.Read(new Uri(path));
// Calculate scale to achieve desired height
var drawingBounds = drawing.Bounds;
if (drawingBounds.Height <= 0)
{
throw new InvalidOperationException($"Invalid SVG dimensions: Height must be greater than zero in {path}");
}
var scale = desiredHeight / drawingBounds.Height;
var scaledWidth = drawingBounds.Width * scale;
var scaledHeight = drawingBounds.Height * scale;
// Convert the Drawing to a Bitmap
var drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.PushTransform(new ScaleTransform(scale, scale));
drawingContext.DrawDrawing(drawing);
}
// Create a RenderTargetBitmap to hold the rendered image
var bitmap = new RenderTargetBitmap(
(int)Math.Ceiling(scaledWidth),
(int)Math.Ceiling(scaledHeight),
96, // DpiX
96, // DpiY
PixelFormats.Pbgra32);
bitmap.Render(drawingVisual);
return bitmap;
}
} }
} }

View file

@ -12,7 +12,7 @@ using Windows.Win32.Graphics.Gdi;
namespace Flow.Launcher.Infrastructure.Image namespace Flow.Launcher.Infrastructure.Image
{ {
/// <summary> /// <summary>
/// Subclass of <see cref="Windows.Win32.UI.Shell.SIIGBF"/> /// Subclass of <see cref="SIIGBF"/>
/// </summary> /// </summary>
[Flags] [Flags]
public enum ThumbnailOptions public enum ThumbnailOptions
@ -31,7 +31,9 @@ namespace Flow.Launcher.Infrastructure.Image
private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID; 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) 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); 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); imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
} }
catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly) catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly)
@ -89,6 +92,11 @@ namespace Flow.Launcher.Infrastructure.Image
// Fallback to IconOnly if files cannot be found // Fallback to IconOnly if files cannot be found
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap); imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
} }
catch (System.Exception ex)
{
// Handle other exceptions
throw new InvalidOperationException("Failed to get thumbnail", ex);
}
} }
finally finally
{ {

View file

@ -1,24 +1,24 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using Flow.Launcher.Infrastructure.UserSettings;
using NLog; using NLog;
using NLog.Config; using NLog.Config;
using NLog.Targets; using NLog.Targets;
using Flow.Launcher.Infrastructure.UserSettings;
using NLog.Targets.Wrappers; using NLog.Targets.Wrappers;
using System.Runtime.ExceptionServices;
namespace Flow.Launcher.Infrastructure.Logger namespace Flow.Launcher.Infrastructure.Logger
{ {
public static class Log public static class Log
{ {
public const string DirectoryName = "Logs"; public const string DirectoryName = Constant.Logs;
public static string CurrentLogDirectory { get; } public static string CurrentLogDirectory { get; }
static Log() static Log()
{ {
CurrentLogDirectory = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version); CurrentLogDirectory = DataLocation.VersionLogDirectory;
if (!Directory.Exists(CurrentLogDirectory)) if (!Directory.Exists(CurrentLogDirectory))
{ {
Directory.CreateDirectory(CurrentLogDirectory); Directory.CreateDirectory(CurrentLogDirectory);
@ -94,13 +94,6 @@ namespace Flow.Launcher.Infrastructure.Logger
logger.Fatal(message); 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 = "") public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
{ {
exception = exception.Demystify(); exception = exception.Demystify();
@ -135,57 +128,14 @@ namespace Flow.Launcher.Infrastructure.Logger
return className; return className;
} }
#if !DEBUG
private static void ExceptionInternal(string classAndMethod, string message, System.Exception e) private static void ExceptionInternal(string classAndMethod, string message, System.Exception e)
{ {
var logger = LogManager.GetLogger(classAndMethod); var logger = LogManager.GetLogger(classAndMethod);
logger.Error(e, message); 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 #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 = "") 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); 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 = "") public static void Info(string className, string message, [CallerMemberName] string methodName = "")
{ {
LogInternal(LogLevel.Info, className, message, 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 = "") public static void Warn(string className, string message, [CallerMemberName] string methodName = "")
{ {
LogInternal(LogLevel.Warn, className, message, methodName); LogInternal(LogLevel.Warn, className, message, methodName);
} }
/// Example: "|ClassName.MethodName|Message"
public static void Warn(string message)
{
LogInternal(message, LogLevel.Warn);
}
} }
public enum LOGLEVEL public enum LOGLEVEL

View file

@ -11,11 +11,6 @@ GetModuleHandle
GetKeyState GetKeyState
VIRTUAL_KEY VIRTUAL_KEY
WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
EnumWindows EnumWindows
DwmSetWindowAttribute DwmSetWindowAttribute
@ -48,6 +43,9 @@ MONITORINFOEXW
WM_ENTERSIZEMOVE WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE WM_EXITSIZEMOVE
OleInitialize
OleUninitialize
GetKeyboardLayout GetKeyboardLayout
GetWindowThreadProcessId GetWindowThreadProcessId
ActivateKeyboardLayout ActivateKeyboardLayout
@ -58,4 +56,4 @@ INPUTLANGCHANGE_FORWARD
LOCALE_TRANSIENT_KEYBOARD1 LOCALE_TRANSIENT_KEYBOARD1
LOCALE_TRANSIENT_KEYBOARD2 LOCALE_TRANSIENT_KEYBOARD2
LOCALE_TRANSIENT_KEYBOARD3 LOCALE_TRANSIENT_KEYBOARD3
LOCALE_TRANSIENT_KEYBOARD4 LOCALE_TRANSIENT_KEYBOARD4

View file

@ -1,5 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
@ -7,91 +7,54 @@ namespace Flow.Launcher.Infrastructure
{ {
public static class Stopwatch public static class Stopwatch
{ {
private static readonly Dictionary<string, long> Count = new Dictionary<string, long>();
private static readonly object Locker = new object();
/// <summary> /// <summary>
/// This stopwatch will appear only in Debug mode /// This stopwatch will appear only in Debug mode
/// </summary> /// </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(); var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start(); stopWatch.Start();
action(); action();
stopWatch.Stop(); stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds; var milliseconds = stopWatch.ElapsedMilliseconds;
string info = $"{message} <{milliseconds}ms>"; Log.Debug(className, $"{message} <{milliseconds}ms>", methodName);
Log.Debug(info);
return milliseconds; return milliseconds;
} }
/// <summary> /// <summary>
/// This stopwatch will appear only in Debug mode /// This stopwatch will appear only in Debug mode
/// </summary> /// </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(); var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start(); stopWatch.Start();
await action(); await action();
stopWatch.Stop(); stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds; var milliseconds = stopWatch.ElapsedMilliseconds;
string info = $"{message} <{milliseconds}ms>"; Log.Debug(className, $"{message} <{milliseconds}ms>", methodName);
Log.Debug(info);
return milliseconds; 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(); var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start(); stopWatch.Start();
action(); action();
stopWatch.Stop(); stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds; var milliseconds = stopWatch.ElapsedMilliseconds;
string info = $"{message} <{milliseconds}ms>"; Log.Info(className, $"{message} <{milliseconds}ms>", methodName);
Log.Info(info);
return milliseconds; 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(); var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start(); stopWatch.Start();
await action(); await action();
stopWatch.Stop(); stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds; var milliseconds = stopWatch.ElapsedMilliseconds;
string info = $"{message} <{milliseconds}ms>"; Log.Info(className, $"{message} <{milliseconds}ms>", methodName);
Log.Info(info);
return milliseconds; return milliseconds;
} }
public static void StartCount(string name, Action action)
{
var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start();
action();
stopWatch.Stop();
var milliseconds = stopWatch.ElapsedMilliseconds;
lock (Locker)
{
if (Count.ContainsKey(name))
{
Count[name] += milliseconds;
}
else
{
Count[name] = 0;
}
}
}
public static void EndCount()
{
foreach (var key in Count.Keys)
{
string info = $"{key} already cost {Count[key]}ms";
Log.Debug(info);
}
}
} }
} }

View file

@ -1,14 +1,14 @@
using System; using System;
using System.IO; using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using MemoryPack; using MemoryPack;
#nullable enable
namespace Flow.Launcher.Infrastructure.Storage namespace Flow.Launcher.Infrastructure.Storage
{ {
/// <summary> /// <summary>
@ -16,64 +16,111 @@ namespace Flow.Launcher.Infrastructure.Storage
/// Normally, it has better performance, but not readable /// Normally, it has better performance, but not readable
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable /// It utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
/// https://github.com/Cysharp/MemoryPack
/// </remarks> /// </remarks>
public class BinaryStorage<T> public class BinaryStorage<T> : ISavable
{ {
const string DirectoryName = "Cache"; private static readonly string ClassName = "BinaryStorage";
const string FileSuffix = ".cache"; protected T? Data;
public const string FileSuffix = ".cache";
protected string FilePath { get; init; } = null!;
protected string DirectoryPath { get; init; } = null!;
// Let the derived class to set the file path
protected BinaryStorage()
{
}
public BinaryStorage(string filename) public BinaryStorage(string filename)
{ {
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); DirectoryPath = DataLocation.CacheDirectory;
Helper.ValidateDirectory(directoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
} }
public string FilePath { get; } // 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) public async ValueTask<T> TryLoadAsync(T defaultData)
{ {
if (Data != null) return Data;
if (File.Exists(FilePath)) if (File.Exists(FilePath))
{ {
if (new FileInfo(FilePath).Length == 0) if (new FileInfo(FilePath).Length == 0)
{ {
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>"); Log.Error(ClassName, $"Zero length cache file <{FilePath}>");
await SaveAsync(defaultData); Data = defaultData;
return defaultData; await SaveAsync();
} }
await using var stream = new FileStream(FilePath, FileMode.Open); await using var stream = new FileStream(FilePath, FileMode.Open);
var d = await DeserializeAsync(stream, defaultData); Data = await DeserializeAsync(stream, defaultData);
return d;
} }
else else
{ {
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data"); Log.Info(ClassName, "Cache file not exist, load default data");
await SaveAsync(defaultData); Data = defaultData;
return defaultData; await SaveAsync();
} }
return Data;
} }
private async ValueTask<T> DeserializeAsync(Stream stream, T defaultData) private static async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
{ {
try try
{ {
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream); var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
return t; return t ?? defaultData;
} }
catch (System.Exception e) catch (System.Exception)
{ {
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); // Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
return defaultData; return defaultData;
} }
} }
public void Save()
{
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
var serialized = MemoryPackSerializer.Serialize(Data);
File.WriteAllBytes(FilePath, serialized);
}
public async ValueTask SaveAsync()
{
await SaveAsync(Data.NonNull());
}
// ImageCache need to convert data into concurrent dictionary for usage,
// so we would better to clear the data
public void ClearData()
{
Data = default;
}
// ImageCache storages data in its class,
// so we need to pass it to SaveAsync
public async ValueTask SaveAsync(T data) 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 using var stream = new FileStream(FilePath, FileMode.Create);
await MemoryPackSerializer.SerializeAsync(stream, data); await MemoryPackSerializer.SerializeAsync(stream, data);
} }

View file

@ -1,17 +1,46 @@
using System.IO; using System.IO;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage namespace Flow.Launcher.Infrastructure.Storage
{ {
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new() public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
{ {
private static readonly string ClassName = "FlowLauncherJsonStorage";
public FlowLauncherJsonStorage() public FlowLauncherJsonStorage()
{ {
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
Helper.ValidateDirectory(directoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
var filename = typeof(T).Name; var filename = typeof(T).Name;
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
public new void Save()
{
try
{
base.Save();
}
catch (System.Exception e)
{
Log.Exception(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
}
}
public new async Task SaveAsync()
{
try
{
await base.SaveAsync();
}
catch (System.Exception e)
{
Log.Exception(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
}
} }
} }
} }

View file

@ -1,22 +1,27 @@
#nullable enable using System;
using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
#nullable enable
namespace Flow.Launcher.Infrastructure.Storage namespace Flow.Launcher.Infrastructure.Storage
{ {
/// <summary> /// <summary>
/// Serialize object using json format. /// Serialize object using json format.
/// </summary> /// </summary>
public class JsonStorage<T> where T : new() public class JsonStorage<T> : ISavable where T : new()
{ {
private static readonly string ClassName = "JsonStorage";
protected T? Data; protected T? Data;
// need a new directory name // need a new directory name
public const string DirectoryName = "Settings"; public const string DirectoryName = Constant.Settings;
public const string FileSuffix = ".json"; public const string FileSuffix = ".json";
protected string FilePath { get; init; } = null!; protected string FilePath { get; init; } = null!;
@ -37,7 +42,7 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = filePath; FilePath = filePath;
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path"); DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
Helper.ValidateDirectory(DirectoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
} }
public async Task<T> LoadAsync() public async Task<T> LoadAsync()
@ -101,7 +106,7 @@ namespace Flow.Launcher.Infrastructure.Storage
private void RestoreBackup() 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)) if (File.Exists(FilePath))
File.Replace(BackupFilePath, FilePath, null); File.Replace(BackupFilePath, FilePath, null);
@ -180,7 +185,10 @@ namespace Flow.Launcher.Infrastructure.Storage
public void Save() 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 }); new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(TempFilePath, serialized); File.WriteAllText(TempFilePath, serialized);
@ -190,6 +198,9 @@ namespace Flow.Launcher.Infrastructure.Storage
public async Task SaveAsync() 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 using var tempOutput = File.OpenWrite(TempFilePath);
await JsonSerializer.SerializeAsync(tempOutput, Data, await JsonSerializer.SerializeAsync(tempOutput, Data,
new JsonSerializerOptions { WriteIndented = true }); new JsonSerializerOptions { WriteIndented = true });

View file

@ -0,0 +1,44 @@
using System.IO;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
{
public class PluginBinaryStorage<T> : BinaryStorage<T> where T : new()
{
private static readonly string ClassName = "PluginBinaryStorage";
public PluginBinaryStorage(string cacheName, string cacheDirectory)
{
DirectoryPath = cacheDirectory;
FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{cacheName}{FileSuffix}");
}
public new void Save()
{
try
{
base.Save();
}
catch (System.Exception e)
{
Log.Exception(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
}
}
public new async Task SaveAsync()
{
try
{
await base.SaveAsync();
}
catch (System.Exception e)
{
Log.Exception(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
}
}
}
}

View file

@ -1,5 +1,8 @@
using System.IO; using System.IO;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage namespace Flow.Launcher.Infrastructure.Storage
{ {
@ -8,13 +11,15 @@ namespace Flow.Launcher.Infrastructure.Storage
// Use assembly name to check which plugin is using this storage // Use assembly name to check which plugin is using this storage
public readonly string AssemblyName; public readonly string AssemblyName;
private static readonly string ClassName = "PluginJsonStorage";
public PluginJsonStorage() public PluginJsonStorage()
{ {
// C# related, add python related below // C# related, add python related below
var dataType = typeof(T); var dataType = typeof(T);
AssemblyName = dataType.Assembly.GetName().Name; AssemblyName = dataType.Assembly.GetName().Name;
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName); DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName);
Helper.ValidateDirectory(DirectoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
} }
@ -24,11 +29,27 @@ namespace Flow.Launcher.Infrastructure.Storage
Data = data; Data = data;
} }
public void DeleteDirectory() public new void Save()
{ {
if (Directory.Exists(DirectoryPath)) try
{ {
Directory.Delete(DirectoryPath, true); base.Save();
}
catch (System.Exception e)
{
Log.Exception(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
}
}
public new async Task SaveAsync()
{
try
{
await base.SaveAsync();
}
catch (System.Exception e)
{
Log.Exception(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
} }
} }
} }

View file

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

View file

@ -1,15 +1,15 @@
using System; using System;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Flow.Launcher.Infrastructure.UserSettings namespace Flow.Launcher.Infrastructure.UserSettings
{ {
#region Base
public abstract class ShortcutBaseModel public abstract class ShortcutBaseModel
{ {
public string Key { get; set; } public string Key { get; set; }
[JsonIgnore]
public Func<string> Expand { get; set; } = () => { return ""; };
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
return obj is ShortcutBaseModel other && return obj is ShortcutBaseModel other &&
@ -22,16 +22,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings
} }
} }
public class CustomShortcutModel : ShortcutBaseModel public class BaseCustomShortcutModel : ShortcutBaseModel
{ {
public string Value { get; set; } public string Value { get; set; }
[JsonConstructorAttribute] public BaseCustomShortcutModel(string key, string value)
public CustomShortcutModel(string key, string value)
{ {
Key = key; Key = key;
Value = value; Value = value;
Expand = () => { return Value; };
} }
public void Deconstruct(out string key, out string value) public void Deconstruct(out string key, out string value)
@ -40,26 +38,69 @@ namespace Flow.Launcher.Infrastructure.UserSettings
value = Value; value = Value;
} }
public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut) public static implicit operator (string Key, string Value)(BaseCustomShortcutModel shortcut)
{ {
return (shortcut.Key, shortcut.Value); return (shortcut.Key, shortcut.Value);
} }
public static implicit operator CustomShortcutModel((string Key, string Value) shortcut) public static implicit operator BaseCustomShortcutModel((string Key, string Value) shortcut)
{ {
return new CustomShortcutModel(shortcut.Key, shortcut.Value); return new BaseCustomShortcutModel(shortcut.Key, shortcut.Value);
} }
} }
public class BuiltinShortcutModel : ShortcutBaseModel public class BaseBuiltinShortcutModel : ShortcutBaseModel
{ {
public string Description { get; set; } public string Description { get; set; }
public BuiltinShortcutModel(string key, string description, Func<string> expand) public BaseBuiltinShortcutModel(string key, string description)
{ {
Key = key; Key = key;
Description = description; Description = description;
Expand = expand ?? (() => { return ""; });
} }
} }
#endregion
#region Custom Shortcut
public class CustomShortcutModel : BaseCustomShortcutModel
{
[JsonIgnore]
public Func<string> Expand { get; set; } = () => { return string.Empty; };
[JsonConstructor]
public CustomShortcutModel(string key, string value) : base(key, value)
{
Expand = () => { return Value; };
}
}
#endregion
#region Builtin Shortcut
public class BuiltinShortcutModel : BaseBuiltinShortcutModel
{
[JsonIgnore]
public Func<string> Expand { get; set; } = () => { return string.Empty; };
public BuiltinShortcutModel(string key, string description, Func<string> expand) : base(key, description)
{
Expand = expand ?? (() => { return string.Empty; });
}
}
public class AsyncBuiltinShortcutModel : BaseBuiltinShortcutModel
{
[JsonIgnore]
public Func<Task<string>> ExpandAsync { get; set; } = () => { return Task.FromResult(string.Empty); };
public AsyncBuiltinShortcutModel(string key, string description, Func<Task<string>> expandAsync) : base(key, description)
{
ExpandAsync = expandAsync ?? (() => { return Task.FromResult(string.Empty); });
}
}
#endregion
} }

View file

@ -25,8 +25,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
return false; return false;
} }
public static string VersionLogDirectory => Path.Combine(LogDirectory, Constant.Version);
public static string LogDirectory => Path.Combine(DataDirectory(), Constant.Logs);
public static readonly string CacheDirectory = Path.Combine(DataDirectory(), Constant.Cache);
public static readonly string SettingsDirectory = Path.Combine(DataDirectory(), Constant.Settings);
public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins); public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins);
public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins); public static readonly string ThemesDirectory = Path.Combine(DataDirectory(), Constant.Themes);
public static readonly string PluginSettingsDirectory = Path.Combine(SettingsDirectory, Constant.Plugins);
public static readonly string PluginCacheDirectory = Path.Combine(DataDirectory(), Constant.Cache, Constant.Plugins);
public const string PythonEnvironmentName = "Python"; public const string PythonEnvironmentName = "Python";
public const string NodeEnvironmentName = "Node.js"; public const string NodeEnvironmentName = "Node.js";

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings namespace Flow.Launcher.Infrastructure.UserSettings
@ -6,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public class PluginsSettings : BaseModel public class PluginsSettings : BaseModel
{ {
private string pythonExecutablePath = string.Empty; private string pythonExecutablePath = string.Empty;
public string PythonExecutablePath { public string PythonExecutablePath
get { return pythonExecutablePath; } {
get => pythonExecutablePath;
set set
{ {
pythonExecutablePath = value; pythonExecutablePath = value;
@ -18,7 +20,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
private string nodeExecutablePath = string.Empty; private string nodeExecutablePath = string.Empty;
public string NodeExecutablePath public string NodeExecutablePath
{ {
get { return nodeExecutablePath; } get => nodeExecutablePath;
set set
{ {
nodeExecutablePath = value; nodeExecutablePath = value;
@ -26,19 +28,32 @@ namespace Flow.Launcher.Infrastructure.UserSettings
} }
} }
public Dictionary<string, Plugin> Plugins { get; set; } = new Dictionary<string, Plugin>(); /// <summary>
/// Only used for serialization
/// </summary>
public Dictionary<string, Plugin> Plugins { get; set; } = new();
/// <summary>
/// Update plugin settings with metadata.
/// FL will get default values from metadata first and then load settings to metadata
/// </summary>
/// <param name="metadatas">Parsed plugin metadatas</param>
public void UpdatePluginSettings(List<PluginMetadata> metadatas) public void UpdatePluginSettings(List<PluginMetadata> metadatas)
{ {
foreach (var metadata in metadatas) foreach (var metadata in metadatas)
{ {
if (Plugins.ContainsKey(metadata.ID)) if (Plugins.TryGetValue(metadata.ID, out var settings))
{ {
var settings = Plugins[metadata.ID]; // If settings exist, update settings & metadata value
// update settings values with metadata
if (string.IsNullOrEmpty(settings.Version)) if (string.IsNullOrEmpty(settings.Version))
{
settings.Version = metadata.Version; settings.Version = metadata.Version;
}
settings.DefaultActionKeywords = metadata.ActionKeywords; // metadata provides default values
settings.DefaultSearchDelayTime = metadata.SearchDelayTime; // metadata provides default values
// update metadata values with settings
if (settings.ActionKeywords?.Count > 0) if (settings.ActionKeywords?.Count > 0)
{ {
metadata.ActionKeywords = settings.ActionKeywords; metadata.ActionKeywords = settings.ActionKeywords;
@ -51,30 +66,64 @@ namespace Flow.Launcher.Infrastructure.UserSettings
} }
metadata.Disabled = settings.Disabled; metadata.Disabled = settings.Disabled;
metadata.Priority = settings.Priority; metadata.Priority = settings.Priority;
metadata.SearchDelayTime = settings.SearchDelayTime;
} }
else else
{ {
// If settings does not exist, create a new one
Plugins[metadata.ID] = new Plugin Plugins[metadata.ID] = new Plugin
{ {
ID = metadata.ID, ID = metadata.ID,
Name = metadata.Name, Name = metadata.Name,
Version = metadata.Version, Version = metadata.Version,
ActionKeywords = metadata.ActionKeywords, DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values
ActionKeywords = metadata.ActionKeywords, // use default value
Disabled = metadata.Disabled, Disabled = metadata.Disabled,
Priority = metadata.Priority Priority = metadata.Priority,
DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values
SearchDelayTime = metadata.SearchDelayTime, // use default value
}; };
} }
} }
} }
public Plugin GetPluginSettings(string id)
{
if (Plugins.TryGetValue(id, out var plugin))
{
return plugin;
}
return null;
}
public Plugin RemovePluginSettings(string id)
{
Plugins.Remove(id, out var plugin);
return plugin;
}
} }
public class Plugin public class Plugin
{ {
public string ID { get; set; } public string ID { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Version { get; set; } public string Version { get; set; }
public List<string> ActionKeywords { get; set; } // a reference of the action keywords from plugin manager
[JsonIgnore]
public List<string> DefaultActionKeywords { get; set; }
// a reference of the action keywords from plugin manager
public List<string> ActionKeywords { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
[JsonIgnore]
public int? DefaultSearchDelayTime { get; set; }
public int? SearchDelayTime { get; set; }
/// <summary> /// <summary>
/// Used only to save the state of the plugin in settings /// Used only to save the state of the plugin in settings
/// </summary> /// </summary>

View file

@ -1,8 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Drawing;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Windows; using System.Windows;
using System.Windows.Media;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
@ -33,7 +33,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
_storage.Save(); _storage.Save();
} }
private string language = Constant.SystemLanguageCode;
private string _theme = Constant.DefaultTheme; private string _theme = Constant.DefaultTheme;
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}"; public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
public string OpenResultModifiers { get; set; } = KeyConstant.Alt; public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
@ -54,12 +53,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up";
public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down";
private string _language = Constant.SystemLanguageCode;
public string Language public string Language
{ {
get => language; get => _language;
set set
{ {
language = value; _language = value;
OnPropertyChanged(); OnPropertyChanged();
} }
} }
@ -82,18 +82,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings
/* Appearance Settings. It should be separated from the setting later.*/ /* Appearance Settings. It should be separated from the setting later.*/
public double WindowHeightSize { get; set; } = 42; public double WindowHeightSize { get; set; } = 42;
public double ItemHeightSize { get; set; } = 58; public double ItemHeightSize { get; set; } = 58;
public double QueryBoxFontSize { get; set; } = 20; public double QueryBoxFontSize { get; set; } = 16;
public double ResultItemFontSize { get; set; } = 16; public double ResultItemFontSize { get; set; } = 16;
public double ResultSubItemFontSize { get; set; } = 13; public double ResultSubItemFontSize { get; set; } = 13;
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name; public string QueryBoxFont { get; set; } = Win32Helper.GetSystemDefaultFont();
public string QueryBoxFontStyle { get; set; } public string QueryBoxFontStyle { get; set; }
public string QueryBoxFontWeight { get; set; } public string QueryBoxFontWeight { get; set; }
public string QueryBoxFontStretch { get; set; } public string QueryBoxFontStretch { get; set; }
public string ResultFont { get; set; } = FontFamily.GenericSansSerif.Name; public string ResultFont { get; set; } = Win32Helper.GetSystemDefaultFont();
public string ResultFontStyle { get; set; } public string ResultFontStyle { get; set; }
public string ResultFontWeight { get; set; } public string ResultFontWeight { get; set; }
public string ResultFontStretch { get; set; } public string ResultFontStretch { get; set; }
public string ResultSubFont { get; set; } = FontFamily.GenericSansSerif.Name; public string ResultSubFont { get; set; } = Win32Helper.GetSystemDefaultFont();
public string ResultSubFontStyle { get; set; } public string ResultSubFontStyle { get; set; }
public string ResultSubFontWeight { get; set; } public string ResultSubFontWeight { get; set; }
public string ResultSubFontStretch { get; set; } public string ResultSubFontStretch { get; set; }
@ -101,6 +101,24 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool UseAnimation { get; set; } = true; public bool UseAnimation { get; set; } = true;
public bool UseSound { get; set; } = true; public bool UseSound { get; set; } = true;
public double SoundVolume { get; set; } = 50; public double SoundVolume { get; set; } = 50;
public bool ShowBadges { get; set; } = false;
public bool ShowBadgesGlobalOnly { get; set; } = false;
private string _settingWindowFont { get; set; } = Win32Helper.GetSystemDefaultFont(false);
public string SettingWindowFont
{
get => _settingWindowFont;
set
{
if (_settingWindowFont != value)
{
_settingWindowFont = value;
OnPropertyChanged();
Application.Current.Resources["SettingWindowFont"] = new FontFamily(value);
Application.Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(value);
}
}
}
public bool UseClock { get; set; } = true; public bool UseClock { get; set; } = true;
public bool UseDate { get; set; } = false; public bool UseDate { get; set; } = false;
@ -112,9 +130,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public double SettingWindowHeight { get; set; } = 700; public double SettingWindowHeight { get; set; } = 700;
public double? SettingWindowTop { get; set; } = null; public double? SettingWindowTop { get; set; } = null;
public double? SettingWindowLeft { get; set; } = null; public double? SettingWindowLeft { get; set; } = null;
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal; public WindowState SettingWindowState { get; set; } = WindowState.Normal;
bool _showPlaceholder { get; set; } = false; private bool _showPlaceholder { get; set; } = true;
public bool ShowPlaceholder public bool ShowPlaceholder
{ {
get => _showPlaceholder; get => _showPlaceholder;
@ -127,7 +145,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
} }
} }
} }
string _placeholderText { get; set; } = string.Empty; private string _placeholderText { get; set; } = string.Empty;
public string PlaceholderText public string PlaceholderText
{ {
get => _placeholderText; get => _placeholderText;
@ -140,7 +158,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
} }
} }
} }
public int CustomExplorerIndex { get; set; } = 0; public int CustomExplorerIndex { get; set; } = 0;
[JsonIgnore] [JsonIgnore]
@ -295,9 +312,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>(); public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>();
[JsonIgnore] [JsonIgnore]
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new() public ObservableCollection<BaseBuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
{ {
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), new AsyncBuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText)),
new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
}; };
@ -307,10 +324,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool StartFlowLauncherOnSystemStartup { get; set; } = false; public bool StartFlowLauncherOnSystemStartup { get; set; } = false;
public bool UseLogonTaskForStartup { get; set; } = false; public bool UseLogonTaskForStartup { get; set; } = false;
public bool HideOnStartup { get; set; } = true; public bool HideOnStartup { get; set; } = true;
bool _hideNotifyIcon { get; set; } private bool _hideNotifyIcon;
public bool HideNotifyIcon public bool HideNotifyIcon
{ {
get { return _hideNotifyIcon; } get => _hideNotifyIcon;
set set
{ {
_hideNotifyIcon = value; _hideNotifyIcon = value;
@ -320,6 +337,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool LeaveCmdOpen { get; set; } public bool LeaveCmdOpen { get; set; }
public bool HideWhenDeactivated { get; set; } = true; public bool HideWhenDeactivated { get; set; } = true;
public bool SearchQueryResultsWithDelay { get; set; }
public int SearchDelayTime { get; set; } = 150;
[JsonConverter(typeof(JsonStringEnumConverter))] [JsonConverter(typeof(JsonStringEnumConverter))]
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor; public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
@ -342,7 +362,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
[JsonIgnore] [JsonIgnore]
public bool WMPInstalled { get; set; } = true; public bool WMPInstalled { get; set; } = true;
// This needs to be loaded last by staying at the bottom // This needs to be loaded last by staying at the bottom
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings(); public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();

View file

@ -1,9 +1,15 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Markup;
using System.Windows.Media; using System.Windows.Media;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Microsoft.Win32; using Microsoft.Win32;
@ -13,6 +19,7 @@ using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging; using Windows.Win32.UI.WindowsAndMessaging;
using Point = System.Windows.Point; using Point = System.Windows.Point;
using SystemFonts = System.Windows.SystemFonts;
namespace Flow.Launcher.Infrastructure namespace Flow.Launcher.Infrastructure
{ {
@ -124,6 +131,16 @@ namespace Flow.Launcher.Infrastructure
return PInvoke.SetForegroundWindow(new(handle)); return PInvoke.SetForegroundWindow(new(handle));
} }
public static bool IsForegroundWindow(Window window)
{
return IsForegroundWindow(GetWindowHandle(window));
}
internal static bool IsForegroundWindow(HWND handle)
{
return handle.Equals(PInvoke.GetForegroundWindow());
}
#endregion #endregion
#region Task Switching #region Task Switching
@ -322,6 +339,78 @@ namespace Flow.Launcher.Infrastructure
#endregion #endregion
#region STA Thread
/*
Inspired by https://github.com/files-community/Files code on STA Thread handling.
*/
public static Task StartSTATaskAsync(Action action)
{
var taskCompletionSource = new TaskCompletionSource();
Thread thread = new(() =>
{
PInvoke.OleInitialize();
try
{
action();
taskCompletionSource.SetResult();
}
catch (System.Exception ex)
{
taskCompletionSource.SetException(ex);
}
finally
{
PInvoke.OleUninitialize();
}
})
{
IsBackground = true,
Priority = ThreadPriority.Normal
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return taskCompletionSource.Task;
}
public static Task<T> StartSTATaskAsync<T>(Func<T> func)
{
var taskCompletionSource = new TaskCompletionSource<T>();
Thread thread = new(() =>
{
PInvoke.OleInitialize();
try
{
taskCompletionSource.SetResult(func());
}
catch (System.Exception ex)
{
taskCompletionSource.SetException(ex);
}
finally
{
PInvoke.OleUninitialize();
}
})
{
IsBackground = true,
Priority = ThreadPriority.Normal
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return taskCompletionSource.Task;
}
#endregion
#region Keyboard Layout #region Keyboard Layout
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; private const string UserProfileRegistryPath = @"Control Panel\International\User Profile";
@ -354,7 +443,7 @@ namespace Flow.Launcher.Infrastructure
// No installed English layout found // No installed English layout found
if (enHKL == HKL.Null) return; if (enHKL == HKL.Null) return;
// Get the current foreground window // Get the foreground window
var hwnd = PInvoke.GetForegroundWindow(); var hwnd = PInvoke.GetForegroundWindow();
if (hwnd == HWND.Null) return; if (hwnd == HWND.Null) return;
@ -367,12 +456,10 @@ namespace Flow.Launcher.Infrastructure
// the IME mode instead of switching to another layout. // the IME mode instead of switching to another layout.
var currentLayout = PInvoke.GetKeyboardLayout(threadId); var currentLayout = PInvoke.GetKeyboardLayout(threadId);
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord; var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
foreach (var langTag in ImeLanguageTags) foreach (var imeLangTag in ImeLanguageTags)
{ {
if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase)) var langTag = GetLanguageTag(currentLangId);
{ if (langTag.StartsWith(imeLangTag, StringComparison.OrdinalIgnoreCase)) return;
return;
}
} }
// Backup current keyboard layout // Backup current keyboard layout
@ -488,5 +575,183 @@ namespace Flow.Launcher.Infrastructure
} }
#endregion #endregion
#region Notification
public static bool IsNotificationSupported()
{
// Notifications only supported on Windows 10 19041+
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
Environment.OSVersion.Version.Build >= 19041;
}
#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
#region System Font
private static readonly Dictionary<string, string> _languageToNotoSans = new()
{
{ "ko", "Noto Sans KR" },
{ "ja", "Noto Sans JP" },
{ "zh-CN", "Noto Sans SC" },
{ "zh-SG", "Noto Sans SC" },
{ "zh-Hans", "Noto Sans SC" },
{ "zh-TW", "Noto Sans TC" },
{ "zh-HK", "Noto Sans TC" },
{ "zh-MO", "Noto Sans TC" },
{ "zh-Hant", "Noto Sans TC" },
{ "th", "Noto Sans Thai" },
{ "ar", "Noto Sans Arabic" },
{ "he", "Noto Sans Hebrew" },
{ "hi", "Noto Sans Devanagari" },
{ "bn", "Noto Sans Bengali" },
{ "ta", "Noto Sans Tamil" },
{ "el", "Noto Sans Greek" },
{ "ru", "Noto Sans" },
{ "en", "Noto Sans" },
{ "fr", "Noto Sans" },
{ "de", "Noto Sans" },
{ "es", "Noto Sans" },
{ "pt", "Noto Sans" }
};
/// <summary>
/// Gets the system default font.
/// </summary>
/// <param name="useNoto">
/// If true, it will try to find the Noto font for the current culture.
/// </param>
/// <returns>
/// The name of the system default font.
/// </returns>
public static string GetSystemDefaultFont(bool useNoto = true)
{
try
{
if (useNoto)
{
var culture = CultureInfo.CurrentCulture;
var language = culture.Name; // e.g., "zh-TW"
var langPrefix = language.Split('-')[0]; // e.g., "zh"
// First, try to find by full name, and if not found, fallback to prefix
if (TryGetNotoFont(language, out var notoFont) || TryGetNotoFont(langPrefix, out notoFont))
{
// If the font is installed, return it
if (Fonts.SystemFontFamilies.Any(f => f.Source.Equals(notoFont)))
{
return notoFont;
}
}
}
// If Noto font is not found, fallback to the system default font
var font = SystemFonts.MessageFontFamily;
if (font.FamilyNames.TryGetValue(XmlLanguage.GetLanguage("en-US"), out var englishName))
{
return englishName;
}
return font.Source ?? "Segoe UI";
}
catch
{
return "Segoe UI";
}
}
private static bool TryGetNotoFont(string langKey, out string notoFont)
{
return _languageToNotoSans.TryGetValue(langKey, out notoFont);
}
#endregion
} }
} }

View file

@ -51,6 +51,9 @@ namespace Flow.Launcher.Plugin
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None); (WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
} }
/// <summary>
/// Default <see cref="SpecialKeyState"/> object with all keys not pressed.
/// </summary>
public static readonly SpecialKeyState Default = new () { public static readonly SpecialKeyState Default = new () {
CtrlPressed = false, CtrlPressed = false,
ShiftPressed = false, ShiftPressed = false,

View file

@ -65,7 +65,42 @@ namespace Flow.Launcher.Plugin
public static bool IsDotNet(string language) public static bool IsDotNet(string language)
{ {
return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase) return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase)
|| language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if this language is a Python language
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
public static bool IsPython(string language)
{
return language.Equals(Python, StringComparison.OrdinalIgnoreCase)
|| language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if this language is a Node.js language
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
public static bool IsNodeJs(string language)
{
return language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase)
|| language.Equals(TypeScriptV2, StringComparison.OrdinalIgnoreCase)
|| language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase)
|| language.Equals(JavaScriptV2, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if this language is a executable language
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
public static bool IsExecutable(string language)
{
return language.Equals(Executable, StringComparison.OrdinalIgnoreCase)
|| language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase);
} }
/// <summary> /// <summary>
@ -76,15 +111,9 @@ namespace Flow.Launcher.Plugin
public static bool IsAllowed(string language) public static bool IsAllowed(string language)
{ {
return IsDotNet(language) return IsDotNet(language)
|| language.Equals(Python, StringComparison.OrdinalIgnoreCase) || IsPython(language)
|| language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase) || IsNodeJs(language)
|| language.Equals(Executable, StringComparison.OrdinalIgnoreCase) || IsExecutable(language);
|| language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase)
|| language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase)
|| language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase)
|| language.Equals(TypeScriptV2, StringComparison.OrdinalIgnoreCase)
|| language.Equals(JavaScriptV2, StringComparison.OrdinalIgnoreCase);
;
} }
} }
} }

View file

@ -76,7 +76,9 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" /> <PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,6 +1,4 @@
using Flow.Launcher.Plugin.SharedModels; using System;
using JetBrains.Annotations;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
@ -8,6 +6,9 @@ using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media;
using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
namespace Flow.Launcher.Plugin namespace Flow.Launcher.Plugin
{ {
@ -141,15 +142,47 @@ namespace Flow.Launcher.Plugin
List<PluginPair> GetAllPlugins(); List<PluginPair> GetAllPlugins();
/// <summary> /// <summary>
/// Register a callback for Global Keyboard Event /// Registers a callback function for global keyboard events.
/// </summary> /// </summary>
/// <param name="callback"></param> /// <param name="callback">
/// The callback function to invoke when a global keyboard event occurs.
/// <para>
/// Parameters:
/// <list type="number">
/// <item><description>int: The type of <see cref="KeyEvent"/> (key down, key up, etc.)</description></item>
/// <item><description>int: The virtual key code of the pressed/released key</description></item>
/// <item><description><see cref="SpecialKeyState"/>: The state of modifier keys (Ctrl, Alt, Shift, etc.)</description></item>
/// </list>
/// </para>
/// <para>
/// Returns: <c>true</c> to allow normal system processing of the key event,
/// or <c>false</c> to intercept and prevent default handling.
/// </para>
/// </param>
/// <remarks>
/// This callback will be invoked for all keyboard events system-wide.
/// Use with caution as intercepting system keys may affect normal system operation.
/// </remarks>
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback); public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
/// <summary> /// <summary>
/// Remove a callback for Global Keyboard Event /// Remove a callback for Global Keyboard Event
/// </summary> /// </summary>
/// <param name="callback"></param> /// <param name="callback">
/// The callback function to invoke when a global keyboard event occurs.
/// <para>
/// Parameters:
/// <list type="number">
/// <item><description>int: The type of <see cref="KeyEvent"/> (key down, key up, etc.)</description></item>
/// <item><description>int: The virtual key code of the pressed/released key</description></item>
/// <item><description><see cref="SpecialKeyState"/>: The state of modifier keys (Ctrl, Alt, Shift, etc.)</description></item>
/// </list>
/// </para>
/// <para>
/// Returns: <c>true</c> to allow normal system processing of the key event,
/// or <c>false</c> to intercept and prevent default handling.
/// </para>
/// </param>
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback); public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
/// <summary> /// <summary>
@ -190,11 +223,15 @@ namespace Flow.Launcher.Plugin
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default); Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default);
/// <summary> /// <summary>
/// Add ActionKeyword and update action keyword metadata for specific plugin /// Add ActionKeyword and update action keyword metadata for specific plugin.
/// Before adding, please check if action keyword is already assigned by <see cref="ActionKeywordAssigned"/> /// Before adding, please check if action keyword is already assigned by <see cref="ActionKeywordAssigned"/>
/// </summary> /// </summary>
/// <param name="pluginId">ID for plugin that needs to add action keyword</param> /// <param name="pluginId">ID for plugin that needs to add action keyword</param>
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param> /// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
/// <remarks>
/// If new action keyword contains any whitespace, FL will still add it but it will not work for users.
/// So plugin should check the whitespace before calling this function.
/// </remarks>
void AddActionKeyword(string pluginId, string newActionKeyword); void AddActionKeyword(string pluginId, string newActionKeyword);
/// <summary> /// <summary>
@ -227,6 +264,11 @@ namespace Flow.Launcher.Plugin
/// </summary> /// </summary>
void LogWarn(string className, string message, [CallerMemberName] string methodName = ""); void LogWarn(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log error message. Preferred error logging method for plugins.
/// </summary>
void LogError(string className, string message, [CallerMemberName] string methodName = "");
/// <summary> /// <summary>
/// Log an Exception. Will throw if in debug mode so developer will be aware, /// Log an Exception. Will throw if in debug mode so developer will be aware,
/// otherwise logs the eror message. This is the primary logging method used for Flow /// otherwise logs the eror message. This is the primary logging method used for Flow
@ -242,9 +284,10 @@ namespace Flow.Launcher.Plugin
T LoadSettingJsonStorage<T>() where T : new(); T LoadSettingJsonStorage<T>() where T : new();
/// <summary> /// <summary>
/// Save JsonStorage for current plugin's setting. This is the method used to save settings to json in Flow.Launcher /// Save JsonStorage for current plugin's setting. This is the method used to save settings to json in Flow.
/// This method will save the original instance loaded with LoadJsonStorage. /// This method will save the original instance loaded with LoadJsonStorage.
/// This API call is for manually Save. Flow will automatically save all setting type that has called LoadSettingJsonStorage or SaveSettingJsonStorage previously. /// This API call is for manually Save.
/// Flow will automatically save all setting type that has called <see cref="LoadSettingJsonStorage"/> or <see cref="SaveSettingJsonStorage"/> previously.
/// </summary> /// </summary>
/// <typeparam name="T">Type for Serialization</typeparam> /// <typeparam name="T">Type for Serialization</typeparam>
/// <returns></returns> /// <returns></returns>
@ -344,5 +387,163 @@ namespace Flow.Launcher.Plugin
/// Stop the loading bar in main window /// Stop the loading bar in main window
/// </summary> /// </summary>
public void StopLoadingBar(); public void StopLoadingBar();
/// <summary>
/// Get all available themes
/// </summary>
/// <returns></returns>
public List<ThemeData> GetAvailableThemes();
/// <summary>
/// Get the current theme
/// </summary>
/// <returns></returns>
public ThemeData GetCurrentTheme();
/// <summary>
/// Set the current theme
/// </summary>
/// <param name="theme"></param>
/// <returns>
/// True if the theme is set successfully, false otherwise.
/// </returns>
public bool SetCurrentTheme(ThemeData theme);
/// <summary>
/// Save all Flow's plugins caches
/// </summary>
void SavePluginCaches();
/// <summary>
/// Load BinaryStorage for current plugin's cache. This is the method used to load cache from binary in Flow.
/// When the file is not exist, it will create a new instance for the specific type.
/// </summary>
/// <typeparam name="T">Type for deserialization</typeparam>
/// <param name="cacheName">Cache file name</param>
/// <param name="cacheDirectory">Cache directory from plugin metadata</param>
/// <param name="defaultData">Default data to return</param>
/// <returns></returns>
/// <remarks>
/// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
/// </remarks>
Task<T> LoadCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory, T defaultData) where T : new();
/// <summary>
/// Save BinaryStorage for current plugin's cache. This is the method used to save cache to binary in Flow.
/// This method will save the original instance loaded with LoadCacheBinaryStorageAsync.
/// This API call is for manually Save.
/// Flow will automatically save all cache type that has called <see cref="LoadCacheBinaryStorageAsync"/> or <see cref="SaveCacheBinaryStorageAsync"/> previously.
/// </summary>
/// <typeparam name="T">Type for Serialization</typeparam>
/// <param name="cacheName">Cache file name</param>
/// <param name="cacheDirectory">Cache directory from plugin metadata</param>
/// <returns></returns>
/// <remarks>
/// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
/// </remarks>
Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new();
/// <summary>
/// Load image from path.
/// Support local, remote and data:image url.
/// Support png, jpg, jpeg, gif, bmp, tiff, ico, svg image files.
/// If image path is missing, it will return a missing icon.
/// </summary>
/// <param name="path">The path of the image.</param>
/// <param name="loadFullImage">
/// Load full image or not.
/// </param>
/// <param name="cacheImage">
/// Cache the image or not. Cached image will be stored in FL cache.
/// If the image is just used one time, it's better to set this to false.
/// </param>
/// <returns></returns>
ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true);
/// <summary>
/// Update the plugin manifest
/// </summary>
/// <param name="usePrimaryUrlOnly">
/// FL has multiple urls to download the plugin manifest. Set this to true to only use the primary url.
/// </param>
/// <param name="token"></param>
/// <returns>True if the manifest is updated successfully, false otherwise</returns>
public Task<bool> UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default);
/// <summary>
/// Get the plugin manifest.
/// </summary>
/// <remarks>
/// If Flow cannot get manifest data, this could be null
/// </remarks>
/// <returns></returns>
public IReadOnlyList<UserPlugin> GetPluginManifest();
/// <summary>
/// Check if the plugin has been modified.
/// If this plugin is updated, installed or uninstalled and users do not restart the app,
/// it will be marked as modified
/// </summary>
/// <param name="id">Plugin id</param>
/// <returns></returns>
public bool PluginModified(string id);
/// <summary>
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
/// unless it's a local path installation
/// </summary>
/// <param name="pluginMetadata">The metadata of the old plugin to update</param>
/// <param name="plugin">The new plugin to update</param>
/// <param name="zipFilePath">
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
/// </param>
/// <returns></returns>
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
/// <summary>
/// Install a plugin. By default will remove the zip file if installation is from url,
/// unless it's a local path installation
/// </summary>
/// <param name="plugin">The plugin to install</param>
/// <param name="zipFilePath">
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
/// </param>
public void InstallPlugin(UserPlugin plugin, string zipFilePath);
/// <summary>
/// Uninstall a plugin
/// </summary>
/// <param name="pluginMetadata">The metadata of the plugin to uninstall</param>
/// <param name="removePluginSettings">
/// Plugin has their own settings. If this is set to true, the plugin settings will be removed.
/// </param>
/// <returns></returns>
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
/// <summary>
/// Log debug message of the time taken to execute a method
/// Message will only be logged in Debug mode
/// </summary>
/// <returns>The time taken to execute the method in milliseconds</returns>
public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "");
/// <summary>
/// Log debug message of the time taken to execute a method asynchronously
/// Message will only be logged in Debug mode
/// </summary>
/// <returns>The time taken to execute the method in milliseconds</returns>
public Task<long> StopwatchLogDebugAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "");
/// <summary>
/// Log info message of the time taken to execute a method
/// </summary>
/// <returns>The time taken to execute the method in milliseconds</returns>
public long StopwatchLogInfo(string className, string message, Action action, [CallerMemberName] string methodName = "");
/// <summary>
/// Log info message of the time taken to execute a method asynchronously
/// </summary>
/// <returns>The time taken to execute the method in milliseconds</returns>
public Task<long> StopwatchLogInfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "");
} }
} }

View file

@ -4,17 +4,42 @@ using System.Threading;
namespace Flow.Launcher.Plugin namespace Flow.Launcher.Plugin
{ {
/// <summary>
/// Interface for plugins that want to manually update their results
/// </summary>
public interface IResultUpdated : IFeatures public interface IResultUpdated : IFeatures
{ {
/// <summary>
/// Event that is triggered when the results are updated
/// </summary>
event ResultUpdatedEventHandler ResultsUpdated; event ResultUpdatedEventHandler ResultsUpdated;
} }
/// <summary>
/// Delegate for the ResultsUpdated event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e); public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
/// <summary>
/// Event arguments for the ResultsUpdated event
/// </summary>
public class ResultUpdatedEventArgs : EventArgs public class ResultUpdatedEventArgs : EventArgs
{ {
/// <summary>
/// List of results that should be displayed
/// </summary>
public List<Result> Results; public List<Result> Results;
/// <summary>
/// Query that triggered the update
/// </summary>
public Query Query; public Query Query;
/// <summary>
/// Token that can be used to cancel the update
/// </summary>
public CancellationToken Token { get; init; } public CancellationToken Token { get; init; }
} }
} }

View file

@ -1,18 +1,21 @@
namespace Flow.Launcher.Plugin namespace Flow.Launcher.Plugin
{ {
/// <summary> /// <summary>
/// Inherit this interface if additional data e.g. cache needs to be saved. /// Inherit this interface if you need to save additional data which is not a setting or cache,
/// please implement this interface.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// For storing plugin settings, prefer <see cref="IPublicAPI.LoadSettingJsonStorage{T}"/> /// For storing plugin settings, prefer <see cref="IPublicAPI.LoadSettingJsonStorage{T}"/>
/// or <see cref="IPublicAPI.SaveSettingJsonStorage{T}"/>. /// or <see cref="IPublicAPI.SaveSettingJsonStorage{T}"/>.
/// Once called, your settings will be automatically saved by Flow. /// For storing plugin caches, prefer <see cref="IPublicAPI.LoadCacheBinaryStorageAsync{T}"/>
/// or <see cref="IPublicAPI.SaveCacheBinaryStorageAsync{T}(string, string)"/>.
/// Once called, those settings and caches will be automatically saved by Flow.
/// </remarks> /// </remarks>
public interface ISavable : IFeatures public interface ISavable : IFeatures
{ {
/// <summary> /// <summary>
/// Save additional plugin data, such as cache. /// Save additional plugin data.
/// </summary> /// </summary>
void Save(); void Save();
} }
} }

View file

@ -2,8 +2,15 @@
namespace Flow.Launcher.Plugin namespace Flow.Launcher.Plugin
{ {
/// <summary>
/// This interface is used to create settings panel for .Net plugins
/// </summary>
public interface ISettingProvider public interface ISettingProvider
{ {
/// <summary>
/// Create settings panel control for .Net plugins
/// </summary>
/// <returns></returns>
Control CreateSettingPanel(); Control CreateSettingPanel();
} }
} }

View file

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

View file

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

View file

@ -5,10 +5,18 @@
/// </summary> /// </summary>
public class PluginInitContext public class PluginInitContext
{ {
/// <summary>
/// Default constructor.
/// </summary>
public PluginInitContext() public PluginInitContext()
{ {
} }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="currentPluginMetadata"></param>
/// <param name="api"></param>
public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api) public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api)
{ {
CurrentPluginMetadata = currentPluginMetadata; CurrentPluginMetadata = currentPluginMetadata;

View file

@ -4,24 +4,77 @@ using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin namespace Flow.Launcher.Plugin
{ {
/// <summary>
/// Plugin metadata
/// </summary>
public class PluginMetadata : BaseModel public class PluginMetadata : BaseModel
{ {
private string _pluginDirectory; /// <summary>
/// Plugin ID.
/// </summary>
public string ID { get; set; } public string ID { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public string Version { get; set; }
public string Language { get; set; }
public string Description { get; set; }
public string Website { get; set; }
public bool Disabled { get; set; }
public string ExecuteFilePath { get; private set;}
/// <summary>
/// Plugin name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Plugin author.
/// </summary>
public string Author { get; set; }
/// <summary>
/// Plugin version.
/// </summary>
public string Version { get; set; }
/// <summary>
/// Plugin language.
/// See <see cref="AllowedLanguage"/>
/// </summary>
public string Language { get; set; }
/// <summary>
/// Plugin description.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Plugin website.
/// </summary>
public string Website { get; set; }
/// <summary>
/// Whether plugin is disabled.
/// </summary>
public bool Disabled { get; set; }
/// <summary>
/// Plugin execute file path.
/// </summary>
public string ExecuteFilePath { get; private set; }
/// <summary>
/// Plugin execute file name.
/// </summary>
public string ExecuteFileName { get; set; } public string ExecuteFileName { get; set; }
/// <summary>
/// Plugin assembly name.
/// Only available for .Net plugins.
/// </summary>
[JsonIgnore]
public string AssemblyName { get; internal set; }
private string _pluginDirectory;
/// <summary>
/// Plugin source directory.
/// </summary>
public string PluginDirectory public string PluginDirectory
{ {
get { return _pluginDirectory; } get => _pluginDirectory;
internal set internal set
{ {
_pluginDirectory = value; _pluginDirectory = value;
@ -30,30 +83,77 @@ namespace Flow.Launcher.Plugin
} }
} }
/// <summary>
/// The first action keyword of plugin.
/// </summary>
public string ActionKeyword { get; set; } public string ActionKeyword { get; set; }
/// <summary>
/// All action keywords of plugin.
/// </summary>
public List<string> ActionKeywords { get; set; } public List<string> ActionKeywords { get; set; }
/// <summary>
/// Hide plugin keyword setting panel.
/// </summary>
public bool HideActionKeywordPanel { get; set; } public bool HideActionKeywordPanel { get; set; }
public string IcoPath { get; set;} /// <summary>
/// Plugin search delay time in ms. Null means use default search delay time.
public override string ToString() /// </summary>
{ public int? SearchDelayTime { get; set; } = null;
return Name;
}
/// <summary>
/// Plugin icon path.
/// </summary>
public string IcoPath { get; set;}
/// <summary>
/// Plugin priority.
/// </summary>
[JsonIgnore] [JsonIgnore]
public int Priority { get; set; } public int Priority { get; set; }
/// <summary> /// <summary>
/// Init time include both plugin load time and init time /// Init time include both plugin load time and init time.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public long InitTime { get; set; } public long InitTime { get; set; }
/// <summary>
/// Average query time.
/// </summary>
[JsonIgnore] [JsonIgnore]
public long AvgQueryTime { get; set; } public long AvgQueryTime { get; set; }
/// <summary>
/// Query count.
/// </summary>
[JsonIgnore] [JsonIgnore]
public int QueryCount { get; set; } public int QueryCount { get; set; }
/// <summary>
/// The path to the plugin settings directory which is not validated.
/// It is used to store plugin settings files and data files.
/// When plugin is deleted, FL will ask users whether to keep its settings.
/// If users do not want to keep, this directory will be deleted.
/// </summary>
public string PluginSettingsDirectoryPath { get; internal set; }
/// <summary>
/// The path to the plugin cache directory which is not validated.
/// It is used to store cache files.
/// When plugin is deleted, this directory will be deleted as well.
/// </summary>
public string PluginCacheDirectoryPath { get; internal set; }
/// <summary>
/// Convert <see cref="PluginMetadata"/> to string.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Name;
}
} }
} }

View file

@ -1,21 +1,37 @@
namespace Flow.Launcher.Plugin namespace Flow.Launcher.Plugin
{ {
/// <summary>
/// Plugin instance and plugin metadata
/// </summary>
public class PluginPair public class PluginPair
{ {
/// <summary>
/// Plugin instance
/// </summary>
public IAsyncPlugin Plugin { get; internal set; } public IAsyncPlugin Plugin { get; internal set; }
/// <summary>
/// Plugin metadata
/// </summary>
public PluginMetadata Metadata { get; internal set; } public PluginMetadata Metadata { get; internal set; }
/// <summary>
/// Convert to string
/// </summary>
/// <returns></returns>
public override string ToString() public override string ToString()
{ {
return Metadata.Name; return Metadata.Name;
} }
/// <summary>
/// Compare by plugin metadata ID
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
PluginPair r = obj as PluginPair; if (obj is PluginPair r)
if (r != null)
{ {
return string.Equals(r.Metadata.ID, Metadata.ID); return string.Equals(r.Metadata.ID, Metadata.ID);
} }
@ -25,6 +41,10 @@
} }
} }
/// <summary>
/// Get hash code
/// </summary>
/// <returns></returns>
public override int GetHashCode() public override int GetHashCode()
{ {
var hashcode = Metadata.ID?.GetHashCode() ?? 0; var hashcode = Metadata.ID?.GetHashCode() ?? 0;

View file

@ -2,12 +2,14 @@
namespace Flow.Launcher.Plugin namespace Flow.Launcher.Plugin
{ {
/// <summary>
/// Represents a query that is sent to a plugin.
/// </summary>
public class Query public class Query
{ {
public Query() { }
/// <summary> /// <summary>
/// Raw query, this includes action keyword if it has /// Raw query, this includes action keyword if it has.
/// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace.
/// We didn't recommend use this property directly. You should always use Search property. /// We didn't recommend use this property directly. You should always use Search property.
/// </summary> /// </summary>
public string RawQuery { get; internal init; } public string RawQuery { get; internal init; }
@ -54,18 +56,18 @@ namespace Flow.Launcher.Plugin
/// </summary> /// </summary>
public string ActionKeyword { get; init; } public string ActionKeyword { get; init; }
[JsonIgnore]
/// <summary> /// <summary>
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item. /// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items. /// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
/// </remarks> /// </remarks>
[JsonIgnore]
public string FirstSearch => SplitSearch(0); public string FirstSearch => SplitSearch(0);
[JsonIgnore] [JsonIgnore]
private string _secondToEndSearch; private string _secondToEndSearch;
/// <summary> /// <summary>
/// strings from second search (including) to last search /// strings from second search (including) to last search
/// </summary> /// </summary>

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Runtime;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,6 +12,10 @@ namespace Flow.Launcher.Plugin
/// </summary> /// </summary>
public class Result 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 _pluginDirectory;
@ -20,6 +23,8 @@ namespace Flow.Launcher.Plugin
private string _copyText = string.Empty; private string _copyText = string.Empty;
private string _badgeIcoPath;
/// <summary> /// <summary>
/// The title of the result. This is always required. /// The title of the result. This is always required.
/// </summary> /// </summary>
@ -62,7 +67,7 @@ namespace Flow.Launcher.Plugin
/// <remarks>GlyphInfo is prioritized if not null</remarks> /// <remarks>GlyphInfo is prioritized if not null</remarks>
public string IcoPath public string IcoPath
{ {
get { return _icoPath; } get => _icoPath;
set set
{ {
// As a standard this property will handle prepping and converting to absolute local path for icon image processing // As a standard this property will handle prepping and converting to absolute local path for icon image processing
@ -82,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> /// <summary>
/// Determines if Icon has a border radius /// Determines if Icon has a border radius
/// </summary> /// </summary>
@ -96,14 +128,18 @@ namespace Flow.Launcher.Plugin
/// <summary> /// <summary>
/// Delegate to load an icon for this result. /// Delegate to load an icon for this result.
/// </summary> /// </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> /// <summary>
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
/// </summary> /// </summary>
public GlyphInfo Glyph { get; init; } public GlyphInfo Glyph { get; init; }
/// <summary> /// <summary>
/// An action to take in the form of a function call when the result has been selected. /// An action to take in the form of a function call when the result has been selected.
/// </summary> /// </summary>
@ -145,59 +181,19 @@ namespace Flow.Launcher.Plugin
/// </summary> /// </summary>
public string PluginDirectory public string PluginDirectory
{ {
get { return _pluginDirectory; } get => _pluginDirectory;
set set
{ {
_pluginDirectory = value; _pluginDirectory = value;
// When the Result object is returned from the query call, PluginDirectory is not provided until // 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 // 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; 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> /// <summary>
/// Additional data associated with this result /// Additional data associated with this result
/// </summary> /// </summary>
@ -226,16 +222,6 @@ namespace Flow.Launcher.Plugin
/// </summary> /// </summary>
public Lazy<UserControl> PreviewPanel { get; set; } 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> /// <summary>
/// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result /// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result
/// </summary> /// </summary>
@ -257,11 +243,6 @@ namespace Flow.Launcher.Plugin
/// </summary> /// </summary>
public bool AddSelectedCount { get; set; } = true; 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> /// <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. /// 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. /// This can be useful when your plugin will change the Title or SubTitle of the result dynamically.
@ -270,6 +251,66 @@ namespace Flow.Launcher.Plugin
/// </summary> /// </summary>
public string RecordKey { get; set; } = null; 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> /// <summary>
/// Info of the preview section of a <see cref="Result"/> /// Info of the preview section of a <see cref="Result"/>
/// </summary> /// </summary>

View file

@ -264,12 +264,12 @@ namespace Flow.Launcher.Plugin.SharedCommands
var index = path.LastIndexOf('\\'); var index = path.LastIndexOf('\\');
if (index > 0 && index < (path.Length - 1)) if (index > 0 && index < (path.Length - 1))
{ {
string previousDirectoryPath = path.Substring(0, index + 1); string previousDirectoryPath = path[..(index + 1)];
return locationExists(previousDirectoryPath) ? previousDirectoryPath : ""; return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty;
} }
else else
{ {
return ""; return string.Empty;
} }
} }
@ -285,7 +285,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
// not full path, get previous level directory string // not full path, get previous level directory string
var indexOfSeparator = path.LastIndexOf('\\'); var indexOfSeparator = path.LastIndexOf('\\');
return path.Substring(0, indexOfSeparator + 1); return path[..(indexOfSeparator + 1)];
} }
return path; return path;
@ -318,5 +318,51 @@ namespace Flow.Launcher.Plugin.SharedCommands
{ {
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
} }
/// <summary>
/// Validates a directory, creating it if it doesn't exist
/// </summary>
/// <param name="path"></param>
public static void ValidateDirectory(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
/// <summary>
/// Validates a data directory, synchronizing it by ensuring all files from a bundled source directory exist in it.
/// If files are missing or outdated, they are copied from the bundled directory to the data directory.
/// </summary>
/// <param name="bundledDataDirectory"></param>
/// <param name="dataDirectory"></param>
public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
{
if (!Directory.Exists(dataDirectory))
{
Directory.CreateDirectory(dataDirectory);
}
foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
{
var data = Path.GetFileName(bundledDataPath);
if (data == null) continue;
var dataPath = Path.Combine(dataDirectory, data);
if (!File.Exists(dataPath))
{
File.Copy(bundledDataPath, dataPath);
}
else
{
var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
if (time1 != time2)
{
File.Copy(bundledDataPath, dataPath, true);
}
}
}
}
} }
} }

View file

@ -6,6 +6,9 @@ using System.Linq;
namespace Flow.Launcher.Plugin.SharedCommands namespace Flow.Launcher.Plugin.SharedCommands
{ {
/// <summary>
/// Contains methods to open a search in a new browser window or tab.
/// </summary>
public static class SearchWeb public static class SearchWeb
{ {
private static string GetDefaultBrowserPath() private static string GetDefaultBrowserPath()
@ -106,4 +109,4 @@ namespace Flow.Launcher.Plugin.SharedCommands
} }
} }
} }
} }

View file

@ -8,12 +8,26 @@ using Windows.Win32.Foundation;
namespace Flow.Launcher.Plugin.SharedCommands namespace Flow.Launcher.Plugin.SharedCommands
{ {
/// <summary>
/// Contains methods for running shell commands
/// </summary>
public static class ShellCommand public static class ShellCommand
{ {
/// <summary>
/// Delegate for EnumThreadWindows
/// </summary>
/// <param name="hwnd"></param>
/// <param name="lParam"></param>
/// <returns></returns>
public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam); public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
private static bool containsSecurityWindow; private static bool containsSecurityWindow;
/// <summary>
/// Runs a windows command using the provided ProcessStartInfo
/// </summary>
/// <param name="processStartInfo"></param>
/// <returns></returns>
public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo) public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo)
{ {
processStartInfo.Verb = "RunAsUser"; processStartInfo.Verb = "RunAsUser";
@ -65,6 +79,15 @@ namespace Flow.Launcher.Plugin.SharedCommands
return buffer[..length].ToString(); return buffer[..length].ToString();
} }
/// <summary>
/// Runs a windows command using the provided ProcessStartInfo
/// </summary>
/// <param name="fileName"></param>
/// <param name="workingDirectory"></param>
/// <param name="arguments"></param>
/// <param name="verb"></param>
/// <param name="createNoWindow"></param>
/// <returns></returns>
public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "",
string arguments = "", string verb = "", bool createNoWindow = false) string arguments = "", string verb = "", bool createNoWindow = false)
{ {

View file

@ -2,14 +2,29 @@
namespace Flow.Launcher.Plugin.SharedModels namespace Flow.Launcher.Plugin.SharedModels
{ {
/// <summary>
/// Represents the result of a match operation.
/// </summary>
public class MatchResult public class MatchResult
{ {
/// <summary>
/// Initializes a new instance of the <see cref="MatchResult"/> class.
/// </summary>
/// <param name="success"></param>
/// <param name="searchPrecision"></param>
public MatchResult(bool success, SearchPrecisionScore searchPrecision) public MatchResult(bool success, SearchPrecisionScore searchPrecision)
{ {
Success = success; Success = success;
SearchPrecision = searchPrecision; SearchPrecision = searchPrecision;
} }
/// <summary>
/// Initializes a new instance of the <see cref="MatchResult"/> class.
/// </summary>
/// <param name="success"></param>
/// <param name="searchPrecision"></param>
/// <param name="matchData"></param>
/// <param name="rawScore"></param>
public MatchResult(bool success, SearchPrecisionScore searchPrecision, List<int> matchData, int rawScore) public MatchResult(bool success, SearchPrecisionScore searchPrecision, List<int> matchData, int rawScore)
{ {
Success = success; Success = success;
@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels
RawScore = rawScore; RawScore = rawScore;
} }
/// <summary>
/// Whether the match operation was successful.
/// </summary>
public bool Success { get; set; } public bool Success { get; set; }
/// <summary> /// <summary>
@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels
/// </summary> /// </summary>
private int _rawScore; private int _rawScore;
/// <summary>
/// The raw calculated search score without any search precision filtering applied.
/// </summary>
public int RawScore public int RawScore
{ {
get { return _rawScore; } get { return _rawScore; }
@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels
/// </summary> /// </summary>
public List<int> MatchData { get; set; } public List<int> MatchData { get; set; }
/// <summary>
/// The search precision score used to filter the search results.
/// </summary>
public SearchPrecisionScore SearchPrecision { get; set; } public SearchPrecisionScore SearchPrecision { get; set; }
/// <summary>
/// Determines if the search precision score is met.
/// </summary>
/// <returns></returns>
public bool IsSearchPrecisionScoreMet() public bool IsSearchPrecisionScoreMet()
{ {
return IsSearchPrecisionScoreMet(_rawScore); return IsSearchPrecisionScoreMet(_rawScore);
@ -63,10 +91,24 @@ namespace Flow.Launcher.Plugin.SharedModels
} }
} }
/// <summary>
/// Represents the search precision score used to filter search results.
/// </summary>
public enum SearchPrecisionScore public enum SearchPrecisionScore
{ {
/// <summary>
/// The highest search precision score.
/// </summary>
Regular = 50, Regular = 50,
/// <summary>
/// The medium search precision score.
/// </summary>
Low = 20, Low = 20,
/// <summary>
/// The lowest search precision score.
/// </summary>
None = 0 None = 0
} }
} }

View file

@ -0,0 +1,77 @@
using System;
namespace Flow.Launcher.Plugin.SharedModels;
/// <summary>
/// Theme data model
/// </summary>
public class ThemeData
{
/// <summary>
/// Theme file name without extension
/// </summary>
public string FileNameWithoutExtension { get; private init; }
/// <summary>
/// Theme name
/// </summary>
public string Name { get; private init; }
/// <summary>
/// Indicates whether the theme supports dark mode
/// </summary>
public bool? IsDark { get; private init; }
/// <summary>
/// Indicates whether the theme supports blur effects
/// </summary>
public bool? HasBlur { get; private init; }
/// <summary>
/// Theme data constructor
/// </summary>
public ThemeData(string fileNameWithoutExtension, string name, bool? isDark = null, bool? hasBlur = null)
{
FileNameWithoutExtension = fileNameWithoutExtension;
Name = name;
IsDark = isDark;
HasBlur = hasBlur;
}
/// <inheritdoc />
public static bool operator ==(ThemeData left, ThemeData right)
{
if (left is null && right is null)
return true;
if (left is null || right is null)
return false;
return left.Equals(right);
}
/// <inheritdoc />
public static bool operator !=(ThemeData left, ThemeData right)
{
return !(left == right);
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is not ThemeData other)
return false;
return FileNameWithoutExtension == other.FileNameWithoutExtension &&
Name == other.Name;
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(FileNameWithoutExtension, Name);
}
/// <inheritdoc />
public override string ToString()
{
return Name;
}
}

View file

@ -0,0 +1,80 @@
using System;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// User Plugin Model for Flow Launcher
/// </summary>
public record UserPlugin
{
/// <summary>
/// Unique identifier of the plugin
/// </summary>
public string ID { get; set; }
/// <summary>
/// Name of the plugin
/// </summary>
public string Name { get; set; }
/// <summary>
/// Description of the plugin
/// </summary>
public string Description { get; set; }
/// <summary>
/// Author of the plugin
/// </summary>
public string Author { get; set; }
/// <summary>
/// Version of the plugin
/// </summary>
public string Version { get; set; }
/// <summary>
/// Allow language of the plugin <see cref="AllowedLanguage"/>
/// </summary>
public string Language { get; set; }
/// <summary>
/// Website of the plugin
/// </summary>
public string Website { get; set; }
/// <summary>
/// URL to download the plugin
/// </summary>
public string UrlDownload { get; set; }
/// <summary>
/// URL to the source code of the plugin
/// </summary>
public string UrlSourceCode { get; set; }
/// <summary>
/// Local path where the plugin is installed
/// </summary>
public string LocalInstallPath { get; set; }
/// <summary>
/// Icon path of the plugin
/// </summary>
public string IcoPath { get; set; }
/// <summary>
/// The date when the plugin was last updated
/// </summary>
public DateTime? LatestReleaseDate { get; set; }
/// <summary>
/// The date when the plugin was added to the local system
/// </summary>
public DateTime? DateAdded { get; set; }
/// <summary>
/// Indicates whether the plugin is installed from a local path
/// </summary>
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
}
}

View file

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

View file

@ -53,11 +53,11 @@
</Button> </Button>
</Grid> </Grid>
</StackPanel> </StackPanel>
<StackPanel Margin="26,12,26,0"> <StackPanel Margin="26 12 26 0">
<StackPanel Grid.Row="0" Margin="0,0,0,12"> <StackPanel Grid.Row="0" Margin="0 0 0 12">
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
Margin="0,0,0,0" Margin="0 0 0 0"
FontSize="20" FontSize="20"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="{DynamicResource actionKeywordsTitle}" Text="{DynamicResource actionKeywordsTitle}"
@ -71,7 +71,7 @@
TextWrapping="WrapWithOverflow" /> TextWrapping="WrapWithOverflow" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,18,0,0" Orientation="Horizontal"> <StackPanel Margin="0 18 0 0" Orientation="Horizontal">
<TextBlock <TextBlock
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
@ -83,14 +83,14 @@
x:Name="tbOldActionKeyword" x:Name="tbOldActionKeyword"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Margin="14,10,10,10" Margin="14 10 10 10"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="14" FontSize="14"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource Color05B}" /> Foreground="{DynamicResource Color05B}" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,0,0,10" Orientation="Horizontal"> <StackPanel Margin="0 0 0 10" Orientation="Horizontal">
<TextBlock <TextBlock
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
@ -101,7 +101,7 @@
<TextBox <TextBox
x:Name="tbAction" x:Name="tbAction"
Width="105" Width="105"
Margin="10,10,15,10" Margin="10 10 15 10"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</StackPanel> </StackPanel>
@ -112,20 +112,20 @@
Grid.Row="1" Grid.Row="1"
Background="{DynamicResource PopupButtonAreaBGColor}" Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}" BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0"> BorderThickness="0 1 0 0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
Width="145" Width="145"
Height="30" Height="30"
Margin="10,0,5,0" Margin="10 0 5 0"
Click="BtnCancel_OnClick" Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" /> Content="{DynamicResource cancel}" />
<Button <Button
x:Name="btnDone" x:Name="btnDone"
Width="145" Width="145"
Height="30" Height="30"
Margin="5,0,10,0" Margin="5 0 10 0"
Click="btnDone_OnClick" Click="btnDone_OnClick"
Style="{StaticResource AccentButtonStyle}"> Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" /> <TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />

View file

@ -1,8 +1,6 @@
using System.Windows; using System.Windows;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel; using Flow.Launcher.ViewModel;
using Flow.Launcher.Core;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
@ -10,20 +8,19 @@ namespace Flow.Launcher
{ {
public partial class ActionKeywords public partial class ActionKeywords
{ {
private readonly PluginPair plugin; private readonly PluginPair _plugin;
private readonly Internationalization translater = InternationalizationManager.Instance; private readonly PluginViewModel _pluginViewModel;
private readonly PluginViewModel pluginViewModel;
public ActionKeywords(PluginViewModel pluginViewModel) public ActionKeywords(PluginViewModel pluginViewModel)
{ {
InitializeComponent(); InitializeComponent();
plugin = pluginViewModel.PluginPair; _plugin = pluginViewModel.PluginPair;
this.pluginViewModel = pluginViewModel; _pluginViewModel = pluginViewModel;
} }
private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e) private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e)
{ {
tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, plugin.Metadata.ActionKeywords.ToArray()); tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, _plugin.Metadata.ActionKeywords.ToArray());
tbAction.Focus(); tbAction.Focus();
} }
@ -34,7 +31,7 @@ namespace Flow.Launcher
private void btnDone_OnClick(object sender, RoutedEventArgs _) private void btnDone_OnClick(object sender, RoutedEventArgs _)
{ {
var oldActionKeywords = plugin.Metadata.ActionKeywords; var oldActionKeywords = _plugin.Metadata.ActionKeywords;
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList(); var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList();
newActionKeywords.RemoveAll(string.IsNullOrEmpty); newActionKeywords.RemoveAll(string.IsNullOrEmpty);
@ -48,7 +45,7 @@ namespace Flow.Launcher
{ {
if (oldActionKeywords.Count != newActionKeywords.Count) if (oldActionKeywords.Count != newActionKeywords.Count)
{ {
ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords); ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
return; return;
} }
@ -58,18 +55,16 @@ namespace Flow.Launcher
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords)) if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
{ {
// User just changes the sequence of action keywords // User just changes the sequence of action keywords
var msg = translater.GetTranslation("newActionKeywordsSameAsOld"); App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld"));
MessageBoxEx.Show(msg);
} }
else else
{ {
ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords); ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
} }
} }
else else
{ {
string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned"); App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
App.API.ShowMsgBox(msg);
} }
} }
@ -85,7 +80,7 @@ namespace Flow.Launcher
} }
// Update action keywords text and close window // Update action keywords text and close window
pluginViewModel.OnActionKeywordsChanged(); _pluginViewModel.OnActionKeywordsTextChanged();
Close(); Close();
} }
} }

View file

@ -4,6 +4,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core; using Flow.Launcher.Core;
using Flow.Launcher.Core.Configuration; using Flow.Launcher.Core.Configuration;
@ -18,10 +19,11 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.SettingPages.ViewModels;
using Flow.Launcher.ViewModel; using Flow.Launcher.ViewModel;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher namespace Flow.Launcher
{ {
@ -30,13 +32,16 @@ namespace Flow.Launcher
#region Public Properties #region Public Properties
public static IPublicAPI API { get; private set; } public static IPublicAPI API { get; private set; }
public static bool Exiting => _mainWindow.CanClose;
#endregion #endregion
#region Private Fields #region Private Fields
private static readonly string ClassName = nameof(App);
private static bool _disposed; private static bool _disposed;
private MainWindow _mainWindow; private static MainWindow _mainWindow;
private readonly MainViewModel _mainVM; private readonly MainViewModel _mainVM;
private readonly Settings _settings; private readonly Settings _settings;
@ -72,14 +77,27 @@ namespace Flow.Launcher
.AddSingleton(_ => _settings) .AddSingleton(_ => _settings)
.AddSingleton(sp => new Updater(sp.GetRequiredService<IPublicAPI>(), Launcher.Properties.Settings.Default.GithubRepo)) .AddSingleton(sp => new Updater(sp.GetRequiredService<IPublicAPI>(), Launcher.Properties.Settings.Default.GithubRepo))
.AddSingleton<Portable>() .AddSingleton<Portable>()
.AddSingleton<SettingWindowViewModel>()
.AddSingleton<IAlphabet, PinyinAlphabet>() .AddSingleton<IAlphabet, PinyinAlphabet>()
.AddSingleton<StringMatcher>() .AddSingleton<StringMatcher>()
.AddSingleton<Internationalization>() .AddSingleton<Internationalization>()
.AddSingleton<IPublicAPI, PublicAPIInstance>() .AddSingleton<IPublicAPI, PublicAPIInstance>()
.AddSingleton<MainViewModel>()
.AddSingleton<Theme>() .AddSingleton<Theme>()
// Use one instance for main window view model because we only have one main window
.AddSingleton<MainViewModel>()
// Use one instance for welcome window view model & setting window view model because
// pages in welcome window & setting window need to share the same instance and
// these two view models do not need to be reset when creating new windows
.AddSingleton<WelcomeViewModel>() .AddSingleton<WelcomeViewModel>()
.AddSingleton<SettingWindowViewModel>()
// Use transient instance for setting window page view models because
// pages in setting window need to be recreated when setting window is closed
.AddTransient<SettingsPaneAboutViewModel>()
.AddTransient<SettingsPaneGeneralViewModel>()
.AddTransient<SettingsPaneHotkeyViewModel>()
.AddTransient<SettingsPanePluginsViewModel>()
.AddTransient<SettingsPanePluginStoreViewModel>()
.AddTransient<SettingsPaneProxyViewModel>()
.AddTransient<SettingsPaneThemeViewModel>()
).Build(); ).Build();
Ioc.Default.ConfigureServices(host.Services); Ioc.Default.ConfigureServices(host.Services);
} }
@ -136,17 +154,27 @@ namespace Flow.Launcher
private async void OnStartup(object sender, StartupEventArgs e) private async void OnStartup(object sender, StartupEventArgs e)
{ {
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
{ {
// Because new message box api uses MessageBoxEx window,
// if it is created and closed before main window is created, it will cause the application to exit.
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
Log.SetLogLevel(_settings.LogLevel); Log.SetLogLevel(_settings.LogLevel);
// Update dynamic resources base on settings
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate(); Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------"); API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}"); API.LogInfo(ClassName, $"Runtime info:{ErrorReporting.RuntimeInfo()}");
RegisterAppDomainExceptions(); RegisterAppDomainExceptions();
RegisterDispatcherUnhandledException(); RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException();
var imageLoadertask = ImageLoader.InitializeAsync(); var imageLoadertask = ImageLoader.InitializeAsync();
@ -162,21 +190,20 @@ namespace Flow.Launcher
await PluginManager.InitializePluginsAsync(); await PluginManager.InitializePluginsAsync();
// Change language after all plugins are initialized because we need to update plugin title based on their api // Change language after all plugins are initialized because we need to update plugin title based on their api
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync(); await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
await imageLoadertask; await imageLoadertask;
_mainWindow = new MainWindow(); _mainWindow = new MainWindow();
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
Current.MainWindow = _mainWindow; Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher; Current.MainWindow.Title = Constant.FlowLauncher;
// Initialize hotkey mapper instantly after main window is created because
// it will steal focus from main window which causes window hide
HotKeyMapper.Initialize(); HotKeyMapper.Initialize();
// main windows needs initialized before theme change because of blur settings // Main windows needs initialized before theme change because of blur settings
Ioc.Default.GetRequiredService<Theme>().ChangeTheme(); Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
@ -187,28 +214,25 @@ namespace Flow.Launcher
AutoUpdates(); AutoUpdates();
API.SaveAppAllSettings(); API.SaveAppAllSettings();
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------"); API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
}); });
} }
#pragma warning restore VSTHRD100 // Avoid async void methods #pragma warning restore VSTHRD100 // Avoid async void methods
/// <summary>
/// Check startup only for Release
/// </summary>
[Conditional("RELEASE")]
private void AutoStartup() private void AutoStartup()
{ {
// we try to enable auto-startup on first launch, or reenable if it was removed // we try to enable auto-startup on first launch, or reenable if it was removed
// but the user still has the setting set // but the user still has the setting set
if (_settings.StartFlowLauncherOnSystemStartup && !Helper.AutoStartup.IsEnabled) if (_settings.StartFlowLauncherOnSystemStartup)
{ {
try try
{ {
if (_settings.UseLogonTaskForStartup) Helper.AutoStartup.CheckIsEnabled(_settings.UseLogonTaskForStartup);
{
Helper.AutoStartup.EnableViaLogonTask();
}
else
{
Helper.AutoStartup.EnableViaRegistry();
}
} }
catch (Exception e) catch (Exception e)
{ {
@ -220,6 +244,7 @@ namespace Flow.Launcher
} }
} }
[Conditional("RELEASE")]
private void AutoUpdates() private void AutoUpdates()
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
@ -245,25 +270,25 @@ namespace Flow.Launcher
{ {
AppDomain.CurrentDomain.ProcessExit += (s, e) => AppDomain.CurrentDomain.ProcessExit += (s, e) =>
{ {
Log.Info("|App.RegisterExitEvents|Process Exit"); API.LogInfo(ClassName, "Process Exit");
Dispose(); Dispose();
}; };
Current.Exit += (s, e) => Current.Exit += (s, e) =>
{ {
Log.Info("|App.RegisterExitEvents|Application Exit"); API.LogInfo(ClassName, "Application Exit");
Dispose(); Dispose();
}; };
Current.SessionEnding += (s, e) => Current.SessionEnding += (s, e) =>
{ {
Log.Info("|App.RegisterExitEvents|Session Ending"); API.LogInfo(ClassName, "Session Ending");
Dispose(); Dispose();
}; };
} }
/// <summary> /// <summary>
/// let exception throw as normal is better for Debug /// Let exception throw as normal is better for Debug
/// </summary> /// </summary>
[Conditional("RELEASE")] [Conditional("RELEASE")]
private void RegisterDispatcherUnhandledException() private void RegisterDispatcherUnhandledException()
@ -272,12 +297,20 @@ namespace Flow.Launcher
} }
/// <summary> /// <summary>
/// let exception throw as normal is better for Debug /// Let exception throw as normal is better for Debug
/// </summary> /// </summary>
[Conditional("RELEASE")] [Conditional("RELEASE")]
private static void RegisterAppDomainExceptions() private static void RegisterAppDomainExceptions()
{ {
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledException;
}
/// <summary>
/// Let exception throw as normal is better for Debug
/// </summary>
private static void RegisterTaskSchedulerUnhandledException()
{
TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException;
} }
#endregion #endregion
@ -299,12 +332,20 @@ namespace Flow.Launcher
return; return;
} }
// If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close()
// Accessing _mainWindow?.Dispatcher will cause the application stuck
// So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher
if (!_mainWindow.CanClose)
{
return;
}
_disposed = true; _disposed = true;
} }
Stopwatch.Normal("|App.Dispose|Dispose cost", () => API.StopwatchLogInfo(ClassName, "Dispose cost", () =>
{ {
Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); API.LogInfo(ClassName, "Begin Flow Launcher dispose ----------------------------------------------------");
if (disposing) if (disposing)
{ {
@ -314,7 +355,7 @@ namespace Flow.Launcher
_mainVM?.Dispose(); _mainVM?.Dispose();
} }
Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------");
}); });
} }
@ -331,7 +372,7 @@ namespace Flow.Launcher
public void OnSecondAppStarted() public void OnSecondAppStarted()
{ {
Ioc.Default.GetRequiredService<MainViewModel>().Show(); API.ShowMainWindow();
} }
#endregion #endregion

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

@ -1,15 +1,17 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Media; using System.Windows.Media;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.ViewModel; using Flow.Launcher.ViewModel;
namespace Flow.Launcher.Converters; namespace Flow.Launcher.Converters;
public class QuerySuggestionBoxConverter : IMultiValueConverter public class QuerySuggestionBoxConverter : IMultiValueConverter
{ {
private static readonly string ClassName = nameof(QuerySuggestionBoxConverter);
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{ {
// values[0] is TextBox: The textbox displaying the autocomplete suggestion // values[0] is TextBox: The textbox displaying the autocomplete suggestion
@ -43,8 +45,16 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter
// Check if Text will be larger than our QueryTextBox // Check if Text will be larger than our QueryTextBox
Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch); Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch);
// TODO: Obsolete warning? var dpi = VisualTreeHelper.GetDpi(queryTextBox);
var ft = new FormattedText(queryTextBox.Text, CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black); var ft = new FormattedText(
queryTextBox.Text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
queryTextBox.FontSize,
Brushes.Black,
dpi.PixelsPerDip
);
var offset = queryTextBox.Padding.Right; var offset = queryTextBox.Padding.Right;
@ -55,7 +65,7 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter
} }
catch (Exception e) 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; 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

@ -1,7 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Windows.Data; using System.Windows.Data;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.ViewModel; using Flow.Launcher.ViewModel;
namespace Flow.Launcher.Converters; namespace Flow.Launcher.Converters;
@ -23,7 +22,7 @@ public class TextConverter : IValueConverter
if (translationKey is null) if (translationKey is null)
return id; return id;
return InternationalizationManager.Instance.GetTranslation(translationKey); return App.API.GetTranslation(translationKey);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();

View file

@ -1,17 +1,17 @@
using Flow.Launcher.Core.Resource; using System.Collections.ObjectModel;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.UserSettings;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Controls; using System.Windows.Controls;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher namespace Flow.Launcher
{ {
public partial class CustomQueryHotkeySetting : Window public partial class CustomQueryHotkeySetting : Window
{ {
private readonly Settings _settings; private readonly Settings _settings;
private bool update; private bool update;
private CustomPluginHotkey updateCustomHotkey; private CustomPluginHotkey updateCustomHotkey;
@ -53,14 +53,13 @@ namespace Flow.Launcher
Close(); Close();
} }
public void UpdateItem(CustomPluginHotkey item) public void UpdateItem(CustomPluginHotkey item)
{ {
updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o => updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o =>
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey); o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
if (updateCustomHotkey == null) if (updateCustomHotkey == null)
{ {
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey")); App.API.ShowMsgBox(App.API.GetTranslation("invalidPluginHotkey"));
Close(); Close();
return; return;
} }
@ -68,7 +67,7 @@ namespace Flow.Launcher
tbAction.Text = updateCustomHotkey.ActionKeyword; tbAction.Text = updateCustomHotkey.ActionKeyword;
HotkeyControl.SetHotkey(updateCustomHotkey.Hotkey, false); HotkeyControl.SetHotkey(updateCustomHotkey.Hotkey, false);
update = true; update = true;
lblAdd.Text = InternationalizationManager.Instance.GetTranslation("update"); lblAdd.Text = App.API.GetTranslation("update");
} }
private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e) private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e)

View file

@ -56,11 +56,11 @@
</Button> </Button>
</Grid> </Grid>
</StackPanel> </StackPanel>
<StackPanel Margin="26,0,26,0"> <StackPanel Margin="26 0 26 0">
<StackPanel Grid.Row="0" Margin="0,0,0,12"> <StackPanel Grid.Row="0" Margin="0 0 0 12">
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
Margin="0,0,0,0" Margin="0 0 0 0"
FontSize="20" FontSize="20"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="{DynamicResource customQueryShortcut}" Text="{DynamicResource customQueryShortcut}"
@ -73,18 +73,18 @@
TextAlignment="Left" TextAlignment="Left"
TextWrapping="WrapWithOverflow" /> TextWrapping="WrapWithOverflow" />
<TextBlock <TextBlock
Margin="0,20,0,0" Margin="0 20 0 0"
FontSize="14" FontSize="14"
Text="{DynamicResource customeQueryShortcutGuide}" Text="{DynamicResource customeQueryShortcutGuide}"
TextAlignment="Left" TextAlignment="Left"
TextWrapping="WrapWithOverflow" /> TextWrapping="WrapWithOverflow" />
<Image <Image
Width="478" Width="478"
Margin="0,20,0,0" Margin="0 20 0 0"
Source="/Images/illustration_02.png" /> Source="/Images/illustration_02.png" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,10,0,10" Orientation="Horizontal"> <StackPanel Margin="0 10 0 10" Orientation="Horizontal">
<Grid Width="478"> <Grid Width="478">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />
@ -124,14 +124,14 @@
LastChildFill="True"> LastChildFill="True">
<Button <Button
x:Name="btnTestShortcut" x:Name="btnTestShortcut"
Margin="0,0,10,0" Margin="0 0 10 0"
Padding="10,5,10,5" Padding="10 5 10 5"
Click="BtnTestShortcut_OnClick" Click="BtnTestShortcut_OnClick"
Content="{DynamicResource preview}" Content="{DynamicResource preview}"
DockPanel.Dock="Right" /> DockPanel.Dock="Right" />
<TextBox <TextBox
x:Name="tbExpand" x:Name="tbExpand"
Margin="10,0,10,0" Margin="10 0 10 0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding Value}" /> Text="{Binding Value}" />
@ -142,21 +142,21 @@
</StackPanel> </StackPanel>
<Border <Border
Grid.Row="1" Grid.Row="1"
Margin="0,10,0,0" Margin="0 10 0 0"
Background="{DynamicResource PopupButtonAreaBGColor}" Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}" BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0"> BorderThickness="0 1 0 0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
MinWidth="140" MinWidth="140"
Margin="10,0,5,0" Margin="10 0 5 0"
Click="BtnCancel_OnClick" Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" /> Content="{DynamicResource cancel}" />
<Button <Button
x:Name="btnAdd" x:Name="btnAdd"
MinWidth="140" MinWidth="140"
Margin="5,0,10,0" Margin="5 0 10 0"
Click="BtnAdd_OnClick" Click="BtnAdd_OnClick"
Style="{StaticResource AccentButtonStyle}"> Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" /> <TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />

View file

@ -1,17 +1,14 @@
using Flow.Launcher.Core.Resource; using System.Windows;
using System;
using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using Flow.Launcher.SettingPages.ViewModels; using Flow.Launcher.SettingPages.ViewModels;
using Flow.Launcher.Core;
namespace Flow.Launcher namespace Flow.Launcher
{ {
public partial class CustomShortcutSetting : Window public partial class CustomShortcutSetting : Window
{ {
private readonly SettingsPaneHotkeyViewModel _hotkeyVm; private readonly SettingsPaneHotkeyViewModel _hotkeyVm;
public string Key { get; set; } = String.Empty; public string Key { get; set; } = string.Empty;
public string Value { get; set; } = String.Empty; public string Value { get; set; } = string.Empty;
private string originalKey { get; } = null; private string originalKey { get; } = null;
private string originalValue { get; } = null; private string originalValue { get; } = null;
private bool update { get; } = false; private bool update { get; } = false;
@ -41,15 +38,15 @@ namespace Flow.Launcher
private void BtnAdd_OnClick(object sender, RoutedEventArgs e) private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
{ {
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value)) if (string.IsNullOrEmpty(Key) || string.IsNullOrEmpty(Value))
{ {
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("emptyShortcut")); App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut"));
return; return;
} }
// Check if key is modified or adding a new one // Check if key is modified or adding a new one
if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key)) if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key))
{ {
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("duplicateShortcut")); App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut"));
return; return;
} }
DialogResult = !update || originalKey != Key || originalValue != Value; DialogResult = !update || originalKey != Key || originalValue != Value;

View file

@ -98,7 +98,9 @@
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 --> <!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
<PackageReference Include="ModernWpfUI" Version="0.9.4" /> <PackageReference Include="ModernWpfUI" Version="0.9.4" />
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" /> <PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" /> <PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="SemanticVersioning" Version="3.0.0" /> <PackageReference Include="SemanticVersioning" Version="3.0.0" />
<PackageReference Include="TaskScheduler" Version="2.12.1" /> <PackageReference Include="TaskScheduler" Version="2.12.1" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" /> <PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" />

View file

@ -1,43 +1,53 @@
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Security.Principal; using System.Security.Principal;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Microsoft.Win32; using Microsoft.Win32;
using Microsoft.Win32.TaskScheduler; using Microsoft.Win32.TaskScheduler;
#nullable enable
namespace Flow.Launcher.Helper; namespace Flow.Launcher.Helper;
public class AutoStartup public class AutoStartup
{ {
private static readonly string ClassName = nameof(AutoStartup);
private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
private const string LogonTaskName = $"{Constant.FlowLauncher} Startup"; private const string LogonTaskName = $"{Constant.FlowLauncher} Startup";
private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup"; private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup";
public static bool IsEnabled public static void CheckIsEnabled(bool useLogonTaskForStartup)
{ {
get // We need to check both because if both of them are enabled,
// Hide Flow Launcher on startup will not work since the later one will trigger main window show event
var logonTaskEnabled = CheckLogonTask();
var registryEnabled = CheckRegistry();
if (useLogonTaskForStartup)
{ {
// Check if logon task is enabled // Enable logon task
if (CheckLogonTask()) if (!logonTaskEnabled)
{ {
return true; Enable(true);
} }
// Disable registry
// Check if registry is enabled if (registryEnabled)
try
{ {
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true); Disable(false);
var path = key?.GetValue(Constant.FlowLauncher) as string;
return path == Constant.ExecutablePath;
} }
catch (Exception e) }
else
{
// Enable registry
if (!registryEnabled)
{ {
Log.Error("AutoStartup", $"Ignoring non-critical registry error (querying if enabled): {e}"); Enable(false);
}
// Disable logon task
if (logonTaskEnabled)
{
Disable(true);
} }
return false;
} }
} }
@ -50,40 +60,63 @@ public class AutoStartup
try try
{ {
// Check if the action is the same as the current executable path // Check if the action is the same as the current executable path
var action = task.Definition.Actions.FirstOrDefault()!.ToString().Trim(); // If not, we need to unschedule and reschedule the task
if (!Constant.ExecutablePath.Equals(action, StringComparison.OrdinalIgnoreCase) && !File.Exists(action)) if (task.Definition.Actions.FirstOrDefault() is Microsoft.Win32.TaskScheduler.Action taskAction)
{ {
UnscheduleLogonTask(); var action = taskAction.ToString().Trim();
ScheduleLogonTask(); if (!action.Equals(Constant.ExecutablePath, StringComparison.OrdinalIgnoreCase))
{
UnscheduleLogonTask();
ScheduleLogonTask();
}
} }
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error("AutoStartup", $"Failed to check logon task: {e}"); App.API.LogError(ClassName, $"Failed to check logon task: {e}");
throw; // Throw exception so that App.AutoStartup can show error message
} }
} }
return false; return false;
} }
private static bool CheckRegistry()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
if (key != null)
{
// Check if the action is the same as the current executable path
// If not, we need to unschedule and reschedule the task
var action = (key.GetValue(Constant.FlowLauncher) as string) ?? string.Empty;
if (!action.Equals(Constant.ExecutablePath, StringComparison.OrdinalIgnoreCase))
{
UnscheduleRegistry();
ScheduleRegistry();
}
return true;
}
return false;
}
catch (Exception e)
{
App.API.LogError(ClassName, $"Failed to check registry: {e}");
throw; // Throw exception so that App.AutoStartup can show error message
}
}
public static void DisableViaLogonTaskAndRegistry() public static void DisableViaLogonTaskAndRegistry()
{ {
Disable(true); Disable(true);
Disable(false); Disable(false);
} }
public static void EnableViaLogonTask()
{
Enable(true);
}
public static void EnableViaRegistry()
{
Enable(false);
}
public static void ChangeToViaLogonTask() public static void ChangeToViaLogonTask()
{ {
Disable(false); Disable(false);
@ -106,13 +139,12 @@ public class AutoStartup
} }
else else
{ {
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true); UnscheduleRegistry();
key?.DeleteValue(Constant.FlowLauncher, false);
} }
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error("AutoStartup", $"Failed to disable auto-startup: {e}"); App.API.LogError(ClassName, $"Failed to disable auto-startup: {e}");
throw; throw;
} }
} }
@ -127,13 +159,12 @@ public class AutoStartup
} }
else else
{ {
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true); ScheduleRegistry();
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
} }
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error("AutoStartup", $"Failed to enable auto-startup: {e}"); App.API.LogError(ClassName, $"Failed to enable auto-startup: {e}");
throw; throw;
} }
} }
@ -161,7 +192,7 @@ public class AutoStartup
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error("AutoStartup", $"Failed to schedule logon task: {e}"); App.API.LogError(ClassName, $"Failed to schedule logon task: {e}");
return false; return false;
} }
} }
@ -176,7 +207,7 @@ public class AutoStartup
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error("AutoStartup", $"Failed to unschedule logon task: {e}"); App.API.LogError(ClassName, $"Failed to unschedule logon task: {e}");
return false; return false;
} }
} }
@ -187,4 +218,18 @@ public class AutoStartup
var principal = new WindowsPrincipal(identity); var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator); return principal.IsInRole(WindowsBuiltInRole.Administrator);
} }
private static bool UnscheduleRegistry()
{
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
key?.DeleteValue(Constant.FlowLauncher, false);
return true;
}
private static bool ScheduleRegistry()
{
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
return true;
}
} }

View file

@ -28,18 +28,18 @@ public class DataWebRequestFactory : IWebRequestCreate
public DataWebResponse(Uri uri) public DataWebResponse(Uri uri)
{ {
string uriString = uri.AbsoluteUri; var uriString = uri.AbsoluteUri;
int commaIndex = uriString.IndexOf(','); var commaIndex = uriString.IndexOf(',');
var headers = uriString.Substring(0, commaIndex).Split(';'); var headers = uriString[..commaIndex].Split(';');
_contentType = headers[0]; _contentType = headers[0];
string dataString = uriString.Substring(commaIndex + 1); var dataString = uriString[(commaIndex + 1)..];
_data = Convert.FromBase64String(dataString); _data = Convert.FromBase64String(dataString);
} }
public override string ContentType public override string ContentType
{ {
get { return _contentType; } get => _contentType;
set set
{ {
throw new NotSupportedException(); throw new NotSupportedException();
@ -48,7 +48,7 @@ public class DataWebRequestFactory : IWebRequestCreate
public override long ContentLength public override long ContentLength
{ {
get { return _data.Length; } get => _data.Length;
set set
{ {
throw new NotSupportedException(); throw new NotSupportedException();

View file

@ -1,35 +1,46 @@
using System; using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
using NLog;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Exception; using Flow.Launcher.Infrastructure.Exception;
using NLog;
namespace Flow.Launcher.Helper; namespace Flow.Launcher.Helper;
public static class ErrorReporting public static class ErrorReporting
{ {
private static void Report(Exception e) private static void Report(Exception e, [CallerMemberName] string methodName = "UnHandledException")
{ {
var logger = LogManager.GetLogger("UnHandledException"); var logger = LogManager.GetLogger(methodName);
logger.Fatal(ExceptionFormatter.FormatExcpetion(e)); logger.Fatal(ExceptionFormatter.FormatExcpetion(e));
var reportWindow = new ReportWindow(e); var reportWindow = new ReportWindow(e);
reportWindow.Show(); reportWindow.Show();
} }
public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e) public static void UnhandledException(object sender, UnhandledExceptionEventArgs e)
{ {
//handle non-ui thread exceptions // handle non-ui thread exceptions
Report((Exception)e.ExceptionObject); Report((Exception)e.ExceptionObject);
} }
public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{ {
//handle ui thread exceptions // handle ui thread exceptions
Report(e.Exception); Report(e.Exception);
//prevent application exist, so the user can copy prompted error info // prevent application exist, so the user can copy prompted error info
e.Handled = true; e.Handled = true;
} }
public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
// handle unobserved task exceptions on UI thread
Application.Current.Dispatcher.Invoke(() => Report(e.Exception));
// prevent application exit, so the user can copy the prompted error info
e.SetObserved();
}
public static string RuntimeInfo() public static string RuntimeInfo()
{ {
var info = var info =

View file

@ -3,16 +3,16 @@ using Flow.Launcher.Infrastructure.UserSettings;
using System; using System;
using NHotkey; using NHotkey;
using NHotkey.Wpf; using NHotkey.Wpf;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.ViewModel; using Flow.Launcher.ViewModel;
using ChefKeys; using ChefKeys;
using Flow.Launcher.Infrastructure.Logger;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Helper; namespace Flow.Launcher.Helper;
internal static class HotKeyMapper internal static class HotKeyMapper
{ {
private static readonly string ClassName = nameof(HotKeyMapper);
private static Settings _settings; private static Settings _settings;
private static MainViewModel _mainViewModel; private static MainViewModel _mainViewModel;
@ -52,13 +52,13 @@ internal static class HotKeyMapper
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error( App.API.LogError(ClassName,
string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}", string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
e.Message, e.Message,
e.StackTrace)); e.StackTrace));
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle"); string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
MessageBoxEx.Show(errorMsg, errorMsgTitle); App.API.ShowMsgBox(errorMsg, errorMsgTitle);
} }
} }
@ -77,13 +77,13 @@ internal static class HotKeyMapper
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error( App.API.LogError(ClassName,
string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}", string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}",
e.Message, e.Message,
e.StackTrace, e.StackTrace,
hotkeyStr)); hotkeyStr));
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle"); string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
App.API.ShowMsgBox(errorMsg, errorMsgTitle); App.API.ShowMsgBox(errorMsg, errorMsgTitle);
} }
} }
@ -103,13 +103,13 @@ internal static class HotKeyMapper
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error( App.API.LogError(ClassName,
string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}", string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}",
e.Message, e.Message,
e.StackTrace)); e.StackTrace));
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("unregisterHotkeyFailed"), hotkeyStr); string errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr);
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle"); string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
MessageBoxEx.Show(errorMsg, errorMsgTitle); App.API.ShowMsgBox(errorMsg, errorMsgTitle);
} }
} }
@ -137,8 +137,8 @@ internal static class HotKeyMapper
if (_mainViewModel.ShouldIgnoreHotkeys()) if (_mainViewModel.ShouldIgnoreHotkeys())
return; return;
_mainViewModel.Show(); App.API.ShowMainWindow();
_mainViewModel.ChangeQueryText(hotkey.ActionKeyword, true); App.API.ChangeQuery(hotkey.ActionKeyword, true);
}); });
} }

View file

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

View file

@ -7,15 +7,15 @@
mc:Ignorable="d"> mc:Ignorable="d">
<Button <Button
Width="Auto" Width="Auto"
Click="GetNewHotkey"
FontSize="13" FontSize="13"
FontWeight="Bold" FontWeight="Bold"
Foreground="{DynamicResource Color01B}" Foreground="{DynamicResource Color01B}">
Click="GetNewHotkey">
<Button.Template> <Button.Template>
<ControlTemplate TargetType="Button"> <ControlTemplate TargetType="Button">
<Border <Border
x:Name="ButtonBorder" x:Name="ButtonBorder"
Padding="5,0,5,0" Padding="5 0 5 0"
Background="{DynamicResource ButtonBackgroundColor}" Background="{DynamicResource ButtonBackgroundColor}"
BorderBrush="{DynamicResource ButtonInsideBorder}" BorderBrush="{DynamicResource ButtonInsideBorder}"
BorderThickness="1" BorderThickness="1"
@ -28,26 +28,21 @@
<Condition Property="IsMouseOver" Value="True" /> <Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsPressed" Value="True" /> <Condition Property="IsPressed" Value="True" />
</MultiTrigger.Conditions> </MultiTrigger.Conditions>
<Setter TargetName="ButtonBorder" Property="Background" <Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource ButtonMousePressed}" />
Value="{DynamicResource ButtonMousePressed}" /> <Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{DynamicResource ButtonMousePressedInsideBorder}" />
<Setter TargetName="ButtonBorder" Property="BorderBrush"
Value="{DynamicResource ButtonMousePressedInsideBorder}" />
</MultiTrigger> </MultiTrigger>
<MultiTrigger> <MultiTrigger>
<MultiTrigger.Conditions> <MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" /> <Condition Property="IsMouseOver" Value="True" />
</MultiTrigger.Conditions> </MultiTrigger.Conditions>
<Setter TargetName="ButtonBorder" Property="Background" <Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource ButtonMouseOver}" />
Value="{DynamicResource ButtonMouseOver}" />
</MultiTrigger> </MultiTrigger>
<MultiTrigger> <MultiTrigger>
<MultiTrigger.Conditions> <MultiTrigger.Conditions>
<Condition Property="IsPressed" Value="True" /> <Condition Property="IsPressed" Value="True" />
</MultiTrigger.Conditions> </MultiTrigger.Conditions>
<Setter TargetName="ButtonBorder" Property="Background" <Setter TargetName="ButtonBorder" Property="Background" Value="{DynamicResource ButtonMousePressed}" />
Value="{DynamicResource ButtonMousePressed}" /> <Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{DynamicResource CustomContextHover}" />
<Setter TargetName="ButtonBorder" Property="BorderBrush"
Value="{DynamicResource CustomContextHover}" />
</MultiTrigger> </MultiTrigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
@ -62,8 +57,8 @@
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border <Border
Margin="2,5,2,5" Margin="2 5 2 5"
Padding="10,5,10,5" Padding="10 5 10 5"
Background="{DynamicResource AccentButtonBackground}" Background="{DynamicResource AccentButtonBackground}"
BorderThickness="1" BorderThickness="1"
CornerRadius="5"> CornerRadius="5">

View file

@ -1,15 +1,14 @@
#nullable enable using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper; using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
#nullable enable
namespace Flow.Launcher namespace Flow.Launcher
{ {
public partial class HotkeyControl public partial class HotkeyControl
@ -65,7 +64,6 @@ namespace Flow.Launcher
hotkeyControl.RefreshHotkeyInterface(hotkeyControl.Hotkey); hotkeyControl.RefreshHotkeyInterface(hotkeyControl.Hotkey);
} }
public static readonly DependencyProperty ChangeHotkeyProperty = DependencyProperty.Register( public static readonly DependencyProperty ChangeHotkeyProperty = DependencyProperty.Register(
nameof(ChangeHotkey), nameof(ChangeHotkey),
typeof(ICommand), typeof(ICommand),
@ -79,7 +77,6 @@ namespace Flow.Launcher
set { SetValue(ChangeHotkeyProperty, value); } set { SetValue(ChangeHotkeyProperty, value); }
} }
public static readonly DependencyProperty TypeProperty = DependencyProperty.Register( public static readonly DependencyProperty TypeProperty = DependencyProperty.Register(
nameof(Type), nameof(Type),
typeof(HotkeyType), typeof(HotkeyType),
@ -227,26 +224,29 @@ namespace Flow.Launcher
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
public string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none"); public string EmptyHotkey => App.API.GetTranslation("none");
public ObservableCollection<string> KeysToDisplay { get; set; } = new(); public ObservableCollection<string> KeysToDisplay { get; set; } = new();
public HotkeyModel CurrentHotkey { get; private set; } = new(false, false, false, false, Key.None); public HotkeyModel CurrentHotkey { get; private set; } = new(false, false, false, false, Key.None);
public void GetNewHotkey(object sender, RoutedEventArgs e) public void GetNewHotkey(object sender, RoutedEventArgs e)
{ {
OpenHotkeyDialog(); _ = OpenHotkeyDialogAsync();
} }
private async Task OpenHotkeyDialog() private async Task OpenHotkeyDialogAsync()
{ {
if (!string.IsNullOrEmpty(Hotkey)) if (!string.IsNullOrEmpty(Hotkey))
{ {
HotKeyMapper.RemoveHotkey(Hotkey); HotKeyMapper.RemoveHotkey(Hotkey);
} }
var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle); var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle)
{
Owner = Window.GetWindow(this)
};
await dialog.ShowAsync(); await dialog.ShowAsync();
switch (dialog.ResultType) switch (dialog.ResultType)
{ {
@ -262,12 +262,11 @@ namespace Flow.Launcher
} }
} }
private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true) private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true)
{ {
if (triggerValidate) if (triggerValidate)
{ {
bool hotkeyAvailable = false; bool hotkeyAvailable;
// TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157 // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157
if (keyModel.ToString() == "LWin" || keyModel.ToString() == "RWin") if (keyModel.ToString() == "LWin" || keyModel.ToString() == "RWin")
{ {

View file

@ -5,7 +5,7 @@
xmlns:ui="http://schemas.modernwpf.com/2019" xmlns:ui="http://schemas.modernwpf.com/2019"
Background="{DynamicResource PopuBGColor}" Background="{DynamicResource PopuBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}" BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0" BorderThickness="0 1 0 0"
CornerRadius="8" CornerRadius="8"
DataContext="{Binding RelativeSource={RelativeSource Self}}" DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}" Foreground="{DynamicResource PopupTextColor}"
@ -24,14 +24,14 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Window title and the keys in the hotkey --> <!-- Window title and the keys in the hotkey -->
<Grid Grid.Row="0" Margin="26,12,26,0"> <Grid Grid.Row="0" Margin="26 12 26 0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel> <StackPanel>
<TextBlock <TextBlock
Margin="0,0,0,0" Margin="0 0 0 0"
FontSize="20" FontSize="20"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="{Binding WindowTitle}" Text="{Binding WindowTitle}"
@ -42,8 +42,8 @@
Grid.Row="1" Grid.Row="1"
Width="450" Width="450"
Height="100" Height="100"
Margin="0,100,0,0" Margin="0 100 0 0"
Padding="26,12,26,0"> Padding="26 12 26 0">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<ItemsControl ItemsSource="{Binding KeysToDisplay}"> <ItemsControl ItemsSource="{Binding KeysToDisplay}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
@ -56,12 +56,12 @@
<Border <Border
MinWidth="50" MinWidth="50"
MinHeight="50" MinHeight="50"
Margin="5,0,5,0" Margin="5 0 5 0"
Padding="8" Padding="8"
Background="{DynamicResource AccentButtonBackground}" Background="{DynamicResource AccentButtonBackground}"
CornerRadius="6"> CornerRadius="6">
<TextBlock <TextBlock
Margin="5,0,5,0" Margin="5 0 5 0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="18" FontSize="18"
@ -82,9 +82,9 @@
<Border <Border
x:Name="Alert" x:Name="Alert"
Width="420" Width="420"
Padding="0, 10" Padding="0 10"
VerticalAlignment="Center"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource InfoBarWarningBG}" Background="{DynamicResource InfoBarWarningBG}"
BorderBrush="{DynamicResource InfoBarBD}" BorderBrush="{DynamicResource InfoBarBD}"
BorderThickness="1" BorderThickness="1"
@ -97,21 +97,21 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ui:FontIcon <ui:FontIcon
Grid.Column="0" Grid.Column="0"
Margin="20,0,14,0" Margin="20 0 14 0"
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="15" FontSize="15"
Foreground="{DynamicResource InfoBarWarningIcon}" Foreground="{DynamicResource InfoBarWarningIcon}"
Glyph="&#xf167;" /> Glyph="&#xf167;" />
<TextBlock <TextBlock
Grid.Column="1"
x:Name="tbMsg" x:Name="tbMsg"
Margin="0,0,0,2" Grid.Column="1"
Padding="0,0,8,0" Margin="0 0 0 2"
Padding="0 0 8 0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
FontSize="13" FontSize="13"
FontWeight="SemiBold" FontWeight="SemiBold"
TextWrapping="Wrap" Foreground="{DynamicResource Color05B}"
Foreground="{DynamicResource Color05B}" /> TextWrapping="Wrap" />
</Grid> </Grid>
</Border> </Border>
@ -122,7 +122,7 @@
Grid.Row="2" Grid.Row="2"
Background="{DynamicResource PopupButtonAreaBGColor}" Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}" BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0" BorderThickness="0 1 0 0"
CornerRadius="0 0 8 8"> CornerRadius="0 0 8 8">
<StackPanel <StackPanel
Margin="10" Margin="10"
@ -132,35 +132,35 @@
x:Name="OverwriteBtn" x:Name="OverwriteBtn"
Height="30" Height="30"
MinWidth="100" MinWidth="100"
Margin="0,0,4,0" Margin="0 0 4 0"
Click="Overwrite" Click="Overwrite"
Content="{DynamicResource commonOverwrite}" Content="{DynamicResource commonOverwrite}"
Visibility="Collapsed" Style="{StaticResource AccentButtonStyle}"
Style="{StaticResource AccentButtonStyle}" /> Visibility="Collapsed" />
<Button <Button
x:Name="SaveBtn" x:Name="SaveBtn"
Height="30" Height="30"
MinWidth="100" MinWidth="100"
Margin="0,0,4,0" Margin="0 0 4 0"
Click="Save" Click="Save"
Content="{DynamicResource commonSave}" Content="{DynamicResource commonSave}"
Style="{StaticResource AccentButtonStyle}" /> Style="{StaticResource AccentButtonStyle}" />
<Button <Button
Height="30" Height="30"
MinWidth="100" MinWidth="100"
Margin="4,0,4,0" Margin="4 0 4 0"
Click="Reset" Click="Reset"
Content="{DynamicResource commonReset}" /> Content="{DynamicResource commonReset}" />
<Button <Button
Height="30" Height="30"
MinWidth="100" MinWidth="100"
Margin="4,0,4,0" Margin="4 0 4 0"
Click="Delete" Click="Delete"
Content="{DynamicResource commonDelete}" /> Content="{DynamicResource commonDelete}" />
<Button <Button
Height="30" Height="30"
MinWidth="100" MinWidth="100"
Margin="4,0,0,0" Margin="4 0 0 0"
Click="Cancel" Click="Cancel"
Content="{DynamicResource commonCancel}" /> Content="{DynamicResource commonCancel}" />
</StackPanel> </StackPanel>

View file

@ -5,7 +5,6 @@ using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using ChefKeys; using ChefKeys;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper; using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
@ -34,7 +33,7 @@ public partial class HotkeyControlDialog : ContentDialog
public EResultType ResultType { get; private set; } = EResultType.Cancel; public EResultType ResultType { get; private set; } = EResultType.Cancel;
public string ResultValue { get; private set; } = string.Empty; public string ResultValue { get; private set; } = string.Empty;
public static string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none"); public static string EmptyHotkey => App.API.GetTranslation("none");
private static bool isOpenFlowHotkey; private static bool isOpenFlowHotkey;
@ -42,7 +41,7 @@ public partial class HotkeyControlDialog : ContentDialog
{ {
WindowTitle = windowTitle switch WindowTitle = windowTitle switch
{ {
"" or null => InternationalizationManager.Instance.GetTranslation("hotkeyRegTitle"), "" or null => App.API.GetTranslation("hotkeyRegTitle"),
_ => windowTitle _ => windowTitle
}; };
DefaultHotkey = defaultHotkey; DefaultHotkey = defaultHotkey;
@ -141,14 +140,14 @@ public partial class HotkeyControlDialog : ContentDialog
if (_hotkeySettings.RegisteredHotkeys.FirstOrDefault(v => v.Hotkey == hotkey) is { } registeredHotkeyData) if (_hotkeySettings.RegisteredHotkeys.FirstOrDefault(v => v.Hotkey == hotkey) is { } registeredHotkeyData)
{ {
var description = string.Format( var description = string.Format(
InternationalizationManager.Instance.GetTranslation(registeredHotkeyData.DescriptionResourceKey), App.API.GetTranslation(registeredHotkeyData.DescriptionResourceKey),
registeredHotkeyData.DescriptionFormatVariables registeredHotkeyData.DescriptionFormatVariables
); );
Alert.Visibility = Visibility.Visible; Alert.Visibility = Visibility.Visible;
if (registeredHotkeyData.RemoveHotkey is not null) if (registeredHotkeyData.RemoveHotkey is not null)
{ {
tbMsg.Text = string.Format( tbMsg.Text = string.Format(
InternationalizationManager.Instance.GetTranslation("hotkeyUnavailableEditable"), App.API.GetTranslation("hotkeyUnavailableEditable"),
description description
); );
SaveBtn.IsEnabled = false; SaveBtn.IsEnabled = false;
@ -160,7 +159,7 @@ public partial class HotkeyControlDialog : ContentDialog
else else
{ {
tbMsg.Text = string.Format( tbMsg.Text = string.Format(
InternationalizationManager.Instance.GetTranslation("hotkeyUnavailableUneditable"), App.API.GetTranslation("hotkeyUnavailableUneditable"),
description description
); );
SaveBtn.IsEnabled = false; SaveBtn.IsEnabled = false;
@ -176,7 +175,7 @@ public partial class HotkeyControlDialog : ContentDialog
if (!CheckHotkeyAvailability(hotkey.Value, true)) if (!CheckHotkeyAvailability(hotkey.Value, true))
{ {
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable"); tbMsg.Text = App.API.GetTranslation("hotkeyUnavailable");
Alert.Visibility = Visibility.Visible; Alert.Visibility = Visibility.Visible;
SaveBtn.IsEnabled = false; SaveBtn.IsEnabled = false;
SaveBtn.Visibility = Visibility.Visible; SaveBtn.Visibility = Visibility.Visible;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -7,6 +7,11 @@
انقر فوق لا إذا كان مثبتاً بالفعل، وسوف يطلب منك تحديد المجلد الذي يحتوي على {1} القابل للتنفيذ انقر فوق لا إذا كان مثبتاً بالفعل، وسوف يطلب منك تحديد المجلد الذي يحتوي على {1} القابل للتنفيذ
</system:String> </system:String>
<system:String x:Key="runtimePluginChooseRuntimeExecutable">الرجاء اختيار الملف التنفيذي لـ {0}</system:String> <system:String x:Key="runtimePluginChooseRuntimeExecutable">الرجاء اختيار الملف التنفيذي لـ {0}</system:String>
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">تعذر تعيين مسار الملف التنفيذي لـ {0}، يرجى المحاولة من إعدادات Flow (قم بالتمرير إلى الأسفل).</system:String> <system:String x:Key="runtimePluginUnableToSetExecutablePath">تعذر تعيين مسار الملف التنفيذي لـ {0}، يرجى المحاولة من إعدادات Flow (قم بالتمرير إلى الأسفل).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">فشل في تهيئة الإضافات</system:String> <system:String x:Key="failedToInitializePluginsTitle">فشل في تهيئة الإضافات</system:String>
<system:String x:Key="failedToInitializePluginsMessage">الإضافات: {0} - فشل في التحميل وسيتم تعطيلها، يرجى الاتصال بمطور الإضافة للحصول على المساعدة</system:String> <system:String x:Key="failedToInitializePluginsMessage">الإضافات: {0} - فشل في التحميل وسيتم تعطيلها، يرجى الاتصال بمطور الإضافة للحصول على المساعدة</system:String>
@ -37,7 +42,7 @@
<system:String x:Key="GameMode">وضع اللعب</system:String> <system:String x:Key="GameMode">وضع اللعب</system:String>
<system:String x:Key="GameModeToolTip">تعليق استخدام مفاتيح التشغيل السريع.</system:String> <system:String x:Key="GameModeToolTip">تعليق استخدام مفاتيح التشغيل السريع.</system:String>
<system:String x:Key="PositionReset">إعادة تعيين الموقع</system:String> <system:String x:Key="PositionReset">إعادة تعيين الموقع</system:String>
<system:String x:Key="PositionResetToolTip">إعادة تعيين موضع نافذة البحث</system:String> <system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General --> <!-- Setting General -->
<system:String x:Key="flowlauncher_settings">الإعدادات</system:String> <system:String x:Key="flowlauncher_settings">الإعدادات</system:String>
@ -70,8 +75,6 @@
<system:String x:Key="LastQueryEmpty">تفريغ الاستعلام الأخير</system:String> <system:String x:Key="LastQueryEmpty">تفريغ الاستعلام الأخير</system:String>
<system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String> <system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String>
<system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String> <system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String>
<system:String x:Key="KeepMaxResults">ارتفاع ثابت للنافذة</system:String>
<system:String x:Key="KeepMaxResultsToolTip">ارتفاع النافذة غير قابل للتعديل عن طريق السحب.</system:String>
<system:String x:Key="maxShowResults">الحد الأقصى للنتائج المعروضة</system:String> <system:String x:Key="maxShowResults">الحد الأقصى للنتائج المعروضة</system:String>
<system:String x:Key="maxShowResultsToolTip">يمكنك أيضًا تعديل هذا بسرعة باستخدام CTRL+Plus وCTRL+Minus.</system:String> <system:String x:Key="maxShowResultsToolTip">يمكنك أيضًا تعديل هذا بسرعة باستخدام CTRL+Plus وCTRL+Minus.</system:String>
<system:String x:Key="ignoreHotkeysOnFullscreen">تجاهل مفاتيح التشغيل السريع في وضع ملء الشاشة</system:String> <system:String x:Key="ignoreHotkeysOnFullscreen">تجاهل مفاتيح التشغيل السريع في وضع ملء الشاشة</system:String>
@ -102,6 +105,15 @@
<system:String x:Key="AlwaysPreview">دائمًا معاينة</system:String> <system:String x:Key="AlwaysPreview">دائمًا معاينة</system:String>
<system:String x:Key="AlwaysPreviewToolTip">فتح لوحة المعاينة دائمًا عند تنشيط Flow. اضغط على {0} للتبديل بين المعاينة وعدمها.</system:String> <system:String x:Key="AlwaysPreviewToolTip">فتح لوحة المعاينة دائمًا عند تنشيط Flow. اضغط على {0} للتبديل بين المعاينة وعدمها.</system:String>
<system:String x:Key="shadowEffectNotAllowed">تأثير الظل غير مسموح به بينما يتم تمكين تأثير التمويه في السمة الحالية</system:String> <system:String x:Key="shadowEffectNotAllowed">تأثير الظل غير مسموح به بينما يتم تمكين تأثير التمويه في السمة الحالية</system:String>
<system:String x:Key="searchDelay">Search Delay</system:String>
<system:String x:Key="searchDelayToolTip">Delay for a while to search when typing. This reduces interface jumpiness and result load.</system:String>
<system:String x:Key="searchDelayTime">Default Search Delay Time</system:String>
<system:String x:Key="searchDelayTimeToolTip">Plugin default delay time after which search results appear when typing is stopped.</system:String>
<system:String x:Key="SearchDelayTimeVeryLong">Very long</system:String>
<system:String x:Key="SearchDelayTimeLong">Long</system:String>
<system:String x:Key="SearchDelayTimeNormal">Normal</system:String>
<system:String x:Key="SearchDelayTimeShort">Short</system:String>
<system:String x:Key="SearchDelayTimeVeryShort">Very short</system:String>
<!-- Setting Plugin --> <!-- Setting Plugin -->
<system:String x:Key="searchplugin">البحث عن إضافة</system:String> <system:String x:Key="searchplugin">البحث عن إضافة</system:String>
@ -118,6 +130,8 @@
<system:String x:Key="currentActionKeywords">كلمة الفعل الحالية</system:String> <system:String x:Key="currentActionKeywords">كلمة الفعل الحالية</system:String>
<system:String x:Key="newActionKeyword">كلمة فعل جديدة</system:String> <system:String x:Key="newActionKeyword">كلمة فعل جديدة</system:String>
<system:String x:Key="actionKeywordsTooltip">تغيير كلمات الفعل</system:String> <system:String x:Key="actionKeywordsTooltip">تغيير كلمات الفعل</system:String>
<system:String x:Key="pluginSearchDelayTime">Plugin seach delay time</system:String>
<system:String x:Key="pluginSearchDelayTimeTooltip">Change Plugin Seach Delay Time</system:String>
<system:String x:Key="currentPriority">الأولوية الحالية</system:String> <system:String x:Key="currentPriority">الأولوية الحالية</system:String>
<system:String x:Key="newPriority">أولوية جديدة</system:String> <system:String x:Key="newPriority">أولوية جديدة</system:String>
<system:String x:Key="priority">الأولوية</system:String> <system:String x:Key="priority">الأولوية</system:String>
@ -131,6 +145,9 @@
<system:String x:Key="plugin_uninstall">إلغاء التثبيت</system:String> <system:String x:Key="plugin_uninstall">إلغاء التثبيت</system:String>
<system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String> <system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String>
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String> <system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="default">Default</system:String>
<!-- Setting Plugin Store --> <!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">متجر الإضافات</system:String> <system:String x:Key="pluginStore">متجر الإضافات</system:String>
@ -193,8 +210,20 @@
<system:String x:Key="AnimationSpeedCustom">مخصص</system:String> <system:String x:Key="AnimationSpeedCustom">مخصص</system:String>
<system:String x:Key="Clock">الساعة</system:String> <system:String x:Key="Clock">الساعة</system:String>
<system:String x:Key="Date">التاريخ</system:String> <system:String x:Key="Date">التاريخ</system:String>
<system:String x:Key="BackdropType">Backdrop Type</system:String>
<system:String x:Key="BackdropTypeDisabledToolTip">Backdrop supported starting from Windows 11 build 22000 and above</system:String>
<system:String x:Key="BackdropTypesNone">بلا</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<system:String x:Key="TypeIsDarkToolTip">هذه السمة تدعم الوضعين (فاتح/داكن).</system:String> <system:String x:Key="TypeIsDarkToolTip">هذه السمة تدعم الوضعين (فاتح/داكن).</system:String>
<system:String x:Key="TypeHasBlurToolTip">هذه السمة تدعم الخلفية الضبابية الشفافة.</system:String> <system:String x:Key="TypeHasBlurToolTip">هذه السمة تدعم الخلفية الضبابية الشفافة.</system:String>
<system:String x:Key="ShowPlaceholder">Show placeholder</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
<system:String x:Key="PlaceholderText">Placeholder text</system:String>
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<!-- Setting Hotkey --> <!-- Setting Hotkey -->
<system:String x:Key="hotkey">مفتاح الاختصار</system:String> <system:String x:Key="hotkey">مفتاح الاختصار</system:String>
@ -294,6 +323,9 @@
<system:String x:Key="logfolder">مجلد السجلات</system:String> <system:String x:Key="logfolder">مجلد السجلات</system:String>
<system:String x:Key="clearlogfolder">مسح السجلات</system:String> <system:String x:Key="clearlogfolder">مسح السجلات</system:String>
<system:String x:Key="clearlogfolderMessage">هل أنت متأكد أنك تريد حذف جميع السجلات؟</system:String> <system:String x:Key="clearlogfolderMessage">هل أنت متأكد أنك تريد حذف جميع السجلات؟</system:String>
<system:String x:Key="clearcachefolder">Clear Caches</system:String>
<system:String x:Key="clearcachefolderMessage">Are you sure you want to delete all caches?</system:String>
<system:String x:Key="clearfolderfailMessage">Failed to clear part of folders and files. Please see log file for more information</system:String>
<system:String x:Key="welcomewindow">معالج الترحيب</system:String> <system:String x:Key="welcomewindow">معالج الترحيب</system:String>
<system:String x:Key="userdatapath">موقع بيانات المستخدم</system:String> <system:String x:Key="userdatapath">موقع بيانات المستخدم</system:String>
<system:String x:Key="userdatapathToolTip">يتم حفظ إعدادات المستخدم والإضافات المثبتة في مجلد بيانات المستخدم. قد يختلف هذا الموقع اعتمادًا على ما إذا كان في وضع النقل أم لا.</system:String> <system:String x:Key="userdatapathToolTip">يتم حفظ إعدادات المستخدم والإضافات المثبتة في مجلد بيانات المستخدم. قد يختلف هذا الموقع اعتمادًا على ما إذا كان في وضع النقل أم لا.</system:String>
@ -335,9 +367,16 @@
<system:String x:Key="cannotFindSpecifiedPlugin">لا يمكن العثور على الإضافة المحددة</system:String> <system:String x:Key="cannotFindSpecifiedPlugin">لا يمكن العثور على الإضافة المحددة</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">كلمة المفتاح الجديدة لا يمكن أن تكون فارغة</system:String> <system:String x:Key="newActionKeywordsCannotBeEmpty">كلمة المفتاح الجديدة لا يمكن أن تكون فارغة</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">تم تعيين كلمة المفتاح الجديدة هذه إلى إضافة أخرى، يرجى اختيار كلمة أخرى</system:String> <system:String x:Key="newActionKeywordsHasBeenAssigned">تم تعيين كلمة المفتاح الجديدة هذه إلى إضافة أخرى، يرجى اختيار كلمة أخرى</system:String>
<system:String x:Key="newActionKeywordsSameAsOld">This new Action Keyword is the same as old, please choose a different one</system:String>
<system:String x:Key="success">نجاح</system:String> <system:String x:Key="success">نجاح</system:String>
<system:String x:Key="completedSuccessfully">اكتمل بنجاح</system:String> <system:String x:Key="completedSuccessfully">اكتمل بنجاح</system:String>
<system:String x:Key="actionkeyword_tips">أدخل كلمة المفتاح التي ترغب في استخدامها لبدء الإضافة. استخدم * إذا كنت لا ترغب في تحديد أي كلمة، وسيتم تشغيل الإضافة بدون كلمات مفتاحية.</system:String> <system:String x:Key="actionkeyword_tips">Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>
<!-- Search Delay Settings Dialog -->
<system:String x:Key="searchDelayTimeTitle">Search Delay Time Setting</system:String>
<system:String x:Key="searchDelayTime_tips">Select the search delay time you like to use for the plugin. Select &quot;{0}&quot; if you don't want to specify any, and the plugin will use default search delay time.</system:String>
<system:String x:Key="currentSearchDelayTime">Current search delay time</system:String>
<system:String x:Key="newSearchDelayTime">New search delay time</system:String>
<!-- Custom Query Hotkey Dialog --> <!-- Custom Query Hotkey Dialog -->
<system:String x:Key="customeQueryHotkeyTitle">مفتاح اختصار الاستعلام المخصص</system:String> <system:String x:Key="customeQueryHotkeyTitle">مفتاح اختصار الاستعلام المخصص</system:String>

View file

@ -7,6 +7,11 @@
Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable
</system:String> </system:String>
<system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String> <system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String>
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String> <system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String> <system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String> <system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String>
@ -37,7 +42,7 @@
<system:String x:Key="GameMode">Herní režim</system:String> <system:String x:Key="GameMode">Herní režim</system:String>
<system:String x:Key="GameModeToolTip">Potlačit užívání klávesových zkratek.</system:String> <system:String x:Key="GameModeToolTip">Potlačit užívání klávesových zkratek.</system:String>
<system:String x:Key="PositionReset">Obnovit pozici</system:String> <system:String x:Key="PositionReset">Obnovit pozici</system:String>
<system:String x:Key="PositionResetToolTip">Obnovit pozici vyhledávacího okna</system:String> <system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General --> <!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Nastavení</system:String> <system:String x:Key="flowlauncher_settings">Nastavení</system:String>
@ -70,8 +75,6 @@
<system:String x:Key="LastQueryEmpty">Smazat poslední dotaz</system:String> <system:String x:Key="LastQueryEmpty">Smazat poslední dotaz</system:String>
<system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String> <system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String>
<system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String> <system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Height</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window height is not adjustable by dragging.</system:String>
<system:String x:Key="maxShowResults">Počet zobrazených výsledků</system:String> <system:String x:Key="maxShowResults">Počet zobrazených výsledků</system:String>
<system:String x:Key="maxShowResultsToolTip">Toto nastavení můžete také rychle upravit pomocí CTRL + Plus a CTRL + Minus.</system:String> <system:String x:Key="maxShowResultsToolTip">Toto nastavení můžete také rychle upravit pomocí CTRL + Plus a CTRL + Minus.</system:String>
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignorovat klávesové zkratky v režimu celé obrazovky</system:String> <system:String x:Key="ignoreHotkeysOnFullscreen">Ignorovat klávesové zkratky v režimu celé obrazovky</system:String>
@ -102,6 +105,15 @@
<system:String x:Key="AlwaysPreview">Vždy zobrazit náhled</system:String> <system:String x:Key="AlwaysPreview">Vždy zobrazit náhled</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Při aktivaci služby Flow vždy otevřete panel náhledu. Stisknutím klávesy {0} přepnete náhled.</system:String> <system:String x:Key="AlwaysPreviewToolTip">Při aktivaci služby Flow vždy otevřete panel náhledu. Stisknutím klávesy {0} přepnete náhled.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Stínový efekt není povolen, pokud je aktivní efekt rozostření</system:String> <system:String x:Key="shadowEffectNotAllowed">Stínový efekt není povolen, pokud je aktivní efekt rozostření</system:String>
<system:String x:Key="searchDelay">Search Delay</system:String>
<system:String x:Key="searchDelayToolTip">Delay for a while to search when typing. This reduces interface jumpiness and result load.</system:String>
<system:String x:Key="searchDelayTime">Default Search Delay Time</system:String>
<system:String x:Key="searchDelayTimeToolTip">Plugin default delay time after which search results appear when typing is stopped.</system:String>
<system:String x:Key="SearchDelayTimeVeryLong">Very long</system:String>
<system:String x:Key="SearchDelayTimeLong">Long</system:String>
<system:String x:Key="SearchDelayTimeNormal">Normal</system:String>
<system:String x:Key="SearchDelayTimeShort">Short</system:String>
<system:String x:Key="SearchDelayTimeVeryShort">Very short</system:String>
<!-- Setting Plugin --> <!-- Setting Plugin -->
<system:String x:Key="searchplugin">Vyhledat plugin</system:String> <system:String x:Key="searchplugin">Vyhledat plugin</system:String>
@ -118,6 +130,8 @@
<system:String x:Key="currentActionKeywords">Aktuální aktivační příkaz</system:String> <system:String x:Key="currentActionKeywords">Aktuální aktivační příkaz</system:String>
<system:String x:Key="newActionKeyword">Nový aktivační příkaz</system:String> <system:String x:Key="newActionKeyword">Nový aktivační příkaz</system:String>
<system:String x:Key="actionKeywordsTooltip">Upravit aktivační příkaz</system:String> <system:String x:Key="actionKeywordsTooltip">Upravit aktivační příkaz</system:String>
<system:String x:Key="pluginSearchDelayTime">Plugin seach delay time</system:String>
<system:String x:Key="pluginSearchDelayTimeTooltip">Change Plugin Seach Delay Time</system:String>
<system:String x:Key="currentPriority">Aktuální priorita</system:String> <system:String x:Key="currentPriority">Aktuální priorita</system:String>
<system:String x:Key="newPriority">Nová priorita</system:String> <system:String x:Key="newPriority">Nová priorita</system:String>
<system:String x:Key="priority">Priorita</system:String> <system:String x:Key="priority">Priorita</system:String>
@ -131,6 +145,9 @@
<system:String x:Key="plugin_uninstall">Odinstalovat</system:String> <system:String x:Key="plugin_uninstall">Odinstalovat</system:String>
<system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String> <system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String>
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String> <system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="default">Default</system:String>
<!-- Setting Plugin Store --> <!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Obchod s pluginy</system:String> <system:String x:Key="pluginStore">Obchod s pluginy</system:String>
@ -193,8 +210,20 @@
<system:String x:Key="AnimationSpeedCustom">Vlastní</system:String> <system:String x:Key="AnimationSpeedCustom">Vlastní</system:String>
<system:String x:Key="Clock">Hodiny</system:String> <system:String x:Key="Clock">Hodiny</system:String>
<system:String x:Key="Date">Datum</system:String> <system:String x:Key="Date">Datum</system:String>
<system:String x:Key="BackdropType">Backdrop Type</system:String>
<system:String x:Key="BackdropTypeDisabledToolTip">Backdrop supported starting from Windows 11 build 22000 and above</system:String>
<system:String x:Key="BackdropTypesNone">None</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String> <system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String>
<system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String> <system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String>
<system:String x:Key="ShowPlaceholder">Show placeholder</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
<system:String x:Key="PlaceholderText">Placeholder text</system:String>
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<!-- Setting Hotkey --> <!-- Setting Hotkey -->
<system:String x:Key="hotkey">Klávesová zkratka</system:String> <system:String x:Key="hotkey">Klávesová zkratka</system:String>
@ -294,6 +323,9 @@
<system:String x:Key="logfolder">Složka s logy</system:String> <system:String x:Key="logfolder">Složka s logy</system:String>
<system:String x:Key="clearlogfolder">Vymazat logy</system:String> <system:String x:Key="clearlogfolder">Vymazat logy</system:String>
<system:String x:Key="clearlogfolderMessage">Opravdu chcete odstranit všechny logy?</system:String> <system:String x:Key="clearlogfolderMessage">Opravdu chcete odstranit všechny logy?</system:String>
<system:String x:Key="clearcachefolder">Clear Caches</system:String>
<system:String x:Key="clearcachefolderMessage">Are you sure you want to delete all caches?</system:String>
<system:String x:Key="clearfolderfailMessage">Failed to clear part of folders and files. Please see log file for more information</system:String>
<system:String x:Key="welcomewindow">Průvodce</system:String> <system:String x:Key="welcomewindow">Průvodce</system:String>
<system:String x:Key="userdatapath">User Data Location</system:String> <system:String x:Key="userdatapath">User Data Location</system:String>
<system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String> <system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String>
@ -335,9 +367,16 @@
<system:String x:Key="cannotFindSpecifiedPlugin">Nepodařilo se najít zadaný plugin</system:String> <system:String x:Key="cannotFindSpecifiedPlugin">Nepodařilo se najít zadaný plugin</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">Nový aktivační příkaz nemůže být prázdný</system:String> <system:String x:Key="newActionKeywordsCannotBeEmpty">Nový aktivační příkaz nemůže být prázdný</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">Nový aktivační příkaz byl již přiřazen jinému pluginu, vyberte jiný aktivační příkaz</system:String> <system:String x:Key="newActionKeywordsHasBeenAssigned">Nový aktivační příkaz byl již přiřazen jinému pluginu, vyberte jiný aktivační příkaz</system:String>
<system:String x:Key="newActionKeywordsSameAsOld">This new Action Keyword is the same as old, please choose a different one</system:String>
<system:String x:Key="success">Úspěšné</system:String> <system:String x:Key="success">Úspěšné</system:String>
<system:String x:Key="completedSuccessfully">Úspěšně dokončeno</system:String> <system:String x:Key="completedSuccessfully">Úspěšně dokončeno</system:String>
<system:String x:Key="actionkeyword_tips">Zadejte aktivační příkaz, který je nutný ke spuštění pluginu. Pokud nechcete zadávat aktivační příkaz, použijte * a plugin bude spuštěn bez aktivačního příkazu.</system:String> <system:String x:Key="actionkeyword_tips">Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>
<!-- Search Delay Settings Dialog -->
<system:String x:Key="searchDelayTimeTitle">Search Delay Time Setting</system:String>
<system:String x:Key="searchDelayTime_tips">Select the search delay time you like to use for the plugin. Select &quot;{0}&quot; if you don't want to specify any, and the plugin will use default search delay time.</system:String>
<system:String x:Key="currentSearchDelayTime">Current search delay time</system:String>
<system:String x:Key="newSearchDelayTime">New search delay time</system:String>
<!-- Custom Query Hotkey Dialog --> <!-- Custom Query Hotkey Dialog -->
<system:String x:Key="customeQueryHotkeyTitle">Vlastní klávesová zkratka pro vyhledávání</system:String> <system:String x:Key="customeQueryHotkeyTitle">Vlastní klávesová zkratka pro vyhledávání</system:String>

View file

@ -7,6 +7,11 @@
Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable
</system:String> </system:String>
<system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String> <system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String>
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String> <system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String> <system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String> <system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String>
@ -37,7 +42,7 @@
<system:String x:Key="GameMode">Game Mode</system:String> <system:String x:Key="GameMode">Game Mode</system:String>
<system:String x:Key="GameModeToolTip">Suspend the use of Hotkeys.</system:String> <system:String x:Key="GameModeToolTip">Suspend the use of Hotkeys.</system:String>
<system:String x:Key="PositionReset">Position Reset</system:String> <system:String x:Key="PositionReset">Position Reset</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String> <system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General --> <!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Indstillinger</system:String> <system:String x:Key="flowlauncher_settings">Indstillinger</system:String>
@ -70,8 +75,6 @@
<system:String x:Key="LastQueryEmpty">Empty last Query</system:String> <system:String x:Key="LastQueryEmpty">Empty last Query</system:String>
<system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String> <system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String>
<system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String> <system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Height</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window height is not adjustable by dragging.</system:String>
<system:String x:Key="maxShowResults">Maksimum antal resultater vist</system:String> <system:String x:Key="maxShowResults">Maksimum antal resultater vist</system:String>
<system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String> <system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String>
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignorer genvejstaster i fuldskærmsmode</system:String> <system:String x:Key="ignoreHotkeysOnFullscreen">Ignorer genvejstaster i fuldskærmsmode</system:String>
@ -102,6 +105,15 @@
<system:String x:Key="AlwaysPreview">Always Preview</system:String> <system:String x:Key="AlwaysPreview">Always Preview</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String> <system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String> <system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
<system:String x:Key="searchDelay">Search Delay</system:String>
<system:String x:Key="searchDelayToolTip">Delay for a while to search when typing. This reduces interface jumpiness and result load.</system:String>
<system:String x:Key="searchDelayTime">Default Search Delay Time</system:String>
<system:String x:Key="searchDelayTimeToolTip">Plugin default delay time after which search results appear when typing is stopped.</system:String>
<system:String x:Key="SearchDelayTimeVeryLong">Very long</system:String>
<system:String x:Key="SearchDelayTimeLong">Long</system:String>
<system:String x:Key="SearchDelayTimeNormal">Normal</system:String>
<system:String x:Key="SearchDelayTimeShort">Short</system:String>
<system:String x:Key="SearchDelayTimeVeryShort">Very short</system:String>
<!-- Setting Plugin --> <!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String> <system:String x:Key="searchplugin">Search Plugin</system:String>
@ -118,6 +130,8 @@
<system:String x:Key="currentActionKeywords">Current action keyword</system:String> <system:String x:Key="currentActionKeywords">Current action keyword</system:String>
<system:String x:Key="newActionKeyword">New action keyword</system:String> <system:String x:Key="newActionKeyword">New action keyword</system:String>
<system:String x:Key="actionKeywordsTooltip">Change Action Keywords</system:String> <system:String x:Key="actionKeywordsTooltip">Change Action Keywords</system:String>
<system:String x:Key="pluginSearchDelayTime">Plugin seach delay time</system:String>
<system:String x:Key="pluginSearchDelayTimeTooltip">Change Plugin Seach Delay Time</system:String>
<system:String x:Key="currentPriority">Current Priority</system:String> <system:String x:Key="currentPriority">Current Priority</system:String>
<system:String x:Key="newPriority">New Priority</system:String> <system:String x:Key="newPriority">New Priority</system:String>
<system:String x:Key="priority">Priority</system:String> <system:String x:Key="priority">Priority</system:String>
@ -131,6 +145,9 @@
<system:String x:Key="plugin_uninstall">Uninstall</system:String> <system:String x:Key="plugin_uninstall">Uninstall</system:String>
<system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String> <system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String>
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String> <system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="default">Default</system:String>
<!-- Setting Plugin Store --> <!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plugin Store</system:String> <system:String x:Key="pluginStore">Plugin Store</system:String>
@ -193,8 +210,20 @@
<system:String x:Key="AnimationSpeedCustom">Custom</system:String> <system:String x:Key="AnimationSpeedCustom">Custom</system:String>
<system:String x:Key="Clock">Clock</system:String> <system:String x:Key="Clock">Clock</system:String>
<system:String x:Key="Date">Date</system:String> <system:String x:Key="Date">Date</system:String>
<system:String x:Key="BackdropType">Backdrop Type</system:String>
<system:String x:Key="BackdropTypeDisabledToolTip">Backdrop supported starting from Windows 11 build 22000 and above</system:String>
<system:String x:Key="BackdropTypesNone">None</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String> <system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String>
<system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String> <system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String>
<system:String x:Key="ShowPlaceholder">Show placeholder</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
<system:String x:Key="PlaceholderText">Placeholder text</system:String>
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<!-- Setting Hotkey --> <!-- Setting Hotkey -->
<system:String x:Key="hotkey">Genvejstast</system:String> <system:String x:Key="hotkey">Genvejstast</system:String>
@ -294,6 +323,9 @@
<system:String x:Key="logfolder">Log Folder</system:String> <system:String x:Key="logfolder">Log Folder</system:String>
<system:String x:Key="clearlogfolder">Clear Logs</system:String> <system:String x:Key="clearlogfolder">Clear Logs</system:String>
<system:String x:Key="clearlogfolderMessage">Are you sure you want to delete all logs?</system:String> <system:String x:Key="clearlogfolderMessage">Are you sure you want to delete all logs?</system:String>
<system:String x:Key="clearcachefolder">Clear Caches</system:String>
<system:String x:Key="clearcachefolderMessage">Are you sure you want to delete all caches?</system:String>
<system:String x:Key="clearfolderfailMessage">Failed to clear part of folders and files. Please see log file for more information</system:String>
<system:String x:Key="welcomewindow">Wizard</system:String> <system:String x:Key="welcomewindow">Wizard</system:String>
<system:String x:Key="userdatapath">User Data Location</system:String> <system:String x:Key="userdatapath">User Data Location</system:String>
<system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String> <system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String>
@ -335,9 +367,16 @@
<system:String x:Key="cannotFindSpecifiedPlugin">Kan ikke finde det valgte plugin</system:String> <system:String x:Key="cannotFindSpecifiedPlugin">Kan ikke finde det valgte plugin</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">Nyt nøgleord må ikke være tomt</system:String> <system:String x:Key="newActionKeywordsCannotBeEmpty">Nyt nøgleord må ikke være tomt</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">Nyt nøgleord er tilknyttet et andet plugin, tilknyt venligst et andet nyt nøgeleord</system:String> <system:String x:Key="newActionKeywordsHasBeenAssigned">Nyt nøgleord er tilknyttet et andet plugin, tilknyt venligst et andet nyt nøgeleord</system:String>
<system:String x:Key="newActionKeywordsSameAsOld">This new Action Keyword is the same as old, please choose a different one</system:String>
<system:String x:Key="success">Fortsæt</system:String> <system:String x:Key="success">Fortsæt</system:String>
<system:String x:Key="completedSuccessfully">Completed successfully</system:String> <system:String x:Key="completedSuccessfully">Completed successfully</system:String>
<system:String x:Key="actionkeyword_tips">Brug * hvis du ikke vil angive et nøgleord</system:String> <system:String x:Key="actionkeyword_tips">Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>
<!-- Search Delay Settings Dialog -->
<system:String x:Key="searchDelayTimeTitle">Search Delay Time Setting</system:String>
<system:String x:Key="searchDelayTime_tips">Select the search delay time you like to use for the plugin. Select &quot;{0}&quot; if you don't want to specify any, and the plugin will use default search delay time.</system:String>
<system:String x:Key="currentSearchDelayTime">Current search delay time</system:String>
<system:String x:Key="newSearchDelayTime">New search delay time</system:String>
<!-- Custom Query Hotkey Dialog --> <!-- Custom Query Hotkey Dialog -->
<system:String x:Key="customeQueryHotkeyTitle">Tilpasset søgegenvejstast</system:String> <system:String x:Key="customeQueryHotkeyTitle">Tilpasset søgegenvejstast</system:String>

View file

@ -7,13 +7,18 @@
Klicken Sie auf „Nein“, wenn es bereits installiert ist, und Sie werden aufgefordert, den Ordner auszuwählen, der die ausführbare Datei {1} enthält Klicken Sie auf „Nein“, wenn es bereits installiert ist, und Sie werden aufgefordert, den Ordner auszuwählen, der die ausführbare Datei {1} enthält
</system:String> </system:String>
<system:String x:Key="runtimePluginChooseRuntimeExecutable">Bitte wählen Sie die ausführbare Datei {0} aus</system:String> <system:String x:Key="runtimePluginChooseRuntimeExecutable">Bitte wählen Sie die ausführbare Datei {0} aus</system:String>
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Der Pfad zur ausführbaren Datei {0} kann nicht festgelegt werden. Bitte versuchen Sie es in den Einstellungen von Flow (scrollen Sie nach unten).</system:String> <system:String x:Key="runtimePluginUnableToSetExecutablePath">Der Pfad zur ausführbaren Datei {0} kann nicht festgelegt werden. Bitte versuchen Sie es in den Einstellungen von Flow (scrollen Sie nach unten).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Plug-ins können nicht initialisiert werden</system:String> <system:String x:Key="failedToInitializePluginsTitle">Plug-ins können nicht initialisiert werden</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plug-ins: {0} - nicht geladen werden und wird deaktiviert, bitte kontaktiere Sie den Ersteller des Plug-ins für Hilfe</system:String> <system:String x:Key="failedToInitializePluginsMessage">Plug-ins: {0} - nicht geladen werden und wird deaktiviert, bitte kontaktieren Sie den Ersteller des Plug-ins für Hilfe</system:String>
<!-- MainWindow --> <!-- MainWindow -->
<system:String x:Key="registerHotkeyFailed">Hotkey &quot;{0}&quot; konnte nicht registriert werden. Der Hotkey ist möglicherweise von einem anderen Programm in Verwendung. Wechseln Sie zu einem anderen Hotkey oder beenden Sie das andere Programm.</system:String> <system:String x:Key="registerHotkeyFailed">Hotkey &quot;{0}&quot; konnte nicht registriert werden. Der Hotkey ist möglicherweise von einem anderen Programm in Verwendung. Wechseln Sie zu einem anderen Hotkey oder beenden Sie das andere Programm.</system:String>
<system:String x:Key="unregisterHotkeyFailed">Failed to unregister hotkey &quot;{0}&quot;. Please try again or see log for details</system:String> <system:String x:Key="unregisterHotkeyFailed">Registrierung des Hotkeys &quot;{0}&quot; konnte nicht aufgehoben werden. Bitte versuchen Sie es erneut oder lesen Sie das Log für Details</system:String>
<system:String x:Key="MessageBoxTitle">Flow Launcher</system:String> <system:String x:Key="MessageBoxTitle">Flow Launcher</system:String>
<system:String x:Key="couldnotStartCmd">Konnte nicht gestartet werden {0}</system:String> <system:String x:Key="couldnotStartCmd">Konnte nicht gestartet werden {0}</system:String>
<system:String x:Key="invalidFlowLauncherPluginFileFormat">Flow Launcher Plug-in-Dateiformat ungültig</system:String> <system:String x:Key="invalidFlowLauncherPluginFileFormat">Flow Launcher Plug-in-Dateiformat ungültig</system:String>
@ -37,7 +42,7 @@
<system:String x:Key="GameMode">Spielmodus</system:String> <system:String x:Key="GameMode">Spielmodus</system:String>
<system:String x:Key="GameModeToolTip">Aussetzen der Verwendung von Hotkeys.</system:String> <system:String x:Key="GameModeToolTip">Aussetzen der Verwendung von Hotkeys.</system:String>
<system:String x:Key="PositionReset">Position zurücksetzen</system:String> <system:String x:Key="PositionReset">Position zurücksetzen</system:String>
<system:String x:Key="PositionResetToolTip">Position des Suchfensters zurücksetze</system:String> <system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General --> <!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Einstellungen</system:String> <system:String x:Key="flowlauncher_settings">Einstellungen</system:String>
@ -45,9 +50,9 @@
<system:String x:Key="portableMode">Portabler Modus</system:String> <system:String x:Key="portableMode">Portabler Modus</system:String>
<system:String x:Key="portableModeToolTIp">Speichern Sie alle Einstellungen und Benutzerdaten in einem Ordner (nützlich bei Verwendung von Wechsellaufwerken oder Cloud-Diensten).</system:String> <system:String x:Key="portableModeToolTIp">Speichern Sie alle Einstellungen und Benutzerdaten in einem Ordner (nützlich bei Verwendung von Wechsellaufwerken oder Cloud-Diensten).</system:String>
<system:String x:Key="startFlowLauncherOnSystemStartup">Flow Launcher bei Systemstart starten</system:String> <system:String x:Key="startFlowLauncherOnSystemStartup">Flow Launcher bei Systemstart starten</system:String>
<system:String x:Key="useLogonTaskForStartup">Use logon task instead of startup entry for faster startup experience</system:String> <system:String x:Key="useLogonTaskForStartup">Log-on-Aufgabe anstelle des Starteintrags für schnelleres Startup-Erfahrung verwenden</system:String>
<system:String x:Key="useLogonTaskForStartupTooltip">After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler</system:String> <system:String x:Key="useLogonTaskForStartupTooltip">Nach der Deinstallation müssen Sie diese Aufgabe (Flow.Launcher Startup) via Task-Scheduler manuell entfernen</system:String>
<system:String x:Key="setAutoStartFailed">Fehler bei Einstellungsstart bei Start</system:String> <system:String x:Key="setAutoStartFailed">Fehler bei Einstellungsstart beim Start</system:String>
<system:String x:Key="hideFlowLauncherWhenLoseFocus">Flow Launcher ausblenden, wenn Fokus verloren geht</system:String> <system:String x:Key="hideFlowLauncherWhenLoseFocus">Flow Launcher ausblenden, wenn Fokus verloren geht</system:String>
<system:String x:Key="dontPromptUpdateMsg">Versionsbenachrichtigungen nicht zeigen</system:String> <system:String x:Key="dontPromptUpdateMsg">Versionsbenachrichtigungen nicht zeigen</system:String>
<system:String x:Key="SearchWindowPosition">Position des Suchfensters</system:String> <system:String x:Key="SearchWindowPosition">Position des Suchfensters</system:String>
@ -70,8 +75,6 @@
<system:String x:Key="LastQueryEmpty">Letzte Abfrage leeren</system:String> <system:String x:Key="LastQueryEmpty">Letzte Abfrage leeren</system:String>
<system:String x:Key="LastQueryActionKeywordPreserved">Letztes Aktions-Schlüsselwort beibehalten</system:String> <system:String x:Key="LastQueryActionKeywordPreserved">Letztes Aktions-Schlüsselwort beibehalten</system:String>
<system:String x:Key="LastQueryActionKeywordSelected">Letztes Aktions-Schlüsselwort auswählen</system:String> <system:String x:Key="LastQueryActionKeywordSelected">Letztes Aktions-Schlüsselwort auswählen</system:String>
<system:String x:Key="KeepMaxResults">Feste Fensterhöhe</system:String>
<system:String x:Key="KeepMaxResultsToolTip">Die Fensterhöhe ist durch Ziehen nicht anpassbar.</system:String>
<system:String x:Key="maxShowResults">Maximal gezeigte Ergebnisse</system:String> <system:String x:Key="maxShowResults">Maximal gezeigte Ergebnisse</system:String>
<system:String x:Key="maxShowResultsToolTip">Sie können dies auch unter Verwendung von STRG+Plus und STRG+Minus schnell anpassen.</system:String> <system:String x:Key="maxShowResultsToolTip">Sie können dies auch unter Verwendung von STRG+Plus und STRG+Minus schnell anpassen.</system:String>
<system:String x:Key="ignoreHotkeysOnFullscreen">Hotkeys im Vollbildmodus ignorieren</system:String> <system:String x:Key="ignoreHotkeysOnFullscreen">Hotkeys im Vollbildmodus ignorieren</system:String>
@ -102,6 +105,15 @@
<system:String x:Key="AlwaysPreview">Immer Vorschau</system:String> <system:String x:Key="AlwaysPreview">Immer Vorschau</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Vorschau-Panel immer öffnen, wenn Flow aktiviert ist. Drücken Sie {0}, um Vorschau umzuschalten.</system:String> <system:String x:Key="AlwaysPreviewToolTip">Vorschau-Panel immer öffnen, wenn Flow aktiviert ist. Drücken Sie {0}, um Vorschau umzuschalten.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Schatteneffekt ist nicht erlaubt, während das aktuelle Theme den Unschärfe-Effekt aktiviert hat</system:String> <system:String x:Key="shadowEffectNotAllowed">Schatteneffekt ist nicht erlaubt, während das aktuelle Theme den Unschärfe-Effekt aktiviert hat</system:String>
<system:String x:Key="searchDelay">Search Delay</system:String>
<system:String x:Key="searchDelayToolTip">Delay for a while to search when typing. This reduces interface jumpiness and result load.</system:String>
<system:String x:Key="searchDelayTime">Default Search Delay Time</system:String>
<system:String x:Key="searchDelayTimeToolTip">Plugin default delay time after which search results appear when typing is stopped.</system:String>
<system:String x:Key="SearchDelayTimeVeryLong">Very long</system:String>
<system:String x:Key="SearchDelayTimeLong">Long</system:String>
<system:String x:Key="SearchDelayTimeNormal">Normal</system:String>
<system:String x:Key="SearchDelayTimeShort">Short</system:String>
<system:String x:Key="SearchDelayTimeVeryShort">Very short</system:String>
<!-- Setting Plugin --> <!-- Setting Plugin -->
<system:String x:Key="searchplugin">Plug-in suchen</system:String> <system:String x:Key="searchplugin">Plug-in suchen</system:String>
@ -118,6 +130,8 @@
<system:String x:Key="currentActionKeywords">Aktuelles Action-Schlüsselwort</system:String> <system:String x:Key="currentActionKeywords">Aktuelles Action-Schlüsselwort</system:String>
<system:String x:Key="newActionKeyword">Neues Aktions-Schlüsselwort</system:String> <system:String x:Key="newActionKeyword">Neues Aktions-Schlüsselwort</system:String>
<system:String x:Key="actionKeywordsTooltip">Aktions-Schlüsselwörter ändern</system:String> <system:String x:Key="actionKeywordsTooltip">Aktions-Schlüsselwörter ändern</system:String>
<system:String x:Key="pluginSearchDelayTime">Plugin seach delay time</system:String>
<system:String x:Key="pluginSearchDelayTimeTooltip">Change Plugin Seach Delay Time</system:String>
<system:String x:Key="currentPriority">Aktuelle Priorität</system:String> <system:String x:Key="currentPriority">Aktuelle Priorität</system:String>
<system:String x:Key="newPriority">Neue Priorität</system:String> <system:String x:Key="newPriority">Neue Priorität</system:String>
<system:String x:Key="priority">Priorität</system:String> <system:String x:Key="priority">Priorität</system:String>
@ -129,8 +143,11 @@
<system:String x:Key="plugin_query_version">Version</system:String> <system:String x:Key="plugin_query_version">Version</system:String>
<system:String x:Key="plugin_query_web">Website</system:String> <system:String x:Key="plugin_query_web">Website</system:String>
<system:String x:Key="plugin_uninstall">Deinstallieren</system:String> <system:String x:Key="plugin_uninstall">Deinstallieren</system:String>
<system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String> <system:String x:Key="failedToRemovePluginSettingsTitle">Plug-in-Einstellungen können nicht entfernt werden</system:String>
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String> <system:String x:Key="failedToRemovePluginSettingsMessage">Plug-ins: {0} - Plug-in-Einstellungsdateien können nicht entfernt werden, bitte entfernen Sie diese manuell</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="default">Default</system:String>
<!-- Setting Plugin Store --> <!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plug-in-Store</system:String> <system:String x:Key="pluginStore">Plug-in-Store</system:String>
@ -193,8 +210,20 @@
<system:String x:Key="AnimationSpeedCustom">Benutzerdefiniert</system:String> <system:String x:Key="AnimationSpeedCustom">Benutzerdefiniert</system:String>
<system:String x:Key="Clock">Uhr</system:String> <system:String x:Key="Clock">Uhr</system:String>
<system:String x:Key="Date">Datum</system:String> <system:String x:Key="Date">Datum</system:String>
<system:String x:Key="BackdropType">Backdrop-Typ</system:String>
<system:String x:Key="BackdropTypeDisabledToolTip">Backdrop supported starting from Windows 11 build 22000 and above</system:String>
<system:String x:Key="BackdropTypesNone">Keine</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<system:String x:Key="TypeIsDarkToolTip">Dieses Theme unterstützt zwei Modi (hell/dunkel).</system:String> <system:String x:Key="TypeIsDarkToolTip">Dieses Theme unterstützt zwei Modi (hell/dunkel).</system:String>
<system:String x:Key="TypeHasBlurToolTip">Dieses Theme unterstützt Unschärfe und transparenten Hintergrund.</system:String> <system:String x:Key="TypeHasBlurToolTip">Dieses Theme unterstützt Unschärfe und transparenten Hintergrund.</system:String>
<system:String x:Key="ShowPlaceholder">Show placeholder</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
<system:String x:Key="PlaceholderText">Placeholder text</system:String>
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<!-- Setting Hotkey --> <!-- Setting Hotkey -->
<system:String x:Key="hotkey">Hotkey</system:String> <system:String x:Key="hotkey">Hotkey</system:String>
@ -294,11 +323,14 @@
<system:String x:Key="logfolder">Ordner »Logs«</system:String> <system:String x:Key="logfolder">Ordner »Logs«</system:String>
<system:String x:Key="clearlogfolder">Logs löschen</system:String> <system:String x:Key="clearlogfolder">Logs löschen</system:String>
<system:String x:Key="clearlogfolderMessage">Sind Sie sicher, dass Sie alle Logs löschen wollen?</system:String> <system:String x:Key="clearlogfolderMessage">Sind Sie sicher, dass Sie alle Logs löschen wollen?</system:String>
<system:String x:Key="clearcachefolder">Clear Caches</system:String>
<system:String x:Key="clearcachefolderMessage">Are you sure you want to delete all caches?</system:String>
<system:String x:Key="clearfolderfailMessage">Failed to clear part of folders and files. Please see log file for more information</system:String>
<system:String x:Key="welcomewindow">Assistent</system:String> <system:String x:Key="welcomewindow">Assistent</system:String>
<system:String x:Key="userdatapath">Speicherort für Benutzerdaten</system:String> <system:String x:Key="userdatapath">Speicherort für Benutzerdaten</system:String>
<system:String x:Key="userdatapathToolTip">Benutzereinstellungen und installierte Plug-ins werden im Ordner für Benutzerdaten gespeichert. Dieser Speicherort kann variieren, je nachdem, ob sich das Programm im portablen Modus befindet oder nicht.</system:String> <system:String x:Key="userdatapathToolTip">Benutzereinstellungen und installierte Plug-ins werden im Ordner für Benutzerdaten gespeichert. Dieser Speicherort kann variieren, je nachdem, ob sich das Programm im portablen Modus befindet oder nicht.</system:String>
<system:String x:Key="userdatapathButton">Ordner öffnen</system:String> <system:String x:Key="userdatapathButton">Ordner öffnen</system:String>
<system:String x:Key="logLevel">Log Level</system:String> <system:String x:Key="logLevel">Log-Ebene</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String> <system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="LogLevelINFO">Info</system:String> <system:String x:Key="LogLevelINFO">Info</system:String>
@ -335,9 +367,16 @@
<system:String x:Key="cannotFindSpecifiedPlugin">Das angegebene Plug-in kann nicht gefunden werden</system:String> <system:String x:Key="cannotFindSpecifiedPlugin">Das angegebene Plug-in kann nicht gefunden werden</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">Neues Aktions-Schlüsselwort darf nicht leer sein</system:String> <system:String x:Key="newActionKeywordsCannotBeEmpty">Neues Aktions-Schlüsselwort darf nicht leer sein</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">Dieses neue Aktions-Schlüsselwort ist bereits einem anderen Plug-in zugewiesen, bitte wählen Sie ein anderes</system:String> <system:String x:Key="newActionKeywordsHasBeenAssigned">Dieses neue Aktions-Schlüsselwort ist bereits einem anderen Plug-in zugewiesen, bitte wählen Sie ein anderes</system:String>
<system:String x:Key="newActionKeywordsSameAsOld">Dieses neue Aktions-Schlüsselwort ist dasselbe wie das alte, bitte wählen Sie ein anderes</system:String>
<system:String x:Key="success">Erfolg</system:String> <system:String x:Key="success">Erfolg</system:String>
<system:String x:Key="completedSuccessfully">Erfolgreich abgeschlossen</system:String> <system:String x:Key="completedSuccessfully">Erfolgreich abgeschlossen</system:String>
<system:String x:Key="actionkeyword_tips">Geben Sie das Aktions-Schlüsselwort ein, das Sie verwenden möchten, um das Plug-in zu starten. Verwenden Sie *, wenn Sie keines angeben möchten, und das Plug-in wird ohne irgendwelche Aktions-Schlüsselwörter ausgelöst.</system:String> <system:String x:Key="actionkeyword_tips">Geben Sie die Aktions-Schlüsselwörter ein, die Sie zum Starten des Plug-ins verwenden möchten, und trennen Sie sie durch Leerzeichen voneinander ab. Verwenden Sie *, wenn Sie keine spezifizieren möchten, und das Plug-in wird ohne jegliche Aktions-Schlüsselwörter ausgelöst.</system:String>
<!-- Search Delay Settings Dialog -->
<system:String x:Key="searchDelayTimeTitle">Search Delay Time Setting</system:String>
<system:String x:Key="searchDelayTime_tips">Select the search delay time you like to use for the plugin. Select &quot;{0}&quot; if you don't want to specify any, and the plugin will use default search delay time.</system:String>
<system:String x:Key="currentSearchDelayTime">Current search delay time</system:String>
<system:String x:Key="newSearchDelayTime">New search delay time</system:String>
<!-- Custom Query Hotkey Dialog --> <!-- Custom Query Hotkey Dialog -->
<system:String x:Key="customeQueryHotkeyTitle">Benutzerdefinierter Abfrage-Hotkey</system:String> <system:String x:Key="customeQueryHotkeyTitle">Benutzerdefinierter Abfrage-Hotkey</system:String>
@ -388,9 +427,9 @@ Wenn Sie bei der Eingabe eines Shortcuts ein '@'-Präfix hinzufügen, stimmt die
<system:String x:Key="reportWindow_report_succeed">Bericht erfolgreich gesendet</system:String> <system:String x:Key="reportWindow_report_succeed">Bericht erfolgreich gesendet</system:String>
<system:String x:Key="reportWindow_report_failed">Bericht konnte nicht gesendet werden</system:String> <system:String x:Key="reportWindow_report_failed">Bericht konnte nicht gesendet werden</system:String>
<system:String x:Key="reportWindow_flowlauncher_got_an_error">Flow Launcher hat einen Fehler</system:String> <system:String x:Key="reportWindow_flowlauncher_got_an_error">Flow Launcher hat einen Fehler</system:String>
<system:String x:Key="reportWindow_please_open_issue">Please open new issue in</system:String> <system:String x:Key="reportWindow_please_open_issue">Bitte öffnen Sie einen neuen Fall in</system:String>
<system:String x:Key="reportWindow_upload_log">1. Upload log file: {0}</system:String> <system:String x:Key="reportWindow_upload_log">1. Logdatei hochladen: {0}</system:String>
<system:String x:Key="reportWindow_copy_below">2. Copy below exception message</system:String> <system:String x:Key="reportWindow_copy_below">2. Kopieren Sie die Ausnahmemeldung unterhalb</system:String>
<!-- General Notice --> <!-- General Notice -->
<system:String x:Key="pleaseWait">Bitte warten Sie ...</system:String> <system:String x:Key="pleaseWait">Bitte warten Sie ...</system:String>

View file

@ -9,6 +9,11 @@
Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable
</system:String> </system:String>
<system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String> <system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String>
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String> <system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String> <system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String> <system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String>
@ -39,6 +44,7 @@
<system:String x:Key="GameMode">Game Mode</system:String> <system:String x:Key="GameMode">Game Mode</system:String>
<system:String x:Key="GameModeToolTip">Suspend the use of Hotkeys.</system:String> <system:String x:Key="GameModeToolTip">Suspend the use of Hotkeys.</system:String>
<system:String x:Key="PositionReset">Position Reset</system:String> <system:String x:Key="PositionReset">Position Reset</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String> <system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General --> <!-- Setting General -->
@ -52,7 +58,7 @@
<system:String x:Key="setAutoStartFailed">Error setting launch on startup</system:String> <system:String x:Key="setAutoStartFailed">Error setting launch on startup</system:String>
<system:String x:Key="hideFlowLauncherWhenLoseFocus">Hide Flow Launcher when focus is lost</system:String> <system:String x:Key="hideFlowLauncherWhenLoseFocus">Hide Flow Launcher when focus is lost</system:String>
<system:String x:Key="dontPromptUpdateMsg">Do not show new version notifications</system:String> <system:String x:Key="dontPromptUpdateMsg">Do not show new version notifications</system:String>
<system:String x:Key="SearchWindowPosition">Search Window Position</system:String> <system:String x:Key="SearchWindowPosition">Search Window Location</system:String>
<system:String x:Key="SearchWindowScreenRememberLastLaunchLocation">Remember Last Position</system:String> <system:String x:Key="SearchWindowScreenRememberLastLaunchLocation">Remember Last Position</system:String>
<system:String x:Key="SearchWindowScreenCursor">Monitor with Mouse Cursor</system:String> <system:String x:Key="SearchWindowScreenCursor">Monitor with Mouse Cursor</system:String>
<system:String x:Key="SearchWindowScreenFocus">Monitor with Focused Window</system:String> <system:String x:Key="SearchWindowScreenFocus">Monitor with Focused Window</system:String>
@ -102,6 +108,24 @@
<system:String x:Key="AlwaysPreview">Always Preview</system:String> <system:String x:Key="AlwaysPreview">Always Preview</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String> <system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String> <system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
<system:String x:Key="searchDelay">Search Delay</system:String>
<system:String x:Key="searchDelayToolTip">Adds a short delay while typing to reduce UI flicker and result load. Recommended if your typing speed is average.</system:String>
<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 --> <!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String> <system:String x:Key="searchplugin">Search Plugin</system:String>
@ -118,6 +142,12 @@
<system:String x:Key="currentActionKeywords">Current action keyword</system:String> <system:String x:Key="currentActionKeywords">Current action keyword</system:String>
<system:String x:Key="newActionKeyword">New action keyword</system:String> <system:String x:Key="newActionKeyword">New action keyword</system:String>
<system:String x:Key="actionKeywordsTooltip">Change Action Keywords</system:String> <system:String x:Key="actionKeywordsTooltip">Change Action Keywords</system:String>
<system:String x:Key="pluginSearchDelayTime">Plugin search delay time</system:String>
<system:String x:Key="pluginSearchDelayTimeTooltip">Change Plugin Search Delay Time</system:String>
<system:String x:Key="FilterComboboxLabel">Advanced Settings:</system:String>
<system:String x:Key="DisplayModeOnOff">Enabled</system:String>
<system:String x:Key="DisplayModePriority">Priority</system:String>
<system:String x:Key="DisplayModeSearchDelay">Search Delay</system:String>
<system:String x:Key="currentPriority">Current Priority</system:String> <system:String x:Key="currentPriority">Current Priority</system:String>
<system:String x:Key="newPriority">New Priority</system:String> <system:String x:Key="newPriority">New Priority</system:String>
<system:String x:Key="priority">Priority</system:String> <system:String x:Key="priority">Priority</system:String>
@ -131,6 +161,8 @@
<system:String x:Key="plugin_uninstall">Uninstall</system:String> <system:String x:Key="plugin_uninstall">Uninstall</system:String>
<system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String> <system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String>
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String> <system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<!-- Setting Plugin Store --> <!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plugin Store</system:String> <system:String x:Key="pluginStore">Plugin Store</system:String>
@ -167,6 +199,9 @@
<system:String x:Key="resultItemFont">Result Title Font</system:String> <system:String x:Key="resultItemFont">Result Title Font</system:String>
<system:String x:Key="resultSubItemFont">Result Subtitle Font</system:String> <system:String x:Key="resultSubItemFont">Result Subtitle Font</system:String>
<system:String x:Key="resetCustomize">Reset</system:String> <system:String x:Key="resetCustomize">Reset</system:String>
<system:String x:Key="resetCustomizeToolTip">Reset to the recommended font and size settings.</system:String>
<system:String x:Key="ImportThemeSize">Import Theme Size</system:String>
<system:String x:Key="ImportThemeSizeToolTip">If a size value intended by the theme designer is available, it will be retrieved and applied.</system:String>
<system:String x:Key="CustomizeToolTip">Customize</system:String> <system:String x:Key="CustomizeToolTip">Customize</system:String>
<system:String x:Key="windowMode">Window Mode</system:String> <system:String x:Key="windowMode">Window Mode</system:String>
<system:String x:Key="opacity">Opacity</system:String> <system:String x:Key="opacity">Opacity</system:String>
@ -194,11 +229,13 @@
<system:String x:Key="Clock">Clock</system:String> <system:String x:Key="Clock">Clock</system:String>
<system:String x:Key="Date">Date</system:String> <system:String x:Key="Date">Date</system:String>
<system:String x:Key="BackdropType">Backdrop Type</system:String> <system:String x:Key="BackdropType">Backdrop Type</system:String>
<system:String x:Key="BackdropInfo">The backdrop effect is not applied in the preview.</system:String>
<system:String x:Key="BackdropTypeDisabledToolTip">Backdrop supported starting from Windows 11 build 22000 and above</system:String>
<system:String x:Key="BackdropTypesNone">None</system:String> <system:String x:Key="BackdropTypesNone">None</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String> <system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String> <system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String> <system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String> <system:String x:Key="TypeIsDarkToolTip">This theme supports two (light/dark) modes.</system:String>
<system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String> <system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String>
<system:String x:Key="ShowPlaceholder">Show placeholder</system:String> <system:String x:Key="ShowPlaceholder">Show placeholder</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String> <system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
@ -265,6 +302,9 @@
<system:String x:Key="useGlyphUI">Use Segoe Fluent Icons</system:String> <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="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="flowlauncherPressHotkey">Press Key</system:String>
<system:String x:Key="showBadges">Show Result Badges</system:String>
<system:String x:Key="showBadgesToolTip">For supported plugins, badges are displayed to help distinguish them more easily.</system:String>
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
<!-- Setting Proxy --> <!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP Proxy</system:String> <system:String x:Key="proxy">HTTP Proxy</system:String>
@ -305,6 +345,10 @@
<system:String x:Key="logfolder">Log Folder</system:String> <system:String x:Key="logfolder">Log Folder</system:String>
<system:String x:Key="clearlogfolder">Clear Logs</system:String> <system:String x:Key="clearlogfolder">Clear Logs</system:String>
<system:String x:Key="clearlogfolderMessage">Are you sure you want to delete all logs?</system:String> <system:String x:Key="clearlogfolderMessage">Are you sure you want to delete all logs?</system:String>
<system:String x:Key="cachefolder">Cache Folder</system:String>
<system:String x:Key="clearcachefolder">Clear Caches</system:String>
<system:String x:Key="clearcachefolderMessage">Are you sure you want to delete all caches?</system:String>
<system:String x:Key="clearfolderfailMessage">Failed to clear part of folders and files. Please see log file for more information</system:String>
<system:String x:Key="welcomewindow">Wizard</system:String> <system:String x:Key="welcomewindow">Wizard</system:String>
<system:String x:Key="userdatapath">User Data Location</system:String> <system:String x:Key="userdatapath">User Data Location</system:String>
<system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String> <system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String>
@ -312,6 +356,7 @@
<system:String x:Key="logLevel">Log Level</system:String> <system:String x:Key="logLevel">Log Level</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String> <system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="LogLevelINFO">Info</system:String> <system:String x:Key="LogLevelINFO">Info</system:String>
<system:String x:Key="settingWindowFontTitle">Setting Window Font</system:String>
<!-- FileManager Setting Dialog --> <!-- FileManager Setting Dialog -->
<system:String x:Key="fileManagerWindow">Select File Manager</system:String> <system:String x:Key="fileManagerWindow">Select File Manager</system:String>
@ -349,8 +394,13 @@
<system:String x:Key="newActionKeywordsSameAsOld">This new Action Keyword is the same as old, please choose a different one</system:String> <system:String x:Key="newActionKeywordsSameAsOld">This new Action Keyword is the same as old, please choose a different one</system:String>
<system:String x:Key="success">Success</system:String> <system:String x:Key="success">Success</system:String>
<system:String x:Key="completedSuccessfully">Completed successfully</system:String> <system:String x:Key="completedSuccessfully">Completed successfully</system:String>
<system:String x:Key="failedToCopy">Failed to copy</system:String>
<system:String x:Key="actionkeyword_tips">Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String> <system:String x:Key="actionkeyword_tips">Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>
<!-- Search Delay Settings Dialog -->
<system:String x:Key="searchDelayTimeTitle">Search Delay Time Setting</system:String>
<system:String x:Key="searchDelayTimeTips">Input the search delay time in ms you like to use for the plugin. Input empty if you don't want to specify any, and the plugin will use default search delay time.</system:String>
<!-- Custom Query Hotkey Dialog --> <!-- Custom Query Hotkey Dialog -->
<system:String x:Key="customeQueryHotkeyTitle">Custom Query Hotkey</system:String> <system:String x:Key="customeQueryHotkeyTitle">Custom Query Hotkey</system:String>
<system:String x:Key="customeQueryHotkeyTips">Press a custom hotkey to open Flow Launcher and input the specified query automatically.</system:String> <system:String x:Key="customeQueryHotkeyTips">Press a custom hotkey to open Flow Launcher and input the specified query automatically.</system:String>

View file

@ -7,6 +7,11 @@
Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable
</system:String> </system:String>
<system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String> <system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String>
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String> <system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String> <system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String> <system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String>
@ -37,7 +42,7 @@
<system:String x:Key="GameMode">Modo de juego</system:String> <system:String x:Key="GameMode">Modo de juego</system:String>
<system:String x:Key="GameModeToolTip">Suspender el uso de las teclas de acceso directo.</system:String> <system:String x:Key="GameModeToolTip">Suspender el uso de las teclas de acceso directo.</system:String>
<system:String x:Key="PositionReset">Position Reset</system:String> <system:String x:Key="PositionReset">Position Reset</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String> <system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General --> <!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Ajustes</system:String> <system:String x:Key="flowlauncher_settings">Ajustes</system:String>
@ -70,8 +75,6 @@
<system:String x:Key="LastQueryEmpty">Borrar última consulta</system:String> <system:String x:Key="LastQueryEmpty">Borrar última consulta</system:String>
<system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String> <system:String x:Key="LastQueryActionKeywordPreserved">Preserve Last Action Keyword</system:String>
<system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String> <system:String x:Key="LastQueryActionKeywordSelected">Select Last Action Keyword</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Height</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window height is not adjustable by dragging.</system:String>
<system:String x:Key="maxShowResults">Máximo de resultados mostrados</system:String> <system:String x:Key="maxShowResults">Máximo de resultados mostrados</system:String>
<system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String> <system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String>
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignorar atajos de teclado en modo pantalla completa</system:String> <system:String x:Key="ignoreHotkeysOnFullscreen">Ignorar atajos de teclado en modo pantalla completa</system:String>
@ -102,6 +105,15 @@
<system:String x:Key="AlwaysPreview">Always Preview</system:String> <system:String x:Key="AlwaysPreview">Always Preview</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String> <system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String>
<system:String x:Key="shadowEffectNotAllowed">El efecto de sombra no está permitido mientras el tema actual tenga el efecto de desenfoque habilitado</system:String> <system:String x:Key="shadowEffectNotAllowed">El efecto de sombra no está permitido mientras el tema actual tenga el efecto de desenfoque habilitado</system:String>
<system:String x:Key="searchDelay">Search Delay</system:String>
<system:String x:Key="searchDelayToolTip">Delay for a while to search when typing. This reduces interface jumpiness and result load.</system:String>
<system:String x:Key="searchDelayTime">Default Search Delay Time</system:String>
<system:String x:Key="searchDelayTimeToolTip">Plugin default delay time after which search results appear when typing is stopped.</system:String>
<system:String x:Key="SearchDelayTimeVeryLong">Very long</system:String>
<system:String x:Key="SearchDelayTimeLong">Long</system:String>
<system:String x:Key="SearchDelayTimeNormal">Normal</system:String>
<system:String x:Key="SearchDelayTimeShort">Short</system:String>
<system:String x:Key="SearchDelayTimeVeryShort">Very short</system:String>
<!-- Setting Plugin --> <!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String> <system:String x:Key="searchplugin">Search Plugin</system:String>
@ -118,6 +130,8 @@
<system:String x:Key="currentActionKeywords">Palabra clave actual</system:String> <system:String x:Key="currentActionKeywords">Palabra clave actual</system:String>
<system:String x:Key="newActionKeyword">Nueva palabra clave</system:String> <system:String x:Key="newActionKeyword">Nueva palabra clave</system:String>
<system:String x:Key="actionKeywordsTooltip">Cambiar palabras clave</system:String> <system:String x:Key="actionKeywordsTooltip">Cambiar palabras clave</system:String>
<system:String x:Key="pluginSearchDelayTime">Plugin seach delay time</system:String>
<system:String x:Key="pluginSearchDelayTimeTooltip">Change Plugin Seach Delay Time</system:String>
<system:String x:Key="currentPriority">Prioridad Actual</system:String> <system:String x:Key="currentPriority">Prioridad Actual</system:String>
<system:String x:Key="newPriority">Nueva Prioridad</system:String> <system:String x:Key="newPriority">Nueva Prioridad</system:String>
<system:String x:Key="priority">Prioridad</system:String> <system:String x:Key="priority">Prioridad</system:String>
@ -131,6 +145,9 @@
<system:String x:Key="plugin_uninstall">Uninstall</system:String> <system:String x:Key="plugin_uninstall">Uninstall</system:String>
<system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String> <system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String>
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String> <system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="default">Default</system:String>
<!-- Setting Plugin Store --> <!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Tienda de Plugins</system:String> <system:String x:Key="pluginStore">Tienda de Plugins</system:String>
@ -193,8 +210,20 @@
<system:String x:Key="AnimationSpeedCustom">Custom</system:String> <system:String x:Key="AnimationSpeedCustom">Custom</system:String>
<system:String x:Key="Clock">Clock</system:String> <system:String x:Key="Clock">Clock</system:String>
<system:String x:Key="Date">Date</system:String> <system:String x:Key="Date">Date</system:String>
<system:String x:Key="BackdropType">Backdrop Type</system:String>
<system:String x:Key="BackdropTypeDisabledToolTip">Backdrop supported starting from Windows 11 build 22000 and above</system:String>
<system:String x:Key="BackdropTypesNone">None</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
<system:String x:Key="BackdropTypesMicaAlt">Mica Alt</system:String>
<system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String> <system:String x:Key="TypeIsDarkToolTip">This theme supports two(light/dark) modes.</system:String>
<system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String> <system:String x:Key="TypeHasBlurToolTip">This theme supports Blur Transparent Background.</system:String>
<system:String x:Key="ShowPlaceholder">Show placeholder</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
<system:String x:Key="PlaceholderText">Placeholder text</system:String>
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<!-- Setting Hotkey --> <!-- Setting Hotkey -->
<system:String x:Key="hotkey">Tecla Rápida</system:String> <system:String x:Key="hotkey">Tecla Rápida</system:String>
@ -294,6 +323,9 @@
<system:String x:Key="logfolder">Carpeta de registros</system:String> <system:String x:Key="logfolder">Carpeta de registros</system:String>
<system:String x:Key="clearlogfolder">Clear Logs</system:String> <system:String x:Key="clearlogfolder">Clear Logs</system:String>
<system:String x:Key="clearlogfolderMessage">Are you sure you want to delete all logs?</system:String> <system:String x:Key="clearlogfolderMessage">Are you sure you want to delete all logs?</system:String>
<system:String x:Key="clearcachefolder">Clear Caches</system:String>
<system:String x:Key="clearcachefolderMessage">Are you sure you want to delete all caches?</system:String>
<system:String x:Key="clearfolderfailMessage">Failed to clear part of folders and files. Please see log file for more information</system:String>
<system:String x:Key="welcomewindow">Asistente</system:String> <system:String x:Key="welcomewindow">Asistente</system:String>
<system:String x:Key="userdatapath">User Data Location</system:String> <system:String x:Key="userdatapath">User Data Location</system:String>
<system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String> <system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String>
@ -335,9 +367,16 @@
<system:String x:Key="cannotFindSpecifiedPlugin">No se puede encontrar el plugin especificado</system:String> <system:String x:Key="cannotFindSpecifiedPlugin">No se puede encontrar el plugin especificado</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">La nueva palabra clave no puede estar vacía</system:String> <system:String x:Key="newActionKeywordsCannotBeEmpty">La nueva palabra clave no puede estar vacía</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">Esta palabra clave ya está asignada a otro plugin, por favor elija una diferente</system:String> <system:String x:Key="newActionKeywordsHasBeenAssigned">Esta palabra clave ya está asignada a otro plugin, por favor elija una diferente</system:String>
<system:String x:Key="newActionKeywordsSameAsOld">This new Action Keyword is the same as old, please choose a different one</system:String>
<system:String x:Key="success">Éxito</system:String> <system:String x:Key="success">Éxito</system:String>
<system:String x:Key="completedSuccessfully">Completado con éxito</system:String> <system:String x:Key="completedSuccessfully">Completado con éxito</system:String>
<system:String x:Key="actionkeyword_tips">Introduzca la palabra clave que desea utilizar para iniciar el plugin. Utilice * si no desea especificar ninguno, y el plugin se activará sin ninguna palabra clave.</system:String> <system:String x:Key="actionkeyword_tips">Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>
<!-- Search Delay Settings Dialog -->
<system:String x:Key="searchDelayTimeTitle">Search Delay Time Setting</system:String>
<system:String x:Key="searchDelayTime_tips">Select the search delay time you like to use for the plugin. Select &quot;{0}&quot; if you don't want to specify any, and the plugin will use default search delay time.</system:String>
<system:String x:Key="currentSearchDelayTime">Current search delay time</system:String>
<system:String x:Key="newSearchDelayTime">New search delay time</system:String>
<!-- Custom Query Hotkey Dialog --> <!-- Custom Query Hotkey Dialog -->
<system:String x:Key="customeQueryHotkeyTitle">Tecla de Acceso Personalizada</system:String> <system:String x:Key="customeQueryHotkeyTitle">Tecla de Acceso Personalizada</system:String>

Some files were not shown because too many files have changed in this diff Show more