Merge branch 'Flow-Launcher:dev' into Fix-'100%-CPU'-BrowserBookmark-issue

This commit is contained in:
dcog989 2025-07-22 20:37:24 +01:00 committed by GitHub
commit d57eca279f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 5241 additions and 460 deletions

View file

@ -4,5 +4,4 @@ ssh
ubuntu
runcount
Firefox
Português
Português (Brasil)
workaround

View file

@ -103,3 +103,4 @@ Reloadable
metadatas
WMP
VSTHRD
CJK

View file

@ -134,3 +134,12 @@
\bčeština\b
\bPortuguês\b
\bIoc\b
\bXiao\s*He\b
\bZi\s*Ran\s*Ma\b
\bWei\s*Ruan\b
\bZhi\s*Neng\s*ABC\b
\bZi\s*Guang\s*Pin\s*Yin\b
\bPin\s*Yin\s*Jia\s*Jia\b
\bXing\s*Kong\s*Jian\s*Dao\b
\bDa\s*Niu\b
\bXiao\s*Lang\b

View file

@ -14,7 +14,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
dotnet-version: 9.0.x
- name: Update Plugins To Production Version
run: |
@ -42,7 +42,7 @@ jobs:
- name: Build BrowserBookmark
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
7z a -tzip "Flow.Launcher.Plugin.BrowserBookmark.zip" "./Flow.Launcher.Plugin.BrowserBookmark/*"
rm -r "Flow.Launcher.Plugin.BrowserBookmark"
@ -66,7 +66,7 @@ jobs:
- name: Build Calculator
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
7z a -tzip "Flow.Launcher.Plugin.Calculator.zip" "./Flow.Launcher.Plugin.Calculator/*"
rm -r "Flow.Launcher.Plugin.Calculator"
@ -90,7 +90,7 @@ jobs:
- name: Build Explorer
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
7z a -tzip "Flow.Launcher.Plugin.Explorer.zip" "./Flow.Launcher.Plugin.Explorer/*"
rm -r "Flow.Launcher.Plugin.Explorer"
@ -114,7 +114,7 @@ jobs:
- name: Build PluginIndicator
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
7z a -tzip "Flow.Launcher.Plugin.PluginIndicator.zip" "./Flow.Launcher.Plugin.PluginIndicator/*"
rm -r "Flow.Launcher.Plugin.PluginIndicator"
@ -138,7 +138,7 @@ jobs:
- name: Build PluginsManager
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
7z a -tzip "Flow.Launcher.Plugin.PluginsManager.zip" "./Flow.Launcher.Plugin.PluginsManager/*"
rm -r "Flow.Launcher.Plugin.PluginsManager"
@ -162,7 +162,7 @@ jobs:
- name: Build ProcessKiller
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
7z a -tzip "Flow.Launcher.Plugin.ProcessKiller.zip" "./Flow.Launcher.Plugin.ProcessKiller/*"
rm -r "Flow.Launcher.Plugin.ProcessKiller"
@ -186,7 +186,7 @@ jobs:
- name: Build Program
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net7.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net9.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
7z a -tzip "Flow.Launcher.Plugin.Program.zip" "./Flow.Launcher.Plugin.Program/*"
rm -r "Flow.Launcher.Plugin.Program"
@ -210,7 +210,7 @@ jobs:
- name: Build Shell
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
7z a -tzip "Flow.Launcher.Plugin.Shell.zip" "./Flow.Launcher.Plugin.Shell/*"
rm -r "Flow.Launcher.Plugin.Shell"
@ -234,7 +234,7 @@ jobs:
- name: Build Sys
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
7z a -tzip "Flow.Launcher.Plugin.Sys.zip" "./Flow.Launcher.Plugin.Sys/*"
rm -r "Flow.Launcher.Plugin.Sys"
@ -258,7 +258,7 @@ jobs:
- name: Build Url
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
7z a -tzip "Flow.Launcher.Plugin.Url.zip" "./Flow.Launcher.Plugin.Url/*"
rm -r "Flow.Launcher.Plugin.Url"
@ -282,7 +282,7 @@ jobs:
- name: Build WebSearch
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
7z a -tzip "Flow.Launcher.Plugin.WebSearch.zip" "./Flow.Launcher.Plugin.WebSearch/*"
rm -r "Flow.Launcher.Plugin.WebSearch"
@ -306,7 +306,7 @@ jobs:
- name: Build WindowsSettings
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
7z a -tzip "Flow.Launcher.Plugin.WindowsSettings.zip" "./Flow.Launcher.Plugin.WindowsSettings/*"
rm -r "Flow.Launcher.Plugin.WindowsSettings"

View file

@ -16,7 +16,7 @@ jobs:
runs-on: windows-latest
env:
FlowVersion: 1.19.5
FlowVersion: 1.20.2
NUGET_CERT_REVOCATION_MODE: offline
BUILD_NUMBER: ${{ github.run_number }}
steps:
@ -31,7 +31,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
dotnet-version: 9.0.x
# cache: true
# cache-dependency-path: |
# Flow.Launcher/packages.lock.json

View file

@ -72,7 +72,7 @@ jobs:
steps:
- name: check-spelling
id: spelling
uses: check-spelling/check-spelling@prerelease
uses: check-spelling/check-spelling@v0.0.25
with:
suppress_push_for_open_pull_request: 1
checkout: true
@ -128,7 +128,7 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
uses: check-spelling/check-spelling@prerelease
uses: check-spelling/check-spelling@v0.0.25
with:
checkout: true
spell_check_this: check-spelling/spell-check-this@main

View file

@ -45,8 +45,7 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.PortableDataPath);
API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " +
"after the restart your portable data profile will be deleted and roaming data profile kept");
API.ShowMsgBox(API.GetTranslation("restartToDisablePortableMode"));
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
@ -69,8 +68,7 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.RoamingDataPath);
API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " +
"after the restart your roaming data profile will be deleted and portable data profile kept");
API.ShowMsgBox(API.GetTranslation("restartToEnablePortableMode"));
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
@ -154,9 +152,8 @@ namespace Flow.Launcher.Core.Configuration
{
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " +
"would you like to move it to a different location?", string.Empty,
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
if (API.ShowMsgBox(API.GetTranslation("moveToDifferentLocation"),
string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
@ -169,8 +166,7 @@ namespace Flow.Launcher.Core.Configuration
{
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " +
"the relevant shortcuts and uninstaller entry have been created");
API.ShowMsgBox(API.GetTranslation("shortcutsUninstallerCreated"));
}
}
@ -181,9 +177,8 @@ namespace Flow.Launcher.Core.Configuration
if (roamingLocationExists && portableLocationExists)
{
API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " +
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
API.ShowMsgBox(string.Format(API.GetTranslation("userDataDuplicated"),
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
return false;
}

View file

@ -22,6 +22,10 @@ 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)
@ -46,7 +50,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
}
catch (Exception e)
{
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(ClassName, "Http request failed", e);
API.LogException(ClassName, "Http request failed", e);
}
finally
{

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<OutputType>Library</OutputType>
@ -12,6 +12,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -57,6 +58,7 @@
<PackageReference Include="FSharp.Core" Version="9.0.201" />
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
<PackageReference Include="StreamJsonRpc" Version="2.21.10" />
</ItemGroup>

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
@ -277,6 +278,122 @@ public static class PluginInstaller
}
}
/// <summary>
/// Updates the plugin to the latest version available from its source.
/// </summary>
/// <param name="updateAllPlugins">Action to execute when the user chooses to update all plugins.</param>
/// <param name="silentUpdate">If true, do not show any messages when there is no update available.</param>
/// <param name="usePrimaryUrlOnly">If true, only use the primary URL for updates.</param>
/// <param name="token">Cancellation token to cancel the update operation.</param>
/// <returns></returns>
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);
// Get all plugins that can be updated
var resultsForUpdate = (
from existingPlugin in API.GetAllPlugins()
join pluginUpdateSource in API.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)
select
new PluginUpdateInfo()
{
ID = existingPlugin.Metadata.ID,
Name = existingPlugin.Metadata.Name,
Author = existingPlugin.Metadata.Author,
CurrentVersion = existingPlugin.Metadata.Version,
NewVersion = pluginUpdateSource.Version,
IcoPath = existingPlugin.Metadata.IcoPath,
PluginExistingMetadata = existingPlugin.Metadata,
PluginNewUserPlugin = pluginUpdateSource
}).ToList();
// No updates
if (!resultsForUpdate.Any())
{
if (!silentUpdate)
{
API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle"));
}
return;
}
// If all plugins are modified, just return
if (resultsForUpdate.All(x => API.PluginModified(x.ID)))
{
return;
}
// Show message box with button to update all plugins
API.ShowMsgWithButton(
API.GetTranslation("updateAllPluginsTitle"),
API.GetTranslation("updateAllPluginsButtonContent"),
() =>
{
updateAllPlugins(resultsForUpdate);
},
string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name)));
}
/// <summary>
/// Updates all plugins that have available updates.
/// </summary>
/// <param name="resultsForUpdate"></param>
/// <param name="restart"></param>
public static async Task UpdateAllPluginsAsync(IEnumerable<PluginUpdateInfo> resultsForUpdate, bool restart)
{
var anyPluginSuccess = false;
await Task.WhenAll(resultsForUpdate.Select(async plugin =>
{
var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip");
try
{
using var cts = new CancellationTokenSource();
await DownloadFileAsync(
$"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}",
plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
{
return;
}
anyPluginSuccess = true;
}
catch (Exception e)
{
API.LogException(ClassName, "Failed to update plugin", e);
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
}
}));
if (!anyPluginSuccess) return;
if (restart)
{
API.RestartApp();
}
else
{
API.ShowMsg(
API.GetTranslation("updatebtn"),
API.GetTranslation("PluginsUpdateSuccessNoRestart"));
}
}
/// <summary>
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
/// </summary>
@ -351,3 +468,15 @@ public static class PluginInstaller
);
}
}
public record PluginUpdateInfo
{
public string ID { get; init; }
public string Name { get; init; }
public string Author { get; init; }
public string CurrentVersion { get; init; }
public string NewVersion { get; init; }
public string IcoPath { get; init; }
public PluginMetadata PluginExistingMetadata { get; init; }
public UserPlugin PluginNewUserPlugin { get; init; }
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@ -9,6 +9,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -40,10 +41,13 @@ namespace Flow.Launcher.Core.Plugin
private static IEnumerable<PluginPair> _resultUpdatePlugin;
private static IEnumerable<PluginPair> _translationPlugins;
private static readonly List<DialogJumpExplorerPair> _dialogJumpExplorerPlugins = new();
private static readonly List<DialogJumpDialogPair> _dialogJumpDialogPlugins = new();
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
private static readonly string[] Directories =
public static readonly string[] Directories =
{
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
};
@ -186,6 +190,24 @@ namespace Flow.Launcher.Core.Plugin
_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)
@ -288,20 +310,24 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static ICollection<PluginPair> ValidPluginsForQuery(Query query)
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))
{
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
if (dialogJump)
return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList();
else
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
if (API.PluginModified(plugin.Metadata.ID))
{
if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
return Array.Empty<PluginPair>();
if (API.PluginModified(plugin.Metadata.ID))
return Array.Empty<PluginPair>();
}
return new List<PluginPair>
{
@ -388,6 +414,36 @@ namespace Flow.Launcher.Core.Plugin
return results;
}
public static async Task<List<DialogJumpResult>> QueryDialogJumpForPluginAsync(PluginPair pair, Query query, CancellationToken token)
{
var results = new List<DialogJumpResult>();
var metadata = pair.Metadata;
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
if (results == null)
return null;
UpdatePluginMetadata(results, metadata, query);
token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException)
{
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null;
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e);
return null;
}
return results;
}
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
{
foreach (var r in results)
@ -463,6 +519,16 @@ namespace Flow.Launcher.Core.Plugin
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
}
public static IList<DialogJumpExplorerPair> GetDialogJumpExplorers()
{
return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static IList<DialogJumpDialogPair> GetDialogJumpDialogs()
{
return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static bool ActionKeywordRegistered(string actionKeyword)
{
// this method is only checking for action keywords (defined as not '*') registration
@ -719,7 +785,7 @@ namespace Flow.Launcher.Core.Plugin
catch (Exception e)
{
API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
API.ShowMsgError(API.GetTranslation("failedToRemovePluginSettingsTitle"),
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
}
}
@ -735,7 +801,7 @@ namespace Flow.Launcher.Core.Plugin
catch (Exception e)
{
API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"),
API.ShowMsgError(API.GetTranslation("failedToRemovePluginCacheTitle"),
string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name));
}
Settings.RemovePluginSettings(plugin.ID);

View file

@ -120,15 +120,15 @@ namespace Flow.Launcher.Core.Plugin
{
var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
var errorMessage = "The following "
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
+ "errored and cannot be loaded:";
var errorMessage = erroredPlugins.Count > 1 ?
API.GetTranslation("pluginsHaveErrored") :
API.GetTranslation("pluginHasErrored");
_ = Task.Run(() =>
{
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
API.ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
$"Please refer to the logs for more information", "",
API.GetTranslation("referToLogs"), string.Empty,
MessageBoxButton.OK, MessageBoxImage.Warning);
});
}

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@ -28,22 +27,20 @@ namespace Flow.Launcher.Core.Resource
private const string DefaultFile = "en.xaml";
private const string Extension = ".xaml";
private readonly Settings _settings;
private readonly List<string> _languageDirectories = new();
private readonly List<ResourceDictionary> _oldResources = new();
private readonly List<string> _languageDirectories = [];
private readonly List<ResourceDictionary> _oldResources = [];
private static string SystemLanguageCode;
public Internationalization(Settings settings)
{
_settings = settings;
AddFlowLauncherLanguageDirectory();
}
private void AddFlowLauncherLanguageDirectory()
{
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
_languageDirectories.Add(directory);
}
#region Initialization
/// <summary>
/// Initialize the system language code based on the current culture.
/// </summary>
public static void InitSystemLanguageCode()
{
var availableLanguages = AvailableLanguages.GetAvailableLanguages();
@ -72,35 +69,6 @@ namespace Flow.Launcher.Core.Resource
SystemLanguageCode = DefaultLanguageCode;
}
private void AddPluginLanguageDirectories()
{
foreach (var plugin in PluginManager.GetTranslationPlugins())
{
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
var dir = Path.GetDirectoryName(location);
if (dir != null)
{
var pluginThemeDirectory = Path.Combine(dir, Folder);
_languageDirectories.Add(pluginThemeDirectory);
}
else
{
API.LogError(ClassName, $"Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
}
}
LoadDefaultLanguage();
}
private void LoadDefaultLanguage()
{
// Removes language files loaded before any plugins were loaded.
// Prevents the language Flow started in from overwriting English if the user switches back to English
RemoveOldLanguageFiles();
LoadLanguage(AvailableLanguages.English);
_oldResources.Clear();
}
/// <summary>
/// Initialize language. Will change app language and plugin language based on settings.
/// </summary>
@ -116,13 +84,64 @@ namespace Flow.Launcher.Core.Resource
// Get language by language code and change language
var language = GetLanguageByLanguageCode(languageCode);
// Add Flow Launcher language directory
AddFlowLauncherLanguageDirectory();
// Add plugin language directories first so that we can load language files from plugins
AddPluginLanguageDirectories();
// Load default language resources
LoadDefaultLanguage();
// Change language
await ChangeLanguageAsync(language);
await ChangeLanguageAsync(language, false);
}
private void AddFlowLauncherLanguageDirectory()
{
// Check if Flow Launcher language directory exists
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
if (!Directory.Exists(directory))
{
API.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>");
return;
}
_languageDirectories.Add(directory);
}
private void AddPluginLanguageDirectories()
{
foreach (var pluginsDir in PluginManager.Directories)
{
if (!Directory.Exists(pluginsDir)) continue;
// Enumerate all top directories in the plugin directory
foreach (var dir in Directory.GetDirectories(pluginsDir))
{
// Check if the directory contains a language folder
var pluginLanguageDir = Path.Combine(dir, Folder);
if (!Directory.Exists(pluginLanguageDir)) continue;
// Check if the language directory contains default language file since it will be checked later
_languageDirectories.Add(pluginLanguageDir);
}
}
}
private void LoadDefaultLanguage()
{
// Removes language files loaded before any plugins were loaded.
// Prevents the language Flow started in from overwriting English if the user switches back to English
RemoveOldLanguageFiles();
LoadLanguage(AvailableLanguages.English);
_oldResources.Clear();
}
#endregion
#region Change Language
/// <summary>
/// Change language during runtime. Will change app language and plugin language & save settings.
/// </summary>
@ -151,8 +170,8 @@ namespace Flow.Launcher.Core.Resource
private static Language GetLanguageByLanguageCode(string languageCode)
{
var lowercase = languageCode.ToLower();
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
var language = AvailableLanguages.GetAvailableLanguages().
FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase));
if (language == null)
{
API.LogError(ClassName, $"Language code can't be found <{languageCode}>");
@ -164,7 +183,7 @@ namespace Flow.Launcher.Core.Resource
}
}
private async Task ChangeLanguageAsync(Language language)
private async Task ChangeLanguageAsync(Language language, bool updateMetadata = true)
{
// Remove old language files and load language
RemoveOldLanguageFiles();
@ -176,8 +195,11 @@ namespace Flow.Launcher.Core.Resource
// Change culture info
ChangeCultureInfo(language.LanguageCode);
// Raise event for plugins after culture is set
await Task.Run(UpdatePluginMetadataTranslations);
if (updateMetadata)
{
// Raise event for plugins after culture is set
await Task.Run(UpdatePluginMetadataTranslations);
}
}
public static void ChangeCultureInfo(string languageCode)
@ -200,6 +222,10 @@ namespace Flow.Launcher.Core.Resource
thread.CurrentUICulture = currentCulture;
}
#endregion
#region Prompt Pinyin
public bool PromptShouldUsePinyin(string languageCodeToSet)
{
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
@ -212,14 +238,18 @@ namespace Flow.Launcher.Core.Resource
// No other languages should show the following text so just make it hard-coded
// "Do you want to search with pinyin?"
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?";
if (Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
if (API.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
return false;
return true;
}
#endregion
#region Language Resources Management
private void RemoveOldLanguageFiles()
{
var dicts = Application.Current.Resources.MergedDictionaries;
@ -255,46 +285,6 @@ namespace Flow.Launcher.Core.Resource
}
}
public List<Language> LoadAvailableLanguages()
{
var list = AvailableLanguages.GetAvailableLanguages();
list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
return list;
}
public static string GetTranslation(string key)
{
var translation = Application.Current.TryFindResource(key);
if (translation is string)
{
return translation.ToString();
}
else
{
API.LogError(ClassName, $"No Translation for key {key}");
return $"No Translation for key {key}";
}
}
private void UpdatePluginMetadataTranslations()
{
// Update plugin metadata name & description
foreach (var p in PluginManager.GetTranslationPlugins())
{
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)
{
API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
}
private static string LanguageFile(string folder, string language)
{
if (Directory.Exists(folder))
@ -324,5 +314,59 @@ namespace Flow.Launcher.Core.Resource
return string.Empty;
}
}
#endregion
#region Available Languages
public List<Language> LoadAvailableLanguages()
{
var list = AvailableLanguages.GetAvailableLanguages();
list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
return list;
}
#endregion
#region Get Translations
public static string GetTranslation(string key)
{
var translation = Application.Current.TryFindResource(key);
if (translation is string)
{
return translation.ToString();
}
else
{
API.LogError(ClassName, $"No Translation for key {key}");
return $"No Translation for key {key}";
}
}
#endregion
#region Update Metadata
public static void UpdatePluginMetadataTranslations()
{
// Update plugin metadata name & description
foreach (var p in PluginManager.GetTranslationPlugins())
{
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)
{
API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
}
#endregion
}
}

View file

@ -49,8 +49,9 @@ namespace Flow.Launcher.Core
// UpdateApp CheckForUpdate will return value only if the app is squirrel installed
var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false);
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = Version.Parse(Constant.Version);
var newReleaseVersion =
SemanticVersioning.Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = SemanticVersioning.Version.Parse(Constant.Version);
_api.LogInfo(ClassName, $"Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
@ -71,10 +72,13 @@ namespace Flow.Launcher.Core
if (DataLocation.PortableDataLocationInUse())
{
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
var targetDestination = updateManager.RootAppDirectory +
$"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
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"),
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));
}
@ -87,24 +91,27 @@ namespace Flow.Launcher.Core
_api.LogInfo(ClassName, $"Update success:{newVersionTips}");
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
}
catch (Exception e)
{
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
if (e is HttpRequestException or WebException or SocketException ||
e.InnerException is TimeoutException)
{
_api.LogException(ClassName, $"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
_api.LogException(ClassName,
$"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
}
else
{
_api.LogException(ClassName, $"Error Occurred", e);
}
if (!silentUpdate)
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
_api.ShowMsgError(_api.GetTranslation("update_flowlauncher_fail"),
_api.GetTranslation("update_flowlauncher_check_connection"));
}
finally
@ -116,14 +123,11 @@ namespace Flow.Launcher.Core
[UsedImplicitly]
private class GithubRelease
{
[JsonPropertyName("prerelease")]
public bool Prerelease { get; [UsedImplicitly] set; }
[JsonPropertyName("prerelease")] public bool Prerelease { get; [UsedImplicitly] set; }
[JsonPropertyName("published_at")]
public DateTime PublishedAt { get; [UsedImplicitly] set; }
[JsonPropertyName("published_at")] public DateTime PublishedAt { get; [UsedImplicitly] set; }
[JsonPropertyName("html_url")]
public string HtmlUrl { get; [UsedImplicitly] set; }
[JsonPropertyName("html_url")] public string HtmlUrl { get; [UsedImplicitly] set; }
}
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
@ -138,10 +142,7 @@ namespace Flow.Launcher.Core
var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First();
var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/");
var client = new WebClient
{
Proxy = Http.WebProxy
};
var client = new WebClient { Proxy = Http.WebProxy };
var downloader = new FileDownloader(client);
var manager = new UpdateManager(latestUrl, urlDownloader: downloader);
@ -158,10 +159,7 @@ namespace Flow.Launcher.Core
private static string Formatted<T>(T t)
{
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
{
WriteIndented = true
});
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions { WriteIndented = true });
return formatted;
}

View file

@ -0,0 +1,238 @@
{
"version": 1,
"dependencies": {
"net9.0-windows7.0": {
"Droplex": {
"type": "Direct",
"requested": "[1.7.0, )",
"resolved": "1.7.0",
"contentHash": "wutfIus/Ufw/9TDsp86R1ycnIH+wWrj4UhcmrzAHWjsdyC2iM07WEQ9+APTB7pQynsDnYH1r2i58XgAJ3lxUXA==",
"dependencies": {
"YamlDotNet": "9.1.0"
}
},
"FSharp.Core": {
"type": "Direct",
"requested": "[9.0.101, )",
"resolved": "9.0.101",
"contentHash": "3/YR1SDWFA+Ojx9HiBwND+0UR8ZWoeZfkhD0DWAPCDdr/YI+CyFkArmMGzGSyPXeYtjG0sy0emzfyNwjt7zhig=="
},
"Meziantou.Framework.Win32.Jobs": {
"type": "Direct",
"requested": "[3.4.0, )",
"resolved": "3.4.0",
"contentHash": "5GGLckfpwoC1jznInEYfK2INrHyD7K1RtwZJ98kNPKBU6jeu24i4zfgDGHHfb+eK3J+eFPAxo0aYcbUxNXIbNw=="
},
"Microsoft.IO.RecyclableMemoryStream": {
"type": "Direct",
"requested": "[3.0.1, )",
"resolved": "3.0.1",
"contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g=="
},
"squirrel.windows": {
"type": "Direct",
"requested": "[1.5.2, )",
"resolved": "1.5.2",
"contentHash": "89Y/CFxWm7SEOjvuV2stVa8p+SNM9GOLk4tUNm2nUF792nfkimAgwRA/umVsdyd/OXBH8byXSh4V1qck88ZAyQ==",
"dependencies": {
"DeltaCompressionDotNet": "[1.0.0, 2.0.0)",
"Mono.Cecil": "0.9.6.1",
"Splat": "1.6.2"
}
},
"StreamJsonRpc": {
"type": "Direct",
"requested": "[2.20.20, )",
"resolved": "2.20.20",
"contentHash": "gwG7KViLbSWS7EI0kYevinVmIga9wZNrpSY/FnWyC6DbdjKJ1xlv/FV1L9b0rLkVP8cGxfIMexdvo/+2W5eq6Q==",
"dependencies": {
"MessagePack": "2.5.187",
"Microsoft.VisualStudio.Threading": "17.10.48",
"Microsoft.VisualStudio.Threading.Analyzers": "17.10.48",
"Microsoft.VisualStudio.Validation": "17.8.8",
"Nerdbank.Streams": "2.11.74",
"Newtonsoft.Json": "13.0.1",
"System.IO.Pipelines": "8.0.0"
}
},
"Ben.Demystifier": {
"type": "Transitive",
"resolved": "0.4.1",
"contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==",
"dependencies": {
"System.Reflection.Metadata": "5.0.0"
}
},
"BitFaster.Caching": {
"type": "Transitive",
"resolved": "2.5.3",
"contentHash": "Vo/39qcam5Xe+DbyfH0JZyqPswdOoa7jv4PGtRJ6Wj8AU+aZ+TuJRlJcIe+MQjRTJwliI8k8VSQpN8sEoBIv2g=="
},
"CommunityToolkit.Mvvm": {
"type": "Transitive",
"resolved": "8.4.0",
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
},
"DeltaCompressionDotNet": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "nwbZAYd+DblXAIzlnwDSnl0CiCm8jWLfHSYnoN4wYhtIav6AegB3+T/vKzLbU2IZlPB8Bvl8U3NXpx3eaz+N5w=="
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2024.3.0",
"contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug=="
},
"MemoryPack": {
"type": "Transitive",
"resolved": "1.21.3",
"contentHash": "cwCtED8y400vMWx/Vp0QCSeEpVFjDU4JwF52VX9WTaqVERUvNqjG9n6osFlmFuytegyXnHvYEu1qRJ8rv/rkbg==",
"dependencies": {
"MemoryPack.Core": "1.21.3",
"MemoryPack.Generator": "1.21.3"
}
},
"MemoryPack.Core": {
"type": "Transitive",
"resolved": "1.21.3",
"contentHash": "ajrYoBWT2aKeH4tlY8q/1C9qK1R/NK+7FkuVOX58ebOSxkABoFTqCR7W+Zk2rakUHZiEgNdRqO67hiRZPq6fLA=="
},
"MemoryPack.Generator": {
"type": "Transitive",
"resolved": "1.21.3",
"contentHash": "hYU0TAIarDKnbkNIWvb7P4zBUL+CTahkuNkczsKvycSMR5kiwQ4IfLexywNKX3s05Izp4gzDSPbueepNWZRpWA=="
},
"MessagePack": {
"type": "Transitive",
"resolved": "2.5.187",
"contentHash": "uW4j8m4Nc+2Mk5n6arOChavJ9bLjkis0qWASOj2h2OwmfINuzYv+mjCHUymrYhmyyKTu3N+ObtTXAY4uQ7jIhg==",
"dependencies": {
"MessagePack.Annotations": "2.5.187",
"Microsoft.NET.StringTools": "17.6.3"
}
},
"MessagePack.Annotations": {
"type": "Transitive",
"resolved": "2.5.187",
"contentHash": "/IvvMMS8opvlHjEJ/fR2Cal4Co726Kj77Z8KiohFhuHfLHHmb9uUxW5+tSCL4ToKFfkQlrS3HD638mRq83ySqA=="
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.6.3",
"contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA=="
},
"Microsoft.VisualStudio.Threading": {
"type": "Transitive",
"resolved": "17.12.19",
"contentHash": "eLiGMkMYyaSguqHs3lsrFxy3tAWSLuPEL2pIWRcADMDVAs2xqm3dr1d9QYjiEusTgiClF9KD6OB2NdZP72Oy0Q==",
"dependencies": {
"Microsoft.VisualStudio.Threading.Analyzers": "17.12.19",
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Transitive",
"resolved": "17.12.19",
"contentHash": "v3IYeedjoktvZ+GqYmLudxZJngmf/YWIxNT2Uy6QMMN19cvw+nkWoip1Gr1RtnFkUo1MPUVMis4C8Kj8d8DpSQ=="
},
"Microsoft.VisualStudio.Validation": {
"type": "Transitive",
"resolved": "17.8.8",
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
},
"Microsoft.Win32.SystemEvents": {
"type": "Transitive",
"resolved": "9.0.2",
"contentHash": "5BkGZ6mHp2dHydR29sb0fDfAuqkv30AHtTih8wMzvPZysOmBFvHfnkR2w3tsc0pSiIg8ZoKyefJXWy9r3pBh0w=="
},
"Mono.Cecil": {
"type": "Transitive",
"resolved": "0.9.6.1",
"contentHash": "yMsurNaOxxKIjyW9pEB+tRrR1S3DFnN1+iBgKvYvXG8kW0Y6yknJeMAe/tl3+P78/2C6304TgF7aVqpqXgEQ9Q=="
},
"Nerdbank.Streams": {
"type": "Transitive",
"resolved": "2.11.74",
"contentHash": "r4G7uHHfoo8LCilPOdtf2C+Q5ymHOAXtciT4ZtB2xRlAvv4gPkWBYNAijFblStv3+uidp81j5DP11jMZl4BfJw==",
"dependencies": {
"Microsoft.VisualStudio.Threading": "17.10.48",
"Microsoft.VisualStudio.Validation": "17.8.8",
"System.IO.Pipelines": "8.0.0"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.1",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
},
"NLog": {
"type": "Transitive",
"resolved": "4.7.10",
"contentHash": "rcegW7kYOCjl7wX0SzsqpPBqnJ51JKi1WkYb6QBVX0Wc5IgH19Pv4t/co+T0s06OS0Ne44xgkY/mHg0PdrmJow=="
},
"PropertyChanged.Fody": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "IAZyq0uolKo2WYm4mjx+q7A8fSGFT0x2e1s3y+ODn4JI0kqTDoo9GF2tdaypUzRFJZfdMxfC5HZW9QzdJLtOnA==",
"dependencies": {
"Fody": "6.5.1"
}
},
"Splat": {
"type": "Transitive",
"resolved": "1.6.2",
"contentHash": "DeH0MxPU+D4JchkIDPYG4vUT+hsWs9S41cFle0/4K5EJMXWurx5DzAkj2366DfK14/XKNhsu6tCl4dZXJ3CD4w=="
},
"System.Drawing.Common": {
"type": "Transitive",
"resolved": "9.0.2",
"contentHash": "JU947wzf8JbBS16Y5EIZzAlyQU+k68D7LRx6y03s2wlhlvLqkt/8uPBrjv2hJnnaJKbdb0GhQ3JZsfYXhrRjyg==",
"dependencies": {
"Microsoft.Win32.SystemEvents": "9.0.2"
}
},
"System.IO.Pipelines": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ=="
},
"ToolGood.Words.Pinyin": {
"type": "Transitive",
"resolved": "3.0.1.4",
"contentHash": "uQo97618y9yzLDxrnehPN+/tuiOlk5BqieEdwctHZOAS9miMXnHKgMFYVw8CSGXRglyTYXlrW7qtUlU7Fje5Ew=="
},
"YamlDotNet": {
"type": "Transitive",
"resolved": "9.1.0",
"contentHash": "fuvGXU4Ec5HrsmEc+BiFTNPCRf1cGBI2kh/3RzMWgddM2M4ALhbSPoI3X3mhXZUD1qqQd9oSkFAtWjpz8z9eRg=="
},
"flow.launcher.infrastructure": {
"type": "Project",
"dependencies": {
"Ben.Demystifier": "[0.4.1, )",
"BitFaster.Caching": "[2.5.3, )",
"CommunityToolkit.Mvvm": "[8.4.0, )",
"Flow.Launcher.Plugin": "[4.4.0, )",
"MemoryPack": "[1.21.3, )",
"Microsoft.VisualStudio.Threading": "[17.12.19, )",
"NLog": "[4.7.10, )",
"PropertyChanged.Fody": "[3.4.0, )",
"System.Drawing.Common": "[9.0.2, )",
"ToolGood.Words.Pinyin": "[3.0.1.4, )"
}
},
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"JetBrains.Annotations": "[2024.3.0, )",
"PropertyChanged.Fody": "[3.4.0, )"
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.DialogJump;
public class DialogJumpExplorerPair
{
public IDialogJumpExplorer Plugin { get; init; }
public PluginMetadata Metadata { get; init; }
public override string ToString()
{
return Metadata.Name;
}
public override bool Equals(object obj)
{
if (obj is DialogJumpExplorerPair r)
{
return string.Equals(r.Metadata.ID, Metadata.ID);
}
else
{
return false;
}
}
public override int GetHashCode()
{
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
return hashcode;
}
}
public class DialogJumpDialogPair
{
public IDialogJumpDialog Plugin { get; init; }
public PluginMetadata Metadata { get; init; }
public override string ToString()
{
return Metadata.Name;
}
public override bool Equals(object obj)
{
if (obj is DialogJumpDialogPair r)
{
return string.Equals(r.Metadata.ID, Metadata.ID);
}
else
{
return false;
}
}
public override int GetHashCode()
{
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
return hashcode;
}
}

View file

@ -0,0 +1,345 @@
using System;
using System.Threading;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using WindowsInput;
using WindowsInput.Native;
namespace Flow.Launcher.Infrastructure.DialogJump.Models
{
/// <summary>
/// Class for handling Windows File Dialog instances in DialogJump.
/// </summary>
public class WindowsDialog : IDialogJumpDialog
{
private const string WindowsDialogClassName = "#32770";
public IDialogJumpDialogWindow CheckDialogWindow(IntPtr hwnd)
{
// Is it a Win32 dialog box?
if (GetClassName(new(hwnd)) == WindowsDialogClassName)
{
// Is it a windows file dialog?
var dialogType = GetFileDialogType(new(hwnd));
if (dialogType != DialogType.Others)
{
return new WindowsDialogWindow(hwnd, dialogType);
}
}
return null;
}
public void Dispose()
{
}
#region Help Methods
private static unsafe string GetClassName(HWND handle)
{
fixed (char* buf = new char[256])
{
return PInvoke.GetClassName(handle, buf, 256) switch
{
0 => string.Empty,
_ => new string(buf),
};
}
}
private static DialogType GetFileDialogType(HWND handle)
{
// Is it a Windows Open file dialog?
var fileEditor = PInvoke.GetDlgItem(handle, 0x047C);
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open;
// Is it a Windows Save or Save As file dialog?
fileEditor = PInvoke.GetDlgItem(handle, 0x0000);
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs;
return DialogType.Others;
}
#endregion
}
public class WindowsDialogWindow : IDialogJumpDialogWindow
{
public IntPtr Handle { get; private set; } = IntPtr.Zero;
// After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore
// So we need to cache the current tab and use the original handle
private IDialogJumpDialogWindowTab _currentTab { get; set; } = null;
private readonly DialogType _dialogType;
internal WindowsDialogWindow(IntPtr handle, DialogType dialogType)
{
Handle = handle;
_dialogType = dialogType;
}
public IDialogJumpDialogWindowTab GetCurrentTab()
{
return _currentTab ??= new WindowsDialogTab(Handle, _dialogType);
}
public void Dispose()
{
}
}
public class WindowsDialogTab : IDialogJumpDialogWindowTab
{
#region Public Properties
public IntPtr Handle { get; private set; } = IntPtr.Zero;
#endregion
#region Private Fields
private static readonly string ClassName = nameof(WindowsDialogTab);
private static readonly InputSimulator _inputSimulator = new();
private readonly DialogType _dialogType;
private bool _legacy { get; set; } = false;
private HWND _pathControl { get; set; } = HWND.Null;
private HWND _pathEditor { get; set; } = HWND.Null;
private HWND _fileEditor { get; set; } = HWND.Null;
private HWND _openButton { get; set; } = HWND.Null;
#endregion
#region Constructor
internal WindowsDialogTab(IntPtr handle, DialogType dialogType)
{
Handle = handle;
_dialogType = dialogType;
Log.Debug(ClassName, $"File dialog type: {dialogType}");
}
#endregion
#region Public Methods
public string GetCurrentFolder()
{
if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty;
return GetWindowText(_pathEditor);
}
public string GetCurrentFile()
{
if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty;
return GetWindowText(_fileEditor);
}
public bool JumpFolder(string path, bool auto)
{
if (auto)
{
// Use legacy jump folder method for auto Dialog Jump because file editor is default value.
// After setting path using file editor, we do not need to revert its value.
return JumpFolderWithFileEditor(path, false);
}
// Alt-D or Ctrl-L to focus on the path input box
// "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus
_inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D);
// _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L);
if (_pathControl.IsNull && !GetPathControlEditor())
{
// https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1
// The dialog is a legacy one, so we can only edit file editor directly.
Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
var timeOut = !SpinWait.SpinUntil(() =>
{
var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0;
}, 1000);
if (timeOut)
{
// Path control is not visible, so we can only edit file editor directly.
Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
if (_pathEditor.IsNull)
{
// Path editor cannot be found, so we can only edit file editor directly.
Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
SetWindowText(_pathEditor, path);
_inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN);
return true;
}
public bool JumpFile(string path)
{
if (_fileEditor.IsNull && !GetFileEditor()) return false;
SetWindowText(_fileEditor, path);
return true;
}
public bool Open()
{
if (_openButton.IsNull && !GetOpenButton()) return false;
PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
return true;
}
public void Dispose()
{
}
#endregion
#region Helper Methods
#region Get Handles
private bool GetPathControlEditor()
{
// Get the handle of the path editor
// Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control
_pathControl = PInvoke.FindWindowEx(new(Handle), HWND.Null, "WorkerW", null); // 0x0000
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205
if (_pathControl == HWND.Null)
{
_pathEditor = HWND.Null;
_legacy = true;
Log.Info(ClassName, "Legacy dialog");
}
else
{
_pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox
_pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit
if (_pathEditor == HWND.Null)
{
_legacy = true;
Log.Error(ClassName, "Failed to find path editor handle");
}
}
return !_legacy;
}
private bool GetFileEditor()
{
if (_dialogType == DialogType.Open)
{
// Get the handle of the file name editor of Open file dialog
_fileEditor = PInvoke.GetDlgItem(new(Handle), 0x047C); // ComboBoxEx32
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit
}
else
{
// Get the handle of the file name editor of Save / SaveAs file dialog
_fileEditor = PInvoke.GetDlgItem(new(Handle), 0x0000); // DUIViewWndClassName
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit
}
if (_fileEditor == HWND.Null)
{
Log.Error(ClassName, "Failed to find file name editor handle");
return false;
}
return true;
}
private bool GetOpenButton()
{
// Get the handle of the open button
_openButton = PInvoke.GetDlgItem(new(Handle), 0x0001); // Open/Save/SaveAs Button
if (_openButton == HWND.Null)
{
Log.Error(ClassName, "Failed to find open button handle");
return false;
}
return true;
}
#endregion
#region Windows Text
private static unsafe string GetWindowText(HWND handle)
{
int length;
Span<char> buffer = stackalloc char[1000];
fixed (char* pBuffer = buffer)
{
// If the control has no title bar or text, or if the control handle is invalid, the return value is zero.
length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer);
}
return buffer[..length].ToString();
}
private static unsafe nint SetWindowText(HWND handle, string text)
{
fixed (char* textPtr = text + '\0')
{
return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value;
}
}
#endregion
#region Legacy Jump Folder
private bool JumpFolderWithFileEditor(string path, bool resetFocus)
{
// For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors.
if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false;
if (_fileEditor.IsNull && !GetFileEditor()) return false;
SetWindowText(_fileEditor, path);
if (_openButton.IsNull && !GetOpenButton()) return false;
PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
return true;
}
#endregion
#endregion
}
internal enum DialogType
{
Others,
Open,
SaveOrSaveAs
}
}

View file

@ -0,0 +1,260 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Flow.Launcher.Plugin;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
namespace Flow.Launcher.Infrastructure.DialogJump.Models
{
/// <summary>
/// Class for handling Windows Explorer instances in DialogJump.
/// </summary>
public class WindowsExplorer : IDialogJumpExplorer
{
public IDialogJumpExplorerWindow CheckExplorerWindow(IntPtr hwnd)
{
IDialogJumpExplorerWindow explorerWindow = null;
// Is it from Explorer?
var processName = Win32Helper.GetProcessNameFromHwnd(new(hwnd));
if (processName.Equals("explorer.exe", StringComparison.OrdinalIgnoreCase))
{
EnumerateShellWindows((shellWindow) =>
{
try
{
if (shellWindow is not IWebBrowser2 explorer) return true;
if (explorer.HWND != hwnd) return true;
explorerWindow = new WindowsExplorerWindow(hwnd);
return false;
}
catch
{
// Ignored
}
return true;
});
}
return explorerWindow;
}
internal static unsafe void EnumerateShellWindows(Func<object, bool> action)
{
// Create an instance of ShellWindows
var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass
var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows
var result = PInvoke.CoCreateInstance(
&clsidShellWindows,
null,
CLSCTX.CLSCTX_ALL,
&iidIShellWindows,
out var shellWindowsObj);
if (result.Failed) return;
var shellWindows = (IShellWindows)shellWindowsObj;
// Enumerate the shell windows
var count = shellWindows.Count;
for (var i = 0; i < count; i++)
{
if (!action(shellWindows.Item(i)))
{
return;
}
}
}
public void Dispose()
{
}
}
public class WindowsExplorerWindow : IDialogJumpExplorerWindow
{
public IntPtr Handle { get; }
private static Guid _shellBrowserGuid = typeof(IShellBrowser).GUID;
internal WindowsExplorerWindow(IntPtr handle)
{
Handle = handle;
}
public string GetExplorerPath()
{
if (Handle == IntPtr.Zero) return null;
var activeTabHandle = GetActiveTabHandle(new(Handle));
if (activeTabHandle.IsNull) return null;
var window = GetExplorerByTabHandle(activeTabHandle);
if (window == null) return null;
var path = GetLocation(window);
return path;
}
public void Dispose()
{
}
#region Helper Methods
// Inspired by: https://github.com/w4po/ExplorerTabUtility
private static HWND GetActiveTabHandle(HWND windowHandle)
{
// Active tab always at the top of the z-index, so it is the first child of the ShellTabWindowClass.
var activeTab = PInvoke.FindWindowEx(windowHandle, HWND.Null, "ShellTabWindowClass", null);
return activeTab;
}
private static IWebBrowser2 GetExplorerByTabHandle(HWND tabHandle)
{
if (tabHandle.IsNull) return null;
IWebBrowser2 window = null;
WindowsExplorer.EnumerateShellWindows((shellWindow) =>
{
try
{
return StartSTAThread(() =>
{
if (shellWindow is not IWebBrowser2 explorer) return true;
if (explorer is not IServiceProvider sp) return true;
sp.QueryService(ref _shellBrowserGuid, ref _shellBrowserGuid, out var shellBrowser);
if (shellBrowser == null) return true;
try
{
shellBrowser.GetWindow(out var hWnd); // Must execute in STA thread to get this hWnd
if (hWnd == tabHandle)
{
window = explorer;
return false;
}
}
catch
{
// Ignored
}
finally
{
Marshal.ReleaseComObject(shellBrowser);
}
return true;
}) ?? true;
}
catch
{
// Ignored
}
return true;
});
return window;
}
private static bool? StartSTAThread(Func<bool> action)
{
bool? result = null;
var thread = new Thread(() =>
{
result = action();
})
{
IsBackground = true
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return result;
}
private static string GetLocation(IWebBrowser2 window)
{
var path = window.LocationURL.ToString();
if (!string.IsNullOrWhiteSpace(path)) return NormalizeLocation(path);
// Recycle Bin, This PC, etc
if (window.Document is not IShellFolderViewDual folderView) return null;
// Attempt to get the path from the folder view
try
{
// CSWin32 Folder does not have Self, so we need to use dynamic type here
// Use dynamic to bypass static typing
dynamic folder = folderView.Folder;
// Access the Self property via dynamic binding
dynamic folderItem = folder.Self;
// Get path from the folder item
path = folderItem.Path;
}
catch
{
return null;
}
return NormalizeLocation(path);
}
private static string NormalizeLocation(string location)
{
if (location.IndexOf('%') > -1)
location = Environment.ExpandEnvironmentVariables(location);
if (location.StartsWith("::", StringComparison.Ordinal))
location = $"shell:{location}";
else if (location.StartsWith("{", StringComparison.Ordinal))
location = $"shell:::{location}";
location = location.Trim(' ', '/', '\\', '\n', '\'', '"');
return location.Replace('/', '\\');
}
#endregion
}
#region COM Interfaces
// Inspired by: https://github.com/w4po/ExplorerTabUtility
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("6d5140c1-7436-11ce-8034-00aa006009fa")]
[ComImport]
public interface IServiceProvider
{
[PreserveSig]
int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellBrowser ppvObject);
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214E2-0000-0000-C000-000000000046")]
[ComImport]
public interface IShellBrowser
{
[PreserveSig]
int GetWindow(out nint handle);
}
#endregion
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
<OutputType>Library</OutputType>
<UseWpf>true</UseWpf>
@ -12,6 +12,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -59,12 +60,14 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="MemoryPack" Version="1.21.4" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.12.19" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="NLog" Version="4.7.10" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
<PrivateAssets>all</PrivateAssets>
@ -76,5 +79,4 @@
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
</ItemGroup>
</Project>

View file

@ -20,6 +20,10 @@ 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
@ -78,7 +82,7 @@ namespace Flow.Launcher.Infrastructure.Http
}
catch (UriFormatException e)
{
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsg("Please try again", "Unable to parse Http Proxy");
API.ShowMsgError(API.GetTranslation("pleaseTryAgain"), API.GetTranslation("parseProxyFailed"));
Log.Exception(ClassName, "Unable to parse Uri", e);
}
}

View file

@ -66,3 +66,27 @@ LOCALE_TRANSIENT_KEYBOARD4
SHParseDisplayName
SHOpenFolderAndSelectItems
CoTaskMemFree
SetWinEventHook
UnhookWinEvent
SendMessage
EVENT_SYSTEM_FOREGROUND
WINEVENT_OUTOFCONTEXT
WM_SETTEXT
IShellFolderViewDual2
CoCreateInstance
CLSCTX
IShellWindows
IWebBrowser2
EVENT_OBJECT_DESTROY
EVENT_OBJECT_LOCATIONCHANGE
EVENT_SYSTEM_MOVESIZESTART
EVENT_SYSTEM_MOVESIZEEND
GetDlgItem
PostMessage
BM_CLICK
WM_GETTEXT
OpenProcess
QueryFullProcessImageName
EVENT_OBJECT_HIDE
EVENT_SYSTEM_DIALOGEND

View file

@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure
{
public class PinyinAlphabet : IAlphabet
{
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
new();
private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = new();
private readonly Settings _settings;
private ReadOnlyDictionary<string, string> currentDoublePinyinTable;
public PinyinAlphabet()
@ -28,10 +25,21 @@ namespace Flow.Launcher.Infrastructure
_settings.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(Settings.UseDoublePinyin) ||
e.PropertyName == nameof(Settings.DoublePinyinSchema))
switch (e.PropertyName)
{
Reload();
case nameof (Settings.ShouldUsePinyin):
if (_settings.ShouldUsePinyin)
{
Reload();
}
break;
case nameof(Settings.UseDoublePinyin):
case nameof(Settings.DoublePinyinSchema):
if (_settings.UseDoublePinyin)
{
Reload();
}
break;
}
};
}
@ -44,105 +52,142 @@ namespace Flow.Launcher.Infrastructure
private void CreateDoublePinyinTableFromStream(Stream jsonStream)
{
Dictionary<string, Dictionary<string, string>> table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream);
string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string
if (!table.TryGetValue(schemaKey, out var value))
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();
if (!table.TryGetValue(schemaKey, out var schemaDict))
{
throw new ArgumentException("DoublePinyinSchema is invalid or double pinyin table is broken.");
throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken.");
}
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(value);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(schemaDict);
}
private void LoadDoublePinyinTable()
{
if (_settings.UseDoublePinyin)
if (!_settings.UseDoublePinyin)
{
var tablePath = Path.Join(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
try
{
using var fs = File.OpenRead(tablePath);
CreateDoublePinyinTableFromStream(fs);
}
catch (System.Exception e)
{
Log.Exception(nameof(PinyinAlphabet), "Failed to load double pinyin table from file: " + tablePath, e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
return;
}
else
var tablePath = Path.Combine(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
try
{
using var fs = File.OpenRead(tablePath);
CreateDoublePinyinTableFromStream(fs);
}
catch (FileNotFoundException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Double pinyin table file not found: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (DirectoryNotFoundException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (UnauthorizedAccessException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (System.Exception e)
{
Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
}
public bool ShouldTranslate(string stringToTranslate)
{
// If a string has Chinese characters, we don't need to translate it to pinyin.
return _settings.ShouldUsePinyin && !WordsHelper.HasChinese(stringToTranslate);
// If the query (stringToTranslate) does NOT contain Chinese characters,
// we should translate the target string to pinyin for matching
return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate);
}
public (string translation, TranslationMapping map) Translate(string content)
{
if (!_settings.ShouldUsePinyin || !WordsHelper.HasChinese(content))
if (!_settings.ShouldUsePinyin || !ContainsChinese(content))
return (content, null);
return _pinyinCache.TryGetValue(content, out var value)
? value
: BuildCacheFromContent(content);
return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content);
}
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
{
var resultList = WordsHelper.GetPinyinList(content);
var resultBuilder = new StringBuilder();
var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity
var map = new TranslationMapping();
var previousIsChinese = false;
for (var i = 0; i < resultList.Length; i++)
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
if (IsChineseCharacter(content[i]))
{
string translated = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i];
var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i];
if (i > 0)
{
resultBuilder.Append(' ');
}
map.AddNewIndex(resultBuilder.Length, translated.Length);
resultBuilder.Append(translated);
previousIsChinese = true;
}
else
{
// Add space after Chinese characters before non-Chinese characters
if (previousIsChinese)
{
previousIsChinese = false;
resultBuilder.Append(' ');
}
map.AddNewIndex(resultBuilder.Length, resultList[i].Length);
resultBuilder.Append(resultList[i]);
}
}
map.endConstruct();
map.EndConstruct();
var key = resultBuilder.ToString();
return _pinyinCache[content] = (key, map);
var translation = resultBuilder.ToString();
var result = (translation, map);
return _pinyinCache[content] = result;
}
#region Double Pinyin
private string ToDoublePin(string fullPinyin)
/// <summary>
/// Optimized Chinese character detection using the comprehensive CJK Unicode ranges
/// </summary>
private static bool ContainsChinese(ReadOnlySpan<char> text)
{
if (currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue))
foreach (var c in text)
{
return doublePinyinValue;
if (IsChineseCharacter(c))
return true;
}
return fullPinyin;
return false;
}
#endregion
/// <summary>
/// Check if a character is a Chinese character using comprehensive Unicode ranges
/// Covers CJK Unified Ideographs, Extension A
/// </summary>
private static bool IsChineseCharacter(char c)
{
return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs
(c >= 0x3400 && c <= 0x4DBF); // CJK Extension A
}
private string ToDoublePinyin(string fullPinyin)
{
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
? doublePinyinValue
: fullPinyin;
}
}
}

View file

@ -5,31 +5,30 @@ namespace Flow.Launcher.Infrastructure
{
public class TranslationMapping
{
private bool constructed;
private bool _isConstructed;
// Assuming one original item maps to multi translated items
// list[i] is the last translated index + 1 of original index i
private readonly List<int> originalToTranslated = new();
// Assuming one original item maps to multi translated items
// list[i] is the last translated index + 1 of original index i
private readonly List<int> _originalToTranslated = new();
public void AddNewIndex(int translatedIndex, int length)
{
if (constructed)
throw new InvalidOperationException("Mapping shouldn't be changed after constructed");
originalToTranslated.Add(translatedIndex + length);
if (_isConstructed)
throw new InvalidOperationException("Mapping shouldn't be changed after construction");
_originalToTranslated.Add(translatedIndex + length);
}
public int MapToOriginalIndex(int translatedIndex)
{
int loc = originalToTranslated.BinarySearch(translatedIndex);
return loc >= 0 ? loc : ~loc;
var searchResult = _originalToTranslated.BinarySearch(translatedIndex);
return searchResult >= 0 ? searchResult : ~searchResult;
}
public void endConstruct()
public void EndConstruct()
{
if (constructed)
if (_isConstructed)
throw new InvalidOperationException("Mapping has already been constructed");
constructed = true;
_isConstructed = true;
}
}
}

View file

@ -86,6 +86,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public string OpenHistoryHotkey { get; set; } = $"Ctrl+H";
public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up";
public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down";
public string DialogJumpHotkey { get; set; } = $"{KeyConstant.Alt} + G";
private string _language = Constant.SystemLanguageCode;
public string Language
@ -233,6 +234,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool AutoRestartAfterChanging { get; set; } = false;
public bool ShowUnknownSourceWarning { get; set; } = true;
public bool AutoUpdatePlugins { get; set; } = true;
public int CustomExplorerIndex { get; set; } = 0;
@ -322,13 +324,40 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
};
public bool EnableDialogJump { get; set; } = true;
public bool AutoDialogJump { get; set; } = false;
public bool ShowDialogJumpWindow { get; set; } = false;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpWindowPositions DialogJumpWindowPosition { get; set; } = DialogJumpWindowPositions.UnderDialog;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpResultBehaviours DialogJumpResultBehaviour { get; set; } = DialogJumpResultBehaviours.LeftClick;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpFileResultBehaviours DialogJumpFileResultBehaviour { get; set; } = DialogJumpFileResultBehaviours.FullPath;
[JsonConverter(typeof(JsonStringEnumConverter))]
public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO;
/// <summary>
/// when false Alphabet static service will always return empty results
/// </summary>
public bool ShouldUsePinyin { get; set; } = false;
private bool _useAlphabet = true;
public bool ShouldUsePinyin
{
get => _useAlphabet;
set
{
if (_useAlphabet != value)
{
_useAlphabet = value;
OnPropertyChanged();
}
}
}
private bool _useDoublePinyin = false;
public bool UseDoublePinyin
@ -502,7 +531,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
var list = FixedHotkeys();
// Customizeable hotkeys
// Customizable hotkeys
if (!string.IsNullOrEmpty(Hotkey))
list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = ""));
if (!string.IsNullOrEmpty(PreviewHotkey))
@ -533,6 +562,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = ""));
if (!string.IsNullOrEmpty(CycleHistoryDownHotkey))
list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = ""));
if (!string.IsNullOrEmpty(DialogJumpHotkey))
list.Add(new(DialogJumpHotkey, "dialogJumpHotkey", () => DialogJumpHotkey = ""));
// Custom Query Hotkeys
foreach (var customPluginHotkey in CustomPluginHotkeys)
@ -646,4 +677,23 @@ namespace Flow.Launcher.Infrastructure.UserSettings
DaNiu,
XiaoLang
}
public enum DialogJumpWindowPositions
{
UnderDialog,
FollowDefault
}
public enum DialogJumpResultBehaviours
{
LeftClick,
RightClick
}
public enum DialogJumpFileResultBehaviours
{
FullPath,
FullPathOpen,
Directory
}
}

View file

@ -14,9 +14,11 @@ using System.Windows.Markup;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.UserSettings;
using Microsoft.Win32;
using Microsoft.Win32.SafeHandles;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.System.Threading;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.Shell.Common;
using Windows.Win32.UI.WindowsAndMessaging;
@ -138,6 +140,11 @@ namespace Flow.Launcher.Infrastructure
return IsForegroundWindow(GetWindowHandle(window));
}
public static bool IsForegroundWindow(nint handle)
{
return IsForegroundWindow(new HWND(handle));
}
internal static bool IsForegroundWindow(HWND handle)
{
return handle.Equals(PInvoke.GetForegroundWindow());
@ -344,6 +351,16 @@ namespace Flow.Launcher.Infrastructure
return new(windowHelper.Handle);
}
internal static HWND GetMainWindowHandle()
{
// When application is exiting, the Application.Current will be null
if (Application.Current == null) return HWND.Null;
// Get the FL main window
var hwnd = GetWindowHandle(Application.Current.MainWindow, true);
return hwnd;
}
#endregion
#region STA Thread
@ -761,6 +778,65 @@ namespace Flow.Launcher.Infrastructure
#endregion
#region Window Rect
public static unsafe bool GetWindowRect(nint handle, out Rect outRect)
{
var rect = new RECT();
var result = PInvoke.GetWindowRect(new(handle), &rect);
if (!result)
{
outRect = new Rect();
return false;
}
// Convert RECT to Rect
outRect = new Rect(
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top
);
return true;
}
#endregion
#region Window Process
internal static unsafe string GetProcessNameFromHwnd(HWND hWnd)
{
return Path.GetFileName(GetProcessPathFromHwnd(hWnd));
}
internal static unsafe string GetProcessPathFromHwnd(HWND hWnd)
{
uint pid;
var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid);
if (threadId == 0) return string.Empty;
var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (process.Value != IntPtr.Zero)
{
using var safeHandle = new SafeProcessHandle(process.Value, true);
uint capacity = 2000;
Span<char> buffer = new char[capacity];
fixed (char* pBuffer = buffer)
{
if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, (PWSTR)pBuffer, ref capacity))
{
return string.Empty;
}
return buffer[..(int)capacity].ToString();
}
}
return string.Empty;
}
#endregion
#region Explorer
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems

View file

@ -0,0 +1,155 @@
{
"version": 1,
"dependencies": {
"net9.0-windows7.0": {
"Ben.Demystifier": {
"type": "Direct",
"requested": "[0.4.1, )",
"resolved": "0.4.1",
"contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==",
"dependencies": {
"System.Reflection.Metadata": "5.0.0"
}
},
"BitFaster.Caching": {
"type": "Direct",
"requested": "[2.5.3, )",
"resolved": "2.5.3",
"contentHash": "Vo/39qcam5Xe+DbyfH0JZyqPswdOoa7jv4PGtRJ6Wj8AU+aZ+TuJRlJcIe+MQjRTJwliI8k8VSQpN8sEoBIv2g=="
},
"CommunityToolkit.Mvvm": {
"type": "Direct",
"requested": "[8.4.0, )",
"resolved": "8.4.0",
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
},
"Fody": {
"type": "Direct",
"requested": "[6.5.5, )",
"resolved": "6.5.5",
"contentHash": "Krca41L/PDva1VsmDec5n52cQZxQAQp/bsHdzsNi8iLLI0lqKL94fNIkNaC8tVolUkCyWsbzvxfxJCeD2789fA=="
},
"MemoryPack": {
"type": "Direct",
"requested": "[1.21.3, )",
"resolved": "1.21.3",
"contentHash": "cwCtED8y400vMWx/Vp0QCSeEpVFjDU4JwF52VX9WTaqVERUvNqjG9n6osFlmFuytegyXnHvYEu1qRJ8rv/rkbg==",
"dependencies": {
"MemoryPack.Core": "1.21.3",
"MemoryPack.Generator": "1.21.3"
}
},
"Microsoft.VisualStudio.Threading": {
"type": "Direct",
"requested": "[17.12.19, )",
"resolved": "17.12.19",
"contentHash": "eLiGMkMYyaSguqHs3lsrFxy3tAWSLuPEL2pIWRcADMDVAs2xqm3dr1d9QYjiEusTgiClF9KD6OB2NdZP72Oy0Q==",
"dependencies": {
"Microsoft.VisualStudio.Threading.Analyzers": "17.12.19",
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Microsoft.Windows.CsWin32": {
"type": "Direct",
"requested": "[0.3.106, )",
"resolved": "0.3.106",
"contentHash": "Mx5fK7uN6fwLR4wUghs6//HonAnwPBNmC2oonyJVhCUlHS/r6SUS3NkBc3+gaQiv+0/9bqdj1oSCKQFkNI+21Q==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
"Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview",
"Microsoft.Windows.WDK.Win32Metadata": "0.11.4-experimental"
}
},
"NLog": {
"type": "Direct",
"requested": "[4.7.10, )",
"resolved": "4.7.10",
"contentHash": "rcegW7kYOCjl7wX0SzsqpPBqnJ51JKi1WkYb6QBVX0Wc5IgH19Pv4t/co+T0s06OS0Ne44xgkY/mHg0PdrmJow=="
},
"PropertyChanged.Fody": {
"type": "Direct",
"requested": "[3.4.0, )",
"resolved": "3.4.0",
"contentHash": "IAZyq0uolKo2WYm4mjx+q7A8fSGFT0x2e1s3y+ODn4JI0kqTDoo9GF2tdaypUzRFJZfdMxfC5HZW9QzdJLtOnA==",
"dependencies": {
"Fody": "6.5.1"
}
},
"System.Drawing.Common": {
"type": "Direct",
"requested": "[9.0.2, )",
"resolved": "9.0.2",
"contentHash": "JU947wzf8JbBS16Y5EIZzAlyQU+k68D7LRx6y03s2wlhlvLqkt/8uPBrjv2hJnnaJKbdb0GhQ3JZsfYXhrRjyg==",
"dependencies": {
"Microsoft.Win32.SystemEvents": "9.0.2"
}
},
"ToolGood.Words.Pinyin": {
"type": "Direct",
"requested": "[3.0.1.4, )",
"resolved": "3.0.1.4",
"contentHash": "uQo97618y9yzLDxrnehPN+/tuiOlk5BqieEdwctHZOAS9miMXnHKgMFYVw8CSGXRglyTYXlrW7qtUlU7Fje5Ew=="
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2024.3.0",
"contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug=="
},
"MemoryPack.Core": {
"type": "Transitive",
"resolved": "1.21.3",
"contentHash": "ajrYoBWT2aKeH4tlY8q/1C9qK1R/NK+7FkuVOX58ebOSxkABoFTqCR7W+Zk2rakUHZiEgNdRqO67hiRZPq6fLA=="
},
"MemoryPack.Generator": {
"type": "Transitive",
"resolved": "1.21.3",
"contentHash": "hYU0TAIarDKnbkNIWvb7P4zBUL+CTahkuNkczsKvycSMR5kiwQ4IfLexywNKX3s05Izp4gzDSPbueepNWZRpWA=="
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Transitive",
"resolved": "17.12.19",
"contentHash": "v3IYeedjoktvZ+GqYmLudxZJngmf/YWIxNT2Uy6QMMN19cvw+nkWoip1Gr1RtnFkUo1MPUVMis4C8Kj8d8DpSQ=="
},
"Microsoft.VisualStudio.Validation": {
"type": "Transitive",
"resolved": "17.8.8",
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
},
"Microsoft.Win32.SystemEvents": {
"type": "Transitive",
"resolved": "9.0.2",
"contentHash": "5BkGZ6mHp2dHydR29sb0fDfAuqkv30AHtTih8wMzvPZysOmBFvHfnkR2w3tsc0pSiIg8ZoKyefJXWy9r3pBh0w=="
},
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",
"resolved": "0.1.42-alpha",
"contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA=="
},
"Microsoft.Windows.SDK.Win32Metadata": {
"type": "Transitive",
"resolved": "60.0.34-preview",
"contentHash": "TA3DUNi4CTeo+ItTXBnGZFt2159XOGSl0UOlG5vjDj4WHqZjhwYyyUnzOtrbCERiSaP2Hzg7otJNWwOSZgutyA=="
},
"Microsoft.Windows.WDK.Win32Metadata": {
"type": "Transitive",
"resolved": "0.11.4-experimental",
"contentHash": "bf5MCmUyZf0gBlYQjx9UpRAZWBkRndyt9XicR+UNLvAUAFTZQbu6YaX/sNKZlR98Grn0gydfh/yT4I3vc0AIQA==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview"
}
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ=="
},
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"JetBrains.Annotations": "[2024.3.0, )",
"PropertyChanged.Fody": "[3.4.0, )"
}
}
}
}
}

View file

@ -0,0 +1,92 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Describes a result of a <see cref="Query"/> executed by a plugin in Dialog Jump window
/// </summary>
public class DialogJumpResult : Result
{
/// <summary>
/// This holds the path which can be provided by plugin to be navigated to the
/// file dialog when records in Dialog Jump window is right clicked on a result.
/// </summary>
public required string DialogJumpPath { get; init; }
/// <summary>
/// Clones the current Dialog Jump result
/// </summary>
public new DialogJumpResult Clone()
{
return new DialogJumpResult
{
Title = Title,
SubTitle = SubTitle,
ActionKeywordAssigned = ActionKeywordAssigned,
CopyText = CopyText,
AutoCompleteText = AutoCompleteText,
IcoPath = IcoPath,
BadgeIcoPath = BadgeIcoPath,
RoundedIcon = RoundedIcon,
Icon = Icon,
BadgeIcon = BadgeIcon,
Glyph = Glyph,
Action = Action,
AsyncAction = AsyncAction,
Score = Score,
TitleHighlightData = TitleHighlightData,
OriginQuery = OriginQuery,
PluginDirectory = PluginDirectory,
ContextData = ContextData,
PluginID = PluginID,
TitleToolTip = TitleToolTip,
SubTitleToolTip = SubTitleToolTip,
PreviewPanel = PreviewPanel,
ProgressBar = ProgressBar,
ProgressBarColor = ProgressBarColor,
Preview = Preview,
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey,
ShowBadge = ShowBadge,
DialogJumpPath = DialogJumpPath
};
}
/// <summary>
/// Convert <see cref="Result"/> to <see cref="DialogJumpResult"/>.
/// </summary>
public static DialogJumpResult From(Result result, string dialogJumpPath)
{
return new DialogJumpResult
{
Title = result.Title,
SubTitle = result.SubTitle,
ActionKeywordAssigned = result.ActionKeywordAssigned,
CopyText = result.CopyText,
AutoCompleteText = result.AutoCompleteText,
IcoPath = result.IcoPath,
BadgeIcoPath = result.BadgeIcoPath,
RoundedIcon = result.RoundedIcon,
Icon = result.Icon,
BadgeIcon = result.BadgeIcon,
Glyph = result.Glyph,
Action = result.Action,
AsyncAction = result.AsyncAction,
Score = result.Score,
TitleHighlightData = result.TitleHighlightData,
OriginQuery = result.OriginQuery,
PluginDirectory = result.PluginDirectory,
ContextData = result.ContextData,
PluginID = result.PluginID,
TitleToolTip = result.TitleToolTip,
SubTitleToolTip = result.SubTitleToolTip,
PreviewPanel = result.PreviewPanel,
ProgressBar = result.ProgressBar,
ProgressBarColor = result.ProgressBarColor,
Preview = result.Preview,
AddSelectedCount = result.AddSelectedCount,
RecordKey = result.RecordKey,
ShowBadge = result.ShowBadge,
DialogJumpPath = dialogJumpPath
};
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}</ProjectGuid>
<UseWPF>true</UseWPF>
<OutputType>Library</OutputType>
@ -11,6 +11,7 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup>

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Asynchronous Dialog Jump Model
/// </summary>
public interface IAsyncDialogJump : IFeatures
{
/// <summary>
/// Asynchronous querying for Dialog Jump window
/// </summary>
/// <para>
/// If the Querying method requires high IO transmission
/// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncDialogJump interface
/// </para>
/// <param name="query">Query to search</param>
/// <param name="token">Cancel when querying job is obsolete</param>
/// <returns></returns>
Task<List<DialogJumpResult>> QueryDialogJumpAsync(Query query, CancellationToken token);
}
}

View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Synchronous Dialog Jump Model
/// <para>
/// If the Querying method requires high IO transmission
/// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncDialogJump interface
/// </para>
/// </summary>
public interface IDialogJump : IAsyncDialogJump
{
/// <summary>
/// Querying for Dialog Jump window
/// <para>
/// This method will be called within a Task.Run,
/// so please avoid synchrously wait for long.
/// </para>
/// </summary>
/// <param name="query">Query to search</param>
/// <returns></returns>
List<DialogJumpResult> QueryDialogJump(Query query);
Task<List<DialogJumpResult>> IAsyncDialogJump.QueryDialogJumpAsync(Query query, CancellationToken token) => Task.Run(() => QueryDialogJump(query), token);
}
}

View file

@ -0,0 +1,96 @@
using System;
#nullable enable
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Interface for handling file dialog instances in DialogJump.
/// </summary>
public interface IDialogJumpDialog : IFeatures, IDisposable
{
/// <summary>
/// Check if the foreground window is a file dialog instance.
/// </summary>
/// <param name="hwnd">
/// The handle of the foreground window to check.
/// </param>
/// <returns>
/// The window if the foreground window is a file dialog instance. Null if it is not.
/// </returns>
IDialogJumpDialogWindow? CheckDialogWindow(IntPtr hwnd);
}
/// <summary>
/// Interface for handling a specific file dialog window in DialogJump.
/// </summary>
public interface IDialogJumpDialogWindow : IDisposable
{
/// <summary>
/// The handle of the dialog window.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current tab of the dialog window.
/// </summary>
/// <returns></returns>
IDialogJumpDialogWindowTab GetCurrentTab();
}
/// <summary>
/// Interface for handling a specific tab in a file dialog window in DialogJump.
/// </summary>
public interface IDialogJumpDialogWindowTab : IDisposable
{
/// <summary>
/// The handle of the dialog tab.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current folder path of the dialog tab.
/// </summary>
/// <returns></returns>
string GetCurrentFolder();
/// <summary>
/// Get the current file of the dialog tab.
/// </summary>
/// <returns></returns>
string GetCurrentFile();
/// <summary>
/// Jump to a folder in the dialog tab.
/// </summary>
/// <param name="path">
/// The path to the folder to jump to.
/// </param>
/// <param name="auto">
/// Whether folder jump is under automatical mode.
/// </param>
/// <returns>
/// True if the jump was successful, false otherwise.
/// </returns>
bool JumpFolder(string path, bool auto);
/// <summary>
/// Jump to a file in the dialog tab.
/// </summary>
/// <param name="path">
/// The path to the file to jump to.
/// </param>
/// <returns>
/// True if the jump was successful, false otherwise.
/// </returns>
bool JumpFile(string path);
/// <summary>
/// Open the file in the dialog tab.
/// </summary>
/// <returns>
/// True if the file was opened successfully, false otherwise.
/// </returns>
bool Open();
}
}

View file

@ -0,0 +1,40 @@
using System;
#nullable enable
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Interface for handling file explorer instances in DialogJump.
/// </summary>
public interface IDialogJumpExplorer : IFeatures, IDisposable
{
/// <summary>
/// Check if the foreground window is a Windows Explorer instance.
/// </summary>
/// <param name="hwnd">
/// The handle of the foreground window to check.
/// </param>
/// <returns>
/// The window if the foreground window is a file explorer instance. Null if it is not.
/// </returns>
IDialogJumpExplorerWindow? CheckExplorerWindow(IntPtr hwnd);
}
/// <summary>
/// Interface for handling a specific file explorer window in DialogJump.
/// </summary>
public interface IDialogJumpExplorerWindow : IDisposable
{
/// <summary>
/// The handle of the explorer window.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current folder path of the explorer window.
/// </summary>
/// <returns></returns>
string? GetExplorerPath();
}
}

View file

@ -32,6 +32,6 @@ namespace Flow.Launcher.Plugin
Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context));
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query));
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query), token);
}
}

View file

@ -613,5 +613,17 @@ namespace Flow.Launcher.Plugin
/// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
/// </summary>
event ActualApplicationThemeChangedEventHandler ActualApplicationThemeChanged;
/// <summary>
/// Get the user data directory of Flow Launcher.
/// </summary>
/// <returns></returns>
string GetDataDirectory();
/// <summary>
/// Get the log directory of Flow Launcher.
/// </summary>
/// <returns></returns>
string GetLogDirectory();
}
}

View file

@ -307,7 +307,7 @@ namespace Flow.Launcher.Plugin
Preview = Preview,
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey,
ShowBadge = ShowBadge,
ShowBadge = ShowBadge
};
}

View file

@ -0,0 +1,77 @@
{
"version": 1,
"dependencies": {
"net9.0-windows7.0": {
"Fody": {
"type": "Direct",
"requested": "[6.5.4, )",
"resolved": "6.5.4",
"contentHash": "GXZuti428IZctfby10xkMbWLCibcb6s29I/psLbBoO2vHJI5eTNVybnlV/Wi1tlIu9GG0bgW/PQwMH+MCldHxw=="
},
"JetBrains.Annotations": {
"type": "Direct",
"requested": "[2024.3.0, )",
"resolved": "2024.3.0",
"contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[1.1.1, )",
"resolved": "1.1.1",
"contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.Windows.CsWin32": {
"type": "Direct",
"requested": "[0.3.106, )",
"resolved": "0.3.106",
"contentHash": "Mx5fK7uN6fwLR4wUghs6//HonAnwPBNmC2oonyJVhCUlHS/r6SUS3NkBc3+gaQiv+0/9bqdj1oSCKQFkNI+21Q==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
"Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview",
"Microsoft.Windows.WDK.Win32Metadata": "0.11.4-experimental"
}
},
"PropertyChanged.Fody": {
"type": "Direct",
"requested": "[3.4.0, )",
"resolved": "3.4.0",
"contentHash": "IAZyq0uolKo2WYm4mjx+q7A8fSGFT0x2e1s3y+ODn4JI0kqTDoo9GF2tdaypUzRFJZfdMxfC5HZW9QzdJLtOnA==",
"dependencies": {
"Fody": "6.5.1"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg=="
},
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",
"resolved": "0.1.42-alpha",
"contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA=="
},
"Microsoft.Windows.SDK.Win32Metadata": {
"type": "Transitive",
"resolved": "60.0.34-preview",
"contentHash": "TA3DUNi4CTeo+ItTXBnGZFt2159XOGSl0UOlG5vjDj4WHqZjhwYyyUnzOtrbCERiSaP2Hzg7otJNWwOSZgutyA=="
},
"Microsoft.Windows.WDK.Win32Metadata": {
"type": "Transitive",
"resolved": "0.11.4-experimental",
"contentHash": "bf5MCmUyZf0gBlYQjx9UpRAZWBkRndyt9XicR+UNLvAUAFTZQbu6YaX/sNKZlR98Grn0gydfh/yT4I3vc0AIQA==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview"
}
}
}
}
}

View file

@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using Flow.Launcher.Infrastructure;
using ToolGood.Words.Pinyin;
namespace Flow.Launcher.Test
{
/// <summary>
/// Performance test comparing ContainsChinese() vs WordsHelper.HasChinese()
///
/// This test verifies:
/// 1. Both methods produce identical results (correctness)
/// 2. Performance characteristics of both implementations
/// 3. Memory allocation patterns
///
/// The ContainsChinese() method uses optimized Unicode range checking with ReadOnlySpan
/// while WordsHelper.HasChinese() uses the ToolGood.Words library implementation.
/// </summary>
[TestFixture]
public class ChineseDetectionPerformanceTest
{
private readonly List<string> _testStrings = new()
{
// Pure English - should return false
"Hello World",
"Visual Studio Code",
"Microsoft Office 2023",
"Adobe Photoshop Creative Suite",
"Google Chrome Browser Application",
// Pure Chinese - should return true
"你好世界",
"微软办公软件",
"谷歌浏览器",
"北京大学计算机科学与技术学院",
"中华人民共和国国家发展和改革委员会",
// Mixed content - should return true
"Hello 世界",
"Visual Studio 代码编辑器",
"QQ音乐 Music Player",
"Windows 10 操作系统",
"GitHub 代码仓库管理平台",
// Edge cases
"",
" ",
"123456",
"!@#$%^&*()",
"café résumé naïve", // Accented characters (not Chinese)
// Long strings for performance testing
"This is a very long English string that contains no Chinese characters but is designed to test performance with longer text content that might appear in file names or application descriptions",
"这是一个非常长的中文字符串,包含了很多汉字,用来测试在处理较长中文文本时的性能表现,比如可能出现在文件名或应用程序描述中的文本内容",
"This is a mixed 混合内容的字符串 that contains both English and Chinese characters 中英文混合 to test performance with 复杂的文本内容 in real-world scenarios 真实场景中的应用"
};
[Test]
public void ContainsChinese_CorrectnessTest()
{
// Verify ContainsChinese works correctly for known cases
ClassicAssert.IsFalse(ContainsChinese("Hello World"), "Pure English should return false");
ClassicAssert.IsTrue(ContainsChinese("你好世界"), "Pure Chinese should return true");
ClassicAssert.IsTrue(ContainsChinese("Hello 世界"), "Mixed content should return true");
ClassicAssert.IsFalse(ContainsChinese(""), "Empty string should return false");
ClassicAssert.IsFalse(ContainsChinese("123456"), "Numbers should return false");
ClassicAssert.IsFalse(ContainsChinese("café résumé"), "Accented characters should return false");
}
[Test]
public void WordsHelper_CorrectnessTest()
{
// Verify WordsHelper.HasChinese works correctly for known cases
ClassicAssert.IsFalse(WordsHelper.HasChinese("Hello World"), "Pure English should return false");
ClassicAssert.IsTrue(WordsHelper.HasChinese("你好世界"), "Pure Chinese should return true");
ClassicAssert.IsTrue(WordsHelper.HasChinese("Hello 世界"), "Mixed content should return true");
ClassicAssert.IsFalse(WordsHelper.HasChinese(""), "Empty string should return false");
ClassicAssert.IsFalse(WordsHelper.HasChinese("123456"), "Numbers should return false");
ClassicAssert.IsFalse(WordsHelper.HasChinese("café résumé"), "Accented characters should return false");
}
[Test]
public void BothMethods_ShouldProduceSameResults()
{
// Critical test: verify both methods produce identical results for all test cases
foreach (var testString in _testStrings)
{
var wordsHelperResult = WordsHelper.HasChinese(testString);
var containsChineseResult = ContainsChinese(testString);
ClassicAssert.AreEqual(wordsHelperResult, containsChineseResult,
$"Results differ for string: '{testString}'. WordsHelper: {wordsHelperResult}, ContainsChinese: {containsChineseResult}");
}
Console.WriteLine($"✓ Both methods produce identical results for all {_testStrings.Count} test cases");
}
[Test]
public void PerformanceComparison_BasicBenchmark()
{
const int iterations = 1000000;
Console.WriteLine("=== CHINESE CHARACTER DETECTION PERFORMANCE TEST ===");
Console.WriteLine($"Test iterations: {iterations:N0}");
Console.WriteLine($"Test strings: {_testStrings.Count}");
Console.WriteLine($"Total operations: {iterations * _testStrings.Count:N0}");
Console.WriteLine();
// Warmup to ensure JIT compilation
Console.WriteLine("Warming up...");
for (int i = 0; i < 1000; i++)
{
foreach (var testString in _testStrings)
{
_ = ContainsChinese(testString);
_ = WordsHelper.HasChinese(testString);
}
}
// Benchmark ContainsChinese method
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var sw1 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
foreach (var testString in _testStrings)
{
_ = ContainsChinese(testString);
}
}
sw1.Stop();
// Benchmark WordsHelper.HasChinese method
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var sw2 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
foreach (var testString in _testStrings)
{
_ = WordsHelper.HasChinese(testString);
}
}
sw2.Stop();
// Calculate and display results
var containsChineseMs = sw1.Elapsed.TotalMilliseconds;
var wordsHelperMs = sw2.Elapsed.TotalMilliseconds;
var speedRatio = wordsHelperMs / containsChineseMs;
var timeDifference = wordsHelperMs - containsChineseMs;
Console.WriteLine("RESULTS:");
Console.WriteLine($"ContainsChinese(): {containsChineseMs:F3} ms");
Console.WriteLine($"WordsHelper.HasChinese(): {wordsHelperMs:F3} ms");
Console.WriteLine($"Time difference: {timeDifference:F3} ms");
Console.WriteLine($"Speed improvement: {speedRatio:F2}x");
Console.WriteLine($"Performance gain: {((speedRatio - 1) * 100):F1}%");
Console.WriteLine();
if (speedRatio > 1.0)
{
Console.WriteLine($"✓ ContainsChinese() is {speedRatio:F2}x faster than WordsHelper.HasChinese()");
}
else
{
Console.WriteLine($"⚠ WordsHelper.HasChinese() is {(1/speedRatio):F2}x faster than ContainsChinese()");
}
// Test always passes - this is a measurement test
ClassicAssert.IsTrue(true);
}
[Test]
public void PerformanceComparison_ByStringType()
{
Console.WriteLine("=== PERFORMANCE BY STRING TYPE ===");
var categories = new Dictionary<string, List<string>>
{
["Pure English"] = _testStrings.Where(s => !ContainsChinese(s) && s.All(c => c <= 127)).ToList(),
["Pure Chinese"] = _testStrings.Where(s => ContainsChinese(s) && s.All(c => IsChineseCharacter(c) || char.IsWhiteSpace(c))).ToList(),
["Mixed Content"] = _testStrings.Where(s => ContainsChinese(s) && s.Any(c => c <= 127 && char.IsLetter(c))).ToList(),
["Edge Cases"] = _testStrings.Where(s => string.IsNullOrWhiteSpace(s) || s.All(c => !char.IsLetter(c))).ToList()
};
foreach (var category in categories)
{
if (category.Value.Count == 0) continue;
Console.WriteLine($"\n{category.Key} ({category.Value.Count} strings):");
var sample = category.Value.First();
var displayText = sample.Length > 40 ? sample.Substring(0, 40) + "..." : sample;
Console.WriteLine($" Sample: '{displayText}'");
const int categoryIterations = 5000;
// Test each method
var sw1 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < categoryIterations; i++)
{
foreach (var str in category.Value)
{
_ = ContainsChinese(str);
}
}
sw1.Stop();
var sw2 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < categoryIterations; i++)
{
foreach (var str in category.Value)
{
_ = WordsHelper.HasChinese(str);
}
}
sw2.Stop();
var ratio = (double)sw2.ElapsedTicks / sw1.ElapsedTicks;
Console.WriteLine($" Performance: ContainsChinese is {ratio:F2}x faster");
}
ClassicAssert.IsTrue(true);
}
/// <summary>
/// Optimized Chinese character detection using comprehensive CJK Unicode ranges
/// This method uses ReadOnlySpan for better performance and covers all CJK character ranges
/// </summary>
private static bool ContainsChinese(ReadOnlySpan<char> text)
{
foreach (var c in text)
{
if (IsChineseCharacter(c))
return true;
}
return false;
}
/// <summary>
/// Check if a character is a Chinese character using comprehensive Unicode ranges
/// Covers CJK Unified Ideographs and all extension blocks
/// </summary>
private static bool IsChineseCharacter(char c)
{
return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs (most common Chinese characters)
(c >= 0x3400 && c <= 0x4DBF) || // CJK Extension A
(c >= 0x20000 && c <= 0x2A6DF) || // CJK Extension B
(c >= 0x2A700 && c <= 0x2B73F) || // CJK Extension C
(c >= 0x2B740 && c <= 0x2B81F) || // CJK Extension D
(c >= 0x2B820 && c <= 0x2CEAF) || // CJK Extension E
(c >= 0x2CEB0 && c <= 0x2EBEF) || // CJK Extension F
(c >= 0xF900 && c <= 0xFAFF) || // CJK Compatibility Ideographs
(c >= 0x2F800 && c <= 0x2FA1F); // CJK Compatibility Supplement
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<ProjectGuid>{FF742965-9A80-41A5-B042-D6C7D3A21708}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>

View file

@ -1,4 +1,6 @@
using Flow.Launcher.Infrastructure;
using System.Collections.Generic;
using System.Reflection;
using Flow.Launcher.Infrastructure;
using NUnit.Framework;
using NUnit.Framework.Legacy;
@ -34,22 +36,21 @@ namespace Flow.Launcher.Test
mapping.AddNewIndex(2, 2);
mapping.AddNewIndex(5, 3);
var result = mapping.MapToOriginalIndex(translatedIndex);
ClassicAssert.AreEqual(expectedOriginalIndex, result);
}
private int GetOriginalToTranslatedCount(TranslationMapping mapping)
private static int GetOriginalToTranslatedCount(TranslationMapping mapping)
{
var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var list = (System.Collections.Generic.List<int>)field.GetValue(mapping);
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
var list = (List<int>)field.GetValue(mapping);
return list.Count;
}
private int GetOriginalToTranslatedAt(TranslationMapping mapping, int index)
private static int GetOriginalToTranslatedAt(TranslationMapping mapping, int index)
{
var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var list = (System.Collections.Generic.List<int>)field.GetValue(mapping);
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
var list = (List<int>)field.GetValue(mapping);
return list[index];
}
}

View file

@ -16,6 +16,7 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -191,6 +192,9 @@ namespace Flow.Launcher
// Enable Win32 dark mode if the system is in dark mode before creating all windows
Win32Helper.EnableWin32DarkMode(_settings.ColorScheme);
// Initialize language before portable clean up since it needs translations
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
@ -216,8 +220,8 @@ namespace Flow.Launcher
await PluginManager.InitializePluginsAsync();
// Change language after all plugins are initialized because we need to update plugin title based on their api
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
// Update plugin titles after plugins are initialized with their api instances
Internationalization.UpdatePluginMetadataTranslations();
await imageLoadertask;
@ -233,12 +237,16 @@ 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 ----------------------------------------------------");
@ -251,7 +259,7 @@ namespace Flow.Launcher
/// Check startup only for Release
/// </summary>
[Conditional("RELEASE")]
private void AutoStartup()
private static void AutoStartup()
{
// we try to enable auto-startup on first launch, or reenable if it was removed
// but the user still has the setting set
@ -266,13 +274,13 @@ 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.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
API.ShowMsgError(API.GetTranslation("setAutoStartFailed"), e.Message);
}
}
}
[Conditional("RELEASE")]
private void AutoUpdates()
private static void AutoUpdates()
{
_ = Task.Run(async () =>
{
@ -289,6 +297,37 @@ namespace Flow.Launcher
});
}
private static void AutoPluginUpdates()
{
_ = Task.Run(async () =>
{
if (_settings.AutoUpdatePlugins)
{
// check plugin updates every 5 hour
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Current.Dispatcher.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
});
});
while (await timer.WaitForNextTickAsync())
// check updates on startup
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Current.Dispatcher.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
});
});
}
});
}
#endregion
#region Register Events
@ -380,6 +419,7 @@ namespace Flow.Launcher
// since some resources owned by the thread need to be disposed.
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
_mainVM?.Dispose();
DialogJump.Dispose();
}
API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------");

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>false</UseWindowsForms>
<StartupObject>Flow.Launcher.App</StartupObject>
@ -12,6 +12,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -77,7 +78,7 @@
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Segoe Fluent Icons.ttf">
<Content Include="Resources\SegoeFluentIcons.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@ -89,7 +90,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="MdXaml" Version="1.27.0" />
<PackageReference Include="MdXaml.AnimatedGif" Version="1.27.0" />
<PackageReference Include="MdXaml.Html" Version="1.27.0" />
@ -102,7 +102,6 @@
<!-- 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="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -1,11 +1,12 @@
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.ViewModel;
using System;
using ChefKeys;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.ViewModel;
using NHotkey;
using NHotkey.Wpf;
namespace Flow.Launcher.Helper;
@ -22,6 +23,10 @@ internal static class HotKeyMapper
_settings = Ioc.Default.GetService<Settings>();
SetHotkey(_settings.Hotkey, OnToggleHotkey);
if (_settings.EnableDialogJump)
{
SetHotkey(_settings.DialogJumpHotkey, DialogJump.OnToggleHotkey);
}
LoadCustomPluginHotkey();
}

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;
@ -110,7 +110,8 @@ namespace Flow.Launcher
SelectPrevItemHotkey,
SelectPrevItemHotkey2,
SelectNextItemHotkey,
SelectNextItemHotkey2
SelectNextItemHotkey2,
DialogJumpHotkey,
}
// We can initialize settings in static field because it has been constructed in App constuctor
@ -142,6 +143,7 @@ namespace Flow.Launcher
HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2,
HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey,
HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2,
HotkeyType.DialogJumpHotkey => _settings.DialogJumpHotkey,
_ => throw new System.NotImplementedException("Hotkey type not set")
};
}
@ -201,6 +203,9 @@ namespace Flow.Launcher
case HotkeyType.SelectNextItemHotkey2:
_settings.SelectNextItemHotkey2 = value;
break;
case HotkeyType.DialogJumpHotkey:
_settings.DialogJumpHotkey = value;
break;
default:
throw new System.NotImplementedException("Hotkey type not set");
}

View file

@ -18,6 +18,22 @@
<system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String>
<!-- Portable -->
<system:String x:Key="restartToDisablePortableMode">Flow Launcher needs to restart to finish disabling portable mode, after the restart your portable data profile will be deleted and roaming data profile kept</system:String>
<system:String x:Key="restartToEnablePortableMode">Flow Launcher needs to restart to finish enabling portable mode, after the restart your roaming data profile will be deleted and portable data profile kept</system:String>
<system:String x:Key="moveToDifferentLocation">Flow Launcher has detected you enabled portable mode, would you like to move it to a different location?</system:String>
<system:String x:Key="shortcutsUninstallerCreated">Flow Launcher has detected you disabled portable mode, the relevant shortcuts and uninstaller entry have been created</system:String>
<system:String x:Key="userDataDuplicated">Flow Launcher detected your user data exists both in {0} and {1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.</system:String>
<!-- Plugin Loader -->
<system:String x:Key="pluginHasErrored">The following plugin has errored and cannot be loaded:</system:String>
<system:String x:Key="pluginsHaveErrored">The following plugins have errored and cannot be loaded:</system:String>
<system:String x:Key="referToLogs">Please refer to the logs for more information</system:String>
<!-- Http -->
<system:String x:Key="pleaseTryAgain">Please try again</system:String>
<system:String x:Key="parseProxyFailed">Unable to parse Http Proxy</system:String>
<!-- MainWindow -->
<system:String x:Key="registerHotkeyFailed">Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program.</system:String>
<system:String x:Key="unregisterHotkeyFailed">Failed to unregister hotkey "{0}". Please try again or see log for details</system:String>
@ -93,6 +109,7 @@
<system:String x:Key="typingStartEn">Always Start Typing in English Mode</system:String>
<system:String x:Key="typingStartEnTooltip">Temporarily change your input method to English mode when activating Flow.</system:String>
<system:String x:Key="autoUpdates">Auto Update</system:String>
<system:String x:Key="autoUpdatesTooltip">Automatically check and update the app when available</system:String>
<system:String x:Key="select">Select</system:String>
<system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String>
<system:String x:Key="hideOnStartupToolTip">Flow Launcher search window is hidden in the tray after starting up.</system:String>
@ -106,7 +123,7 @@
<system:String x:Key="ShouldUsePinyin">Search with Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese.</system:String>
<system:String x:Key="ShouldUseDoublePinyin">Use Double Pinyin</system:String>
<system:String x:Key="ShouldUseDoublePinyinToolTip">Allows using Double Pinyin to search. Double Pinyin is a variation of Pinyin that uses two characters.</system:String>
<system:String x:Key="ShouldUseDoublePinyinToolTip">Use Double Pinyin instead of Full Pinyin to search.</system:String>
<system:String x:Key="DoublePinyinSchema">Double Pinyin Schema</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoHe">Xiao He</system:String>
<system:String x:Key="DoublePinyinSchemasZiRanMa">Zi Ran Ma</system:String>
@ -117,7 +134,7 @@
<system:String x:Key="DoublePinyinSchemasXingKongJianDao">Xing Kong Jian Dao</system:String>
<system:String x:Key="DoublePinyinSchemasDaNiu">Da Niu</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoLang">Xiao Lang</system:String>
<system:String x:Key="AlwaysPreview">Always Preview</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
@ -139,6 +156,8 @@
<system:String x:Key="KoreanImeOpenLinkButton">Open</system:String>
<system:String x:Key="KoreanImeRegistry">Use Previous Korean IME</system:String>
<system:String x:Key="KoreanImeRegistryTooltip">You can change the Previous Korean IME settings directly from here</system:String>
<system:String x:Key="KoreanImeSettingChangeFailTitle">Failed to change Korean IME setting</system:String>
<system:String x:Key="KoreanImeSettingChangeFailSubTitle">Please check your system registry access or contact support.</system:String>
<system:String x:Key="homePage">Home Page</system:String>
<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>
@ -150,6 +169,8 @@
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
<system:String x:Key="showUnknownSourceWarning">Show unknown source warning</system:String>
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
<system:String x:Key="autoUpdatePlugins">Auto update plugins</system:String>
<system:String x:Key="autoUpdatePluginsToolTip">Automatically check plugin updates and notify if there are any updates available</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String>
@ -231,6 +252,12 @@
<system:String x:Key="ZipFiles">Zip files</system:String>
<system:String x:Key="SelectZipFile">Please select zip file</system:String>
<system:String x:Key="installLocalPluginTooltip">Install plugin from local path</system:String>
<system:String x:Key="updateNoResultTitle">No update available</system:String>
<system:String x:Key="updateNoResultSubtitle">All plugins are up to date</system:String>
<system:String x:Key="updateAllPluginsTitle">Plugin updates available</system:String>
<system:String x:Key="updateAllPluginsButtonContent">Update plugins</system:String>
<system:String x:Key="checkPluginUpdatesTooltip">Check plugin updates</system:String>
<system:String x:Key="PluginsUpdateSuccessNoRestart">Plugins are successfully updated. Please restart Flow.</system:String>
<!-- Setting Theme -->
<system:String x:Key="theme">Theme</system:String>
@ -358,6 +385,28 @@
<system:String x:Key="showBadges">Show Result Badges</system:String>
<system:String x:Key="showBadgesToolTip">For supported plugins, badges are displayed to help distinguish them more easily.</system:String>
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
<system:String x:Key="showBadgesGlobalOnlyToolTip">Show badges for global query results only</system:String>
<system:String x:Key="dialogJumpHotkey">Dialog Jump</system:String>
<system:String x:Key="dialogJumpHotkeyToolTip">Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager.</system:String>
<system:String x:Key="dialogJump">Dialog Jump</system:String>
<system:String x:Key="dialogJumpToolTip">When Open/Save As dialog window opens, quickly navigate to the current path of the file manager.</system:String>
<system:String x:Key="autoDialogJump">Dialog Jump Automatically</system:String>
<system:String x:Key="autoDialogJumpToolTip">When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental)</system:String>
<system:String x:Key="showDialogJumpWindow">Show Dialog Jump Window</system:String>
<system:String x:Key="showDialogJumpWindowToolTip">Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations.</system:String>
<system:String x:Key="dialogJumpWindowPosition">Dialog Jump Window Position</system:String>
<system:String x:Key="dialogJumpWindowPositionToolTip">Select position for the Dialog Jump search window</system:String>
<system:String x:Key="DialogJumpWindowPositionUnderDialog">Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed</system:String>
<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="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>
<!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP Proxy</system:String>
@ -555,6 +604,11 @@
<system:String x:Key="update_flowlauncher_update_files">Update files</system:String>
<system:String x:Key="update_flowlauncher_update_update_description">Update description</system:String>
<!-- Plugin Update Window -->
<system:String x:Key="restartAfterUpdating">Restart Flow Launcher after updating plugins</system:String>
<system:String x:Key="updatePluginCheckboxContent">{0}: Update from v{1} to v{2}</system:String>
<system:String x:Key="updatePluginNoSelected">No plugin selected</system:String>
<!-- Welcome Window -->
<system:String x:Key="Skip">Skip</system:String>
<system:String x:Key="Welcome_Page1_Title">Welcome to Flow Launcher</system:String>

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
using System.Linq;
using System.Media;
@ -19,6 +19,7 @@ using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -119,7 +120,7 @@ namespace Flow.Launcher
Win32Helper.DisableControlBox(this);
}
private void OnLoaded(object sender, RoutedEventArgs _)
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Check first launch
if (_settings.FirstLaunch)
@ -168,10 +169,12 @@ namespace Flow.Launcher
if (_settings.HideOnStartup)
{
_viewModel.Hide();
_viewModel.InitializeVisibilityStatus(false);
}
else
{
_viewModel.Show();
_viewModel.InitializeVisibilityStatus(true);
// When HideOnStartup is off and UseAnimation is on,
// there was a bug where the clock would not appear at all on the initial launch
// So we need to forcibly trigger animation here to ensure the clock is visible
@ -214,6 +217,9 @@ namespace Flow.Launcher
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
_viewModel.QueryTextCursorMovedToEnd = false;
// Register Dialog Jump events
InitializeDialogJump();
// View model property changed event
_viewModel.PropertyChanged += (o, e) =>
{
@ -226,7 +232,7 @@ namespace Flow.Launcher
if (_viewModel.MainWindowVisibilityStatus)
{
// Play sound effect before activing the window
if (_settings.UseSound)
if (_settings.UseSound && !_viewModel.IsDialogJumpWindowUnderDialog())
{
SoundPlay();
}
@ -249,7 +255,7 @@ namespace Flow.Launcher
QueryTextBox.Focus();
// Play window animation
if (_settings.UseAnimation)
if (_settings.UseAnimation && !_viewModel.IsDialogJumpWindowUnderDialog())
{
WindowAnimation();
}
@ -379,6 +385,11 @@ namespace Flow.Launcher
private void OnLocationChanged(object sender, EventArgs e)
{
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return;
}
if (IsLoaded)
{
_settings.WindowLeft = Left;
@ -388,6 +399,11 @@ namespace Flow.Launcher
private async void OnDeactivated(object sender, EventArgs e)
{
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return;
}
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
@ -577,11 +593,23 @@ namespace Flow.Launcher
switch (msg)
{
case Win32Helper.WM_ENTERSIZEMOVE:
// Do do handle size move event for dialog jump window
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return IntPtr.Zero;
}
_initialWidth = (int)Width;
_initialHeight = (int)Height;
handled = true;
break;
case Win32Helper.WM_EXITSIZEMOVE:
// Do do handle size move event for Dialog Jump window
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return IntPtr.Zero;
}
//Prevent updating the number of results when the window height is below the height of a single result item.
//This situation occurs not only when the user manually resizes the window, but also when the window is released from a side snap, as the OS automatically adjusts the window height.
//(Without this check, releasing from a snap can cause the window height to hit the minimum, resulting in only 2 results being shown.)
@ -792,11 +820,19 @@ namespace Flow.Launcher
#region Window Position
private void UpdatePosition()
public void UpdatePosition()
{
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
InitializePosition();
InitializePosition();
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
InitializeDialogJumpPosition();
}
else
{
InitializePosition();
InitializePosition();
}
}
private async Task PositionResetAsync()
@ -1354,6 +1390,46 @@ namespace Flow.Launcher
#endregion
#region Dialog Jump
private void InitializeDialogJump()
{
DialogJump.ShowDialogJumpWindowAsync = _viewModel.SetupDialogJumpAsync;
DialogJump.UpdateDialogJumpWindow = InitializeDialogJumpPosition;
DialogJump.ResetDialogJumpWindow = _viewModel.ResetDialogJump;
DialogJump.HideDialogJumpWindow = _viewModel.HideDialogJump;
}
private void InitializeDialogJumpPosition()
{
if (_viewModel.DialogWindowHandle == nint.Zero || !_viewModel.MainWindowVisibilityStatus) return;
if (!_viewModel.IsDialogJumpWindowUnderDialog()) return;
// Get dialog window rect
var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window);
if (!result) return;
// Move window below the bottom of the dialog and keep it center
Top = VerticalBottom(window);
Left = HorizonCenter(window);
}
private double HorizonCenter(Rect window)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, window.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, window.Width, 0);
var left = (dip2.X - ActualWidth) / 2 + dip1.X;
return left;
}
private double VerticalBottom(Rect window)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, window.Bottom);
return dip1.Y;
}
#endregion
#region IDisposable
protected virtual void Dispose(bool disposing)

View file

@ -37,8 +37,9 @@ namespace Flow.Launcher
try
{
msgBox = new MessageBoxEx(button);
if (caption == string.Empty && button == MessageBoxButton.OK && icon == MessageBoxImage.None)
if (caption == string.Empty && icon == MessageBoxImage.None)
{
// If there is no caption and no icon, use DescOnlyTextBlock for vertically centered text
msgBox.Title = messageBoxText;
msgBox.DescOnlyTextBlock.Visibility = Visibility.Visible;
msgBox.DescOnlyTextBlock.Text = messageBoxText;

View file

@ -0,0 +1,111 @@
<Window
x:Class="Flow.Launcher.PluginUpdateWindow"
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:flowlauncher="clr-namespace:Flow.Launcher"
Title="{DynamicResource updateAllPluginsButtonContent}"
Width="530"
Background="{DynamicResource PopuBGColor}"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}"
Icon="Images\app.png"
ResizeMode="NoResize"
SizeToContent="Height"
Topmost="True"
WindowStartupLocation="CenterScreen">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="Close" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Close" Executed="cmdEsc_OnPress" />
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="80" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="1"
Click="BtnCancel_OnClick"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
</Grid>
</StackPanel>
<StackPanel Margin="26 0 26 0">
<TextBlock
Margin="0 0 0 12"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource updateAllPluginsButtonContent}"
TextAlignment="Left" />
<ScrollViewer
MaxHeight="300"
Margin="0 5 0 5"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="UpdatePluginStackPanel" />
</ScrollViewer>
<Rectangle
Height="1"
Margin="0 5 0 5"
Fill="{DynamicResource SeparatorForeground}" />
<CheckBox
Margin="0 10 0 10"
Content="{DynamicResource restartAfterUpdating}"
IsChecked="{Binding Restart, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
<Border
Grid.Row="1"
Margin="0 14 0 0"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0 1 0 0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
x:Name="btnCancel"
MinWidth="140"
Margin="10 0 5 0"
Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" />
<Button
x:Name="btnUpdate"
MinWidth="140"
Margin="5 0 10 0"
Click="btnUpdate_OnClick"
Content="{DynamicResource update}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher
{
public partial class PluginUpdateWindow : Window
{
public List<PluginUpdateInfo> Plugins { get; set; } = new();
public bool Restart { get; set; }
private readonly Settings _settings = Ioc.Default.GetRequiredService<Settings>();
public PluginUpdateWindow(List<PluginUpdateInfo> allPlugins)
{
Restart = _settings.AutoRestartAfterChanging;
InitializeComponent();
foreach (var plugin in allPlugins)
{
var checkBox = new CheckBox
{
Content = string.Format(App.API.GetTranslation("updatePluginCheckboxContent"), plugin.Name, plugin.CurrentVersion, plugin.NewVersion),
IsChecked = true,
Margin = new Thickness(0, 5, 0, 5),
Tag = plugin,
VerticalAlignment = VerticalAlignment.Center,
};
checkBox.Checked += CheckBox_Checked;
checkBox.Unchecked += CheckBox_Unchecked;
UpdatePluginStackPanel.Children.Add(checkBox);
Plugins.Add(plugin);
}
}
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
if (sender is not CheckBox cb) return;
if (cb.Tag is not PluginUpdateInfo plugin) return;
if (!Plugins.Contains(plugin))
{
Plugins.Add(plugin);
}
}
private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
{
if (sender is not CheckBox cb) return;
if (cb.Tag is not PluginUpdateInfo plugin) return;
if (Plugins.Contains(plugin))
{
Plugins.Remove(plugin);
}
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void btnUpdate_OnClick(object sender, RoutedEventArgs e)
{
if (Plugins.Count == 0)
{
App.API.ShowMsgBox(App.API.GetTranslation("updatePluginNoSelected"));
return;
}
_ = PluginInstaller.UpdateAllPluginsAsync(Plugins, Restart);
DialogResult = true;
Close();
}
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}

View file

@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<PublishDir>..\Output\Release\</PublishDir>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>

View file

@ -599,6 +599,10 @@ namespace Flow.Launcher
remove => _mainVM.ActualApplicationThemeChanged -= value;
}
public string GetDataDirectory() => DataLocation.DataDirectory();
public string GetLogDirectory() => DataLocation.VersionLogDirectory;
#endregion
#region Private Methods

View file

@ -1,15 +1,15 @@
using Flow.Launcher.Core.ExternalPlugins;
using System;
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher
{
@ -44,7 +44,7 @@ namespace Flow.Launcher
var websiteUrl = exception switch
{
FlowPluginException pluginException =>GetIssuesUrl(pluginException.Metadata.Website),
FlowPluginException pluginException => GetIssuesUrl(pluginException.Metadata.Website),
_ => Constant.IssuesUrl
};
@ -73,17 +73,36 @@ namespace Flow.Launcher
Margin = new Thickness(0)
};
var link = new Hyperlink
Hyperlink link = null;
try
{
IsEnabled = true
};
link.Inlines.Add(url);
link.NavigateUri = new Uri(url);
link.Click += (s, e) => SearchWeb.OpenInBrowserTab(url);
var uri = new Uri(url);
link = new Hyperlink
{
IsEnabled = true
};
link.Inlines.Add(url);
link.NavigateUri = uri;
link.Click += (s, e) => SearchWeb.OpenInBrowserTab(url);
}
catch (Exception)
{
// Leave link as null if the URL is invalid
}
paragraph.Inlines.Add(textBeforeUrl);
paragraph.Inlines.Add(" ");
paragraph.Inlines.Add(link);
if (link is null)
{
// Add the URL as plain text if it is invalid
paragraph.Inlines.Add(url);
}
else
{
// Add the hyperlink if it is valid
paragraph.Inlines.Add(link);
}
paragraph.Inlines.Add("\n");
return paragraph;

View file

@ -59,7 +59,7 @@ namespace Flow.Launcher.Resources.Pages
}
catch (Exception e)
{
App.API.ShowMsg(App.API.GetTranslation("setAutoStartFailed"), e.Message);
App.API.ShowMsgError(App.API.GetTranslation("setAutoStartFailed"), e.Message);
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
@ -8,6 +8,7 @@ using Flow.Launcher.Core.Configuration;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@ -64,7 +65,7 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
}
catch (Exception e)
{
App.API.ShowMsg(App.API.GetTranslation("setAutoStartFailed"), e.Message);
App.API.ShowMsgError(App.API.GetTranslation("setAutoStartFailed"), e.Message);
}
}
}
@ -91,7 +92,7 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
}
catch (Exception e)
{
App.API.ShowMsg(App.API.GetTranslation("setAutoStartFailed"), e.Message);
App.API.ShowMsgError(App.API.GetTranslation("setAutoStartFailed"), e.Message);
}
}
}
@ -146,6 +147,40 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
public List<LastQueryModeData> LastQueryModes { get; } =
DropdownDataGeneric<LastQueryMode>.GetValues<LastQueryModeData>("LastQuery");
public bool EnableDialogJump
{
get => Settings.EnableDialogJump;
set
{
if (Settings.EnableDialogJump != value)
{
Settings.EnableDialogJump = value;
DialogJump.SetupDialogJump(value);
if (Settings.EnableDialogJump)
{
HotKeyMapper.SetHotkey(new(Settings.DialogJumpHotkey), DialogJump.OnToggleHotkey);
}
else
{
HotKeyMapper.RemoveHotkey(Settings.DialogJumpHotkey);
}
}
}
}
public class DialogJumpWindowPositionData : DropdownDataGeneric<DialogJumpWindowPositions> { }
public class DialogJumpResultBehaviourData : DropdownDataGeneric<DialogJumpResultBehaviours> { }
public class DialogJumpFileResultBehaviourData : DropdownDataGeneric<DialogJumpFileResultBehaviours> { }
public List<DialogJumpWindowPositionData> DialogJumpWindowPositions { get; } =
DropdownDataGeneric<DialogJumpWindowPositions>.GetValues<DialogJumpWindowPositionData>("DialogJumpWindowPosition");
public List<DialogJumpResultBehaviourData> DialogJumpResultBehaviours { get; } =
DropdownDataGeneric<DialogJumpResultBehaviours>.GetValues<DialogJumpResultBehaviourData>("DialogJumpResultBehaviour");
public List<DialogJumpFileResultBehaviourData> DialogJumpFileResultBehaviours { get; } =
DropdownDataGeneric<DialogJumpFileResultBehaviours>.GetValues<DialogJumpFileResultBehaviourData>("DialogJumpFileResultBehaviour");
public int SearchDelayTimeValue
{
get => Settings.SearchDelayTime;
@ -179,6 +214,9 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
DropdownDataGeneric<SearchPrecisionScore>.UpdateLabels(SearchPrecisionScores);
DropdownDataGeneric<LastQueryMode>.UpdateLabels(LastQueryModes);
DropdownDataGeneric<DoublePinyinSchemas>.UpdateLabels(DoublePinyinSchemas);
DropdownDataGeneric<DialogJumpWindowPositions>.UpdateLabels(DialogJumpWindowPositions);
DropdownDataGeneric<DialogJumpResultBehaviours>.UpdateLabels(DialogJumpResultBehaviours);
DropdownDataGeneric<DialogJumpFileResultBehaviours>.UpdateLabels(DialogJumpFileResultBehaviours);
// Since we are using Binding instead of DynamicResource, we need to manually trigger the update
OnPropertyChanged(nameof(AlwaysPreviewToolTip));
}
@ -216,8 +254,8 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
}
else
{
//Since this is rarely seen text, language support is not provided.
App.API.ShowMsg("Failed to change Korean IME setting", "Please check your system registry access or contact support.");
// Since this is rarely seen text, language support is not provided.
App.API.ShowMsgError(App.API.GetTranslation("KoreanImeSettingChangeFailTitle"), App.API.GetTranslation("KoreanImeSettingChangeFailSubTitle"));
}
}
}

View file

@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -34,6 +35,15 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey);
}
[RelayCommand]
private void SetDialogJumpHotkey(HotkeyModel hotkey)
{
if (Settings.EnableDialogJump)
{
HotKeyMapper.SetHotkey(hotkey, DialogJump.OnToggleHotkey);
}
}
[RelayCommand]
private void CustomHotkeyDelete()
{

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
@ -109,6 +110,19 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
await PluginInstaller.InstallPluginAndCheckRestartAsync(file);
}
[RelayCommand]
private async Task CheckPluginUpdatesAsync()
{
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Application.Current.Dispatcher.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
});
}, silentUpdate: false);
}
private static string GetFileFromDialog(string title, string filter = "")
{
var dlg = new Microsoft.Win32.OpenFileDialog

View file

@ -1,4 +1,4 @@
<ui:Page
<ui:Page
x:Class="Flow.Launcher.SettingPages.Views.SettingsPaneGeneral"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -182,7 +182,8 @@
<cc:Card
Title="{DynamicResource autoUpdates}"
Margin="0 14 0 0"
Icon="&#xecc5;">
Icon="&#xecc5;"
Sub="{DynamicResource autoUpdatesTooltip}">
<ui:ToggleSwitch
IsOn="{Binding AutoUpdates}"
OffContent="{DynamicResource disable}"
@ -241,14 +242,99 @@
Title="{DynamicResource showUnknownSourceWarning}"
Icon="&#xE7BA;"
Sub="{DynamicResource showUnknownSourceWarningToolTip}"
Type="Last">
Type="Middle">
<ui:ToggleSwitch
IsOn="{Binding Settings.ShowUnknownSourceWarning}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource autoUpdatePlugins}"
Icon="&#xecc5;"
Sub="{DynamicResource autoUpdatePluginsToolTip}"
Type="Last">
<ui:ToggleSwitch
IsOn="{Binding Settings.AutoUpdatePlugins}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
</cc:CardGroup>
<cc:ExCard
Title="{DynamicResource dialogJump}"
Margin="0 14 0 0"
Icon="&#xE8AB;"
Sub="{DynamicResource dialogJumpToolTip}">
<cc:ExCard.SideContent>
<ui:ToggleSwitch
IsOn="{Binding EnableDialogJump}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:ExCard.SideContent>
<StackPanel>
<cc:Card
Title="{DynamicResource autoDialogJump}"
Sub="{DynamicResource autoDialogJumpToolTip}"
Type="InsideFit"
Visibility="Collapsed">
<ui:ToggleSwitch
IsOn="{Binding Settings.AutoDialogJump}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource showDialogJumpWindow}"
Sub="{DynamicResource showDialogJumpWindowToolTip}"
Type="InsideFit">
<ui:ToggleSwitch
IsOn="{Binding Settings.ShowDialogJumpWindow}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource dialogJumpWindowPosition}"
Sub="{DynamicResource dialogJumpWindowPositionToolTip}"
Type="InsideFit">
<ComboBox
MinWidth="120"
MaxWidth="210"
DisplayMemberPath="Display"
ItemsSource="{Binding DialogJumpWindowPositions}"
SelectedValue="{Binding Settings.DialogJumpWindowPosition}"
SelectedValuePath="Value" />
</cc:Card>
<cc:Card
Title="{DynamicResource dialogJumpResultBehaviour}"
Sub="{DynamicResource dialogJumpResultBehaviourToolTip}"
Type="InsideFit">
<ComboBox
MinWidth="120"
DisplayMemberPath="Display"
ItemsSource="{Binding DialogJumpResultBehaviours}"
SelectedValue="{Binding Settings.DialogJumpResultBehaviour}"
SelectedValuePath="Value" />
</cc:Card>
<cc:Card
Title="{DynamicResource dialogJumpFileResultBehaviour}"
Sub="{DynamicResource dialogJumpFileResultBehaviourToolTip}"
Type="InsideFit">
<ComboBox
MinWidth="120"
MaxWidth="240"
DisplayMemberPath="Display"
ItemsSource="{Binding DialogJumpFileResultBehaviours}"
SelectedValue="{Binding Settings.DialogJumpFileResultBehaviour}"
SelectedValuePath="Value" />
</cc:Card>
</StackPanel>
</cc:ExCard>
<cc:ExCard
Title="{DynamicResource searchDelay}"
Margin="0 14 0 0"
@ -371,44 +457,39 @@
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:CardGroup Margin="0 4 0 0">
<cc:Card
Title="{DynamicResource ShouldUsePinyin}"
Icon="&#xe98a;"
Sub="{DynamicResource ShouldUsePinyinToolTip}"
Type="First">
<ui:ToggleSwitch
IsOn="{Binding ShouldUsePinyin}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}"
ToolTip="{DynamicResource ShouldUsePinyinToolTip}" />
</cc:Card>
<cc:Card
Visibility="{ext:VisibleWhen {Binding ShouldUsePinyin},
<cc:Card
Title="{DynamicResource ShouldUsePinyin}"
Margin="0 4 0 0"
Icon="&#xe98a;"
Sub="{DynamicResource ShouldUsePinyinToolTip}">
<ui:ToggleSwitch
IsOn="{Binding ShouldUsePinyin}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}"
ToolTip="{DynamicResource ShouldUsePinyinToolTip}" />
</cc:Card>
<cc:ExCard
Title="{DynamicResource ShouldUseDoublePinyin}"
Icon="&#xf085;"
Visibility="{ext:VisibleWhen {Binding ShouldUsePinyin},
IsEqualToBool=True}"
Title="{DynamicResource ShouldUseDoublePinyin}"
Icon="&#xf085;"
Sub="{DynamicResource ShouldUseDoublePinyinToolTip}"
Type="Middle">
Sub="{DynamicResource ShouldUseDoublePinyinToolTip}">
<cc:ExCard.SideContent>
<ui:ToggleSwitch
IsOn="{Binding UseDoublePinyin}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}"
ToolTip="{DynamicResource ShouldUseDoublePinyinToolTip}" />
</cc:Card>
<cc:Card
Visibility="{ext:VisibleWhen {Binding UseDoublePinyin},
IsEqualToBool=True}"
Title="{DynamicResource DoublePinyinSchema}"
Sub="{DynamicResource DoublePinyinSchemaToolTip}"
Type="Last">
</cc:ExCard.SideContent>
<cc:Card Title="{DynamicResource DoublePinyinSchema}" Type="InsideFit">
<ComboBox
DisplayMemberPath="Display"
ItemsSource="{Binding DoublePinyinSchemas}"
SelectedValue="{Binding Settings.DoublePinyinSchema}"
SelectedValuePath="Value" />
</cc:Card>
</cc:CardGroup>
</cc:ExCard>
<cc:Card
Title="{DynamicResource language}"

View file

@ -73,6 +73,19 @@
</cc:Card>
</cc:CardGroup>
<cc:Card
Title="{DynamicResource dialogJumpHotkey}"
Margin="0 14 0 0"
Icon="&#xE8AB;"
Sub="{DynamicResource dialogJumpHotkeyToolTip}">
<flowlauncher:HotkeyControl
ChangeHotkey="{Binding SetDialogJumpHotkeyCommand}"
DefaultHotkey="Alt+G"
Type="DialogJumpHotkey"
ValidateKeyGesture="False"
WindowTitle="{DynamicResource dialogJumpHotkey}" />
</cc:Card>
<cc:ExCard
Title="{DynamicResource hotkeyPresets}"
Margin="0 14 0 0"

View file

@ -99,6 +99,13 @@
ToolTip="{DynamicResource installLocalPluginTooltip}">
<ui:FontIcon FontSize="14" Glyph="&#xE8DA;" />
</Button>
<Button
Height="34"
Margin="0 0 10 0"
Command="{Binding CheckPluginUpdatesCommand}"
ToolTip="{DynamicResource checkPluginUpdatesTooltip}">
<ui:FontIcon FontSize="14" Glyph="&#xecc5;" />
</Button>
<TextBox
Name="PluginStoreFilterTextbox"
Width="150"

View file

@ -16,6 +16,7 @@ using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
@ -52,6 +53,7 @@ namespace Flow.Launcher.ViewModel
private Task _resultsViewUpdateTask;
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
private readonly IReadOnlyList<DialogJumpResult> _emptyDialogJumpResult = new List<DialogJumpResult>();
private readonly PluginMetadata _historyMetadata = new()
{
@ -215,7 +217,8 @@ namespace Flow.Launcher.ViewModel
var resultUpdateChannel = Channel.CreateUnbounded<ResultsForUpdate>();
_resultsUpdateChannelWriter = resultUpdateChannel.Writer;
_resultsViewUpdateTask =
Task.Run(UpdateActionAsync).ContinueWith(continueAction, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
Task.Run(UpdateActionAsync).ContinueWith(continueAction,
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
async Task UpdateActionAsync()
{
@ -285,8 +288,16 @@ namespace Flow.Launcher.ViewModel
var token = e.Token == default ? _updateToken : e.Token;
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
var resultsCopy = DeepCloneResults(e.Results, token);
IReadOnlyList<Result> resultsCopy;
if (e.Results == null)
{
resultsCopy = _emptyResult;
}
else
{
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
resultsCopy = DeepCloneResults(e.Results, false, token);
}
foreach (var result in resultsCopy)
{
@ -394,12 +405,30 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void LoadContextMenu()
{
// For Dialog Jump and right click mode, we need to navigate to the path
if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.RightClick)
{
if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero)
{
var result = SelectedResults.SelectedItem.Result;
if (result is DialogJumpResult dialogJumpResult)
{
Win32Helper.SetForegroundWindow(DialogWindowHandle);
_ = Task.Run(() => DialogJump.JumpToPathAsync(DialogWindowHandle, dialogJumpResult.DialogJumpPath));
}
}
return;
}
// For query mode, we load context menu
if (QueryResultsSelected())
{
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
if (SelectedResults.SelectedItem != null)
{
SelectedResults = ContextMenu;
}
}
else
{
@ -469,12 +498,34 @@ namespace Flow.Launcher.ViewModel
return;
}
var hideWindow = await result.ExecuteAsync(new ActionContext
// For Dialog Jump and left click mode, we need to navigate to the path
if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.LeftClick)
{
// not null means pressing modifier key + number, should ignore the modifier key
SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers()
})
.ConfigureAwait(false);
Hide();
if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero)
{
if (result is DialogJumpResult dialogJumpResult)
{
Win32Helper.SetForegroundWindow(DialogWindowHandle);
_ = Task.Run(() => DialogJump.JumpToPathAsync(DialogWindowHandle, dialogJumpResult.DialogJumpPath));
}
}
}
// For query mode, we execute the result
else
{
var hideWindow = await result.ExecuteAsync(new ActionContext
{
// not null means pressing modifier key + number, should ignore the modifier key
SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers()
}).ConfigureAwait(false);
if (hideWindow)
{
Hide();
}
}
if (QueryResultsSelected())
{
@ -482,26 +533,33 @@ namespace Flow.Launcher.ViewModel
_history.Add(result.OriginQuery.RawQuery);
lastHistoryIndex = 1;
}
if (hideWindow)
{
Hide();
}
}
private static IReadOnlyList<Result> DeepCloneResults(IReadOnlyList<Result> results, CancellationToken token = default)
private static IReadOnlyList<Result> DeepCloneResults(IReadOnlyList<Result> results, bool isDialogJump, CancellationToken token = default)
{
var resultsCopy = new List<Result>();
foreach (var result in results.ToList())
{
if (token.IsCancellationRequested)
{
break;
}
var resultCopy = result.Clone();
resultsCopy.Add(resultCopy);
if (isDialogJump)
{
foreach (var result in results.ToList())
{
if (token.IsCancellationRequested) break;
var resultCopy = ((DialogJumpResult)result).Clone();
resultsCopy.Add(resultCopy);
}
}
else
{
foreach (var result in results.ToList())
{
if (token.IsCancellationRequested) break;
var resultCopy = result.Clone();
resultsCopy.Add(resultCopy);
}
}
return resultsCopy;
}
@ -1279,25 +1337,21 @@ namespace Flow.Launcher.ViewModel
if (query == null) // shortcut expanded
{
App.API.LogDebug(ClassName, $"Clear query results");
// Hide and clear results again because running query may show and add some results
Results.Visibility = Visibility.Collapsed;
Results.Clear();
// Reset plugin icon
PluginIconPath = null;
PluginIconSource = null;
SearchIconVisibility = Visibility.Visible;
// Hide progress bar again because running query may set this to visible
ProgressBarVisibility = Visibility.Hidden;
ClearResults();
return;
}
App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>");
var currentIsHomeQuery = query.IsHomeQuery;
var currentIsDialogJump = _isDialogJump;
// Do not show home page for Dialog Jump window
if (currentIsHomeQuery && currentIsDialogJump)
{
ClearResults();
return;
}
_updateSource?.Dispose();
@ -1331,7 +1385,7 @@ namespace Flow.Launcher.ViewModel
}
else
{
plugins = PluginManager.ValidPluginsForQuery(query);
plugins = PluginManager.ValidPluginsForQuery(query, currentIsDialogJump);
if (plugins.Count == 1)
{
@ -1425,6 +1479,23 @@ namespace Flow.Launcher.ViewModel
}
// Local function
void ClearResults()
{
App.API.LogDebug(ClassName, $"Clear query results");
// Hide and clear results again because running query may show and add some results
Results.Visibility = Visibility.Collapsed;
Results.Clear();
// Reset plugin icon
PluginIconPath = null;
PluginIconSource = null;
SearchIconVisibility = Visibility.Visible;
// Hide progress bar again because running query may set this to visible
ProgressBarVisibility = Visibility.Hidden;
}
async Task QueryTaskAsync(PluginPair plugin, CancellationToken token)
{
App.API.LogDebug(ClassName, $"Wait for querying plugin <{plugin.Metadata.Name}>");
@ -1442,21 +1513,23 @@ namespace Flow.Launcher.ViewModel
// Task.Yield will force it to run in ThreadPool
await Task.Yield();
var results = currentIsHomeQuery ?
await PluginManager.QueryHomeForPluginAsync(plugin, query, token) :
await PluginManager.QueryForPluginAsync(plugin, query, token);
IReadOnlyList<Result> results = currentIsDialogJump ?
await PluginManager.QueryDialogJumpForPluginAsync(plugin, query, token) :
currentIsHomeQuery ?
await PluginManager.QueryHomeForPluginAsync(plugin, query, token) :
await PluginManager.QueryForPluginAsync(plugin, query, token);
if (token.IsCancellationRequested) return;
IReadOnlyList<Result> resultsCopy;
if (results == null)
{
resultsCopy = _emptyResult;
resultsCopy = currentIsDialogJump ? _emptyDialogJumpResult : _emptyResult;
}
else
{
// make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc.
resultsCopy = DeepCloneResults(results, token);
resultsCopy = DeepCloneResults(results, currentIsDialogJump, token);
}
foreach (var result in resultsCopy)
@ -1751,6 +1824,208 @@ namespace Flow.Launcher.ViewModel
#endregion
#region Dialog Jump
public nint DialogWindowHandle { get; private set; } = nint.Zero;
private bool _isDialogJump = false;
private bool _previousMainWindowVisibilityStatus;
private CancellationTokenSource _dialogJumpSource;
public void InitializeVisibilityStatus(bool visibilityStatus)
{
_previousMainWindowVisibilityStatus = visibilityStatus;
}
public bool IsDialogJumpWindowUnderDialog()
{
return _isDialogJump && DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog;
}
public async Task SetupDialogJumpAsync(nint handle)
{
if (handle == nint.Zero) return;
// Only set flag & reset window once for one file dialog
var dialogWindowHandleChanged = false;
if (DialogWindowHandle != handle)
{
DialogWindowHandle = handle;
_previousMainWindowVisibilityStatus = MainWindowVisibilityStatus;
_isDialogJump = true;
dialogWindowHandleChanged = true;
// If don't give a time, Positioning will be weird
await Task.Delay(300);
}
// If handle is cleared, which means the dialog is closed, clear Dialog Jump state
if (DialogWindowHandle == nint.Zero)
{
_isDialogJump = false;
return;
}
// Initialize Dialog Jump window
if (MainWindowVisibilityStatus)
{
if (dialogWindowHandleChanged)
{
// Only update the position
Application.Current?.Dispatcher.Invoke(() =>
{
(Application.Current?.MainWindow as MainWindow)?.UpdatePosition();
});
_ = ResetWindowAsync();
}
}
else
{
if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog)
{
// We wait for window to be reset before showing it because if window has results,
// showing it before resetting will cause flickering when results are clearing
if (dialogWindowHandleChanged)
{
await ResetWindowAsync();
}
Show();
}
else
{
if (dialogWindowHandleChanged)
{
_ = ResetWindowAsync();
}
}
}
if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog)
{
// Cancel the previous Dialog Jump task
_dialogJumpSource?.Cancel();
// Create a new cancellation token source
_dialogJumpSource = new CancellationTokenSource();
_ = Task.Run(() =>
{
try
{
// Check task cancellation
if (_dialogJumpSource.Token.IsCancellationRequested) return;
// Check dialog handle
if (DialogWindowHandle == nint.Zero) return;
// Wait 150ms to check if Dialog Jump window gets the focus
var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150);
if (timeOut) return;
// Bring focus back to the dialog
Win32Helper.SetForegroundWindow(DialogWindowHandle);
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to focus on dialog window", e);
}
});
}
}
#pragma warning disable VSTHRD100 // Avoid async void methods
public async void ResetDialogJump()
{
// Cache original dialog window handle
var dialogWindowHandle = DialogWindowHandle;
// Reset the Dialog Jump state
DialogWindowHandle = nint.Zero;
_isDialogJump = false;
// If dialog window handle is not set, we should not reset the main window visibility
if (dialogWindowHandle == nint.Zero) return;
if (_previousMainWindowVisibilityStatus != MainWindowVisibilityStatus)
{
// We wait for window to be reset before showing it because if window has results,
// showing it before resetting will cause flickering when results are clearing
await ResetWindowAsync();
// Show or hide to change visibility
if (_previousMainWindowVisibilityStatus)
{
Show();
}
else
{
Hide(false);
}
}
else
{
if (_previousMainWindowVisibilityStatus)
{
// Only update the position
Application.Current?.Dispatcher.Invoke(() =>
{
(Application.Current?.MainWindow as MainWindow)?.UpdatePosition();
});
_ = ResetWindowAsync();
}
else
{
_ = ResetWindowAsync();
}
}
}
#pragma warning restore VSTHRD100 // Avoid async void methods
public void HideDialogJump()
{
if (DialogWindowHandle != nint.Zero)
{
if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog)
{
// Warning: Main window is already in foreground
// This is because if you click popup menus in other applications to hide Dialog Jump window,
// they can steal focus before showing main window
if (MainWindowVisibilityStatus)
{
Hide();
}
}
}
}
// Reset index & preview & selected results & query text
private async Task ResetWindowAsync()
{
lastHistoryIndex = 1;
if (ExternalPreviewVisible)
{
await CloseExternalPreviewAsync();
}
if (!QueryResultsSelected())
{
SelectedResults = Results;
}
await ChangeQueryTextAsync(string.Empty, true);
}
#endregion
#region Public Methods
#pragma warning disable VSTHRD100 // Avoid async void methods
@ -1770,7 +2045,7 @@ namespace Flow.Launcher.ViewModel
Win32Helper.DWMSetCloakForWindow(mainWindow, false);
// Set clock and search icon opacity
var opacity = Settings.UseAnimation ? 0.0 : 1.0;
var opacity = (Settings.UseAnimation && !_isDialogJump) ? 0.0 : 1.0;
ClockPanelOpacity = opacity;
SearchIconOpacity = opacity;
@ -1799,37 +2074,40 @@ namespace Flow.Launcher.ViewModel
}
}
public async void Hide()
public async void Hide(bool reset = true)
{
lastHistoryIndex = 1;
if (ExternalPreviewVisible)
if (reset)
{
await CloseExternalPreviewAsync();
}
lastHistoryIndex = 1;
BackToQueryResults();
if (ExternalPreviewVisible)
{
await CloseExternalPreviewAsync();
}
switch (Settings.LastQueryMode)
{
case LastQueryMode.Empty:
await ChangeQueryTextAsync(string.Empty);
break;
case LastQueryMode.Preserved:
case LastQueryMode.Selected:
LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved;
break;
case LastQueryMode.ActionKeywordPreserved:
case LastQueryMode.ActionKeywordSelected:
var newQuery = _lastQuery?.ActionKeyword;
BackToQueryResults();
if (!string.IsNullOrEmpty(newQuery))
newQuery += " ";
await ChangeQueryTextAsync(newQuery);
switch (Settings.LastQueryMode)
{
case LastQueryMode.Empty:
await ChangeQueryTextAsync(string.Empty);
break;
case LastQueryMode.Preserved:
case LastQueryMode.Selected:
LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved;
break;
case LastQueryMode.ActionKeywordPreserved:
case LastQueryMode.ActionKeywordSelected:
var newQuery = _lastQuery.ActionKeyword;
if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected)
LastQuerySelected = false;
break;
if (!string.IsNullOrEmpty(newQuery))
newQuery += " ";
await ChangeQueryTextAsync(newQuery);
if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected)
LastQuerySelected = false;
break;
}
}
// When application is exiting, the Application.Current will be null
@ -1839,7 +2117,7 @@ namespace Flow.Launcher.ViewModel
if (Application.Current?.MainWindow is MainWindow mainWindow)
{
// Set clock and search icon opacity
var opacity = Settings.UseAnimation ? 0.0 : 1.0;
var opacity = (Settings.UseAnimation && !_isDialogJump) ? 0.0 : 1.0;
ClockPanelOpacity = opacity;
SearchIconOpacity = opacity;
@ -1984,6 +2262,7 @@ namespace Flow.Launcher.ViewModel
if (disposing)
{
_updateSource?.Dispose();
_dialogJumpSource?.Dispose();
_resultsUpdateChannelWriter?.Complete();
if (_resultsViewUpdateTask?.IsCompleted == true)
{

View file

@ -0,0 +1,661 @@
{
"version": 1,
"dependencies": {
"net9.0-windows10.0.19041": {
"ChefKeys": {
"type": "Direct",
"requested": "[0.1.2, )",
"resolved": "0.1.2",
"contentHash": "hnayWejg57tg8+lZ1Q/zPR8tj9ezUtB1sY8aCv9jiZ+3wcqK0eGL+Skt9OzT9mjSsBIg4o9Jv1HdQdzjd1lkQw=="
},
"CommunityToolkit.Mvvm": {
"type": "Direct",
"requested": "[8.4.0, )",
"resolved": "8.4.0",
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
},
"Fody": {
"type": "Direct",
"requested": "[6.5.4, )",
"resolved": "6.5.4",
"contentHash": "GXZuti428IZctfby10xkMbWLCibcb6s29I/psLbBoO2vHJI5eTNVybnlV/Wi1tlIu9GG0bgW/PQwMH+MCldHxw=="
},
"InputSimulator": {
"type": "Direct",
"requested": "[1.0.4, )",
"resolved": "1.0.4",
"contentHash": "D0LvRCPQMX6/FJHBjng+RO+wRDuHTJrfo7IAc7rmkPvRqchdVGJWg3y70peOtDy3OLNK+HSOwVkH4GiuLnkKgA=="
},
"Jack251970.TaskScheduler": {
"type": "Direct",
"requested": "[2.12.1, )",
"resolved": "2.12.1",
"contentHash": "+epAtsLMugiznJCNRYCYB6eBcr+bx+CVlwPWMprO5CbnNkWu9mlSV8XN5BQJrGYwmlAtlGfZA3p3PcFFlrgR6A==",
"dependencies": {
"Microsoft.Win32.Registry": "5.0.0",
"System.Diagnostics.EventLog": "8.0.0",
"System.Security.AccessControl": "6.0.1"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Direct",
"requested": "[7.0.0, )",
"resolved": "7.0.0",
"contentHash": "elNeOmkeX3eDVG6pYVeV82p29hr+UKDaBhrZyWvWLw/EVZSYEkZlQdkp0V39k/Xehs2Qa0mvoCvkVj3eQxNQ1Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0"
}
},
"Microsoft.Extensions.Hosting": {
"type": "Direct",
"requested": "[7.0.1, )",
"resolved": "7.0.1",
"contentHash": "aoeMou6XSW84wiqd895OdaGyO9PfH6nohQJ0XBcshRDafbdIU6PQIVl8TpOCssPYq3ciRseP5064hbFyCR9J9w==",
"dependencies": {
"Microsoft.Extensions.Configuration": "7.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0",
"Microsoft.Extensions.Configuration.Binder": "7.0.3",
"Microsoft.Extensions.Configuration.CommandLine": "7.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "7.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "7.0.0",
"Microsoft.Extensions.Configuration.Json": "7.0.0",
"Microsoft.Extensions.Configuration.UserSecrets": "7.0.0",
"Microsoft.Extensions.DependencyInjection": "7.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "7.0.0",
"Microsoft.Extensions.FileProviders.Physical": "7.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging.Configuration": "7.0.0",
"Microsoft.Extensions.Logging.Console": "7.0.0",
"Microsoft.Extensions.Logging.Debug": "7.0.0",
"Microsoft.Extensions.Logging.EventLog": "7.0.0",
"Microsoft.Extensions.Logging.EventSource": "7.0.0",
"Microsoft.Extensions.Options": "7.0.1",
"System.Diagnostics.DiagnosticSource": "7.0.1"
}
},
"Microsoft.Toolkit.Uwp.Notifications": {
"type": "Direct",
"requested": "[7.1.3, )",
"resolved": "7.1.3",
"contentHash": "A1dglAzb24gjehmb7DwGd07mfyZ1gacAK7ObE0KwDlRc3mayH2QW7cSOy3TkkyELjLg19OQBuhPOj4SpXET9lg==",
"dependencies": {
"Microsoft.Win32.Registry": "4.7.0",
"System.Drawing.Common": "4.7.0",
"System.Reflection.Emit": "4.7.0",
"System.ValueTuple": "4.5.0"
}
},
"Microsoft.Windows.CsWin32": {
"type": "Direct",
"requested": "[0.3.106, )",
"resolved": "0.3.106",
"contentHash": "Mx5fK7uN6fwLR4wUghs6//HonAnwPBNmC2oonyJVhCUlHS/r6SUS3NkBc3+gaQiv+0/9bqdj1oSCKQFkNI+21Q==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
"Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview",
"Microsoft.Windows.WDK.Win32Metadata": "0.11.4-experimental"
}
},
"ModernWpfUI": {
"type": "Direct",
"requested": "[0.9.4, )",
"resolved": "0.9.4",
"contentHash": "HJ07Be9KOiGKGcMLz/AwY+84h3yGHRPuYpYXCE6h1yPtaFwGMWfanZ70jX7W5XWx8+Qk1vGox+WGKgxxsy6EHw=="
},
"NHotkey.Wpf": {
"type": "Direct",
"requested": "[3.0.0, )",
"resolved": "3.0.0",
"contentHash": "BIUKlhTG5KtFf9OQzWvkmVmktt5/FFj6AOEgag8Uf0R2YdZt5ajUzs3sVskcJcT2TztWlEHKQr1jFj3KQ0D9Nw==",
"dependencies": {
"NHotkey": "3.0.0"
}
},
"PropertyChanged.Fody": {
"type": "Direct",
"requested": "[3.4.0, )",
"resolved": "3.4.0",
"contentHash": "IAZyq0uolKo2WYm4mjx+q7A8fSGFT0x2e1s3y+ODn4JI0kqTDoo9GF2tdaypUzRFJZfdMxfC5HZW9QzdJLtOnA==",
"dependencies": {
"Fody": "6.5.1"
}
},
"SemanticVersioning": {
"type": "Direct",
"requested": "[3.0.0, )",
"resolved": "3.0.0",
"contentHash": "RR+8GbPQ/gjDqov/1QN1OPoUlbUruNwcL3WjWCeLw+MY7+od/ENhnkYxCfAC6rQLIu3QifaJt3kPYyP3RumqMQ=="
},
"VirtualizingWrapPanel": {
"type": "Direct",
"requested": "[2.1.1, )",
"resolved": "2.1.1",
"contentHash": "Fc/yjU8jqC3qpIsNxeO5RjK2lPU7xnJtBLMSQ6L9egA2PyJLQeVeXpG8WBb5N1kN15rlJEYG8dHWJ5qUGgaNrg=="
},
"Ben.Demystifier": {
"type": "Transitive",
"resolved": "0.4.1",
"contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==",
"dependencies": {
"System.Reflection.Metadata": "5.0.0"
}
},
"BitFaster.Caching": {
"type": "Transitive",
"resolved": "2.5.3",
"contentHash": "Vo/39qcam5Xe+DbyfH0JZyqPswdOoa7jv4PGtRJ6Wj8AU+aZ+TuJRlJcIe+MQjRTJwliI8k8VSQpN8sEoBIv2g=="
},
"DeltaCompressionDotNet": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "nwbZAYd+DblXAIzlnwDSnl0CiCm8jWLfHSYnoN4wYhtIav6AegB3+T/vKzLbU2IZlPB8Bvl8U3NXpx3eaz+N5w=="
},
"Droplex": {
"type": "Transitive",
"resolved": "1.7.0",
"contentHash": "wutfIus/Ufw/9TDsp86R1ycnIH+wWrj4UhcmrzAHWjsdyC2iM07WEQ9+APTB7pQynsDnYH1r2i58XgAJ3lxUXA==",
"dependencies": {
"YamlDotNet": "9.1.0"
}
},
"FSharp.Core": {
"type": "Transitive",
"resolved": "9.0.101",
"contentHash": "3/YR1SDWFA+Ojx9HiBwND+0UR8ZWoeZfkhD0DWAPCDdr/YI+CyFkArmMGzGSyPXeYtjG0sy0emzfyNwjt7zhig=="
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2024.3.0",
"contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug=="
},
"MemoryPack": {
"type": "Transitive",
"resolved": "1.21.3",
"contentHash": "cwCtED8y400vMWx/Vp0QCSeEpVFjDU4JwF52VX9WTaqVERUvNqjG9n6osFlmFuytegyXnHvYEu1qRJ8rv/rkbg==",
"dependencies": {
"MemoryPack.Core": "1.21.3",
"MemoryPack.Generator": "1.21.3"
}
},
"MemoryPack.Core": {
"type": "Transitive",
"resolved": "1.21.3",
"contentHash": "ajrYoBWT2aKeH4tlY8q/1C9qK1R/NK+7FkuVOX58ebOSxkABoFTqCR7W+Zk2rakUHZiEgNdRqO67hiRZPq6fLA=="
},
"MemoryPack.Generator": {
"type": "Transitive",
"resolved": "1.21.3",
"contentHash": "hYU0TAIarDKnbkNIWvb7P4zBUL+CTahkuNkczsKvycSMR5kiwQ4IfLexywNKX3s05Izp4gzDSPbueepNWZRpWA=="
},
"MessagePack": {
"type": "Transitive",
"resolved": "2.5.187",
"contentHash": "uW4j8m4Nc+2Mk5n6arOChavJ9bLjkis0qWASOj2h2OwmfINuzYv+mjCHUymrYhmyyKTu3N+ObtTXAY4uQ7jIhg==",
"dependencies": {
"MessagePack.Annotations": "2.5.187",
"Microsoft.NET.StringTools": "17.6.3"
}
},
"MessagePack.Annotations": {
"type": "Transitive",
"resolved": "2.5.187",
"contentHash": "/IvvMMS8opvlHjEJ/fR2Cal4Co726Kj77Z8KiohFhuHfLHHmb9uUxW5+tSCL4ToKFfkQlrS3HD638mRq83ySqA=="
},
"Meziantou.Framework.Win32.Jobs": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "5GGLckfpwoC1jznInEYfK2INrHyD7K1RtwZJ98kNPKBU6jeu24i4zfgDGHHfb+eK3J+eFPAxo0aYcbUxNXIbNw=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "tldQUBWt/xeH2K7/hMPPo5g8zuLc3Ro9I5d4o/XrxvxOCA2EZBtW7bCHHTc49fcBtvB8tLAb/Qsmfrq+2SJ4vA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0",
"Microsoft.Extensions.Primitives": "7.0.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "f34u2eaqIjNO9YLHBz8rozVZ+TcFiFs0F3r7nUJd7FRkVSxk8u4OpoK226mi49MwexHOR2ibP9MFvRUaLilcQQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "7.0.0"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "7.0.3",
"contentHash": "1eRFwJBrkkncTpvh6mivB8zg4uBVm6+Y6stEJERrVEqZZc8Hvf+N1iIgj2ySYDUQko4J1Gw1rLf1M8bG83F0eA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0"
}
},
"Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "a8Iq8SCw5m8W5pZJcPCgBpBO4E89+NaObPng+ApIhrGSv9X4JPrcFAaGM4sDgR0X83uhLgsNJq8VnGP/wqhr8A==",
"dependencies": {
"Microsoft.Extensions.Configuration": "7.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0"
}
},
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "RIkfqCkvrAogirjsqSrG1E1FxgrLsOZU2nhRbl07lrajnxzSU2isj2lwQah0CtCbLWo/pOIukQzM1GfneBUnxA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "7.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "xk2lRJ1RDuqe57BmgvRPyCt6zyePKUmvT6iuXqiHR+/OIIgWVR8Ff5k2p6DwmqY8a17hx/OnrekEhziEIeQP6Q==",
"dependencies": {
"Microsoft.Extensions.Configuration": "7.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "7.0.0",
"Microsoft.Extensions.FileProviders.Physical": "7.0.0",
"Microsoft.Extensions.Primitives": "7.0.0"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "LDNYe3uw76W35Jci+be4LDf2lkQZe0A7EEYQVChFbc509CpZ4Iupod8li4PUXPBhEUOFI/rlQNf5xkzJRQGvtA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "7.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "7.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "7.0.0",
"System.Text.Json": "7.0.0"
}
},
"Microsoft.Extensions.Configuration.UserSecrets": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "33HPW1PmB2RS0ietBQyvOxjp4O3wlt+4tIs8KPyMn1kqp04goiZGa7+3mc69NRLv6bphkLDy0YR7Uw3aZyf8Zw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0",
"Microsoft.Extensions.Configuration.Json": "7.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "7.0.0",
"Microsoft.Extensions.FileProviders.Physical": "7.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw=="
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "NyawiW9ZT/liQb34k9YqBSNPLuuPkrjMgQZ24Y/xXX1RoiBkLUdPMaQTmxhZ5TYu8ZKZ9qayzil75JX95vGQUg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "7.0.0"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "K8D2MTR+EtzkbZ8z80LrG7Ur64R7ZZdRLt1J5cgpc/pUWl0C6IkAUapPuK28oionHueCPELUqq0oYEvZfalNdg==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "7.0.0",
"Microsoft.Extensions.FileSystemGlobbing": "7.0.0",
"Microsoft.Extensions.Primitives": "7.0.0"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "2jONjKHiF+E92ynz2ZFcr9OvxIw+rTGMPEH+UZGeHTEComVav93jQUWGkso8yWwVBcEJGcNcZAaqY01FFJcj7w=="
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "43n9Je09z0p/7ViPxfRqs5BUItRLNVh5b6JH40F2Agkh2NBsY/jpNYTtbCcxrHCsA3oRmbR6RJBzUutB4VZvNQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "7.0.0"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "Nw2muoNrOG5U5qa2ZekXwudUn2BJcD41e65zwmDHb1fQegTX66UokLWZkJRpqSSHXDOWZ5V0iqhbxOEky91atA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "7.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "7.0.0",
"Microsoft.Extensions.Options": "7.0.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "kmn78+LPVMOWeITUjIlfxUPDsI0R6G0RkeAMBmQxAJ7vBJn4q2dTva7pWi65ceN5vPGjJ9q/Uae2WKgvfktJAw=="
},
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "FLDA0HcffKA8ycoDQLJuCNGIE42cLWPxgdQGRBaSzZrYTkMBjnf9zrr8pGT06psLq9Q+RKWmmZczQ9bCrXEBcA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "7.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0",
"Microsoft.Extensions.Configuration.Binder": "7.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "7.0.0",
"Microsoft.Extensions.Options": "7.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "7.0.0"
}
},
"Microsoft.Extensions.Logging.Console": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "qt5n8bHLZPUfuRnFxJKW5q9ZwOTncdh96rtWzWpX3Y/064MlxzCSw2ELF5Jlwdo+Y4wK3I47NmUTFsV7Sg8rqg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging.Configuration": "7.0.0",
"Microsoft.Extensions.Options": "7.0.0",
"System.Text.Json": "7.0.0"
}
},
"Microsoft.Extensions.Logging.Debug": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "tFGGyPDpJ8ZdQdeckCArP7nZuoY3am9zJWuvp4OD1bHq65S0epW9BNHzAWeaIO4eYwWnGm1jRNt3vRciH8H6MA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "7.0.0"
}
},
"Microsoft.Extensions.Logging.EventLog": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "Rp7cYL9xQRVTgjMl77H5YDxszAaO+mlA+KT0BnLSVhuCoKQQOOs1sSK2/x8BK2dZ/lKeAC/CVF+20Ef2dpKXwg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "7.0.0",
"Microsoft.Extensions.Options": "7.0.0",
"System.Diagnostics.EventLog": "7.0.0"
}
},
"Microsoft.Extensions.Logging.EventSource": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "MxQXndQFviIyOPqyMeLNshXnmqcfzEHE2wWcr7BF1unSisJgouZ3tItnq+aJLGPojrW8OZSC/ZdRoR6wAq+c7w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "7.0.0",
"Microsoft.Extensions.Options": "7.0.0",
"Microsoft.Extensions.Primitives": "7.0.0",
"System.Text.Json": "7.0.0"
}
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "7.0.1",
"contentHash": "pZRDYdN1FpepOIfHU62QoBQ6zdAoTvnjxFfqAzEd9Jhb2dfhA5i6jeTdgGgcgTWFRC7oT0+3XrbQu4LjvgX1Nw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Primitives": "7.0.0"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "95UnxZkkFdXxF6vSrtJsMHCzkDeSMuUWGs2hDT54cX+U5eVajrCJ3qLyQRW+CtpTt5OJ8bmTvpQVHu1DLhH+cA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0",
"Microsoft.Extensions.Configuration.Binder": "7.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Options": "7.0.0",
"Microsoft.Extensions.Primitives": "7.0.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q=="
},
"Microsoft.IO.RecyclableMemoryStream": {
"type": "Transitive",
"resolved": "3.0.1",
"contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g=="
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.6.3",
"contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA=="
},
"Microsoft.VisualStudio.Threading": {
"type": "Transitive",
"resolved": "17.12.19",
"contentHash": "eLiGMkMYyaSguqHs3lsrFxy3tAWSLuPEL2pIWRcADMDVAs2xqm3dr1d9QYjiEusTgiClF9KD6OB2NdZP72Oy0Q==",
"dependencies": {
"Microsoft.VisualStudio.Threading.Analyzers": "17.12.19",
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Transitive",
"resolved": "17.12.19",
"contentHash": "v3IYeedjoktvZ+GqYmLudxZJngmf/YWIxNT2Uy6QMMN19cvw+nkWoip1Gr1RtnFkUo1MPUVMis4C8Kj8d8DpSQ=="
},
"Microsoft.VisualStudio.Validation": {
"type": "Transitive",
"resolved": "17.8.8",
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
},
"Microsoft.Win32.Registry": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
"dependencies": {
"System.Security.AccessControl": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
}
},
"Microsoft.Win32.SystemEvents": {
"type": "Transitive",
"resolved": "9.0.2",
"contentHash": "5BkGZ6mHp2dHydR29sb0fDfAuqkv30AHtTih8wMzvPZysOmBFvHfnkR2w3tsc0pSiIg8ZoKyefJXWy9r3pBh0w=="
},
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",
"resolved": "0.1.42-alpha",
"contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA=="
},
"Microsoft.Windows.SDK.Win32Metadata": {
"type": "Transitive",
"resolved": "60.0.34-preview",
"contentHash": "TA3DUNi4CTeo+ItTXBnGZFt2159XOGSl0UOlG5vjDj4WHqZjhwYyyUnzOtrbCERiSaP2Hzg7otJNWwOSZgutyA=="
},
"Microsoft.Windows.WDK.Win32Metadata": {
"type": "Transitive",
"resolved": "0.11.4-experimental",
"contentHash": "bf5MCmUyZf0gBlYQjx9UpRAZWBkRndyt9XicR+UNLvAUAFTZQbu6YaX/sNKZlR98Grn0gydfh/yT4I3vc0AIQA==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview"
}
},
"Mono.Cecil": {
"type": "Transitive",
"resolved": "0.9.6.1",
"contentHash": "yMsurNaOxxKIjyW9pEB+tRrR1S3DFnN1+iBgKvYvXG8kW0Y6yknJeMAe/tl3+P78/2C6304TgF7aVqpqXgEQ9Q=="
},
"Nerdbank.Streams": {
"type": "Transitive",
"resolved": "2.11.74",
"contentHash": "r4G7uHHfoo8LCilPOdtf2C+Q5ymHOAXtciT4ZtB2xRlAvv4gPkWBYNAijFblStv3+uidp81j5DP11jMZl4BfJw==",
"dependencies": {
"Microsoft.VisualStudio.Threading": "17.10.48",
"Microsoft.VisualStudio.Validation": "17.8.8",
"System.IO.Pipelines": "8.0.0"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.1",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
},
"NHotkey": {
"type": "Transitive",
"resolved": "3.0.0",
"contentHash": "IEghs0QqWsQYH0uUmvIl0Ye6RaebWRh38eB6ToOkDnQucTYRGFOgtig0gSxlwCszTilYFz3n1ZuY762x+kDR3A=="
},
"NLog": {
"type": "Transitive",
"resolved": "4.7.10",
"contentHash": "rcegW7kYOCjl7wX0SzsqpPBqnJ51JKi1WkYb6QBVX0Wc5IgH19Pv4t/co+T0s06OS0Ne44xgkY/mHg0PdrmJow=="
},
"Splat": {
"type": "Transitive",
"resolved": "1.6.2",
"contentHash": "DeH0MxPU+D4JchkIDPYG4vUT+hsWs9S41cFle0/4K5EJMXWurx5DzAkj2366DfK14/XKNhsu6tCl4dZXJ3CD4w=="
},
"squirrel.windows": {
"type": "Transitive",
"resolved": "1.5.2",
"contentHash": "89Y/CFxWm7SEOjvuV2stVa8p+SNM9GOLk4tUNm2nUF792nfkimAgwRA/umVsdyd/OXBH8byXSh4V1qck88ZAyQ==",
"dependencies": {
"DeltaCompressionDotNet": "[1.0.0, 2.0.0)",
"Mono.Cecil": "0.9.6.1",
"Splat": "1.6.2"
}
},
"StreamJsonRpc": {
"type": "Transitive",
"resolved": "2.20.20",
"contentHash": "gwG7KViLbSWS7EI0kYevinVmIga9wZNrpSY/FnWyC6DbdjKJ1xlv/FV1L9b0rLkVP8cGxfIMexdvo/+2W5eq6Q==",
"dependencies": {
"MessagePack": "2.5.187",
"Microsoft.VisualStudio.Threading": "17.10.48",
"Microsoft.VisualStudio.Threading.Analyzers": "17.10.48",
"Microsoft.VisualStudio.Validation": "17.8.8",
"Nerdbank.Streams": "2.11.74",
"Newtonsoft.Json": "13.0.1",
"System.IO.Pipelines": "8.0.0"
}
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "7.0.1",
"contentHash": "T9SLFxzDp0SreCffRDXSAS5G+lq6E8qP4knHS2IBjwCdx2KEvGnGZsq7gFpselYOda7l6gXsJMD93TQsFj/URA=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A=="
},
"System.Drawing.Common": {
"type": "Transitive",
"resolved": "9.0.2",
"contentHash": "JU947wzf8JbBS16Y5EIZzAlyQU+k68D7LRx6y03s2wlhlvLqkt/8uPBrjv2hJnnaJKbdb0GhQ3JZsfYXhrRjyg==",
"dependencies": {
"Microsoft.Win32.SystemEvents": "9.0.2"
}
},
"System.IO.Pipelines": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA=="
},
"System.Reflection.Emit": {
"type": "Transitive",
"resolved": "4.7.0",
"contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ=="
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw=="
},
"System.Security.Principal.Windows": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg=="
},
"System.Text.Json": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "DaGSsVqKsn/ia6RG8frjwmJonfos0srquhw09TlT8KRw5I43E+4gs+/bZj4K0vShJ5H9imCuXupb4RmS+dBy3w==",
"dependencies": {
"System.Text.Encodings.Web": "7.0.0"
}
},
"System.ValueTuple": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
},
"ToolGood.Words.Pinyin": {
"type": "Transitive",
"resolved": "3.0.1.4",
"contentHash": "uQo97618y9yzLDxrnehPN+/tuiOlk5BqieEdwctHZOAS9miMXnHKgMFYVw8CSGXRglyTYXlrW7qtUlU7Fje5Ew=="
},
"YamlDotNet": {
"type": "Transitive",
"resolved": "9.1.0",
"contentHash": "fuvGXU4Ec5HrsmEc+BiFTNPCRf1cGBI2kh/3RzMWgddM2M4ALhbSPoI3X3mhXZUD1qqQd9oSkFAtWjpz8z9eRg=="
},
"flow.launcher.core": {
"type": "Project",
"dependencies": {
"Droplex": "[1.7.0, )",
"FSharp.Core": "[9.0.101, )",
"Flow.Launcher.Infrastructure": "[1.0.0, )",
"Flow.Launcher.Plugin": "[4.4.0, )",
"Meziantou.Framework.Win32.Jobs": "[3.4.0, )",
"Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )",
"StreamJsonRpc": "[2.20.20, )",
"squirrel.windows": "[1.5.2, )"
}
},
"flow.launcher.infrastructure": {
"type": "Project",
"dependencies": {
"Ben.Demystifier": "[0.4.1, )",
"BitFaster.Caching": "[2.5.3, )",
"CommunityToolkit.Mvvm": "[8.4.0, )",
"Flow.Launcher.Plugin": "[4.4.0, )",
"MemoryPack": "[1.21.3, )",
"Microsoft.VisualStudio.Threading": "[17.12.19, )",
"NLog": "[4.7.10, )",
"PropertyChanged.Fody": "[3.4.0, )",
"System.Drawing.Common": "[9.0.2, )",
"ToolGood.Words.Pinyin": "[3.0.1.4, )"
}
},
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"JetBrains.Annotations": "[2024.3.0, )",
"PropertyChanged.Fody": "[3.4.0, )"
}
}
}
}
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ProjectGuid>{9B130CC5-14FB-41FF-B310-0A95B6894C37}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder>

View file

@ -7,6 +7,9 @@
<system:String x:Key="flowlauncher_plugin_browserbookmark_plugin_name">Browser Bookmarks</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_plugin_description">Search your browser bookmarks</system:String>
<!-- Main -->
<system:String x:Key="flowlauncher_plugin_browserbookmark_copy_failed">Failed to set url in clipboard</system:String>
<!-- Settings -->
<system:String x:Key="flowlauncher_plugin_browserbookmark_bookmarkDataSetting">Bookmark Data</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_settings_openBookmarks">Open bookmarks in:</system:String>

View file

@ -223,11 +223,8 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
}
catch (Exception e)
{
var message = "Failed to set url in clipboard";
_context.API.LogException(ClassName, message, e);
_context.API.ShowMsg(message);
_context.API.LogException(ClassName, "Failed to set url in clipboard", e);
_context.API.ShowMsgError(_context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copy_failed"));
return false;
}
},

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{59BD9891-3837-438A-958D-ADC7F91F6F7E}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Flow.Launcher.Plugin.Calculator</RootNamespace>

View file

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="flowlauncher_plugin_caculator_plugin_name">Calculator</system:String>
<system:String x:Key="flowlauncher_plugin_caculator_plugin_description">Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher)</system:String>
@ -13,4 +14,5 @@
<system:String x:Key="flowlauncher_plugin_calculator_decimal_seperator_comma">Comma (,)</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_decimal_seperator_dot">Dot (.)</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_max_decimal_places">Max. decimal places</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_failed_to_copy">Copy failed, please try later</system:String>
</ResourceDictionary>

View file

@ -100,7 +100,7 @@ namespace Flow.Launcher.Plugin.Calculator
}
catch (ExternalException)
{
Context.API.ShowMsgBox("Copy failed, please try later");
Context.API.ShowMsgBox(Context.API.GetTranslation("flowlauncher_plugin_calculator_failed_to_copy"));
return false;
}
}

View file

@ -132,9 +132,8 @@ namespace Flow.Launcher.Plugin.Explorer
}
catch (Exception e)
{
var message = "Fail to set text in clipboard";
LogException(message, e);
Context.API.ShowMsg(message);
LogException("Fail to set text in clipboard", e);
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_explorer_fail_to_set_text"));
return false;
}
},
@ -155,9 +154,8 @@ namespace Flow.Launcher.Plugin.Explorer
}
catch (Exception e)
{
var message = "Fail to set text in clipboard";
LogException(message, e);
Context.API.ShowMsg(message);
LogException("Fail to set text in clipboard", e);
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_explorer_fail_to_set_text"));
return false;
}
},
@ -178,9 +176,8 @@ namespace Flow.Launcher.Plugin.Explorer
}
catch (Exception e)
{
var message = $"Fail to set file/folder in clipboard";
LogException(message, e);
Context.API.ShowMsg(message);
LogException($"Fail to set file/folder in clipboard", e);
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_explorer_fail_to_set_files"));
return false;
}
@ -221,9 +218,8 @@ namespace Flow.Launcher.Plugin.Explorer
}
catch (Exception e)
{
var message = $"Fail to delete {record.FullPath}";
LogException(message, e);
Context.API.ShowMsgError(message);
LogException($"Fail to delete {record.FullPath}", e);
Context.API.ShowMsgError(string.Format(Context.API.GetTranslation("plugin_explorer_fail_to_delete"), record.FullPath));
return false;
}
@ -265,9 +261,9 @@ namespace Flow.Launcher.Plugin.Explorer
}
catch (FileNotFoundException e)
{
var name = "Plugin: Folder";
var message = $"File not found: {e.Message}";
Context.API.ShowMsgError(name, message);
Context.API.ShowMsgError(
Context.API.GetTranslation("plugin_explorer_plugin_name"),
string.Format(Context.API.GetTranslation("plugin_explorer_file_not_found"), e.Message));
return false;
}
@ -334,9 +330,8 @@ namespace Flow.Launcher.Plugin.Explorer
}
catch (Exception e)
{
var message = $"Fail to open file at {record.FullPath}";
LogException(message, e);
Context.API.ShowMsgError(message);
LogException($"Fail to open file at {record.FullPath}", e);
Context.API.ShowMsgError(string.Format(Context.API.GetTranslation("plugin_explorer_fail_to_open"), record.FullPath));
return false;
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -48,7 +48,7 @@
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Droplex" Version="1.7.0" />
<!-- Do not upgrade System.Data.OleDb since we are .Net7.0 -->
<PackageReference Include="System.Data.OleDb" Version="8.0.1" />
<PackageReference Include="System.Data.OleDb" Version="9.0.3" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="tlbimp-Microsoft.Search.Interop" Version="1.0.0" />
</ItemGroup>

View file

@ -7,6 +7,8 @@
<system:String x:Key="plugin_explorer_make_selection_warning">Please make a selection first</system:String>
<system:String x:Key="plugin_explorer_quick_access_link_no_folder_selected">Please select a folder path.</system:String>
<system:String x:Key="plugin_explorer_quick_access_link_path_already_exists">Please choose a different name or folder path.</system:String>
<system:String x:Key="plugin_explorer_delete_quick_access_link">Are you sure you want to delete this quick access link?</system:String>
<system:String x:Key="plugin_explorer_delete_index_search_excluded_path">Are you sure you want to delete this index search excluded path?</system:String>
<system:String x:Key="plugin_explorer_select_folder_link_warning">Please select a folder link</system:String>
<system:String x:Key="plugin_explorer_delete_folder_link">Are you sure you want to delete {0}?</system:String>
<system:String x:Key="plugin_explorer_deletefileconfirm">Are you sure you want to permanently delete this file?</system:String>
@ -130,6 +132,11 @@
<system:String x:Key="plugin_explorer_show_contextmenu_title">Show Windows Context Menu</system:String>
<system:String x:Key="plugin_explorer_openwith">Open With</system:String>
<system:String x:Key="plugin_explorer_openwith_subtitle">Select a program to open with</system:String>
<system:String x:Key="plugin_explorer_fail_to_delete">Fail to delete {0}</system:String>
<system:String x:Key="plugin_explorer_file_not_found">File not found: {0}</system:String>
<system:String x:Key="plugin_explorer_fail_to_open">Fail to open {0}</system:String>
<system:String x:Key="plugin_explorer_fail_to_set_text">Fail to set text in clipboard</system:String>
<system:String x:Key="plugin_explorer_fail_to_set_files">Fail to set files/folders in clipboard</system:String>
<!-- Special Results -->
<system:String x:Key="plugin_explorer_diskfreespace">{0} free of {1}</system:String>

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.ViewModels;
@ -10,10 +10,11 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using Flow.Launcher.Plugin.Explorer.Exceptions;
using System.Linq;
namespace Flow.Launcher.Plugin.Explorer
{
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncDialogJump
{
internal static PluginInitContext Context { get; set; }
@ -25,6 +26,8 @@ namespace Flow.Launcher.Plugin.Explorer
private SearchManager searchManager;
private static readonly List<DialogJumpResult> _emptyDialogJumpResultList = new();
public Control CreateSettingPanel()
{
return new ExplorerSettings(viewModel);
@ -108,5 +111,18 @@ namespace Flow.Launcher.Plugin.Explorer
}
}
}
public async Task<List<DialogJumpResult>> QueryDialogJumpAsync(Query query, CancellationToken token)
{
try
{
var results = await searchManager.SearchAsync(query, token);
return results.Select(r => DialogJumpResult.From(r, r.CopyText)).ToList();
}
catch (Exception e) when (e is SearchException or EngineNotAvailableException)
{
return _emptyDialogJumpResultList;
}
}
}
}

View file

@ -283,15 +283,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false)
{
bool isMedia = IsMedia(Path.GetExtension(filePath));
var title = Path.GetFileName(filePath);
var isMedia = IsMedia(Path.GetExtension(filePath));
var title = Path.GetFileName(filePath) ?? string.Empty;
var directory = Path.GetDirectoryName(filePath) ?? string.Empty;
/* Preview Detail */
var result = new Result
{
Title = title,
SubTitle = Path.GetDirectoryName(filePath),
SubTitle = directory,
IcoPath = filePath,
Preview = new Result.PreviewInfo
{
@ -315,7 +316,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift))
{
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, true);
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty, true);
}
else if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control)
{
@ -323,7 +324,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}
else
{
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty);
OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty);
}
}
catch (Exception ex)

View file

@ -431,10 +431,24 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
{
case "QuickAccessLink":
if (SelectedQuickAccessLink == null) return;
if (Context.API.ShowMsgBox(
Context.API.GetTranslation("plugin_explorer_delete_quick_access_link"),
Context.API.GetTranslation("plugin_explorer_delete"),
MessageBoxButton.OKCancel,
MessageBoxImage.Warning)
== MessageBoxResult.Cancel)
return;
Settings.QuickAccessLinks.Remove(SelectedQuickAccessLink);
break;
case "IndexSearchExcludedPaths":
if (SelectedIndexSearchExcludedPath == null) return;
if (Context.API.ShowMsgBox(
Context.API.GetTranslation("plugin_explorer_delete_index_search_excluded_path"),
Context.API.GetTranslation("plugin_explorer_delete"),
MessageBoxButton.OKCancel,
MessageBoxImage.Warning)
== MessageBoxResult.Cancel)
return;
Settings.IndexSearchExcludedSubdirectoryPaths.Remove(SelectedIndexSearchExcludedPath);
break;
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{FDED22C8-B637-42E8-824A-63B5B6E05A3A}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Flow.Launcher.Plugin.PluginIndicator</RootNamespace>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<AssemblyName>Flow.Launcher.Plugin.ProcessKiller</AssemblyName>
<PackageId>Flow.Launcher.Plugin.ProcessKiller</PackageId>
<Authors>Flow-Launcher</Authors>

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<ProjectGuid>{FDB3555B-58EF-4AE6-B5F1-904719637AB4}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Flow.Launcher.Plugin.Program</RootNamespace>

View file

@ -446,7 +446,7 @@ namespace Flow.Launcher.Plugin.Program
var title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error");
var message = string.Format(Context.API.GetTranslation("flowlauncher_plugin_program_run_failed"),
info.FileName);
Context.API.ShowMsg(title, string.Format(message, info.FileName), string.Empty);
Context.API.ShowMsgError(title, message);
}
}

View file

@ -462,7 +462,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var message =
api.GetTranslation(
"flowlauncher_plugin_program_run_as_administrator_not_supported_message");
api.ShowMsg(title, message, string.Empty);
api.ShowMsgError(title, message);
}
return true;

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Flow.Launcher.Plugin.Shell</RootNamespace>

View file

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="flowlauncher_plugin_cmd_relace_winr">Replace Win+R</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_close_cmd_after_press">Close Command Prompt after pressing any key</system:String>
@ -16,4 +17,6 @@
<system:String x:Key="flowlauncher_plugin_cmd_run_as_administrator">Run As Administrator</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_copy">Copy the command</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_history">Only show number of most used commands:</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_command_not_found">Command not found: {0}</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_error_running_command">Error running the command: {0}</system:String>
</ResourceDictionary>

View file

@ -335,15 +335,17 @@ namespace Flow.Launcher.Plugin.Shell
}
catch (FileNotFoundException e)
{
var name = "Plugin: Shell";
var message = $"Command not found: {e.Message}";
Context.API.ShowMsg(name, message);
Context.API.ShowMsgError(GetTranslatedPluginTitle(),
string.Format(Context.API.GetTranslation("flowlauncher_plugin_cmd_command_not_found"), e.Message));
}
catch (Win32Exception e)
{
var name = "Plugin: Shell";
var message = $"Error running the command: {e.Message}";
Context.API.ShowMsg(name, message);
Context.API.ShowMsgError(GetTranslatedPluginTitle(),
string.Format(Context.API.GetTranslation("flowlauncher_plugin_cmd_error_running_command"), e.Message));
}
catch (Exception e)
{
Context.API.LogException(ClassName, $"Error executing command: {info.FileName} {string.Join(" ", info.ArgumentList)}", e);
}
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{0B9DE348-9361-4940-ADB6-F5953BFFCCEC}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Flow.Launcher.Plugin.Sys</RootNamespace>
@ -37,7 +37,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>

View file

@ -63,6 +63,8 @@
<system:String x:Key="flowlauncher_plugin_sys_dlgtext_restart_computer">Are you sure you want to restart the computer?</system:String>
<system:String x:Key="flowlauncher_plugin_sys_dlgtext_restart_computer_advanced">Are you sure you want to restart the computer with Advanced Boot Options?</system:String>
<system:String x:Key="flowlauncher_plugin_sys_dlgtext_logoff_computer">Are you sure you want to log off?</system:String>
<system:String x:Key="flowlauncher_plugin_sys_dlgtitle_error">Error</system:String>
<system:String x:Key="flowlauncher_plugin_sys_dlgtext_empty_recycle_bin_failed">Failed to empty the recycle bin. This might happen if:{0}- Some items are currently in use{0}- Some items can't be deleted due to permissions{0}Please close any applications that might be using these files and try again.</system:String>
<system:String x:Key="flowlauncher_plugin_sys_command_keyword_setting_window_title">Command Keyword Setting</system:String>
<system:String x:Key="flowlauncher_plugin_sys_custom_command_keyword">Custom Command Keyword</system:String>

View file

@ -5,8 +5,6 @@ using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Security;
@ -52,6 +50,8 @@ namespace Flow.Launcher.Plugin.Sys
private const SHUTDOWN_REASON REASON = SHUTDOWN_REASON.SHTDN_REASON_MAJOR_OTHER |
SHUTDOWN_REASON.SHTDN_REASON_FLAG_PLANNED;
private const string Documentation = "https://flowlauncher.com/docs/#/usage-tips";
private PluginInitContext _context;
private Settings _settings;
private ThemeSelector _themeSelector;
@ -70,13 +70,19 @@ namespace Flow.Launcher.Plugin.Sys
return _themeSelector.Query(query);
}
var commands = Commands();
var commands = Commands(query);
var results = new List<Result>();
var isEmptyQuery = string.IsNullOrWhiteSpace(query.Search);
foreach (var c in commands)
{
var command = _settings.Commands.First(x => x.Key == c.Title);
c.Title = command.Name;
c.SubTitle = command.Description;
if (isEmptyQuery)
{
results.Add(c);
continue;
}
// Match from localized title & localized subtitle & keyword
var titleMatch = _context.API.FuzzySearch(query.Search, c.Title);
@ -188,7 +194,7 @@ namespace Flow.Launcher.Plugin.Sys
}
}
private List<Result> Commands()
private List<Result> Commands(Query query)
{
var results = new List<Result>();
var recycleBinFolder = "shell:RecycleBinFolder";
@ -332,11 +338,9 @@ namespace Flow.Launcher.Plugin.Sys
var result = PInvoke.SHEmptyRecycleBin(new(), string.Empty, 0);
if (result != HRESULT.S_OK && result != HRESULT.E_UNEXPECTED)
{
_context.API.ShowMsgBox("Failed to empty the recycle bin. This might happen if:\n" +
"- A file in the recycle bin is in use\n" +
"- You don't have permission to delete some items\n" +
"Please close any applications that might be using these files and try again.",
"Error",
_context.API.ShowMsgBox(
string.Format(_context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_empty_recycle_bin_failed"), Environment.NewLine),
_context.API.GetTranslation("flowlauncher_plugin_sys_dlgtitle_error"),
MessageBoxButton.OK, MessageBoxImage.Error);
}
@ -398,6 +402,8 @@ namespace Flow.Launcher.Plugin.Sys
IcoPath = "Images\\app.png",
Action = c =>
{
// Hide the window first then open setting dialog because main window can be topmost window which will still display on top of the setting dialog for a while
_context.API.HideMainWindow();
_context.API.OpenSettingDialog();
return true;
}
@ -439,11 +445,11 @@ namespace Flow.Launcher.Plugin.Sys
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf12b"),
Title = "Open Log Location",
IcoPath = "Images\\app.png",
CopyText = DataLocation.VersionLogDirectory,
AutoCompleteText = DataLocation.VersionLogDirectory,
CopyText = _context.API.GetLogDirectory(),
AutoCompleteText = _context.API.GetLogDirectory(),
Action = c =>
{
_context.API.OpenDirectory(DataLocation.VersionLogDirectory);
_context.API.OpenDirectory(_context.API.GetLogDirectory());
return true;
}
},
@ -452,11 +458,11 @@ namespace Flow.Launcher.Plugin.Sys
Title = "Flow Launcher Tips",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe897"),
IcoPath = "Images\\app.png",
CopyText = Constant.Documentation,
AutoCompleteText = Constant.Documentation,
CopyText = Documentation,
AutoCompleteText = Documentation,
Action = c =>
{
_context.API.OpenUrl(Constant.Documentation);
_context.API.OpenUrl(Documentation);
return true;
}
},
@ -465,11 +471,11 @@ namespace Flow.Launcher.Plugin.Sys
Title = "Flow Launcher UserData Folder",
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf12b"),
IcoPath = "Images\\app.png",
CopyText = DataLocation.DataDirectory(),
AutoCompleteText = DataLocation.DataDirectory(),
CopyText = _context.API.GetDataDirectory(),
AutoCompleteText = _context.API.GetDataDirectory(),
Action = c =>
{
_context.API.OpenDirectory(DataLocation.DataDirectory());
_context.API.OpenDirectory(_context.API.GetDataDirectory());
return true;
}
},
@ -491,7 +497,15 @@ namespace Flow.Launcher.Plugin.Sys
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue790"),
Action = c =>
{
_context.API.ChangeQuery($"{ThemeSelector.Keyword} ");
if (string.IsNullOrEmpty(query.ActionKeyword))
{
_context.API.ChangeQuery($"{ThemeSelector.Keyword}{Plugin.Query.ActionKeywordSeparator}");
}
else
{
_context.API.ChangeQuery($"{query.ActionKeyword}{Plugin.Query.ActionKeywordSeparator}{ThemeSelector.Keyword}{Plugin.Query.ActionKeywordSeparator}");
}
return false;
}
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{A3DCCBCA-ACC1-421D-B16E-210896234C26}</ProjectGuid>
<UseWPF>true</UseWPF>
<AppDesignerFolder>Properties</AppDesignerFolder>

View file

@ -70,7 +70,7 @@ namespace Flow.Launcher.Plugin.Url
}
catch(Exception)
{
context.API.ShowMsg(string.Format(context.API.GetTranslation("flowlauncher_plugin_url_cannot_open_url"), raw));
context.API.ShowMsgError(string.Format(context.API.GetTranslation("flowlauncher_plugin_url_cannot_open_url"), raw));
return false;
}
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{403B57F2-1856-4FC7-8A24-36AB346B763E}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder>
<UseWPF>true</UseWPF>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>

View file

@ -11,6 +11,6 @@
<description>Flow Launcher - Quick file search and app launcher for Windows with community-made plugins</description>
</metadata>
<files>
<file src="**\*.*" target="lib\net7.0\" exclude="Flow.Launcher.vshost.exe;Flow.Launcher.vshost.exe.config;Flow.Launcher.vshost.exe.manifest;*.nupkg;Setup.exe;RELEASES"/>
<file src="**\*.*" target="lib\net9.0\" exclude="Flow.Launcher.vshost.exe;Flow.Launcher.vshost.exe.config;Flow.Launcher.vshost.exe.manifest;*.nupkg;Setup.exe;RELEASES"/>
</files>
</package>

View file

@ -99,7 +99,7 @@ function Pack-Squirrel-Installer ($path, $version, $output) {
function Publish-Self-Contained ($p) {
$csproj = Join-Path "$p" "Flow.Launcher/Flow.Launcher.csproj" -Resolve
$profile = Join-Path "$p" "Flow.Launcher/Properties/PublishProfiles/Net7.0-SelfContained.pubxml" -Resolve
$profile = Join-Path "$p" "Flow.Launcher/Properties/PublishProfiles/Net9.0-SelfContained.pubxml" -Resolve
# we call dotnet publish on the main project.
# The other projects should have been built in Release at this point.

View file

@ -76,7 +76,7 @@ deploy:
This build includes new changes from commit:
$(APPVEYOR_REPO_COMMIT_MESSAGE)
See all changes in this early access by going to the [milstones](https://github.com/Flow-Launcher/Flow.Launcher/milestones?sort=title&direction=asc) section and choosing the upcoming milestone.
See all changes in this early access by going to the [milestones](https://github.com/Flow-Launcher/Flow.Launcher/milestones?sort=title&direction=asc) section and choosing the upcoming milestone.
For latest production release visit [here](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest)
Please report any bugs or issues over at the [main repository](https://github.com/Flow-Launcher/Flow.Launcher/issues)'

View file

@ -1,6 +1,6 @@
{
"sdk": {
"version": "7.0.*",
"version": "9.0.*",
"rollForward": "latestPatch"
}
}