Merge branch 'dev' into avalonia_migration
Some checks are pending
Build / build (push) Waiting to run

Resolved conflicts:
- Calculator.csproj: Keep Mages 3.0.1 (from dev) + Avalonia packages
- Explorer SettingsViewModel.cs: Use PromptUserSelectFileAsync for ShellPath
- ExplorerSettings.xaml: Removed (deleted in HEAD)
- packages.lock.json: Restored from dev
This commit is contained in:
Shengkai Lin 2026-02-01 13:33:39 +08:00
commit c51c029263
199 changed files with 5721 additions and 10458 deletions

View file

@ -10,7 +10,7 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:

View file

@ -22,7 +22,7 @@ jobs:
NUGET_CERT_REVOCATION_MODE: offline
BUILD_NUMBER: ${{ github.run_number }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set Flow.Launcher.csproj version
id: update
uses: vers-one/dotnet-project-version-updater@v1.7
@ -56,28 +56,28 @@ jobs:
shell: powershell
run: .\Scripts\post_build.ps1
- name: Upload Plugin Nupkg
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: Plugin nupkg
path: |
Output\Release\Flow.Launcher.Plugin.*.nupkg
compression-level: 0
- name: Upload Setup
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: Flow Installer
path: |
Output\Packages\Flow-Launcher-*.exe
compression-level: 0
- name: Upload Portable Version
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: Portable Version
path: |
Output\Packages\Flow-Launcher-Portable.zip
compression-level: 0
- name: Upload Full Nupkg
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: Full nupkg
path: |
@ -85,7 +85,7 @@ jobs:
compression-level: 0
- name: Upload Release Information
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: RELEASES
path: |

View file

@ -14,4 +14,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Assign PR to creator
uses: toshimaru/auto-author-assign@v2.1.1
uses: toshimaru/auto-author-assign@v3.0.1

View file

@ -11,7 +11,7 @@ jobs:
update-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:

View file

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<!-- Work around https://github.com/dotnet/runtime/issues/109682 -->
<!-- Workaround https://github.com/dotnet/runtime/issues/109682 -->
<CETCompat>false</CETCompat>
</PropertyGroup>
</Project>

View file

@ -3,10 +3,8 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.Win32;
using Squirrel;
@ -17,8 +15,6 @@ namespace Flow.Launcher.Core.Configuration
{
private static readonly string ClassName = nameof(Portable);
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
/// <summary>
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
/// </summary>
@ -45,13 +41,13 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.PortableDataPath);
API.ShowMsgBox(API.GetTranslation("restartToDisablePortableMode"));
PublicApi.Instance.ShowMsgBox(Localize.restartToDisablePortableMode());
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
catch (Exception e)
{
API.LogException(ClassName, "Error occurred while disabling portable mode", e);
PublicApi.Instance.LogException(ClassName, "Error occurred while disabling portable mode", e);
}
}
@ -68,13 +64,13 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.RoamingDataPath);
API.ShowMsgBox(API.GetTranslation("restartToEnablePortableMode"));
PublicApi.Instance.ShowMsgBox(Localize.restartToEnablePortableMode());
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
catch (Exception e)
{
API.LogException(ClassName, "Error occurred while enabling portable mode", e);
PublicApi.Instance.LogException(ClassName, "Error occurred while enabling portable mode", e);
}
}
@ -94,13 +90,13 @@ namespace Flow.Launcher.Core.Configuration
public void MoveUserDataFolder(string fromLocation, string toLocation)
{
FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
FilesFolders.CopyAll(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s));
VerifyUserDataAfterMove(fromLocation, toLocation);
}
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
{
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s));
}
public void CreateShortcuts()
@ -150,12 +146,12 @@ namespace Flow.Launcher.Core.Configuration
// delete it and prompt the user to pick the portable data location
if (File.Exists(roamingDataDeleteFilePath))
{
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => PublicApi.Instance.ShowMsgBox(s));
if (API.ShowMsgBox(API.GetTranslation("moveToDifferentLocation"),
if (PublicApi.Instance.ShowMsgBox(Localize.moveToDifferentLocation(),
string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
FilesFolders.OpenPath(Constant.RootDirectory, (s) => PublicApi.Instance.ShowMsgBox(s));
Environment.Exit(0);
}
@ -164,9 +160,9 @@ namespace Flow.Launcher.Core.Configuration
// delete it and notify the user about it.
else if (File.Exists(portableDataDeleteFilePath))
{
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => PublicApi.Instance.ShowMsgBox(s));
API.ShowMsgBox(API.GetTranslation("shortcutsUninstallerCreated"));
PublicApi.Instance.ShowMsgBox(Localize.shortcutsUninstallerCreated());
}
}
@ -177,8 +173,7 @@ namespace Flow.Launcher.Core.Configuration
if (roamingLocationExists && portableLocationExists)
{
API.ShowMsgBox(string.Format(API.GetTranslation("userDataDuplicated"),
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
PublicApi.Instance.ShowMsgBox(Localize.userDataDuplicated(DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
return false;
}

View file

@ -8,7 +8,6 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Plugin;
@ -18,13 +17,9 @@ namespace Flow.Launcher.Core.ExternalPlugins
{
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 List<UserPlugin> plugins = new();
private List<UserPlugin> plugins = [];
private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new()
{
@ -41,7 +36,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
/// </remarks>
public async Task<List<UserPlugin>> FetchAsync(CancellationToken token)
{
API.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
PublicApi.Instance.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl);
@ -59,40 +54,40 @@ namespace Flow.Launcher.Core.ExternalPlugins
.ConfigureAwait(false);
latestEtag = response.Headers.ETag?.Tag;
API.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
PublicApi.Instance.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
return plugins;
}
else if (response.StatusCode == HttpStatusCode.NotModified)
{
API.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
PublicApi.Instance.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
return plugins;
}
else
{
API.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
PublicApi.Instance.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
return null;
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
API.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller.");
PublicApi.Instance.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller.");
return null;
}
catch (TaskCanceledException)
{
// Likely an HttpClient timeout or external cancellation not requested by our token
API.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out.");
PublicApi.Instance.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out.");
return null;
}
catch (Exception e)
{
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
{
API.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
PublicApi.Instance.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
}
else
{
API.LogException(ClassName, "Error Occurred", e);
PublicApi.Instance.LogException(ClassName, "Error Occurred", e);
}
return null;
}

View file

@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -15,7 +14,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
private static readonly string ClassName = nameof(AbstractPluginEnvironment);
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
protected readonly IPublicAPI API = PublicApi.Instance;
internal abstract string Language { get; }
@ -58,15 +57,10 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
}
var noRuntimeMessage = string.Format(
API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
Language,
EnvName,
Environment.NewLine
);
var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine);
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{
var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
var msg = Localize.runtimePluginChooseRuntimeExecutable(EnvName);
var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
@ -77,12 +71,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
// Nothing selected because user pressed cancel from the file dialog window
else
{
var forceDownloadMessage = string.Format(
API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
Language,
EnvName,
Environment.NewLine
);
var forceDownloadMessage = Localize.runtimeExecutableInvalidChooseDownload(Language, EnvName, Environment.NewLine);
// Let users select valid path or choose to download
while (string.IsNullOrEmpty(selectedFile))
@ -120,7 +109,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
else
{
API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
API.ShowMsgBox(Localize.runtimePluginUnableToSetExecutablePath(Language));
API.LogError(ClassName,
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment");
@ -248,7 +237,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
private static string GetUpdatedEnvironmentPath(string filePath)
{
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
// get the substring after "Environments" because we can not determine it dynamically
var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";

View file

@ -51,7 +51,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
catch (System.Exception e)
{
API.ShowMsgError(API.GetTranslation("failToInstallPythonEnv"));
API.ShowMsgError(Localize.failToInstallPythonEnv());
API.LogException(ClassName, "Failed to install Python environment", e);
}
});

View file

@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
catch (System.Exception e)
{
API.ShowMsgError(API.GetTranslation("failToInstallTypeScriptEnv"));
API.ShowMsgError(Localize.failToInstallTypeScriptEnv());
API.LogException(ClassName, "Failed to install TypeScript environment", e);
}
});

View file

@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
catch (System.Exception e)
{
API.ShowMsgError(API.GetTranslation("failToInstallTypeScriptEnv"));
API.ShowMsgError(Localize.failToInstallTypeScriptEnv());
API.LogException(ClassName, "Failed to install TypeScript environment", e);
}
});

View file

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
using Flow.Launcher.Infrastructure;
@ -23,10 +22,6 @@ namespace Flow.Launcher.Core.ExternalPlugins
private static DateTime lastFetchedAt = DateTime.MinValue;
private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
// 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<UserPlugin> UserPlugins { get; private set; }
public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
@ -67,7 +62,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
}
catch (Exception e)
{
API.LogException(ClassName, "Http request failed", e);
PublicApi.Instance.LogException(ClassName, "Http request failed", e);
}
finally
{
@ -90,12 +85,12 @@ namespace Flow.Launcher.Core.ExternalPlugins
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. "
PublicApi.Instance.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. "
+ "Plugin excluded from manifest", e);
return false;
}
API.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, "
PublicApi.Instance.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, "
+ $"but current version is {Constant.Version}. Plugin excluded from manifest.");
return false;

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
@ -34,6 +34,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -55,6 +56,7 @@
<ItemGroup>
<PackageReference Include="Droplex" Version="1.7.0" />
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
<PackageReference Include="FSharp.Core" Version="9.0.303" />
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.5" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
@ -63,6 +65,17 @@
<PackageReference Include="squirrel.windows" Version="1.9.0" NoWarn="NU1701" />
<PackageReference Include="StreamJsonRpc" Version="2.22.11" />
</ItemGroup>
<PropertyGroup>
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Remove="Languages\en.xaml" />
<AdditionalFiles Include="..\Flow.Launcher\Languages\en.xaml">
<Link>Languages\en.xaml</Link>
</AdditionalFiles>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />

View file

@ -0,0 +1,12 @@
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin;
public interface IResultUpdateRegister
{
/// <summary>
/// Register a plugin to receive results updated event.
/// </summary>
/// <param name="pair"></param>
void RegisterResultsUpdatedEvent(PluginPair pair);
}

View file

@ -285,7 +285,7 @@ namespace Flow.Launcher.Core.Plugin
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftMargin,
Content = API.GetTranslation("select")
Content = Localize.select()
};
Btn.Click += (_, _) =>

View file

@ -100,11 +100,11 @@ namespace Flow.Launcher.Core.Plugin
RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API));
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((rawQuery, response) =>
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((trimmedQuery, response) =>
{
var results = ParseResults(response);
ResultsUpdated?.Invoke(this,
new ResultUpdatedEventArgs { Query = new Query() { RawQuery = rawQuery }, Results = results });
new ResultUpdatedEventArgs { Query = new Query() { TrimmedQuery = trimmedQuery }, Results = results });
}));
RPC.SynchronizationContext = null;
RPC.StartListening();

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
@ -6,7 +6,6 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;
using System.Text.Json;
using Flow.Launcher.Infrastructure.UserSettings;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Plugin
{
@ -14,10 +13,6 @@ namespace Flow.Launcher.Core.Plugin
{
private static readonly string ClassName = nameof(PluginConfig);
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
/// <summary>
/// Parse plugin metadata in the given directories
/// </summary>
@ -39,7 +34,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Can't delete <{directory}>", e);
PublicApi.Instance.LogException(ClassName, $"Can't delete <{directory}>", e);
}
}
else
@ -56,7 +51,7 @@ namespace Flow.Launcher.Core.Plugin
duplicateList
.ForEach(
x => API.LogWarn(ClassName,
x => PublicApi.Instance.LogWarn(ClassName,
string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " +
"not loaded due to version not the highest of the duplicates",
x.Name, x.ID, x.Version),
@ -108,7 +103,7 @@ namespace Flow.Launcher.Core.Plugin
string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName);
if (!File.Exists(configPath))
{
API.LogError(ClassName, $"Didn't find config file <{configPath}>");
PublicApi.Instance.LogError(ClassName, $"Didn't find config file <{configPath}>");
return null;
}
@ -124,19 +119,19 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
PublicApi.Instance.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
return null;
}
if (!AllowedLanguage.IsAllowed(metadata.Language))
{
API.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
PublicApi.Instance.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
return null;
}
if (!File.Exists(metadata.ExecuteFilePath))
{
API.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
PublicApi.Instance.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
return null;
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
@ -22,10 +22,6 @@ public static class PluginInstaller
private static readonly Settings Settings = Ioc.Default.GetRequiredService<Settings>();
// 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>
/// Installs a plugin and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
/// </summary>
@ -33,18 +29,16 @@ public static class PluginInstaller
/// <returns>A Task representing the asynchronous install operation.</returns>
public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin)
{
if (API.PluginModified(newPlugin.ID))
if (PublicApi.Instance.PluginModified(newPlugin.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), newPlugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(newPlugin.Name),
Localize.pluginModifiedAlreadyMessage());
return;
}
if (API.ShowMsgBox(
string.Format(
API.GetTranslation("InstallPromptSubtitle"),
newPlugin.Name, newPlugin.Author, Environment.NewLine),
API.GetTranslation("InstallPromptTitle"),
if (PublicApi.Instance.ShowMsgBox(
Localize.InstallPromptSubtitle(newPlugin.Name, newPlugin.Author, Environment.NewLine),
Localize.InstallPromptTitle(),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
try
@ -61,7 +55,7 @@ public static class PluginInstaller
if (!newPlugin.IsFromLocalInstallPath)
{
await DownloadFileAsync(
$"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}",
$"{Localize.DownloadingPlugin()} {newPlugin.Name}",
newPlugin.UrlDownload, filePath, cts);
}
else
@ -80,7 +74,7 @@ public static class PluginInstaller
throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath);
}
if (!API.InstallPlugin(newPlugin, filePath))
if (!PublicApi.Instance.InstallPlugin(newPlugin, filePath))
{
return;
}
@ -92,23 +86,20 @@ public static class PluginInstaller
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to install plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin"));
PublicApi.Instance.LogException(ClassName, "Failed to install plugin", e);
PublicApi.Instance.ShowMsgError(Localize.ErrorInstallingPlugin());
return; // do not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
API.RestartApp();
PublicApi.Instance.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("installbtn"),
string.Format(
API.GetTranslation(
"InstallSuccessNoRestart"),
newPlugin.Name));
PublicApi.Instance.ShowMsg(
Localize.installbtn(),
Localize.InstallSuccessNoRestart(newPlugin.Name));
}
}
@ -133,24 +124,23 @@ public static class PluginInstaller
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to validate zip file", e);
API.ShowMsgError(API.GetTranslation("ZipFileNotHavePluginJson"));
PublicApi.Instance.LogException(ClassName, "Failed to validate zip file", e);
PublicApi.Instance.ShowMsgError(Localize.ZipFileNotHavePluginJson());
return;
}
if (API.PluginModified(plugin.ID))
if (PublicApi.Instance.PluginModified(plugin.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
Localize.pluginModifiedAlreadyMessage());
return;
}
if (Settings.ShowUnknownSourceWarning)
{
if (!InstallSourceKnown(plugin.Website)
&& API.ShowMsgBox(string.Format(
API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine),
API.GetTranslation("InstallFromUnknownSourceTitle"),
&& PublicApi.Instance.ShowMsgBox(Localize.InstallFromUnknownSourceSubtitle(Environment.NewLine),
Localize.InstallFromUnknownSourceTitle(),
MessageBoxButton.YesNo) == MessageBoxResult.No)
return;
}
@ -165,51 +155,46 @@ public static class PluginInstaller
/// <returns>A Task representing the asynchronous uninstall operation.</returns>
public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin)
{
if (API.PluginModified(oldPlugin.ID))
if (PublicApi.Instance.PluginModified(oldPlugin.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), oldPlugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(oldPlugin.Name),
Localize.pluginModifiedAlreadyMessage());
return;
}
if (API.ShowMsgBox(
string.Format(
API.GetTranslation("UninstallPromptSubtitle"),
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
API.GetTranslation("UninstallPromptTitle"),
if (PublicApi.Instance.ShowMsgBox(
Localize.UninstallPromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
Localize.UninstallPromptTitle(),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
var removePluginSettings = API.ShowMsgBox(
API.GetTranslation("KeepPluginSettingsSubtitle"),
API.GetTranslation("KeepPluginSettingsTitle"),
var removePluginSettings = PublicApi.Instance.ShowMsgBox(
Localize.KeepPluginSettingsSubtitle(),
Localize.KeepPluginSettingsTitle(),
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
try
{
if (!await API.UninstallPluginAsync(oldPlugin, removePluginSettings))
if (!await PublicApi.Instance.UninstallPluginAsync(oldPlugin, removePluginSettings))
{
return;
}
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to uninstall plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin"));
PublicApi.Instance.LogException(ClassName, "Failed to uninstall plugin", e);
PublicApi.Instance.ShowMsgError(Localize.ErrorUninstallingPlugin());
return; // don not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
API.RestartApp();
PublicApi.Instance.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("uninstallbtn"),
string.Format(
API.GetTranslation(
"UninstallSuccessNoRestart"),
oldPlugin.Name));
PublicApi.Instance.ShowMsg(
Localize.uninstallbtn(),
Localize.UninstallSuccessNoRestart(oldPlugin.Name));
}
}
@ -221,11 +206,9 @@ public static class PluginInstaller
/// <returns>A Task representing the asynchronous update operation.</returns>
public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin)
{
if (API.ShowMsgBox(
string.Format(
API.GetTranslation("UpdatePromptSubtitle"),
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
API.GetTranslation("UpdatePromptTitle"),
if (PublicApi.Instance.ShowMsgBox(
Localize.UpdatePromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
Localize.UpdatePromptTitle(),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
try
@ -237,7 +220,7 @@ public static class PluginInstaller
if (!newPlugin.IsFromLocalInstallPath)
{
await DownloadFileAsync(
$"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}",
$"{Localize.DownloadingPlugin()} {newPlugin.Name}",
newPlugin.UrlDownload, filePath, cts);
}
else
@ -251,30 +234,27 @@ public static class PluginInstaller
return;
}
if (!await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath))
if (!await PublicApi.Instance.UpdatePluginAsync(oldPlugin, newPlugin, filePath))
{
return;
}
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to update plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e);
PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin());
return; // do not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
API.RestartApp();
PublicApi.Instance.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("updatebtn"),
string.Format(
API.GetTranslation(
"UpdateSuccessNoRestart"),
newPlugin.Name));
PublicApi.Instance.ShowMsg(
Localize.updatebtn(),
Localize.UpdateSuccessNoRestart(newPlugin.Name));
}
}
@ -289,17 +269,17 @@ public static class PluginInstaller
public static async Task CheckForPluginUpdatesAsync(Action<List<PluginUpdateInfo>> updateAllPlugins, bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default)
{
// Update the plugin manifest
await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
await PublicApi.Instance.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
// Get all plugins that can be updated
var resultsForUpdate = (
from existingPlugin in API.GetAllPlugins()
join pluginUpdateSource in API.GetPluginManifest()
from existingPlugin in PublicApi.Instance.GetAllPlugins()
join pluginUpdateSource in PublicApi.Instance.GetPluginManifest()
on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
StringComparison.InvariantCulture) <
0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
&& !API.PluginModified(existingPlugin.Metadata.ID)
&& !PublicApi.Instance.PluginModified(existingPlugin.Metadata.ID)
select
new PluginUpdateInfo()
{
@ -314,25 +294,25 @@ public static class PluginInstaller
}).ToList();
// No updates
if (!resultsForUpdate.Any())
if (resultsForUpdate.Count == 0)
{
if (!silentUpdate)
{
API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle"));
PublicApi.Instance.ShowMsg(Localize.updateNoResultTitle(), Localize.updateNoResultSubtitle());
}
return;
}
// If all plugins are modified, just return
if (resultsForUpdate.All(x => API.PluginModified(x.ID)))
if (resultsForUpdate.All(x => PublicApi.Instance.PluginModified(x.ID)))
{
return;
}
// Show message box with button to update all plugins
API.ShowMsgWithButton(
API.GetTranslation("updateAllPluginsTitle"),
API.GetTranslation("updateAllPluginsButtonContent"),
PublicApi.Instance.ShowMsgWithButton(
Localize.updateAllPluginsTitle(),
Localize.updateAllPluginsButtonContent(),
() =>
{
updateAllPlugins(resultsForUpdate);
@ -357,7 +337,7 @@ public static class PluginInstaller
using var cts = new CancellationTokenSource();
await DownloadFileAsync(
$"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}",
$"{Localize.DownloadingPlugin()} {plugin.PluginNewUserPlugin.Name}",
plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
// check if user cancelled download before installing plugin
@ -366,7 +346,7 @@ public static class PluginInstaller
return;
}
if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
if (!await PublicApi.Instance.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
{
return;
}
@ -375,8 +355,8 @@ public static class PluginInstaller
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to update plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e);
PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin());
}
}));
@ -384,13 +364,13 @@ public static class PluginInstaller
if (restart)
{
API.RestartApp();
PublicApi.Instance.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("updatebtn"),
API.GetTranslation("PluginsUpdateSuccessNoRestart"));
PublicApi.Instance.ShowMsg(
Localize.updatebtn(),
Localize.PluginsUpdateSuccessNoRestart());
}
}
@ -412,7 +392,7 @@ public static class PluginInstaller
if (showProgress)
{
var exceptionHappened = false;
await API.ShowProgressBoxAsync(progressBoxTitle,
await PublicApi.Instance.ShowProgressBoxAsync(progressBoxTitle,
async (reportProgress) =>
{
if (reportProgress == null)
@ -424,18 +404,18 @@ public static class PluginInstaller
}
else
{
await API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
}
}, cts.Cancel);
// if exception happened while downloading and user does not cancel downloading,
// we need to redownload the plugin
if (exceptionHappened && (!cts.IsCancellationRequested))
await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
else
{
await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
}
@ -462,7 +442,7 @@ public static class PluginInstaller
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost)
return false;
return API.GetAllPlugins().Any(x =>
return PublicApi.Instance.GetAllPlugins().Any(x =>
!string.IsNullOrEmpty(x.Metadata.Website) &&
x.Metadata.Website.StartsWith(constructedUrlPart)
);

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@ -6,8 +6,8 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
@ -25,48 +25,36 @@ namespace Flow.Launcher.Core.Plugin
{
private static readonly string ClassName = nameof(PluginManager);
public static List<PluginPair> AllPlugins { get; private set; }
public static readonly HashSet<PluginPair> GlobalPlugins = new();
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
private static readonly ConcurrentDictionary<string, PluginPair> _allLoadedPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _allInitializedPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _initFailedPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _globalPlugins = [];
private static readonly ConcurrentDictionary<string, PluginPair> _nonGlobalPlugins = [];
private static PluginsSettings Settings;
private static readonly ConcurrentBag<string> ModifiedPlugins = new();
private static readonly ConcurrentBag<string> ModifiedPlugins = [];
private static IEnumerable<PluginPair> _contextMenuPlugins;
private static IEnumerable<PluginPair> _homePlugins;
private static IEnumerable<PluginPair> _resultUpdatePlugin;
private static IEnumerable<PluginPair> _translationPlugins;
private static readonly List<DialogJumpExplorerPair> _dialogJumpExplorerPlugins = new();
private static readonly List<DialogJumpDialogPair> _dialogJumpDialogPlugins = new();
private static readonly ConcurrentBag<PluginPair> _contextMenuPlugins = [];
private static readonly ConcurrentBag<PluginPair> _homePlugins = [];
private static readonly ConcurrentBag<PluginPair> _translationPlugins = [];
private static readonly ConcurrentBag<PluginPair> _externalPreviewPlugins = [];
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
public static readonly string[] Directories =
{
[
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
};
];
private static void DeletePythonBinding()
{
const string binding = "flowlauncher.py";
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
{
File.Delete(Path.Combine(subDirectory, binding));
}
}
#region Save & Dispose & Reload Plugin
/// <summary>
/// Save json and ISavable
/// </summary>
public static void Save()
{
foreach (var pluginPair in AllPlugins)
foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: false))
{
var savable = pluginPair.Plugin as ISavable;
try
@ -75,17 +63,18 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
}
}
API.SavePluginSettings();
API.SavePluginCaches();
PublicApi.Instance.SavePluginSettings();
PublicApi.Instance.SavePluginCaches();
}
public static async ValueTask DisposePluginsAsync()
{
foreach (var pluginPair in AllPlugins)
// Still call dispose for all plugins even if initialization failed, so that we can clean up resources
foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: true))
{
await DisposePluginAsync(pluginPair);
}
@ -107,55 +96,59 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
}
}
public static async Task ReloadDataAsync()
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
{
IReloadable p => Task.Run(p.ReloadData),
IAsyncReloadable p => p.ReloadDataAsync(),
_ => Task.CompletedTask,
}).ToArray());
})]);
}
#endregion
#region External Preview
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
}).ToArray());
})]);
}
public static async Task CloseExternalPreviewAsync()
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.ClosePreviewAsync(),
_ => Task.CompletedTask,
}).ToArray());
})]);
}
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
}).ToArray());
})]);
}
public static bool UseExternalPreview()
{
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled);
return GetExternalPreviewPlugins().Any(x => !x.Metadata.Disabled);
}
public static bool AllowAlwaysPreview()
{
var plugin = GetPluginsForInterface<IAsyncExternalPreview>().FirstOrDefault(x => !x.Metadata.Disabled);
var plugin = GetExternalPreviewPlugins().FirstOrDefault(x => !x.Metadata.Disabled);
if (plugin is null)
return false;
@ -163,6 +156,15 @@ namespace Flow.Launcher.Core.Plugin
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
}
private static IList<PluginPair> GetExternalPreviewPlugins()
{
return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
#endregion
#region Constructor
static PluginManager()
{
// validate user directory
@ -171,9 +173,28 @@ namespace Flow.Launcher.Core.Plugin
DeletePythonBinding();
}
private static void DeletePythonBinding()
{
const string binding = "flowlauncher.py";
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
{
try
{
File.Delete(Path.Combine(subDirectory, binding));
}
catch (Exception e)
{
PublicApi.Instance.LogDebug(ClassName, $"Failed to delete {binding} in {subDirectory}: {e.Message}");
}
}
}
#endregion
#region Load & Initialize Plugins
/// <summary>
/// because InitializePlugins needs API, so LoadPlugins needs to be called first
/// todo happlebao The API should be removed
/// Load plugins from the directories specified in Directories.
/// </summary>
/// <param name="settings"></param>
public static void LoadPlugins(PluginsSettings settings)
@ -181,33 +202,22 @@ namespace Flow.Launcher.Core.Plugin
var metadatas = PluginConfig.Parse(Directories);
Settings = settings;
Settings.UpdatePluginSettings(metadatas);
AllPlugins = PluginsLoader.Plugins(metadatas, Settings);
// Load plugins
var allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings);
foreach (var plugin in allLoadedPlugins)
{
if (plugin != null)
{
if (!_allLoadedPlugins.TryAdd(plugin.Metadata.ID, plugin))
{
PublicApi.Instance.LogError(ClassName, $"Plugin with ID {plugin.Metadata.ID} already loaded");
}
}
}
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
UpdatePluginDirectory(metadatas);
// Initialize plugin enumerable after all plugins are initialized
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
_resultUpdatePlugin = GetPluginsForInterface<IResultUpdated>();
_translationPlugins = GetPluginsForInterface<IPluginI18n>();
// Initialize Dialog Jump plugin pairs
foreach (var pair in GetPluginsForInterface<IDialogJumpExplorer>())
{
_dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair
{
Plugin = (IDialogJumpExplorer)pair.Plugin,
Metadata = pair.Metadata
});
}
foreach (var pair in GetPluginsForInterface<IDialogJumpDialog>())
{
_dialogJumpDialogPlugins.Add(new DialogJumpDialogPair
{
Plugin = (IDialogJumpDialog)pair.Plugin,
Metadata = pair.Metadata
});
}
}
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
@ -218,7 +228,7 @@ namespace Flow.Launcher.Core.Plugin
{
if (string.IsNullOrEmpty(metadata.AssemblyName))
{
API.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}");
PublicApi.Instance.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}");
continue; // Skip if AssemblyName is not set, which can happen for erroneous plugins
}
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName);
@ -228,7 +238,7 @@ namespace Flow.Launcher.Core.Plugin
{
if (string.IsNullOrEmpty(metadata.Name))
{
API.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}");
PublicApi.Instance.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}");
continue; // Skip if Name is not set, which can happen for erroneous plugins
}
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name);
@ -238,106 +248,146 @@ namespace Flow.Launcher.Core.Plugin
}
/// <summary>
/// Call initialize for all plugins
/// Initialize all plugins asynchronously.
/// </summary>
/// <param name="register">The register to register results updated event for each plugin.</param>
/// <returns>return the list of failed to init plugins or null for none</returns>
public static async Task InitializePluginsAsync()
public static async Task InitializePluginsAsync(IResultUpdateRegister register)
{
var failedPlugins = new ConcurrentQueue<PluginPair>();
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
var initTasks = _allLoadedPlugins.Select(x => Task.Run(async () =>
{
var pair = x.Value;
// Register plugin action keywords so that plugins can be queried in results
RegisterPluginActionKeywords(pair);
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, PublicApi.Instance)));
pair.Metadata.InitTime += milliseconds;
API.LogInfo(ClassName,
PublicApi.Instance.LogInfo(ClassName,
$"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
}
catch (Exception e)
{
API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
if (pair.Metadata.Disabled && pair.Metadata.HomeDisabled)
{
// If this plugin is already disabled, do not show error message again
// Or else it will be shown every time
API.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error");
PublicApi.Instance.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error");
}
else
{
pair.Metadata.Disabled = true;
pair.Metadata.HomeDisabled = true;
failedPlugins.Enqueue(pair);
API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
PublicApi.Instance.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
}
// Even if the plugin cannot be initialized, we still need to add it in all plugin list so that
// we can remove the plugin from Plugin or Store page or Plugin Manager plugin.
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
_initFailedPlugins.TryAdd(pair.Metadata.ID, pair);
return;
}
// Register ResultsUpdated event so that plugin query can use results updated interface
register.RegisterResultsUpdatedEvent(pair);
// Update plugin metadata translation after the plugin is initialized with IPublicAPI instance
Internationalization.UpdatePluginMetadataTranslation(pair);
// Add plugin to Dialog Jump plugin list after the plugin is initialized
DialogJump.InitializeDialogJumpPlugin(pair);
// Add plugin to lists after the plugin is initialized
AddPluginToLists(pair);
}));
await Task.WhenAll(InitTasks);
await Task.WhenAll(initTasks);
foreach (var plugin in AllPlugins)
if (!_initFailedPlugins.IsEmpty)
{
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
// has multiple global and action keywords because we will only add them here once.
foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct())
{
switch (actionKeyword)
{
case Query.GlobalPluginWildcardSign:
GlobalPlugins.Add(plugin);
break;
default:
NonGlobalPlugins[actionKeyword] = plugin;
break;
}
}
}
if (failedPlugins.Any())
{
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
API.ShowMsg(
API.GetTranslation("failedToInitializePluginsTitle"),
string.Format(
API.GetTranslation("failedToInitializePluginsMessage"),
failed
),
var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name));
PublicApi.Instance.ShowMsg(
Localize.failedToInitializePluginsTitle(),
Localize.failedToInitializePluginsMessage(failed),
"",
false
);
}
}
private static void RegisterPluginActionKeywords(PluginPair pair)
{
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
// has multiple global and action keywords because we will only add them here once.
foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct())
{
switch (actionKeyword)
{
case Query.GlobalPluginWildcardSign:
_globalPlugins.TryAdd(pair.Metadata.ID, pair);
break;
default:
_nonGlobalPlugins.TryAdd(actionKeyword, pair);
break;
}
}
}
private static void AddPluginToLists(PluginPair pair)
{
if (pair.Plugin is IContextMenu)
{
_contextMenuPlugins.Add(pair);
}
if (pair.Plugin is IAsyncHomeQuery)
{
_homePlugins.Add(pair);
}
if (pair.Plugin is IPluginI18n)
{
_translationPlugins.Add(pair);
}
if (pair.Plugin is IAsyncExternalPreview)
{
_externalPreviewPlugins.Add(pair);
}
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
}
#endregion
#region Validate & Query Plugins
public static ICollection<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump)
{
if (query is null)
return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
{
if (dialogJump)
return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList();
return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))];
else
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))];
}
if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
return Array.Empty<PluginPair>();
if (API.PluginModified(plugin.Metadata.ID))
if (PluginModified(plugin.Metadata.ID))
return Array.Empty<PluginPair>();
return new List<PluginPair>
{
plugin
};
return [plugin];
}
public static ICollection<PluginPair> ValidPluginsForHomeQuery()
{
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
return [.. _homePlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
@ -345,9 +395,31 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<Result>();
var metadata = pair.Metadata;
if (IsPluginInitializing(metadata))
{
Result r = new()
{
Title = Localize.pluginStillInitializing(metadata.Name),
SubTitle = Localize.pluginStillInitializingSubtitle(),
AutoCompleteText = query.TrimmedQuery,
IcoPath = metadata.IcoPath,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
OriginQuery = query,
Action = _ =>
{
PublicApi.Instance.ReQuery();
return false;
}
};
results.Add(r);
return results;
}
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
@ -369,15 +441,15 @@ namespace Flow.Launcher.Core.Plugin
{
Result r = new()
{
Title = $"{metadata.Name}: Failed to respond!",
SubTitle = "Select this result for more info",
Title = Localize.pluginFailedToRespond(metadata.Name),
SubTitle = Localize.pluginFailedToRespondSubtitle(),
AutoCompleteText = query.TrimmedQuery,
IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
OriginQuery = query,
Action = _ => { throw new FlowPluginException(metadata, e);},
Score = -100
Action = _ => { throw new FlowPluginException(metadata, e);}
};
results.Add(r);
}
@ -389,9 +461,31 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<Result>();
var metadata = pair.Metadata;
if (IsPluginInitializing(metadata))
{
Result r = new()
{
Title = Localize.pluginStillInitializing(metadata.Name),
SubTitle = Localize.pluginStillInitializingSubtitle(),
AutoCompleteText = query.TrimmedQuery,
IcoPath = metadata.IcoPath,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
OriginQuery = query,
Action = _ =>
{
PublicApi.Instance.ReQuery();
return false;
}
};
results.Add(r);
return results;
}
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await ((IAsyncHomeQuery)pair.Plugin).HomeQueryAsync(token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
@ -408,7 +502,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e);
return null;
}
return results;
@ -419,9 +513,15 @@ namespace Flow.Launcher.Core.Plugin
var results = new List<DialogJumpResult>();
var metadata = pair.Metadata;
if (IsPluginInitializing(metadata))
{
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null;
}
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
@ -438,12 +538,58 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e);
return null;
}
return results;
}
private static bool IsPluginInitializing(PluginMetadata metadata)
{
return !_allInitializedPlugins.ContainsKey(metadata.ID);
}
#endregion
#region Get Plugin List
public static List<PluginPair> GetAllLoadedPlugins()
{
return [.. _allLoadedPlugins.Values];
}
public static List<PluginPair> GetAllInitializedPlugins(bool includeFailed)
{
if (includeFailed)
{
return [.. _allInitializedPlugins.Values];
}
else
{
return [.. _allInitializedPlugins.Values
.Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))];
}
}
private static List<PluginPair> GetGlobalPlugins()
{
return [.. _globalPlugins.Values];
}
public static Dictionary<string, PluginPair> GetNonGlobalPlugins()
{
return _nonGlobalPlugins.ToDictionary();
}
public static List<PluginPair> GetTranslationPlugins()
{
return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
#endregion
#region Update Metadata & Get Plugin
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
{
foreach (var r in results)
@ -462,28 +608,19 @@ namespace Flow.Launcher.Core.Plugin
/// <summary>
/// get specified plugin, return null if not found
/// </summary>
/// <remarks>
/// Plugin may not be initialized, so do not use its plugin model to execute any commands
/// </remarks>
/// <param name="id"></param>
/// <returns></returns>
public static PluginPair GetPluginForId(string id)
{
return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id);
return GetAllLoadedPlugins().FirstOrDefault(o => o.Metadata.ID == id);
}
private static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
{
// Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup
return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty<PluginPair>();
}
#endregion
public static IList<PluginPair> GetResultUpdatePlugin()
{
return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static IList<PluginPair> GetTranslationPlugins()
{
return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
#region Get Context Menus
public static List<Result> GetContextMenusForPlugin(Result result)
{
@ -505,7 +642,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName,
PublicApi.Instance.LogException(ClassName,
$"Can't load context menus for plugin <{pluginPair.Metadata.Name}>",
e);
}
@ -514,27 +651,82 @@ namespace Flow.Launcher.Core.Plugin
return results;
}
#endregion
#region Check Home Plugin
public static bool IsHomePlugin(string id)
{
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
}
public static IList<DialogJumpExplorerPair> GetDialogJumpExplorers()
#endregion
#region Check Initializing & Init Failed
public static bool IsInitializingOrInitFailed(string id)
{
return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
// Id does not exist in loaded plugins
if (!_allLoadedPlugins.ContainsKey(id)) return false;
// Plugin initialized already
if (_allInitializedPlugins.ContainsKey(id))
{
// Check if the plugin initialization failed
return _initFailedPlugins.ContainsKey(id);
}
// Plugin is still initializing
else
{
return true;
}
}
public static IList<DialogJumpDialogPair> GetDialogJumpDialogs()
public static bool IsInitializing(string id)
{
return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
// Id does not exist in loaded plugins
if (!_allLoadedPlugins.ContainsKey(id)) return false;
// Plugin initialized already
if (_allInitializedPlugins.ContainsKey(id))
{
return false;
}
// Plugin is still initializing
else
{
return true;
}
}
public static bool IsInitializationFailed(string id)
{
// Id does not exist in loaded plugins
if (!_allLoadedPlugins.ContainsKey(id)) return false;
// Plugin initialized already
if (_allInitializedPlugins.ContainsKey(id))
{
// Check if the plugin initialization failed
return _initFailedPlugins.ContainsKey(id);
}
// Plugin is still initializing
else
{
return false;
}
}
#endregion
#region Plugin Action Keyword
public static bool ActionKeywordRegistered(string actionKeyword)
{
// this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword);
&& _nonGlobalPlugins.ContainsKey(actionKeyword);
}
/// <summary>
@ -546,11 +738,11 @@ namespace Flow.Launcher.Core.Plugin
var plugin = GetPluginForId(id);
if (newActionKeyword == Query.GlobalPluginWildcardSign)
{
GlobalPlugins.Add(plugin);
_globalPlugins.TryAdd(id, plugin);
}
else
{
NonGlobalPlugins[newActionKeyword] = plugin;
_nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin);
}
// Update action keywords and action keyword in plugin metadata
@ -577,11 +769,13 @@ namespace Flow.Launcher.Core.Plugin
plugin.Metadata.ActionKeywords
.Count(x => x == Query.GlobalPluginWildcardSign) == 1)
{
GlobalPlugins.Remove(plugin);
_globalPlugins.TryRemove(id, out _);
}
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
NonGlobalPlugins.Remove(oldActionkeyword);
{
_nonGlobalPlugins.TryRemove(oldActionkeyword, out _);
}
// Update action keywords and action keyword in plugin metadata
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
@ -595,6 +789,12 @@ namespace Flow.Launcher.Core.Plugin
}
}
#endregion
#region Plugin Install & Uninstall & Update
#region Private Functions
private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
{
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
@ -620,12 +820,15 @@ namespace Flow.Launcher.Core.Plugin
if (!Version.TryParse(newMetadata.Version, out var newVersion))
return true; // If version is not valid, we assume it is lesser than any existing version
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
&& Version.TryParse(x.Metadata.Version, out var version)
&& newVersion <= version);
// Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists
return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == newMetadata.ID
&& Version.TryParse(x.Metadata.Version, out var version)
&& newVersion <= version);
}
#region Public functions
#endregion
#region Public Functions
public static bool PluginModified(string id)
{
@ -636,8 +839,8 @@ namespace Flow.Launcher.Core.Plugin
{
if (PluginModified(existingVersion.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), existingVersion.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(existingVersion.Name),
Localize.pluginModifiedAlreadyMessage());
return false;
}
@ -663,14 +866,14 @@ namespace Flow.Launcher.Core.Plugin
#endregion
#region Internal functions
#region Internal Functions
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
{
if (checkModified && PluginModified(plugin.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
Localize.pluginModifiedAlreadyMessage());
return false;
}
@ -689,15 +892,15 @@ namespace Flow.Launcher.Core.Plugin
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
{
API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name),
string.Format(API.GetTranslation("fileNotFoundMessage"), pluginFolderPath));
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.fileNotFoundMessage(pluginFolderPath));
return false;
}
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
{
API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name),
API.GetTranslation("pluginExistAlreadyMessage"));
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.pluginExistAlreadyMessage());
return false;
}
@ -726,7 +929,7 @@ namespace Flow.Launcher.Core.Plugin
var newPluginPath = Path.Combine(installDirectory, folderName);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s));
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s));
try
{
@ -735,7 +938,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
}
if (checkModified)
@ -750,8 +953,8 @@ namespace Flow.Launcher.Core.Plugin
{
if (checkModified && PluginModified(plugin.ID))
{
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
API.GetTranslation("pluginModifiedAlreadyMessage"));
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
Localize.pluginModifiedAlreadyMessage());
return false;
}
@ -760,7 +963,7 @@ namespace Flow.Launcher.Core.Plugin
// 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);
var pluginPairs = GetAllInitializedPlugins(includeFailed: true).Where(p => p.Metadata.ID == plugin.ID).ToList();
foreach (var pluginPair in pluginPairs)
{
await DisposePluginAsync(pluginPair);
@ -770,7 +973,7 @@ namespace Flow.Launcher.Core.Plugin
if (removePluginSettings)
{
// For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances
if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable)
if (AllowedLanguage.IsDotNet(plugin.Language) && PublicApi.Instance is IRemovable removable)
{
removable.RemovePluginSettings(plugin.AssemblyName);
removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath);
@ -784,9 +987,9 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
API.ShowMsgError(API.GetTranslation("failedToRemovePluginSettingsTitle"),
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginSettingsTitle(),
Localize.failedToRemovePluginSettingsMessage(plugin.Name));
}
}
@ -800,17 +1003,27 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
API.ShowMsgError(API.GetTranslation("failedToRemovePluginCacheTitle"),
string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name));
PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginCacheTitle(),
Localize.failedToRemovePluginCacheMessage(plugin.Name));
}
Settings.RemovePluginSettings(plugin.ID);
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID);
var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
{
_allLoadedPlugins.TryRemove(plugin.ID, out var _);
}
{
_allInitializedPlugins.TryRemove(plugin.ID, out var _);
}
{
_initFailedPlugins.TryRemove(plugin.ID, out var _);
}
{
_globalPlugins.TryRemove(plugin.ID, out var _);
}
var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
foreach (var key in keysToRemove)
{
NonGlobalPlugins.Remove(key);
_nonGlobalPlugins.TryRemove(key, out var _);
}
}
@ -826,5 +1039,7 @@ namespace Flow.Launcher.Core.Plugin
}
#endregion
#endregion
}
}

View file

@ -2,9 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins.Environments;
#pragma warning disable IDE0005
using Flow.Launcher.Infrastructure.Logger;
@ -18,10 +15,6 @@ namespace Flow.Launcher.Core.Plugin
{
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)
{
var dotnetPlugins = DotNetPlugins(metadatas);
@ -55,7 +48,7 @@ namespace Flow.Launcher.Core.Plugin
return plugins;
}
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
private static List<PluginPair> DotNetPlugins(List<PluginMetadata> source)
{
var erroredPlugins = new List<string>();
@ -64,56 +57,58 @@ namespace Flow.Launcher.Core.Plugin
foreach (var metadata in metadatas)
{
var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
{
Assembly assembly = null;
IAsyncPlugin plugin = null;
try
{
Assembly assembly = null;
IAsyncPlugin plugin = null;
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
try
{
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
metadata.AssemblyName = assembly.GetName().Name;
}
metadata.AssemblyName = assembly.GetName().Name;
}
#if DEBUG
catch (Exception)
{
throw;
}
catch (Exception)
{
throw;
}
#else
catch (Exception e) when (assembly == null)
{
Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
}
catch (InvalidOperationException e)
{
Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
}
catch (ReflectionTypeLoadException e)
{
Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
}
catch (Exception e)
{
Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
catch (Exception e) when (assembly == null)
{
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
}
catch (InvalidOperationException e)
{
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
}
catch (ReflectionTypeLoadException e)
{
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
#endif
if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
return;
}
if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
return;
}
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
});
metadata.InitTime += milliseconds;
PublicApi.Instance.LogDebug(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
}
if (erroredPlugins.Count > 0)
@ -121,12 +116,12 @@ namespace Flow.Launcher.Core.Plugin
var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
var errorMessage = erroredPlugins.Count > 1 ?
API.GetTranslation("pluginsHaveErrored") :
API.GetTranslation("pluginHasErrored");
Localize.pluginsHaveErrored():
Localize.pluginHasErrored();
API.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
PublicApi.Instance.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
API.GetTranslation("referToLogs"));
Localize.referToLogs());
}
return plugins;

View file

@ -6,15 +6,16 @@ namespace Flow.Launcher.Core.Plugin
{
public static class QueryBuilder
{
public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalPlugins)
public static Query Build(string originalQuery, string trimmedQuery, Dictionary<string, PluginPair> nonGlobalPlugins)
{
// home query
if (string.IsNullOrEmpty(text))
if (string.IsNullOrEmpty(trimmedQuery))
{
return new Query()
{
Search = string.Empty,
RawQuery = string.Empty,
OriginalQuery = string.Empty,
TrimmedQuery = string.Empty,
SearchTerms = Array.Empty<string>(),
ActionKeyword = string.Empty,
IsHomeQuery = true
@ -22,14 +23,13 @@ namespace Flow.Launcher.Core.Plugin
}
// replace multiple white spaces with one white space
var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
var terms = trimmedQuery.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
if (terms.Length == 0)
{
// nothing was typed
return null;
}
var rawQuery = text;
string actionKeyword, search;
string possibleActionKeyword = terms[0];
string[] searchTerms;
@ -38,21 +38,22 @@ namespace Flow.Launcher.Core.Plugin
{
// use non global plugin for query
actionKeyword = possibleActionKeyword;
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
search = terms.Length > 1 ? trimmedQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
searchTerms = terms[1..];
}
else
{
// non action keyword
actionKeyword = string.Empty;
search = rawQuery.TrimStart();
search = trimmedQuery.TrimStart();
searchTerms = terms;
}
return new Query()
{
Search = search,
RawQuery = rawQuery,
OriginalQuery = originalQuery,
TrimmedQuery = trimmedQuery,
SearchTerms = searchTerms,
ActionKeyword = actionKeyword,
IsHomeQuery = false

View file

@ -6,7 +6,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
@ -18,10 +17,6 @@ namespace Flow.Launcher.Core.Resource
{
private static readonly string ClassName = nameof(Internationalization);
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
private const string Folder = "Languages";
private const string DefaultLanguageCode = "en";
private const string DefaultFile = "en.xaml";
@ -104,7 +99,7 @@ namespace Flow.Launcher.Core.Resource
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
if (!Directory.Exists(directory))
{
API.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>");
PublicApi.Instance.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>");
return;
}
@ -175,7 +170,7 @@ namespace Flow.Launcher.Core.Resource
FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase));
if (language == null)
{
API.LogError(ClassName, $"Language code can't be found <{languageCode}>");
PublicApi.Instance.LogError(ClassName, $"Language code can't be found <{languageCode}>");
return AvailableLanguages.English;
}
else
@ -208,7 +203,7 @@ namespace Flow.Launcher.Core.Resource
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e);
PublicApi.Instance.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e);
}
finally
{
@ -254,7 +249,7 @@ namespace Flow.Launcher.Core.Resource
// "Do you want to search with pinyin?"
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?";
if (API.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
if (PublicApi.Instance.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
return false;
return true;
@ -311,7 +306,7 @@ namespace Flow.Launcher.Core.Resource
}
else
{
API.LogError(ClassName, $"Language path can't be found <{path}>");
PublicApi.Instance.LogError(ClassName, $"Language path can't be found <{path}>");
var english = Path.Combine(folder, DefaultFile);
if (File.Exists(english))
{
@ -319,7 +314,7 @@ namespace Flow.Launcher.Core.Resource
}
else
{
API.LogError(ClassName, $"Default English Language path can't be found <{path}>");
PublicApi.Instance.LogError(ClassName, $"Default English Language path can't be found <{path}>");
return string.Empty;
}
}
@ -354,7 +349,7 @@ namespace Flow.Launcher.Core.Resource
}
else
{
API.LogError(ClassName, $"No Translation for key {key}");
PublicApi.Instance.LogError(ClassName, $"No Translation for key {key}");
return $"No Translation for key {key}";
}
}
@ -377,11 +372,27 @@ namespace Flow.Launcher.Core.Resource
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
}
public static void UpdatePluginMetadataTranslation(PluginPair p)
{
// Update plugin metadata name & description
if (p.Plugin is not IPluginI18n pluginI18N) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
#endregion
#region IDisposable

View file

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

View file

@ -444,17 +444,27 @@ namespace Flow.Launcher.Core.Resource
_api.LogError(ClassName, $"Theme <{theme}> path can't be found");
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme));
_api.ShowMsgBox(Localize.theme_load_failure_path_not_exists(theme));
ChangeTheme(Constant.DefaultTheme);
}
return false;
}
catch (XamlParseException)
catch (XamlParseException e)
{
_api.LogError(ClassName, $"Theme <{theme}> fail to parse");
_api.LogException(ClassName, $"Theme <{theme}> fail to parse xaml", e);
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));
_api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme));
ChangeTheme(Constant.DefaultTheme);
}
return false;
}
catch (Exception e)
{
_api.LogException(ClassName, $"Theme <{theme}> fail to load", e);
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme));
ChangeTheme(Constant.DefaultTheme);
}
return false;

View file

@ -41,8 +41,8 @@ namespace Flow.Launcher.Core
try
{
if (!silentUpdate)
_api.ShowMsg(_api.GetTranslation("pleaseWait"),
_api.GetTranslation("update_flowlauncher_update_check"));
_api.ShowMsg(Localize.pleaseWait(),
Localize.update_flowlauncher_update_check());
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
@ -58,13 +58,13 @@ namespace Flow.Launcher.Core
if (newReleaseVersion <= currentVersion)
{
if (!silentUpdate)
_api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
_api.ShowMsgBox(Localize.update_flowlauncher_already_on_latest());
return;
}
if (!silentUpdate)
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
_api.GetTranslation("update_flowlauncher_updating"));
_api.ShowMsg(Localize.update_flowlauncher_update_found(),
Localize.update_flowlauncher_updating());
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
@ -77,10 +77,7 @@ namespace Flow.Launcher.Core
FilesFolders.CopyAll(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"),
DataLocation.PortableDataPath,
targetDestination));
_api.ShowMsgBox(Localize.update_flowlauncher_fail_moving_portable_user_profile_data(DataLocation.PortableDataPath, targetDestination));
}
else
{
@ -91,7 +88,7 @@ namespace Flow.Launcher.Core
_api.LogInfo(ClassName, $"Update success:{newVersionTips}");
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"),
if (_api.ShowMsgBox(newVersionTips, Localize.update_flowlauncher_new_update(),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
UpdateManager.RestartApp(Constant.ApplicationFileName);
@ -111,8 +108,8 @@ namespace Flow.Launcher.Core
}
if (!silentUpdate)
_api.ShowMsgError(_api.GetTranslation("update_flowlauncher_fail"),
_api.GetTranslation("update_flowlauncher_check_connection"));
_api.ShowMsgError(Localize.update_flowlauncher_fail(),
Localize.update_flowlauncher_check_connection());
}
finally
{
@ -150,9 +147,9 @@ namespace Flow.Launcher.Core
return manager;
}
private string NewVersionTips(string version)
private static string NewVersionTips(string version)
{
var tips = string.Format(_api.GetTranslation("newVersionTips"), version);
var tips = Localize.newVersionTips(version);
return tips;
}

View file

@ -11,6 +11,12 @@
"YamlDotNet": "9.1.0"
}
},
"Flow.Launcher.Localization": {
"type": "Direct",
"requested": "[0.0.6, )",
"resolved": "0.0.6",
"contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw=="
},
"FSharp.Core": {
"type": "Direct",
"requested": "[9.0.303, )",
@ -61,26 +67,6 @@
"System.IO.Pipelines": "8.0.0"
}
},
"Avalonia": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "pD6woFAUfGcyEvMmrpctntU4jv4fT8752pfx1J5iRORVX3Ob0oQi8PWo0TXVaAJZiSfH0cdKTeKx0w0DzD0/mg==",
"dependencies": {
"Avalonia.BuildServices": "0.0.29",
"Avalonia.Remote.Protocol": "11.2.3",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "0.0.29",
"contentHash": "U4eJLQdoDNHXtEba7MZUCwrBErBTxFp6sUewXBOdAhU0Kwzwaa/EKFcYm8kpcysjzKtfB4S0S9n0uxKZFz/ikw=="
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.2.3",
"contentHash": "6V0aNtld48WmO8tAlWwlRlUmXYcOWv+1eJUSl1ETF+1blUe5yhcSmuWarPprO0hDk8Ta6wGfdfcrnVl2gITYcA=="
},
"Ben.Demystifier": {
"type": "Transitive",
"resolved": "0.4.1",
@ -104,6 +90,11 @@
"resolved": "1.1.0",
"contentHash": "j/zGAQ9hLbl7JDpeO40DaXvyyNxwQNDwnJEN7eCexn5F9Kid+VKya/Er0rfIv5Zod/32XarkqFP/V6WFHS/UpQ=="
},
"ini-parser": {
"type": "Transitive",
"resolved": "2.5.2",
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
},
"InputSimulator": {
"type": "Transitive",
"resolved": "1.0.4",
@ -147,11 +138,6 @@
"resolved": "2.5.192",
"contentHash": "jaJuwcgovWIZ8Zysdyf3b7b34/BrADw4v82GaEZymUhDd3ScMPrYd/cttekeDteJJPXseJxp04yTIcxiVUjTWg=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.6.3",
@ -1186,7 +1172,8 @@
"Ben.Demystifier": "[0.4.1, )",
"BitFaster.Caching": "[2.5.4, )",
"CommunityToolkit.Mvvm": "[8.4.0, )",
"Flow.Launcher.Plugin": "[5.1.0, )",
"Flow.Launcher.Localization": "[0.0.6, )",
"Flow.Launcher.Plugin": "[5.0.0, )",
"InputSimulator": "[1.0.4, )",
"MemoryPack": "[1.21.4, )",
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
@ -1195,13 +1182,13 @@
"NLog.OutputDebugString": "[6.0.4, )",
"SharpVectors.Wpf": "[1.8.5, )",
"System.Drawing.Common": "[7.0.0, )",
"ToolGood.Words.Pinyin": "[3.1.0.3, )"
"ToolGood.Words.Pinyin": "[3.1.0.3, )",
"ini-parser": "[2.5.2, )"
}
},
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.2.3, )",
"JetBrains.Annotations": "[2025.2.2, )"
}
}

View file

@ -13,6 +13,7 @@ using NHotkey;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Accessibility;
using System.Collections.Concurrent;
namespace Flow.Launcher.Infrastructure.DialogJump
{
@ -58,21 +59,17 @@ namespace Flow.Launcher.Infrastructure.DialogJump
private static readonly Settings _settings = Ioc.Default.GetRequiredService<Settings>();
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
private static HWND _mainWindowHandle = HWND.Null;
private static readonly Dictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
private static readonly ConcurrentDictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
private static DialogJumpExplorerPair _lastExplorer = null;
private static readonly object _lastExplorerLock = new();
private static readonly Lock _lastExplorerLock = new();
private static readonly Dictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
private static readonly ConcurrentDictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
private static IDialogJumpDialogWindow _dialogWindow = null;
private static readonly object _dialogWindowLock = new();
private static readonly Lock _dialogWindowLock = new();
private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null;
private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null;
@ -89,8 +86,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
private static DispatcherTimer _dragMoveTimer = null;
// A list of all file dialog windows that are auto switched already
private static readonly List<HWND> _autoSwitchedDialogs = new();
private static readonly object _autoSwitchedDialogsLock = new();
private static readonly List<HWND> _autoSwitchedDialogs = [];
private static readonly Lock _autoSwitchedDialogsLock = new();
private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null;
private static readonly WINEVENTPROC _moveProc = MoveSizeCallBack;
@ -105,22 +102,13 @@ namespace Flow.Launcher.Infrastructure.DialogJump
#region Initialize & Setup
public static void InitializeDialogJump(IList<DialogJumpExplorerPair> dialogJumpExplorers,
IList<DialogJumpDialogPair> dialogJumpDialogs)
public static void InitializeDialogJump()
{
if (_initialized) return;
// Initialize Dialog Jump explorers & dialogs
_dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null);
foreach (var explorer in dialogJumpExplorers)
{
_dialogJumpExplorers.Add(explorer, null);
}
_dialogJumpDialogs.Add(WindowsDialogJumpDialog, null);
foreach (var dialog in dialogJumpDialogs)
{
_dialogJumpDialogs.Add(dialog, null);
}
// Initialize preinstalled Dialog Jump explorers & dialogs
_dialogJumpExplorers.TryAdd(WindowsDialogJumpExplorer, null);
_dialogJumpDialogs.TryAdd(WindowsDialogJumpDialog, null);
// Initialize main window handle
_mainWindowHandle = Win32Helper.GetMainWindowHandle();
@ -135,6 +123,29 @@ namespace Flow.Launcher.Infrastructure.DialogJump
_initialized = true;
}
public static void InitializeDialogJumpPlugin(PluginPair pair)
{
// Add Dialog Jump explorers & dialogs
if (pair.Plugin is IDialogJumpExplorer explorer)
{
var dialogJumpExplorer = new DialogJumpExplorerPair
{
Plugin = explorer,
Metadata = pair.Metadata
};
_dialogJumpExplorers.TryAdd(dialogJumpExplorer, null);
}
if (pair.Plugin is IDialogJumpDialog dialog)
{
var dialogJumpDialog = new DialogJumpDialogPair
{
Plugin = dialog,
Metadata = pair.Metadata
};
_dialogJumpDialogs.TryAdd(dialogJumpDialog, null);
}
}
public static void SetupDialogJump(bool enabled)
{
if (enabled == _enabled) return;
@ -315,7 +326,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
{
foreach (var explorer in _dialogJumpExplorers.Keys)
{
if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified
if (PublicApi.Instance.PluginModified(explorer.Metadata.ID) || // Plugin is modified
explorer.Metadata.Disabled) continue; // Plugin is disabled
var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd);
@ -485,6 +496,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
uint dwmsEventTime
)
{
if (hwnd.IsNull) return;
await _foregroundChangeLock.WaitAsync();
try
{
@ -493,7 +506,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
var dialogWindowChanged = false;
foreach (var dialog in _dialogJumpDialogs.Keys)
{
if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified
if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified
dialog.Metadata.Disabled) continue; // Plugin is disabled
IDialogJumpDialogWindow dialogWindow;
@ -596,7 +609,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
{
foreach (var explorer in _dialogJumpExplorers.Keys)
{
if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified
if (PublicApi.Instance.PluginModified(explorer.Metadata.ID) || // Plugin is modified
explorer.Metadata.Disabled) continue; // Plugin is disabled
var explorerWindow = explorer.Plugin.CheckExplorerWindow(hwnd);
@ -636,6 +649,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
uint dwmsEventTime
)
{
if (hwnd.IsNull) return;
// If the dialog window is moved, update the Dialog Jump window position
var dialogWindowExist = false;
lock (_dialogWindowLock)
@ -661,6 +676,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
uint dwmsEventTime
)
{
if (hwnd.IsNull) return;
// If the dialog window is moved or resized, update the Dialog Jump window position
if (_dragMoveTimer != null)
{
@ -686,6 +703,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
uint dwmsEventTime
)
{
if (hwnd.IsNull) return;
// If the dialog window is destroyed, set _dialogWindowHandle to null
var dialogWindowExist = false;
lock (_dialogWindowLock)
@ -717,6 +736,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
uint dwmsEventTime
)
{
if (hwnd.IsNull) return;
// If the dialog window is hidden, set _dialogWindowHandle to null
var dialogWindowExist = false;
lock (_dialogWindowLock)
@ -748,6 +769,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
uint dwmsEventTime
)
{
if (hwnd.IsNull) return;
// If the dialog window is ended, set _dialogWindowHandle to null
var dialogWindowExist = false;
lock (_dialogWindowLock)
@ -887,7 +910,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
// Then check all dialog windows
foreach (var dialog in _dialogJumpDialogs.Keys)
{
if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified
if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified
dialog.Metadata.Disabled) continue; // Plugin is disabled
var dialogWindow = _dialogJumpDialogs[dialog];
@ -900,7 +923,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
// Finally search for the dialog window again
foreach (var dialog in _dialogJumpDialogs.Keys)
{
if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified
if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified
dialog.Metadata.Disabled) continue; // Plugin is disabled
IDialogJumpDialogWindow dialogWindow;
@ -1083,11 +1106,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
_navigationLock.Dispose();
// Stop drag move timer
if (_dragMoveTimer != null)
{
_dragMoveTimer.Stop();
_dragMoveTimer = null;
}
_dragMoveTimer?.Stop();
_dragMoveTimer = null;
}
#endregion

View file

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Windows.Win32;
namespace Flow.Launcher.Infrastructure
{
@ -13,9 +9,10 @@ namespace Flow.Launcher.Infrastructure
/// </summary>
public static string GetActiveExplorerPath()
{
var explorerWindow = GetActiveExplorer();
string locationUrl = explorerWindow?.LocationURL;
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
var explorerPath = DialogJump.DialogJump.GetActiveExplorerPath();
return !string.IsNullOrEmpty(explorerPath) ?
GetDirectoryPath(new Uri(explorerPath).LocalPath) :
null;
}
/// <summary>
@ -23,74 +20,12 @@ namespace Flow.Launcher.Infrastructure
/// </summary>
private static string GetDirectoryPath(string path)
{
if (!path.EndsWith("\\"))
if (!path.EndsWith('\\'))
{
return path + "\\";
}
return path;
}
/// <summary>
/// Gets the file explorer that is currently in the foreground
/// </summary>
private static dynamic GetActiveExplorer()
{
Type type = Type.GetTypeFromProgID("Shell.Application");
if (type == null) return null;
dynamic shell = Activator.CreateInstance(type);
if (shell == null)
{
return null;
}
var explorerWindows = new List<dynamic>();
var openWindows = shell.Windows();
for (int i = 0; i < openWindows.Count; i++)
{
var window = openWindows.Item(i);
if (window == null) continue;
// find the desired window and make sure that it is indeed a file explorer
// we don't want the Internet Explorer or the classic control panel
// ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE"
if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe")
{
explorerWindows.Add(window);
}
}
if (explorerWindows.Count == 0) return null;
var zOrders = GetZOrder(explorerWindows);
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
}
/// <summary>
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
/// </summary>
private static IEnumerable<int> GetZOrder(List<dynamic> hWnds)
{
var z = new int[hWnds.Count];
for (var i = 0; i < hWnds.Count; i++) z[i] = -1;
var index = 0;
var numRemaining = hWnds.Count;
PInvoke.EnumWindows((wnd, _) =>
{
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
if (searchIndex != -1)
{
z[searchIndex] = index;
numRemaining--;
if (numRemaining == 0) return false;
}
index++;
return true;
}, IntPtr.Zero);
return z;
}
}
}

View file

@ -34,6 +34,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -56,10 +57,12 @@
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="BitFaster.Caching" Version="2.5.4" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
<PackageReference Include="Fody" Version="6.9.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="MemoryPack" Version="1.21.4" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.14.15" />
@ -80,4 +83,15 @@
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
</ItemGroup>
<PropertyGroup>
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Remove="Languages\en.xaml" />
<AdditionalFiles Include="..\Flow.Launcher\Languages\en.xaml">
<Link>Languages\en.xaml</Link>
</AdditionalFiles>
</ItemGroup>
</Project>

View file

@ -4,10 +4,8 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using JetBrains.Annotations;
namespace Flow.Launcher.Infrastructure.Http
@ -20,10 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http
private static readonly HttpClient client = new();
// 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>();
static Http()
{
// need to be added so it would work on a win10 machine
@ -82,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Http
}
catch (UriFormatException e)
{
API.ShowMsgError(API.GetTranslation("pleaseTryAgain"), API.GetTranslation("parseProxyFailed"));
PublicApi.Instance.ShowMsgError(Localize.pleaseTryAgain(), Localize.parseProxyFailed());
Log.Exception(ClassName, "Unable to parse Uri", e);
}
}

View file

@ -1,13 +1,14 @@
using System;
using System.Runtime.InteropServices;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using IniParser;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.Shell;
namespace Flow.Launcher.Infrastructure.Image
{
@ -35,9 +36,32 @@ namespace Flow.Launcher.Infrastructure.Image
private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205;
private const string UrlExtension = ".url";
/// <summary>
/// Obtains a BitmapSource thumbnail for the specified file.
/// </summary>
/// <remarks>
/// If the file is a Windows URL shortcut (".url"), the method attempts to resolve the shortcut's icon and use that for the thumbnail; otherwise it requests a thumbnail for the file path. The native HBITMAP used to create the BitmapSource is always released to avoid native memory leaks.
/// </remarks>
/// <param name="fileName">Path to the file (can be a regular file or a ".url" shortcut).</param>
/// <param name="width">Requested thumbnail width in pixels.</param>
/// <param name="height">Requested thumbnail height in pixels.</param>
/// <param name="options">Thumbnail extraction options (flags) controlling fallback and caching behavior.</param>
/// <returns>A BitmapSource representing the requested thumbnail.</returns>
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
{
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
HBITMAP hBitmap;
var extension = Path.GetExtension(fileName);
if (string.Equals(extension, UrlExtension, StringComparison.OrdinalIgnoreCase))
{
hBitmap = GetHBitmapForUrlFile(fileName, width, height, options);
}
else
{
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
}
try
{
@ -50,6 +74,21 @@ namespace Flow.Launcher.Infrastructure.Image
}
}
/// <summary>
/// Obtains a native HBITMAP for the specified file at the requested size using the Windows Shell image factory.
/// </summary>
/// <remarks>
/// If <paramref name="options"/> is <see cref="ThumbnailOptions.ThumbnailOnly"/> and thumbnail extraction fails
/// due to extraction errors or a missing path, the method falls back to requesting an icon (<see cref="ThumbnailOptions.IconOnly"/>).
/// The returned HBITMAP is a raw GDI handle; the caller is responsible for releasing it (e.g., via DeleteObject) to avoid native memory leaks.
/// </remarks>
/// <param name="fileName">Path to the file to thumbnail.</param>
/// <param name="width">Requested thumbnail width in pixels.</param>
/// <param name="height">Requested thumbnail height in pixels.</param>
/// <param name="options">Thumbnail request flags that control behavior (e.g., ThumbnailOnly, IconOnly).</param>
/// <returns>An HBITMAP handle containing the image. Caller must free the handle when finished.</returns>
/// <exception cref="COMException">If creating the shell item fails (HRESULT returned by SHCreateItemFromParsingName).</exception>
/// <exception cref="InvalidOperationException">If the shell item does not expose IShellItemImageFactory or if an unexpected error occurs while obtaining the image.</exception>
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
{
var retCode = PInvoke.SHCreateItemFromParsingName(
@ -108,5 +147,44 @@ namespace Flow.Launcher.Infrastructure.Image
return hBitmap;
}
/// <summary>
/// Obtains an HBITMAP for a Windows .url shortcut by resolving its IconFile entry and delegating to GetHBitmap.
/// </summary>
/// <remarks>
/// The method parses the .url file as an INI, looks in the "InternetShortcut" section for the "IconFile" entry,
/// and requests a bitmap for that icon path. If no IconFile is present or any error occurs while reading or
/// resolving the icon, it falls back to requesting a thumbnail for the .url file itself.
/// </remarks>
/// <param name="fileName">Path to the .url shortcut file.</param>
/// <param name="width">Requested thumbnail width (pixels).</param>
/// <param name="height">Requested thumbnail height (pixels).</param>
/// <param name="options">ThumbnailOptions flags controlling extraction behavior.</param>
/// <returns>An HBITMAP containing the requested image; callers are responsible for freeing the native handle.</returns>
private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, int height, ThumbnailOptions options)
{
HBITMAP hBitmap;
try
{
var parser = new FileIniDataParser();
var data = parser.ReadFile(fileName);
var urlSection = data["InternetShortcut"];
var iconPath = urlSection?["IconFile"];
if (!File.Exists(iconPath))
{
// If the IconFile is missing, throw exception to fallback to the default icon
throw new FileNotFoundException("Icon file not specified in Internet shortcut (.url) file.");
}
hBitmap = GetHBitmap(Path.GetFullPath(iconPath), width, height, options);
}
catch
{
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
}
return hBitmap;
}
}
}

View file

@ -34,7 +34,7 @@ namespace Flow.Launcher.Infrastructure.Logger
var fileTarget = new FileTarget
{
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt",
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/Flow.Launcher.${date:format=yyyy-MM-dd}.log",
Layout = layout
};
@ -65,26 +65,22 @@ namespace Flow.Launcher.Infrastructure.Logger
public static void SetLogLevel(LOGLEVEL level)
{
switch (level)
var rule = LogManager.Configuration.FindRuleByName("file");
var nlogLevel = level switch
{
case LOGLEVEL.DEBUG:
UseDebugLogLevel();
break;
default:
UseInfoLogLevel();
break;
}
Info(nameof(Logger), $"Using log level: {level}.");
}
LOGLEVEL.NONE => LogLevel.Off,
LOGLEVEL.ERROR => LogLevel.Error,
LOGLEVEL.DEBUG => LogLevel.Debug,
_ => LogLevel.Info
};
private static void UseDebugLogLevel()
{
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
}
rule.SetLoggingLevels(nlogLevel, LogLevel.Fatal);
private static void UseInfoLogLevel()
{
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
LogManager.ReconfigExistingLoggers();
// We can't log Info when level is set to Error or None, so we use Debug
Debug(nameof(Logger), $"Using log level: {level}.");
}
private static void LogFaultyFormat(string message)
@ -172,7 +168,9 @@ namespace Flow.Launcher.Infrastructure.Logger
public enum LOGLEVEL
{
DEBUG,
INFO
NONE,
ERROR,
INFO,
DEBUG
}
}

View file

@ -91,4 +91,6 @@ PBT_APMRESUMEAUTOMATIC
PBT_APMRESUMESUSPEND
PowerRegisterSuspendResumeNotification
PowerUnregisterSuspendResumeNotification
DeviceNotifyCallbackRoutine
DeviceNotifyCallbackRoutine
MonitorFromWindow

View file

@ -27,7 +27,7 @@ namespace Flow.Launcher.Infrastructure
{
switch (e.PropertyName)
{
case nameof (Settings.ShouldUsePinyin):
case nameof(Settings.ShouldUsePinyin):
if (_settings.ShouldUsePinyin)
{
Reload();
@ -52,7 +52,7 @@ namespace Flow.Launcher.Infrastructure
private void CreateDoublePinyinTableFromStream(Stream jsonStream)
{
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null");
var schemaKey = _settings.DoublePinyinSchema.ToString();
@ -128,12 +128,12 @@ namespace Flow.Launcher.Infrastructure
if (IsChineseCharacter(content[i]))
{
var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i];
if (i > 0)
if (i > 0 && content[i - 1] != ' ')
{
resultBuilder.Append(' ');
}
map.AddNewIndex(resultBuilder.Length, translated.Length);
resultBuilder.Append(translated);
previousIsChinese = true;
@ -144,11 +144,14 @@ namespace Flow.Launcher.Infrastructure
if (previousIsChinese)
{
previousIsChinese = false;
resultBuilder.Append(' ');
if (content[i] != ' ')
{
resultBuilder.Append(' ');
}
}
map.AddNewIndex(resultBuilder.Length, resultList[i].Length);
resultBuilder.Append(resultList[i]);
map.AddNewIndex(resultBuilder.Length, 1);
resultBuilder.Append(content[i]);
}
}
@ -156,7 +159,7 @@ namespace Flow.Launcher.Infrastructure
var translation = resultBuilder.ToString();
var result = (translation, map);
return _pinyinCache[content] = result;
}
@ -185,8 +188,8 @@ namespace Flow.Launcher.Infrastructure
private string ToDoublePinyin(string fullPinyin)
{
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
? doublePinyinValue
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
? doublePinyinValue
: fullPinyin;
}
}

View file

@ -2,35 +2,118 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Storage
{
public class History
{
[JsonInclude]
public List<HistoryItem> Items { get; private set; } = new List<HistoryItem>();
#pragma warning disable CS0618 // Type or member is obsolete
public List<HistoryItem> Items { get; private set; } = [];
#pragma warning restore CS0618 // Type or member is obsolete
private int _maxHistory = 300;
[JsonInclude]
public List<LastOpenedHistoryResult> LastOpenedHistoryItems { get; private set; } = [];
public void Add(string query)
private readonly int _maxHistory = 300;
/// <summary>
/// Migrate legacy history data (stored in <see cref="Items"/>) into the new
/// <see cref="LastOpenedHistoryResult"/> format and append them to
/// <see cref="LastOpenedHistoryItems"/>.
/// </summary>
[Obsolete("For backwards compatibility. Remove after release v2.3.0")]
public void PopulateHistoryFromLegacyHistory()
{
if (string.IsNullOrEmpty(query)) return;
if (Items.Count > _maxHistory)
if (Items.Count == 0) return;
// Migrate old history items to new LastOpenedHistoryItems
foreach (var item in Items)
{
Items.RemoveAt(0);
LastOpenedHistoryItems.Add(new LastOpenedHistoryResult
{
Title = Localize.executeQuery(item.Query),
OriginQuery = new Query { TrimmedQuery = item.Query },
Query = item.Query,
Action = _ =>
{
App.API.BackToQueryResults();
App.API.ChangeQuery(item.Query);
return false;
},
ExecutedDateTime = item.ExecutedDateTime
});
}
Items.Clear();
}
/// <summary>
/// Records a result into the last-opened history list (<see cref="LastOpenedHistoryItems"/>).
/// This will also update the IcoPath if existing history item has one that is different.
/// </summary>
/// <param name="result">The result to add to history. Must have a non-empty <see cref="Result.OriginQuery"/>.<see cref="Query.TrimmedQuery"/>.</param>
public void Add(Result result)
{
if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return;
// History results triggered from homepage do not contain PluginID,
// these are intentionally not saved otherwise cause duplicates due to subtitle
// containing datetime string.
if (string.IsNullOrEmpty(result.PluginID)) return;
// Maintain the max history limit
if (LastOpenedHistoryItems.Count > _maxHistory)
{
LastOpenedHistoryItems.RemoveAt(0);
}
if (Items.Count > 0 && Items.Last().Query == query)
// If the last item is the same as the current result, just update the timestamp and the icon path
if (LastOpenedHistoryItems.Count > 0 &&
TryGetLastOpenedHistoryResult(result, out var existingHistoryItem))
{
Items.Last().ExecutedDateTime = DateTime.Now;
existingHistoryItem.ExecutedDateTime = DateTime.Now;
if (existingHistoryItem.IcoPath != result.IcoPath)
existingHistoryItem.IcoPath = result.IcoPath;
if (existingHistoryItem.Glyph?.Glyph != result.Glyph?.Glyph
|| existingHistoryItem.Glyph?.FontFamily != result.Glyph?.FontFamily)
existingHistoryItem.SetGlyph(result.Glyph);
}
else
{
Items.Add(new HistoryItem
{
Query = query,
ExecutedDateTime = DateTime.Now
});
LastOpenedHistoryItems.Add(new LastOpenedHistoryResult(result));
}
}
/// <summary>
/// Attempts to find an existing <see cref="LastOpenedHistoryResult"/> in <see cref="LastOpenedHistoryItems"/>
/// that is considered equal to the supplied <paramref name="result"/>.
/// </summary>
private bool TryGetLastOpenedHistoryResult(Result result, out LastOpenedHistoryResult historyItem)
{
historyItem = LastOpenedHistoryItems.FirstOrDefault(x => x.Equals(result));
return historyItem is not null;
}
/// <summary>
/// Flow uses IcoPathAbsolute property to display result the icons. This refreshes the IcoPathAbsolute
/// property using current plugin metadata by updating the PluginDirectory property, which in turn also
/// updates IcoPath. This keeps the saved icon paths of results updated correctly if flow is moved around.
/// </summary>
/// <remarks> Call this after plugins are loaded/initialized.</remarks>
public void UpdateIcoPathAbsolute()
{
if (LastOpenedHistoryItems.Count == 0) return;
foreach (var item in LastOpenedHistoryItems)
{
if (string.IsNullOrEmpty(item.PluginID)) continue;
var pluginPair = PluginManager.GetPluginForId(item.PluginID);
if (pluginPair == null) continue;
item.PluginDirectory = pluginPair.Metadata.PluginDirectory;
}
}
}

View file

@ -1,7 +1,8 @@
using System;
using System;
namespace Flow.Launcher.Storage
{
[Obsolete("Use LastOpenedHistoryResult instead. This class will be removed in future versions.")]
public class HistoryItem
{
public string Query { get; set; }
@ -42,4 +43,4 @@ namespace Flow.Launcher.Storage
return string.Empty;
}
}
}
}

View file

@ -21,7 +21,7 @@ namespace Flow.Launcher.Infrastructure
public int MapToOriginalIndex(int translatedIndex)
{
var searchResult = _originalToTranslated.BinarySearch(translatedIndex);
return searchResult >= 0 ? searchResult : ~searchResult;
return searchResult >= 0 ? searchResult + 1 : ~searchResult;
}
public void EndConstruct()

View file

@ -1,18 +1,13 @@
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings
{
public class CustomBrowserViewModel : BaseModel
{
// 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 string Name { get; set; }
[JsonIgnore]
public string DisplayName => Name == "Default" ? API.GetTranslation("defaultBrowser_default") : Name;
public string DisplayName => Name == "Default" ? Localize.defaultBrowser_default() : Name;
public string Path { get; set; }
public string PrivateArg { get; set; }
public bool EnablePrivate { get; set; }

View file

@ -1,18 +1,13 @@
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings
{
public class CustomExplorerViewModel : BaseModel
{
// 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 string Name { get; set; }
[JsonIgnore]
public string DisplayName => Name == "Explorer" ? API.GetTranslation("fileManagerExplorer") : Name;
public string DisplayName => Name == "Explorer" ? Localize.fileManagerExplorer() : Name;
public string Path { get; set; }
public string FileArgument { get; set; } = "\"%d\"";
public string DirectoryArgument { get; set; } = "\"%d\"";

View file

@ -1,8 +1,6 @@
using System;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings
{
@ -55,11 +53,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
public string Description { get; set; }
public string LocalizedDescription => API.GetTranslation(Description);
// 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 string LocalizedDescription => PublicApi.Instance.GetTranslation(Description);
public BaseBuiltinShortcutModel(string key, string description)
{

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using System.Windows;
@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Localization.Attributes;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@ -480,6 +481,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
public bool LeaveCmdOpen { get; set; }
public bool HideWhenDeactivated { get; set; } = true;
public bool ShowTaskbarWhenInvoked { get; set; } = false;
private bool _showAtTopmost = false;
public bool ShowAtTopmost
@ -513,6 +515,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings
[JsonConverter(typeof(JsonStringEnumConverter))]
public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected;
private HistoryStyle _historyStyle = HistoryStyle.Query;
[JsonConverter(typeof(JsonStringEnumConverter))]
public HistoryStyle HistoryStyle
{
get => _historyStyle;
set
{
if (_historyStyle != value)
{
_historyStyle = value;
OnPropertyChanged();
}
}
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
public int CustomAnimationLength { get; set; } = 360;
@ -695,4 +712,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings
FullPathOpen,
Directory
}
[EnumLocalize]
public enum HistoryStyle
{
[EnumLocalizeKey(nameof(Localize.queryHistory))]
Query,
[EnumLocalizeKey(nameof(Localize.executedHistory))]
LastOpened
}
}

View file

@ -1016,5 +1016,32 @@ namespace Flow.Launcher.Infrastructure
}
#endregion
#region Taskbar
public static unsafe void ShowTaskbar()
{
// Find the taskbar window
var taskbarHwnd = PInvoke.FindWindowEx(HWND.Null, HWND.Null, "Shell_TrayWnd", null);
if (taskbarHwnd == HWND.Null) return;
// Magic from https://github.com/Oliviaophia/SmartTaskbar
const uint TrayBarFlag = 0x05D1;
var mon = PInvoke.MonitorFromWindow(taskbarHwnd, Windows.Win32.Graphics.Gdi.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
PInvoke.PostMessage(taskbarHwnd, TrayBarFlag, new WPARAM(1), new LPARAM((nint)mon.Value));
}
public static void HideTaskbar()
{
// Find the taskbar window
var taskbarHwnd = PInvoke.FindWindowEx(HWND.Null, HWND.Null, "Shell_TrayWnd", null);
if (taskbarHwnd == HWND.Null) return;
// Magic from https://github.com/Oliviaophia/SmartTaskbar
const uint TrayBarFlag = 0x05D1;
PInvoke.PostMessage(taskbarHwnd, TrayBarFlag, new WPARAM(0), IntPtr.Zero);
}
#endregion
}
}

View file

@ -23,12 +23,24 @@
"resolved": "8.4.0",
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
},
"Flow.Launcher.Localization": {
"type": "Direct",
"requested": "[0.0.6, )",
"resolved": "0.0.6",
"contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw=="
},
"Fody": {
"type": "Direct",
"requested": "[6.9.3, )",
"resolved": "6.9.3",
"contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA=="
},
"ini-parser": {
"type": "Direct",
"requested": "[2.5.2, )",
"resolved": "2.5.2",
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
},
"InputSimulator": {
"type": "Direct",
"requested": "[1.0.4, )",

View file

@ -73,7 +73,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.102" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.205">
<PrivateAssets>all</PrivateAssets>

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
@ -173,9 +173,21 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Get all loaded plugins
/// </summary>
/// <remarks>
/// Will also return any plugins not fully initialized yet
/// </remarks>
/// <returns></returns>
List<PluginPair> GetAllPlugins();
/// <summary>
/// Get all initialized plugins
/// </summary>
/// <param name="includeFailed">
/// Whether to include plugins that failed to initialize
/// </param>
/// <returns></returns>
List<PluginPair> GetAllInitializedPlugins(bool includeFailed);
/// <summary>
/// Registers a callback function for global keyboard events.
/// </summary>

View file

@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin
{
@ -8,11 +9,29 @@ namespace Flow.Launcher.Plugin
public class Query
{
/// <summary>
/// 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.
/// Original query, exactly how the user has typed into the search box.
/// We don't recommend using this property directly. You should always use Search property.
/// </summary>
public string RawQuery { get; internal init; }
public string OriginalQuery { get; internal init; }
/// <summary>
/// Raw query, this includes action keyword if it has.
/// It has handled built-in custom query hotkeys and built-in shortcuts, and it trims the whitespace.
/// We don't recommend using this property directly. You should always use Search property.
/// </summary>
[Obsolete("RawQuery is renamed to TrimmedQuery. This property will be removed. Update the code to use TrimmedQuery instead.")]
public string RawQuery {
get => TrimmedQuery;
internal init { TrimmedQuery = value; }
}
/// <summary>
/// Original query but with trimmed whitespace. Includes action keyword.
/// It has handled built-in custom query hotkeys and build-in shortcuts.
/// If you need the exact original query from the search box, use OriginalQuery property instead.
/// We don't recommend using this property directly. You should always use Search property.
/// </summary>
public string TrimmedQuery { get; internal init; }
/// <summary>
/// Determines whether the query was forced to execute again.
@ -28,7 +47,7 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Search part of a query.
/// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as RawQuery.
/// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as TrimmedQuery.
/// Since we allow user to switch a exclusive plugin to generic plugin,
/// so this property will always give you the "real" query part of the query
/// </summary>
@ -103,6 +122,6 @@ namespace Flow.Launcher.Plugin
}
/// <inheritdoc />
public override string ToString() => RawQuery;
public override string ToString() => TrimmedQuery;
}
}

View file

@ -4,11 +4,13 @@ using System.IO;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Describes a result of a <see cref="Query"/> executed by a plugin
/// Describes a result of a <see cref="Query"/> executed by a plugin.
/// This or its child classes is serializable.
/// </summary>
public class Result
{
@ -21,6 +23,8 @@ namespace Flow.Launcher.Plugin
private string _icoPath;
private string _icoPathAbsolute;
private string _copyText = string.Empty;
private string _badgeIcoPath;
@ -64,15 +68,27 @@ namespace Flow.Launcher.Plugin
public string AutoCompleteText { get; set; }
/// <summary>
/// The image to be displayed for the result.
/// Path or URI to the icon image for this result.
/// Updates <see cref="IcoPathAbsolute"/> appropriately when set.
/// </summary>
/// <value>Can be a local file path or a URL.</value>
/// <remarks>GlyphInfo is prioritized if not null</remarks>
/// <remarks>
/// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png").
/// Because <see cref="IcoPath"/> is serialized, using relative paths keeps the icon reference portable
/// when Flow is moved.
///
/// Accepted formats:
/// - Relative file paths (resolved against <see cref="PluginDirectory"/> into <see cref="IcoPathAbsolute"/>)
/// - Absolute file paths (left as-is)
/// - HTTP/HTTPS URLs (left as-is)
/// - Data URIs (left as-is)
/// </remarks>
public string IcoPath
{
get => _icoPath;
set
{
_icoPath = value;
// 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)
@ -81,15 +97,23 @@ namespace Flow.Launcher.Plugin
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
{
_icoPath = Path.Combine(PluginDirectory, value);
_icoPathAbsolute = Path.Combine(PluginDirectory, value);
}
else
{
_icoPath = value;
_icoPathAbsolute = value;
}
}
}
/// <summary>
/// Absolute path or URI which is used to load and display the result icon for Flow.
/// This is populated by the <see cref="IcoPath"/> setter.
/// If a relative path was provided to <see cref="IcoPath"/>, this property will contain the resolved
/// absolute local path after combining with <see cref="PluginDirectory"/>.
/// </summary>
public string IcoPathAbsolute => _icoPathAbsolute;
/// <summary>
/// The image to be displayed for the badge of the result.
/// </summary>
@ -131,17 +155,34 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Delegate to load an icon for this result.
/// </summary>
[JsonIgnore]
public IconDelegate Icon = null;
/// <summary>
/// Delegate to load an icon for the badge of this result.
/// </summary>
[JsonIgnore]
public IconDelegate BadgeIcon = null;
private GlyphInfo _glyph;
/// <summary>
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
/// </summary>
public GlyphInfo Glyph { get; init; }
public GlyphInfo Glyph
{
get => _glyph;
init => _glyph = value;
}
/// <summary>
/// Set the Glyph Icon after initialization
/// </summary>
/// <param name="glyph"></param>
public void SetGlyph(GlyphInfo glyph)
{
_glyph = glyph;
}
/// <summary>
/// An action to take in the form of a function call when the result has been selected.
@ -151,6 +192,7 @@ namespace Flow.Launcher.Plugin
/// Its result determines what happens to Flow Launcher's query form:
/// when true, the form will be hidden; when false, it will stay in focus.
/// </remarks>
[JsonIgnore]
public Func<ActionContext, bool> Action { get; set; }
/// <summary>
@ -161,6 +203,7 @@ namespace Flow.Launcher.Plugin
/// Its result determines what happens to Flow Launcher's query form:
/// when true, the form will be hidden; when false, it will stay in focus.
/// </remarks>
[JsonIgnore]
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
/// <summary>
@ -203,11 +246,13 @@ namespace Flow.Launcher.Plugin
/// <example>
/// As external information for ContextMenu
/// </example>
[JsonIgnore]
public object ContextData { get; set; }
/// <summary>
/// Plugin ID that generated this result
/// </summary>
[JsonInclude]
public string PluginID { get; internal set; }
/// <summary>
@ -223,6 +268,7 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Customized Preview Panel
/// </summary>
[JsonIgnore]
public Lazy<UserControl> PreviewPanel { get; set; }
/// <summary>
@ -352,6 +398,7 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Delegate to get the preview panel's image
/// </summary>
[JsonIgnore]
public IconDelegate PreviewDelegate { get; set; } = null;
/// <summary>

View file

@ -27,12 +27,12 @@
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
"requested": "[10.0.102, )",
"resolved": "10.0.102",
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
"Microsoft.Build.Tasks.Git": "10.0.102",
"Microsoft.SourceLink.Common": "10.0.102"
}
},
"Microsoft.Windows.CsWin32": {
@ -72,13 +72,13 @@
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
"resolved": "10.0.102",
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
"resolved": "10.0.102",
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
},
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",

View file

@ -16,14 +16,15 @@ namespace Flow.Launcher.Test.Plugins
{
DecimalSeparator = DecimalSeparator.UseSystemLocale,
MaxDecimalPlaces = 10,
ShowErrorMessage = false // Make sure we return the empty results when error occurs
ShowErrorMessage = false, // Make sure we return the empty results when error occurs
UseThousandsSeparator = true // Default value
};
private readonly Engine _engine = new(new Configuration
{
Scope = new Dictionary<string, object>
{
{ "e", Math.E }, // e is not contained in the default mages engine
}
{
{ "e", Math.E }, // e is not contained in the default mages engine
}
});
public CalculatorPluginTest()
@ -41,6 +42,44 @@ namespace Flow.Launcher.Test.Plugins
engineField.SetValue(null, _engine);
}
[Test]
public void ThousandsSeparatorTest_Enabled()
{
_settings.UseThousandsSeparator = true;
_settings.DecimalSeparator = DecimalSeparator.Dot;
var result = GetCalculationResult("1000+234");
// When thousands separator is enabled, the result should contain a separator
// Since decimal separator is dot, thousands separator should be comma
ClassicAssert.AreEqual("1,234", result);
_settings.DecimalSeparator = DecimalSeparator.Comma;
var result2 = GetCalculationResult("1000+234");
// When thousands separator is enabled, the result should contain a separator
// Since decimal separator is comma, thousands separator should be dot
ClassicAssert.AreEqual("1.234", result2);
}
[Test]
public void ThousandsSeparatorTest_Disabled()
{
_settings.UseThousandsSeparator = false;
_settings.DecimalSeparator = DecimalSeparator.UseSystemLocale;
var result = GetCalculationResult("1000+234");
ClassicAssert.AreEqual("1234", result);
}
[Test]
public void ThousandsSeparatorTest_LargeNumber()
{
_settings.UseThousandsSeparator = false;
_settings.DecimalSeparator = DecimalSeparator.UseSystemLocale;
var result = GetCalculationResult("1000000+234567");
ClassicAssert.AreEqual("1234567", result);
}
// Basic operations
[TestCase(@"1+1", "2")]
[TestCase(@"2-1", "1")]
@ -77,6 +116,9 @@ namespace Flow.Launcher.Test.Plugins
[TestCase(@"invalid_expression", "")]
public void CalculatorTest(string expression, string result)
{
_settings.UseThousandsSeparator = false;
_settings.DecimalSeparator = DecimalSeparator.Dot;
ClassicAssert.AreEqual(GetCalculationResult(expression), result);
}

View file

@ -16,9 +16,9 @@ namespace Flow.Launcher.Test
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}}}}
};
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins);
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.TrimmedQuery);
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
ClassicAssert.AreEqual(">", q.ActionKeyword);
@ -39,10 +39,10 @@ namespace Flow.Launcher.Test
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}, Disabled = true}}}
};
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins);
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search);
ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
ClassicAssert.AreEqual(q.Search, q.TrimmedQuery, "TrimmedQuery should be equal to Search.");
ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
@ -51,7 +51,7 @@ namespace Flow.Launcher.Test
[Test]
public void GenericPluginQueryTest()
{
Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary<string, PluginPair>());
Query q = QueryBuilder.Build("file.txt file2 file3", "file.txt file2 file3", new Dictionary<string, PluginPair>());
ClassicAssert.AreEqual("file.txt file2 file3", q.Search);
ClassicAssert.AreEqual("", q.ActionKeyword);

View file

@ -22,19 +22,33 @@ namespace Flow.Launcher.Test
ClassicAssert.AreEqual(10, GetOriginalToTranslatedAt(mapping, 1));
}
[TestCase(0, 0)]
[TestCase(2, 1)]
[TestCase(3, 1)]
[TestCase(5, 2)]
[TestCase(6, 2)]
[TestCase(0, 0)] // "F" -> "F"
[TestCase(1, 1)] // "l" -> "l"
[TestCase(2, 2)] // "o" -> "o"
[TestCase(3, 3)] // "w" -> "w"
[TestCase(4, 4)] // " " -> " "
[TestCase(5, 5)] // "Y" (translated from "用") -> original index 5
[TestCase(6, 5)] // "o" (translated from "用") -> original index 5
[TestCase(7, 5)] // "n" (translated from "用") -> original index 5
[TestCase(8, 5)] // "g" (translated from "用") -> original index 5
[TestCase(10, 6)] // "H" (translated from "户") -> original index 6
[TestCase(11, 6)] // "u" (translated from "户") -> original index 6
public void MapToOriginalIndex_ShouldReturnExpectedIndex(int translatedIndex, int expectedOriginalIndex)
{
var mapping = new TranslationMapping();
// a测试
// a Ce Shi
mapping.AddNewIndex(0, 1);
mapping.AddNewIndex(2, 2);
mapping.AddNewIndex(5, 3);
// Test case :
// 0123456
// Flow 用户
// 012345678901
// Flow Yong Hu
mapping.AddNewIndex(0, 1); // F
mapping.AddNewIndex(1, 1); // l
mapping.AddNewIndex(2, 1); // o
mapping.AddNewIndex(3, 1); // w
mapping.AddNewIndex(4, 1); // ' '
mapping.AddNewIndex(5, 4); // 用 -> Yong
mapping.AddNewIndex(10, 2); // 户 -> Hu
var result = mapping.MapToOriginalIndex(translatedIndex);
ClassicAssert.AreEqual(expectedOriginalIndex, result);

View file

@ -47,7 +47,7 @@ namespace Flow.Launcher
if (addedActionKeywords.Any(App.API.ActionKeywordAssigned))
{
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
App.API.ShowMsgBox(Localize.newActionKeywordsHasBeenAssigned());
return;
}
@ -63,7 +63,7 @@ namespace Flow.Launcher
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
{
// User just changes the sequence of action keywords
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld"));
App.API.ShowMsgBox(Localize.newActionKeywordsSameAsOld());
}
else
{

View file

@ -2,7 +2,8 @@
x:Class="Flow.Launcher.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
ShutdownMode="OnMainWindowClose"
Startup="OnStartup">
<Application.Resources>
@ -10,17 +11,17 @@
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources>
<ui:ThemeResources.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<ResourceDictionary x:Key="Light" ui:ThemeDictionary.Key="Light">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Light.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<ResourceDictionary x:Key="Dark" ui:ThemeDictionary.Key="Dark">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<ResourceDictionary x:Key="HighContrast" ui:ThemeDictionary.Key="HighContrast">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
</ResourceDictionary.MergedDictionaries>
@ -33,6 +34,15 @@
<ResourceDictionary Source="pack://application:,,,/Themes/Win11Light.xaml" />
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Override styles in UI.Modern.WPF -->
<Thickness x:Key="ListViewItemCompactSelectedBorderThemeThickness">2</Thickness>
<sys:Double x:Key="CheckBoxMinWidth">0</sys:Double>
<sys:Double x:Key="GridViewItemMinWidth">0</sys:Double>
<sys:Double x:Key="GridViewItemMinHeight">40</sys:Double>
<sys:Double x:Key="ListViewItemMinWidth">0</sys:Double>
<sys:Double x:Key="ListViewItemMinHeight">36</sys:Double>
<SolidColorBrush x:Key="NavigationViewSelectionIndicatorForeground" Color="#FF0063B1" />
</ResourceDictionary>
</Application.Resources>
</Application>

View file

@ -22,6 +22,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.SettingPages.ViewModels;
using Flow.Launcher.ViewModel;
using iNKORE.UI.WPF.Modern.Common;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.VisualStudio.Threading;
@ -56,6 +57,9 @@ namespace Flow.Launcher
public App()
{
// Do not use bitmap cache since it can cause WPF second window freezing issue
ShadowAssist.UseBitmapCache = false;
// Initialize settings
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
@ -183,12 +187,14 @@ namespace Flow.Launcher
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
// Setup log level before any logging is done
Log.SetLogLevel(_settings.LogLevel);
// Update dynamic resources base on settings
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
// Initialize notification system before any notification api is called
Notification.Install();
// Enable Win32 dark mode if the system is in dark mode before creating all windows
@ -197,6 +203,7 @@ namespace Flow.Launcher
// Initialize language before portable clean up since it needs translations
await _internationalization.InitializeLanguageAsync();
// Clean up after portability update
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
@ -206,32 +213,25 @@ namespace Flow.Launcher
RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException();
var imageLoadertask = ImageLoader.InitializeAsync();
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
PluginManager.LoadPlugins(_settings.PluginSettings);
// Register ResultsUpdated event after all plugins are loaded
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
var imageLoaderTask = ImageLoader.InitializeAsync();
Http.Proxy = _settings.Proxy;
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
await API.UpdatePluginManifestAsync();
await PluginManager.InitializePluginsAsync();
// Update plugin titles after plugins are initialized with their api instances
Internationalization.UpdatePluginMetadataTranslations();
await imageLoadertask;
await imageLoaderTask;
_mainWindow = new MainWindow();
Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher;
// Initialize Dialog Jump before hotkey mapper since hotkey mapper will register its hotkey
// Initialize Dialog Jump after main window is created so that it can access main window handle
DialogJump.InitializeDialogJump();
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
// Initialize hotkey mapper instantly after main window is created because
// it will steal focus from main window which causes window hide
HotKeyMapper.Initialize();
@ -239,19 +239,43 @@ namespace Flow.Launcher
// Initialize theme for main window
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs());
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
RegisterExitEvents();
AutoStartup();
AutoUpdates();
AutoPluginUpdates();
API.SaveAppAllSettings();
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
API.LogInfo(ClassName, "End Flow Launcher startup ------------------------------------------------------");
_ = API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
{
API.LogInfo(ClassName, "Begin plugin initialization ----------------------------------------------------");
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
PluginManager.LoadPlugins(_settings.PluginSettings);
await PluginManager.InitializePluginsAsync(_mainVM);
// Refresh the history results after plugins are initialized so that we can parse the absolute icon paths
_mainVM.RefreshLastOpenedHistoryResults();
// Refresh home page after plugins are initialized because users may open main window during plugin initialization
// And home page is created without full plugin list
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
{
_mainVM.QueryResults();
}
AutoPluginUpdates();
// Save all settings since we possibly update the plugin environment paths
API.SaveAppAllSettings();
API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------");
});
});
}
@ -276,7 +300,7 @@ namespace Flow.Launcher
// but if it fails (permissions, etc) then don't keep retrying
// this also gives the user a visual indication in the Settings widget
_settings.StartFlowLauncherOnSystemStartup = false;
API.ShowMsgError(API.GetTranslation("setAutoStartFailed"), e.Message);
API.ShowMsgError(Localize.setAutoStartFailed(), e.Message);
}
}
}

View file

@ -5,7 +5,7 @@ using System.Windows.Input;
namespace Flow.Launcher.Converters;
internal class BoolToIMEConversionModeConverter : IValueConverter
public class BoolToIMEConversionModeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
@ -22,7 +22,7 @@ internal class BoolToIMEConversionModeConverter : IValueConverter
}
}
internal class BoolToIMEStateConverter : IValueConverter
public class BoolToIMEStateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{

View file

@ -0,0 +1,91 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Flow.Launcher.Converters;
public class CornerRadiusFilterConverter : DependencyObject, IValueConverter
{
public CornerRadiusFilterKind Filter { get; set; }
public double Scale { get; set; } = 1.0;
public static CornerRadius Convert(CornerRadius radius, CornerRadiusFilterKind filterKind)
{
CornerRadius result = radius;
switch (filterKind)
{
case CornerRadiusFilterKind.Top:
result.BottomLeft = 0;
result.BottomRight = 0;
break;
case CornerRadiusFilterKind.Right:
result.TopLeft = 0;
result.BottomLeft = 0;
break;
case CornerRadiusFilterKind.Bottom:
result.TopLeft = 0;
result.TopRight = 0;
break;
case CornerRadiusFilterKind.Left:
result.TopRight = 0;
result.BottomRight = 0;
break;
}
return result;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var cornerRadius = (CornerRadius)value;
var scale = Scale;
if (!double.IsNaN(scale))
{
cornerRadius.TopLeft *= scale;
cornerRadius.TopRight *= scale;
cornerRadius.BottomRight *= scale;
cornerRadius.BottomLeft *= scale;
}
var filterType = Filter;
if (filterType == CornerRadiusFilterKind.TopLeftValue ||
filterType == CornerRadiusFilterKind.BottomRightValue)
{
return GetDoubleValue(cornerRadius, filterType);
}
return Convert(cornerRadius, filterType);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private static double GetDoubleValue(CornerRadius radius, CornerRadiusFilterKind filterKind)
{
switch (filterKind)
{
case CornerRadiusFilterKind.TopLeftValue:
return radius.TopLeft;
case CornerRadiusFilterKind.BottomRightValue:
return radius.BottomRight;
}
return 0;
}
}
public enum CornerRadiusFilterKind
{
None,
Top,
Right,
Bottom,
Left,
TopLeftValue,
BottomRightValue
}

View file

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Flow.Launcher.Converters;
public class PlacementRectangleConverter : IMultiValueConverter
{
public Thickness Margin { get; set; }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 &&
values[0] is double width &&
values[1] is double height)
{
var margin = Margin;
var topLeft = new Point(margin.Left, margin.Top);
var bottomRight = new Point(width - margin.Right, height - margin.Bottom);
var rect = new Rect(topLeft, bottomRight);
return rect;
}
return Rect.Empty;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Flow.Launcher.Converters;
public class SharedSizeGroupConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (Visibility)value != Visibility.Collapsed ? (string)parameter : null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View file

@ -5,7 +5,7 @@ using System.Windows.Input;
namespace Flow.Launcher.Converters;
class StringToKeyBindingConverter : IValueConverter
public class StringToKeyBindingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{

View file

@ -41,7 +41,7 @@ namespace Flow.Launcher
if (string.IsNullOrEmpty(Hotkey) && string.IsNullOrEmpty(ActionKeyword))
{
App.API.ShowMsgBox(App.API.GetTranslation("emptyPluginHotkey"));
App.API.ShowMsgBox(Localize.emptyPluginHotkey());
return;
}

View file

@ -40,14 +40,14 @@ namespace Flow.Launcher
{
if (string.IsNullOrEmpty(Key) || string.IsNullOrEmpty(Value))
{
App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut"));
App.API.ShowMsgBox(Localize.emptyShortcut());
return;
}
// Check if key is modified or adding a new one
if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key))
{
App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut"));
App.API.ShowMsgBox(Localize.duplicateShortcut());
return;
}

View file

@ -37,14 +37,53 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
</PropertyGroup>
<Target Name="RemoveUnnecessaryRuntimesAfterBuild" AfterTargets="Build">
<RemoveDir Directories="$(OutputPath)runtimes\browser-wasm;&#xD;&#xA; $(OutputPath)runtimes\linux-arm;&#xD;&#xA; $(OutputPath)runtimes\linux-arm64;&#xD;&#xA; $(OutputPath)runtimes\linux-armel;&#xD;&#xA; $(OutputPath)runtimes\linux-mips64;&#xD;&#xA; $(OutputPath)runtimes\linux-musl-arm;&#xD;&#xA; $(OutputPath)runtimes\linux-musl-arm64;&#xD;&#xA; $(OutputPath)runtimes\linux-musl-x64;&#xD;&#xA; $(OutputPath)runtimes\linux-musl-s390x;&#xD;&#xA; $(OutputPath)runtimes\linux-ppc64le;&#xD;&#xA; $(OutputPath)runtimes\linux-s390x;&#xD;&#xA; $(OutputPath)runtimes\linux-x64;&#xD;&#xA; $(OutputPath)runtimes\linux-x86;&#xD;&#xA; $(OutputPath)runtimes\maccatalyst-arm64;&#xD;&#xA; $(OutputPath)runtimes\maccatalyst-x64;&#xD;&#xA; $(OutputPath)runtimes\osx;&#xD;&#xA; $(OutputPath)runtimes\osx-arm64;&#xD;&#xA; $(OutputPath)runtimes\osx-x64;&#xD;&#xA; $(OutputPath)runtimes\win-arm;&#xD;&#xA; $(OutputPath)runtimes\win-arm64;" />
<RemoveDir Directories="$(OutputPath)runtimes\browser-wasm;
$(OutputPath)runtimes\linux-arm;
$(OutputPath)runtimes\linux-arm64;
$(OutputPath)runtimes\linux-armel;
$(OutputPath)runtimes\linux-mips64;
$(OutputPath)runtimes\linux-musl-arm;
$(OutputPath)runtimes\linux-musl-arm64;
$(OutputPath)runtimes\linux-musl-x64;
$(OutputPath)runtimes\linux-musl-s390x;
$(OutputPath)runtimes\linux-ppc64le;
$(OutputPath)runtimes\linux-s390x;
$(OutputPath)runtimes\linux-x64;
$(OutputPath)runtimes\linux-x86;
$(OutputPath)runtimes\maccatalyst-arm64;
$(OutputPath)runtimes\maccatalyst-x64;
$(OutputPath)runtimes\osx;
$(OutputPath)runtimes\osx-arm64;
$(OutputPath)runtimes\osx-x64;
$(OutputPath)runtimes\win-arm;
$(OutputPath)runtimes\win-arm64;"/>
</Target>
<Target Name="RemoveUnnecessaryRuntimesAfterPublish" AfterTargets="Publish">
<RemoveDir Directories="$(PublishDir)runtimes\browser-wasm;&#xD;&#xA; $(PublishDir)runtimes\linux-arm;&#xD;&#xA; $(PublishDir)runtimes\linux-arm64;&#xD;&#xA; $(PublishDir)runtimes\linux-armel;&#xD;&#xA; $(PublishDir)runtimes\linux-mips64;&#xD;&#xA; $(PublishDir)runtimes\linux-musl-arm;&#xD;&#xA; $(PublishDir)runtimes\linux-musl-arm64;&#xD;&#xA; $(PublishDir)runtimes\linux-musl-x64;&#xD;&#xA; $(PublishDir)runtimes\linux-musl-s390x;&#xD;&#xA; $(PublishDir)runtimes\linux-ppc64le;&#xD;&#xA; $(PublishDir)runtimes\linux-s390x;&#xD;&#xA; $(PublishDir)runtimes\linux-x64;&#xD;&#xA; $(PublishDir)runtimes\linux-x86;&#xD;&#xA; $(PublishDir)runtimes\maccatalyst-arm64;&#xD;&#xA; $(PublishDir)runtimes\maccatalyst-x64;&#xD;&#xA; $(PublishDir)runtimes\osx;&#xD;&#xA; $(PublishDir)runtimes\osx-arm64;&#xD;&#xA; $(PublishDir)runtimes\osx-x64;&#xD;&#xA; $(PublishDir)runtimes\win-arm;&#xD;&#xA; $(PublishDir)runtimes\win-arm64;" />
<RemoveDir Directories="$(PublishDir)runtimes\browser-wasm;
$(PublishDir)runtimes\linux-arm;
$(PublishDir)runtimes\linux-arm64;
$(PublishDir)runtimes\linux-armel;
$(PublishDir)runtimes\linux-mips64;
$(PublishDir)runtimes\linux-musl-arm;
$(PublishDir)runtimes\linux-musl-arm64;
$(PublishDir)runtimes\linux-musl-x64;
$(PublishDir)runtimes\linux-musl-s390x;
$(PublishDir)runtimes\linux-ppc64le;
$(PublishDir)runtimes\linux-s390x;
$(PublishDir)runtimes\linux-x64;
$(PublishDir)runtimes\linux-x86;
$(PublishDir)runtimes\maccatalyst-arm64;
$(PublishDir)runtimes\maccatalyst-x64;
$(PublishDir)runtimes\osx;
$(PublishDir)runtimes\osx-arm64;
$(PublishDir)runtimes\osx-x64;
$(PublishDir)runtimes\win-arm;
$(PublishDir)runtimes\win-arm64;"/>
</Target>
<ItemGroup>
@ -94,10 +133,12 @@
<ItemGroup>
<PackageReference Include="ChefKeys" Version="0.1.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
<PackageReference Include="Fody" Version="6.9.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.1" />
<PackageReference Include="MdXaml" Version="1.27.0" />
<PackageReference Include="MdXaml.AnimatedGif" Version="1.27.0" />
<PackageReference Include="MdXaml.Html" Version="1.27.0" />
@ -106,9 +147,6 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<!-- ModernWpfUI v0.9.5 introduced WinRT changes that causes Notification platform unavailable error on some machines -->
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -123,6 +161,10 @@
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<PropertyGroup>
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
</PropertyGroup>
<ItemGroup>
<Content Include="Resources\open.wav">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
@ -143,7 +185,7 @@
</Target>
<Target Name="RemoveDuplicateAnalyzers" BeforeTargets="CoreCompile">
<!-- Work around https://github.com/dotnet/wpf/issues/6792 -->
<!-- Workaround https://github.com/dotnet/wpf/issues/6792 -->
<ItemGroup>
<FilteredAnalyzer Include="@(Analyzer-&gt;Distinct())" />
<Analyzer Remove="@(Analyzer)" />

View file

@ -0,0 +1,33 @@
using System.Windows;
using System.Windows.Controls;
namespace Flow.Launcher.Helper;
public static class BorderHelper
{
#region Child
public static readonly DependencyProperty ChildProperty =
DependencyProperty.RegisterAttached(
"Child",
typeof(UIElement),
typeof(BorderHelper),
new PropertyMetadata(default(UIElement), OnChildChanged));
public static UIElement GetChild(Border border)
{
return (UIElement)border.GetValue(ChildProperty);
}
public static void SetChild(Border border, UIElement value)
{
border.SetValue(ChildProperty, value);
}
private static void OnChildChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Border)d).Child = (UIElement)e.NewValue;
}
#endregion
}

View file

@ -61,8 +61,8 @@ internal static class HotKeyMapper
string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace));
string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
string errorMsg = Localize.registerHotkeyFailed(hotkeyStr);
string errorMsgTitle = Localize.MessageBoxTitle();
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
@ -87,8 +87,8 @@ internal static class HotKeyMapper
e.Message,
e.StackTrace,
hotkeyStr));
string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
string errorMsg = Localize.registerHotkeyFailed(hotkeyStr);
string errorMsgTitle = Localize.MessageBoxTitle();
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
@ -112,8 +112,8 @@ internal static class HotKeyMapper
string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace));
string errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr);
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
string errorMsg = Localize.unregisterHotkeyFailed(hotkeyStr);
string errorMsgTitle = Localize.MessageBoxTitle();
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
@ -143,6 +143,8 @@ internal static class HotKeyMapper
return;
App.API.ShowMainWindow();
// Make sure to go back to the query results page first since it can cause issues if current page is context menu
App.API.BackToQueryResults();
App.API.ChangeQuery(hotkey.ActionKeyword, true);
});
}

View file

@ -0,0 +1,45 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
using Flow.Launcher.Storage;
namespace Flow.Launcher.Helper;
#nullable enable
public static class ResultHelper
{
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryResult item)
{
return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
}
public static async Task<Result?> PopulateResultsAsync(string pluginId, string trimmedQuery, string title, string subTitle, string recordKey)
{
var plugin = PluginManager.GetPluginForId(pluginId);
if (plugin == null) return null;
var query = QueryBuilder.Build(trimmedQuery, trimmedQuery, PluginManager.GetNonGlobalPlugins());
if (query == null) return null;
try
{
var freshResults = await PluginManager.QueryForPluginAsync(plugin, query, CancellationToken.None);
// Try to match by record key first if it is valid, otherwise fall back to title + subtitle match
if (string.IsNullOrEmpty(recordKey))
{
return freshResults?.FirstOrDefault(r => r.Title == title && r.SubTitle == subTitle);
}
else
{
return freshResults?.FirstOrDefault(r => r.RecordKey == recordKey) ??
freshResults?.FirstOrDefault(r => r.Title == title && r.SubTitle == subTitle);
}
}
catch (System.Exception e)
{
App.API.LogException(nameof(ResultHelper), $"Failed to query results for {plugin.Metadata.Name}", e);
return null;
}
}
}

View file

@ -1,4 +1,4 @@
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
@ -234,7 +234,7 @@ namespace Flow.Launcher
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
public string EmptyHotkey => App.API.GetTranslation("none");
public string EmptyHotkey => Localize.none();
public ObservableCollection<string> KeysToDisplay { get; set; } = new();

View file

@ -2,7 +2,7 @@
x:Class="Flow.Launcher.HotkeyControlDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
Background="{DynamicResource PopuBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0 1 0 0"

View file

@ -9,7 +9,7 @@ using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using ModernWpf.Controls;
using iNKORE.UI.WPF.Modern.Controls;
namespace Flow.Launcher;
@ -33,7 +33,7 @@ public partial class HotkeyControlDialog : ContentDialog
public EResultType ResultType { get; private set; } = EResultType.Cancel;
public string ResultValue { get; private set; } = string.Empty;
public static string EmptyHotkey => App.API.GetTranslation("none");
public static string EmptyHotkey => Localize.none();
private static bool isOpenFlowHotkey;
@ -41,7 +41,7 @@ public partial class HotkeyControlDialog : ContentDialog
{
WindowTitle = windowTitle switch
{
"" or null => App.API.GetTranslation("hotkeyRegTitle"),
"" or null => Localize.hotkeyRegTitle(),
_ => windowTitle
};
DefaultHotkey = defaultHotkey;
@ -146,10 +146,7 @@ public partial class HotkeyControlDialog : ContentDialog
Alert.Visibility = Visibility.Visible;
if (registeredHotkeyData.RemoveHotkey is not null)
{
tbMsg.Text = string.Format(
App.API.GetTranslation("hotkeyUnavailableEditable"),
description
);
tbMsg.Text = Localize.hotkeyUnavailableEditable(description);
SaveBtn.IsEnabled = false;
SaveBtn.Visibility = Visibility.Collapsed;
OverwriteBtn.IsEnabled = true;
@ -158,10 +155,7 @@ public partial class HotkeyControlDialog : ContentDialog
}
else
{
tbMsg.Text = string.Format(
App.API.GetTranslation("hotkeyUnavailableUneditable"),
description
);
tbMsg.Text = Localize.hotkeyUnavailableUneditable(description);
SaveBtn.IsEnabled = false;
SaveBtn.Visibility = Visibility.Visible;
OverwriteBtn.IsEnabled = false;
@ -175,7 +169,7 @@ public partial class HotkeyControlDialog : ContentDialog
if (!CheckHotkeyAvailability(hotkey.Value, true))
{
tbMsg.Text = App.API.GetTranslation("hotkeyUnavailable");
tbMsg.Text = Localize.hotkeyUnavailable();
Alert.Visibility = Visibility.Visible;
SaveBtn.IsEnabled = false;
SaveBtn.Visibility = Visibility.Visible;

View file

@ -66,6 +66,10 @@
<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="pluginStillInitializing">{0}: This plugin is still initializing...</system:String>
<system:String x:Key="pluginStillInitializingSubtitle">Select this result to requery</system:String>
<system:String x:Key="pluginFailedToRespond">{0}: Failed to respond!</system:String>
<system:String x:Key="pluginFailedToRespondSubtitle">Select this result for more info</system:String>
<!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Settings</system:String>
@ -77,6 +81,8 @@
<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="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="showTaskbarWhenOpened">Show taskbar when Flow Launcher is opened</system:String>
<system:String x:Key="showTaskbarWhenOpenedToolTip">Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars.</system:String>
<system:String x:Key="dontPromptUpdateMsg">Do not show new version notifications</system:String>
<system:String x:Key="SearchWindowPosition">Search Window Location</system:String>
<system:String x:Key="SearchWindowScreenRememberLastLaunchLocation">Remember Last Position</system:String>
@ -166,6 +172,10 @@
<system:String x:Key="homePageToolTip">Show home page results when query text is empty.</system:String>
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
<system:String x:Key="historyStyle">History Style</system:String>
<system:String x:Key="historyStyleTooltip">Choose the type of history to show in the History and Home Page</system:String>
<system:String x:Key="queryHistory">Query history</system:String>
<system:String x:Key="executedHistory">Last opened history</system:String>
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
<system:String x:Key="showAtTopmost">Show Search Window at Foremost</system:String>
<system:String x:Key="showAtTopmostToolTip">Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.</system:String>
@ -209,6 +219,8 @@
<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_uninstall">Uninstall</system:String>
<system:String x:Key="plugin_default_search_delay_time">Search delay time: default</system:String>
<system:String x:Key="plugin_search_delay_time">Search delay time: {0}ms</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="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
@ -219,6 +231,7 @@
<system:String x:Key="failedToUninstallPluginTitle">Fail to uninstall {0}</system:String>
<system:String x:Key="fileNotFoundMessage">Unable to find plugin.json from the extracted zip file, or this path {0} does not exist</system:String>
<system:String x:Key="pluginExistAlreadyMessage">A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin</system:String>
<system:String x:Key="errorCreatingSettingPanel">Error creating setting panel for plugin {0}:{1}{2}</system:String>
<!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plugin Store</system:String>
@ -467,8 +480,10 @@
<system:String x:Key="userdatapathButton">Open Folder</system:String>
<system:String x:Key="advanced">Advanced</system:String>
<system:String x:Key="logLevel">Log Level</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="LogLevelNONE">Silent</system:String>
<system:String x:Key="LogLevelERROR">Error</system:String>
<system:String x:Key="LogLevelINFO">Info</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="settingWindowFontTitle">Setting Window Font</system:String>
<!-- Release Notes Window -->
@ -590,7 +605,7 @@
The specified file manager could not be found. Please check the Custom File Manager setting under Settings > General.
</system:String>
<system:String x:Key="errorTitle">Error</system:String>
<system:String x:Key="folderOpenError">An error occurred while opening the folder. {0}</system:String>
<system:String x:Key="folderOpenError">An error occurred while opening the folder.</system:String>
<system:String x:Key="browserOpenError">An error occurred while opening the URL in the browser. Please check your Default Web Browser configuration in the General section of the settings window</system:String>
<system:String x:Key="fileNotFoundError">File or directory not found: {0}</system:String>

View file

@ -64,10 +64,10 @@
<system:String x:Key="PositionReset">位置のリセット</system:String>
<system:String x:Key="PositionResetToolTip">検索ウィンドウの位置をリセット</system:String>
<system:String x:Key="queryTextBoxPlaceholder">ここに入力して検索</system:String>
<system:String x:Key="pluginStillInitializing">{0}: This plugin is still initializing...</system:String>
<system:String x:Key="pluginStillInitializingSubtitle">Select this result to requery</system:String>
<system:String x:Key="pluginFailedToRespond">{0}: Failed to respond!</system:String>
<system:String x:Key="pluginFailedToRespondSubtitle">Select this result for more info</system:String>
<system:String x:Key="pluginStillInitializing">{0}: このプラグインはまだ初期化中です…</system:String>
<system:String x:Key="pluginStillInitializingSubtitle">この結果を選択して再検索する</system:String>
<system:String x:Key="pluginFailedToRespond">{0}: 応答に失敗しました!</system:String>
<system:String x:Key="pluginFailedToRespondSubtitle">詳細については、この結果を選択してください</system:String>
<!-- Setting General -->
<system:String x:Key="flowlauncher_settings">設定</system:String>
@ -454,7 +454,7 @@
<system:String x:Key="icons">アイコン</system:String>
<system:String x:Key="about_activate_times">あなたはFlow Launcherを {0} 回利用しました</system:String>
<system:String x:Key="checkUpdates">アップデートを確認する</system:String>
<system:String x:Key="BecomeASponsor">Become a Sponsor</system:String>
<system:String x:Key="BecomeASponsor">スポンサーになる</system:String>
<system:String x:Key="newVersionTips">新しいバージョン {0} が利用可能です。Flow Launcherを再起動してください。</system:String>
<system:String x:Key="checkUpdatesFailed">アップデートの確認に失敗しました、api.github.com への接続とプロキシ設定を確認してください。</system:String>
<system:String x:Key="downloadUpdatesFailed">

View file

@ -420,13 +420,13 @@
<system:String x:Key="DialogJumpWindowPositionFollowDefault">Default search window position. Displayed when triggered by search window hotkey</system:String>
<system:String x:Key="dialogJumpResultBehaviour">Dialog Jump Result Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window to the selected result path</system:String>
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Left click or Enter key</system:String>
<system:String x:Key="DialogJumpResultBehaviourRightClick">Right click</system:String>
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Clique esquerdo ou tecla Enter</system:String>
<system:String x:Key="DialogJumpResultBehaviourRightClick">Clique direito</system:String>
<system:String x:Key="dialogJumpFileResultBehaviour">Dialog Jump File Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpFileResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window when the result is a file path</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Fill full path in file name box</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Fill full path in file name box and open</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Fill directory in path box</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Preencher caminho total na caixa Nome do ficheiro</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Preencher caminho total na caixa Nome do ficheiro e abrir</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Preencher diretório na caixa Caminho</system:String>
<!-- Setting Proxy -->
<system:String x:Key="proxy">Proxy HTTP</system:String>

View file

@ -636,7 +636,7 @@ Ak pri zadávaní skratky pred ňu pridáte &quot;@&quot;, bude sa zhodovať s
<!-- Plugin Update Window -->
<system:String x:Key="restartAfterUpdating">Po aktualizácii pluginov reštartovať Flow Launcher</system:String>
<system:String x:Key="updatePluginCheckboxContent">{0}: Aktualizované z v{1} na v{2}</system:String>
<system:String x:Key="updatePluginCheckboxContent">{0}: Aktualizácia z v{1} na v{2}</system:String>
<system:String x:Key="updatePluginNoSelected">Nie je vybraný žiaden plugin</system:String>
<!-- Welcome Window -->

View file

@ -6,7 +6,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
Name="FlowMainWindow"
Title="Flow Launcher"
@ -526,7 +526,7 @@
<TextBlock
x:Name="PreviewSubTitle"
Style="{DynamicResource PreviewItemSubTitleStyle}"
Text="{Binding Result.SubTitle}" />
Text="{Binding PreviewDescription}" />
</StackPanel>
</Grid>
</Border>

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
using System.Linq;
using System.Media;
@ -25,7 +25,8 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.ViewModel;
using ModernWpf.Controls;
using iNKORE.UI.WPF.Modern;
using iNKORE.UI.WPF.Modern.Controls;
using DataObject = System.Windows.DataObject;
using Key = System.Windows.Input.Key;
using MouseButtons = System.Windows.Forms.MouseButtons;
@ -148,8 +149,8 @@ namespace Flow.Launcher
_settings.ReleaseNotesVersion = Constant.Version;
// Show release note popup with button
App.API.ShowMsgWithButton(
string.Format(App.API.GetTranslation("appUpdateTitle"), Constant.Version),
App.API.GetTranslation("appUpdateButtonContent"),
Localize.appUpdateTitle(Constant.Version),
Localize.appUpdateButtonContent(),
() =>
{
Application.Current.Dispatcher.Invoke(() =>
@ -191,11 +192,11 @@ namespace Flow.Launcher
// Initialize color scheme
if (_settings.ColorScheme == Constant.Light)
{
ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light;
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light;
}
else if (_settings.ColorScheme == Constant.Dark)
{
ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark;
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark;
}
// Initialize position
@ -321,6 +322,7 @@ namespace Flow.Launcher
break;
case nameof(Settings.ShowHomePage):
case nameof(Settings.ShowHistoryResultsForHomePage):
case nameof(Settings.HistoryStyle):
if (_viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
{
_viewModel.QueryResults();
@ -475,7 +477,7 @@ namespace Flow.Launcher
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{
var queryWithoutActionKeyword =
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search;
QueryBuilder.Build(QueryTextBox.Text, QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
{
@ -793,12 +795,12 @@ namespace Flow.Launcher
private void UpdateNotifyIconText()
{
var menu = _contextMenu;
((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") +
((MenuItem)menu.Items[0]).Header = Localize.iconTrayOpen() +
" (" + _settings.Hotkey + ")";
((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode");
((MenuItem)menu.Items[2]).Header = App.API.GetTranslation("PositionReset");
((MenuItem)menu.Items[3]).Header = App.API.GetTranslation("iconTraySettings");
((MenuItem)menu.Items[4]).Header = App.API.GetTranslation("iconTrayExit");
((MenuItem)menu.Items[1]).Header = Localize.GameMode();
((MenuItem)menu.Items[2]).Header = Localize.PositionReset();
((MenuItem)menu.Items[3]).Header = Localize.iconTraySettings();
((MenuItem)menu.Items[4]).Header = Localize.iconTrayExit();
}
private void InitializeContextMenu()
@ -808,31 +810,31 @@ namespace Flow.Launcher
var openIcon = new FontIcon { Glyph = "\ue71e" };
var open = new MenuItem
{
Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")",
Header = Localize.iconTrayOpen() + " (" + _settings.Hotkey + ")",
Icon = openIcon
};
var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };
var gamemode = new MenuItem
{
Header = App.API.GetTranslation("GameMode"),
Header = Localize.GameMode(),
Icon = gamemodeIcon
};
var positionresetIcon = new FontIcon { Glyph = "\ue73f" };
var positionreset = new MenuItem
{
Header = App.API.GetTranslation("PositionReset"),
Header = Localize.PositionReset(),
Icon = positionresetIcon
};
var settingsIcon = new FontIcon { Glyph = "\ue713" };
var settings = new MenuItem
{
Header = App.API.GetTranslation("iconTraySettings"),
Header = Localize.iconTraySettings(),
Icon = settingsIcon
};
var exitIcon = new FontIcon { Glyph = "\ue7e8" };
var exit = new MenuItem
{
Header = App.API.GetTranslation("iconTrayExit"),
Header = Localize.iconTrayExit(),
Icon = exitIcon
};
@ -842,8 +844,8 @@ namespace Flow.Launcher
settings.Click += (o, e) => App.API.OpenSettingDialog();
exit.Click += (o, e) => Close();
gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip");
positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip");
gamemode.ToolTip = Localize.GameModeToolTip();
positionreset.ToolTip = Localize.PositionResetToolTip();
_contextMenu.Items.Add(open);
_contextMenu.Items.Add(gamemode);
@ -858,7 +860,7 @@ namespace Flow.Launcher
public void UpdatePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
@ -882,7 +884,7 @@ namespace Flow.Launcher
private void InitializePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
InitializePositionInner();
InitializePositionInner();
return;

View file

@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
Title="{DynamicResource updateAllPluginsButtonContent}"
Width="530"
Background="{DynamicResource PopuBGColor}"
@ -66,13 +67,13 @@
Text="{DynamicResource updateAllPluginsButtonContent}"
TextAlignment="Left" />
<ScrollViewer
<ui:ScrollViewerEx
MaxHeight="300"
Margin="0 5 0 5"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="UpdatePluginStackPanel" />
</ScrollViewer>
</ui:ScrollViewerEx>
<Rectangle
Height="1"

View file

@ -23,7 +23,7 @@ namespace Flow.Launcher
{
var checkBox = new CheckBox
{
Content = string.Format(App.API.GetTranslation("updatePluginCheckboxContent"), plugin.Name, plugin.CurrentVersion, plugin.NewVersion),
Content = Localize.updatePluginCheckboxContent(plugin.Name, plugin.CurrentVersion, plugin.NewVersion),
IsChecked = true,
Margin = new Thickness(0, 5, 0, 5),
Tag = plugin,
@ -50,10 +50,7 @@ namespace Flow.Launcher
{
if (sender is not CheckBox cb) return;
if (cb.Tag is not PluginUpdateInfo plugin) return;
if (Plugins.Contains(plugin))
{
Plugins.Remove(plugin);
}
Plugins.Remove(plugin);
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
@ -66,7 +63,7 @@ namespace Flow.Launcher
{
if (Plugins.Count == 0)
{
App.API.ShowMsgBox(App.API.GetTranslation("updatePluginNoSelected"));
App.API.ShowMsgBox(Localize.updatePluginNoSelected());
return;
}

View file

@ -1,11 +1,10 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -31,8 +30,8 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.ViewModel;
using iNKORE.UI.WPF.Modern;
using JetBrains.Annotations;
using ModernWpf;
using Squirrel;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
@ -184,14 +183,14 @@ namespace Flow.Launcher
if (showDefaultNotification)
{
ShowMsg(
$"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}",
GetTranslation("completedSuccessfully"));
$"{Localize.copy()} {(isFile ? Localize.fileTitle(): Localize.folderTitle())}",
Localize.completedSuccessfully());
}
}
else
{
LogException(nameof(PublicAPIInstance), "Failed to copy file/folder to clipboard", exception);
ShowMsgError(GetTranslation("failedToCopy"));
ShowMsgError(Localize.failedToCopy());
}
}
else
@ -209,14 +208,14 @@ namespace Flow.Launcher
if (showDefaultNotification)
{
ShowMsg(
$"{GetTranslation("copy")} {GetTranslation("textTitle")}",
GetTranslation("completedSuccessfully"));
$"{Localize.copy()} {Localize.textTitle()}",
Localize.completedSuccessfully());
}
}
else
{
LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception);
ShowMsgError(GetTranslation("failedToCopy"));
ShowMsgError(Localize.failedToCopy());
}
}
}
@ -248,7 +247,10 @@ namespace Flow.Launcher
public string GetTranslation(string key) => Internationalization.GetTranslation(key);
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();
public List<PluginPair> GetAllPlugins() => PluginManager.GetAllLoadedPlugins();
public List<PluginPair> GetAllInitializedPlugins(bool includeFailed) =>
PluginManager.GetAllInitializedPlugins(includeFailed);
public MatchResult FuzzySearch(string query, string stringToCompare) =>
StringMatcher.FuzzySearch(query, stringToCompare);
@ -393,18 +395,18 @@ namespace Flow.Launcher
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 2)
{
LogError(ClassName, "File Manager not found");
LogException(ClassName, "File Manager not found", ex);
ShowMsgError(
GetTranslation("fileManagerNotFoundTitle"),
string.Format(GetTranslation("fileManagerNotFound"), ex.Message)
Localize.fileManagerNotFoundTitle(),
Localize.fileManagerNotFound()
);
}
catch (Exception ex)
{
LogException(ClassName, "Failed to open folder", ex);
ShowMsgError(
GetTranslation("errorTitle"),
string.Format(GetTranslation("folderOpenError"), ex.Message)
Localize.errorTitle(),
Localize.folderOpenError()
);
}
}
@ -413,7 +415,7 @@ namespace Flow.Launcher
{
if (uri.IsFile && !FilesFolders.FileOrLocationExists(uri.LocalPath))
{
ShowMsgError(GetTranslation("errorTitle"), string.Format(GetTranslation("fileNotFoundError"), uri.LocalPath));
ShowMsgError(Localize.errorTitle(), Localize.fileNotFoundError(uri.LocalPath));
return;
}
@ -439,8 +441,8 @@ namespace Flow.Launcher
var tabOrWindow = browserInfo.OpenInTab ? "tab" : "window";
LogException(ClassName, $"Failed to open URL in browser {tabOrWindow}: {path}, {inPrivate ?? browserInfo.EnablePrivate}, {browserInfo.PrivateArg}", e);
ShowMsgError(
GetTranslation("errorTitle"),
GetTranslation("browserOpenError")
Localize.errorTitle(),
Localize.browserOpenError()
);
}
}
@ -457,7 +459,7 @@ namespace Flow.Launcher
catch (Exception e)
{
LogException(ClassName, $"Failed to open: {uri.AbsoluteUri}", e);
ShowMsgError(GetTranslation("errorTitle"), e.Message);
ShowMsgError(Localize.errorTitle(), e.Message);
}
}
}

View file

@ -7,7 +7,7 @@
xmlns:local="clr-namespace:Flow.Launcher"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mdxam="clr-namespace:MdXaml;assembly=MdXaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
Title="{DynamicResource releaseNotes}"
Width="940"
@ -16,6 +16,7 @@
MinHeight="600"
Background="{DynamicResource PopuBGColor}"
Closed="Window_Closed"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}"
Loaded="Window_Loaded"
ResizeMode="CanResize"
@ -44,7 +45,7 @@
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="24" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- TitleBar and Control -->
@ -161,18 +162,23 @@
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="5"
Margin="18 0 18 0">
<cc:HyperLink x:Name="SeeMore" Text="{DynamicResource seeMoreReleaseNotes}" />
Margin="6 0 18 0">
<ui:HyperlinkButton
x:Name="SeeMore"
Content="{DynamicResource seeMoreReleaseNotes}"
NavigateUri="{Binding ReleaseNotes}" />
</Grid>
<!-- Do not use scroll function of MarkdownViewer because it does not support smooth scroll -->
<ScrollViewer
<ui:ScrollViewerEx
x:Name="MarkdownScrollViewer"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="5"
Width="500"
Height="500">
Height="500"
Margin="15 0 0 0"
Padding="0 0 15 0"
HorizontalAlignment="Stretch">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -193,11 +199,11 @@
VerticalScrollBarVisibility="Disabled"
Visibility="Collapsed" />
</Grid>
</ScrollViewer>
</ui:ScrollViewerEx>
<!-- This Grid is for display progress ring and refresh button. -->
<!-- And it is also for changing the size of the MarkdownViewer. -->
<!-- Because VerticalAlignment="Stretch" can cause size issue with MarkdownScrollViewer. -->
<!-- Because VerticalAlignment="Stretch" can cause height issue with MarkdownScrollViewer. -->
<Grid
Grid.Row="2"
Grid.Column="0"

View file

@ -10,27 +10,27 @@ using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.Http;
using iNKORE.UI.WPF.Modern;
namespace Flow.Launcher
{
public partial class ReleaseNotesWindow : Window
{
private static readonly string ReleaseNotes = Properties.Settings.Default.GithubRepo + "/releases";
public string ReleaseNotes => Properties.Settings.Default.GithubRepo + "/releases";
public ReleaseNotesWindow()
{
InitializeComponent();
SeeMore.Uri = ReleaseNotes;
ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged += ThemeManager_ActualApplicationThemeChanged;
ThemeManager.Current.ActualApplicationThemeChanged += ThemeManager_ActualApplicationThemeChanged;
}
#region Window Events
private void ThemeManager_ActualApplicationThemeChanged(ModernWpf.ThemeManager sender, object args)
private void ThemeManager_ActualApplicationThemeChanged(ThemeManager sender, object args)
{
Application.Current.Dispatcher.Invoke(() =>
{
if (ModernWpf.ThemeManager.Current.ActualApplicationTheme == ModernWpf.ApplicationTheme.Light)
if (ThemeManager.Current.ActualApplicationTheme == ApplicationTheme.Light)
{
MarkdownViewer.MarkdownStyle = (Style)Application.Current.Resources["DocumentStyleGithubLikeLight"];
MarkdownViewer.Foreground = Brushes.Black;
@ -58,7 +58,7 @@ namespace Flow.Launcher
private void Window_Closed(object sender, EventArgs e)
{
ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged;
ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged;
}
#endregion
@ -132,8 +132,8 @@ namespace Flow.Launcher
RefreshButton.Visibility = Visibility.Visible;
MarkdownViewer.Visibility = Visibility.Collapsed;
App.API.ShowMsgError(
App.API.GetTranslation("checkNetworkConnectionTitle"),
App.API.GetTranslation("checkNetworkConnectionSubTitle"));
Localize.checkNetworkConnectionTitle(),
Localize.checkNetworkConnectionSubTitle());
}
else
{
@ -147,7 +147,6 @@ namespace Flow.Launcher
private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)
{
MarkdownScrollViewer.Height = e.NewSize.Height;
MarkdownScrollViewer.Width = e.NewSize.Width;
}
private void MarkdownViewer_MouseWheel(object sender, MouseWheelEventArgs e)

View file

@ -48,10 +48,10 @@ namespace Flow.Launcher
_ => Constant.IssuesUrl
};
var paragraph = Hyperlink(App.API.GetTranslation("reportWindow_please_open_issue"), websiteUrl);
paragraph.Inlines.Add(string.Format(App.API.GetTranslation("reportWindow_upload_log"), log.FullName));
var paragraph = Hyperlink(Localize.reportWindow_please_open_issue(), websiteUrl);
paragraph.Inlines.Add(Localize.reportWindow_upload_log(log.FullName));
paragraph.Inlines.Add("\n");
paragraph.Inlines.Add(App.API.GetTranslation("reportWindow_copy_below"));
paragraph.Inlines.Add(Localize.reportWindow_copy_below());
ErrorTextbox.Document.Blocks.Add(paragraph);
StringBuilder content = new StringBuilder();

View file

@ -1,139 +0,0 @@
<UserControl
x:Class="Flow.Launcher.Resources.Controls.Card"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Flow.Launcher.Resources.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.modernwpf.com/2019"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Template>
<ControlTemplate TargetType="UserControl">
<Border x:Name="BD" HorizontalAlignment="Stretch">
<Border.Style>
<Style TargetType="{x:Type Border}">
<Setter Property="Background" Value="{DynamicResource Color00B}" />
<Setter Property="BorderBrush" Value="{DynamicResource Color03B}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="MinHeight" Value="68" />
<Setter Property="Padding" Value="0 15 0 15" />
<Setter Property="Margin" Value="0 4 0 0" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="Inside">
<Setter Property="BorderThickness" Value="0 1 0 0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Margin" Value="0 0 0 0" />
<Setter Property="Background" Value="Transparent" />
</DataTrigger>
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="InsideFit">
<Setter Property="BorderThickness" Value="0 1 0 0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Margin" Value="0 0 0 0" />
<Setter Property="Padding" Value="35 0 26 0" />
<Setter Property="Background" Value="Transparent" />
</DataTrigger>
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="First">
<Setter Property="Margin" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</DataTrigger>
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="Middle">
<Setter Property="Margin" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 1 0 0" />
</DataTrigger>
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="Last">
<Setter Property="Margin" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 1 0 0" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="auto"
MinWidth="20"
MaxWidth="60" />
<ColumnDefinition Width="8*" />
<ColumnDefinition Width="Auto" MinWidth="30" />
</Grid.ColumnDefinitions>
<ContentControl
Grid.Row="0"
Grid.Column="2"
Margin="0 0 16 0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
<StackPanel>
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<Setter Property="Grid.Column" Value="1" />
<Setter Property="Width" Value="Auto" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</StackPanel.Style>
<TextBlock x:Name="ItemTitle" Text="{Binding Title, RelativeSource={RelativeSource AncestorType=local:Card}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
<Setter Property="Margin" Value="0 0 0 0" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock x:Name="SubTitle" Text="{Binding Sub, RelativeSource={RelativeSource AncestorType=local:Card}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=SubTitle, Path=Text}" Value="{x:Static sys:String.Empty}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Foreground" Value="{DynamicResource Color04B}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="0 0 0 0" />
<Setter Property="Padding" Value="0 0 24 0" />
<Setter Property="TextWrapping" Value="WrapWithOverflow" />
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<TextBlock x:Name="ItemIcon" Text="{Binding Icon, RelativeSource={RelativeSource AncestorType=local:Card}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=ItemIcon, Path=Text}" Value="{x:Static sys:String.Empty}">
<Setter Property="Margin" Value="24 0 0 0" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Margin" Value="24 0 16 0" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontSize" Value="20" />
<Setter Property="FontFamily" Value="/Resources/#Segoe Fluent Icons" />
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Border>
</ControlTemplate>
</UserControl.Template>
</UserControl>

View file

@ -1,67 +0,0 @@
using System.Windows;
using UserControl = System.Windows.Controls.UserControl;
namespace Flow.Launcher.Resources.Controls
{
public partial class Card : UserControl
{
public enum CardType
{
Default,
Inside,
InsideFit,
First,
Middle,
Last
}
public Card()
{
InitializeComponent();
}
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(Card), new PropertyMetadata(string.Empty));
public string Sub
{
get { return (string)GetValue(SubProperty); }
set { SetValue(SubProperty, value); }
}
public static readonly DependencyProperty SubProperty =
DependencyProperty.Register(nameof(Sub), typeof(string), typeof(Card), new PropertyMetadata(string.Empty));
public string Icon
{
get { return (string)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(nameof(Icon), typeof(string), typeof(Card), new PropertyMetadata(string.Empty));
/// <summary>
/// Gets or sets additional content for the UserControl
/// </summary>
public object AdditionalContent
{
get { return (object)GetValue(AdditionalContentProperty); }
set { SetValue(AdditionalContentProperty, value); }
}
public static readonly DependencyProperty AdditionalContentProperty =
DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(Card),
new PropertyMetadata(null));
public CardType Type
{
get { return (CardType)GetValue(TypeProperty); }
set { SetValue(TypeProperty, value); }
}
public static readonly DependencyProperty TypeProperty =
DependencyProperty.Register(nameof(Type), typeof(CardType), typeof(Card),
new PropertyMetadata(CardType.Default));
}
}

View file

@ -1,32 +0,0 @@
<UserControl x:Class="Flow.Launcher.Resources.Controls.CardGroup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance cc:CardGroup}"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<Style TargetType="cc:Card" x:Key="FirstStyle">
<Setter Property="cc:CardGroup.Position" Value="First" />
</Style>
<Style TargetType="cc:Card" x:Key="MiddleStyle">
<Setter Property="cc:CardGroup.Position" Value="Middle" />
</Style>
<Style TargetType="cc:Card" x:Key="LastStyle">
<Setter Property="cc:CardGroup.Position" Value="Last" />
</Style>
<cc:CardGroupCardStyleSelector
x:Key="CardStyleSelector"
FirstStyle="{StaticResource FirstStyle}"
MiddleStyle="{StaticResource MiddleStyle}"
LastStyle="{StaticResource LastStyle}" />
</UserControl.Resources>
<Border Background="{DynamicResource Color00B}" BorderBrush="{DynamicResource Color03B}" BorderThickness="1"
CornerRadius="5">
<ItemsControl ItemsSource="{Binding Content, RelativeSource={RelativeSource AncestorType=cc:CardGroup}}"
ItemContainerStyleSelector="{StaticResource CardStyleSelector}" />
</Border>
</UserControl>

View file

@ -1,47 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace Flow.Launcher.Resources.Controls;
public partial class CardGroup : UserControl
{
public enum CardGroupPosition
{
NotInGroup,
First,
Middle,
Last
}
public new ObservableCollection<Card> Content
{
get { return (ObservableCollection<Card>)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public static new readonly DependencyProperty ContentProperty =
DependencyProperty.Register(nameof(Content), typeof(ObservableCollection<Card>), typeof(CardGroup));
public static readonly DependencyProperty PositionProperty = DependencyProperty.RegisterAttached(
"Position", typeof(CardGroupPosition), typeof(CardGroup),
new FrameworkPropertyMetadata(CardGroupPosition.NotInGroup, FrameworkPropertyMetadataOptions.AffectsRender)
);
public static void SetPosition(UIElement element, CardGroupPosition value)
{
element.SetValue(PositionProperty, value);
}
public static CardGroupPosition GetPosition(UIElement element)
{
return (CardGroupPosition)element.GetValue(PositionProperty);
}
public CardGroup()
{
InitializeComponent();
Content = new ObservableCollection<Card>();
}
}

View file

@ -1,21 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace Flow.Launcher.Resources.Controls;
public class CardGroupCardStyleSelector : StyleSelector
{
public Style FirstStyle { get; set; }
public Style MiddleStyle { get; set; }
public Style LastStyle { get; set; }
public override Style SelectStyle(object item, DependencyObject container)
{
var itemsControl = ItemsControl.ItemsControlFromItemContainer(container);
var index = itemsControl.ItemContainerGenerator.IndexFromContainer(container);
if (index == 0) return FirstStyle;
if (index == itemsControl.Items.Count - 1) return LastStyle;
return MiddleStyle;
}
}

View file

@ -0,0 +1,253 @@
using iNKORE.UI.WPF.Modern.Controls;
using iNKORE.UI.WPF.Modern.Controls.Helpers;
using iNKORE.UI.WPF.Modern.Controls.Primitives;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Flow.Launcher.Resources.Controls
{
// TODO: Use IsScrollAnimationEnabled property in future: https://github.com/iNKORE-NET/UI.WPF.Modern/pull/347
public class CustomScrollViewerEx : ScrollViewer
{
private double LastVerticalLocation = 0;
private double LastHorizontalLocation = 0;
public CustomScrollViewerEx()
{
Loaded += OnLoaded;
var valueSource = DependencyPropertyHelper.GetValueSource(this, AutoPanningMode.IsEnabledProperty).BaseValueSource;
if (valueSource == BaseValueSource.Default)
{
AutoPanningMode.SetIsEnabled(this, true);
}
}
#region Orientation
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register(
nameof(Orientation),
typeof(Orientation),
typeof(CustomScrollViewerEx),
new PropertyMetadata(Orientation.Vertical));
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
#endregion
#region AutoHideScrollBars
public static readonly DependencyProperty AutoHideScrollBarsProperty =
ScrollViewerHelper.AutoHideScrollBarsProperty
.AddOwner(
typeof(CustomScrollViewerEx),
new PropertyMetadata(true, OnAutoHideScrollBarsChanged));
public bool AutoHideScrollBars
{
get => (bool)GetValue(AutoHideScrollBarsProperty);
set => SetValue(AutoHideScrollBarsProperty, value);
}
private static void OnAutoHideScrollBarsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomScrollViewerEx sv)
{
sv.UpdateVisualState();
}
}
#endregion
private void OnLoaded(object sender, RoutedEventArgs e)
{
LastVerticalLocation = VerticalOffset;
LastHorizontalLocation = HorizontalOffset;
UpdateVisualState(false);
}
/// <inheritdoc/>
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
if (Style == null && ReadLocalValue(StyleProperty) == DependencyProperty.UnsetValue)
{
SetResourceReference(StyleProperty, typeof(ScrollViewer));
}
}
/// <inheritdoc/>
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
var Direction = GetDirection();
ScrollViewerBehavior.SetIsAnimating(this, true);
if (Direction == Orientation.Vertical)
{
if (ScrollableHeight > 0)
{
e.Handled = true;
}
var WheelChange = e.Delta * (ViewportHeight / 1.5) / ActualHeight;
var newOffset = LastVerticalLocation - WheelChange;
if (newOffset < 0)
{
newOffset = 0;
}
if (newOffset > ScrollableHeight)
{
newOffset = ScrollableHeight;
}
if (newOffset == LastVerticalLocation)
{
return;
}
ScrollToVerticalOffset(LastVerticalLocation);
ScrollToValue(newOffset, Direction);
LastVerticalLocation = newOffset;
}
else
{
if (ScrollableWidth > 0)
{
e.Handled = true;
}
var WheelChange = e.Delta * (ViewportWidth / 1.5) / ActualWidth;
var newOffset = LastHorizontalLocation - WheelChange;
if (newOffset < 0)
{
newOffset = 0;
}
if (newOffset > ScrollableWidth)
{
newOffset = ScrollableWidth;
}
if (newOffset == LastHorizontalLocation)
{
return;
}
ScrollToHorizontalOffset(LastHorizontalLocation);
ScrollToValue(newOffset, Direction);
LastHorizontalLocation = newOffset;
}
}
/// <inheritdoc/>
protected override void OnScrollChanged(ScrollChangedEventArgs e)
{
base.OnScrollChanged(e);
if (!ScrollViewerBehavior.GetIsAnimating(this))
{
LastVerticalLocation = VerticalOffset;
LastHorizontalLocation = HorizontalOffset;
}
}
private Orientation GetDirection()
{
var isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
if (Orientation == Orientation.Horizontal)
{
return isShiftDown ? Orientation.Vertical : Orientation.Horizontal;
}
else
{
return isShiftDown ? Orientation.Horizontal : Orientation.Vertical;
}
}
/// <summary>
/// Causes the <see cref="ScrollViewerEx"/> to load a new view into the viewport using the specified offsets and zoom factor.
/// </summary>
/// <param name="horizontalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableWidth"/> that specifies the distance the content should be scrolled horizontally.</param>
/// <param name="verticalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableHeight"/> that specifies the distance the content should be scrolled vertically.</param>
/// <param name="zoomFactor">A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor.</param>
/// <returns><see langword="true"/> if the view is changed; otherwise, <see langword="false"/>.</returns>
public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor)
{
return ChangeView(horizontalOffset, verticalOffset, zoomFactor, false);
}
/// <summary>
/// Causes the <see cref="ScrollViewerEx"/> to load a new view into the viewport using the specified offsets and zoom factor, and optionally disables scrolling animation.
/// </summary>
/// <param name="horizontalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableWidth"/> that specifies the distance the content should be scrolled horizontally.</param>
/// <param name="verticalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableHeight"/> that specifies the distance the content should be scrolled vertically.</param>
/// <param name="zoomFactor">A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor.</param>
/// <param name="disableAnimation"><see langword="true"/> to disable zoom/pan animations while changing the view; otherwise, <see langword="false"/>. The default is false.</param>
/// <returns><see langword="true"/> if the view is changed; otherwise, <see langword="false"/>.</returns>
public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor, bool disableAnimation)
{
if (disableAnimation)
{
if (horizontalOffset.HasValue)
{
ScrollToHorizontalOffset(horizontalOffset.Value);
}
if (verticalOffset.HasValue)
{
ScrollToVerticalOffset(verticalOffset.Value);
}
}
else
{
if (horizontalOffset.HasValue)
{
ScrollToHorizontalOffset(LastHorizontalLocation);
ScrollToValue(Math.Min(ScrollableWidth, horizontalOffset.Value), Orientation.Horizontal);
LastHorizontalLocation = horizontalOffset.Value;
}
if (verticalOffset.HasValue)
{
ScrollToVerticalOffset(LastVerticalLocation);
ScrollToValue(Math.Min(ScrollableHeight, verticalOffset.Value), Orientation.Vertical);
LastVerticalLocation = verticalOffset.Value;
}
}
return true;
}
private void ScrollToValue(double value, Orientation Direction)
{
if (Direction == Orientation.Vertical)
{
ScrollToVerticalOffset(value);
}
else
{
ScrollToHorizontalOffset(value);
}
ScrollViewerBehavior.SetIsAnimating(this, false);
}
private void UpdateVisualState(bool useTransitions = true)
{
var stateName = AutoHideScrollBars ? "NoIndicator" : "MouseIndicator";
VisualStateManager.GoToState(this, stateName, useTransitions);
}
}
}

View file

@ -1,312 +0,0 @@
<UserControl
x:Class="Flow.Launcher.Resources.Controls.ExCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Flow.Launcher.Resources.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.modernwpf.com/2019"
mc:Ignorable="d">
<UserControl.Template>
<ControlTemplate TargetType="UserControl">
<Expander
x:Name="expanderHeader"
Padding="0"
BorderThickness="1"
SnapsToDevicePixels="False">
<Expander.Style>
<Style TargetType="{x:Type Expander}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="Background" Value="{DynamicResource Color00B}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="BorderBrush" Value="{DynamicResource Color03B}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<Border
x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="5"
SnapsToDevicePixels="true">
<DockPanel>
<ToggleButton
x:Name="HeaderSite"
MinWidth="0"
MinHeight="68"
Margin="0,0,0,0"
Padding="0,0,0,0"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
DockPanel.Dock="Top"
FocusVisualStyle="{DynamicResource ExpanderHeaderFocusVisual}"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontStretch="{TemplateBinding FontStretch}"
FontStyle="{TemplateBinding FontStyle}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=Expander}}">
<ToggleButton.Style>
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border
x:Name="ToggleBtn"
Padding="{TemplateBinding Padding}"
Background="{DynamicResource Color00B}"
ClipToBounds="True"
CornerRadius="5">
<Grid SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="30" />
</Grid.ColumnDefinitions>
<ContentPresenter
Grid.Column="0"
Margin="0,0,0,0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
RecognizesAccessKey="True"
SnapsToDevicePixels="True" />
<Grid
x:Name="ChevronGrid"
Grid.Column="2"
Margin="0,0,18,0"
VerticalAlignment="Center"
Background="Transparent"
RenderTransformOrigin="0.5, 0.5">
<Grid.RenderTransform>
<RotateTransform Angle="0" />
</Grid.RenderTransform>
<Ellipse
x:Name="circle"
Width="19"
Height="19"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stroke="Transparent" />
<Path
x:Name="arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 1,1.5 L 4.5,5 L 8,1.5"
SnapsToDevicePixels="false"
Stroke="#666"
StrokeThickness="1" />
</Grid>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true">
<Setter TargetName="arrow" Property="Data" Value="M 1,4.5 L 4.5,1 L 8,4.5" />
<Setter TargetName="ToggleBtn" Property="CornerRadius" Value="5 5 0 0" />
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="circle" Property="Stroke" Value="Transparent" />
<Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color05B}" />
<Setter TargetName="ToggleBtn" Property="Background" Value="{DynamicResource CustomExpanderHover}" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="circle" Property="Stroke" Value="Transparent" />
<Setter TargetName="circle" Property="StrokeThickness" Value="1.5" />
<Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color17B}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
<Border x:Name="ContentPresenterBorder" BorderThickness="0">
<ContentPresenter
x:Name="ExpandSite"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
DockPanel.Dock="Bottom"
Focusable="false" />
<Border.LayoutTransform>
<ScaleTransform ScaleY="0" />
</Border.LayoutTransform>
</Border>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="true">
<Setter TargetName="ExpandSite" Property="Visibility" Value="Visible" />
<Setter TargetName="ContentPresenterBorder" Property="BorderThickness" Value="0,0,0,0" />
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
From="0.0"
To="1.0"
Duration="00:00:00.00" />
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.Opacity)"
From="0.0"
To="1.0"
Duration="00:00:00.00" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
From="1.0"
To="0"
Duration="00:00:00.00" />
<!-- Animation 00:00:00.167 -->
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.Opacity)"
From="1.0"
To="0.0"
Duration="00:00:00.00" />
<!-- Animation 00:00:00.167 -->
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="ExpandDirection" Value="Right">
<Setter TargetName="ExpandSite" Property="DockPanel.Dock" Value="Right" />
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Left" />
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource ExpanderRightHeaderStyle}" />
</Trigger>
<Trigger Property="ExpandDirection" Value="Up">
<Setter TargetName="ExpandSite" Property="DockPanel.Dock" Value="Top" />
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Bottom" />
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource ExpanderUpHeaderStyle}" />
</Trigger>
<Trigger Property="ExpandDirection" Value="Left">
<Setter TargetName="ExpandSite" Property="DockPanel.Dock" Value="Left" />
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Right" />
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource ExpanderLeftHeaderStyle}" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Expander.Style>
<Expander.Header>
<Border Margin="0" Padding="0,12,0,12">
<Grid Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Expander}}, Path=ActualWidth}" HorizontalAlignment="Left">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="auto"
MinWidth="20"
MaxWidth="60" />
<ColumnDefinition Width="7*" />
<ColumnDefinition Width="Auto" MinWidth="30" />
<ColumnDefinition Width="Auto" MinWidth="30" />
</Grid.ColumnDefinitions>
<ContentControl
x:Name="firstContentPresenter"
Grid.Column="2"
Margin="0,0,14,0"
HorizontalAlignment="Right"
Content="{Binding SideContent, RelativeSource={RelativeSource AncestorType=local:ExCard}}" />
<TextBlock
x:Name="ItemIcon"
Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding Icon, RelativeSource={RelativeSource AncestorType=local:ExCard}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=ItemIcon, Path=Text}" Value="{x:Static sys:String.Empty}">
<Setter Property="Margin" Value="24,0,0,0" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Margin" Value="24,0,16,0" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontSize" Value="20" />
<Setter Property="FontFamily" Value="/Resources/#Segoe Fluent Icons" />
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
</Style>
</TextBlock.Style>
</TextBlock>
<StackPanel Grid.Column="1" Margin="0,0,14,0">
<StackPanel.Style>
<Style TargetType="{x:Type StackPanel}">
<Setter Property="Grid.Column" Value="1" />
<Setter Property="Width" Value="Auto" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</StackPanel.Style>
<TextBlock x:Name="ItemTitle" Text="{Binding Title, RelativeSource={RelativeSource AncestorType=local:ExCard}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock x:Name="SubTitle" Text="{Binding Sub, RelativeSource={RelativeSource AncestorType=local:ExCard}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=SubTitle, Path=Text}" Value="{x:Static sys:String.Empty}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Foreground" Value="{DynamicResource Color04B}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="Padding" Value="0,0,24,0" />
<Setter Property="TextWrapping" Value="WrapWithOverflow" />
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</Grid>
</Border>
</Expander.Header>
<Grid
Grid.Column="0"
Grid.ColumnSpan="4"
HorizontalAlignment="Stretch"
FlowDirection="LeftToRight">
<StackPanel Margin="0,0,0,0" Orientation="Vertical">
<ContentControl
Grid.Column="0"
Grid.ColumnSpan="4"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
</StackPanel>
</Grid>
</Expander>
</ControlTemplate>
</UserControl.Template>
</UserControl>

View file

@ -1,57 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace Flow.Launcher.Resources.Controls
{
public partial class ExCard : UserControl
{
public ExCard()
{
InitializeComponent();
}
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty));
public string Sub
{
get { return (string)GetValue(SubProperty); }
set { SetValue(SubProperty, value); }
}
public static readonly DependencyProperty SubProperty =
DependencyProperty.Register(nameof(Sub), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty));
public string Icon
{
get { return (string)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(nameof(Icon), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty));
/// <summary>
/// Gets or sets additional content for the UserControl
/// </summary>
public object AdditionalContent
{
get { return (object)GetValue(AdditionalContentProperty); }
set { SetValue(AdditionalContentProperty, value); }
}
public static readonly DependencyProperty AdditionalContentProperty =
DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(ExCard),
new PropertyMetadata(null));
public object SideContent
{
get { return (object)GetValue(SideContentProperty); }
set { SetValue(SideContentProperty, value); }
}
public static readonly DependencyProperty SideContentProperty =
DependencyProperty.Register(nameof(SideContent), typeof(object), typeof(ExCard),
new PropertyMetadata(null));
}
}

View file

@ -1,14 +0,0 @@
<UserControl x:Class="Flow.Launcher.Resources.Controls.HyperLink"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<TextBlock>
<Hyperlink NavigateUri="{Binding Uri, RelativeSource={RelativeSource AncestorType=UserControl}}"
RequestNavigate="Hyperlink_OnRequestNavigate">
<Run Text="{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</Hyperlink>
</TextBlock>
</UserControl>

View file

@ -1,39 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
namespace Flow.Launcher.Resources.Controls;
public partial class HyperLink : UserControl
{
public static readonly DependencyProperty UriProperty = DependencyProperty.Register(
nameof(Uri), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string))
);
public string Uri
{
get => (string)GetValue(UriProperty);
set => SetValue(UriProperty, value);
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
nameof(Text), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string))
);
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public HyperLink()
{
InitializeComponent();
}
private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e)
{
App.API.OpenUrl(e.Uri);
e.Handled = true;
}
}

View file

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

View file

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

View file

@ -6,7 +6,7 @@
xmlns:converters="clr-namespace:Flow.Launcher.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:viewModel="clr-namespace:Flow.Launcher.ViewModel"
d:DataContext="{d:DesignInstance viewModel:PluginViewModel}"
d:DesignHeight="300"
@ -66,6 +66,7 @@
Text="{DynamicResource priority}"
ToolTip="{DynamicResource priorityToolTip}" />
<ui:NumberBox
MinWidth="120"
Margin="0 0 8 0"
Maximum="999"
Minimum="-999"
@ -89,6 +90,7 @@
ToolTip="{DynamicResource searchDelayToolTip}" />
<ui:NumberBox
Width="120"
MinWidth="120"
Margin="0 0 8 0"
IsEnabled="{Binding SearchDelayEnabled}"
Maximum="1000"

View file

@ -1,4 +1,4 @@
using ModernWpf.Controls;
using iNKORE.UI.WPF.Modern.Controls;
namespace Flow.Launcher.Resources.Controls;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Flow.Launcher.Resources.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
Title="WelcomePage1"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d">
@ -99,11 +99,11 @@
</Style.Triggers>
</Style>
</Page.Resources>
<ScrollViewer>
<ui:ScrollViewerEx>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="250" />
<RowDefinition Height="340"/>
<RowDefinition Height="340" />
</Grid.RowDefinitions>
<Border Grid.Row="0" HorizontalAlignment="Stretch">
@ -156,7 +156,8 @@
<TextBlock
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource Welcome_Page1_Title}" TextWrapping="WrapWithOverflow"/>
Text="{DynamicResource Welcome_Page1_Title}"
TextWrapping="WrapWithOverflow" />
<TextBlock
Margin="0 10 24 0"
FontSize="14"
@ -185,5 +186,5 @@
</StackPanel>
</Canvas>
</Grid>
</ScrollViewer>
</ui:ScrollViewerEx>
</ui:Page>

View file

@ -7,7 +7,7 @@
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
xmlns:local="clr-namespace:Flow.Launcher.Resources.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
Title="WelcomePage2"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d">
@ -34,11 +34,11 @@
</Style.Triggers>
</Style>
</Page.Resources>
<ScrollViewer>
<ui:ScrollViewerEx>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="250" />
<RowDefinition Height="340"/>
<RowDefinition Height="340" />
</Grid.RowDefinitions>
<Border Grid.Row="0" HorizontalAlignment="Stretch">
@ -89,12 +89,13 @@
</StackPanel>
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Visible">
<StackPanel Margin="24 20 24 20">
<Grid Grid.Row="1">
<StackPanel Margin="24 20 24 20">
<TextBlock
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource Welcome_Page2_Title}" TextWrapping="WrapWithOverflow"/>
Text="{DynamicResource Welcome_Page2_Title}"
TextWrapping="WrapWithOverflow" />
<TextBlock
Margin="0 10 0 0"
FontSize="14"
@ -119,7 +120,7 @@
WindowTitle="{DynamicResource flowlauncherHotkey}" />
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</ScrollViewer>
</ui:ScrollViewerEx>
</ui:Page>

View file

@ -4,9 +4,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
xmlns:local="clr-namespace:Flow.Launcher.Resources.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
Title="WelcomePage3"
VerticalAlignment="Stretch"
mc:Ignorable="d">
@ -40,75 +41,97 @@
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource Welcome_Page3_Title}" />
<ScrollViewer
<ui:ScrollViewerEx
Grid.Row="1"
Height="478"
Margin="0 0 0 0"
Height="483"
HorizontalAlignment="Stretch"
FontSize="13">
<StackPanel Margin="24 0 24 0">
<Border
BorderBrush="{DynamicResource Color03B}"
BorderThickness="0"
CornerRadius="5">
<StackPanel>
<cc:Card
Title="{DynamicResource HotkeyUpDownDesc}"
BorderThickness="0 0 0 0"
Type="Inside">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="←+→" Type="Small" />
</StackPanel>
</cc:Card>
<cc:Card
Title="{DynamicResource HotkeyLeftRightDesc}"
BorderThickness="0 0 0 0"
Type="Inside">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="↑+↓" Type="Small" />
</StackPanel>
</cc:Card>
<cc:Card
Title="{DynamicResource HotkeyESCDesc}"
BorderThickness="0 0 0 0"
Type="Inside">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="ESC" Type="Small" />
</StackPanel>
</cc:Card>
<cc:Card
Title="{DynamicResource HotkeyRunDesc}"
BorderThickness="0 0 0 0"
Type="Inside">
<cc:HotkeyDisplay Keys="ENTER" Type="Small" />
</cc:Card>
<cc:Card
Title="{DynamicResource HotkeyShiftEnterDesc}"
BorderThickness="0 0 0 0"
Type="Inside">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="SHIFT+ENTER" Type="Small" />
</StackPanel>
</cc:Card>
<cc:Card
Title="{DynamicResource HotkeyCtrlEnterDesc}"
BorderThickness="0 0 0 0"
Type="Inside">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="CTRL+ENTER" Type="Small" />
</StackPanel>
</cc:Card>
<cc:Card
Title="{DynamicResource HotkeyCtrlShiftEnterDesc}"
BorderThickness="0 0 0 0"
Type="Inside">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="CTRL+SHIFT+ENTER" Type="Small" />
</StackPanel>
</cc:Card>
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyUpDownDesc}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="←+→" Type="Small" />
</StackPanel>
</Border>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyLeftRightDesc}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="↑+↓" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyESCDesc}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="ESC" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyRunDesc}">
<cc:HotkeyDisplay Keys="ENTER" Type="Small" />
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyShiftEnterDesc}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="SHIFT+ENTER" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyCtrlEnterDesc}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="CTRL+ENTER" Type="Small" />
</StackPanel>
</ui:SettingsCard>
<Border
Height="1"
Background="{DynamicResource Color03B}"
BorderThickness="0" />
<ui:SettingsCard
Background="Transparent"
BorderThickness="0 0 0 0"
Header="{DynamicResource HotkeyCtrlShiftEnterDesc}">
<StackPanel Orientation="Horizontal">
<cc:HotkeyDisplay Keys="CTRL+SHIFT+ENTER" Type="Small" />
</StackPanel>
</ui:SettingsCard>
</StackPanel>
</ScrollViewer>
</ui:ScrollViewerEx>
</Grid>
</ui:Page>

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