diff --git a/.github/workflows/default_plugins.yml b/.github/workflows/default_plugins.yml index 83e830d7541..381044c5147 100644 --- a/.github/workflows/default_plugins.yml +++ b/.github/workflows/default_plugins.yml @@ -10,7 +10,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup .NET uses: actions/setup-dotnet@v5 with: diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 416c75a9ded..0659ae64583 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -20,7 +20,7 @@ jobs: NUGET_CERT_REVOCATION_MODE: offline BUILD_NUMBER: ${{ github.run_number }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set Flow.Launcher.csproj version id: update uses: vers-one/dotnet-project-version-updater@v1.7 @@ -54,28 +54,28 @@ jobs: shell: powershell run: .\Scripts\post_build.ps1 - name: Upload Plugin Nupkg - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Plugin nupkg path: | Output\Release\Flow.Launcher.Plugin.*.nupkg compression-level: 0 - name: Upload Setup - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Flow Installer path: | Output\Packages\Flow-Launcher-*.exe compression-level: 0 - name: Upload Portable Version - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Portable Version path: | Output\Packages\Flow-Launcher-Portable.zip compression-level: 0 - name: Upload Full Nupkg - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Full nupkg path: | @@ -83,7 +83,7 @@ jobs: compression-level: 0 - name: Upload Release Information - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: RELEASES path: | diff --git a/.github/workflows/pr_assignee.yml b/.github/workflows/pr_assignee.yml index 5be603df632..33098672bc5 100644 --- a/.github/workflows/pr_assignee.yml +++ b/.github/workflows/pr_assignee.yml @@ -14,4 +14,4 @@ jobs: runs-on: ubuntu-latest steps: - name: Assign PR to creator - uses: toshimaru/auto-author-assign@v2.1.1 + uses: toshimaru/auto-author-assign@v3.0.1 diff --git a/.github/workflows/release_pr.yml b/.github/workflows/release_pr.yml index 58a877ba3b7..c7e9a90a6a2 100644 --- a/.github/workflows/release_pr.yml +++ b/.github/workflows/release_pr.yml @@ -11,7 +11,7 @@ jobs: update-pr: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index bc6f073c315..9c23db53731 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -3,10 +3,8 @@ using System.Linq; using System.Reflection; using System.Windows; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using Microsoft.Win32; using Squirrel; @@ -17,8 +15,6 @@ public class Portable : IPortable { private static readonly string ClassName = nameof(Portable); - private readonly IPublicAPI API = Ioc.Default.GetRequiredService(); - /// /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish /// @@ -45,13 +41,13 @@ public void DisablePortableMode() #endif IndicateDeletion(DataLocation.PortableDataPath); - API.ShowMsgBox(API.GetTranslation("restartToDisablePortableMode")); + PublicApi.Instance.ShowMsgBox(Localize.restartToDisablePortableMode()); UpdateManager.RestartApp(Constant.ApplicationFileName); } catch (Exception e) { - API.LogException(ClassName, "Error occurred while disabling portable mode", e); + PublicApi.Instance.LogException(ClassName, "Error occurred while disabling portable mode", e); } } @@ -68,13 +64,13 @@ public void EnablePortableMode() #endif IndicateDeletion(DataLocation.RoamingDataPath); - API.ShowMsgBox(API.GetTranslation("restartToEnablePortableMode")); + PublicApi.Instance.ShowMsgBox(Localize.restartToEnablePortableMode()); UpdateManager.RestartApp(Constant.ApplicationFileName); } catch (Exception e) { - API.LogException(ClassName, "Error occurred while enabling portable mode", e); + PublicApi.Instance.LogException(ClassName, "Error occurred while enabling portable mode", e); } } @@ -94,13 +90,13 @@ public void RemoveUninstallerEntry() public void MoveUserDataFolder(string fromLocation, string toLocation) { - FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s)); + FilesFolders.CopyAll(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s)); VerifyUserDataAfterMove(fromLocation, toLocation); } public void VerifyUserDataAfterMove(string fromLocation, string toLocation) { - FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s)); + FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s)); } public void CreateShortcuts() @@ -150,12 +146,12 @@ public void PreStartCleanUpAfterPortabilityUpdate() // delete it and prompt the user to pick the portable data location if (File.Exists(roamingDataDeleteFilePath)) { - FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s)); + FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => PublicApi.Instance.ShowMsgBox(s)); - if (API.ShowMsgBox(API.GetTranslation("moveToDifferentLocation"), + if (PublicApi.Instance.ShowMsgBox(Localize.moveToDifferentLocation(), string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) { - FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s)); + FilesFolders.OpenPath(Constant.RootDirectory, (s) => PublicApi.Instance.ShowMsgBox(s)); Environment.Exit(0); } @@ -164,9 +160,9 @@ public void PreStartCleanUpAfterPortabilityUpdate() // delete it and notify the user about it. else if (File.Exists(portableDataDeleteFilePath)) { - FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s)); + FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => PublicApi.Instance.ShowMsgBox(s)); - API.ShowMsgBox(API.GetTranslation("shortcutsUninstallerCreated")); + PublicApi.Instance.ShowMsgBox(Localize.shortcutsUninstallerCreated()); } } @@ -177,8 +173,7 @@ public bool CanUpdatePortability() if (roamingLocationExists && portableLocationExists) { - API.ShowMsgBox(string.Format(API.GetTranslation("userDataDuplicated"), - DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); + PublicApi.Instance.ShowMsgBox(Localize.userDataDuplicated(DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); return false; } diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs index 841099dd1e2..7c0290b2a53 100644 --- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs @@ -8,7 +8,6 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Plugin; @@ -18,13 +17,9 @@ public record CommunityPluginSource(string ManifestFileUrl) { private static readonly string ClassName = nameof(CommunityPluginSource); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - private string latestEtag = ""; - private List plugins = new(); + private List plugins = []; private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new() { @@ -41,7 +36,7 @@ public record CommunityPluginSource(string ManifestFileUrl) /// public async Task> FetchAsync(CancellationToken token) { - API.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}"); + PublicApi.Instance.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}"); var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl); @@ -59,40 +54,40 @@ public async Task> FetchAsync(CancellationToken token) .ConfigureAwait(false); latestEtag = response.Headers.ETag?.Tag; - API.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}"); + PublicApi.Instance.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}"); return plugins; } else if (response.StatusCode == HttpStatusCode.NotModified) { - API.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified."); + PublicApi.Instance.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified."); return plugins; } else { - API.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}"); + PublicApi.Instance.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}"); return null; } } catch (OperationCanceledException) when (token.IsCancellationRequested) { - API.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller."); + PublicApi.Instance.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller."); return null; } catch (TaskCanceledException) { // Likely an HttpClient timeout or external cancellation not requested by our token - API.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out."); + PublicApi.Instance.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out."); return null; } catch (Exception e) { if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException) { - API.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e); + PublicApi.Instance.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e); } else { - API.LogException(ClassName, "Error Occurred", e); + PublicApi.Instance.LogException(ClassName, "Error Occurred", e); } return null; } diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 14796a87a93..1a324a9930a 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Windows; using System.Windows.Forms; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -15,7 +14,7 @@ public abstract class AbstractPluginEnvironment { private static readonly string ClassName = nameof(AbstractPluginEnvironment); - protected readonly IPublicAPI API = Ioc.Default.GetRequiredService(); + protected readonly IPublicAPI API = PublicApi.Instance; internal abstract string Language { get; } @@ -58,15 +57,10 @@ internal IEnumerable Setup() return SetPathForPluginPairs(PluginsSettingsFilePath, Language); } - var noRuntimeMessage = string.Format( - API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"), - Language, - EnvName, - Environment.NewLine - ); + var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine); if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) { - var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName); + var msg = Localize.runtimePluginChooseRuntimeExecutable(EnvName); var selectedFile = GetFileFromDialog(msg, FileDialogFilter); @@ -77,12 +71,7 @@ internal IEnumerable Setup() // Nothing selected because user pressed cancel from the file dialog window else { - var forceDownloadMessage = string.Format( - API.GetTranslation("runtimeExecutableInvalidChooseDownload"), - Language, - EnvName, - Environment.NewLine - ); + var forceDownloadMessage = Localize.runtimeExecutableInvalidChooseDownload(Language, EnvName, Environment.NewLine); // Let users select valid path or choose to download while (string.IsNullOrEmpty(selectedFile)) @@ -120,7 +109,7 @@ internal IEnumerable Setup() } else { - API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language)); + API.ShowMsgBox(Localize.runtimePluginUnableToSetExecutablePath(Language)); API.LogError(ClassName, $"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.", $"{Language}Environment"); @@ -248,7 +237,7 @@ private static bool IsUsingRoamingPath(string filePath) private static string GetUpdatedEnvironmentPath(string filePath) { var index = filePath.IndexOf(DataLocation.PluginEnvironments); - + // get the substring after "Environments" because we can not determine it dynamically var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..]; return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}"; diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs index 89286dfb0f9..76c775fb47b 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -51,7 +51,7 @@ internal override void InstallEnvironment() } catch (System.Exception e) { - API.ShowMsgError(API.GetTranslation("failToInstallPythonEnv")); + API.ShowMsgError(Localize.failToInstallPythonEnv()); API.LogException(ClassName, "Failed to install Python environment", e); } }); diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs index 724ae20f46c..d8244cbf3d1 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -46,7 +46,7 @@ internal override void InstallEnvironment() } catch (System.Exception e) { - API.ShowMsgError(API.GetTranslation("failToInstallTypeScriptEnv")); + API.ShowMsgError(Localize.failToInstallTypeScriptEnv()); API.LogException(ClassName, "Failed to install TypeScript environment", e); } }); diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs index 6a32664a13a..e2de53e39d1 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -46,7 +46,7 @@ internal override void InstallEnvironment() } catch (System.Exception e) { - API.ShowMsgError(API.GetTranslation("failToInstallTypeScriptEnv")); + API.ShowMsgError(Localize.failToInstallTypeScriptEnv()); API.LogException(ClassName, "Failed to install TypeScript environment", e); } }); diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index 7d3d78ef0e5..4fed10d25ff 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Plugin; using Flow.Launcher.Infrastructure; @@ -23,10 +22,6 @@ public static class PluginsManifest 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(); - public static List UserPlugins { get; private set; } public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) @@ -67,7 +62,7 @@ public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = fals } catch (Exception e) { - API.LogException(ClassName, "Http request failed", e); + PublicApi.Instance.LogException(ClassName, "Http request failed", e); } finally { @@ -90,12 +85,12 @@ private static bool IsMinimumAppVersionSatisfied(UserPlugin plugin, SemanticVers } catch (Exception e) { - API.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. " + PublicApi.Instance.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. " + "Plugin excluded from manifest", e); return false; } - API.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, " + PublicApi.Instance.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, " + $"but current version is {Constant.Version}. Plugin excluded from manifest."); return false; diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 540eabbf0aa..7bf90ea5165 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -1,4 +1,4 @@ - + net9.0-windows @@ -34,6 +34,7 @@ prompt 4 false + $(NoWarn);FLSG0007 @@ -55,6 +56,7 @@ + @@ -63,6 +65,17 @@ + + + true + + + + + + Languages\en.xaml + + diff --git a/Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs b/Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs new file mode 100644 index 00000000000..1da04bf01a6 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs @@ -0,0 +1,12 @@ +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin; + +public interface IResultUpdateRegister +{ + /// + /// Register a plugin to receive results updated event. + /// + /// + void RegisterResultsUpdatedEvent(PluginPair pair); +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index 9212dada6de..abefd47bcb4 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -285,7 +285,7 @@ public Control CreateSettingPanel() HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center, Margin = SettingPanelItemLeftMargin, - Content = API.GetTranslation("select") + Content = Localize.select() }; Btn.Click += (_, _) => diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index 148fd969e49..47001914343 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -100,11 +100,11 @@ private void SetupJsonRPC() RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API)); - RPC.AddLocalRpcMethod("UpdateResults", new Action((rawQuery, response) => + RPC.AddLocalRpcMethod("UpdateResults", new Action((trimmedQuery, response) => { var results = ParseResults(response); ResultsUpdated?.Invoke(this, - new ResultUpdatedEventArgs { Query = new Query() { RawQuery = rawQuery }, Results = results }); + new ResultUpdatedEventArgs { Query = new Query() { TrimmedQuery = trimmedQuery }, Results = results }); })); RPC.SynchronizationContext = null; RPC.StartListening(); diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs index 4313a51af49..db6813deb6b 100644 --- a/Flow.Launcher.Core/Plugin/PluginConfig.cs +++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.IO; @@ -6,7 +6,7 @@ using Flow.Launcher.Plugin; using System.Text.Json; using Flow.Launcher.Infrastructure.UserSettings; -using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Core.Plugin { @@ -14,10 +14,6 @@ internal abstract class PluginConfig { private static readonly string ClassName = nameof(PluginConfig); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - /// /// Parse plugin metadata in the given directories /// @@ -35,11 +31,22 @@ public static List Parse(string[] pluginDirectories) { try { - Directory.Delete(directory, true); + var fullyDeleted = FilesFolders.TryDeleteDirectoryRobust(directory, maxRetries: 3, retryDelayMs: 200); + if (!fullyDeleted) + { + PublicApi.Instance.LogWarn(ClassName, $"Directory <{directory}> was not fully deleted."); + + // Directory was not fully deleted, recreate the marker file so deletion will be retried on next startup + var markerFilePath = Path.Combine(directory, DataLocation.PluginDeleteFile); + if (!File.Exists(markerFilePath)) + { + File.WriteAllText(markerFilePath, string.Empty); + } + } } catch (Exception e) { - API.LogException(ClassName, $"Can't delete <{directory}>", e); + PublicApi.Instance.LogException(ClassName, $"Can't delete <{directory}>", e); } } else @@ -56,7 +63,7 @@ public static List Parse(string[] pluginDirectories) duplicateList .ForEach( - x => API.LogWarn(ClassName, + x => PublicApi.Instance.LogWarn(ClassName, string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " + "not loaded due to version not the highest of the duplicates", x.Name, x.ID, x.Version), @@ -108,7 +115,7 @@ private static PluginMetadata GetPluginMetadata(string pluginDirectory) string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName); if (!File.Exists(configPath)) { - API.LogError(ClassName, $"Didn't find config file <{configPath}>"); + PublicApi.Instance.LogError(ClassName, $"Didn't find config file <{configPath}>"); return null; } @@ -124,19 +131,19 @@ private static PluginMetadata GetPluginMetadata(string pluginDirectory) } catch (Exception e) { - API.LogException(ClassName, $"Invalid json for config <{configPath}>", e); + PublicApi.Instance.LogException(ClassName, $"Invalid json for config <{configPath}>", e); return null; } if (!AllowedLanguage.IsAllowed(metadata.Language)) { - API.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>"); + PublicApi.Instance.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>"); return null; } if (!File.Exists(metadata.ExecuteFilePath)) { - API.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}"); + PublicApi.Instance.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}"); return null; } diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index d01b34ab6bd..6027b712e73 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; @@ -22,10 +22,6 @@ public static class PluginInstaller private static readonly Settings Settings = Ioc.Default.GetRequiredService(); - // 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(); - /// /// Installs a plugin and restarts the application if required by settings. Prompts user for confirmation and handles download if needed. /// @@ -33,18 +29,16 @@ public static class PluginInstaller /// A Task representing the asynchronous install operation. public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin) { - if (API.PluginModified(newPlugin.ID)) + if (PublicApi.Instance.PluginModified(newPlugin.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), newPlugin.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(newPlugin.Name), + Localize.pluginModifiedAlreadyMessage()); return; } - if (API.ShowMsgBox( - string.Format( - API.GetTranslation("InstallPromptSubtitle"), - newPlugin.Name, newPlugin.Author, Environment.NewLine), - API.GetTranslation("InstallPromptTitle"), + if (PublicApi.Instance.ShowMsgBox( + Localize.InstallPromptSubtitle(newPlugin.Name, newPlugin.Author, Environment.NewLine), + Localize.InstallPromptTitle(), button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return; try @@ -61,7 +55,7 @@ public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin) if (!newPlugin.IsFromLocalInstallPath) { await DownloadFileAsync( - $"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}", + $"{Localize.DownloadingPlugin()} {newPlugin.Name}", newPlugin.UrlDownload, filePath, cts); } else @@ -80,7 +74,7 @@ await DownloadFileAsync( throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath); } - if (!API.InstallPlugin(newPlugin, filePath)) + if (!PublicApi.Instance.InstallPlugin(newPlugin, filePath)) { return; } @@ -92,23 +86,20 @@ await DownloadFileAsync( } catch (Exception e) { - API.LogException(ClassName, "Failed to install plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin")); + PublicApi.Instance.LogException(ClassName, "Failed to install plugin", e); + PublicApi.Instance.ShowMsgError(Localize.ErrorInstallingPlugin()); return; // do not restart on failure } if (Settings.AutoRestartAfterChanging) { - API.RestartApp(); + PublicApi.Instance.RestartApp(); } else { - API.ShowMsg( - API.GetTranslation("installbtn"), - string.Format( - API.GetTranslation( - "InstallSuccessNoRestart"), - newPlugin.Name)); + PublicApi.Instance.ShowMsg( + Localize.installbtn(), + Localize.InstallSuccessNoRestart(newPlugin.Name)); } } @@ -133,24 +124,23 @@ public static async Task InstallPluginAndCheckRestartAsync(string filePath) } catch (Exception e) { - API.LogException(ClassName, "Failed to validate zip file", e); - API.ShowMsgError(API.GetTranslation("ZipFileNotHavePluginJson")); + PublicApi.Instance.LogException(ClassName, "Failed to validate zip file", e); + PublicApi.Instance.ShowMsgError(Localize.ZipFileNotHavePluginJson()); return; } - if (API.PluginModified(plugin.ID)) + if (PublicApi.Instance.PluginModified(plugin.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name), + Localize.pluginModifiedAlreadyMessage()); return; } if (Settings.ShowUnknownSourceWarning) { if (!InstallSourceKnown(plugin.Website) - && API.ShowMsgBox(string.Format( - API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine), - API.GetTranslation("InstallFromUnknownSourceTitle"), + && PublicApi.Instance.ShowMsgBox(Localize.InstallFromUnknownSourceSubtitle(Environment.NewLine), + Localize.InstallFromUnknownSourceTitle(), MessageBoxButton.YesNo) == MessageBoxResult.No) return; } @@ -165,51 +155,46 @@ public static async Task InstallPluginAndCheckRestartAsync(string filePath) /// A Task representing the asynchronous uninstall operation. public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin) { - if (API.PluginModified(oldPlugin.ID)) + if (PublicApi.Instance.PluginModified(oldPlugin.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), oldPlugin.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(oldPlugin.Name), + Localize.pluginModifiedAlreadyMessage()); return; } - if (API.ShowMsgBox( - string.Format( - API.GetTranslation("UninstallPromptSubtitle"), - oldPlugin.Name, oldPlugin.Author, Environment.NewLine), - API.GetTranslation("UninstallPromptTitle"), + if (PublicApi.Instance.ShowMsgBox( + Localize.UninstallPromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine), + Localize.UninstallPromptTitle(), button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return; - var removePluginSettings = API.ShowMsgBox( - API.GetTranslation("KeepPluginSettingsSubtitle"), - API.GetTranslation("KeepPluginSettingsTitle"), + var removePluginSettings = PublicApi.Instance.ShowMsgBox( + Localize.KeepPluginSettingsSubtitle(), + Localize.KeepPluginSettingsTitle(), button: MessageBoxButton.YesNo) == MessageBoxResult.No; try { - if (!await API.UninstallPluginAsync(oldPlugin, removePluginSettings)) + if (!await PublicApi.Instance.UninstallPluginAsync(oldPlugin, removePluginSettings)) { return; } } catch (Exception e) { - API.LogException(ClassName, "Failed to uninstall plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin")); + PublicApi.Instance.LogException(ClassName, "Failed to uninstall plugin", e); + PublicApi.Instance.ShowMsgError(Localize.ErrorUninstallingPlugin()); return; // don not restart on failure } if (Settings.AutoRestartAfterChanging) { - API.RestartApp(); + PublicApi.Instance.RestartApp(); } else { - API.ShowMsg( - API.GetTranslation("uninstallbtn"), - string.Format( - API.GetTranslation( - "UninstallSuccessNoRestart"), - oldPlugin.Name)); + PublicApi.Instance.ShowMsg( + Localize.uninstallbtn(), + Localize.UninstallSuccessNoRestart(oldPlugin.Name)); } } @@ -221,11 +206,9 @@ public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldP /// A Task representing the asynchronous update operation. public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin) { - if (API.ShowMsgBox( - string.Format( - API.GetTranslation("UpdatePromptSubtitle"), - oldPlugin.Name, oldPlugin.Author, Environment.NewLine), - API.GetTranslation("UpdatePromptTitle"), + if (PublicApi.Instance.ShowMsgBox( + Localize.UpdatePromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine), + Localize.UpdatePromptTitle(), button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return; try @@ -237,7 +220,7 @@ public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, if (!newPlugin.IsFromLocalInstallPath) { await DownloadFileAsync( - $"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}", + $"{Localize.DownloadingPlugin()} {newPlugin.Name}", newPlugin.UrlDownload, filePath, cts); } else @@ -251,30 +234,27 @@ await DownloadFileAsync( return; } - if (!await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath)) + if (!await PublicApi.Instance.UpdatePluginAsync(oldPlugin, newPlugin, filePath)) { return; } } catch (Exception e) { - API.LogException(ClassName, "Failed to update plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); + PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e); + PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin()); return; // do not restart on failure } if (Settings.AutoRestartAfterChanging) { - API.RestartApp(); + PublicApi.Instance.RestartApp(); } else { - API.ShowMsg( - API.GetTranslation("updatebtn"), - string.Format( - API.GetTranslation( - "UpdateSuccessNoRestart"), - newPlugin.Name)); + PublicApi.Instance.ShowMsg( + Localize.updatebtn(), + Localize.UpdateSuccessNoRestart(newPlugin.Name)); } } @@ -289,17 +269,17 @@ await DownloadFileAsync( public static async Task CheckForPluginUpdatesAsync(Action> updateAllPlugins, bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default) { // Update the plugin manifest - await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token); + await PublicApi.Instance.UpdatePluginManifestAsync(usePrimaryUrlOnly, token); // Get all plugins that can be updated var resultsForUpdate = ( - from existingPlugin in API.GetAllPlugins() - join pluginUpdateSource in API.GetPluginManifest() + from existingPlugin in PublicApi.Instance.GetAllPlugins() + join pluginUpdateSource in PublicApi.Instance.GetPluginManifest() on existingPlugin.Metadata.ID equals pluginUpdateSource.ID where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version, StringComparison.InvariantCulture) < 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest) - && !API.PluginModified(existingPlugin.Metadata.ID) + && !PublicApi.Instance.PluginModified(existingPlugin.Metadata.ID) select new PluginUpdateInfo() { @@ -314,25 +294,25 @@ where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version }).ToList(); // No updates - if (!resultsForUpdate.Any()) + if (resultsForUpdate.Count == 0) { if (!silentUpdate) { - API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle")); + PublicApi.Instance.ShowMsg(Localize.updateNoResultTitle(), Localize.updateNoResultSubtitle()); } return; } // If all plugins are modified, just return - if (resultsForUpdate.All(x => API.PluginModified(x.ID))) + if (resultsForUpdate.All(x => PublicApi.Instance.PluginModified(x.ID))) { return; } // Show message box with button to update all plugins - API.ShowMsgWithButton( - API.GetTranslation("updateAllPluginsTitle"), - API.GetTranslation("updateAllPluginsButtonContent"), + PublicApi.Instance.ShowMsgWithButton( + Localize.updateAllPluginsTitle(), + Localize.updateAllPluginsButtonContent(), () => { updateAllPlugins(resultsForUpdate); @@ -357,7 +337,7 @@ await Task.WhenAll(resultsForUpdate.Select(async plugin => using var cts = new CancellationTokenSource(); await DownloadFileAsync( - $"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}", + $"{Localize.DownloadingPlugin()} {plugin.PluginNewUserPlugin.Name}", plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts); // check if user cancelled download before installing plugin @@ -366,7 +346,7 @@ await DownloadFileAsync( return; } - if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath)) + if (!await PublicApi.Instance.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath)) { return; } @@ -375,8 +355,8 @@ await DownloadFileAsync( } catch (Exception e) { - API.LogException(ClassName, "Failed to update plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); + PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e); + PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin()); } })); @@ -384,13 +364,13 @@ await DownloadFileAsync( if (restart) { - API.RestartApp(); + PublicApi.Instance.RestartApp(); } else { - API.ShowMsg( - API.GetTranslation("updatebtn"), - API.GetTranslation("PluginsUpdateSuccessNoRestart")); + PublicApi.Instance.ShowMsg( + Localize.updatebtn(), + Localize.PluginsUpdateSuccessNoRestart()); } } @@ -412,7 +392,7 @@ private static async Task DownloadFileAsync(string progressBoxTitle, string down if (showProgress) { var exceptionHappened = false; - await API.ShowProgressBoxAsync(progressBoxTitle, + await PublicApi.Instance.ShowProgressBoxAsync(progressBoxTitle, async (reportProgress) => { if (reportProgress == null) @@ -424,18 +404,18 @@ await API.ShowProgressBoxAsync(progressBoxTitle, } else { - await API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false); + await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false); } }, cts.Cancel); // if exception happened while downloading and user does not cancel downloading, // we need to redownload the plugin if (exceptionHappened && (!cts.IsCancellationRequested)) - await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); + await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); } else { - await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); + await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); } } @@ -462,7 +442,7 @@ private static bool InstallSourceKnown(string url) if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost) return false; - return API.GetAllPlugins().Any(x => + return PublicApi.Instance.GetAllPlugins().Any(x => !string.IsNullOrEmpty(x.Metadata.Website) && x.Metadata.Website.StartsWith(constructedUrlPart) ); diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 7bdfc800932..b808e2a7fbd 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -6,8 +6,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins; +using Flow.Launcher.Core.Resource; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; @@ -25,48 +25,36 @@ public static class PluginManager { private static readonly string ClassName = nameof(PluginManager); - public static List AllPlugins { get; private set; } - public static readonly HashSet GlobalPlugins = new(); - public static readonly Dictionary NonGlobalPlugins = new(); - - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + private static readonly ConcurrentDictionary _allLoadedPlugins = []; + private static readonly ConcurrentDictionary _allInitializedPlugins = []; + private static readonly ConcurrentDictionary _initFailedPlugins = []; + private static readonly ConcurrentDictionary _globalPlugins = []; + private static readonly ConcurrentDictionary _nonGlobalPlugins = []; private static PluginsSettings Settings; - private static readonly ConcurrentBag ModifiedPlugins = new(); - - private static IEnumerable _contextMenuPlugins; - private static IEnumerable _homePlugins; - private static IEnumerable _resultUpdatePlugin; - private static IEnumerable _translationPlugins; + private static readonly ConcurrentBag ModifiedPlugins = []; - private static readonly List _dialogJumpExplorerPlugins = new(); - private static readonly List _dialogJumpDialogPlugins = new(); + private static readonly ConcurrentBag _contextMenuPlugins = []; + private static readonly ConcurrentBag _homePlugins = []; + private static readonly ConcurrentBag _translationPlugins = []; + private static readonly ConcurrentBag _externalPreviewPlugins = []; /// /// Directories that will hold Flow Launcher plugin directory /// public static readonly string[] Directories = - { + [ Constant.PreinstalledDirectory, DataLocation.PluginsDirectory - }; + ]; - private static void DeletePythonBinding() - { - const string binding = "flowlauncher.py"; - foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory)) - { - File.Delete(Path.Combine(subDirectory, binding)); - } - } + #region Save & Dispose & Reload Plugin /// /// Save json and ISavable /// public static void Save() { - foreach (var pluginPair in AllPlugins) + foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: false)) { var savable = pluginPair.Plugin as ISavable; try @@ -75,17 +63,18 @@ public static void Save() } catch (Exception e) { - API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e); } } - API.SavePluginSettings(); - API.SavePluginCaches(); + PublicApi.Instance.SavePluginSettings(); + PublicApi.Instance.SavePluginCaches(); } public static async ValueTask DisposePluginsAsync() { - foreach (var pluginPair in AllPlugins) + // Still call dispose for all plugins even if initialization failed, so that we can clean up resources + foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: true)) { await DisposePluginAsync(pluginPair); } @@ -107,55 +96,59 @@ private static async Task DisposePluginAsync(PluginPair pluginPair) } catch (Exception e) { - API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e); } } public static async Task ReloadDataAsync() { - await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch { IReloadable p => Task.Run(p.ReloadData), IAsyncReloadable p => p.ReloadDataAsync(), _ => Task.CompletedTask, - }).ToArray()); + })]); } + #endregion + + #region External Preview + public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, - }).ToArray()); + })]); } public static async Task CloseExternalPreviewAsync() { - await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.ClosePreviewAsync(), _ => Task.CompletedTask, - }).ToArray()); + })]); } public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) { - await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch { IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast), _ => Task.CompletedTask, - }).ToArray()); + })]); } public static bool UseExternalPreview() { - return GetPluginsForInterface().Any(x => !x.Metadata.Disabled); + return GetExternalPreviewPlugins().Any(x => !x.Metadata.Disabled); } public static bool AllowAlwaysPreview() { - var plugin = GetPluginsForInterface().FirstOrDefault(x => !x.Metadata.Disabled); + var plugin = GetExternalPreviewPlugins().FirstOrDefault(x => !x.Metadata.Disabled); if (plugin is null) return false; @@ -163,6 +156,15 @@ public static bool AllowAlwaysPreview() return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview(); } + private static IList GetExternalPreviewPlugins() + { + return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))]; + } + + #endregion + + #region Constructor + static PluginManager() { // validate user directory @@ -171,9 +173,28 @@ static PluginManager() DeletePythonBinding(); } + private static void DeletePythonBinding() + { + const string binding = "flowlauncher.py"; + foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory)) + { + try + { + File.Delete(Path.Combine(subDirectory, binding)); + } + catch (Exception e) + { + PublicApi.Instance.LogDebug(ClassName, $"Failed to delete {binding} in {subDirectory}: {e.Message}"); + } + } + } + + #endregion + + #region Load & Initialize Plugins + /// - /// because InitializePlugins needs API, so LoadPlugins needs to be called first - /// todo happlebao The API should be removed + /// Load plugins from the directories specified in Directories. /// /// public static void LoadPlugins(PluginsSettings settings) @@ -181,33 +202,22 @@ public static void LoadPlugins(PluginsSettings settings) var metadatas = PluginConfig.Parse(Directories); Settings = settings; Settings.UpdatePluginSettings(metadatas); - AllPlugins = PluginsLoader.Plugins(metadatas, Settings); - // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins - UpdatePluginDirectory(metadatas); - - // Initialize plugin enumerable after all plugins are initialized - _contextMenuPlugins = GetPluginsForInterface(); - _homePlugins = GetPluginsForInterface(); - _resultUpdatePlugin = GetPluginsForInterface(); - _translationPlugins = GetPluginsForInterface(); - // Initialize Dialog Jump plugin pairs - foreach (var pair in GetPluginsForInterface()) + // Load plugins + var allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings); + foreach (var plugin in allLoadedPlugins) { - _dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair + if (plugin != null) { - Plugin = (IDialogJumpExplorer)pair.Plugin, - Metadata = pair.Metadata - }); - } - foreach (var pair in GetPluginsForInterface()) - { - _dialogJumpDialogPlugins.Add(new DialogJumpDialogPair - { - Plugin = (IDialogJumpDialog)pair.Plugin, - Metadata = pair.Metadata - }); + if (!_allLoadedPlugins.TryAdd(plugin.Metadata.ID, plugin)) + { + PublicApi.Instance.LogError(ClassName, $"Plugin with ID {plugin.Metadata.ID} already loaded"); + } + } } + + // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins + UpdatePluginDirectory(metadatas); } private static void UpdatePluginDirectory(List metadatas) @@ -218,7 +228,7 @@ private static void UpdatePluginDirectory(List metadatas) { if (string.IsNullOrEmpty(metadata.AssemblyName)) { - API.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}"); + PublicApi.Instance.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}"); continue; // Skip if AssemblyName is not set, which can happen for erroneous plugins } metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName); @@ -228,7 +238,7 @@ private static void UpdatePluginDirectory(List metadatas) { if (string.IsNullOrEmpty(metadata.Name)) { - API.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}"); + PublicApi.Instance.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}"); continue; // Skip if Name is not set, which can happen for erroneous plugins } metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name); @@ -238,106 +248,146 @@ private static void UpdatePluginDirectory(List metadatas) } /// - /// Call initialize for all plugins + /// Initialize all plugins asynchronously. /// + /// The register to register results updated event for each plugin. /// return the list of failed to init plugins or null for none - public static async Task InitializePluginsAsync() + public static async Task InitializePluginsAsync(IResultUpdateRegister register) { - var failedPlugins = new ConcurrentQueue(); - - var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate + var initTasks = _allLoadedPlugins.Select(x => Task.Run(async () => { + var pair = x.Value; + + // Register plugin action keywords so that plugins can be queried in results + RegisterPluginActionKeywords(pair); + try { - var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>", - () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API))); + var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>", + () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, PublicApi.Instance))); pair.Metadata.InitTime += milliseconds; - API.LogInfo(ClassName, + PublicApi.Instance.LogInfo(ClassName, $"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); } catch (Exception e) { - API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e); if (pair.Metadata.Disabled && pair.Metadata.HomeDisabled) { // If this plugin is already disabled, do not show error message again // Or else it will be shown every time - API.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error"); + PublicApi.Instance.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error"); } else { pair.Metadata.Disabled = true; pair.Metadata.HomeDisabled = true; - failedPlugins.Enqueue(pair); - API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed"); + PublicApi.Instance.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed"); } + + // Even if the plugin cannot be initialized, we still need to add it in all plugin list so that + // we can remove the plugin from Plugin or Store page or Plugin Manager plugin. + _allInitializedPlugins.TryAdd(pair.Metadata.ID, pair); + _initFailedPlugins.TryAdd(pair.Metadata.ID, pair); + return; } + + // Register ResultsUpdated event so that plugin query can use results updated interface + register.RegisterResultsUpdatedEvent(pair); + + // Update plugin metadata translation after the plugin is initialized with IPublicAPI instance + Internationalization.UpdatePluginMetadataTranslation(pair); + + // Add plugin to Dialog Jump plugin list after the plugin is initialized + DialogJump.InitializeDialogJumpPlugin(pair); + + // Add plugin to lists after the plugin is initialized + AddPluginToLists(pair); })); - await Task.WhenAll(InitTasks); + await Task.WhenAll(initTasks); - foreach (var plugin in AllPlugins) + if (!_initFailedPlugins.IsEmpty) { - // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin - // has multiple global and action keywords because we will only add them here once. - foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct()) + var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name)); + PublicApi.Instance.ShowMsg( + Localize.failedToInitializePluginsTitle(), + Localize.failedToInitializePluginsMessage(failed), + "", + false + ); + } + } + + private static void RegisterPluginActionKeywords(PluginPair pair) + { + // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin + // has multiple global and action keywords because we will only add them here once. + foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct()) + { + switch (actionKeyword) { - switch (actionKeyword) - { - case Query.GlobalPluginWildcardSign: - GlobalPlugins.Add(plugin); - break; - default: - NonGlobalPlugins[actionKeyword] = plugin; - break; - } + case Query.GlobalPluginWildcardSign: + _globalPlugins.TryAdd(pair.Metadata.ID, pair); + break; + default: + _nonGlobalPlugins.TryAdd(actionKeyword, pair); + break; } } + } - if (failedPlugins.Any()) + private static void AddPluginToLists(PluginPair pair) + { + if (pair.Plugin is IContextMenu) { - var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); - API.ShowMsg( - API.GetTranslation("failedToInitializePluginsTitle"), - string.Format( - API.GetTranslation("failedToInitializePluginsMessage"), - failed - ), - "", - false - ); + _contextMenuPlugins.Add(pair); + } + if (pair.Plugin is IAsyncHomeQuery) + { + _homePlugins.Add(pair); + } + if (pair.Plugin is IPluginI18n) + { + _translationPlugins.Add(pair); + } + if (pair.Plugin is IAsyncExternalPreview) + { + _externalPreviewPlugins.Add(pair); } + _allInitializedPlugins.TryAdd(pair.Metadata.ID, pair); } + #endregion + + #region Validate & Query Plugins + public static ICollection ValidPluginsForQuery(Query query, bool dialogJump) { if (query is null) return Array.Empty(); - if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) + if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) { if (dialogJump) - return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList(); + return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))]; else - return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))]; } if (dialogJump && plugin.Plugin is not IAsyncDialogJump) return Array.Empty(); - if (API.PluginModified(plugin.Metadata.ID)) + if (PluginModified(plugin.Metadata.ID)) return Array.Empty(); - return new List - { - plugin - }; + return [plugin]; } public static ICollection ValidPluginsForHomeQuery() { - return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + return [.. _homePlugins.Where(p => !PluginModified(p.Metadata.ID))]; } public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) @@ -345,9 +395,31 @@ public static async Task> QueryForPluginAsync(PluginPair pair, Quer var results = new List(); var metadata = pair.Metadata; + if (IsPluginInitializing(metadata)) + { + Result r = new() + { + Title = Localize.pluginStillInitializing(metadata.Name), + SubTitle = Localize.pluginStillInitializingSubtitle(), + AutoCompleteText = query.TrimmedQuery, + IcoPath = metadata.IcoPath, + PluginDirectory = metadata.PluginDirectory, + ActionKeywordAssigned = query.ActionKeyword, + PluginID = metadata.ID, + OriginQuery = query, + Action = _ => + { + PublicApi.Instance.ReQuery(); + return false; + } + }; + results.Add(r); + return results; + } + try { - var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", + var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false)); token.ThrowIfCancellationRequested(); @@ -369,15 +441,15 @@ public static async Task> QueryForPluginAsync(PluginPair pair, Quer { Result r = new() { - Title = $"{metadata.Name}: Failed to respond!", - SubTitle = "Select this result for more info", + Title = Localize.pluginFailedToRespond(metadata.Name), + SubTitle = Localize.pluginFailedToRespondSubtitle(), + AutoCompleteText = query.TrimmedQuery, IcoPath = Constant.ErrorIcon, PluginDirectory = metadata.PluginDirectory, ActionKeywordAssigned = query.ActionKeyword, PluginID = metadata.ID, OriginQuery = query, - Action = _ => { throw new FlowPluginException(metadata, e);}, - Score = -100 + Action = _ => { throw new FlowPluginException(metadata, e);} }; results.Add(r); } @@ -389,9 +461,31 @@ public static async Task> QueryHomeForPluginAsync(PluginPair pair, var results = new List(); var metadata = pair.Metadata; + if (IsPluginInitializing(metadata)) + { + Result r = new() + { + Title = Localize.pluginStillInitializing(metadata.Name), + SubTitle = Localize.pluginStillInitializingSubtitle(), + AutoCompleteText = query.TrimmedQuery, + IcoPath = metadata.IcoPath, + PluginDirectory = metadata.PluginDirectory, + ActionKeywordAssigned = query.ActionKeyword, + PluginID = metadata.ID, + OriginQuery = query, + Action = _ => + { + PublicApi.Instance.ReQuery(); + return false; + } + }; + results.Add(r); + return results; + } + try { - var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", + var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", async () => results = await ((IAsyncHomeQuery)pair.Plugin).HomeQueryAsync(token).ConfigureAwait(false)); token.ThrowIfCancellationRequested(); @@ -408,7 +502,7 @@ public static async Task> QueryHomeForPluginAsync(PluginPair pair, } catch (Exception e) { - API.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e); return null; } return results; @@ -419,9 +513,15 @@ public static async Task> QueryDialogJumpForPluginAsync(P var results = new List(); var metadata = pair.Metadata; + if (IsPluginInitializing(metadata)) + { + // null will be fine since the results will only be added into queue if the token hasn't been cancelled + return null; + } + try { - var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", + var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false)); token.ThrowIfCancellationRequested(); @@ -438,12 +538,58 @@ public static async Task> QueryDialogJumpForPluginAsync(P } catch (Exception e) { - API.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e); return null; } return results; } + private static bool IsPluginInitializing(PluginMetadata metadata) + { + return !_allInitializedPlugins.ContainsKey(metadata.ID); + } + + #endregion + + #region Get Plugin List + + public static List GetAllLoadedPlugins() + { + return [.. _allLoadedPlugins.Values]; + } + + public static List GetAllInitializedPlugins(bool includeFailed) + { + if (includeFailed) + { + return [.. _allInitializedPlugins.Values]; + } + else + { + return [.. _allInitializedPlugins.Values + .Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))]; + } + } + + private static List GetGlobalPlugins() + { + return [.. _globalPlugins.Values]; + } + + public static Dictionary GetNonGlobalPlugins() + { + return _nonGlobalPlugins.ToDictionary(); + } + + public static List GetTranslationPlugins() + { + return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))]; + } + + #endregion + + #region Update Metadata & Get Plugin + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) @@ -462,28 +608,19 @@ public static void UpdatePluginMetadata(IReadOnlyList results, PluginMet /// /// get specified plugin, return null if not found /// + /// + /// Plugin may not be initialized, so do not use its plugin model to execute any commands + /// /// /// public static PluginPair GetPluginForId(string id) { - return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id); + return GetAllLoadedPlugins().FirstOrDefault(o => o.Metadata.ID == id); } - private static IEnumerable GetPluginsForInterface() where T : IFeatures - { - // Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup - return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty(); - } + #endregion - public static IList GetResultUpdatePlugin() - { - return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList(); - } - - public static IList GetTranslationPlugins() - { - return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); - } + #region Get Context Menus public static List GetContextMenusForPlugin(Result result) { @@ -505,7 +642,7 @@ public static List GetContextMenusForPlugin(Result result) } catch (Exception e) { - API.LogException(ClassName, + PublicApi.Instance.LogException(ClassName, $"Can't load context menus for plugin <{pluginPair.Metadata.Name}>", e); } @@ -514,27 +651,82 @@ public static List GetContextMenusForPlugin(Result result) return results; } + #endregion + + #region Check Home Plugin + public static bool IsHomePlugin(string id) { return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id); } - public static IList GetDialogJumpExplorers() + #endregion + + #region Check Initializing & Init Failed + + public static bool IsInitializingOrInitFailed(string id) + { + // Id does not exist in loaded plugins + if (!_allLoadedPlugins.ContainsKey(id)) return false; + + // Plugin initialized already + if (_allInitializedPlugins.ContainsKey(id)) + { + // Check if the plugin initialization failed + return _initFailedPlugins.ContainsKey(id); + } + // Plugin is still initializing + else + { + return true; + } + } + + public static bool IsInitializing(string id) { - return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + // Id does not exist in loaded plugins + if (!_allLoadedPlugins.ContainsKey(id)) return false; + + // Plugin initialized already + if (_allInitializedPlugins.ContainsKey(id)) + { + return false; + } + // Plugin is still initializing + else + { + return true; + } } - public static IList GetDialogJumpDialogs() + public static bool IsInitializationFailed(string id) { - return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + // Id does not exist in loaded plugins + if (!_allLoadedPlugins.ContainsKey(id)) return false; + + // Plugin initialized already + if (_allInitializedPlugins.ContainsKey(id)) + { + // Check if the plugin initialization failed + return _initFailedPlugins.ContainsKey(id); + } + // Plugin is still initializing + else + { + return false; + } } + #endregion + + #region Plugin Action Keyword + public static bool ActionKeywordRegistered(string actionKeyword) { // this method is only checking for action keywords (defined as not '*') registration // hence the actionKeyword != Query.GlobalPluginWildcardSign logic return actionKeyword != Query.GlobalPluginWildcardSign - && NonGlobalPlugins.ContainsKey(actionKeyword); + && _nonGlobalPlugins.ContainsKey(actionKeyword); } /// @@ -546,11 +738,11 @@ public static void AddActionKeyword(string id, string newActionKeyword) var plugin = GetPluginForId(id); if (newActionKeyword == Query.GlobalPluginWildcardSign) { - GlobalPlugins.Add(plugin); + _globalPlugins.TryAdd(id, plugin); } else { - NonGlobalPlugins[newActionKeyword] = plugin; + _nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin); } // Update action keywords and action keyword in plugin metadata @@ -577,11 +769,13 @@ public static void RemoveActionKeyword(string id, string oldActionkeyword) plugin.Metadata.ActionKeywords .Count(x => x == Query.GlobalPluginWildcardSign) == 1) { - GlobalPlugins.Remove(plugin); + _globalPlugins.TryRemove(id, out _); } if (oldActionkeyword != Query.GlobalPluginWildcardSign) - NonGlobalPlugins.Remove(oldActionkeyword); + { + _nonGlobalPlugins.TryRemove(oldActionkeyword, out _); + } // Update action keywords and action keyword in plugin metadata plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); @@ -595,6 +789,12 @@ public static void RemoveActionKeyword(string id, string oldActionkeyword) } } + #endregion + + #region Plugin Install & Uninstall & Update + + #region Private Functions + private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath) { var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length; @@ -620,12 +820,15 @@ private static bool SameOrLesserPluginVersionExists(string metadataPath) if (!Version.TryParse(newMetadata.Version, out var newVersion)) return true; // If version is not valid, we assume it is lesser than any existing version - return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID - && Version.TryParse(x.Metadata.Version, out var version) - && newVersion <= version); + // Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists + return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == newMetadata.ID + && Version.TryParse(x.Metadata.Version, out var version) + && newVersion <= version); } - #region Public functions + #endregion + + #region Public Functions public static bool PluginModified(string id) { @@ -636,8 +839,8 @@ public static async Task UpdatePluginAsync(PluginMetadata existingVersion, { if (PluginModified(existingVersion.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), existingVersion.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(existingVersion.Name), + Localize.pluginModifiedAlreadyMessage()); return false; } @@ -663,14 +866,14 @@ public static async Task UninstallPluginAsync(PluginMetadata plugin, bool #endregion - #region Internal functions + #region Internal Functions internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified) { if (checkModified && PluginModified(plugin.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name), + Localize.pluginModifiedAlreadyMessage()); return false; } @@ -689,15 +892,15 @@ internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool c if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath)) { - API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name), - string.Format(API.GetTranslation("fileNotFoundMessage"), pluginFolderPath)); + PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), + Localize.fileNotFoundMessage(pluginFolderPath)); return false; } if (SameOrLesserPluginVersionExists(metadataJsonFilePath)) { - API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name), - API.GetTranslation("pluginExistAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), + Localize.pluginExistAlreadyMessage()); return false; } @@ -726,7 +929,19 @@ internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool c var newPluginPath = Path.Combine(installDirectory, folderName); - FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s)); + FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s)); + + // Check if marker file exists and delete it + try + { + var markerFilePath = Path.Combine(newPluginPath, DataLocation.PluginDeleteFile); + if (File.Exists(markerFilePath)) + File.Delete(markerFilePath); + } + catch (Exception e) + { + PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin marker file in {newPluginPath}", e); + } try { @@ -735,7 +950,7 @@ internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool c } catch (Exception e) { - API.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e); } if (checkModified) @@ -750,8 +965,8 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo { if (checkModified && PluginModified(plugin.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name), + Localize.pluginModifiedAlreadyMessage()); return false; } @@ -760,7 +975,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo // If we want to remove plugin from AllPlugins, // we need to dispose them so that they can release file handles // which can help FL to delete the plugin settings & cache folders successfully - var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID); + var pluginPairs = GetAllInitializedPlugins(includeFailed: true).Where(p => p.Metadata.ID == plugin.ID).ToList(); foreach (var pluginPair in pluginPairs) { await DisposePluginAsync(pluginPair); @@ -770,7 +985,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo if (removePluginSettings) { // For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances - if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable) + if (AllowedLanguage.IsDotNet(plugin.Language) && PublicApi.Instance is IRemovable removable) { removable.RemovePluginSettings(plugin.AssemblyName); removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath); @@ -784,9 +999,9 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo } catch (Exception e) { - API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e); - API.ShowMsgError(API.GetTranslation("failedToRemovePluginSettingsTitle"), - string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); + PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e); + PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginSettingsTitle(), + Localize.failedToRemovePluginSettingsMessage(plugin.Name)); } } @@ -800,17 +1015,27 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo } catch (Exception e) { - API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e); - API.ShowMsgError(API.GetTranslation("failedToRemovePluginCacheTitle"), - string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); + PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e); + PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginCacheTitle(), + Localize.failedToRemovePluginCacheMessage(plugin.Name)); } Settings.RemovePluginSettings(plugin.ID); - AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); - GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID); - var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); + { + _allLoadedPlugins.TryRemove(plugin.ID, out var _); + } + { + _allInitializedPlugins.TryRemove(plugin.ID, out var _); + } + { + _initFailedPlugins.TryRemove(plugin.ID, out var _); + } + { + _globalPlugins.TryRemove(plugin.ID, out var _); + } + var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList(); foreach (var key in keysToRemove) { - NonGlobalPlugins.Remove(key); + _nonGlobalPlugins.TryRemove(key, out var _); } } @@ -826,5 +1051,7 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, boo } #endregion + + #endregion } } diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index e9e5ee367cb..119dd83baa3 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -2,9 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Threading.Tasks; -using System.Windows; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins.Environments; #pragma warning disable IDE0005 using Flow.Launcher.Infrastructure.Logger; @@ -18,10 +15,6 @@ public static class PluginsLoader { private static readonly string ClassName = nameof(PluginsLoader); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - public static List Plugins(List metadatas, PluginsSettings settings) { var dotnetPlugins = DotNetPlugins(metadatas); @@ -55,7 +48,7 @@ public static List Plugins(List metadatas, PluginsSe return plugins; } - private static IEnumerable DotNetPlugins(List source) + private static List DotNetPlugins(List source) { var erroredPlugins = new List(); @@ -64,56 +57,58 @@ private static IEnumerable DotNetPlugins(List source foreach (var metadata in metadatas) { - var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () => - { - Assembly assembly = null; - IAsyncPlugin plugin = null; + var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () => + { + Assembly assembly = null; + IAsyncPlugin plugin = null; - try - { - var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); - assembly = assemblyLoader.LoadAssemblyAndDependencies(); + try + { + var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); + assembly = assemblyLoader.LoadAssemblyAndDependencies(); - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, - typeof(IAsyncPlugin)); + var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, + typeof(IAsyncPlugin)); - plugin = Activator.CreateInstance(type) as IAsyncPlugin; + plugin = Activator.CreateInstance(type) as IAsyncPlugin; - metadata.AssemblyName = assembly.GetName().Name; - } + metadata.AssemblyName = assembly.GetName().Name; + } #if DEBUG - catch (Exception) - { - throw; - } + catch (Exception) + { + throw; + } #else - catch (Exception e) when (assembly == null) - { - Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e); - } - catch (InvalidOperationException e) - { - Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); - } - catch (ReflectionTypeLoadException e) - { - Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); - } - catch (Exception e) - { - Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e); - } + catch (Exception e) when (assembly == null) + { + PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e); + } + catch (InvalidOperationException e) + { + PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); + } + catch (ReflectionTypeLoadException e) + { + PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); + } + catch (Exception e) + { + PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e); + } #endif - if (plugin == null) - { - erroredPlugins.Add(metadata.Name); - return; - } + if (plugin == null) + { + erroredPlugins.Add(metadata.Name); + return; + } + + plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); + }); - plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); - }); metadata.InitTime += milliseconds; + PublicApi.Instance.LogDebug(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>"); } if (erroredPlugins.Count > 0) @@ -121,12 +116,12 @@ private static IEnumerable DotNetPlugins(List source var errorPluginString = string.Join(Environment.NewLine, erroredPlugins); var errorMessage = erroredPlugins.Count > 1 ? - API.GetTranslation("pluginsHaveErrored") : - API.GetTranslation("pluginHasErrored"); + Localize.pluginsHaveErrored(): + Localize.pluginHasErrored(); - API.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + + PublicApi.Instance.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + $"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" + - API.GetTranslation("referToLogs")); + Localize.referToLogs()); } return plugins; diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 25a32a728d3..aac620cce64 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -6,15 +6,16 @@ namespace Flow.Launcher.Core.Plugin { public static class QueryBuilder { - public static Query Build(string text, Dictionary nonGlobalPlugins) + public static Query Build(string originalQuery, string trimmedQuery, Dictionary nonGlobalPlugins) { // home query - if (string.IsNullOrEmpty(text)) + if (string.IsNullOrEmpty(trimmedQuery)) { return new Query() { Search = string.Empty, - RawQuery = string.Empty, + OriginalQuery = string.Empty, + TrimmedQuery = string.Empty, SearchTerms = Array.Empty(), ActionKeyword = string.Empty, IsHomeQuery = true @@ -22,14 +23,13 @@ public static Query Build(string text, Dictionary nonGlobalP } // replace multiple white spaces with one white space - var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries); + var terms = trimmedQuery.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries); if (terms.Length == 0) { // nothing was typed return null; } - var rawQuery = text; string actionKeyword, search; string possibleActionKeyword = terms[0]; string[] searchTerms; @@ -38,21 +38,22 @@ public static Query Build(string text, Dictionary nonGlobalP { // use non global plugin for query actionKeyword = possibleActionKeyword; - search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty; + search = terms.Length > 1 ? trimmedQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty; searchTerms = terms[1..]; } else { // non action keyword actionKeyword = string.Empty; - search = rawQuery.TrimStart(); + search = trimmedQuery.TrimStart(); searchTerms = terms; } return new Query() { Search = search, - RawQuery = rawQuery, + OriginalQuery = originalQuery, + TrimmedQuery = trimmedQuery, SearchTerms = searchTerms, ActionKeyword = actionKeyword, IsHomeQuery = false diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index 983f8b23495..7505dca622e 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; @@ -18,10 +17,6 @@ public class Internationalization : IDisposable { private static readonly string ClassName = nameof(Internationalization); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - private const string Folder = "Languages"; private const string DefaultLanguageCode = "en"; private const string DefaultFile = "en.xaml"; @@ -104,7 +99,7 @@ private void AddFlowLauncherLanguageDirectory() var directory = Path.Combine(Constant.ProgramDirectory, Folder); if (!Directory.Exists(directory)) { - API.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>"); + PublicApi.Instance.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>"); return; } @@ -175,7 +170,7 @@ private static Language GetLanguageByLanguageCode(string languageCode) FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase)); if (language == null) { - API.LogError(ClassName, $"Language code can't be found <{languageCode}>"); + PublicApi.Instance.LogError(ClassName, $"Language code can't be found <{languageCode}>"); return AvailableLanguages.English; } else @@ -208,7 +203,7 @@ private async Task ChangeLanguageAsync(Language language, bool updateMetadata = } catch (Exception e) { - API.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e); + PublicApi.Instance.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e); } finally { @@ -254,7 +249,7 @@ public bool PromptShouldUsePinyin(string languageCodeToSet) // "Do you want to search with pinyin?" string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?"; - if (API.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) + if (PublicApi.Instance.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) return false; return true; @@ -311,7 +306,7 @@ private static string LanguageFile(string folder, string language) } else { - API.LogError(ClassName, $"Language path can't be found <{path}>"); + PublicApi.Instance.LogError(ClassName, $"Language path can't be found <{path}>"); var english = Path.Combine(folder, DefaultFile); if (File.Exists(english)) { @@ -319,7 +314,7 @@ private static string LanguageFile(string folder, string language) } else { - API.LogError(ClassName, $"Default English Language path can't be found <{path}>"); + PublicApi.Instance.LogError(ClassName, $"Default English Language path can't be found <{path}>"); return string.Empty; } } @@ -354,7 +349,7 @@ public static string GetTranslation(string key) } else { - API.LogError(ClassName, $"No Translation for key {key}"); + PublicApi.Instance.LogError(ClassName, $"No Translation for key {key}"); return $"No Translation for key {key}"; } } @@ -377,11 +372,27 @@ public static void UpdatePluginMetadataTranslations() } catch (Exception e) { - API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); + PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); } } } + public static void UpdatePluginMetadataTranslation(PluginPair p) + { + // Update plugin metadata name & description + if (p.Plugin is not IPluginI18n pluginI18N) return; + try + { + p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); + p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); + pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); + } + catch (Exception e) + { + PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); + } + } + #endregion #region IDisposable diff --git a/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs b/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs deleted file mode 100644 index 3e1a19a7686..00000000000 --- a/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.ComponentModel; -using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Plugin; - -namespace Flow.Launcher.Core.Resource -{ - public class LocalizedDescriptionAttribute : DescriptionAttribute - { - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - - private readonly string _resourceKey; - - public LocalizedDescriptionAttribute(string resourceKey) - { - _resourceKey = resourceKey; - } - - public override string Description - { - get - { - string description = API.GetTranslation(_resourceKey); - return string.IsNullOrWhiteSpace(description) ? - string.Format("[[{0}]]", _resourceKey) : description; - } - } - } -} diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index a6e8dc6bf54..c3bb6190f01 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -444,17 +444,27 @@ public bool ChangeTheme(string theme = null) _api.LogError(ClassName, $"Theme <{theme}> path can't be found"); if (theme != Constant.DefaultTheme) { - _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme)); + _api.ShowMsgBox(Localize.theme_load_failure_path_not_exists(theme)); ChangeTheme(Constant.DefaultTheme); } return false; } - catch (XamlParseException) + catch (XamlParseException e) { - _api.LogError(ClassName, $"Theme <{theme}> fail to parse"); + _api.LogException(ClassName, $"Theme <{theme}> fail to parse xaml", e); if (theme != Constant.DefaultTheme) { - _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme)); + _api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme)); + ChangeTheme(Constant.DefaultTheme); + } + return false; + } + catch (Exception e) + { + _api.LogException(ClassName, $"Theme <{theme}> fail to load", e); + if (theme != Constant.DefaultTheme) + { + _api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme)); ChangeTheme(Constant.DefaultTheme); } return false; diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 45275696c1d..1f138e843e8 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -41,8 +41,8 @@ public async Task UpdateAppAsync(bool silentUpdate = true) try { if (!silentUpdate) - _api.ShowMsg(_api.GetTranslation("pleaseWait"), - _api.GetTranslation("update_flowlauncher_update_check")); + _api.ShowMsg(Localize.pleaseWait(), + Localize.update_flowlauncher_update_check()); using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false); @@ -58,13 +58,13 @@ public async Task UpdateAppAsync(bool silentUpdate = true) if (newReleaseVersion <= currentVersion) { if (!silentUpdate) - _api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest")); + _api.ShowMsgBox(Localize.update_flowlauncher_already_on_latest()); return; } if (!silentUpdate) - _api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"), - _api.GetTranslation("update_flowlauncher_updating")); + _api.ShowMsg(Localize.update_flowlauncher_update_found(), + Localize.update_flowlauncher_updating()); await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); @@ -77,10 +77,7 @@ public async Task UpdateAppAsync(bool silentUpdate = true) FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)); if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s))) - _api.ShowMsgBox(string.Format( - _api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), - DataLocation.PortableDataPath, - targetDestination)); + _api.ShowMsgBox(Localize.update_flowlauncher_fail_moving_portable_user_profile_data(DataLocation.PortableDataPath, targetDestination)); } else { @@ -91,7 +88,7 @@ public async Task UpdateAppAsync(bool silentUpdate = true) _api.LogInfo(ClassName, $"Update success:{newVersionTips}"); - if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), + if (_api.ShowMsgBox(newVersionTips, Localize.update_flowlauncher_new_update(), MessageBoxButton.YesNo) == MessageBoxResult.Yes) { UpdateManager.RestartApp(Constant.ApplicationFileName); @@ -111,8 +108,8 @@ public async Task UpdateAppAsync(bool silentUpdate = true) } if (!silentUpdate) - _api.ShowMsgError(_api.GetTranslation("update_flowlauncher_fail"), - _api.GetTranslation("update_flowlauncher_check_connection")); + _api.ShowMsgError(Localize.update_flowlauncher_fail(), + Localize.update_flowlauncher_check_connection()); } finally { @@ -150,9 +147,9 @@ private static async Task GitHubUpdateManagerAsync(string reposit return manager; } - private string NewVersionTips(string version) + private static string NewVersionTips(string version) { - var tips = string.Format(_api.GetTranslation("newVersionTips"), version); + var tips = Localize.newVersionTips(version); return tips; } diff --git a/Flow.Launcher.Core/packages.lock.json b/Flow.Launcher.Core/packages.lock.json index b499a5860c2..ba97f57f3db 100644 --- a/Flow.Launcher.Core/packages.lock.json +++ b/Flow.Launcher.Core/packages.lock.json @@ -11,6 +11,12 @@ "YamlDotNet": "9.1.0" } }, + "Flow.Launcher.Localization": { + "type": "Direct", + "requested": "[0.0.6, )", + "resolved": "0.0.6", + "contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw==" + }, "FSharp.Core": { "type": "Direct", "requested": "[9.0.303, )", @@ -84,6 +90,11 @@ "resolved": "1.1.0", "contentHash": "j/zGAQ9hLbl7JDpeO40DaXvyyNxwQNDwnJEN7eCexn5F9Kid+VKya/Er0rfIv5Zod/32XarkqFP/V6WFHS/UpQ==" }, + "ini-parser": { + "type": "Transitive", + "resolved": "2.5.2", + "contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg==" + }, "InputSimulator": { "type": "Transitive", "resolved": "1.0.4", @@ -1161,6 +1172,7 @@ "Ben.Demystifier": "[0.4.1, )", "BitFaster.Caching": "[2.5.4, )", "CommunityToolkit.Mvvm": "[8.4.0, )", + "Flow.Launcher.Localization": "[0.0.6, )", "Flow.Launcher.Plugin": "[5.0.0, )", "InputSimulator": "[1.0.4, )", "MemoryPack": "[1.21.4, )", @@ -1170,7 +1182,8 @@ "NLog.OutputDebugString": "[6.0.4, )", "SharpVectors.Wpf": "[1.8.5, )", "System.Drawing.Common": "[7.0.0, )", - "ToolGood.Words.Pinyin": "[3.1.0.3, )" + "ToolGood.Words.Pinyin": "[3.1.0.3, )", + "ini-parser": "[2.5.2, )" } }, "flow.launcher.plugin": { diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs index aa2c641cab1..53df05bf259 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -13,6 +13,7 @@ using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Accessibility; +using System.Collections.Concurrent; namespace Flow.Launcher.Infrastructure.DialogJump { @@ -58,21 +59,17 @@ public static class DialogJump private static readonly Settings _settings = Ioc.Default.GetRequiredService(); - // 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(); - private static HWND _mainWindowHandle = HWND.Null; - private static readonly Dictionary _dialogJumpExplorers = new(); + private static readonly ConcurrentDictionary _dialogJumpExplorers = new(); private static DialogJumpExplorerPair _lastExplorer = null; - private static readonly object _lastExplorerLock = new(); + private static readonly Lock _lastExplorerLock = new(); - private static readonly Dictionary _dialogJumpDialogs = new(); + private static readonly ConcurrentDictionary _dialogJumpDialogs = new(); private static IDialogJumpDialogWindow _dialogWindow = null; - private static readonly object _dialogWindowLock = new(); + private static readonly Lock _dialogWindowLock = new(); private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null; private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; @@ -89,8 +86,8 @@ public static class DialogJump private static DispatcherTimer _dragMoveTimer = null; // A list of all file dialog windows that are auto switched already - private static readonly List _autoSwitchedDialogs = new(); - private static readonly object _autoSwitchedDialogsLock = new(); + private static readonly List _autoSwitchedDialogs = []; + private static readonly Lock _autoSwitchedDialogsLock = new(); private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; private static readonly WINEVENTPROC _moveProc = MoveSizeCallBack; @@ -105,22 +102,13 @@ public static class DialogJump #region Initialize & Setup - public static void InitializeDialogJump(IList dialogJumpExplorers, - IList dialogJumpDialogs) + public static void InitializeDialogJump() { if (_initialized) return; - // Initialize Dialog Jump explorers & dialogs - _dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null); - foreach (var explorer in dialogJumpExplorers) - { - _dialogJumpExplorers.Add(explorer, null); - } - _dialogJumpDialogs.Add(WindowsDialogJumpDialog, null); - foreach (var dialog in dialogJumpDialogs) - { - _dialogJumpDialogs.Add(dialog, null); - } + // Initialize preinstalled Dialog Jump explorers & dialogs + _dialogJumpExplorers.TryAdd(WindowsDialogJumpExplorer, null); + _dialogJumpDialogs.TryAdd(WindowsDialogJumpDialog, null); // Initialize main window handle _mainWindowHandle = Win32Helper.GetMainWindowHandle(); @@ -135,6 +123,29 @@ public static void InitializeDialogJump(IList dialogJump _initialized = true; } + public static void InitializeDialogJumpPlugin(PluginPair pair) + { + // Add Dialog Jump explorers & dialogs + if (pair.Plugin is IDialogJumpExplorer explorer) + { + var dialogJumpExplorer = new DialogJumpExplorerPair + { + Plugin = explorer, + Metadata = pair.Metadata + }; + _dialogJumpExplorers.TryAdd(dialogJumpExplorer, null); + } + if (pair.Plugin is IDialogJumpDialog dialog) + { + var dialogJumpDialog = new DialogJumpDialogPair + { + Plugin = dialog, + Metadata = pair.Metadata + }; + _dialogJumpDialogs.TryAdd(dialogJumpDialog, null); + } + } + public static void SetupDialogJump(bool enabled) { if (enabled == _enabled) return; @@ -315,7 +326,7 @@ private static bool RefreshLastExplorer() { foreach (var explorer in _dialogJumpExplorers.Keys) { - if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(explorer.Metadata.ID) || // Plugin is modified explorer.Metadata.Disabled) continue; // Plugin is disabled var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd); @@ -485,6 +496,8 @@ private static async void ForegroundChangeCallback( uint dwmsEventTime ) { + if (hwnd.IsNull) return; + await _foregroundChangeLock.WaitAsync(); try { @@ -493,7 +506,7 @@ uint dwmsEventTime var dialogWindowChanged = false; foreach (var dialog in _dialogJumpDialogs.Keys) { - if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled IDialogJumpDialogWindow dialogWindow; @@ -596,7 +609,7 @@ uint dwmsEventTime { foreach (var explorer in _dialogJumpExplorers.Keys) { - if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(explorer.Metadata.ID) || // Plugin is modified explorer.Metadata.Disabled) continue; // Plugin is disabled var explorerWindow = explorer.Plugin.CheckExplorerWindow(hwnd); @@ -636,6 +649,8 @@ private static void LocationChangeCallback( uint dwmsEventTime ) { + if (hwnd.IsNull) return; + // If the dialog window is moved, update the Dialog Jump window position var dialogWindowExist = false; lock (_dialogWindowLock) @@ -661,6 +676,8 @@ private static void MoveSizeCallBack( uint dwmsEventTime ) { + if (hwnd.IsNull) return; + // If the dialog window is moved or resized, update the Dialog Jump window position if (_dragMoveTimer != null) { @@ -686,6 +703,8 @@ private static void DestroyChangeCallback( uint dwmsEventTime ) { + if (hwnd.IsNull) return; + // If the dialog window is destroyed, set _dialogWindowHandle to null var dialogWindowExist = false; lock (_dialogWindowLock) @@ -717,6 +736,8 @@ private static void HideChangeCallback( uint dwmsEventTime ) { + if (hwnd.IsNull) return; + // If the dialog window is hidden, set _dialogWindowHandle to null var dialogWindowExist = false; lock (_dialogWindowLock) @@ -748,6 +769,8 @@ private static void DialogEndChangeCallback( uint dwmsEventTime ) { + if (hwnd.IsNull) return; + // If the dialog window is ended, set _dialogWindowHandle to null var dialogWindowExist = false; lock (_dialogWindowLock) @@ -887,7 +910,7 @@ private static IDialogJumpDialogWindow GetDialogWindow(HWND hwnd) // Then check all dialog windows foreach (var dialog in _dialogJumpDialogs.Keys) { - if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled var dialogWindow = _dialogJumpDialogs[dialog]; @@ -900,7 +923,7 @@ private static IDialogJumpDialogWindow GetDialogWindow(HWND hwnd) // Finally search for the dialog window again foreach (var dialog in _dialogJumpDialogs.Keys) { - if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled IDialogJumpDialogWindow dialogWindow; @@ -1083,11 +1106,8 @@ public static void Dispose() _navigationLock.Dispose(); // Stop drag move timer - if (_dragMoveTimer != null) - { - _dragMoveTimer.Stop(); - _dragMoveTimer = null; - } + _dragMoveTimer?.Stop(); + _dragMoveTimer = null; } #endregion diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index 1085cc83313..6e2d8684986 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Windows.Win32; namespace Flow.Launcher.Infrastructure { @@ -13,9 +9,10 @@ public static class FileExplorerHelper /// public static string GetActiveExplorerPath() { - var explorerWindow = GetActiveExplorer(); - string locationUrl = explorerWindow?.LocationURL; - return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null; + var explorerPath = DialogJump.DialogJump.GetActiveExplorerPath(); + return !string.IsNullOrEmpty(explorerPath) ? + GetDirectoryPath(new Uri(explorerPath).LocalPath) : + null; } /// @@ -23,74 +20,12 @@ public static string GetActiveExplorerPath() /// private static string GetDirectoryPath(string path) { - if (!path.EndsWith("\\")) + if (!path.EndsWith('\\')) { return path + "\\"; } return path; } - - /// - /// Gets the file explorer that is currently in the foreground - /// - private static dynamic GetActiveExplorer() - { - Type type = Type.GetTypeFromProgID("Shell.Application"); - if (type == null) return null; - dynamic shell = Activator.CreateInstance(type); - if (shell == null) - { - return null; - } - - var explorerWindows = new List(); - var openWindows = shell.Windows(); - for (int i = 0; i < openWindows.Count; i++) - { - var window = openWindows.Item(i); - if (window == null) continue; - - // find the desired window and make sure that it is indeed a file explorer - // we don't want the Internet Explorer or the classic control panel - // ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE" - if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe") - { - explorerWindows.Add(window); - } - } - - if (explorerWindows.Count == 0) return null; - - var zOrders = GetZOrder(explorerWindows); - - return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First; - } - - /// - /// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1. - /// - private static IEnumerable GetZOrder(List hWnds) - { - var z = new int[hWnds.Count]; - for (var i = 0; i < hWnds.Count; i++) z[i] = -1; - - var index = 0; - var numRemaining = hWnds.Count; - PInvoke.EnumWindows((wnd, _) => - { - var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd); - if (searchIndex != -1) - { - z[searchIndex] = index; - numRemaining--; - if (numRemaining == 0) return false; - } - index++; - return true; - }, IntPtr.Zero); - - return z; - } } } diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 5b4eaf89394..4cde3f6e09f 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -34,6 +34,7 @@ prompt 4 false + $(NoWarn);FLSG0007 @@ -56,10 +57,12 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -80,4 +83,15 @@ + + true + + + + + + Languages\en.xaml + + + \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 8afab419bbc..f8c111f369d 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -4,10 +4,8 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; using JetBrains.Annotations; namespace Flow.Launcher.Infrastructure.Http @@ -20,10 +18,6 @@ public static class 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(); - static Http() { // need to be added so it would work on a win10 machine @@ -82,7 +76,7 @@ var userName when string.IsNullOrEmpty(userName) => } catch (UriFormatException e) { - API.ShowMsgError(API.GetTranslation("pleaseTryAgain"), API.GetTranslation("parseProxyFailed")); + PublicApi.Instance.ShowMsgError(Localize.pleaseTryAgain(), Localize.parseProxyFailed()); Log.Exception(ClassName, "Unable to parse Uri", e); } } diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 4ce0df0260d..86f757eb887 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -1,13 +1,14 @@ using System; -using System.Runtime.InteropServices; using System.IO; +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; using System.Windows.Media.Imaging; +using IniParser; using Windows.Win32; using Windows.Win32.Foundation; -using Windows.Win32.UI.Shell; using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.Shell; namespace Flow.Launcher.Infrastructure.Image { @@ -35,9 +36,32 @@ public class WindowsThumbnailProvider private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205; + private const string UrlExtension = ".url"; + + /// + /// Obtains a BitmapSource thumbnail for the specified file. + /// + /// + /// If the file is a Windows URL shortcut (".url"), the method attempts to resolve the shortcut's icon and use that for the thumbnail; otherwise it requests a thumbnail for the file path. The native HBITMAP used to create the BitmapSource is always released to avoid native memory leaks. + /// + /// Path to the file (can be a regular file or a ".url" shortcut). + /// Requested thumbnail width in pixels. + /// Requested thumbnail height in pixels. + /// Thumbnail extraction options (flags) controlling fallback and caching behavior. + /// A BitmapSource representing the requested thumbnail. public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options) { - HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); + HBITMAP hBitmap; + + var extension = Path.GetExtension(fileName); + if (string.Equals(extension, UrlExtension, StringComparison.OrdinalIgnoreCase)) + { + hBitmap = GetHBitmapForUrlFile(fileName, width, height, options); + } + else + { + hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); + } try { @@ -50,6 +74,21 @@ public static BitmapSource GetThumbnail(string fileName, int width, int height, } } + /// + /// Obtains a native HBITMAP for the specified file at the requested size using the Windows Shell image factory. + /// + /// + /// If is and thumbnail extraction fails + /// due to extraction errors or a missing path, the method falls back to requesting an icon (). + /// The returned HBITMAP is a raw GDI handle; the caller is responsible for releasing it (e.g., via DeleteObject) to avoid native memory leaks. + /// + /// Path to the file to thumbnail. + /// Requested thumbnail width in pixels. + /// Requested thumbnail height in pixels. + /// Thumbnail request flags that control behavior (e.g., ThumbnailOnly, IconOnly). + /// An HBITMAP handle containing the image. Caller must free the handle when finished. + /// If creating the shell item fails (HRESULT returned by SHCreateItemFromParsingName). + /// If the shell item does not expose IShellItemImageFactory or if an unexpected error occurs while obtaining the image. private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options) { var retCode = PInvoke.SHCreateItemFromParsingName( @@ -108,5 +147,44 @@ private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, return hBitmap; } + + /// + /// Obtains an HBITMAP for a Windows .url shortcut by resolving its IconFile entry and delegating to GetHBitmap. + /// + /// + /// The method parses the .url file as an INI, looks in the "InternetShortcut" section for the "IconFile" entry, + /// and requests a bitmap for that icon path. If no IconFile is present or any error occurs while reading or + /// resolving the icon, it falls back to requesting a thumbnail for the .url file itself. + /// + /// Path to the .url shortcut file. + /// Requested thumbnail width (pixels). + /// Requested thumbnail height (pixels). + /// ThumbnailOptions flags controlling extraction behavior. + /// An HBITMAP containing the requested image; callers are responsible for freeing the native handle. + private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, int height, ThumbnailOptions options) + { + HBITMAP hBitmap; + + try + { + var parser = new FileIniDataParser(); + var data = parser.ReadFile(fileName); + var urlSection = data["InternetShortcut"]; + + var iconPath = urlSection?["IconFile"]; + if (!File.Exists(iconPath)) + { + // If the IconFile is missing, throw exception to fallback to the default icon + throw new FileNotFoundException("Icon file not specified in Internet shortcut (.url) file."); + } + hBitmap = GetHBitmap(Path.GetFullPath(iconPath), width, height, options); + } + catch + { + hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); + } + + return hBitmap; + } } } diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 09eb98f46be..2a5b826a9d9 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -34,7 +34,7 @@ static Log() var fileTarget = new FileTarget { - FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt", + FileName = CurrentLogDirectory.Replace(@"\", "/") + "/Flow.Launcher.${date:format=yyyy-MM-dd}.log", Layout = layout }; @@ -65,26 +65,22 @@ static Log() public static void SetLogLevel(LOGLEVEL level) { - switch (level) + var rule = LogManager.Configuration.FindRuleByName("file"); + + var nlogLevel = level switch { - case LOGLEVEL.DEBUG: - UseDebugLogLevel(); - break; - default: - UseInfoLogLevel(); - break; - } - Info(nameof(Logger), $"Using log level: {level}."); - } + LOGLEVEL.NONE => LogLevel.Off, + LOGLEVEL.ERROR => LogLevel.Error, + LOGLEVEL.DEBUG => LogLevel.Debug, + _ => LogLevel.Info + }; - private static void UseDebugLogLevel() - { - LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal); - } + rule.SetLoggingLevels(nlogLevel, LogLevel.Fatal); - private static void UseInfoLogLevel() - { - LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal); + LogManager.ReconfigExistingLoggers(); + + // We can't log Info when level is set to Error or None, so we use Debug + Debug(nameof(Logger), $"Using log level: {level}."); } private static void LogFaultyFormat(string message) @@ -169,7 +165,9 @@ public static void Warn(string className, string message, [CallerMemberName] str public enum LOGLEVEL { - DEBUG, - INFO + NONE, + ERROR, + INFO, + DEBUG } } diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index cd072f635f6..8c5633cfef5 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -91,4 +91,6 @@ PBT_APMRESUMEAUTOMATIC PBT_APMRESUMESUSPEND PowerRegisterSuspendResumeNotification PowerUnregisterSuspendResumeNotification -DeviceNotifyCallbackRoutine \ No newline at end of file +DeviceNotifyCallbackRoutine + +MonitorFromWindow \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 1c0cc6872ff..7f55d890992 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -27,7 +27,7 @@ public PinyinAlphabet() { switch (e.PropertyName) { - case nameof (Settings.ShouldUsePinyin): + case nameof(Settings.ShouldUsePinyin): if (_settings.ShouldUsePinyin) { Reload(); @@ -52,7 +52,7 @@ public void Reload() private void CreateDoublePinyinTableFromStream(Stream jsonStream) { - var table = JsonSerializer.Deserialize>>(jsonStream) ?? + var table = JsonSerializer.Deserialize>>(jsonStream) ?? throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null"); var schemaKey = _settings.DoublePinyinSchema.ToString(); @@ -128,12 +128,12 @@ public bool ShouldTranslate(string stringToTranslate) if (IsChineseCharacter(content[i])) { var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i]; - - if (i > 0) + + if (i > 0 && content[i - 1] != ' ') { resultBuilder.Append(' '); } - + map.AddNewIndex(resultBuilder.Length, translated.Length); resultBuilder.Append(translated); previousIsChinese = true; @@ -144,11 +144,14 @@ public bool ShouldTranslate(string stringToTranslate) if (previousIsChinese) { previousIsChinese = false; - resultBuilder.Append(' '); + if (content[i] != ' ') + { + resultBuilder.Append(' '); + } } - - map.AddNewIndex(resultBuilder.Length, resultList[i].Length); - resultBuilder.Append(resultList[i]); + + map.AddNewIndex(resultBuilder.Length, 1); + resultBuilder.Append(content[i]); } } @@ -156,7 +159,7 @@ public bool ShouldTranslate(string stringToTranslate) var translation = resultBuilder.ToString(); var result = (translation, map); - + return _pinyinCache[content] = result; } @@ -185,8 +188,8 @@ private static bool IsChineseCharacter(char c) private string ToDoublePinyin(string fullPinyin) { - return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue) - ? doublePinyinValue + return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue) + ? doublePinyinValue : fullPinyin; } } diff --git a/Flow.Launcher.Infrastructure/TranslationMapping.cs b/Flow.Launcher.Infrastructure/TranslationMapping.cs index b4c6764df1a..e7044307761 100644 --- a/Flow.Launcher.Infrastructure/TranslationMapping.cs +++ b/Flow.Launcher.Infrastructure/TranslationMapping.cs @@ -21,7 +21,7 @@ public void AddNewIndex(int translatedIndex, int length) public int MapToOriginalIndex(int translatedIndex) { var searchResult = _originalToTranslated.BinarySearch(translatedIndex); - return searchResult >= 0 ? searchResult : ~searchResult; + return searchResult >= 0 ? searchResult + 1 : ~searchResult; } public void EndConstruct() diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs index 9c795f952a9..009b27666e8 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs @@ -1,18 +1,13 @@ using System.Text.Json.Serialization; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings { public class CustomBrowserViewModel : BaseModel { - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - public string Name { get; set; } [JsonIgnore] - public string DisplayName => Name == "Default" ? API.GetTranslation("defaultBrowser_default") : Name; + public string DisplayName => Name == "Default" ? Localize.defaultBrowser_default() : Name; public string Path { get; set; } public string PrivateArg { get; set; } public bool EnablePrivate { get; set; } diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs index 2af0bb0e53a..ae406f4c5b6 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs @@ -1,18 +1,13 @@ using System.Text.Json.Serialization; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings { public class CustomExplorerViewModel : BaseModel { - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - public string Name { get; set; } [JsonIgnore] - public string DisplayName => Name == "Explorer" ? API.GetTranslation("fileManagerExplorer") : Name; + public string DisplayName => Name == "Explorer" ? Localize.fileManagerExplorer() : Name; public string Path { get; set; } public string FileArgument { get; set; } = "\"%d\""; public string DirectoryArgument { get; set; } = "\"%d\""; diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs index 2603d46751c..a2e95b668cf 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs @@ -1,8 +1,6 @@ using System; using System.Text.Json.Serialization; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings { @@ -55,11 +53,7 @@ public class BaseBuiltinShortcutModel : ShortcutBaseModel { public string Description { get; set; } - public string LocalizedDescription => API.GetTranslation(Description); - - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + public string LocalizedDescription => PublicApi.Instance.GetTranslation(Description); public BaseBuiltinShortcutModel(string key, string description) { diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index d49cd276a16..37ff59a081b 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text.Json.Serialization; using System.Windows; @@ -480,6 +480,7 @@ public bool HideNotifyIcon } public bool LeaveCmdOpen { get; set; } public bool HideWhenDeactivated { get; set; } = true; + public bool ShowTaskbarWhenInvoked { get; set; } = false; private bool _showAtTopmost = false; public bool ShowAtTopmost diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 8a41e12b43c..635a433540f 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1016,5 +1016,32 @@ protected override bool ReleaseHandle() } #endregion + + #region Taskbar + + public static unsafe void ShowTaskbar() + { + // Find the taskbar window + var taskbarHwnd = PInvoke.FindWindowEx(HWND.Null, HWND.Null, "Shell_TrayWnd", null); + if (taskbarHwnd == HWND.Null) return; + + // Magic from https://github.com/Oliviaophia/SmartTaskbar + const uint TrayBarFlag = 0x05D1; + var mon = PInvoke.MonitorFromWindow(taskbarHwnd, Windows.Win32.Graphics.Gdi.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); + PInvoke.PostMessage(taskbarHwnd, TrayBarFlag, new WPARAM(1), new LPARAM((nint)mon.Value)); + } + + public static void HideTaskbar() + { + // Find the taskbar window + var taskbarHwnd = PInvoke.FindWindowEx(HWND.Null, HWND.Null, "Shell_TrayWnd", null); + if (taskbarHwnd == HWND.Null) return; + + // Magic from https://github.com/Oliviaophia/SmartTaskbar + const uint TrayBarFlag = 0x05D1; + PInvoke.PostMessage(taskbarHwnd, TrayBarFlag, new WPARAM(0), IntPtr.Zero); + } + + #endregion } } diff --git a/Flow.Launcher.Infrastructure/packages.lock.json b/Flow.Launcher.Infrastructure/packages.lock.json index 47c94d5f6ab..db77f9d930f 100644 --- a/Flow.Launcher.Infrastructure/packages.lock.json +++ b/Flow.Launcher.Infrastructure/packages.lock.json @@ -23,12 +23,24 @@ "resolved": "8.4.0", "contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw==" }, + "Flow.Launcher.Localization": { + "type": "Direct", + "requested": "[0.0.6, )", + "resolved": "0.0.6", + "contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw==" + }, "Fody": { "type": "Direct", "requested": "[6.9.3, )", "resolved": "6.9.3", "contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA==" }, + "ini-parser": { + "type": "Direct", + "requested": "[2.5.2, )", + "resolved": "2.5.2", + "contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg==" + }, "InputSimulator": { "type": "Direct", "requested": "[1.0.4, )", diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 649d59ad7b5..378d4306afe 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -15,10 +15,10 @@ - 5.1.0 - 5.1.0 - 5.1.0 - 5.1.0 + 5.2.0 + 5.2.0 + 5.2.0 + 5.2.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -72,9 +72,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index dd44647b42a..93844159f75 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; @@ -173,9 +173,21 @@ public interface IPublicAPI /// /// Get all loaded plugins /// + /// + /// Will also return any plugins not fully initialized yet + /// /// List GetAllPlugins(); + /// + /// Get all initialized plugins + /// + /// + /// Whether to include plugins that failed to initialize + /// + /// + List GetAllInitializedPlugins(bool includeFailed); + /// /// Registers a callback function for global keyboard events. /// diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index f50614699fd..79e6f7d62d6 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { @@ -7,12 +8,30 @@ namespace Flow.Launcher.Plugin /// public class Query { + /// + /// Original query, exactly how the user has typed into the search box. + /// We don't recommend using this property directly. You should always use Search property. + /// + public string OriginalQuery { get; internal init; } + /// /// Raw query, this includes action keyword if it has. - /// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace. - /// We didn't recommend use this property directly. You should always use Search property. + /// It has handled built-in custom query hotkeys and built-in shortcuts, and it trims the whitespace. + /// We don't recommend using this property directly. You should always use Search property. + /// + [Obsolete("RawQuery is renamed to TrimmedQuery. This property will be removed. Update the code to use TrimmedQuery instead.")] + public string RawQuery { + get => TrimmedQuery; + internal init { TrimmedQuery = value; } + } + + /// + /// Original query but with trimmed whitespace. Includes action keyword. + /// It has handled built-in custom query hotkeys and build-in shortcuts. + /// If you need the exact original query from the search box, use OriginalQuery property instead. + /// We don't recommend using this property directly. You should always use Search property. /// - public string RawQuery { get; internal init; } + public string TrimmedQuery { get; internal init; } /// /// Determines whether the query was forced to execute again. @@ -28,7 +47,7 @@ public class Query /// /// Search part of a query. - /// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as RawQuery. + /// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as TrimmedQuery. /// Since we allow user to switch a exclusive plugin to generic plugin, /// so this property will always give you the "real" query part of the query /// @@ -103,6 +122,6 @@ private string SplitSearch(int index) } /// - public override string ToString() => RawQuery; + public override string ToString() => TrimmedQuery; } } diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 3af57f00d53..cd1ddf983a0 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -130,6 +130,119 @@ public static void RemoveFolderIfExists(this string path, Func + /// Attempts to delete a directory robustly with retry logic for locked files. + /// This method tries to delete files individually with retries, then removes empty directories. + /// Returns true if the directory was completely deleted, false if some files/folders remain. + /// + /// The directory path to delete + /// Maximum number of retry attempts for locked files (default: 3) + /// Delay in milliseconds between retries (default: 100ms) + /// True if directory was fully deleted, false if some items remain + public static bool TryDeleteDirectoryRobust(string path, int maxRetries = 3, int retryDelayMs = 100) + { + if (!Directory.Exists(path)) + return true; + + bool fullyDeleted = true; + + try + { + // First, try to delete all files in the directory tree + var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories); + foreach (var file in files) + { + bool fileDeleted = false; + for (int attempt = 0; attempt <= maxRetries; attempt++) + { + try + { + // Remove read-only attribute if present + var fileInfo = new FileInfo(file); + if (fileInfo.Exists && fileInfo.IsReadOnly) + { + fileInfo.IsReadOnly = false; + } + + File.Delete(file); + fileDeleted = true; + break; + } + catch (UnauthorizedAccessException) + { + // File is in use or access denied, wait and retry + if (attempt < maxRetries) + { + System.Threading.Thread.Sleep(retryDelayMs); + } + } + catch (IOException) + { + // File is in use, wait and retry + if (attempt < maxRetries) + { + System.Threading.Thread.Sleep(retryDelayMs); + } + } + catch + { + // Other exceptions, don't retry + break; + } + } + + if (!fileDeleted) + { + fullyDeleted = false; + } + } + + // Then, try to delete all empty directories (from deepest to shallowest) + var directories = Directory.GetDirectories(path, "*", SearchOption.AllDirectories) + .OrderByDescending(d => d.Length) // Delete deeper directories first + .ToArray(); + + foreach (var directory in directories) + { + try + { + if (Directory.Exists(directory) && !Directory.EnumerateFileSystemEntries(directory).Any()) + { + Directory.Delete(directory, false); + } + } + catch + { + // If we can't delete an empty directory, mark as not fully deleted + fullyDeleted = false; + } + } + + // Finally, try to delete the root directory itself + try + { + if (Directory.Exists(path) && !Directory.EnumerateFileSystemEntries(path).Any()) + { + Directory.Delete(path, false); + } + else if (Directory.Exists(path)) + { + fullyDeleted = false; + } + } + catch + { + fullyDeleted = false; + } + } + catch + { + fullyDeleted = false; + } + + return fullyDeleted; + } + /// /// Checks if a directory exists /// diff --git a/Flow.Launcher.Plugin/packages.lock.json b/Flow.Launcher.Plugin/packages.lock.json index 70f71f20d29..7565ec3f430 100644 --- a/Flow.Launcher.Plugin/packages.lock.json +++ b/Flow.Launcher.Plugin/packages.lock.json @@ -16,23 +16,23 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.103, )", + "resolved": "10.0.103", + "contentHash": "qZk7r40ftpZY+/sO019sgWAWfNqC2CLSspDdAxNYCJU/bCi/8jVwvOMjzb/d5gjCRNzQ4OCYgBfhdpQyVwLTyw==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.103", + "Microsoft.SourceLink.Common": "10.0.103" } }, "Microsoft.Windows.CsWin32": { "type": "Direct", - "requested": "[0.3.205, )", - "resolved": "0.3.205", - "contentHash": "U5wGAnyKd7/I2YMd43nogm81VMtjiKzZ9dsLMVI4eAB7jtv5IEj0gprj0q/F3iRmAIaGv5omOf8iSYx2+nE6BQ==", + "requested": "[0.3.269, )", + "resolved": "0.3.269", + "contentHash": "O4GVJ0ymxcoFRGS07VcoEClj7A9PIciHIjWDrPymzonhYlOfM7V0ZqGBUK19cUH3BPca9MfSOH0KLK/9JzQ8+Q==", "dependencies": { "Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha", - "Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview", - "Microsoft.Windows.WDK.Win32Metadata": "0.12.8-experimental" + "Microsoft.Windows.SDK.Win32Metadata": "69.0.7-preview", + "Microsoft.Windows.WDK.Win32Metadata": "0.13.25-experimental" } }, "PropertyChanged.Fody": { @@ -46,13 +46,13 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.103", + "contentHash": "QoiCMcPuxC6eqRQmrmF9zBY96ejIznXtve/lJJbonGD9I5Aygf2AUCOWslGiCEtBbfWRSuUnepBjuuVOdAl5ag==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.103", + "contentHash": "cMtGW5/r0ck72Jg2QwZcNTX59z+iB/B1kW84VMa/eX8L19DhHIuIcQjfK0pgLLBxd60Jl0Bj9GUolcM0MnJnZA==" }, "Microsoft.Windows.SDK.Win32Docs": { "type": "Transitive", @@ -61,15 +61,15 @@ }, "Microsoft.Windows.SDK.Win32Metadata": { "type": "Transitive", - "resolved": "61.0.15-preview", - "contentHash": "cysex3dazKtCPALCluC2XX3f5Aedy9H2pw5jb+TW5uas2rkem1Z7FRnbUrg2vKx0pk0Qz+4EJNr37HdYTEcvEQ==" + "resolved": "69.0.7-preview", + "contentHash": "RJoNjQJVCIDNLPbvYuaygCFknTyAxOUE45of1voj0jjOgJa9MB2m1/G8L8F3IYc+2EFG5aqa/9y8PEx7Tk2tLQ==" }, "Microsoft.Windows.WDK.Win32Metadata": { "type": "Transitive", - "resolved": "0.12.8-experimental", - "contentHash": "3n8R44/Z96Ly+ty4eYVJfESqbzvpw96lRLs3zOzyDmr1x1Kw7FNn5CyE416q+bZQV3e1HRuMUvyegMeRE/WedA==", + "resolved": "0.13.25-experimental", + "contentHash": "IM50tb/+UIwBr9FMr6ZKcZjCMW+Axo6NjGqKxgjUfyCY8dRnYUfrJEXxAaXoWtYP4X8EmASmC1Jtwh4XucseZg==", "dependencies": { - "Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview" + "Microsoft.Windows.SDK.Win32Metadata": "63.0.31-preview" } } } diff --git a/Flow.Launcher.Test/FilesFoldersTest.cs b/Flow.Launcher.Test/FilesFoldersTest.cs index 2621fc2da1f..a63b59c3989 100644 --- a/Flow.Launcher.Test/FilesFoldersTest.cs +++ b/Flow.Launcher.Test/FilesFoldersTest.cs @@ -1,6 +1,7 @@ using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; using NUnit.Framework.Legacy; +using System.IO; namespace Flow.Launcher.Test { @@ -50,5 +51,89 @@ public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedRe { ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult)); } + + [Test] + public void TryDeleteDirectoryRobust_WhenDirectoryDoesNotExist_ReturnsTrue() + { + // Arrange + string nonExistentPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + // Act + bool result = FilesFolders.TryDeleteDirectoryRobust(nonExistentPath); + + // Assert + ClassicAssert.IsTrue(result); + } + + [Test] + public void TryDeleteDirectoryRobust_WhenDirectoryIsEmpty_DeletesSuccessfully() + { + // Arrange + string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDir); + + // Act + bool result = FilesFolders.TryDeleteDirectoryRobust(tempDir); + + // Assert + ClassicAssert.IsTrue(result); + ClassicAssert.IsFalse(Directory.Exists(tempDir)); + } + + [Test] + public void TryDeleteDirectoryRobust_WhenDirectoryHasFiles_DeletesSuccessfully() + { + // Arrange + string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDir); + File.WriteAllText(Path.Combine(tempDir, "test.txt"), "test content"); + + // Act + bool result = FilesFolders.TryDeleteDirectoryRobust(tempDir); + + // Assert + ClassicAssert.IsTrue(result); + ClassicAssert.IsFalse(Directory.Exists(tempDir)); + } + + [Test] + public void TryDeleteDirectoryRobust_WhenDirectoryHasNestedStructure_DeletesSuccessfully() + { + // Arrange + string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDir); + string subDir1 = Path.Combine(tempDir, "SubDir1"); + string subDir2 = Path.Combine(tempDir, "SubDir2"); + Directory.CreateDirectory(subDir1); + Directory.CreateDirectory(subDir2); + File.WriteAllText(Path.Combine(subDir1, "file1.txt"), "content1"); + File.WriteAllText(Path.Combine(subDir2, "file2.txt"), "content2"); + File.WriteAllText(Path.Combine(tempDir, "root.txt"), "root content"); + + // Act + bool result = FilesFolders.TryDeleteDirectoryRobust(tempDir); + + // Assert + ClassicAssert.IsTrue(result); + ClassicAssert.IsFalse(Directory.Exists(tempDir)); + } + + [Test] + public void TryDeleteDirectoryRobust_WhenFileIsReadOnly_RemovesAttributeAndDeletes() + { + // Arrange + string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDir); + string filePath = Path.Combine(tempDir, "readonly.txt"); + File.WriteAllText(filePath, "readonly content"); + File.SetAttributes(filePath, FileAttributes.ReadOnly); + + // Act + bool result = FilesFolders.TryDeleteDirectoryRobust(tempDir); + + // Assert + ClassicAssert.IsTrue(result); + ClassicAssert.IsFalse(Directory.Exists(tempDir)); + } } } diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index b075813dbb6..4e40d3645b0 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -16,14 +16,15 @@ public class CalculatorPluginTest { DecimalSeparator = DecimalSeparator.UseSystemLocale, MaxDecimalPlaces = 10, - ShowErrorMessage = false // Make sure we return the empty results when error occurs + ShowErrorMessage = false, // Make sure we return the empty results when error occurs + UseThousandsSeparator = true // Default value }; private readonly Engine _engine = new(new Configuration { Scope = new Dictionary - { - { "e", Math.E }, // e is not contained in the default mages engine - } + { + { "e", Math.E }, // e is not contained in the default mages engine + } }); public CalculatorPluginTest() @@ -41,6 +42,44 @@ public CalculatorPluginTest() engineField.SetValue(null, _engine); } + [Test] + public void ThousandsSeparatorTest_Enabled() + { + _settings.UseThousandsSeparator = true; + + _settings.DecimalSeparator = DecimalSeparator.Dot; + var result = GetCalculationResult("1000+234"); + // When thousands separator is enabled, the result should contain a separator + // Since decimal separator is dot, thousands separator should be comma + ClassicAssert.AreEqual("1,234", result); + + _settings.DecimalSeparator = DecimalSeparator.Comma; + var result2 = GetCalculationResult("1000+234"); + // When thousands separator is enabled, the result should contain a separator + // Since decimal separator is comma, thousands separator should be dot + ClassicAssert.AreEqual("1.234", result2); + } + + [Test] + public void ThousandsSeparatorTest_Disabled() + { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale; + + var result = GetCalculationResult("1000+234"); + ClassicAssert.AreEqual("1234", result); + } + + [Test] + public void ThousandsSeparatorTest_LargeNumber() + { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale; + + var result = GetCalculationResult("1000000+234567"); + ClassicAssert.AreEqual("1234567", result); + } + // Basic operations [TestCase(@"1+1", "2")] [TestCase(@"2-1", "1")] @@ -77,6 +116,9 @@ public CalculatorPluginTest() [TestCase(@"invalid_expression", "")] public void CalculatorTest(string expression, string result) { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.Dot; + ClassicAssert.AreEqual(GetCalculationResult(expression), result); } diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index c8ac17748da..0ede781f8ab 100644 --- a/Flow.Launcher.Test/QueryBuilderTest.cs +++ b/Flow.Launcher.Test/QueryBuilderTest.cs @@ -16,9 +16,9 @@ public void ExclusivePluginQueryTest() {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}}}} }; - Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); + Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins); - ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.TrimmedQuery); ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); ClassicAssert.AreEqual(">", q.ActionKeyword); @@ -39,10 +39,10 @@ public void ExclusivePluginQueryIgnoreDisabledTest() {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}, Disabled = true}}} }; - Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); + Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins); ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search); - ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); + ClassicAssert.AreEqual(q.Search, q.TrimmedQuery, "TrimmedQuery should be equal to Search."); ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); @@ -51,7 +51,7 @@ public void ExclusivePluginQueryIgnoreDisabledTest() [Test] public void GenericPluginQueryTest() { - Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary()); + Query q = QueryBuilder.Build("file.txt file2 file3", "file.txt file2 file3", new Dictionary()); ClassicAssert.AreEqual("file.txt file2 file3", q.Search); ClassicAssert.AreEqual("", q.ActionKeyword); diff --git a/Flow.Launcher.Test/TranslationMappingTest.cs b/Flow.Launcher.Test/TranslationMappingTest.cs index bd3636f0ad8..a3c0026c03d 100644 --- a/Flow.Launcher.Test/TranslationMappingTest.cs +++ b/Flow.Launcher.Test/TranslationMappingTest.cs @@ -22,19 +22,33 @@ public void AddNewIndex_ShouldAddTranslatedIndexPlusLength() ClassicAssert.AreEqual(10, GetOriginalToTranslatedAt(mapping, 1)); } - [TestCase(0, 0)] - [TestCase(2, 1)] - [TestCase(3, 1)] - [TestCase(5, 2)] - [TestCase(6, 2)] + + [TestCase(0, 0)] // "F" -> "F" + [TestCase(1, 1)] // "l" -> "l" + [TestCase(2, 2)] // "o" -> "o" + [TestCase(3, 3)] // "w" -> "w" + [TestCase(4, 4)] // " " -> " " + [TestCase(5, 5)] // "Y" (translated from "用") -> original index 5 + [TestCase(6, 5)] // "o" (translated from "用") -> original index 5 + [TestCase(7, 5)] // "n" (translated from "用") -> original index 5 + [TestCase(8, 5)] // "g" (translated from "用") -> original index 5 + [TestCase(10, 6)] // "H" (translated from "户") -> original index 6 + [TestCase(11, 6)] // "u" (translated from "户") -> original index 6 public void MapToOriginalIndex_ShouldReturnExpectedIndex(int translatedIndex, int expectedOriginalIndex) { var mapping = new TranslationMapping(); - // a测试 - // a Ce Shi - mapping.AddNewIndex(0, 1); - mapping.AddNewIndex(2, 2); - mapping.AddNewIndex(5, 3); + // Test case : + // 0123456 + // Flow 用户 + // 012345678901 + // Flow Yong Hu + mapping.AddNewIndex(0, 1); // F + mapping.AddNewIndex(1, 1); // l + mapping.AddNewIndex(2, 1); // o + mapping.AddNewIndex(3, 1); // w + mapping.AddNewIndex(4, 1); // ' ' + mapping.AddNewIndex(5, 4); // 用 -> Yong + mapping.AddNewIndex(10, 2); // 户 -> Hu var result = mapping.MapToOriginalIndex(translatedIndex); ClassicAssert.AreEqual(expectedOriginalIndex, result); diff --git a/Flow.Launcher/ActionKeywords.xaml.cs b/Flow.Launcher/ActionKeywords.xaml.cs index 8e05686c982..a94b265fc49 100644 --- a/Flow.Launcher/ActionKeywords.xaml.cs +++ b/Flow.Launcher/ActionKeywords.xaml.cs @@ -47,7 +47,7 @@ private void btnDone_OnClick(object sender, RoutedEventArgs _) if (addedActionKeywords.Any(App.API.ActionKeywordAssigned)) { - App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned")); + App.API.ShowMsgBox(Localize.newActionKeywordsHasBeenAssigned()); return; } @@ -63,7 +63,7 @@ private void btnDone_OnClick(object sender, RoutedEventArgs _) if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords)) { // User just changes the sequence of action keywords - App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld")); + App.API.ShowMsgBox(Localize.newActionKeywordsSameAsOld()); } else { diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index 565bbe3c74c..e922cd55842 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -2,7 +2,8 @@ x:Class="Flow.Launcher.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:ui="http://schemas.modernwpf.com/2019" + xmlns:sys="clr-namespace:System;assembly=mscorlib" + xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" ShutdownMode="OnMainWindowClose" Startup="OnStartup"> @@ -10,17 +11,17 @@ - + - + - + @@ -33,6 +34,15 @@ + + + 2 + 0 + 0 + 40 + 0 + 36 + \ No newline at end of file diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 8ec11e5ffb8..b45bbc5494a 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -22,6 +22,7 @@ using Flow.Launcher.Plugin; using Flow.Launcher.SettingPages.ViewModels; using Flow.Launcher.ViewModel; +using iNKORE.UI.WPF.Modern.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.VisualStudio.Threading; @@ -56,6 +57,9 @@ public partial class App : IDisposable, ISingleInstanceApp public App() { + // Do not use bitmap cache since it can cause WPF second window freezing issue + ShadowAssist.UseBitmapCache = false; + // Initialize settings _settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled(); @@ -183,12 +187,14 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // So set to OnExplicitShutdown to prevent the application from shutting down before main window is created Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; + // Setup log level before any logging is done Log.SetLogLevel(_settings.LogLevel); // Update dynamic resources base on settings Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont); Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont); + // Initialize notification system before any notification api is called Notification.Install(); // Enable Win32 dark mode if the system is in dark mode before creating all windows @@ -197,6 +203,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // Initialize language before portable clean up since it needs translations await _internationalization.InitializeLanguageAsync(); + // Clean up after portability update Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate(); API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------"); @@ -206,32 +213,25 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => RegisterDispatcherUnhandledException(); RegisterTaskSchedulerUnhandledException(); - var imageLoadertask = ImageLoader.InitializeAsync(); - - AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); - - PluginManager.LoadPlugins(_settings.PluginSettings); - - // Register ResultsUpdated event after all plugins are loaded - Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); + var imageLoaderTask = ImageLoader.InitializeAsync(); Http.Proxy = _settings.Proxy; // Initialize plugin manifest before initializing plugins so that they can use the manifest instantly await API.UpdatePluginManifestAsync(); - await PluginManager.InitializePluginsAsync(); - - // Update plugin titles after plugins are initialized with their api instances - Internationalization.UpdatePluginMetadataTranslations(); - - await imageLoadertask; + await imageLoaderTask; _mainWindow = new MainWindow(); Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; + // Initialize Dialog Jump before hotkey mapper since hotkey mapper will register its hotkey + // Initialize Dialog Jump after main window is created so that it can access main window handle + DialogJump.InitializeDialogJump(); + DialogJump.SetupDialogJump(_settings.EnableDialogJump); + // Initialize hotkey mapper instantly after main window is created because // it will steal focus from main window which causes window hide HotKeyMapper.Initialize(); @@ -239,19 +239,40 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => // Initialize theme for main window Ioc.Default.GetRequiredService().ChangeTheme(); - DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs()); - DialogJump.SetupDialogJump(_settings.EnableDialogJump); - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); RegisterExitEvents(); AutoStartup(); AutoUpdates(); - AutoPluginUpdates(); API.SaveAppAllSettings(); - API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------"); + API.LogInfo(ClassName, "End Flow Launcher startup ------------------------------------------------------"); + + _ = API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => + { + API.LogInfo(ClassName, "Begin plugin initialization ----------------------------------------------------"); + + AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); + + PluginManager.LoadPlugins(_settings.PluginSettings); + + await PluginManager.InitializePluginsAsync(_mainVM); + + // Refresh home page after plugins are initialized because users may open main window during plugin initialization + // And home page is created without full plugin list + if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText)) + { + _mainVM.QueryResults(); + } + + AutoPluginUpdates(); + + // Save all settings since we possibly update the plugin environment paths + API.SaveAppAllSettings(); + + API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------"); + }); }); } @@ -276,7 +297,7 @@ private static void AutoStartup() // but if it fails (permissions, etc) then don't keep retrying // this also gives the user a visual indication in the Settings widget _settings.StartFlowLauncherOnSystemStartup = false; - API.ShowMsgError(API.GetTranslation("setAutoStartFailed"), e.Message); + API.ShowMsgError(Localize.setAutoStartFailed(), e.Message); } } } diff --git a/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs b/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs index 41e87991317..82da6d936b3 100644 --- a/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs +++ b/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs @@ -5,7 +5,7 @@ namespace Flow.Launcher.Converters; -internal class BoolToIMEConversionModeConverter : IValueConverter +public class BoolToIMEConversionModeConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { @@ -22,7 +22,7 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu } } -internal class BoolToIMEStateConverter : IValueConverter +public class BoolToIMEStateConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { diff --git a/Flow.Launcher/Converters/CornerRadiusFilterConverter.cs b/Flow.Launcher/Converters/CornerRadiusFilterConverter.cs new file mode 100644 index 00000000000..fd43cafacac --- /dev/null +++ b/Flow.Launcher/Converters/CornerRadiusFilterConverter.cs @@ -0,0 +1,91 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Flow.Launcher.Converters; + +public class CornerRadiusFilterConverter : DependencyObject, IValueConverter +{ + public CornerRadiusFilterKind Filter { get; set; } + + public double Scale { get; set; } = 1.0; + + public static CornerRadius Convert(CornerRadius radius, CornerRadiusFilterKind filterKind) + { + CornerRadius result = radius; + + switch (filterKind) + { + case CornerRadiusFilterKind.Top: + result.BottomLeft = 0; + result.BottomRight = 0; + break; + case CornerRadiusFilterKind.Right: + result.TopLeft = 0; + result.BottomLeft = 0; + break; + case CornerRadiusFilterKind.Bottom: + result.TopLeft = 0; + result.TopRight = 0; + break; + case CornerRadiusFilterKind.Left: + result.TopRight = 0; + result.BottomRight = 0; + break; + } + + return result; + } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var cornerRadius = (CornerRadius)value; + + var scale = Scale; + if (!double.IsNaN(scale)) + { + cornerRadius.TopLeft *= scale; + cornerRadius.TopRight *= scale; + cornerRadius.BottomRight *= scale; + cornerRadius.BottomLeft *= scale; + } + + var filterType = Filter; + if (filterType == CornerRadiusFilterKind.TopLeftValue || + filterType == CornerRadiusFilterKind.BottomRightValue) + { + return GetDoubleValue(cornerRadius, filterType); + } + + return Convert(cornerRadius, filterType); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + private static double GetDoubleValue(CornerRadius radius, CornerRadiusFilterKind filterKind) + { + switch (filterKind) + { + case CornerRadiusFilterKind.TopLeftValue: + return radius.TopLeft; + case CornerRadiusFilterKind.BottomRightValue: + return radius.BottomRight; + } + return 0; + } +} + +public enum CornerRadiusFilterKind +{ + None, + Top, + Right, + Bottom, + Left, + TopLeftValue, + BottomRightValue +} diff --git a/Flow.Launcher/Converters/PlacementRectangleConverter.cs b/Flow.Launcher/Converters/PlacementRectangleConverter.cs new file mode 100644 index 00000000000..130d04e160e --- /dev/null +++ b/Flow.Launcher/Converters/PlacementRectangleConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Flow.Launcher.Converters; + +public class PlacementRectangleConverter : IMultiValueConverter +{ + public Thickness Margin { get; set; } + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Length == 2 && + values[0] is double width && + values[1] is double height) + { + var margin = Margin; + var topLeft = new Point(margin.Left, margin.Top); + var bottomRight = new Point(width - margin.Right, height - margin.Bottom); + var rect = new Rect(topLeft, bottomRight); + return rect; + } + + return Rect.Empty; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/Flow.Launcher/Converters/SharedSizeGroupConverter.cs b/Flow.Launcher/Converters/SharedSizeGroupConverter.cs new file mode 100644 index 00000000000..59478702749 --- /dev/null +++ b/Flow.Launcher/Converters/SharedSizeGroupConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Flow.Launcher.Converters; + +public class SharedSizeGroupConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (Visibility)value != Visibility.Collapsed ? (string)parameter : null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/Flow.Launcher/Converters/StringToKeyBindingConverter.cs b/Flow.Launcher/Converters/StringToKeyBindingConverter.cs index 21bf584e7a9..b7bca41c54e 100644 --- a/Flow.Launcher/Converters/StringToKeyBindingConverter.cs +++ b/Flow.Launcher/Converters/StringToKeyBindingConverter.cs @@ -5,7 +5,7 @@ namespace Flow.Launcher.Converters; -class StringToKeyBindingConverter : IValueConverter +public class StringToKeyBindingConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs index 2ee08bf85c0..3bba2c5b832 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs @@ -41,7 +41,7 @@ private void btnAdd_OnClick(object sender, RoutedEventArgs e) if (string.IsNullOrEmpty(Hotkey) && string.IsNullOrEmpty(ActionKeyword)) { - App.API.ShowMsgBox(App.API.GetTranslation("emptyPluginHotkey")); + App.API.ShowMsgBox(Localize.emptyPluginHotkey()); return; } diff --git a/Flow.Launcher/CustomShortcutSetting.xaml.cs b/Flow.Launcher/CustomShortcutSetting.xaml.cs index f4644a267e9..317d059a198 100644 --- a/Flow.Launcher/CustomShortcutSetting.xaml.cs +++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs @@ -40,14 +40,14 @@ private void BtnAdd_OnClick(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(Key) || string.IsNullOrEmpty(Value)) { - App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut")); + App.API.ShowMsgBox(Localize.emptyShortcut()); return; } // Check if key is modified or adding a new one if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key)) { - App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut")); + App.API.ShowMsgBox(Localize.duplicateShortcut()); return; } diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index a99d4d8c2f5..8c7670426bb 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -37,14 +37,53 @@ prompt 4 false + $(NoWarn);FLSG0007 - + - + @@ -94,10 +133,12 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -106,9 +147,6 @@ - - - all @@ -123,6 +161,10 @@ + + true + + Always diff --git a/Flow.Launcher/Helper/AutoStartup.cs b/Flow.Launcher/Helper/AutoStartup.cs index 34700c61015..1f057f83990 100644 --- a/Flow.Launcher/Helper/AutoStartup.cs +++ b/Flow.Launcher/Helper/AutoStartup.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Security.Principal; using Flow.Launcher.Infrastructure; @@ -64,7 +65,9 @@ private static bool CheckLogonTask() if (task.Definition.Actions.FirstOrDefault() is Microsoft.Win32.TaskScheduler.Action taskAction) { var action = taskAction.ToString().Trim(); - if (!action.Equals(Constant.ExecutablePath, StringComparison.OrdinalIgnoreCase)) + var needsRecreation = !action.Equals(Constant.ExecutablePath, StringComparison.OrdinalIgnoreCase) + || task.Definition.Settings.Priority != ProcessPriorityClass.Normal; + if (needsRecreation) { UnscheduleLogonTask(); ScheduleLogonTask(); @@ -184,6 +187,7 @@ private static bool ScheduleLogonTask() td.Settings.StopIfGoingOnBatteries = false; td.Settings.DisallowStartIfOnBatteries = false; td.Settings.ExecutionTimeLimit = TimeSpan.Zero; + td.Settings.Priority = ProcessPriorityClass.Normal; try { diff --git a/Flow.Launcher/Helper/BorderHelper.cs b/Flow.Launcher/Helper/BorderHelper.cs new file mode 100644 index 00000000000..0f2a78e7dd6 --- /dev/null +++ b/Flow.Launcher/Helper/BorderHelper.cs @@ -0,0 +1,33 @@ +using System.Windows; +using System.Windows.Controls; + +namespace Flow.Launcher.Helper; + +public static class BorderHelper +{ + #region Child + + public static readonly DependencyProperty ChildProperty = + DependencyProperty.RegisterAttached( + "Child", + typeof(UIElement), + typeof(BorderHelper), + new PropertyMetadata(default(UIElement), OnChildChanged)); + + public static UIElement GetChild(Border border) + { + return (UIElement)border.GetValue(ChildProperty); + } + + public static void SetChild(Border border, UIElement value) + { + border.SetValue(ChildProperty, value); + } + + private static void OnChildChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((Border)d).Child = (UIElement)e.NewValue; + } + + #endregion +} diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index 86a68475e8d..0a28264849a 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -61,8 +61,8 @@ private static void SetWithChefKeys(string hotkeyStr) string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}", e.Message, e.StackTrace)); - string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); - string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + string errorMsg = Localize.registerHotkeyFailed(hotkeyStr); + string errorMsgTitle = Localize.MessageBoxTitle(); App.API.ShowMsgBox(errorMsg, errorMsgTitle); } } @@ -87,8 +87,8 @@ internal static void SetHotkey(HotkeyModel hotkey, EventHandler e.Message, e.StackTrace, hotkeyStr)); - string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); - string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + string errorMsg = Localize.registerHotkeyFailed(hotkeyStr); + string errorMsgTitle = Localize.MessageBoxTitle(); App.API.ShowMsgBox(errorMsg, errorMsgTitle); } } @@ -112,8 +112,8 @@ internal static void RemoveHotkey(string hotkeyStr) string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}", e.Message, e.StackTrace)); - string errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr); - string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + string errorMsg = Localize.unregisterHotkeyFailed(hotkeyStr); + string errorMsgTitle = Localize.MessageBoxTitle(); App.API.ShowMsgBox(errorMsg, errorMsgTitle); } } @@ -143,6 +143,8 @@ internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey) return; App.API.ShowMainWindow(); + // Make sure to go back to the query results page first since it can cause issues if current page is context menu + App.API.BackToQueryResults(); App.API.ChangeQuery(hotkey.ActionKeyword, true); }); } diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 89bfde3497a..b920b53a740 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -234,7 +234,7 @@ private void RefreshHotkeyInterface(string hotkey) private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); - public string EmptyHotkey => App.API.GetTranslation("none"); + public string EmptyHotkey => Localize.none(); public ObservableCollection KeysToDisplay { get; set; } = new(); diff --git a/Flow.Launcher/HotkeyControlDialog.xaml b/Flow.Launcher/HotkeyControlDialog.xaml index d416f1bdcda..9fdfda865e7 100644 --- a/Flow.Launcher/HotkeyControlDialog.xaml +++ b/Flow.Launcher/HotkeyControlDialog.xaml @@ -2,7 +2,7 @@ x:Class="Flow.Launcher.HotkeyControlDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:ui="http://schemas.modernwpf.com/2019" + xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" Background="{DynamicResource PopuBGColor}" BorderBrush="{DynamicResource PopupButtonAreaBorderColor}" BorderThickness="0 1 0 0" diff --git a/Flow.Launcher/HotkeyControlDialog.xaml.cs b/Flow.Launcher/HotkeyControlDialog.xaml.cs index c7af8c5b8bb..e1fc86f9541 100644 --- a/Flow.Launcher/HotkeyControlDialog.xaml.cs +++ b/Flow.Launcher/HotkeyControlDialog.xaml.cs @@ -9,7 +9,7 @@ using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using ModernWpf.Controls; +using iNKORE.UI.WPF.Modern.Controls; namespace Flow.Launcher; @@ -33,7 +33,7 @@ public enum EResultType public EResultType ResultType { get; private set; } = EResultType.Cancel; public string ResultValue { get; private set; } = string.Empty; - public static string EmptyHotkey => App.API.GetTranslation("none"); + public static string EmptyHotkey => Localize.none(); private static bool isOpenFlowHotkey; @@ -41,7 +41,7 @@ public HotkeyControlDialog(string hotkey, string defaultHotkey, string windowTit { WindowTitle = windowTitle switch { - "" or null => App.API.GetTranslation("hotkeyRegTitle"), + "" or null => Localize.hotkeyRegTitle(), _ => windowTitle }; DefaultHotkey = defaultHotkey; @@ -146,10 +146,7 @@ private void SetKeysToDisplay(HotkeyModel? hotkey) Alert.Visibility = Visibility.Visible; if (registeredHotkeyData.RemoveHotkey is not null) { - tbMsg.Text = string.Format( - App.API.GetTranslation("hotkeyUnavailableEditable"), - description - ); + tbMsg.Text = Localize.hotkeyUnavailableEditable(description); SaveBtn.IsEnabled = false; SaveBtn.Visibility = Visibility.Collapsed; OverwriteBtn.IsEnabled = true; @@ -158,10 +155,7 @@ private void SetKeysToDisplay(HotkeyModel? hotkey) } else { - tbMsg.Text = string.Format( - App.API.GetTranslation("hotkeyUnavailableUneditable"), - description - ); + tbMsg.Text = Localize.hotkeyUnavailableUneditable(description); SaveBtn.IsEnabled = false; SaveBtn.Visibility = Visibility.Visible; OverwriteBtn.IsEnabled = false; @@ -175,7 +169,7 @@ private void SetKeysToDisplay(HotkeyModel? hotkey) if (!CheckHotkeyAvailability(hotkey.Value, true)) { - tbMsg.Text = App.API.GetTranslation("hotkeyUnavailable"); + tbMsg.Text = Localize.hotkeyUnavailable(); Alert.Visibility = Visibility.Visible; SaveBtn.IsEnabled = false; SaveBtn.Visibility = Visibility.Visible; diff --git a/Flow.Launcher/Languages/ar.xaml b/Flow.Launcher/Languages/ar.xaml index 67f1f766ea5..b1c9ca426ad 100644 --- a/Flow.Launcher/Languages/ar.xaml +++ b/Flow.Launcher/Languages/ar.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler خطأ في إعداد التشغيل عند بدء التشغيل إخفاء Flow Launcher عند فقدان التركيز + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. عدم عرض إشعارات الإصدار الجديد Search Window Location تذكر آخر موقع diff --git a/Flow.Launcher/Languages/cs.xaml b/Flow.Launcher/Languages/cs.xaml index 96cbe95e769..b55026e2849 100644 --- a/Flow.Launcher/Languages/cs.xaml +++ b/Flow.Launcher/Languages/cs.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Při nastavování spouštění došlo k chybě Skrýt Flow Launcher při vykliknutí + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Nezobrazovat oznámení o nové verzi Search Window Location Zapamatovat poslední pozici diff --git a/Flow.Launcher/Languages/da.xaml b/Flow.Launcher/Languages/da.xaml index a1fc771d2fa..7d4094c843c 100644 --- a/Flow.Launcher/Languages/da.xaml +++ b/Flow.Launcher/Languages/da.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Skjul Flow Launcher ved mistet fokus + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Vis ikke notifikationer om nye versioner Search Window Location Remember Last Position diff --git a/Flow.Launcher/Languages/de.xaml b/Flow.Launcher/Languages/de.xaml index 2c083646f7e..32f8d5d2bdf 100644 --- a/Flow.Launcher/Languages/de.xaml +++ b/Flow.Launcher/Languages/de.xaml @@ -79,6 +79,8 @@ Nach der Deinstallation müssen Sie diese Aufgabe (Flow.Launcher Startup) via Task-Scheduler manuell entfernen Fehler bei Einstellungsstart beim Start Flow Launcher ausblenden, wenn Fokus verloren geht + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Versionsbenachrichtigungen nicht zeigen Ort des Suchfensters Letzte Position merken diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 22d93f1bd88..451a21ad7af 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -66,6 +66,10 @@ Position Reset Reset search window position Type here to search + {0}: This plugin is still initializing... + Select this result to requery + {0}: Failed to respond! + Select this result for more info Settings @@ -77,6 +81,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Hide Flow Launcher when focus is lost + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Do not show new version notifications Search Window Location Remember Last Position @@ -209,6 +215,8 @@ Version Website Uninstall + Search delay time: default + Search delay time: {0}ms Fail to remove plugin settings Plugins: {0} - Fail to remove plugin settings files, please remove them manually Fail to remove plugin cache @@ -219,6 +227,7 @@ Fail to uninstall {0} Unable to find plugin.json from the extracted zip file, or this path {0} does not exist A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin + Error creating setting panel for plugin {0}:{1}{2} Plugin Store @@ -462,8 +471,10 @@ Open Folder Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -585,7 +596,7 @@ The specified file manager could not be found. Please check the Custom File Manager setting under Settings > General. Error - An error occurred while opening the folder. {0} + An error occurred while opening the folder. An error occurred while opening the URL in the browser. Please check your Default Web Browser configuration in the General section of the settings window File or directory not found: {0} diff --git a/Flow.Launcher/Languages/es-419.xaml b/Flow.Launcher/Languages/es-419.xaml index 9f06c443629..40e8cb97872 100644 --- a/Flow.Launcher/Languages/es-419.xaml +++ b/Flow.Launcher/Languages/es-419.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Ocultar Flow Launcher cuando se pierde el enfoque + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. No mostrar notificaciones de nuevas versiones Search Window Location Remember Last Position diff --git a/Flow.Launcher/Languages/es.xaml b/Flow.Launcher/Languages/es.xaml index f7a3fbb2249..049b33858b0 100644 --- a/Flow.Launcher/Languages/es.xaml +++ b/Flow.Launcher/Languages/es.xaml @@ -79,6 +79,8 @@ Después de la desinstalación, es necesario eliminar manualmente la tarea (Flow.Launcher Startup) mediante el Programador de Tareas Error de configuración de arranque al iniciar Ocultar Flow Launcher cuando se pierde el foco + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. No mostrar notificaciones de nuevas versiones Ubicación de la ventana de búsqueda Recordar última ubicación diff --git a/Flow.Launcher/Languages/fr.xaml b/Flow.Launcher/Languages/fr.xaml index bdce6ddb9fd..d60d53a1c0f 100644 --- a/Flow.Launcher/Languages/fr.xaml +++ b/Flow.Launcher/Languages/fr.xaml @@ -79,6 +79,8 @@ Après une désinstallation, vous devez supprimer manuellement cette tâche (Flow.Launcher Startup) via le planificateur de tâches Erreur lors de la configuration du lancement au démarrage Cacher Flow Launcher lors de la perte de focus + Afficher la barre des tâches lorsque Flow Launcher est ouvert + Afficher temporairement la barre des tâches lorsque Flow Launcher est ouvert, utile pour les barres de tâches auto-masquées. Ne pas afficher le message de mise à jour pour les nouvelles versions Emplacement de la fenêtre de recherche Se souvenir de la dernière position diff --git a/Flow.Launcher/Languages/he.xaml b/Flow.Launcher/Languages/he.xaml index 39f02c702f2..926d9488605 100644 --- a/Flow.Launcher/Languages/he.xaml +++ b/Flow.Launcher/Languages/he.xaml @@ -8,9 +8,9 @@ אנא בחר את קובץ ההפעלה {0} - Your selected {0} executable is invalid. + קובץ ההפעלה {0} שבחרת אינו חוקי. {2}{2} - Click yes if you would like select the {0} executable again. Click no if you would like to download {1} + לחץ על כן אם ברצונך, בחר את {0} ההפעלה הקודמת. לחץ על לא אם ברצונך להוריד את {1} לא ניתן להגדיר נתיב הפעלה {0}, אנא נסה שוב בהגדרות Flow (גלול עד למטה). נכשל בהפעלת תוספים @@ -79,6 +79,8 @@ לאחר הסרת ההתקנה, עליך להסיר ידנית משימה זו (Flow.Launcher Startup) דרך מתזמן המשימות שגיאה בהגדרת ההפעלה בעת הפעלת windows הסתר את Flow Launcher כאשר הוא אינו החלון הפעיל + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. אל תציג התראות על גרסה חדשה מיקום חלון חיפוש זכור את המיקום האחרון diff --git a/Flow.Launcher/Languages/it.xaml b/Flow.Launcher/Languages/it.xaml index ffb7166582c..92afa543668 100644 --- a/Flow.Launcher/Languages/it.xaml +++ b/Flow.Launcher/Languages/it.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Errore nell'impostazione del lancio all'avvio Nascondi Flow Launcher quando perde il focus + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Non mostrare le notifiche per una nuova versione Search Window Location Ricorda L'Ultima Posizione @@ -147,8 +149,8 @@ Search Delay Adds a short delay while typing to reduce UI flicker and result load. Recommended if your typing speed is average. Enter the wait time (in ms) until input is considered complete. This can only be edited if Search Delay is enabled. - Default Search Delay Time - Wait time before showing results after typing stops. Higher values wait longer. (ms) + Tempo predefinito ritardo ricerca + Tempo di attesa prima di mostrare i risultati dopo l'interruzione della digitazione. Valori più alti attendono più a lungo. (ms) Information for Korean IME user The Korean input method used in Windows 11 may cause some issues in Flow Launcher. @@ -265,11 +267,11 @@ Plugin update {0} di {1} {2}{2}Vuoi aggiornare questo plugin? Download del plugin - Automatically restart after installing/uninstalling/updating plugins in plugin store - Zip file does not have a valid plugin.json configuration + Riavvia automaticamente dopo l'installazione/disinstallazione/aggiornamento dei plugin nel Plugin Store + Il file zip non contiene una configurazione plugin.json valida Installazione da una fonte sconosciuta This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window) - Zip files + File zip Please select zip file Install plugin from local path Nessun aggiornamento disponibile @@ -469,7 +471,7 @@ Cancella i log Sei sicuro di voler cancellare tutti i log? Cache Folder - Clear Caches + Cancella cache Are you sure you want to delete all caches? Failed to clear part of folders and files. Please see log file for more information Wizard @@ -636,7 +638,7 @@ Se si aggiunge un prefisso '@' mentre si inserisce una scorciatoia, corrisponde Restart Flow Launcher after updating plugins {0}: Update from v{1} to v{2} - No plugin selected + Nessun plugin selezionato Salta diff --git a/Flow.Launcher/Languages/ja.xaml b/Flow.Launcher/Languages/ja.xaml index 51ec99ddd8b..39b23f26977 100644 --- a/Flow.Launcher/Languages/ja.xaml +++ b/Flow.Launcher/Languages/ja.xaml @@ -64,10 +64,10 @@ 位置のリセット 検索ウィンドウの位置をリセット ここに入力して検索 - {0}: This plugin is still initializing... - Select this result to requery - {0}: Failed to respond! - Select this result for more info + {0}: このプラグインはまだ初期化中です… + この結果を選択して再検索する + {0}: 応答に失敗しました! + 詳細については、この結果を選択してください 設定 @@ -79,6 +79,8 @@ アンインストール後は、「タスク スケジューラ」からこのタスク(Flow.Launcher Startup)を手動で削除する必要があります。 スタートアップ時に起動の設定失敗 フォーカスを失った時にFlow Launcherを隠す + Flow Launcher を開いたときにタスクバーを表示する + Flow Launcher を開いたときに一時的にタスクバーを表示します。タスクバーの自動非表示を設定している場合に便利です。 最新版が入手可能であっても、アップグレードメッセージを表示しない 検索ウィンドウの位置 最後の表示位置を記憶する @@ -169,7 +171,7 @@ 開く 前の韓国語IMEを使用 You can change the Previous Korean IME settings directly from here - Failed to change Korean IME setting + 韓国語IME設定の変更に失敗しました システムのレジストリへのアクセスが可能か確認するか、サポートにお問い合わせください。 ホームページ 検索文字列が空の場合、ホームページの結果を表示します。 @@ -454,7 +456,7 @@ アイコン あなたはFlow Launcherを {0} 回利用しました アップデートを確認する - Become a Sponsor + スポンサーになる 新しいバージョン {0} が利用可能です。Flow Launcherを再起動してください。 アップデートの確認に失敗しました、api.github.com への接続とプロキシ設定を確認してください。 diff --git a/Flow.Launcher/Languages/ko.xaml b/Flow.Launcher/Languages/ko.xaml index 503ff2f1154..22f3bb01189 100644 --- a/Flow.Launcher/Languages/ko.xaml +++ b/Flow.Launcher/Languages/ko.xaml @@ -79,6 +79,8 @@ Flow Launcher를 제거한 후에는 작업 스케줄러에서 이 작업(Flow.Launcher Startup)을 수동으로 삭제해야 합니다 Error setting launch on startup 포커스 잃으면 Flow Launcher 숨김 + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. 새 버전 알림 끄기 검색 창 위치 마지막 위치 기억 diff --git a/Flow.Launcher/Languages/nb.xaml b/Flow.Launcher/Languages/nb.xaml index 8bd7f94a4b8..1ef05712595 100644 --- a/Flow.Launcher/Languages/nb.xaml +++ b/Flow.Launcher/Languages/nb.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Feil ved å sette kjør ved oppstart Skjul Flow Launcher når fokus forsvinner + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Ikke vis varsler om nye versjoner Search Window Location Husk siste posisjon diff --git a/Flow.Launcher/Languages/nl.xaml b/Flow.Launcher/Languages/nl.xaml index 8b7b8632902..412781b55e0 100644 --- a/Flow.Launcher/Languages/nl.xaml +++ b/Flow.Launcher/Languages/nl.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Fout bij het instellen van uitvoeren bij opstarten Verberg Flow Launcher als focus verloren is + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Laat geen nieuwe versie notificaties zien Search Window Location Laatste Positie Onthouden diff --git a/Flow.Launcher/Languages/pl.xaml b/Flow.Launcher/Languages/pl.xaml index 382626eb7e1..efc2ade5563 100644 --- a/Flow.Launcher/Languages/pl.xaml +++ b/Flow.Launcher/Languages/pl.xaml @@ -79,6 +79,8 @@ Kliknij "nie", jeśli jest już zainstalowany. Zostaniesz wtedy popros Po odinstalowaniu musisz ręcznie usunąć to zadanie (Flow.Launcher Startup) za pomocą Harmonogramu zadań Błąd uruchamiania ustawień przy starcie Ukryj okno Flow Launcher kiedy przestanie ono być aktywne + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Nie pokazuj powiadomienia o nowej wersji Pozycja okna wyszukiwania Zapamiętaj Ostatnią Pozycję diff --git a/Flow.Launcher/Languages/pt-br.xaml b/Flow.Launcher/Languages/pt-br.xaml index 15dcae2f48d..a7cf5ac685f 100644 --- a/Flow.Launcher/Languages/pt-br.xaml +++ b/Flow.Launcher/Languages/pt-br.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Erro ao ativar início com o sistema Esconder Flow Launcher quando foco for perdido + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Não mostrar notificações de novas versões Search Window Location Lembrar Última Posição diff --git a/Flow.Launcher/Languages/pt-pt.xaml b/Flow.Launcher/Languages/pt-pt.xaml index 3746ada132d..a2b58b1a081 100644 --- a/Flow.Launcher/Languages/pt-pt.xaml +++ b/Flow.Launcher/Languages/pt-pt.xaml @@ -79,6 +79,8 @@ Se desinstalar a aplicação, tem que remover manualmente a tarefa (Flow.Launcher Startup) no agendamento de tarefas Erro ao definir para iniciar ao arrancar Ocultar Flow Launcher ao perder o foco + Mostrar barra de tarefas ao abrir Flow Launcher + Mostrar, temporariamente, a barra de tarefas ao abrir Flow launcher. Útil para barra de tarefas oculta automaticamente. Não notificar acerca de novas versões Posição da janela de pesquisa Memorizar última posição @@ -420,13 +422,13 @@ Default search window position. Displayed when triggered by search window hotkey Dialog Jump Result Navigation Behaviour Behaviour to navigate Open/Save As dialog window to the selected result path - Left click or Enter key - Right click + Clique esquerdo ou tecla Enter + Clique direito Dialog Jump File Navigation Behaviour Behaviour to navigate Open/Save As dialog window when the result is a file path - Fill full path in file name box - Fill full path in file name box and open - Fill directory in path box + Preencher caminho total na caixa Nome do ficheiro + Preencher caminho total na caixa Nome do ficheiro e abrir + Preencher diretório na caixa Caminho Proxy HTTP diff --git a/Flow.Launcher/Languages/ru.xaml b/Flow.Launcher/Languages/ru.xaml index 7bb5a8ff129..54481668424 100644 --- a/Flow.Launcher/Languages/ru.xaml +++ b/Flow.Launcher/Languages/ru.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Ошибка настройки запуска при запуске Скрывать Flow Launcher, если потерян фокуc + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Не отображать сообщение об обновлении, когда доступна новая версия Search Window Location Запомнить последнее положение diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml index e76061a3bcf..c47b47ae1a9 100644 --- a/Flow.Launcher/Languages/sk.xaml +++ b/Flow.Launcher/Languages/sk.xaml @@ -80,6 +80,8 @@ Nevykonali sa žiadne zmeny. Po odinštalovaní musíte úlohu manuálne odstrániť (Flow.Launcher Startup) cez Plánovač úloh Chybné nastavenie spustenia pri spustení Schovať Flow Launcher po strate fokusu + Zobraziť panel úloh, keď je Flow Launcher otvorený + Dočasne zobraziť panel úloh pri otvorení Flow Launchera, užitočné pri automatickom skrývaní panela úloh. Nezobrazovať upozornenia na novú verziu Poloha vyhľadávacieho okna Zapamätať si poslednú pozíciu @@ -636,7 +638,7 @@ Ak pri zadávaní skratky pred ňu pridáte "@", bude sa zhodovať s Po aktualizácii pluginov reštartovať Flow Launcher - {0}: Aktualizované z v{1} na v{2} + {0}: Aktualizácia z v{1} na v{2} Nie je vybraný žiaden plugin diff --git a/Flow.Launcher/Languages/sr-Cyrl-RS.xaml b/Flow.Launcher/Languages/sr-Cyrl-RS.xaml index d7d60e6a01e..824b48dbf15 100644 --- a/Flow.Launcher/Languages/sr-Cyrl-RS.xaml +++ b/Flow.Launcher/Languages/sr-Cyrl-RS.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Hide Flow Launcher when focus is lost + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Do not show new version notifications Search Window Location Remember Last Position diff --git a/Flow.Launcher/Languages/sr.xaml b/Flow.Launcher/Languages/sr.xaml index 7d1bcb9f33d..3ee9828192f 100644 --- a/Flow.Launcher/Languages/sr.xaml +++ b/Flow.Launcher/Languages/sr.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Sakri Flow Launcher kada se izgubi fokus + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Ne prikazuj obaveštenje o novoj verziji Search Window Location Remember Last Position diff --git a/Flow.Launcher/Languages/tr.xaml b/Flow.Launcher/Languages/tr.xaml index 67cdf9da43a..023eb2aa659 100644 --- a/Flow.Launcher/Languages/tr.xaml +++ b/Flow.Launcher/Languages/tr.xaml @@ -79,6 +79,8 @@ Kaldırma işleminden sonra, bu görevi (Flow.Launcher Startup) Görev Zamanlayıcı üzerinden elle kaldırmanız gerekmektedir Sistemle başlatma ayarı başarısız oldu Odak Pencereden Ayrıldığında Gizle + Flow Launcher açıldığında görev çubuğunu göster + Flow Launcher açıldığında geçici olarak görev çubuğunu gösterir, otomatik gizlenen görev çubukları için kullanışlıdır. Güncelleme bildirimlerini gösterme Pencere Konumu Son Konumu Hatırla @@ -454,7 +456,7 @@ Kullanılan Simgeler Şu ana kadar Flow Launcher'ı {0} kez aktifleştirdiniz. Güncellemeleri Kontrol Et - Become a Sponsor + Sponsor Olun Uygulamanın yeni sürümü ({0}) mevcut, Lütfen Flow Launcher'ı yeniden başlatın. Güncelleme kontrolü başarısız oldu. Lütfen bağlantınız ve vekil sunucu ayarlarınızın api.github.com adresine ulaşabilir olduğunu kontrol edin. diff --git a/Flow.Launcher/Languages/uk-UA.xaml b/Flow.Launcher/Languages/uk-UA.xaml index bec1f85e3e2..0da6c7a47ab 100644 --- a/Flow.Launcher/Languages/uk-UA.xaml +++ b/Flow.Launcher/Languages/uk-UA.xaml @@ -79,6 +79,8 @@ Після видалення, вам необхідно вручну видалити це завдання (Flow.Launcher Startup) через планувальник завдань Помилка запуску налаштування під час запуску Сховати Flow Launcher, якщо втрачено фокус + Показувати панель завдань, коли Flow Launcher відкрито + Тимчасово показувати панель завдань при відкритті Flow Launcher, корисно для автоматично прихованих панелей завдань. Не повідомляти про доступні нові версії Розташування вікна пошуку Пам'ятати останню позицію diff --git a/Flow.Launcher/Languages/vi.xaml b/Flow.Launcher/Languages/vi.xaml index 5809c7837c2..463c11d0be7 100644 --- a/Flow.Launcher/Languages/vi.xaml +++ b/Flow.Launcher/Languages/vi.xaml @@ -79,6 +79,8 @@ After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Không lưu được tính năng tự khởi động khi khởi động hệ thống Ẩn Flow Launcher khi mất tiêu điểm + Show taskbar when Flow Launcher is opened + Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. Không hiển thị thông báo khi có phiên bản mới Search Window Location Ghi nhớ vị trí cuối cùng diff --git a/Flow.Launcher/Languages/zh-cn.xaml b/Flow.Launcher/Languages/zh-cn.xaml index 7af54dc54db..089ba2dd154 100644 --- a/Flow.Launcher/Languages/zh-cn.xaml +++ b/Flow.Launcher/Languages/zh-cn.xaml @@ -2,7 +2,7 @@ - Flow 检测到您已安装 {0} 个插件,需要 {1} 才能运行。是否要下载 {1}? + Flow 检测到您已安装 {0} 插件,需要 {1} 才能运行。是否要下载 {1}? {2}{2} 如果已安装,请单击“否”,系统将提示您选择包含 {1} 可执行文件的文件夹 @@ -79,6 +79,8 @@ 卸载后,您需要通过任务计划程序手动移除此任务 (Flow.Launcher Startup) 设置开机自启时出错 失去焦点时自动隐藏 Flow Launcher + 打开 Flow 时显示任务栏 + 打开 Flow 时临时显示任务栏,用于自动隐藏任务栏 不显示新版本提示 搜索窗口位置 记住上次的位置 diff --git a/Flow.Launcher/Languages/zh-tw.xaml b/Flow.Launcher/Languages/zh-tw.xaml index ca7da7624f1..7aacc21909d 100644 --- a/Flow.Launcher/Languages/zh-tw.xaml +++ b/Flow.Launcher/Languages/zh-tw.xaml @@ -2,43 +2,43 @@ - Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + Flow 檢測到你已安裝 {0} 個插件,需要 {1} 才能運行。是否要下載 {1}? {2}{2} - Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + 如果已安裝,請點擊“否”,系統將提示你選擇包含 {1} 個程式的資料夾 - Please select the {0} executable + 請選擇 {0} 可執行檔 - Your selected {0} executable is invalid. + 您所選擇的 {0} 可執行檔無效。 {2}{2} - Click yes if you would like select the {0} executable again. Click no if you would like to download {1} + 若要重新選取 {0} 可執行檔,請按「是」。若要下載 {1},請按「否」。 - Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom). - Fail to Init Plugins - Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help + 無法設定 {0} 可執行檔路徑,請從 Flow 的設定中嘗試(向下捲動至最底部)。 + 初始化外掛失敗 + 外掛:{0} — 載入失敗,將被停用,請聯絡外掛開發者以取得協助 - 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 - 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 - Flow Launcher has detected you enabled portable mode, would you like to move it to a different location? - Flow Launcher has detected you disabled portable mode, the relevant shortcuts and uninstaller entry have been created - 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. + Flow Launcher 需要重新啟動以完成停用可攜式模式,重新啟動後您的可攜式資料設定檔將被刪除,漫遊資料設定檔會保留 + Flow Launcher 需要重新啟動以完成啟用可攜式模式,重新啟動後您的漫遊資料設定檔將被刪除,而可攜式資料設定檔則會保留 + Flow Launcher 偵測到您已啟用可攜式模式,是否要將它移到其他位置? + Flow Launcher 偵測到您已停用可攜模式,相關的捷徑與解除安裝程式項目已建立 + Flow Launcher 偵測到您的使用者資料同時存在於 {0} 與 {1}。{2}{2}請刪除 {1} 以繼續。尚未發生任何變更。 - The following plugin has errored and cannot be loaded: - The following plugins have errored and cannot be loaded: - Please refer to the logs for more information + 下列外掛發生錯誤,無法載入: + 下列外掛發生錯誤,無法載入: + 請參閱日誌以獲得更多資訊 - Please try again - Unable to parse Http Proxy + 請再試一次 + 無法解析 Http 代理 - Failed to install TypeScript environment. Please try again later - Failed to install Python environment. Please try again later. + 無法安裝 TypeScript 環境。請稍後再試一次 + 安裝 Python 環境失敗。請稍後再試。 - Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. - Failed to unregister hotkey "{0}". Please try again or see log for details + 註冊熱鍵「{0}」失敗。此熱鍵可能已被其他程式使用。請更改為不同的熱鍵,或關閉其他程式。 + 無法取消註冊熱鍵「{0}」。請再試一次或查看日誌以取得詳細資訊 Flow Launcher 啟動命令 {0} 失敗 無效的 Flow Launcher 外掛格式 @@ -54,7 +54,7 @@ 複製 剪下 貼上 - Undo + 還原 全選 檔案 資料夾 @@ -62,12 +62,12 @@ 遊戲模式 暫停使用快捷鍵。 重設位置 - Reset search window position - Type here to search - {0}: This plugin is still initializing... - Select this result to requery - {0}: Failed to respond! - Select this result for more info + 重設搜尋視窗位置 + 在此輸入以搜尋 + 「{0}:此外掛程式仍在初始化……」…… + 選擇此結果以重新查詢 + 「{0}:未能回應!」! + 選取此結果以取得更多資訊 設定 @@ -75,22 +75,24 @@ 便攜模式 將所有設定和使用者資料存儲在一個資料夾中(當與可移動磁碟或雲服務一起使用時很有用)。 開機時啟動 - Use logon task instead of startup entry for faster startup experience - After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler - Error setting launch on startup + 使用登入工作取代啟動項目,以加快啟動體驗 + 解除安裝後,您需要透過工作排程程式手動移除此工作(Flow.Launcher Startup) + 設定開機啟動時發生錯誤 失去焦點時自動隱藏 Flow Launcher + 當 Flow Launcher 開啟時顯示工作列 + 當開啟 Flow Launcher 時暫時顯示工作列,對自動隱藏的工作列很有用。 不顯示新版本提示 - Search Window Location + 搜尋視窗位置 記住最後位置 - Monitor with Mouse Cursor - Monitor with Focused Window - Primary Monitor - Custom Monitor + 帶有滑鼠游標的顯示器 + 以焦點視窗進行監視 + 主顯示器 + 自訂搜尋視窗位置 搜尋視窗在螢幕上的位置 - Center - Center Top - Left Top - Right Top + 置中 + 頂部置中 + 左側置中 + 右側置中 自訂搜尋視窗位置 語言 最後查詢樣式 @@ -98,10 +100,10 @@ 保留上一個查詢 選擇上一個查詢 清空上次搜尋關鍵字 - Preserve Last Action Keyword - Select Last Action Keyword + 保留上次操作的關鍵字 + 選擇最後操作的關鍵字 最大結果顯示個數 - You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. + 您也可以使用「CTRL+加號」和「CTRL+減號」快速調整此設定。 全螢幕模式下忽略快捷鍵 全螢幕模式下停用快捷鍵(推薦用於遊戲時)。 預設檔案管理器 @@ -109,28 +111,28 @@ 預設瀏覽器 設定新增分頁、視窗和無痕模式。 Python 位置 - Node.js Path - Please select the Node.js executable + Node.js的路徑 + 請選擇Node.js的可執行檔 請選擇 pythonw.exe 一律以英文模式開始輸入 啟動 Flow 時暫時將輸入法切換為英文模式。 自動更新 - Automatically check and update the app when available + 在可用時自動檢查並更新應用程式 選擇 啟動時不顯示主視窗 - Flow Launcher search window is hidden in the tray after starting up. + 啟動後,Flow Launcher搜尋視窗會隱藏在系統匣中。 隱藏任務欄圖示 當圖示從系統列隱藏時,可以透過在搜尋視窗上按右鍵來開啟設定選單。 查詢搜尋精確度 更改結果所需的最低匹配分數。 - None - Low - Regular + + + 一般 拼音搜尋 - Pinyin is the standard system of romanized spelling for translating Chinese. Please note, enabling this can significantly increase memory usage during search. - Use Double Pinyin - Use Double Pinyin instead of Full Pinyin to search. - Double Pinyin Schema + 拼音是中文翻譯的標準羅馬化拼字系統。請注意,啟用此功能可能會顯著增加搜尋時的記憶體使用量。 + 啟用雙拼模式 + 請使用雙拼以搜尋,而不是全拼搜尋。 + 雙拼結構 Xiao He Zi Ran Ma Wei Ruan @@ -143,12 +145,12 @@ 一律預覽 當 Flow 啟動時,一律開啟預覽面板。按下 {0} 可切換預覽。 - Shadow effect is not allowed while current theme has blur effect enabled - Search Delay - Adds a short delay while typing to reduce UI flicker and result load. Recommended if your typing speed is average. - Enter the wait time (in ms) until input is considered complete. This can only be edited if Search Delay is enabled. - Default Search Delay Time - Wait time before showing results after typing stops. Higher values wait longer. (ms) + 當目前主題啟用模糊效果時,將不允許使用陰影效果 + 延遲搜尋 + 在輸入時增加短暫延遲,以減少介面閃爍和結果載入時間。建議打字速度中等的使用者使用。 + 輸入等待時間(以毫秒為單位),直到認為輸入完成為止。僅當啟用“搜尋延遲”時才能編輯此設定。 + 預設搜尋延遲時間 + 輸入停止後顯示結果前的等待時間。數值越高,等待時間越長。 (以毫秒為單位) Information for Korean IME user The Korean input method used in Windows 11 may cause some issues in Flow Launcher. @@ -164,17 +166,17 @@ - Open Language and Region System Settings - Opens the Korean IME setting location. Go to Korean > Language Options > Keyboard - Microsoft IME > Compatibility + 開啟語言和區域設定 + 開啟韓語輸入法設定。前往“韓語”>“語言選項”>“鍵盤 - Microsoft 輸入法”>“相容性”。 開啟 - Use Previous Korean IME - You can change the Previous Korean IME settings directly from here - Failed to change Korean IME setting - Please check your system registry access or contact support. - Home Page - Show home page results when query text is empty. - Show History Results in Home Page - Maximum History Results Shown in Home Page + 使用舊版韓語輸入法 + 您可以直接從這裡更改先前的韓語輸入法設定 + 更改韓語輸入法設定失敗 + 請檢查您的系統註冊表存取權限或尋求協助。 + 首頁 + 當查詢文字為空時,顯示首頁結果。 + 在首頁顯示歷史記錄 + 首頁顯示最多歷史搜尋結果 History Style Choose the type of history to show in the History and Home Page Query history @@ -209,8 +211,8 @@ Advanced Settings: 已啟用 優先 - Search Delay - Home Page + 延遲搜尋 + 首頁 目前優先 新增優先 優先 @@ -263,21 +265,21 @@ Plugin uninstall {0} by {1} {2}{2}Would you like to uninstall this plugin? Plugin update - {0} by {1} {2}{2}Would you like to update this plugin? + 您想更新{0} (由 {1} {2}{2} 製作)嗎? 正在下載擴充功能 - Automatically restart after installing/uninstalling/updating plugins in plugin store - Zip file does not have a valid plugin.json configuration - Installing from an unknown source - This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window) - Zip files - Please select zip file - Install plugin from local path + 在外掛商店安裝/卸載/更新插件後自動重啟 + 壓縮檔中沒有有效的plugin.json配置 + 從未知來源安裝 + 您正在安裝來自未知來源的插件,它可能有潛在風險!{0}{0} 請確保您了解此插件的來源並確認其安全性。{0}{0} 是否繼續? {0}{0}(您可以在設定中關閉此警告) + 壓縮檔 + 請選擇一個壓縮檔 + 從本地路徑安裝外掛 無可用更新 所有插件均為最新版本 - Plugin updates available - Update plugins - Check plugin updates - Plugins are successfully updated. Please restart Flow. + 有外掛更新可用 + 更新外掛程式 + 檢查外掛更新 + 插件已成功更新。請重新啟動Flow。 主題 @@ -288,21 +290,21 @@ 檔案總管 搜尋檔案、資料夾和檔案內容 網路搜尋 - Search the web with different search engine support + 使用不同的搜尋引擎搜尋網絡 程式 以系統管理員或其他使用者啟用應用程式 ProcessKiller - Terminate unwanted processes - Search Bar Height - Item Height + 終止不需要的進程 + 搜尋列高度 + 項目高度 查詢框字體 - Result Title Font - Result Subtitle Font - Reset - Reset to the recommended font and size settings. - Import Theme Size - If a size value intended by the theme designer is available, it will be retrieved and applied. - Customize + 結果標題字體 + 結果副標題字體 + 重設 + 恢復預設字體與文字大小設定。 + 匯入主題大小 + 如果主題設計者預設的尺寸值存在,則會擷取並套用該尺寸值。 + 個人化 視窗模式 透明度 找不到主題 {0} ,將回到預設主題 @@ -315,49 +317,49 @@ 暗色系 音效 搜尋窗口打開時播放音效 - Sound Effect Volume - Adjust the volume of the sound effect - Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume. + 音效音量 + 調整音效音量 + Windows Media Player不可用,而Flow的音量調整功能需要它。如果您需要調整音量,請檢查您的安裝情況。 動畫 使用介面動畫 - Animation Speed - The speed of the UI animation - Slow - Medium - Fast - Custom + 動畫速度 + UI動畫速度 + 慢速 + 中等 + 快速 + 自訂 時鐘 日期 - Backdrop Type - The backdrop effect is not applied in the preview. - Backdrop supported starting from Windows 11 build 22000 and above - None - Acrylic - Mica - Mica Alt - This theme supports two (light/dark) modes. - This theme supports Blur Transparent Background. - Show placeholder - Display placeholder when query is empty - Placeholder text - Change placeholder text. Input empty will use: {0} - Fixed Window Size - The window size is not adjustable by dragging. - Since Always Preview is on, maximum results shown may not take effect because preview panel requires a certain minimum height + 背景類型 + 預覽中未套用背景效果。 + 從 Windows 11 版本 22000 及更高版本開始支援背景功能 + + 壓克力 + 雲母 + 雲母(替代樣式) + 此主題支援兩種(淺色/深色)模式。 + 此主題支援模糊透明背景。 + 顯示佔位符 + 當查詢為空時顯示佔位符 + 佔位符文字 + 更改佔位符文字。輸入為空將使用{0} + 固定視窗大小 + 視窗大小無法透過拖曳進行調整。 + 由於「始終預覽」已啟用,因此可能無法顯示最大效果,因為預覽面板需要一定的最小高度 快捷鍵 快捷鍵 - Open Flow Launcher + 開啟Flow Launcher 執行縮寫以顯示 / 隱藏 Flow Launcher。 - Toggle Preview + 切換預覽 Enter shortcut to show/hide preview in search window. Hotkey Presets List of currently registered hotkeys 開放結果修飾符 - Select a modifier key to open selected result via keyboard. + 選擇一個修飾鍵以透過鍵盤開啟已選擇的結果。 顯示快捷鍵 - Show result selection hotkey with results. + 利用結果來顯示選擇的快捷鍵結果。 Auto Complete Runs autocomplete for the selected items. Select Next Item @@ -541,7 +543,7 @@ Input the search delay time in ms you like to use for the plugin. Input empty if you don't want to specify any, and the plugin will use default search delay time. - Home Page + 首頁 Enable the plugin home page state if you like to show the plugin results when query is empty. @@ -655,12 +657,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in 返回 / 快捷選單 - Item Navigation + 物件導覽 打開選單 開啟檔案位置 Run as Admin / Open Folder in Default File Manager 查詢歷史 - Back to Result in Context Menu + 返回右鍵選單中的結果 自動完成 開啟/運行選擇項目 開啟視窗設定 diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 132ec838978..747975b2a62 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -6,7 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:flowlauncher="clr-namespace:Flow.Launcher" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:ui="http://schemas.modernwpf.com/2019" + xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" xmlns:vm="clr-namespace:Flow.Launcher.ViewModel" Name="FlowMainWindow" Title="Flow Launcher" @@ -526,7 +526,7 @@ + Text="{Binding PreviewDescription}" /> diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 7f31de22d45..942cccbe992 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Media; @@ -25,7 +25,8 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.ViewModel; -using ModernWpf.Controls; +using iNKORE.UI.WPF.Modern; +using iNKORE.UI.WPF.Modern.Controls; using DataObject = System.Windows.DataObject; using Key = System.Windows.Input.Key; using MouseButtons = System.Windows.Forms.MouseButtons; @@ -148,8 +149,8 @@ private void OnLoaded(object sender, RoutedEventArgs e) _settings.ReleaseNotesVersion = Constant.Version; // Show release note popup with button App.API.ShowMsgWithButton( - string.Format(App.API.GetTranslation("appUpdateTitle"), Constant.Version), - App.API.GetTranslation("appUpdateButtonContent"), + Localize.appUpdateTitle(Constant.Version), + Localize.appUpdateButtonContent(), () => { Application.Current.Dispatcher.Invoke(() => @@ -191,11 +192,11 @@ private void OnLoaded(object sender, RoutedEventArgs e) // Initialize color scheme if (_settings.ColorScheme == Constant.Light) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light; + ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light; } else if (_settings.ColorScheme == Constant.Dark) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark; + ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark; } // Initialize position @@ -475,7 +476,7 @@ private void OnKeyDown(object sender, KeyEventArgs e) && QueryTextBox.CaretIndex == QueryTextBox.Text.Length) { var queryWithoutActionKeyword = - QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; + QueryBuilder.Build(QueryTextBox.Text, QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search; if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) { @@ -793,12 +794,12 @@ private void InitializeNotifyIcon() private void UpdateNotifyIconText() { var menu = _contextMenu; - ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + + ((MenuItem)menu.Items[0]).Header = Localize.iconTrayOpen() + " (" + _settings.Hotkey + ")"; - ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); - ((MenuItem)menu.Items[2]).Header = App.API.GetTranslation("PositionReset"); - ((MenuItem)menu.Items[3]).Header = App.API.GetTranslation("iconTraySettings"); - ((MenuItem)menu.Items[4]).Header = App.API.GetTranslation("iconTrayExit"); + ((MenuItem)menu.Items[1]).Header = Localize.GameMode(); + ((MenuItem)menu.Items[2]).Header = Localize.PositionReset(); + ((MenuItem)menu.Items[3]).Header = Localize.iconTraySettings(); + ((MenuItem)menu.Items[4]).Header = Localize.iconTrayExit(); } private void InitializeContextMenu() @@ -808,31 +809,31 @@ private void InitializeContextMenu() var openIcon = new FontIcon { Glyph = "\ue71e" }; var open = new MenuItem { - Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")", + Header = Localize.iconTrayOpen() + " (" + _settings.Hotkey + ")", Icon = openIcon }; var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" }; var gamemode = new MenuItem { - Header = App.API.GetTranslation("GameMode"), + Header = Localize.GameMode(), Icon = gamemodeIcon }; var positionresetIcon = new FontIcon { Glyph = "\ue73f" }; var positionreset = new MenuItem { - Header = App.API.GetTranslation("PositionReset"), + Header = Localize.PositionReset(), Icon = positionresetIcon }; var settingsIcon = new FontIcon { Glyph = "\ue713" }; var settings = new MenuItem { - Header = App.API.GetTranslation("iconTraySettings"), + Header = Localize.iconTraySettings(), Icon = settingsIcon }; var exitIcon = new FontIcon { Glyph = "\ue7e8" }; var exit = new MenuItem { - Header = App.API.GetTranslation("iconTrayExit"), + Header = Localize.iconTrayExit(), Icon = exitIcon }; @@ -842,8 +843,8 @@ private void InitializeContextMenu() settings.Click += (o, e) => App.API.OpenSettingDialog(); exit.Click += (o, e) => Close(); - gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); - positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); + gamemode.ToolTip = Localize.GameModeToolTip(); + positionreset.ToolTip = Localize.PositionResetToolTip(); _contextMenu.Items.Add(open); _contextMenu.Items.Add(gamemode); diff --git a/Flow.Launcher/PluginUpdateWindow.xaml b/Flow.Launcher/PluginUpdateWindow.xaml index 04cd1f7bc11..a4bb0643169 100644 --- a/Flow.Launcher/PluginUpdateWindow.xaml +++ b/Flow.Launcher/PluginUpdateWindow.xaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:flowlauncher="clr-namespace:Flow.Launcher" + xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" Title="{DynamicResource updateAllPluginsButtonContent}" Width="530" Background="{DynamicResource PopuBGColor}" @@ -66,13 +67,13 @@ Text="{DynamicResource updateAllPluginsButtonContent}" TextAlignment="Left" /> - - + allPlugins) { var checkBox = new CheckBox { - Content = string.Format(App.API.GetTranslation("updatePluginCheckboxContent"), plugin.Name, plugin.CurrentVersion, plugin.NewVersion), + Content = Localize.updatePluginCheckboxContent(plugin.Name, plugin.CurrentVersion, plugin.NewVersion), IsChecked = true, Margin = new Thickness(0, 5, 0, 5), Tag = plugin, @@ -50,10 +50,7 @@ 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); - } + Plugins.Remove(plugin); } private void BtnCancel_OnClick(object sender, RoutedEventArgs e) @@ -66,7 +63,7 @@ private void btnUpdate_OnClick(object sender, RoutedEventArgs e) { if (Plugins.Count == 0) { - App.API.ShowMsgBox(App.API.GetTranslation("updatePluginNoSelected")); + App.API.ShowMsgBox(Localize.updatePluginNoSelected()); return; } diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index b4c3aa92b82..55737151af3 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -31,8 +30,8 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.ViewModel; +using iNKORE.UI.WPF.Modern; using JetBrains.Annotations; -using ModernWpf; using Squirrel; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; @@ -184,14 +183,14 @@ public async void CopyToClipboard(string stringToCopy, bool directCopy = false, if (showDefaultNotification) { ShowMsg( - $"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}", - GetTranslation("completedSuccessfully")); + $"{Localize.copy()} {(isFile ? Localize.fileTitle(): Localize.folderTitle())}", + Localize.completedSuccessfully()); } } else { LogException(nameof(PublicAPIInstance), "Failed to copy file/folder to clipboard", exception); - ShowMsgError(GetTranslation("failedToCopy")); + ShowMsgError(Localize.failedToCopy()); } } else @@ -209,14 +208,14 @@ public async void CopyToClipboard(string stringToCopy, bool directCopy = false, if (showDefaultNotification) { ShowMsg( - $"{GetTranslation("copy")} {GetTranslation("textTitle")}", - GetTranslation("completedSuccessfully")); + $"{Localize.copy()} {Localize.textTitle()}", + Localize.completedSuccessfully()); } } else { LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception); - ShowMsgError(GetTranslation("failedToCopy")); + ShowMsgError(Localize.failedToCopy()); } } } @@ -248,7 +247,10 @@ private static async Task RetryActionOnSTAThreadAsync(Action action, public string GetTranslation(string key) => Internationalization.GetTranslation(key); - public List GetAllPlugins() => PluginManager.AllPlugins.ToList(); + public List GetAllPlugins() => PluginManager.GetAllLoadedPlugins(); + + public List GetAllInitializedPlugins(bool includeFailed) => + PluginManager.GetAllInitializedPlugins(includeFailed); public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); @@ -393,18 +395,18 @@ public void OpenDirectory(string directoryPath, string fileNameOrFilePath = null } catch (Win32Exception ex) when (ex.NativeErrorCode == 2) { - LogError(ClassName, "File Manager not found"); + LogException(ClassName, "File Manager not found", ex); ShowMsgError( - GetTranslation("fileManagerNotFoundTitle"), - string.Format(GetTranslation("fileManagerNotFound"), ex.Message) + Localize.fileManagerNotFoundTitle(), + Localize.fileManagerNotFound() ); } catch (Exception ex) { LogException(ClassName, "Failed to open folder", ex); ShowMsgError( - GetTranslation("errorTitle"), - string.Format(GetTranslation("folderOpenError"), ex.Message) + Localize.errorTitle(), + Localize.folderOpenError() ); } } @@ -413,7 +415,7 @@ private void OpenUri(Uri uri, bool? inPrivate = null, bool forceBrowser = false) { if (uri.IsFile && !FilesFolders.FileOrLocationExists(uri.LocalPath)) { - ShowMsgError(GetTranslation("errorTitle"), string.Format(GetTranslation("fileNotFoundError"), uri.LocalPath)); + ShowMsgError(Localize.errorTitle(), Localize.fileNotFoundError(uri.LocalPath)); return; } @@ -439,8 +441,8 @@ private void OpenUri(Uri uri, bool? inPrivate = null, bool forceBrowser = false) var tabOrWindow = browserInfo.OpenInTab ? "tab" : "window"; LogException(ClassName, $"Failed to open URL in browser {tabOrWindow}: {path}, {inPrivate ?? browserInfo.EnablePrivate}, {browserInfo.PrivateArg}", e); ShowMsgError( - GetTranslation("errorTitle"), - GetTranslation("browserOpenError") + Localize.errorTitle(), + Localize.browserOpenError() ); } } @@ -457,7 +459,7 @@ private void OpenUri(Uri uri, bool? inPrivate = null, bool forceBrowser = false) catch (Exception e) { LogException(ClassName, $"Failed to open: {uri.AbsoluteUri}", e); - ShowMsgError(GetTranslation("errorTitle"), e.Message); + ShowMsgError(Localize.errorTitle(), e.Message); } } } diff --git a/Flow.Launcher/ReleaseNotesWindow.xaml b/Flow.Launcher/ReleaseNotesWindow.xaml index f0bdbadda19..6072f40f13c 100644 --- a/Flow.Launcher/ReleaseNotesWindow.xaml +++ b/Flow.Launcher/ReleaseNotesWindow.xaml @@ -7,7 +7,7 @@ xmlns:local="clr-namespace:Flow.Launcher" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mdxam="clr-namespace:MdXaml;assembly=MdXaml" - xmlns:ui="http://schemas.modernwpf.com/2019" + xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" xmlns:vm="clr-namespace:Flow.Launcher.ViewModel" Title="{DynamicResource releaseNotes}" Width="940" @@ -16,6 +16,7 @@ MinHeight="600" Background="{DynamicResource PopuBGColor}" Closed="Window_Closed" + DataContext="{Binding RelativeSource={RelativeSource Self}}" Foreground="{DynamicResource PopupTextColor}" Loaded="Window_Loaded" ResizeMode="CanResize" @@ -44,7 +45,7 @@ - + @@ -161,18 +162,23 @@ Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="5" - Margin="18 0 18 0"> - + Margin="6 0 18 0"> + - + Height="500" + Margin="15 0 0 0" + Padding="0 0 15 0" + HorizontalAlignment="Stretch"> @@ -193,11 +199,11 @@ VerticalScrollBarVisibility="Disabled" Visibility="Collapsed" /> - + - + Properties.Settings.Default.GithubRepo + "/releases"; public ReleaseNotesWindow() { InitializeComponent(); - SeeMore.Uri = ReleaseNotes; - ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged += ThemeManager_ActualApplicationThemeChanged; + ThemeManager.Current.ActualApplicationThemeChanged += ThemeManager_ActualApplicationThemeChanged; } #region Window Events - private void ThemeManager_ActualApplicationThemeChanged(ModernWpf.ThemeManager sender, object args) + private void ThemeManager_ActualApplicationThemeChanged(ThemeManager sender, object args) { Application.Current.Dispatcher.Invoke(() => { - if (ModernWpf.ThemeManager.Current.ActualApplicationTheme == ModernWpf.ApplicationTheme.Light) + if (ThemeManager.Current.ActualApplicationTheme == ApplicationTheme.Light) { MarkdownViewer.MarkdownStyle = (Style)Application.Current.Resources["DocumentStyleGithubLikeLight"]; MarkdownViewer.Foreground = Brushes.Black; @@ -58,7 +58,7 @@ private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e) private void Window_Closed(object sender, EventArgs e) { - ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged; + ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged; } #endregion @@ -132,8 +132,8 @@ private async void RefreshMarkdownViewer() RefreshButton.Visibility = Visibility.Visible; MarkdownViewer.Visibility = Visibility.Collapsed; App.API.ShowMsgError( - App.API.GetTranslation("checkNetworkConnectionTitle"), - App.API.GetTranslation("checkNetworkConnectionSubTitle")); + Localize.checkNetworkConnectionTitle(), + Localize.checkNetworkConnectionSubTitle()); } else { @@ -147,7 +147,6 @@ private async void RefreshMarkdownViewer() private void Grid_SizeChanged(object sender, SizeChangedEventArgs e) { MarkdownScrollViewer.Height = e.NewSize.Height; - MarkdownScrollViewer.Width = e.NewSize.Width; } private void MarkdownViewer_MouseWheel(object sender, MouseWheelEventArgs e) diff --git a/Flow.Launcher/ReportWindow.xaml.cs b/Flow.Launcher/ReportWindow.xaml.cs index ae07679349b..bb0ce0073cf 100644 --- a/Flow.Launcher/ReportWindow.xaml.cs +++ b/Flow.Launcher/ReportWindow.xaml.cs @@ -48,10 +48,10 @@ private void SetException(Exception exception) _ => Constant.IssuesUrl }; - var paragraph = Hyperlink(App.API.GetTranslation("reportWindow_please_open_issue"), websiteUrl); - paragraph.Inlines.Add(string.Format(App.API.GetTranslation("reportWindow_upload_log"), log.FullName)); + var paragraph = Hyperlink(Localize.reportWindow_please_open_issue(), websiteUrl); + paragraph.Inlines.Add(Localize.reportWindow_upload_log(log.FullName)); paragraph.Inlines.Add("\n"); - paragraph.Inlines.Add(App.API.GetTranslation("reportWindow_copy_below")); + paragraph.Inlines.Add(Localize.reportWindow_copy_below()); ErrorTextbox.Document.Blocks.Add(paragraph); StringBuilder content = new StringBuilder(); diff --git a/Flow.Launcher/Resources/Controls/Card.xaml b/Flow.Launcher/Resources/Controls/Card.xaml deleted file mode 100644 index e3c5f819490..00000000000 --- a/Flow.Launcher/Resources/Controls/Card.xaml +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Flow.Launcher/Resources/Controls/Card.xaml.cs b/Flow.Launcher/Resources/Controls/Card.xaml.cs deleted file mode 100644 index 6a70dded2c9..00000000000 --- a/Flow.Launcher/Resources/Controls/Card.xaml.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Windows; -using UserControl = System.Windows.Controls.UserControl; - -namespace Flow.Launcher.Resources.Controls -{ - public partial class Card : UserControl - { - public enum CardType - { - Default, - Inside, - InsideFit, - First, - Middle, - Last - } - - public Card() - { - InitializeComponent(); - } - - public string Title - { - get { return (string)GetValue(TitleProperty); } - set { SetValue(TitleProperty, value); } - } - public static readonly DependencyProperty TitleProperty = - DependencyProperty.Register(nameof(Title), typeof(string), typeof(Card), new PropertyMetadata(string.Empty)); - - public string Sub - { - get { return (string)GetValue(SubProperty); } - set { SetValue(SubProperty, value); } - } - public static readonly DependencyProperty SubProperty = - DependencyProperty.Register(nameof(Sub), typeof(string), typeof(Card), new PropertyMetadata(string.Empty)); - - public string Icon - { - get { return (string)GetValue(IconProperty); } - set { SetValue(IconProperty, value); } - } - public static readonly DependencyProperty IconProperty = - DependencyProperty.Register(nameof(Icon), typeof(string), typeof(Card), new PropertyMetadata(string.Empty)); - - /// - /// Gets or sets additional content for the UserControl - /// - public object AdditionalContent - { - get { return (object)GetValue(AdditionalContentProperty); } - set { SetValue(AdditionalContentProperty, value); } - } - public static readonly DependencyProperty AdditionalContentProperty = - DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(Card), - new PropertyMetadata(null)); - public CardType Type - { - get { return (CardType)GetValue(TypeProperty); } - set { SetValue(TypeProperty, value); } - } - public static readonly DependencyProperty TypeProperty = - DependencyProperty.Register(nameof(Type), typeof(CardType), typeof(Card), - new PropertyMetadata(CardType.Default)); - } -} diff --git a/Flow.Launcher/Resources/Controls/CardGroup.xaml b/Flow.Launcher/Resources/Controls/CardGroup.xaml deleted file mode 100644 index f48bf4b6c9f..00000000000 --- a/Flow.Launcher/Resources/Controls/CardGroup.xaml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - diff --git a/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs b/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs deleted file mode 100644 index b9588275c7f..00000000000 --- a/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Windows; -using System.Windows.Controls; - -namespace Flow.Launcher.Resources.Controls; - -public partial class CardGroup : UserControl -{ - public enum CardGroupPosition - { - NotInGroup, - First, - Middle, - Last - } - - public new ObservableCollection Content - { - get { return (ObservableCollection)GetValue(ContentProperty); } - set { SetValue(ContentProperty, value); } - } - - public static new readonly DependencyProperty ContentProperty = - DependencyProperty.Register(nameof(Content), typeof(ObservableCollection), typeof(CardGroup)); - - public static readonly DependencyProperty PositionProperty = DependencyProperty.RegisterAttached( - "Position", typeof(CardGroupPosition), typeof(CardGroup), - new FrameworkPropertyMetadata(CardGroupPosition.NotInGroup, FrameworkPropertyMetadataOptions.AffectsRender) - ); - - public static void SetPosition(UIElement element, CardGroupPosition value) - { - element.SetValue(PositionProperty, value); - } - - public static CardGroupPosition GetPosition(UIElement element) - { - return (CardGroupPosition)element.GetValue(PositionProperty); - } - - public CardGroup() - { - InitializeComponent(); - Content = new ObservableCollection(); - } -} diff --git a/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs b/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs deleted file mode 100644 index 605934e80c1..00000000000 --- a/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Flow.Launcher.Resources.Controls; - -public class CardGroupCardStyleSelector : StyleSelector -{ - public Style FirstStyle { get; set; } - public Style MiddleStyle { get; set; } - public Style LastStyle { get; set; } - - public override Style SelectStyle(object item, DependencyObject container) - { - var itemsControl = ItemsControl.ItemsControlFromItemContainer(container); - var index = itemsControl.ItemContainerGenerator.IndexFromContainer(container); - - if (index == 0) return FirstStyle; - if (index == itemsControl.Items.Count - 1) return LastStyle; - return MiddleStyle; - } -} diff --git a/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs b/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs new file mode 100644 index 00000000000..78985108ce2 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs @@ -0,0 +1,253 @@ +using iNKORE.UI.WPF.Modern.Controls; +using iNKORE.UI.WPF.Modern.Controls.Helpers; +using iNKORE.UI.WPF.Modern.Controls.Primitives; +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace Flow.Launcher.Resources.Controls +{ + // TODO: Use IsScrollAnimationEnabled property in future: https://github.com/iNKORE-NET/UI.WPF.Modern/pull/347 + public class CustomScrollViewerEx : ScrollViewer + { + private double LastVerticalLocation = 0; + private double LastHorizontalLocation = 0; + + public CustomScrollViewerEx() + { + Loaded += OnLoaded; + var valueSource = DependencyPropertyHelper.GetValueSource(this, AutoPanningMode.IsEnabledProperty).BaseValueSource; + if (valueSource == BaseValueSource.Default) + { + AutoPanningMode.SetIsEnabled(this, true); + } + } + + #region Orientation + + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(CustomScrollViewerEx), + new PropertyMetadata(Orientation.Vertical)); + + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + #endregion + + #region AutoHideScrollBars + + public static readonly DependencyProperty AutoHideScrollBarsProperty = + ScrollViewerHelper.AutoHideScrollBarsProperty + .AddOwner( + typeof(CustomScrollViewerEx), + new PropertyMetadata(true, OnAutoHideScrollBarsChanged)); + + public bool AutoHideScrollBars + { + get => (bool)GetValue(AutoHideScrollBarsProperty); + set => SetValue(AutoHideScrollBarsProperty, value); + } + + private static void OnAutoHideScrollBarsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is CustomScrollViewerEx sv) + { + sv.UpdateVisualState(); + } + } + + #endregion + + private void OnLoaded(object sender, RoutedEventArgs e) + { + LastVerticalLocation = VerticalOffset; + LastHorizontalLocation = HorizontalOffset; + UpdateVisualState(false); + } + + /// + protected override void OnInitialized(EventArgs e) + { + base.OnInitialized(e); + + if (Style == null && ReadLocalValue(StyleProperty) == DependencyProperty.UnsetValue) + { + SetResourceReference(StyleProperty, typeof(ScrollViewer)); + } + } + + /// + protected override void OnMouseWheel(MouseWheelEventArgs e) + { + var Direction = GetDirection(); + ScrollViewerBehavior.SetIsAnimating(this, true); + + if (Direction == Orientation.Vertical) + { + if (ScrollableHeight > 0) + { + e.Handled = true; + } + + var WheelChange = e.Delta * (ViewportHeight / 1.5) / ActualHeight; + var newOffset = LastVerticalLocation - WheelChange; + + if (newOffset < 0) + { + newOffset = 0; + } + + if (newOffset > ScrollableHeight) + { + newOffset = ScrollableHeight; + } + + if (newOffset == LastVerticalLocation) + { + return; + } + + ScrollToVerticalOffset(LastVerticalLocation); + + ScrollToValue(newOffset, Direction); + LastVerticalLocation = newOffset; + } + else + { + if (ScrollableWidth > 0) + { + e.Handled = true; + } + + var WheelChange = e.Delta * (ViewportWidth / 1.5) / ActualWidth; + var newOffset = LastHorizontalLocation - WheelChange; + + if (newOffset < 0) + { + newOffset = 0; + } + + if (newOffset > ScrollableWidth) + { + newOffset = ScrollableWidth; + } + + if (newOffset == LastHorizontalLocation) + { + return; + } + + ScrollToHorizontalOffset(LastHorizontalLocation); + + ScrollToValue(newOffset, Direction); + LastHorizontalLocation = newOffset; + } + } + + /// + protected override void OnScrollChanged(ScrollChangedEventArgs e) + { + base.OnScrollChanged(e); + if (!ScrollViewerBehavior.GetIsAnimating(this)) + { + LastVerticalLocation = VerticalOffset; + LastHorizontalLocation = HorizontalOffset; + } + } + + private Orientation GetDirection() + { + var isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + + if (Orientation == Orientation.Horizontal) + { + return isShiftDown ? Orientation.Vertical : Orientation.Horizontal; + } + else + { + return isShiftDown ? Orientation.Horizontal : Orientation.Vertical; + } + } + + /// + /// Causes the to load a new view into the viewport using the specified offsets and zoom factor. + /// + /// A value between 0 and that specifies the distance the content should be scrolled horizontally. + /// A value between 0 and that specifies the distance the content should be scrolled vertically. + /// A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor. + /// if the view is changed; otherwise, . + public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor) + { + return ChangeView(horizontalOffset, verticalOffset, zoomFactor, false); + } + + /// + /// Causes the to load a new view into the viewport using the specified offsets and zoom factor, and optionally disables scrolling animation. + /// + /// A value between 0 and that specifies the distance the content should be scrolled horizontally. + /// A value between 0 and that specifies the distance the content should be scrolled vertically. + /// A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor. + /// to disable zoom/pan animations while changing the view; otherwise, . The default is false. + /// if the view is changed; otherwise, . + public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor, bool disableAnimation) + { + if (disableAnimation) + { + if (horizontalOffset.HasValue) + { + ScrollToHorizontalOffset(horizontalOffset.Value); + } + + if (verticalOffset.HasValue) + { + ScrollToVerticalOffset(verticalOffset.Value); + } + } + else + { + if (horizontalOffset.HasValue) + { + ScrollToHorizontalOffset(LastHorizontalLocation); + ScrollToValue(Math.Min(ScrollableWidth, horizontalOffset.Value), Orientation.Horizontal); + LastHorizontalLocation = horizontalOffset.Value; + } + + if (verticalOffset.HasValue) + { + ScrollToVerticalOffset(LastVerticalLocation); + ScrollToValue(Math.Min(ScrollableHeight, verticalOffset.Value), Orientation.Vertical); + LastVerticalLocation = verticalOffset.Value; + } + } + + return true; + } + + private void ScrollToValue(double value, Orientation Direction) + { + if (Direction == Orientation.Vertical) + { + ScrollToVerticalOffset(value); + } + else + { + ScrollToHorizontalOffset(value); + } + + ScrollViewerBehavior.SetIsAnimating(this, false); + } + + private void UpdateVisualState(bool useTransitions = true) + { + var stateName = AutoHideScrollBars ? "NoIndicator" : "MouseIndicator"; + VisualStateManager.GoToState(this, stateName, useTransitions); + } + } +} diff --git a/Flow.Launcher/Resources/Controls/ExCard.xaml b/Flow.Launcher/Resources/Controls/ExCard.xaml deleted file mode 100644 index a70c0f4ea46..00000000000 --- a/Flow.Launcher/Resources/Controls/ExCard.xaml +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Flow.Launcher/Resources/Controls/ExCard.xaml.cs b/Flow.Launcher/Resources/Controls/ExCard.xaml.cs deleted file mode 100644 index f149951f039..00000000000 --- a/Flow.Launcher/Resources/Controls/ExCard.xaml.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Flow.Launcher.Resources.Controls -{ - public partial class ExCard : UserControl - { - public ExCard() - { - InitializeComponent(); - } - public string Title - { - get { return (string)GetValue(TitleProperty); } - set { SetValue(TitleProperty, value); } - } - public static readonly DependencyProperty TitleProperty = - DependencyProperty.Register(nameof(Title), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty)); - - public string Sub - { - get { return (string)GetValue(SubProperty); } - set { SetValue(SubProperty, value); } - } - public static readonly DependencyProperty SubProperty = - DependencyProperty.Register(nameof(Sub), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty)); - - public string Icon - { - get { return (string)GetValue(IconProperty); } - set { SetValue(IconProperty, value); } - } - public static readonly DependencyProperty IconProperty = - DependencyProperty.Register(nameof(Icon), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty)); - - /// - /// Gets or sets additional content for the UserControl - /// - public object AdditionalContent - { - get { return (object)GetValue(AdditionalContentProperty); } - set { SetValue(AdditionalContentProperty, value); } - } - public static readonly DependencyProperty AdditionalContentProperty = - DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(ExCard), - new PropertyMetadata(null)); - - public object SideContent - { - get { return (object)GetValue(SideContentProperty); } - set { SetValue(SideContentProperty, value); } - } - public static readonly DependencyProperty SideContentProperty = - DependencyProperty.Register(nameof(SideContent), typeof(object), typeof(ExCard), - new PropertyMetadata(null)); - } -} diff --git a/Flow.Launcher/Resources/Controls/HyperLink.xaml b/Flow.Launcher/Resources/Controls/HyperLink.xaml deleted file mode 100644 index 9ea550afd51..00000000000 --- a/Flow.Launcher/Resources/Controls/HyperLink.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs b/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs deleted file mode 100644 index 855cccdbd60..00000000000 --- a/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Navigation; - -namespace Flow.Launcher.Resources.Controls; - -public partial class HyperLink : UserControl -{ - public static readonly DependencyProperty UriProperty = DependencyProperty.Register( - nameof(Uri), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string)) - ); - - public string Uri - { - get => (string)GetValue(UriProperty); - set => SetValue(UriProperty, value); - } - - public static readonly DependencyProperty TextProperty = DependencyProperty.Register( - nameof(Text), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string)) - ); - - public string Text - { - get => (string)GetValue(TextProperty); - set => SetValue(TextProperty, value); - } - - public HyperLink() - { - InitializeComponent(); - } - - private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) - { - App.API.OpenUrl(e.Uri); - e.Handled = true; - } -} diff --git a/Flow.Launcher/Resources/Controls/InfoBar.xaml b/Flow.Launcher/Resources/Controls/InfoBar.xaml deleted file mode 100644 index 2ddcbdd0cc8..00000000000 --- a/Flow.Launcher/Resources/Controls/InfoBar.xaml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + + + + + + + + + + + - - - - - + + + + + +