diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index 721e14dca..8b305263d 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -3,10 +3,8 @@ using System.IO; using System.Linq; using System.Reflection; using System.Windows; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using Microsoft.Win32; using Squirrel; @@ -17,8 +15,6 @@ namespace Flow.Launcher.Core.Configuration { private static readonly string ClassName = nameof(Portable); - private readonly IPublicAPI API = Ioc.Default.GetRequiredService(); - /// /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish /// @@ -45,13 +41,13 @@ namespace Flow.Launcher.Core.Configuration #endif IndicateDeletion(DataLocation.PortableDataPath); - API.ShowMsgBox(API.GetTranslation("restartToDisablePortableMode")); + PublicApi.Instance.ShowMsgBox(Localize.restartToDisablePortableMode()); UpdateManager.RestartApp(Constant.ApplicationFileName); } catch (Exception e) { - API.LogException(ClassName, "Error occurred while disabling portable mode", e); + PublicApi.Instance.LogException(ClassName, "Error occurred while disabling portable mode", e); } } @@ -68,13 +64,13 @@ namespace Flow.Launcher.Core.Configuration #endif IndicateDeletion(DataLocation.RoamingDataPath); - API.ShowMsgBox(API.GetTranslation("restartToEnablePortableMode")); + PublicApi.Instance.ShowMsgBox(Localize.restartToEnablePortableMode()); UpdateManager.RestartApp(Constant.ApplicationFileName); } catch (Exception e) { - API.LogException(ClassName, "Error occurred while enabling portable mode", e); + PublicApi.Instance.LogException(ClassName, "Error occurred while enabling portable mode", e); } } @@ -94,13 +90,13 @@ namespace Flow.Launcher.Core.Configuration public void MoveUserDataFolder(string fromLocation, string toLocation) { - FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s)); + FilesFolders.CopyAll(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s)); VerifyUserDataAfterMove(fromLocation, toLocation); } public void VerifyUserDataAfterMove(string fromLocation, string toLocation) { - FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s)); + FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s)); } public void CreateShortcuts() @@ -150,12 +146,12 @@ namespace Flow.Launcher.Core.Configuration // delete it and prompt the user to pick the portable data location if (File.Exists(roamingDataDeleteFilePath)) { - FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s)); + FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => PublicApi.Instance.ShowMsgBox(s)); - if (API.ShowMsgBox(API.GetTranslation("moveToDifferentLocation"), + if (PublicApi.Instance.ShowMsgBox(Localize.moveToDifferentLocation(), string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) { - FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s)); + FilesFolders.OpenPath(Constant.RootDirectory, (s) => PublicApi.Instance.ShowMsgBox(s)); Environment.Exit(0); } @@ -164,9 +160,9 @@ namespace Flow.Launcher.Core.Configuration // delete it and notify the user about it. else if (File.Exists(portableDataDeleteFilePath)) { - FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s)); + FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => PublicApi.Instance.ShowMsgBox(s)); - API.ShowMsgBox(API.GetTranslation("shortcutsUninstallerCreated")); + PublicApi.Instance.ShowMsgBox(Localize.shortcutsUninstallerCreated()); } } @@ -177,8 +173,7 @@ namespace Flow.Launcher.Core.Configuration if (roamingLocationExists && portableLocationExists) { - API.ShowMsgBox(string.Format(API.GetTranslation("userDataDuplicated"), - DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); + PublicApi.Instance.ShowMsgBox(Localize.userDataDuplicated(DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); return false; } diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs index 2ff51ff73..7c0290b2a 100644 --- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs @@ -8,7 +8,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Plugin; @@ -18,13 +17,9 @@ namespace Flow.Launcher.Core.ExternalPlugins { private static readonly string ClassName = nameof(CommunityPluginSource); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - private string latestEtag = ""; - private List plugins = new(); + private List plugins = []; private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new() { @@ -41,7 +36,7 @@ namespace Flow.Launcher.Core.ExternalPlugins /// 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 @@ namespace Flow.Launcher.Core.ExternalPlugins .ConfigureAwait(false); latestEtag = response.Headers.ETag?.Tag; - API.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}"); + PublicApi.Instance.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}"); return plugins; } else if (response.StatusCode == HttpStatusCode.NotModified) { - API.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified."); + PublicApi.Instance.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified."); return plugins; } else { - API.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}"); + PublicApi.Instance.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}"); return null; } } catch (OperationCanceledException) when (token.IsCancellationRequested) { - API.LogInfo(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 14796a87a..1a324a993 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Windows; using System.Windows.Forms; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -15,7 +14,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments { private static readonly string ClassName = nameof(AbstractPluginEnvironment); - protected readonly IPublicAPI API = Ioc.Default.GetRequiredService(); + protected readonly IPublicAPI API = PublicApi.Instance; internal abstract string Language { get; } @@ -58,15 +57,10 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments return SetPathForPluginPairs(PluginsSettingsFilePath, Language); } - var noRuntimeMessage = string.Format( - API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"), - Language, - EnvName, - Environment.NewLine - ); + var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine); if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) { - var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName); + var msg = Localize.runtimePluginChooseRuntimeExecutable(EnvName); var selectedFile = GetFileFromDialog(msg, FileDialogFilter); @@ -77,12 +71,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments // Nothing selected because user pressed cancel from the file dialog window else { - var forceDownloadMessage = string.Format( - API.GetTranslation("runtimeExecutableInvalidChooseDownload"), - Language, - EnvName, - Environment.NewLine - ); + var forceDownloadMessage = Localize.runtimeExecutableInvalidChooseDownload(Language, EnvName, Environment.NewLine); // Let users select valid path or choose to download while (string.IsNullOrEmpty(selectedFile)) @@ -120,7 +109,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments } else { - API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language)); + API.ShowMsgBox(Localize.runtimePluginUnableToSetExecutablePath(Language)); API.LogError(ClassName, $"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.", $"{Language}Environment"); @@ -248,7 +237,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments private static string GetUpdatedEnvironmentPath(string filePath) { var index = filePath.IndexOf(DataLocation.PluginEnvironments); - + // get the substring after "Environments" because we can not determine it dynamically var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..]; return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}"; diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs index 89286dfb0..76c775fb4 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -51,7 +51,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments } catch (System.Exception e) { - API.ShowMsgError(API.GetTranslation("failToInstallPythonEnv")); + API.ShowMsgError(Localize.failToInstallPythonEnv()); API.LogException(ClassName, "Failed to install Python environment", e); } }); diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs index 724ae20f4..d8244cbf3 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments } catch (System.Exception e) { - API.ShowMsgError(API.GetTranslation("failToInstallTypeScriptEnv")); + API.ShowMsgError(Localize.failToInstallTypeScriptEnv()); API.LogException(ClassName, "Failed to install TypeScript environment", e); } }); diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs index 6a32664a1..e2de53e39 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments } catch (System.Exception e) { - API.ShowMsgError(API.GetTranslation("failToInstallTypeScriptEnv")); + API.ShowMsgError(Localize.failToInstallTypeScriptEnv()); API.LogException(ClassName, "Failed to install TypeScript environment", e); } }); diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index 1e845498c..eab9a8c43 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 @@ namespace Flow.Launcher.Core.ExternalPlugins private static DateTime lastFetchedAt = DateTime.MinValue; private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - public static List UserPlugins { get; private set; } public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) @@ -61,7 +56,7 @@ namespace Flow.Launcher.Core.ExternalPlugins } catch (Exception e) { - API.LogException(ClassName, "Http request failed", e); + PublicApi.Instance.LogException(ClassName, "Http request failed", e); } finally { @@ -83,12 +78,12 @@ namespace Flow.Launcher.Core.ExternalPlugins } catch (Exception e) { - API.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. " + PublicApi.Instance.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. " + "Plugin excluded from manifest", e); return false; } - API.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, " + PublicApi.Instance.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, " + $"but current version is {Constant.Version}. Plugin excluded from manifest."); return false; diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 1369d7e5d..52eaf0501 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 @@ + @@ -62,6 +64,17 @@ + + + true + + + + + + Languages\en.xaml + + diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index 9212dada6..abefd47bc 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -285,7 +285,7 @@ namespace Flow.Launcher.Core.Plugin HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center, Margin = SettingPanelItemLeftMargin, - Content = API.GetTranslation("select") + Content = Localize.select() }; Btn.Click += (_, _) => diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs index f7457b4e1..c5f0f79a7 100644 --- a/Flow.Launcher.Core/Plugin/PluginConfig.cs +++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs @@ -5,7 +5,6 @@ using System.IO; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; using System.Text.Json; -using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Core.Plugin { @@ -13,10 +12,6 @@ namespace Flow.Launcher.Core.Plugin { private static readonly string ClassName = nameof(PluginConfig); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - /// /// Parse plugin metadata in the given directories /// @@ -38,7 +33,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Can't delete <{directory}>", e); + PublicApi.Instance.LogException(ClassName, $"Can't delete <{directory}>", e); } } else @@ -55,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin duplicateList .ForEach( - x => API.LogWarn(ClassName, + x => PublicApi.Instance.LogWarn(ClassName, string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " + "not loaded due to version not the highest of the duplicates", x.Name, x.ID, x.Version), @@ -107,7 +102,7 @@ namespace Flow.Launcher.Core.Plugin string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName); if (!File.Exists(configPath)) { - API.LogError(ClassName, $"Didn't find config file <{configPath}>"); + PublicApi.Instance.LogError(ClassName, $"Didn't find config file <{configPath}>"); return null; } @@ -123,19 +118,19 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Invalid json for config <{configPath}>", e); + PublicApi.Instance.LogException(ClassName, $"Invalid json for config <{configPath}>", e); return null; } if (!AllowedLanguage.IsAllowed(metadata.Language)) { - API.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>"); + PublicApi.Instance.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>"); return null; } if (!File.Exists(metadata.ExecuteFilePath)) { - API.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}"); + PublicApi.Instance.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}"); return null; } diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index d01b34ab6..6027b712e 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 class PluginInstaller if (!newPlugin.IsFromLocalInstallPath) { await DownloadFileAsync( - $"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}", + $"{Localize.DownloadingPlugin()} {newPlugin.Name}", newPlugin.UrlDownload, filePath, cts); } else @@ -80,7 +74,7 @@ public static class PluginInstaller throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath); } - if (!API.InstallPlugin(newPlugin, filePath)) + if (!PublicApi.Instance.InstallPlugin(newPlugin, filePath)) { return; } @@ -92,23 +86,20 @@ public static class PluginInstaller } catch (Exception e) { - API.LogException(ClassName, "Failed to install plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin")); + PublicApi.Instance.LogException(ClassName, "Failed to install plugin", e); + PublicApi.Instance.ShowMsgError(Localize.ErrorInstallingPlugin()); return; // do not restart on failure } if (Settings.AutoRestartAfterChanging) { - API.RestartApp(); + PublicApi.Instance.RestartApp(); } else { - API.ShowMsg( - API.GetTranslation("installbtn"), - string.Format( - API.GetTranslation( - "InstallSuccessNoRestart"), - newPlugin.Name)); + PublicApi.Instance.ShowMsg( + Localize.installbtn(), + Localize.InstallSuccessNoRestart(newPlugin.Name)); } } @@ -133,24 +124,23 @@ public static class PluginInstaller } catch (Exception e) { - API.LogException(ClassName, "Failed to validate zip file", e); - API.ShowMsgError(API.GetTranslation("ZipFileNotHavePluginJson")); + PublicApi.Instance.LogException(ClassName, "Failed to validate zip file", e); + PublicApi.Instance.ShowMsgError(Localize.ZipFileNotHavePluginJson()); return; } - if (API.PluginModified(plugin.ID)) + if (PublicApi.Instance.PluginModified(plugin.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name), + Localize.pluginModifiedAlreadyMessage()); return; } if (Settings.ShowUnknownSourceWarning) { if (!InstallSourceKnown(plugin.Website) - && API.ShowMsgBox(string.Format( - API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine), - API.GetTranslation("InstallFromUnknownSourceTitle"), + && PublicApi.Instance.ShowMsgBox(Localize.InstallFromUnknownSourceSubtitle(Environment.NewLine), + Localize.InstallFromUnknownSourceTitle(), MessageBoxButton.YesNo) == MessageBoxResult.No) return; } @@ -165,51 +155,46 @@ public static class PluginInstaller /// 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 class PluginInstaller /// 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 class PluginInstaller if (!newPlugin.IsFromLocalInstallPath) { await DownloadFileAsync( - $"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}", + $"{Localize.DownloadingPlugin()} {newPlugin.Name}", newPlugin.UrlDownload, filePath, cts); } else @@ -251,30 +234,27 @@ public static class PluginInstaller return; } - if (!await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath)) + if (!await PublicApi.Instance.UpdatePluginAsync(oldPlugin, newPlugin, filePath)) { return; } } catch (Exception e) { - API.LogException(ClassName, "Failed to update plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); + PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e); + PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin()); return; // do not restart on failure } if (Settings.AutoRestartAfterChanging) { - API.RestartApp(); + PublicApi.Instance.RestartApp(); } else { - API.ShowMsg( - API.GetTranslation("updatebtn"), - string.Format( - API.GetTranslation( - "UpdateSuccessNoRestart"), - newPlugin.Name)); + PublicApi.Instance.ShowMsg( + Localize.updatebtn(), + Localize.UpdateSuccessNoRestart(newPlugin.Name)); } } @@ -289,17 +269,17 @@ public static class PluginInstaller public static async Task CheckForPluginUpdatesAsync(Action> updateAllPlugins, bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default) { // Update the plugin manifest - await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token); + await PublicApi.Instance.UpdatePluginManifestAsync(usePrimaryUrlOnly, token); // Get all plugins that can be updated var resultsForUpdate = ( - from existingPlugin in API.GetAllPlugins() - join pluginUpdateSource in API.GetPluginManifest() + from existingPlugin in PublicApi.Instance.GetAllPlugins() + join pluginUpdateSource in PublicApi.Instance.GetPluginManifest() on existingPlugin.Metadata.ID equals pluginUpdateSource.ID where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version, StringComparison.InvariantCulture) < 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest) - && !API.PluginModified(existingPlugin.Metadata.ID) + && !PublicApi.Instance.PluginModified(existingPlugin.Metadata.ID) select new PluginUpdateInfo() { @@ -314,25 +294,25 @@ public static class PluginInstaller }).ToList(); // No updates - if (!resultsForUpdate.Any()) + if (resultsForUpdate.Count == 0) { if (!silentUpdate) { - API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle")); + PublicApi.Instance.ShowMsg(Localize.updateNoResultTitle(), Localize.updateNoResultSubtitle()); } return; } // If all plugins are modified, just return - if (resultsForUpdate.All(x => API.PluginModified(x.ID))) + if (resultsForUpdate.All(x => PublicApi.Instance.PluginModified(x.ID))) { return; } // Show message box with button to update all plugins - API.ShowMsgWithButton( - API.GetTranslation("updateAllPluginsTitle"), - API.GetTranslation("updateAllPluginsButtonContent"), + PublicApi.Instance.ShowMsgWithButton( + Localize.updateAllPluginsTitle(), + Localize.updateAllPluginsButtonContent(), () => { updateAllPlugins(resultsForUpdate); @@ -357,7 +337,7 @@ public static class PluginInstaller using var cts = new CancellationTokenSource(); await DownloadFileAsync( - $"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}", + $"{Localize.DownloadingPlugin()} {plugin.PluginNewUserPlugin.Name}", plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts); // check if user cancelled download before installing plugin @@ -366,7 +346,7 @@ public static class PluginInstaller return; } - if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath)) + if (!await PublicApi.Instance.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath)) { return; } @@ -375,8 +355,8 @@ public static class PluginInstaller } catch (Exception e) { - API.LogException(ClassName, "Failed to update plugin", e); - API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); + PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e); + PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin()); } })); @@ -384,13 +364,13 @@ public static class PluginInstaller if (restart) { - API.RestartApp(); + PublicApi.Instance.RestartApp(); } else { - API.ShowMsg( - API.GetTranslation("updatebtn"), - API.GetTranslation("PluginsUpdateSuccessNoRestart")); + PublicApi.Instance.ShowMsg( + Localize.updatebtn(), + Localize.PluginsUpdateSuccessNoRestart()); } } @@ -412,7 +392,7 @@ public static class PluginInstaller if (showProgress) { var exceptionHappened = false; - await API.ShowProgressBoxAsync(progressBoxTitle, + await PublicApi.Instance.ShowProgressBoxAsync(progressBoxTitle, async (reportProgress) => { if (reportProgress == null) @@ -424,18 +404,18 @@ public static class PluginInstaller } else { - await API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false); + await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false); } }, cts.Cancel); // if exception happened while downloading and user does not cancel downloading, // we need to redownload the plugin if (exceptionHappened && (!cts.IsCancellationRequested)) - await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); + await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); } else { - await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); + await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false); } } @@ -462,7 +442,7 @@ public static class PluginInstaller if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost) return false; - return API.GetAllPlugins().Any(x => + return PublicApi.Instance.GetAllPlugins().Any(x => !string.IsNullOrEmpty(x.Metadata.Website) && x.Metadata.Website.StartsWith(constructedUrlPart) ); diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index a4ab8de08..3090212ba 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,7 +6,6 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.DialogJump; @@ -29,10 +28,6 @@ namespace Flow.Launcher.Core.Plugin 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 PluginsSettings Settings; private static readonly ConcurrentBag ModifiedPlugins = new(); @@ -75,12 +70,12 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e); } } - API.SavePluginSettings(); - API.SavePluginCaches(); + PublicApi.Instance.SavePluginSettings(); + PublicApi.Instance.SavePluginCaches(); } public static async ValueTask DisposePluginsAsync() @@ -107,7 +102,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e); } } @@ -218,7 +213,7 @@ namespace Flow.Launcher.Core.Plugin { if (string.IsNullOrEmpty(metadata.AssemblyName)) { - API.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}"); + PublicApi.Instance.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}"); continue; // Skip if AssemblyName is not set, which can happen for erroneous plugins } metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName); @@ -228,7 +223,7 @@ namespace Flow.Launcher.Core.Plugin { if (string.IsNullOrEmpty(metadata.Name)) { - API.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}"); + PublicApi.Instance.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}"); continue; // Skip if Name is not set, which can happen for erroneous plugins } metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name); @@ -249,28 +244,28 @@ namespace Flow.Launcher.Core.Plugin { 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"); } } })); @@ -295,15 +290,12 @@ namespace Flow.Launcher.Core.Plugin } } - if (failedPlugins.Any()) + if (!failedPlugins.IsEmpty) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); - API.ShowMsg( - API.GetTranslation("failedToInitializePluginsTitle"), - string.Format( - API.GetTranslation("failedToInitializePluginsMessage"), - failed - ), + PublicApi.Instance.ShowMsg( + Localize.failedToInitializePluginsTitle(), + Localize.failedToInitializePluginsMessage(failed), "", false ); @@ -326,7 +318,7 @@ namespace Flow.Launcher.Core.Plugin if (dialogJump && plugin.Plugin is not IAsyncDialogJump) return Array.Empty(); - if (API.PluginModified(plugin.Metadata.ID)) + if (PublicApi.Instance.PluginModified(plugin.Metadata.ID)) return Array.Empty(); return new List @@ -347,7 +339,7 @@ namespace Flow.Launcher.Core.Plugin 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(); @@ -391,7 +383,7 @@ namespace Flow.Launcher.Core.Plugin 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 +400,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e); return null; } return results; @@ -421,7 +413,7 @@ namespace Flow.Launcher.Core.Plugin 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,7 +430,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e); return null; } return results; @@ -505,7 +497,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, + PublicApi.Instance.LogException(ClassName, $"Can't load context menus for plugin <{pluginPair.Metadata.Name}>", e); } @@ -636,8 +628,8 @@ namespace Flow.Launcher.Core.Plugin { if (PluginModified(existingVersion.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), existingVersion.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(existingVersion.Name), + Localize.pluginModifiedAlreadyMessage()); return false; } @@ -669,8 +661,8 @@ namespace Flow.Launcher.Core.Plugin { if (checkModified && PluginModified(plugin.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name), + Localize.pluginModifiedAlreadyMessage()); return false; } @@ -689,15 +681,15 @@ namespace Flow.Launcher.Core.Plugin if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath)) { - API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name), - string.Format(API.GetTranslation("fileNotFoundMessage"), pluginFolderPath)); + PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), + Localize.fileNotFoundMessage(pluginFolderPath)); return false; } if (SameOrLesserPluginVersionExists(metadataJsonFilePath)) { - API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name), - API.GetTranslation("pluginExistAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name), + Localize.pluginExistAlreadyMessage()); return false; } @@ -726,7 +718,7 @@ namespace Flow.Launcher.Core.Plugin var newPluginPath = Path.Combine(installDirectory, folderName); - FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s)); + FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s)); try { @@ -735,7 +727,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e); + PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e); } if (checkModified) @@ -750,8 +742,8 @@ namespace Flow.Launcher.Core.Plugin { if (checkModified && PluginModified(plugin.ID)) { - API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name), - API.GetTranslation("pluginModifiedAlreadyMessage")); + PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name), + Localize.pluginModifiedAlreadyMessage()); return false; } @@ -770,7 +762,7 @@ namespace Flow.Launcher.Core.Plugin if (removePluginSettings) { // For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances - if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable) + if (AllowedLanguage.IsDotNet(plugin.Language) && PublicApi.Instance is IRemovable removable) { removable.RemovePluginSettings(plugin.AssemblyName); removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath); @@ -784,9 +776,9 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e); - API.ShowMsgError(API.GetTranslation("failedToRemovePluginSettingsTitle"), - string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); + PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e); + PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginSettingsTitle(), + Localize.failedToRemovePluginSettingsMessage(plugin.Name)); } } @@ -800,9 +792,9 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e); - API.ShowMsgError(API.GetTranslation("failedToRemovePluginCacheTitle"), - string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); + PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e); + PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginCacheTitle(), + Localize.failedToRemovePluginCacheMessage(plugin.Name)); } Settings.RemovePluginSettings(plugin.ID); AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index e9e5ee367..a8a4fba3a 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 @@ namespace Flow.Launcher.Core.Plugin { private static readonly string ClassName = nameof(PluginsLoader); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - public static List Plugins(List metadatas, PluginsSettings settings) { var dotnetPlugins = DotNetPlugins(metadatas); @@ -64,7 +57,7 @@ namespace Flow.Launcher.Core.Plugin foreach (var metadata in metadatas) { - var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () => + var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () => { Assembly assembly = null; IAsyncPlugin plugin = null; @@ -89,19 +82,19 @@ namespace Flow.Launcher.Core.Plugin #else catch (Exception e) when (assembly == null) { - Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e); + PublicApi.Instance.LogException(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); + PublicApi.Instance.LogException(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); + PublicApi.Instance.LogException(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); + PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e); } #endif @@ -121,12 +114,12 @@ namespace Flow.Launcher.Core.Plugin var errorPluginString = string.Join(Environment.NewLine, erroredPlugins); var errorMessage = erroredPlugins.Count > 1 ? - API.GetTranslation("pluginsHaveErrored") : - API.GetTranslation("pluginHasErrored"); + Localize.pluginsHaveErrored(): + Localize.pluginHasErrored(); - API.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + + PublicApi.Instance.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + $"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" + - API.GetTranslation("referToLogs")); + Localize.referToLogs()); } return plugins; diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index 8261feab3..6f373746e 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -6,7 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; @@ -14,14 +13,10 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Resource { - public class Internationalization + 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"; @@ -30,6 +25,7 @@ namespace Flow.Launcher.Core.Resource private readonly List _languageDirectories = []; private readonly List _oldResources = []; private static string SystemLanguageCode; + private readonly SemaphoreSlim _langChangeLock = new(1, 1); public Internationalization(Settings settings) { @@ -103,7 +99,7 @@ namespace Flow.Launcher.Core.Resource var directory = Path.Combine(Constant.ProgramDirectory, Folder); if (!Directory.Exists(directory)) { - API.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>"); + PublicApi.Instance.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>"); return; } @@ -174,7 +170,7 @@ namespace Flow.Launcher.Core.Resource FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase)); if (language == null) { - API.LogError(ClassName, $"Language code can't be found <{languageCode}>"); + PublicApi.Instance.LogError(ClassName, $"Language code can't be found <{languageCode}>"); return AvailableLanguages.English; } else @@ -185,20 +181,33 @@ namespace Flow.Launcher.Core.Resource private async Task ChangeLanguageAsync(Language language, bool updateMetadata = true) { - // Remove old language files and load language - RemoveOldLanguageFiles(); - if (language != AvailableLanguages.English) + await _langChangeLock.WaitAsync(); + + try { - LoadLanguage(language); + // Remove old language files and load language + RemoveOldLanguageFiles(); + if (language != AvailableLanguages.English) + { + LoadLanguage(language); + } + + // Change culture info + ChangeCultureInfo(language.LanguageCode); + + if (updateMetadata) + { + // Raise event for plugins after culture is set + await Task.Run(UpdatePluginMetadataTranslations); + } } - - // Change culture info - ChangeCultureInfo(language.LanguageCode); - - if (updateMetadata) + catch (Exception e) { - // Raise event for plugins after culture is set - await Task.Run(UpdatePluginMetadataTranslations); + PublicApi.Instance.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e); + } + finally + { + _langChangeLock.Release(); } } @@ -240,7 +249,7 @@ namespace Flow.Launcher.Core.Resource // "Do you want to search with pinyin?" string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?"; - if (API.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) + if (PublicApi.Instance.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) return false; return true; @@ -257,6 +266,7 @@ namespace Flow.Launcher.Core.Resource { dicts.Remove(r); } + _oldResources.Clear(); } private void LoadLanguage(Language language) @@ -296,7 +306,7 @@ namespace Flow.Launcher.Core.Resource } else { - API.LogError(ClassName, $"Language path can't be found <{path}>"); + PublicApi.Instance.LogError(ClassName, $"Language path can't be found <{path}>"); var english = Path.Combine(folder, DefaultFile); if (File.Exists(english)) { @@ -304,7 +314,7 @@ namespace Flow.Launcher.Core.Resource } else { - API.LogError(ClassName, $"Default English Language path can't be found <{path}>"); + PublicApi.Instance.LogError(ClassName, $"Default English Language path can't be found <{path}>"); return string.Empty; } } @@ -339,7 +349,7 @@ namespace Flow.Launcher.Core.Resource } else { - API.LogError(ClassName, $"No Translation for key {key}"); + PublicApi.Instance.LogError(ClassName, $"No Translation for key {key}"); return $"No Translation for key {key}"; } } @@ -362,11 +372,21 @@ namespace Flow.Launcher.Core.Resource } catch (Exception e) { - API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); + PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); } } } #endregion + + #region IDisposable + + public void Dispose() + { + RemoveOldLanguageFiles(); + _langChangeLock.Dispose(); + } + + #endregion } } diff --git a/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs b/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs deleted file mode 100644 index 3e1a19a76..000000000 --- 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 a6e8dc6bf..c3bb6190f 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -444,17 +444,27 @@ namespace Flow.Launcher.Core.Resource _api.LogError(ClassName, $"Theme <{theme}> path can't be found"); if (theme != Constant.DefaultTheme) { - _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme)); + _api.ShowMsgBox(Localize.theme_load_failure_path_not_exists(theme)); ChangeTheme(Constant.DefaultTheme); } return false; } - catch (XamlParseException) + catch (XamlParseException e) { - _api.LogError(ClassName, $"Theme <{theme}> fail to parse"); + _api.LogException(ClassName, $"Theme <{theme}> fail to parse xaml", e); if (theme != Constant.DefaultTheme) { - _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme)); + _api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme)); + ChangeTheme(Constant.DefaultTheme); + } + return false; + } + catch (Exception e) + { + _api.LogException(ClassName, $"Theme <{theme}> fail to load", e); + if (theme != Constant.DefaultTheme) + { + _api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme)); ChangeTheme(Constant.DefaultTheme); } return false; diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 45275696c..1f138e843 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -41,8 +41,8 @@ namespace Flow.Launcher.Core try { if (!silentUpdate) - _api.ShowMsg(_api.GetTranslation("pleaseWait"), - _api.GetTranslation("update_flowlauncher_update_check")); + _api.ShowMsg(Localize.pleaseWait(), + Localize.update_flowlauncher_update_check()); using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false); @@ -58,13 +58,13 @@ namespace Flow.Launcher.Core if (newReleaseVersion <= currentVersion) { if (!silentUpdate) - _api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest")); + _api.ShowMsgBox(Localize.update_flowlauncher_already_on_latest()); return; } if (!silentUpdate) - _api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"), - _api.GetTranslation("update_flowlauncher_updating")); + _api.ShowMsg(Localize.update_flowlauncher_update_found(), + Localize.update_flowlauncher_updating()); await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); @@ -77,10 +77,7 @@ namespace Flow.Launcher.Core FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)); if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s))) - _api.ShowMsgBox(string.Format( - _api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), - DataLocation.PortableDataPath, - targetDestination)); + _api.ShowMsgBox(Localize.update_flowlauncher_fail_moving_portable_user_profile_data(DataLocation.PortableDataPath, targetDestination)); } else { @@ -91,7 +88,7 @@ namespace Flow.Launcher.Core _api.LogInfo(ClassName, $"Update success:{newVersionTips}"); - if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), + if (_api.ShowMsgBox(newVersionTips, Localize.update_flowlauncher_new_update(), MessageBoxButton.YesNo) == MessageBoxResult.Yes) { UpdateManager.RestartApp(Constant.ApplicationFileName); @@ -111,8 +108,8 @@ namespace Flow.Launcher.Core } if (!silentUpdate) - _api.ShowMsgError(_api.GetTranslation("update_flowlauncher_fail"), - _api.GetTranslation("update_flowlauncher_check_connection")); + _api.ShowMsgError(Localize.update_flowlauncher_fail(), + Localize.update_flowlauncher_check_connection()); } finally { @@ -150,9 +147,9 @@ namespace Flow.Launcher.Core return manager; } - private string NewVersionTips(string version) + private static string NewVersionTips(string version) { - var tips = string.Format(_api.GetTranslation("newVersionTips"), version); + var tips = Localize.newVersionTips(version); return tips; } diff --git a/Flow.Launcher.Core/packages.lock.json b/Flow.Launcher.Core/packages.lock.json index b7a00d94d..ec30e484a 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, )", @@ -83,6 +89,11 @@ "resolved": "1.0.0", "contentHash": "nwbZAYd+DblXAIzlnwDSnl0CiCm8jWLfHSYnoN4wYhtIav6AegB3+T/vKzLbU2IZlPB8Bvl8U3NXpx3eaz+N5w==" }, + "ini-parser": { + "type": "Transitive", + "resolved": "2.5.2", + "contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg==" + }, "InputSimulator": { "type": "Transitive", "resolved": "1.0.4", @@ -254,6 +265,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, )", @@ -263,7 +275,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 65652878f..9035a541d 100644 --- a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -58,21 +58,17 @@ namespace Flow.Launcher.Infrastructure.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 DialogJumpExplorerPair _lastExplorer = null; - private static readonly object _lastExplorerLock = new(); + private static readonly Lock _lastExplorerLock = new(); private static readonly Dictionary _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 +85,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump private static DispatcherTimer _dragMoveTimer = null; // A list of all file dialog windows that are auto switched already - private static readonly List _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; @@ -315,7 +311,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump { foreach (var explorer in _dialogJumpExplorers.Keys) { - if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(explorer.Metadata.ID) || // Plugin is modified explorer.Metadata.Disabled) continue; // Plugin is disabled var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd); @@ -493,7 +489,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump var dialogWindowChanged = false; foreach (var dialog in _dialogJumpDialogs.Keys) { - if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled IDialogJumpDialogWindow dialogWindow; @@ -596,7 +592,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump { foreach (var explorer in _dialogJumpExplorers.Keys) { - if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(explorer.Metadata.ID) || // Plugin is modified explorer.Metadata.Disabled) continue; // Plugin is disabled var explorerWindow = explorer.Plugin.CheckExplorerWindow(hwnd); @@ -871,7 +867,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump // Then check all dialog windows foreach (var dialog in _dialogJumpDialogs.Keys) { - if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled var dialogWindow = _dialogJumpDialogs[dialog]; @@ -884,7 +880,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump // Finally search for the dialog window again foreach (var dialog in _dialogJumpDialogs.Keys) { - if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified dialog.Metadata.Disabled) continue; // Plugin is disabled IDialogJumpDialogWindow dialogWindow; @@ -1067,11 +1063,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump _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 1085cc833..6e2d86849 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 @@ namespace Flow.Launcher.Infrastructure /// 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 @@ namespace Flow.Launcher.Infrastructure /// 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 5b4eaf893..4cde3f6e0 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 8afab419b..f8c111f36 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -4,10 +4,8 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; using JetBrains.Annotations; namespace Flow.Launcher.Infrastructure.Http @@ -20,10 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http private static readonly HttpClient client = new(); - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - static Http() { // need to be added so it would work on a win10 machine @@ -82,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Http } catch (UriFormatException e) { - API.ShowMsgError(API.GetTranslation("pleaseTryAgain"), API.GetTranslation("parseProxyFailed")); + PublicApi.Instance.ShowMsgError(Localize.pleaseTryAgain(), Localize.parseProxyFailed()); Log.Exception(ClassName, "Unable to parse Uri", e); } } diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 64d323de6..598347fd2 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -22,7 +22,7 @@ namespace Flow.Launcher.Infrastructure.Image private static Lock storageLock { get; } = new(); private static BinaryStorage> _storage; private static readonly ConcurrentDictionary GuidToKey = new(); - private static IImageHashGenerator _hashGenerator; + private static ImageHashGenerator _hashGenerator; private static readonly bool EnableImageHash = true; public static ImageSource Image => ImageCache[Constant.ImageIcon, false]; public static ImageSource MissingImage => ImageCache[Constant.MissingImgIcon, false]; @@ -31,7 +31,7 @@ namespace Flow.Launcher.Infrastructure.Image public const int FullIconSize = 256; public const int FullImageSize = 320; - private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" }; + private static readonly string[] ImageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"]; private static readonly string SvgExtension = ".svg"; public static async Task InitializeAsync() @@ -327,7 +327,7 @@ namespace Flow.Launcher.Infrastructure.Image return img; } - private static ImageSource LoadFullImage(string path) + private static BitmapImage LoadFullImage(string path) { BitmapImage image = new BitmapImage(); image.BeginInit(); @@ -364,7 +364,7 @@ namespace Flow.Launcher.Infrastructure.Image return image; } - private static ImageSource LoadSvgImage(string path, bool loadFullImage = false) + private static RenderTargetBitmap LoadSvgImage(string path, bool loadFullImage = false) { // Set up drawing settings var desiredHeight = loadFullImage ? FullImageSize : SmallIconSize; diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 4ce0df026..86f757eb8 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 @@ namespace Flow.Launcher.Infrastructure.Image 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 @@ namespace Flow.Launcher.Infrastructure.Image } } + /// + /// 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 @@ namespace Flow.Launcher.Infrastructure.Image 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/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index eb844dd7c..cd072f635 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -85,5 +85,10 @@ QueryFullProcessImageName EVENT_OBJECT_HIDE EVENT_SYSTEM_DIALOGEND +DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS WM_POWERBROADCAST -PBT_APMRESUMEAUTOMATIC \ No newline at end of file +PBT_APMRESUMEAUTOMATIC +PBT_APMRESUMESUSPEND +PowerRegisterSuspendResumeNotification +PowerUnregisterSuspendResumeNotification +DeviceNotifyCallbackRoutine \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs index 24584115d..009b27666 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs @@ -1,11 +1,13 @@ +using System.Text.Json.Serialization; using Flow.Launcher.Plugin; -using System.Text.Json.Serialization; namespace Flow.Launcher.Infrastructure.UserSettings { public class CustomBrowserViewModel : BaseModel { public string Name { get; set; } + [JsonIgnore] + public string DisplayName => Name == "Default" ? Localize.defaultBrowser_default() : Name; public string Path { get; set; } public string PrivateArg { get; set; } public bool EnablePrivate { get; set; } @@ -26,8 +28,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings Editable = Editable }; } + + public void OnDisplayNameChanged() + { + OnPropertyChanged(nameof(DisplayName)); + } } } - - - diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs index c54c30478..ae406f4c5 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs @@ -1,10 +1,13 @@ -using Flow.Launcher.Plugin; +using System.Text.Json.Serialization; +using Flow.Launcher.Plugin; -namespace Flow.Launcher.ViewModel +namespace Flow.Launcher.Infrastructure.UserSettings { public class CustomExplorerViewModel : BaseModel { public string Name { get; set; } + [JsonIgnore] + 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\""; @@ -21,5 +24,10 @@ namespace Flow.Launcher.ViewModel Editable = Editable }; } + + public void OnDisplayNameChanged() + { + OnPropertyChanged(nameof(DisplayName)); + } } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs index 2603d4675..a2e95b668 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 @@ namespace Flow.Launcher.Infrastructure.UserSettings { public string Description { get; set; } - public string LocalizedDescription => API.GetTranslation(Description); - - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + public string LocalizedDescription => PublicApi.Instance.GetTranslation(Description); public BaseBuiltinShortcutModel(string key, string description) { diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index 5b948e450..de9cb841e 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -7,8 +7,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings { public const string PortableFolderName = "UserData"; public const string DeletionIndicatorFile = ".dead"; - public static string PortableDataPath = Path.Combine(Constant.ProgramDirectory, PortableFolderName); - public static string RoamingDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher"); + public static readonly string PortableDataPath = Path.Combine(Constant.ProgramDirectory, PortableFolderName); + public static readonly string RoamingDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher"); public static string DataDirectory() { if (PortableDataLocationInUse()) @@ -19,7 +19,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings public static bool PortableDataLocationInUse() { - if (Directory.Exists(PortableDataPath) && !File.Exists(DeletionIndicatorFile)) + if (Directory.Exists(PortableDataPath) && + !File.Exists(Path.Combine(PortableDataPath, DeletionIndicatorFile))) return true; return false; diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 0c839c497..051326dbc 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -9,7 +9,6 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; -using Flow.Launcher.ViewModel; namespace Flow.Launcher.Infrastructure.UserSettings { diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 811733925..8a41e12b4 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; @@ -19,6 +19,7 @@ using Microsoft.Win32.SafeHandles; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dwm; +using Windows.Win32.System.Power; using Windows.Win32.System.Threading; using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.Shell.Common; @@ -338,9 +339,6 @@ namespace Flow.Launcher.Infrastructure public const int SC_MAXIMIZE = (int)PInvoke.SC_MAXIMIZE; public const int SC_MINIMIZE = (int)PInvoke.SC_MINIMIZE; - public const int WM_POWERBROADCAST = (int)PInvoke.WM_POWERBROADCAST; - public const int PBT_APMRESUMEAUTOMATIC = (int)PInvoke.PBT_APMRESUMEAUTOMATIC; - #endregion #region Window Handle @@ -904,5 +902,119 @@ namespace Flow.Launcher.Infrastructure } #endregion + + #region File / Folder Dialog + + public static string SelectFile() + { + var dlg = new OpenFileDialog(); + var result = dlg.ShowDialog(); + if (result == true) + return dlg.FileName; + + return string.Empty; + } + + #endregion + + #region Sleep Mode Listener + + private static Action _func; + private static PDEVICE_NOTIFY_CALLBACK_ROUTINE _callback = null; + private static DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS _recipient; + private static SafeHandle _recipientHandle; + private static HPOWERNOTIFY _handle = HPOWERNOTIFY.Null; + + /// + /// Registers a listener for sleep mode events. + /// Inspired from: https://github.com/XKaguya/LenovoLegionToolkit + /// https://blog.csdn.net/mochounv/article/details/114668594 + /// + /// + /// + public static unsafe void RegisterSleepModeListener(Action func) + { + if (_callback != null) + { + // Only register if not already registered + return; + } + + _func = func; + _callback = new PDEVICE_NOTIFY_CALLBACK_ROUTINE(DeviceNotifyCallback); + _recipient = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS() + { + Callback = _callback, + Context = null + }; + + _recipientHandle = new StructSafeHandle(_recipient); + _handle = PInvoke.PowerRegisterSuspendResumeNotification( + REGISTER_NOTIFICATION_FLAGS.DEVICE_NOTIFY_CALLBACK, + _recipientHandle, + out var handle) == WIN32_ERROR.ERROR_SUCCESS ? + new HPOWERNOTIFY(new IntPtr(handle)) : + HPOWERNOTIFY.Null; + if (_handle.IsNull) + { + throw new Win32Exception("Error registering for power notifications: " + Marshal.GetLastWin32Error()); + } + } + + /// + /// Unregisters the sleep mode listener. + /// + public static void UnregisterSleepModeListener() + { + if (!_handle.IsNull) + { + PInvoke.PowerUnregisterSuspendResumeNotification(_handle); + _handle = HPOWERNOTIFY.Null; + _func = null; + _callback = null; + _recipientHandle = null; + } + } + + private static unsafe uint DeviceNotifyCallback(void* context, uint type, void* setting) + { + switch (type) + { + case PInvoke.PBT_APMRESUMEAUTOMATIC: + // Operation is resuming automatically from a low-power state.This message is sent every time the system resumes + _func?.Invoke(); + break; + + case PInvoke.PBT_APMRESUMESUSPEND: + // Operation is resuming from a low-power state.This message is sent after PBT_APMRESUMEAUTOMATIC if the resume is triggered by user input, such as pressing a key + _func?.Invoke(); + break; + } + + return 0; + } + + private sealed class StructSafeHandle : SafeHandle where T : struct + { + private readonly nint _ptr = nint.Zero; + + public StructSafeHandle(T recipient) : base(nint.Zero, true) + { + var pRecipient = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(recipient, pRecipient, false); + SetHandle(pRecipient); + _ptr = pRecipient; + } + + public override bool IsInvalid => handle == nint.Zero; + + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(_ptr); + return true; + } + } + + #endregion } } diff --git a/Flow.Launcher.Infrastructure/packages.lock.json b/Flow.Launcher.Infrastructure/packages.lock.json index 47c94d5f6..db77f9d93 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/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 6c506cfc0..3af57f00d 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -150,6 +150,16 @@ namespace Flow.Launcher.Plugin.SharedCommands return File.Exists(filePath); } + /// + /// Checks if a file or directory exists + /// + /// + /// + public static bool FileOrLocationExists(this string path) + { + return LocationExists(path) || FileExists(path); + } + /// /// Open a directory window (using the OS's default handler, usually explorer) /// diff --git a/Flow.Launcher/ActionKeywords.xaml.cs b/Flow.Launcher/ActionKeywords.xaml.cs index 8e05686c9..a94b265fc 100644 --- a/Flow.Launcher/ActionKeywords.xaml.cs +++ b/Flow.Launcher/ActionKeywords.xaml.cs @@ -47,7 +47,7 @@ namespace Flow.Launcher if (addedActionKeywords.Any(App.API.ActionKeywordAssigned)) { - App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned")); + App.API.ShowMsgBox(Localize.newActionKeywordsHasBeenAssigned()); return; } @@ -63,7 +63,7 @@ namespace Flow.Launcher if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords)) { // User just changes the sequence of action keywords - App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld")); + App.API.ShowMsgBox(Localize.newActionKeywordsSameAsOld()); } else { diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index 565bbe3c7..e922cd558 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 0360c761e..1ca3ce2c6 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -22,6 +22,7 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.SettingPages.ViewModels; using Flow.Launcher.ViewModel; +using iNKORE.UI.WPF.Modern.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.VisualStudio.Threading; @@ -45,6 +46,7 @@ namespace Flow.Launcher private static Settings _settings; private static MainWindow _mainWindow; private readonly MainViewModel _mainVM; + private readonly Internationalization _internationalization; // To prevent two disposals running at the same time. private static readonly object _disposingLock = new(); @@ -55,6 +57,9 @@ namespace Flow.Launcher public App() { + // Do not use bitmap cache since it can cause WPF second window freezing issue + ShadowAssist.UseBitmapCache = false; + // Initialize settings _settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled(); @@ -107,6 +112,7 @@ namespace Flow.Launcher API = Ioc.Default.GetRequiredService(); _settings.Initialize(); _mainVM = Ioc.Default.GetRequiredService(); + _internationalization = Ioc.Default.GetRequiredService(); } catch (Exception e) { @@ -193,7 +199,7 @@ namespace Flow.Launcher Win32Helper.EnableWin32DarkMode(_settings.ColorScheme); // Initialize language before portable clean up since it needs translations - await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + await _internationalization.InitializeLanguageAsync(); Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate(); @@ -274,7 +280,7 @@ namespace Flow.Launcher // but if it fails (permissions, etc) then don't keep retrying // this also gives the user a visual indication in the Settings widget _settings.StartFlowLauncherOnSystemStartup = false; - API.ShowMsgError(API.GetTranslation("setAutoStartFailed"), e.Message); + API.ShowMsgError(Localize.setAutoStartFailed(), e.Message); } } } @@ -421,6 +427,7 @@ namespace Flow.Launcher _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); DialogJump.Dispose(); + _internationalization.Dispose(); } API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------"); diff --git a/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs b/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs index 41e879913..82da6d936 100644 --- a/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs +++ b/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs @@ -5,7 +5,7 @@ using System.Windows.Input; namespace Flow.Launcher.Converters; -internal class BoolToIMEConversionModeConverter : IValueConverter +public class BoolToIMEConversionModeConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { @@ -22,7 +22,7 @@ internal class BoolToIMEConversionModeConverter : IValueConverter } } -internal class BoolToIMEStateConverter : IValueConverter +public class BoolToIMEStateConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { diff --git a/Flow.Launcher/Converters/CornerRadiusFilterConverter.cs b/Flow.Launcher/Converters/CornerRadiusFilterConverter.cs new file mode 100644 index 000000000..fd43cafac --- /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 000000000..130d04e16 --- /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 000000000..594787027 --- /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 21bf584e7..b7bca41c5 100644 --- a/Flow.Launcher/Converters/StringToKeyBindingConverter.cs +++ b/Flow.Launcher/Converters/StringToKeyBindingConverter.cs @@ -5,7 +5,7 @@ using System.Windows.Input; namespace Flow.Launcher.Converters; -class StringToKeyBindingConverter : IValueConverter +public class StringToKeyBindingConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs index 2ee08bf85..3bba2c5b8 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs @@ -41,7 +41,7 @@ namespace Flow.Launcher if (string.IsNullOrEmpty(Hotkey) && string.IsNullOrEmpty(ActionKeyword)) { - App.API.ShowMsgBox(App.API.GetTranslation("emptyPluginHotkey")); + App.API.ShowMsgBox(Localize.emptyPluginHotkey()); return; } diff --git a/Flow.Launcher/CustomShortcutSetting.xaml.cs b/Flow.Launcher/CustomShortcutSetting.xaml.cs index f4644a267..317d059a1 100644 --- a/Flow.Launcher/CustomShortcutSetting.xaml.cs +++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs @@ -40,14 +40,14 @@ namespace Flow.Launcher { if (string.IsNullOrEmpty(Key) || string.IsNullOrEmpty(Value)) { - App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut")); + App.API.ShowMsgBox(Localize.emptyShortcut()); return; } // Check if key is modified or adding a new one if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key)) { - App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut")); + App.API.ShowMsgBox(Localize.duplicateShortcut()); return; } diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index a99d4d8c2..8c7670426 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/BorderHelper.cs b/Flow.Launcher/Helper/BorderHelper.cs new file mode 100644 index 000000000..0f2a78e7d --- /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 86a68475e..bb1cddc6c 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -61,8 +61,8 @@ internal static class HotKeyMapper string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}", e.Message, e.StackTrace)); - string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); - string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + string errorMsg = Localize.registerHotkeyFailed(hotkeyStr); + string errorMsgTitle = Localize.MessageBoxTitle(); App.API.ShowMsgBox(errorMsg, errorMsgTitle); } } @@ -87,8 +87,8 @@ internal static class HotKeyMapper e.Message, e.StackTrace, hotkeyStr)); - string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); - string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + string errorMsg = Localize.registerHotkeyFailed(hotkeyStr); + string errorMsgTitle = Localize.MessageBoxTitle(); App.API.ShowMsgBox(errorMsg, errorMsgTitle); } } @@ -112,8 +112,8 @@ internal static class HotKeyMapper string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}", e.Message, e.StackTrace)); - string errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr); - string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + string errorMsg = Localize.unregisterHotkeyFailed(hotkeyStr); + string errorMsgTitle = Localize.MessageBoxTitle(); App.API.ShowMsgBox(errorMsg, errorMsgTitle); } } diff --git a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs index 93b9a8aaa..fd04b3e88 100644 --- a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs +++ b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -16,7 +17,7 @@ public static class WallpaperPathRetrieval private const int MaxCacheSize = 3; private static readonly Dictionary<(string, DateTime), ImageBrush> WallpaperCache = new(); - private static readonly object CacheLock = new(); + private static readonly Lock CacheLock = new(); public static Brush GetWallpaperBrush() { @@ -31,7 +32,7 @@ public static class WallpaperPathRetrieval var wallpaperPath = Win32Helper.GetWallpaperPath(); if (string.IsNullOrEmpty(wallpaperPath) || !File.Exists(wallpaperPath)) { - App.API.LogInfo(ClassName, $"Wallpaper path is invalid: {wallpaperPath}"); + App.API.LogError(ClassName, $"Wallpaper path is invalid: {wallpaperPath}"); var wallpaperColor = GetWallpaperColor(); return new SolidColorBrush(wallpaperColor); } @@ -47,17 +48,22 @@ public static class WallpaperPathRetrieval return cachedWallpaper; } } - - using var fileStream = File.OpenRead(wallpaperPath); - var decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None); - var frame = decoder.Frames[0]; - var originalWidth = frame.PixelWidth; - var originalHeight = frame.PixelHeight; + + int originalWidth, originalHeight; + // Use `using ()` instead of `using var` sentence here to ensure the wallpaper file is not locked + using (var fileStream = File.OpenRead(wallpaperPath)) + { + var decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None); + var frame = decoder.Frames[0]; + originalWidth = frame.PixelWidth; + originalHeight = frame.PixelHeight; + } if (originalWidth == 0 || originalHeight == 0) { - App.API.LogInfo(ClassName, $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}"); - return new SolidColorBrush(Colors.Transparent); + App.API.LogError(ClassName, $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}"); + var wallpaperColor = GetWallpaperColor(); + return new SolidColorBrush(wallpaperColor); } // Calculate the scaling factor to fit the image within 800x600 while preserving aspect ratio @@ -70,7 +76,9 @@ public static class WallpaperPathRetrieval // Set DecodePixelWidth and DecodePixelHeight to resize the image while preserving aspect ratio var bitmap = new BitmapImage(); bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; // Use OnLoad to ensure the wallpaper file is not locked bitmap.UriSource = new Uri(wallpaperPath); + bitmap.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; bitmap.DecodePixelWidth = decodedPixelWidth; bitmap.DecodePixelHeight = decodedPixelHeight; bitmap.EndInit(); @@ -104,13 +112,13 @@ public static class WallpaperPathRetrieval private static Color GetWallpaperColor() { - RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", false); + using var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", false); var result = key?.GetValue("Background", null); if (result is string strResult) { try { - var parts = strResult.Trim().Split(new[] { ' ' }, 3).Select(byte.Parse).ToList(); + var parts = strResult.Trim().Split([' '], 3).Select(byte.Parse).ToList(); return Color.FromRgb(parts[0], parts[1], parts[2]); } catch (Exception ex) diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 89bfde349..b920b53a7 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 @@ namespace Flow.Launcher private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); - public string EmptyHotkey => App.API.GetTranslation("none"); + public string EmptyHotkey => Localize.none(); public ObservableCollection KeysToDisplay { get; set; } = new(); diff --git a/Flow.Launcher/HotkeyControlDialog.xaml b/Flow.Launcher/HotkeyControlDialog.xaml index d416f1bdc..9fdfda865 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 c7af8c5b8..e1fc86f95 100644 --- a/Flow.Launcher/HotkeyControlDialog.xaml.cs +++ b/Flow.Launcher/HotkeyControlDialog.xaml.cs @@ -9,7 +9,7 @@ using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using ModernWpf.Controls; +using iNKORE.UI.WPF.Modern.Controls; namespace Flow.Launcher; @@ -33,7 +33,7 @@ public partial class HotkeyControlDialog : ContentDialog public EResultType ResultType { get; private set; } = EResultType.Cancel; public string ResultValue { get; private set; } = string.Empty; - public static string EmptyHotkey => App.API.GetTranslation("none"); + public static string EmptyHotkey => Localize.none(); private static bool isOpenFlowHotkey; @@ -41,7 +41,7 @@ public partial class HotkeyControlDialog : ContentDialog { WindowTitle = windowTitle switch { - "" or null => App.API.GetTranslation("hotkeyRegTitle"), + "" or null => Localize.hotkeyRegTitle(), _ => windowTitle }; DefaultHotkey = defaultHotkey; @@ -146,10 +146,7 @@ public partial class HotkeyControlDialog : ContentDialog Alert.Visibility = Visibility.Visible; if (registeredHotkeyData.RemoveHotkey is not null) { - tbMsg.Text = string.Format( - App.API.GetTranslation("hotkeyUnavailableEditable"), - description - ); + tbMsg.Text = Localize.hotkeyUnavailableEditable(description); SaveBtn.IsEnabled = false; SaveBtn.Visibility = Visibility.Collapsed; OverwriteBtn.IsEnabled = true; @@ -158,10 +155,7 @@ public partial class HotkeyControlDialog : ContentDialog } else { - tbMsg.Text = string.Format( - App.API.GetTranslation("hotkeyUnavailableUneditable"), - description - ); + tbMsg.Text = Localize.hotkeyUnavailableUneditable(description); SaveBtn.IsEnabled = false; SaveBtn.Visibility = Visibility.Visible; OverwriteBtn.IsEnabled = false; @@ -175,7 +169,7 @@ public partial class HotkeyControlDialog : ContentDialog if (!CheckHotkeyAvailability(hotkey.Value, true)) { - tbMsg.Text = App.API.GetTranslation("hotkeyUnavailable"); + tbMsg.Text = Localize.hotkeyUnavailable(); Alert.Visibility = Visibility.Visible; SaveBtn.IsEnabled = false; SaveBtn.Visibility = Visibility.Visible; diff --git a/Flow.Launcher/Languages/ar.xaml b/Flow.Launcher/Languages/ar.xaml index 9c252f7a7..b8845c3f5 100644 --- a/Flow.Launcher/Languages/ar.xaml +++ b/Flow.Launcher/Languages/ar.xaml @@ -224,6 +224,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} متجر الإضافات @@ -467,8 +468,10 @@ فتح المجلد Advanced Log Level - Debug + Silent + خطأ Info + Debug Setting Window Font @@ -490,6 +493,7 @@ حجة للملف The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer متصفح الويب الافتراضي @@ -500,6 +504,8 @@ نافذة جديدة تبويب جديد الوضع الخاص + Default + New Profile تغيير الأولوية diff --git a/Flow.Launcher/Languages/cs.xaml b/Flow.Launcher/Languages/cs.xaml index 57415948f..30a1cdbb9 100644 --- a/Flow.Launcher/Languages/cs.xaml +++ b/Flow.Launcher/Languages/cs.xaml @@ -224,6 +224,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} Obchod s pluginy @@ -467,8 +468,10 @@ Open Folder Advanced Log Level - Debug + Silent + Chyba Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Argumenty pro Soubor The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Výchozí prohlížeč @@ -500,6 +504,8 @@ Nové okno Nová karta Soukromý režim + Default + New Profile Změnit prioritu diff --git a/Flow.Launcher/Languages/da.xaml b/Flow.Launcher/Languages/da.xaml index 363d8de9a..067ea16fc 100644 --- a/Flow.Launcher/Languages/da.xaml +++ b/Flow.Launcher/Languages/da.xaml @@ -224,6 +224,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-butik @@ -467,8 +468,10 @@ Open Folder Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Arg for fil The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Default Web Browser @@ -500,6 +504,8 @@ New Window New Tab Privattilstand + Default + New Profile Skift prioritet diff --git a/Flow.Launcher/Languages/de.xaml b/Flow.Launcher/Languages/de.xaml index fc16826bd..529531b58 100644 --- a/Flow.Launcher/Languages/de.xaml +++ b/Flow.Launcher/Languages/de.xaml @@ -224,6 +224,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} Plug-in-Store @@ -467,8 +468,10 @@ Ordner öffnen Erweitert Log-Ebene - Debug + Silent + Fehler Info + Debug Einstellung der Fensterschriftart @@ -490,6 +493,7 @@ Arg For File Der Dateimanager '{0}' konnte nicht unter '{1}' gefunden werden. Möchten Sie fortfahren? Pfadfehler bei Dateimanager + File Explorer Webbrowser per Default @@ -500,6 +504,8 @@ Neues Fenster Neuer Tab Privater Modus + Default + New Profile Priorität ändern diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 7fd10272a..0f44d77df 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -214,6 +214,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 @@ -224,6 +226,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 @@ -492,6 +495,7 @@ Arg For File The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Default Web Browser @@ -502,6 +506,8 @@ New Window New Tab Private Mode + Default + New Profile Change Priority @@ -589,8 +595,9 @@ 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} Please wait... diff --git a/Flow.Launcher/Languages/es-419.xaml b/Flow.Launcher/Languages/es-419.xaml index b3333c7a9..e18cdb3fe 100644 --- a/Flow.Launcher/Languages/es-419.xaml +++ b/Flow.Launcher/Languages/es-419.xaml @@ -224,6 +224,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} Tienda de Plugins @@ -467,8 +468,10 @@ Open Folder Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Arg para Archivo The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Navegador Web Predeterminado @@ -500,6 +504,8 @@ Nueva Ventana Nueva Pestaña Modo Privado + Default + New Profile Cambiar Prioridad diff --git a/Flow.Launcher/Languages/es.xaml b/Flow.Launcher/Languages/es.xaml index 73cd943a2..faaef8451 100644 --- a/Flow.Launcher/Languages/es.xaml +++ b/Flow.Launcher/Languages/es.xaml @@ -24,8 +24,8 @@ Flow Launcher ha detectado que los datos de usario existen tanto en {0} como en {1}. {2}{2}Por favor, elimine {1} para continuar. No se han producido cambios. - El siguiente complemento ha sufrido un error y no puede cargarse: - Los siguientes complementos han sufrido un error y no pueden cargarse: + El siguiente complemento ha sufrido un fallo y no se puede cargar: + Los siguientes complementos han sufrido un fallo y no se pueden cargar: Por favor, consulte los registros para más información @@ -224,6 +224,7 @@ Fallo al desinstalar {0} No se puede encontrar plugin.json en el archivo zip extraído, o esta ruta {0} no existe Ya existe un complemento con el mismo ID y versión, o la versión es superior a la de este complemento descargado + Error creating setting panel for plugin {0}:{1}{2} Tienda complementos @@ -332,7 +333,7 @@ Cambia el texto del marcador de posición. La entrada vacía utilizará: {0} Tamaño fijo de la ventana El tamaño de la ventana no se puede ajustar mediante arrastre. - Since Always Preview is on, maximum results shown may not take effect because preview panel requires a certain minimum height + Dado que la vista previa está siempre activada, es posible que no se muestren los resultados máximos, ya que el panel de vista previa requiere una altura mínima determinada Atajo de teclado @@ -395,7 +396,7 @@ Mostrar distintivos en resultados Para los complementos compatibles, se muestran distintivos que ayudan a distinguirlos más fácilmente. Mostrar distintivos en resultados solo para consulta global - Mostrar distintivos solo para los resultados de consultas globales + Muestra distintivos solo para los resultados de consultas globales Salto de diálogo Introducir atajo de teclado para acceder rápidamente a la ventana de diálogo Abrir/Guardar como en la ruta del administrador de archivos actual. Salto de diálogo @@ -467,8 +468,10 @@ Abrir carpeta Avanzado Nivel de registro - Depuración + Silencioso + Error Información + Depuración Configuración de fuente de la ventana @@ -490,6 +493,7 @@ Argumentos del archivo El administrador de archivos '{0}' no pudo ser localizado en '{1}'. ¿Desea continuar? Error de ruta del administrador de archivos + File Explorer Navegador web predeterminado @@ -500,6 +504,8 @@ Nueva ventana Nueva pestaña Modo privado + Default + New Profile Cambiar la prioridad diff --git a/Flow.Launcher/Languages/fr.xaml b/Flow.Launcher/Languages/fr.xaml index ced3aabe0..8aa1b5cd5 100644 --- a/Flow.Launcher/Languages/fr.xaml +++ b/Flow.Launcher/Languages/fr.xaml @@ -224,6 +224,7 @@ Échec de la désinstallation de {0} Impossible de trouver le fichier plugin.json dans le fichier zip extrait, ou ce chemin {0} n'existe pas Un plugin avec le même ID et la même version existe déjà, ou la version est supérieure à ce plugin téléchargé + Erreur lors de la création du panneau de configuration pour le plugin {0}:{1}{2} Magasin des Plugins @@ -466,8 +467,10 @@ Ouvrir le dossier Avancé Niveau de journalisation - Débogage + Silencieux + Erreur Info + Débogage Réglage de la police de la fenêtre @@ -489,6 +492,7 @@ Arguments pour le fichier Le gestionnaire de fichiers '{0}' n'a pas pu être situé à '{1}'. Souhaitez-vous continuer ? Erreur de chemin du gestionnaire de fichiers + Explorateur de fichiers Navigateur web par défaut @@ -499,6 +503,8 @@ Nouvelle fenêtre Nouvel onglet Mode privé + Par défaut + Nouveau profil Changer la priorité diff --git a/Flow.Launcher/Languages/he.xaml b/Flow.Launcher/Languages/he.xaml index 164f13afd..f9f0ba2e3 100644 --- a/Flow.Launcher/Languages/he.xaml +++ b/Flow.Launcher/Languages/he.xaml @@ -223,6 +223,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} חנות תוספים @@ -466,8 +467,10 @@ פתח תיקיה Advanced רמת יומן - ניפוי שגיאות + Silent + שגיאה מידע + ניפוי שגיאות Setting Window Font @@ -489,6 +492,7 @@ ארגומנט לקובץ לא ניתן היה לאתר את מנהל הקבצים '{0}' ב-'{1}'. האם ברצונך להמשיך? שגיאת נתיב למנהל הקבצים + File Explorer דפדפן ברירת מחדל @@ -499,6 +503,8 @@ חלון חדש כרטיסייה חדשה מצב פרטיות + Default + New Profile שנה עדיפות diff --git a/Flow.Launcher/Languages/it.xaml b/Flow.Launcher/Languages/it.xaml index e1d0fadca..60584807c 100644 --- a/Flow.Launcher/Languages/it.xaml +++ b/Flow.Launcher/Languages/it.xaml @@ -224,6 +224,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} Negozio dei Plugin @@ -467,8 +468,10 @@ Apri Cartella Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Arg Per Cartella The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Browser predefinito @@ -500,6 +504,8 @@ Nuova Finestra Nuova Scheda Modalità Privata + Default + New Profile Cambia Priorità diff --git a/Flow.Launcher/Languages/ja.xaml b/Flow.Launcher/Languages/ja.xaml index 13cd30fd7..de142733f 100644 --- a/Flow.Launcher/Languages/ja.xaml +++ b/Flow.Launcher/Languages/ja.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はポータブルモードの無効化のために再起動する必要があります。再起動の後、ポータブルな形式の設定項目は削除され、あなたのパソコンのフォルダに保存されます Flow Launcherはポータブルモードの有効化のために再起動する必要があります。再起動の後、パソコンに保存された設定項目は削除され、ポータブルな形式で保存されます Flow Launcherはポータブルモードの有効化を検知しました。Flow Launcherを別の場所に移動しますか? Flow Launcherはポータブルモードの無効化を検知しました。関連するショートカットやアンインストーラーが配置されます - 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はあなたのユーザーデータが{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 環境のインストールに失敗しました。後でもう一度お試しください。 ホットキー "{0}" の登録に失敗しました。このホットキーは別のプログラムで使用されている可能性があります。別のホットキーに変更するか、このホットキーを使用しているプログラムを終了してください。 - Failed to unregister hotkey "{0}". Please try again or see log for details + ホットキー「{0}」の登録解除に失敗しました。もう一度試すか、ログを参照して詳細を確認してください Flow Launcher {0}の起動に失敗しました Flow Launcherプラグインの形式が正しくありません @@ -58,7 +58,7 @@ 全て選択 ファイル フォルダー - Text + テキスト ゲームモード ホットキーの使用を一時停止します。 位置のリセット @@ -73,7 +73,7 @@ スタートアップ時にFlow Launcherを起動する 起動の高速化のためにスタートアップではなくログオンタスクを使用 アンインストール後は、「タスク スケジューラ」からこのタスク(Flow.Launcher Startup)を手動で削除する必要があります。 - Error setting launch on startup + スタートアップ時に起動の設定失敗 フォーカスを失った時にFlow Launcherを隠す 最新版が入手可能であっても、アップグレードメッセージを表示しない 検索ウィンドウの位置 @@ -111,7 +111,7 @@ 常に英語モードで入力を開始する Flowを起動したとき、一時的に入力方法を英語モードに変更します。 自動更新 - Automatically check and update the app when available + 利用可能な場合、Flow Launcherを自動的に確認して更新します 選択 起動時にFlow Launcherを隠す 起動後、Flow Launcher の検索ウィンドウは非表示になり、トレイに格納されます。 @@ -123,10 +123,10 @@ 標準 ピンインによる検索 - 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 + Pinyinは中国語を翻訳するためのローマ字入力の標準的な方法です。有効にすると、検索時のメモリ使用量が大幅に増加する可能性があります。 + 双拼入力を使用 + 検索するときに全拼の代わりに双拼を使用する。 + 双拼の入力方式 Xiao He Zi Ran Ma Wei Ruan @@ -142,10 +142,10 @@ 現在のテーマでぼかしの効果が有効になっている場合、影の効果を有効にすることはできません 検索遅延 入力中に短い遅延を追加することで、UIのちらつきや結果の読み込みを軽減します。平均的なタイピング速度のユーザーにおすすめです。 - Enter the wait time (in ms) until input is considered complete. This can only be edited if Search Delay is enabled. + 入力中の結果表示までの待ち時間をミリ秒単位で入力します。これは、検索遅延が有効な場合にのみ編集できます。 デフォルトの検索遅延時間 入力が停止した後に結果が表示されるまでの待ち時間。値が大きいほど長く待機します。(単位 ms) - Information for Korean IME user + 韓国語IMEユーザーへの情報 The Korean input method used in Windows 11 may cause some issues in Flow Launcher. @@ -160,29 +160,29 @@ - Open Language and Region System Settings + システムの言語と地域設定を開く Opens the Korean IME setting location. Go to Korean > Language Options > Keyboard - Microsoft IME > Compatibility 開く - Use Previous Korean IME + 前の韓国語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. + システムのレジストリへのアクセスが可能か確認するか、サポートにお問い合わせください。 ホームページ 検索文字列が空の場合、ホームページの結果を表示します。 クエリの履歴をホームページに表示 ホームページに表示される最大の履歴の数 - This can only be edited if plugin supports Home feature and Home Page is enabled. - Show Search Window at Foremost + これは、プラグインがホーム機能をサポートし、ホームページが有効な場合にのみ編集することができます。 + 検索ウィンドウを最前面に表示 他のプログラムの 'Always on Top' (最前面に表示)設定を上書きし、常に最前面のウィンドウで Flow を表示します。 - プラグインストアでプラグインを変更した後に再起動します + プラグインストアでプラグインを変更した後に再起動 プラグインストア経由でプラグインをインストール、アンインストール、または更新した後、Flow Lancherを自動的に再起動します 不明なソースの警告を表示 不明なソースからプラグインをインストールするときに警告を表示する - Auto update plugins - Automatically check plugin updates and notify if there are any updates available + プラグインの自動アップデート + プラグインの更新を自動的にチェックし、利用可能な更新がある場合に通知します - Search Plugin + プラグインの検索 Ctrl+F でプラグインを検索します 検索結果が見つかりませんでした 別の検索を試してみてください。 @@ -191,20 +191,20 @@ プラグインを探す 有効 無効 - Action keyword Setting + アクションキーワードの設定 キーワード - Current action keyword - New action keyword - Change Action Keywords - Plugin search delay time - Change Plugin Search Delay Time + 現在のアクションキーワード + 新しいアクションキーワード + アクションキーワードの変更 + プラグインの検索遅延時間 + プラグインの検索遅延時間を変更 詳細設定: 有効 重要度 検索遅延 ホームページ - Current Priority - New Priority + 現在の優先度 + 新しい優先度 重要度 プラグインの結果の優先度を変更します。 プラグイン・ディレクトリ @@ -214,59 +214,60 @@ バージョン ウェブサイト アンインストール - Fail to remove plugin settings - Plugins: {0} - Fail to remove plugin settings files, please remove them manually - Fail to remove plugin cache - Plugins: {0} - Fail to remove plugin cache files, please remove them manually - {0} modified already - Please restart Flow before making any further changes - Fail to install {0} - 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 + プラグイン設定の削除に失敗 + プラグイン: {0} - プラグイン設定ファイルの削除に失敗しました。手動で削除してください + プラグインキャッシュの削除に失敗 + プラグイン: {0} - プラグインキャッシュファイルの削除に失敗しました。手動で削除してください + {0} は既に変更されています + これ以上変更を加える前に Flow Launcher を再起動してください + {0} のインストールに失敗 + {0} のアンインストールに失敗 + 展開されたzipファイルからplugin.jsonが見つからないか、このパス {0} が存在しません + 同じIDとバージョンのプラグインがすでに存在するか、またはこのダウンロードしたプラグインよりもバージョンが大きいです + Error creating setting panel for plugin {0}:{1}{2} プラグインストア 新規リリース 最近の更新 プラグイン - Installed + インストール済み 更新 インストール アンインストール 更新 - Plugin already installed - New Version - This plugin has been updated within the last 7 days + プラグインは既にインストールされています + 新しいバージョン + このプラグインは過去1週間以内に更新されました 新しいアップデートが利用可能です プラグインのインストール失敗 プラグインのアンインストール失敗 - Error updating plugin + プラグインの更新に失敗 プラグインの設定を維持 再びインストールして使用するときのためにプラグインの設定を維持しますか? - Plugin {0} successfully installed. Please restart Flow. - Plugin {0} successfully uninstalled. Please restart Flow. - Plugin {0} successfully updated. Please restart Flow. + プラグイン {0} のインストールに成功しました。Flow を再起動してください。 + プラグイン {0} のアンインストールに成功しました。Flow を再起動してください。 + プラグイン {0} が正常に更新されました。Flow を再起動してください。 プラグインのインストール {0} by {1} {2}{2}このプラグインをインストールしますか? プラグインのアンインストール {0} by {1} {2}{2}このプラグインをアンインストールしますか? - Plugin update - {0} by {1} {2}{2}Would you like to update this plugin? - Downloading plugin - Automatically restart after installing/uninstalling/updating plugins in plugin store - Zip file does not have a valid plugin.json configuration + プラグインの更新 + {0} by {1} {2}{2}このプラグインを更新しますか? + プラグインをダウンロード中 + プラグインストア経由でのプラグインのインストール 、アンインストール、または更新後に自動的に再起動します + Zipファイルに有効なplugin.jsonファイルがありません 不明なソースからのインストール このプラグインは不明なソースから提供されており、潜在的なリスクを含んでいる可能性があります!{0}{0}このプラグインの開発元をよく調べ、安全であることをご自身で確かめてください。{0}{0}それでもあなたはこのプラグインをインストールしますか?{0}{0}(この警告は設定の「一般」セクションで無効にすることができます) - Zip files - Please select zip file + Zip ファイル + zipファイルを選択してください ローカルパスからプラグインをインストール - No update available - All plugins are up to date - Plugin updates available - Update plugins - Check plugin updates - Plugins are successfully updated. Please restart Flow. + 利用可能な更新はありません + すべてのプラグインが最新です + プラグインの更新が利用可能 + プラグインを更新 + プラグインの更新を確認 + プラグインが正常に更新されました。Flow を再起動してください。 テーマ @@ -285,13 +286,13 @@ 検索バーの高さ アイテムの高さ 検索ボックスのフォント - Result Title Font - Result Subtitle Font + 結果のタイトルのフォント + 結果のサブタイトルのフォント リセット - 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} が存在しません、デフォルトのテーマに戻します。 @@ -306,7 +307,7 @@ 検索ウィンドウが開いたとき、小さな音を鳴らします 効果音の音量 効果音の音量を調整します - 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 を使った音量調整に必要です。ボリュームを調整する必要がある場合は、Windows Media Player がインストールされているかどうか確認してください。 アニメーション UIでアニメーションを使用します アニメーション速度 @@ -324,15 +325,15 @@ アクリル マイカ マイカ(代替) - This theme supports two (light/dark) modes. - This theme supports Blur Transparent Background. + このテーマはライト/ダークの2モードに対応しています。 + このテーマは背景をぼかした透明効果をサポートしています。 プレースホルダーを表示 クエリが空の場合にプレースホルダを表示します 検索欄の案内文 - Change placeholder text. Input empty will use: {0} + プレースホルダのテキストを変更します。空にすると、 {0} が使用されます ウィンドウサイズの固定 ウィンドウのサイズを固定し、ドラッグでの変更を無効にします。 - Since Always Preview is on, maximum results shown may not take effect because preview panel requires a certain minimum height + 「常にプレビューする」が有効になっているため、プレビューパネルの高さの確保のために「結果の最大表示件数」設定は無視される可能性があります ホットキー @@ -372,51 +373,51 @@ カスタムクエリ ホットキー Custom Query Shortcut 組み込みショートカット - Query + クエリー ショートカット 展開 説明 削除 編集 追加 - None + なし 項目を選択してください {0} プラグインのホットキーを本当に削除しますか? 本当にこのショートカットを削除しますか?: {0} を {1} に展開 - Get text from clipboard. + クリップボードからテキストを取得します。 アクティブなエクスプローラーからパスを取得します。 検索ウィンドウの落陰効果 - Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited. - Window Width Size - You can also quickly adjust this by using Ctrl+[ and Ctrl+]. + 影の効果は GPU に大きな負荷をかけます。お使いのコンピューターの性能が限定的な場合、無効にすることをおすすめします。 + ウィンドウ幅のサイズ + Ctrl+Plus と Ctrl+Minus を使用すれば、簡単に調整することもできます。 Segoe Fluent アイコンを使用する サポートされているクエリ結果にSegoe Fluentアイコンを使用する - Press Key - Show Result Badges + キーを入力 + 結果のバッジを表示 サポートされているプラグインでは、バッジが表示され、より簡単に区別できます。 - Show Result Badges for Global Query Only - Show badges for global query results only - Dialog Jump - Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager. - Dialog Jump - When Open/Save As dialog window opens, quickly navigate to the current path of the file manager. - Dialog Jump Automatically - When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental) - Show Dialog Jump Window - Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations. - Dialog Jump Window Position - Select position for the Dialog Jump search window - Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed - 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 - 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 + グローバルクエリのみ、結果のバッジを表示 + グローバルクエリの結果にのみバッジを表示する + ダイアログジャンプ + ショートカットを入力して、「名前を付けて開く/保存」ダイアログ・ウィンドウを現在のファイルマネージャのパスにすばやくナビゲートします。 + ダイアログジャンプ + 「名前を付けて開く/保存」ダイアログウィンドウが開いたら、すぐにファイルマネージャの現在のパスに移動します。 + 自動ダイアログジャンプ + 開く/名前を付けて保存ダイアログが表示されると、自動的に現在のファイルマネージャのパスに移動させます。 (実験的) + ダイアログジャンプウィンドウを表示 + 「名前をつけて保存/開く」ダイアログウィンドウが表示されたときにダイアログジャンプのウィンドウを開いて、ファイルやフォルダーを素早く開く。 + ダイアログジャンプのウィンドウの位置 + ダイアログジャンプ検索ウィンドウの位置を選択します + 「名前を付けて開く/保存」ダイアログウィンドウの下に固定。ウィンドウが閉じるまで開いたまま表示されます + デフォルトの検索ウィンドウの位置。検索ウィンドウのホットキーによってトリガーされたときに表示されます + ダイアログジャンプの検索結果の開き方 + 「開く/名前を付けて保存」ダイアログウィンドウの選択した結果パスに移動する動作 + 左クリックまたはEnter キー + 右クリック + ダイアログジャンプのファイルに対する動作 + 結果がファイルパスの場合の、「開く/名前を付けて保存」ダイアログウィンドウに対する動作 + フルパスをファイル名ボックスに入力 + フルパスをファイル名ボックスに入力して開く + パスボックスに含まれるフォルダを入力 HTTP プロキシ @@ -467,44 +468,49 @@ フォルダーを開く 上級者向け機能 ログレベル - デバッグ + Silent + エラー 情報 + デバッグ 設定ウィンドウで使用するフォント - See more release notes on GitHub - Failed to fetch release notes - Please check your network connection or ensure GitHub is accessible - Flow Launcher has been updated to {0} - Click here to view the release notes + GitHub で詳細なリリース ノートを見る + リリースノートの取得に失敗 + ネットワーク接続を確認するか、GitHubにアクセスできることを確認してください + Flow Launcher が {0}に更新されました + ここをクリックしてリリースノートを表示 デフォルトのファイルマネージャー - Learn more - Please specify the file location of the file manager you using and add arguments as required. The "%d" represents the directory path to open for, used by the Arg for Folder field and for commands opening specific directories. The "%f" represents the file path to open for, used by the Arg for File field and for commands opening specific files. - For example, if the file manager uses a command such as "totalcmd.exe /A c:\windows" to open the c:\windows directory, the File Manager Path will be totalcmd.exe, and the Arg For Folder will be /A "%d". Certain file managers like QTTabBar may just require a path to be supplied, in this instance use "%d" as the File Manager Path and leave the rest of the fields blank. - File Manager - Profile Name - File Manager Path - Arg For Folder - Arg For File - The file manager '{0}' could not be located at '{1}'. Would you like to continue? - File Manager Path Error + 詳細を見る + 使用したいファイルマネージャーのファイルの位置を指定し、コマンドライン引数を入力してください。"%d" は開こうとしているフォルダーのパスを表し、「フォルダー用の引数」の欄で特定のフォルダーを開くために使用されます。"%f" は開こうとしているファイルのパスを表し、「ファイル用の引数」の欄で特定のファイルを開くために使用されます。 + 例として、ファイルマネージャーが "totalcmd.exe /A c:\windows" というコマンドを c:\windows というフォルダを開くために使用する場合を考えます。この場合、ファイルマネージャーのパスは totalcmd.exe で、フォルダー用の引数は /A "%d" になります。QTTabBarのように、パスのみを要求するファイルマネージャーの場合、”%d” をファイルマネージャーのパスの欄に指定し、残りを空欄にしてください。 + ファイル マネージャー + プロファイル名 + ファイルマネージャーのパス + フォルダー用の引数 + ファイル用の引数 + ファイルマネージャー '{0}' は、'{1}' に見つかりませんでした。続行しますか? + ファイルマネージャのパスエラー + File Explorer デフォルトのウェブブラウザー - The default setting follows the OS default browser setting. If specified separately, flow uses that browser. - Browser - Browser Name - Browser Path - New Window - New Tab - Private Mode + デフォルトの設定は、OS のデフォルトのブラウザ設定に従います。別々に指定すると、Flow はそのブラウザを使用します。 + ブラウザー + ブラウザー名 + ブラウザーのパス + 新しいウィンドウ + 新しいタブ + プライベートモード + Default + New Profile - Change Priority - Greater the number, the higher the result will be ranked. Try setting it as 5. If you want the results to be lower than any other plugin's, provide a negative number - Please provide an valid integer for Priority! + 優先度の変更 + 数値が大きいほど、結果の上の方に表示されます。試しに5として設定してみてください。 結果を他のプラグインよりも低くしたい場合は、負の数字を入力してください + 優先度には有効な整数を入力してください! 古いアクションキーワード @@ -514,33 +520,33 @@ 指定されたプラグインが見つかりません 新しいアクションキーワードを空にすることはできません 新しいアクションキーワードは他のプラグインに割り当てられています。他のアクションキーワードを入力してください - This new Action Keyword is the same as old, please choose a different one + そのアクションキーワードは以前のものと同じです。他のアクションキーワードを入力してください 成功しました - Completed successfully - Failed to copy - Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords. + 正常に完了しました + コピーに失敗 + プラグインを起動するためのアクションキーワードを、空白区切りで入力してください。特定のキーワードを使用せずにプラグインを使用したい場合、* を入力してください。 - Search Delay Time Setting - 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. + 検索の遅延時間の設定 + プラグインに使用したい検索の遅延時間をミリ秒で入力します。 何も指定したくない場合は空にしておくと、プラグインはデフォルトの検索の遅延時間を使用します。 ホームページ - Enable the plugin home page state if you like to show the plugin results when query is empty. + クエリが空のときにプラグインの結果を表示したい場合は、プラグインのホームページの設定を有効にします。 カスタムクエリのホットキー - Press a custom hotkey to open Flow Launcher and input the specified query automatically. + カスタムホットキーを押して Flow Launcher を開き、指定したクエリを自動的に入力します。 プレビュー ホットキーは使用できません。新しいホットキーを選択してください - Hotkey is invalid + そのホットキーは無効です 更新 - Binding Hotkey - Current hotkey is unavailable. - This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey. - This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}". - Press the keys you want to use for this function. - Hotkey and action keyword are empty + ホットキーの設定 + 現在のホットキーは使用できません。 + このホットキーは "{0}" で予約されており、使用できません。別のホットキーを選択してください。 + このホットキーは "{0}" によってすでに使用されています。「上書き」を押すと、"{0}"から削除されます。 + この機能に使用するキーを押してください。 + ホットキーとアクションキーワードが空です カスタムクエリのショートカット @@ -551,11 +557,11 @@ そのショートカットは既に存在します。新しいショートカットを入力するか、既存のショートカットを編集してください。 ショートカット、展開の少なくとも一方が空です。 - Shortcut is invalid + ショートカットが無効です 保存 - Overwrite + 上書き キャンセル リセット 削除 @@ -580,46 +586,46 @@ クラッシュレポートの送信に失敗しました Flow Launcherにエラーが発生しました Please open new issue in - 1. Upload log file: {0} - 2. Copy below exception message + 1. ログファイルをアップロード: {0} + 2. 例外メッセージ以下をコピー - File Manager Error + ファイルマネージャのエラー - 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 URL in the browser. Please check your Default Web Browser configuration in the General section of the settings window + エラー + フォルダを開く際にエラーが発生しました。 {0} + ブラウザでURLを開く際にエラーが発生しました。設定ウィンドウの一般セクションでデフォルトのウェブブラウザ設定を確認してください - Please wait... + しばらくお待ちください… - Checking for new update + 新しい更新を確認中 Flow Launcherは既に最新です - Update found - Updating... + 更新が見つかりました + 更新中… - Flow Launcher was not able to move your user profile data to the new update version. - Please manually move your profile data folder from {0} to {1} + Flow Launcherはユーザープロファイルデータを新しいバージョンに移動できませんでした。 + 手動で {0} から {1}にプロフィールデータフォルダを移動してください - New Update + 新しい更新 Flow Launcher の最新バージョン V{0} が入手可能です Flow Launcherのアップデート中にエラーが発生しました 更新 キャンセル - Update Failed - Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com. + アップデート失敗 + 接続を確認し、その後プロキシ設定を github-cloud.s3.amazonaws.com に更新してみてください。 このアップデートでは、Flow Launcherの再起動が必要です 次のファイルがアップデートされます 更新ファイル一覧 アップデートの詳細 - Restart Flow Launcher after updating plugins - {0}: Update from v{1} to v{2} - No plugin selected + プラグインを更新した後、Flow Launcher を再起動する + {0}: v{1} から v{2} へ更新 + プラグインが選択されていません スキップ @@ -642,18 +648,18 @@ コンテキストメニューを開く ファイルのあるフォルダを開く 管理者として実行、または、 デフォルトのファイルマネージャでフォルダを開く - Query History + クエリの履歴 コンテキストメニューから検索結果に戻る - Autocomplete + 自動補完 選択したアイテムを開く、または、実行する Flow Launcherの設定ウインドウを開く プラグインデータのリロード - Select first result - Select last result - Run current query again + 最初の結果を選択 + 最後の結果を選択 + 現在のクエリをもう一度実行 結果を開く - Open result #{0} + #{0} を開く 天気 天気についてのGoogle検索 diff --git a/Flow.Launcher/Languages/ko.xaml b/Flow.Launcher/Languages/ko.xaml index 6cf1a6274..131aa50cb 100644 --- a/Flow.Launcher/Languages/ko.xaml +++ b/Flow.Launcher/Languages/ko.xaml @@ -215,6 +215,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} 플러그인 스토어 @@ -458,8 +459,10 @@ 폴더 열기 Advanced 로그 레벨 - Debug + Silent + Error Info + Debug 설정창 글꼴 @@ -481,6 +484,7 @@ 파일경로 인수 The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer 기본 웹 브라우저 @@ -491,6 +495,8 @@ 새 창 새 탭 사생활 보호 모드 + Default + New Profile 중요도 변경 diff --git a/Flow.Launcher/Languages/nb.xaml b/Flow.Launcher/Languages/nb.xaml index 57afaa87b..a27f66d11 100644 --- a/Flow.Launcher/Languages/nb.xaml +++ b/Flow.Launcher/Languages/nb.xaml @@ -224,6 +224,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} Programtillegg butikk @@ -467,8 +468,10 @@ Åpne mappe Advanced Log Level - Debug + Silent + Feil Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Arg for fil The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Standard nettleser @@ -500,6 +504,8 @@ Nytt vindu Ny fane Privat modus + Default + New Profile Endre prioritet diff --git a/Flow.Launcher/Languages/nl.xaml b/Flow.Launcher/Languages/nl.xaml index 5b7ba1d21..416091858 100644 --- a/Flow.Launcher/Languages/nl.xaml +++ b/Flow.Launcher/Languages/nl.xaml @@ -224,6 +224,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 Winkel @@ -467,8 +468,10 @@ Map openen Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Arg voor bestand The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Standaard webbrowser @@ -500,6 +504,8 @@ Nieuw Venster Nieuw tabblad Privé modus + Default + New Profile Prioriteit wijzigen diff --git a/Flow.Launcher/Languages/pl.xaml b/Flow.Launcher/Languages/pl.xaml index 92b91d287..1295e66c9 100644 --- a/Flow.Launcher/Languages/pl.xaml +++ b/Flow.Launcher/Languages/pl.xaml @@ -223,6 +223,7 @@ Kliknij "nie", jeśli jest już zainstalowany. Zostaniesz wtedy popros 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} Sklep z wtyczkami @@ -466,8 +467,10 @@ Kliknij "nie", jeśli jest już zainstalowany. Zostaniesz wtedy popros Otwórz folder Zaawansowane Poziom logowania - Debug + Silent + Błąd Info + Debug Ustawienia czcionki okna @@ -489,6 +492,7 @@ Kliknij "nie", jeśli jest już zainstalowany. Zostaniesz wtedy popros Arg dla pliku Menedżer plików „{0}” nie został znaleziony w lokalizacji „{1}”. Czy chcesz kontynuować? Błąd ścieżki do menedżera plików + File Explorer Domyślna przeglądarka @@ -499,6 +503,8 @@ Kliknij "nie", jeśli jest już zainstalowany. Zostaniesz wtedy popros Nowe okno Nowa zakładka Tryb prywatny + Default + New Profile Zmień priorytet diff --git a/Flow.Launcher/Languages/pt-br.xaml b/Flow.Launcher/Languages/pt-br.xaml index 9b0db5a9e..91193bd0a 100644 --- a/Flow.Launcher/Languages/pt-br.xaml +++ b/Flow.Launcher/Languages/pt-br.xaml @@ -224,6 +224,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} Loja de Plugins @@ -467,8 +468,10 @@ Open Folder Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Arg para Arquivo The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Navegador da Web Padrão @@ -500,6 +504,8 @@ Nova Janela Nova Aba Modo Privado + Default + New Profile Alterar Prioridade diff --git a/Flow.Launcher/Languages/pt-pt.xaml b/Flow.Launcher/Languages/pt-pt.xaml index 1a68f23f4..080573821 100644 --- a/Flow.Launcher/Languages/pt-pt.xaml +++ b/Flow.Launcher/Languages/pt-pt.xaml @@ -223,6 +223,7 @@ Falha ao desinstalar {0} Não foi possível encontrar plugin.json no ficheiro zip ou, então, o caminho {0} não existe. Já existe um plugin com a mesma ID e versão ou, então, a versão instalada é superior à do plugin descarregado. + Erro ao criar o painel de definição para o plugin {0}:{1}{2} Loja de plugins @@ -331,7 +332,7 @@ O texto do marcador de posição. Se vazio, será utilizado: {0} Janela com tamanho fixo Não pode ajustar o tamanho da janela por arrasto. - Since Always Preview is on, maximum results shown may not take effect because preview panel requires a certain minimum height + Como a opção "Pré-visualizar sempre" está ativa, os resultados máximos mostrados podem não ter efeito porque o painel de visualização requer uma altura mínima Tecla de atalho @@ -465,8 +466,10 @@ Abrir pasta Avançado Nível de registo - Depuração + Silencioso + Erro Informação + Depuração Tipo de letra da aplicação @@ -488,6 +491,7 @@ Argumento para ficheiro Não foi possível encontrar o gestor de ficheiros '{0}' em '{1}'. Deseja continuar? Erro no caminho do gestor de ficheiros + Gestor de ficheiros Navegador web padrão @@ -498,6 +502,8 @@ Nova janela Novo separador Modo privado + Padrão + Novo perfil Alterar prioridade diff --git a/Flow.Launcher/Languages/ru.xaml b/Flow.Launcher/Languages/ru.xaml index 43d26aff2..c506d0765 100644 --- a/Flow.Launcher/Languages/ru.xaml +++ b/Flow.Launcher/Languages/ru.xaml @@ -167,7 +167,7 @@ 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 @@ -199,10 +199,10 @@ Plugin search delay time Change Plugin Search Delay Time Advanced Settings: - Enabled + Включено Приоритет Search Delay - Home Page + Главная страница Текущий приоритет Новый приоритет Приоритет @@ -224,6 +224,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} Магазин плагинов @@ -467,8 +468,10 @@ Open Folder Advanced Log Level - Debug + Silent + Ошибка Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Аргумент для файла The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Браузер по умолчанию @@ -500,6 +504,8 @@ Новое окно Новая вкладка Приватный режим + Default + New Profile Изменить приоритет @@ -525,7 +531,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. diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml index 855c0635e..e909a24b1 100644 --- a/Flow.Launcher/Languages/sk.xaml +++ b/Flow.Launcher/Languages/sk.xaml @@ -176,7 +176,7 @@ Nevykonali sa žiadne zmeny. Zobraziť vyhľadávacie okno v popredí Prepíše nastavenie "Vždy na vrchu" ostatných programov a zobrazí navrchu Flow. Reštartovať po úprave pluginu cez Repozitár pluginov - Automaticky reštartovať Flow Launcher po inštalácii/odinštalácii/aktualizáciu pluginu cez Repozitár pluginov + Automaticky reštartovať Flow Launcher po inštalácii/odinštalácii/aktualizácii pluginu cez Repozitár pluginov Zobraziť upozornenie na neznámy zdroj Zobraziť upozornenie pri inštalácii z neznámych zdrojov Automaticky aktualizovať pluginy @@ -225,6 +225,7 @@ Nevykonali sa žiadne zmeny. Nepodarilo sa odinštalovať {0} Súbor plugin.json sa nenašiel v rozbalenom zip súbore, alebo táto cesta {0} neexistuje Plugin s rovnakým ID už existuje, alebo ide o vyššiu verziu ako stiahnutý plugin + Chyba pri vytváraní panelu nastavení pre plugin {0}:{1}{2} Repozitár pluginov @@ -255,7 +256,7 @@ Nevykonali sa žiadne zmeny. Aktualizácia pluginu {0} od {1} {2}{2}Chcete aktualizovať tento plugin? Sťahovanie pluginu - Automaticky reštartovať po inštalácii/odinštalácii/aktualizáciu pluginov cez Repozitár pluginov + Automaticky reštartovať po inštalácii/odinštalácii/aktualizácii pluginov cez Repozitár pluginov V zipe sa nenachádza platná konfigurácia plugin.json Inštalácia z neznámeho zdroja Tento plugin pochádza z neznámeho zdroja a môže predstavovať potenciálne riziká!{0}{0}Uistite sa, že viete, odkiaľ tento plugin pochádza, a že je bezpečný.{0}{0}Stále chcete pokračovať?{0}{0}(Toto upozornenie môžete vypnúť sekcii Všeobecné v nastaveniach) @@ -267,7 +268,7 @@ Nevykonali sa žiadne zmeny. Dostupná aktualizácia pluginu Aktualizovať pluginy Skontrolovať dostupnosť aktualizácií - Pluginy {0} boli úspešne aktualizované. Prosím, reštartuje Flow. + Pluginy boli úspešne aktualizované. Prosím, reštartuje Flow. Motív @@ -396,28 +397,28 @@ Nevykonali sa žiadne zmeny. Zobraziť výsledok v odznaku Ak to plugin podporuje, zobrazí sa jeho ikona v odznaku na jednoduchšie odlíšenie. Zobraziť výsledok v odznaku len pre globálne vyhľadávanie - Show badges for global query results only - Dialog Jump - Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager. - Dialog Jump - When Open/Save As dialog window opens, quickly navigate to the current path of the file manager. - Dialog Jump Automatically - When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental) - Show Dialog Jump Window - Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations. - Dialog Jump Window Position - Select position for the Dialog Jump search window - Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed - 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 - 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 + Zobrazí výsledok v odznaku len pre výsledky globálneho vyhľadávania + Rýchly prechod + Zadajte skratku na rýchly prechod na aktuálnu cestu správcu súborov v dialógovom okne Otvoriť/Uložiť. + Rýchly prechod + Keď sa otvorí dialógové okno Otvoriť/Uložiť, rýchlo prejdete na aktuálnu cestu správcu súborov. + Automatický rýchly prechod + Keď je otvorené dialógové okno Otvoriť/Uložiť, automaticky prejsť na cestu v aktuálnom správcovi súborov (Experimentálne) + Zobraziť okno na rýchly prechod + Zobraziť okno rýchleho prechodu, keď je zobrazené dialógové okno Ovoriť/Uložiť na rýchlu navigáciu do umiestnenia súborov/priečinkov. + Umiestnenie okna "rýchly prechod" + Vyberte umiestnenie vyhľadávacieho okna pre "rýchly prechod" + Fixné pod oknom Otvoriť/Uložiť. Zostane zobrazené po otvorení až do uzavretia okna + Predvolená pozícia vyhľadávacieho okna. Zobrazí sa po zadaní skratky na otvorenie vyhľadávacieho okna + Akcia na prechod k výsledku rýchleho prechodu + Ako prejsť na vybranú cestu v otvorenom dialógovom okne Otvoriť/Uložiť + Kliknutie ľavým tlačidlom myši alebo klávesom Enter + Kliknutie pravým tlačidlom myši + Akcia na prechod k súboru rýchleho prechodu + Akcia, ktorá sa vykoná na navigáciu v dialógovom okne Otvoriť/Uložiť, ak výsledkom je súbor + Vložiť celú cestu k súboru do poľa názvu súboru + Vložiť celú cestu k súboru do poľa názvu súboru a otvoriť + Vložiť priečinok do poľa s cestou HTTP proxy @@ -468,8 +469,10 @@ Nevykonali sa žiadne zmeny. Otvoriť priečinok Rozšírené Úroveň logovania - Debug + Žiadne + Chyba Info + Debug Nastavenie písma okna @@ -482,8 +485,8 @@ Nevykonali sa žiadne zmeny. Vyberte správcu súborov Viac informácií - Zadajte umiestnenie súboru správcu súborov, ktorý používate, a podľa potreby pridajte argumenty. "%d" predstavuje cestu k priečinku, ktorý sa má otvoriť, používa sa v poli Arg pre priečinok a pri príkazoch na otvorenie konkrétnych priečinkov. "%f" predstavuje cestu k súboru, ktorá sa má otvoriť a používa sa v poli Arg pre súbor a pri príkazoch na otvorenie konkrétnych súborov. - Napríklad, ak správca súborov používa príkaz ako "totalcmd.exe /A c:\windows" na otvorenie priečinka c:\windows, cesta správcu súborov bude totalcmd.exe a Arg pre priečinok bude /A "%d". Niektorí správcovia súborov, ako napríklad QTTabBar, môžu vyžadovať len zadanie cesty, v tomto prípade použite "%d" ako cestu správcu súborov a zvyšok súborov nechajte prázdny. + Zadajte umiestnenie súboru správcu súborov, ktorý používate, a podľa potreby pridajte argumenty. "%d" predstavuje cestu k priečinku, ktorý sa má otvoriť, používa sa v poli Arg. pre priečinok a pri príkazoch na otvorenie konkrétnych priečinkov. "%f" predstavuje cestu k súboru, ktorá sa má otvoriť a používa sa v poli Arg. pre súbor a pri príkazoch na otvorenie konkrétnych súborov. + Napríklad, ak správca súborov používa príkaz ako "totalcmd.exe /A c:\windows" na otvorenie priečinka c:\windows, cesta správcu súborov bude totalcmd.exe a Arg. pre priečinok bude /A "%d". Niektorí správcovia súborov, ako napríklad QTTabBar, môžu vyžadovať len zadanie cesty, v tomto prípade použite "%d" ako cestu správcu súborov a zvyšok súborov nechajte prázdny. Správca súborov Názov profilu Cesta k správcovi súborov @@ -491,6 +494,7 @@ Nevykonali sa žiadne zmeny. Arg. pre súbor Správca súborov '{0}' sa nenachádza na '{1}'. Chcete pokračovať? Chyba v ceste k správcovi súborov + Prieskumník Predvolený webový prehliadač @@ -501,6 +505,8 @@ Nevykonali sa žiadne zmeny. Nové okno Nová karta Privátny režim + Predvolené + Nový profil Zmena priority diff --git a/Flow.Launcher/Languages/sr-Cyrl-RS.xaml b/Flow.Launcher/Languages/sr-Cyrl-RS.xaml index 4e6c35d98..189e882ec 100644 --- a/Flow.Launcher/Languages/sr-Cyrl-RS.xaml +++ b/Flow.Launcher/Languages/sr-Cyrl-RS.xaml @@ -224,6 +224,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 @@ -467,8 +468,10 @@ Open Folder Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Arg For File The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Default Web Browser @@ -500,6 +504,8 @@ New Window New Tab Private Mode + Default + New Profile Change Priority diff --git a/Flow.Launcher/Languages/sr.xaml b/Flow.Launcher/Languages/sr.xaml index e1495efd6..636942ac4 100644 --- a/Flow.Launcher/Languages/sr.xaml +++ b/Flow.Launcher/Languages/sr.xaml @@ -224,6 +224,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 @@ -467,8 +468,10 @@ Open Folder Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -490,6 +493,7 @@ Arg For File The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Default Web Browser @@ -500,6 +504,8 @@ New Window New Tab Private Mode + Default + New Profile Change Priority diff --git a/Flow.Launcher/Languages/tr.xaml b/Flow.Launcher/Languages/tr.xaml index a91b7997d..e91ba5b3f 100644 --- a/Flow.Launcher/Languages/tr.xaml +++ b/Flow.Launcher/Languages/tr.xaml @@ -224,6 +224,7 @@ {0} kaldırılamıyor plugin.json dosyası çıkarılan zip dosyasında bulunamadı veya {0} yolu mevcut değil Bu eklentiyle aynı ID ve sürüme sahip bir eklenti zaten var, ya da mevcut sürüm daha yüksek + Error creating setting panel for plugin {0}:{1}{2} Eklenti Mağazası @@ -405,14 +406,14 @@ Diyalog Atlama Penceresini Göster Dosya/klasör konumlarına hızlı erişim için aç/kaydet penceresi gösterildiğinde Diyalog Atlama arama penceresini görüntüle. Diyalog Atlama Penceresi Konumu - Select position for the Dialog Jump search window + Diyalog Atlama arama penceresi için konum seçin Farklı Aç/Kaydet iletişim penceresinin altında düzeltildi. Açıldığında görüntülenir ve pencere kapatılana kadar kalır Varsayılan arama penceresi konumu. Arama penceresi kısayol tuşu tarafından tetiklendiğinde görüntülenir - Dialog Jump Result Navigation Behaviour + Diyalog Atlama Sonucu Gezinme Davranışı Farklı Aç/Kaydet iletişim penceresini seçilen sonuç yoluna yönlendirmek için davranış Sol tık veya Enter tuşu Sağ tık - Dialog Jump File Navigation Behaviour + Dialog Jump Dosya Gezinme Davranışı Sonuç bir dosya yolu olduğunda Farklı Aç/Kaydet iletişim penceresinde gezinme davranışı Dosya adı kutusuna tam yolu girin Dosya adı kutusuna tam yolu girin ve açın @@ -467,8 +468,10 @@ Klasörü Aç Gelişmiş Günlük Düzeyi - Hata ayıklama + Sessiz + Hata Bilgi + Hata ayıklama Pencere Yazı Tipini Ayarla @@ -490,6 +493,7 @@ Dosya Açarken '{0}' dosya yöneticisi '{1}' konumunda bulunamadı. Devam etmek ister misiniz? Dosya Yöneticisi Yol Hatası + File Explorer İnternet Tarayıcı Seçenekleri @@ -500,6 +504,8 @@ Yeni Pencere Yeni Sekme Gizli Mod için Bağımsız Değişken + Default + New Profile Önceliği Ayarla diff --git a/Flow.Launcher/Languages/uk-UA.xaml b/Flow.Launcher/Languages/uk-UA.xaml index 55d12a14e..42541d046 100644 --- a/Flow.Launcher/Languages/uk-UA.xaml +++ b/Flow.Launcher/Languages/uk-UA.xaml @@ -224,6 +224,7 @@ Не вдалося видалити {0} Не вдалося знайти файл plugin.json у розпакованому zip-файлі або цей шлях {0} не існує. Вже існує плагін з таким самим ідентифікатором та версією, або версія цього плагіну вища за версію завантаженого. + Помилка створення панелі налаштувань для плагіну {0}: {1}{2} Магазин плагінів @@ -467,8 +468,10 @@ Відкрити теку Розширені Рівень журналювання - Налагодження + Без звуку + Помилка Інформація + Налагодження Встановлення шрифту вікна @@ -490,6 +493,7 @@ Аргумент для файлу Не вдалося знайти файловий менеджер «{0}» за адресою «{1}». Чи бажаєте продовжити? Помилка шляху до файлового менеджера + Файловий провідник Типовий веббраузер @@ -500,6 +504,8 @@ Нове вікно Нова вкладка Приватний режим + Типово + Новий профіль Змінити пріоритет diff --git a/Flow.Launcher/Languages/vi.xaml b/Flow.Launcher/Languages/vi.xaml index 29aaffc8a..f56703b7a 100644 --- a/Flow.Launcher/Languages/vi.xaml +++ b/Flow.Launcher/Languages/vi.xaml @@ -224,6 +224,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} Tải tiện ích mở rộng @@ -469,8 +470,10 @@ Mở thư mục Advanced Log Level - Debug + Silent + Lỗi Info + Debug Setting Window Font @@ -492,6 +495,7 @@ Đối số cho tệp The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer Trình duyệt web tiêu chuẩn @@ -502,6 +506,8 @@ Cửa sổ mới Thẻ Mới Chế độ riêng tư + Default + New Profile Thay đổi mức độ ưu tiên diff --git a/Flow.Launcher/Languages/zh-cn.xaml b/Flow.Launcher/Languages/zh-cn.xaml index 0f8934fe4..3b368f170 100644 --- a/Flow.Launcher/Languages/zh-cn.xaml +++ b/Flow.Launcher/Languages/zh-cn.xaml @@ -224,6 +224,7 @@ 卸载 {0} 失败 无法从提取的zip文件中找到plugin.json,或者此路径 {0} 不存在 已存在相同ID和版本的插件,或者存在版本大于此下载的插件 + Error creating setting panel for plugin {0}:{1}{2} 插件商店 @@ -467,8 +468,10 @@ 打开文件夹 高级 日志等级 - 调试 + 静默 + 错误 信息 + 调试 设置窗口字体 @@ -490,6 +493,7 @@ 选中文件路径参数 文件管理器 '{0}' 不能在 '{1}'中定位。您想要继续吗? 文件管理器路径错误 + 文件资源管理器 默认浏览器 @@ -500,6 +504,8 @@ 新窗口 新标签 隐身模式 + 默认 + 新配置 更改优先级 diff --git a/Flow.Launcher/Languages/zh-tw.xaml b/Flow.Launcher/Languages/zh-tw.xaml index 0cec258f1..c80e8b092 100644 --- a/Flow.Launcher/Languages/zh-tw.xaml +++ b/Flow.Launcher/Languages/zh-tw.xaml @@ -224,6 +224,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} 插件商店 @@ -467,8 +468,10 @@ Open Folder Advanced Log Level - Debug + Silent + Error Info + Debug Setting Window Font @@ -490,6 +493,7 @@ 檔案參數 The file manager '{0}' could not be located at '{1}'. Would you like to continue? File Manager Path Error + File Explorer 預設瀏覽器 @@ -500,6 +504,8 @@ 新增視窗 新增分頁 無痕模式 + Default + New Profile 更改優先度 diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 132ec8389..dd47f9d4e 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" diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index e6696c34c..d6bccac46 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Media; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -24,7 +25,8 @@ using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.ViewModel; -using ModernWpf.Controls; +using iNKORE.UI.WPF.Modern; +using iNKORE.UI.WPF.Modern.Controls; using DataObject = System.Windows.DataObject; using Key = System.Windows.Input.Key; using MouseButtons = System.Windows.Forms.MouseButtons; @@ -61,8 +63,9 @@ namespace Flow.Launcher private bool _isArrowKeyPressed = false; // Window Sound Effects - private MediaPlayer animationSoundWMP; - private SoundPlayer animationSoundWPF; + private MediaPlayer _animationSoundWMP; + private SoundPlayer _animationSoundWPF; + private readonly Lock _soundLock = new(); // Window WndProc private HwndSource _hwndSource; @@ -93,6 +96,7 @@ namespace Flow.Launcher UpdatePosition(); InitSoundEffects(); + RegisterSoundEffectsEvent(); DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste); _viewModel.ActualApplicationThemeChanged += ViewModel_ActualApplicationThemeChanged; } @@ -145,8 +149,8 @@ namespace Flow.Launcher _settings.ReleaseNotesVersion = Constant.Version; // Show release note popup with button App.API.ShowMsgWithButton( - string.Format(App.API.GetTranslation("appUpdateTitle"), Constant.Version), - App.API.GetTranslation("appUpdateButtonContent"), + Localize.appUpdateTitle(Constant.Version), + Localize.appUpdateButtonContent(), () => { Application.Current.Dispatcher.Invoke(() => @@ -188,11 +192,11 @@ namespace Flow.Launcher // Initialize color scheme if (_settings.ColorScheme == Constant.Light) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light; + ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light; } else if (_settings.ColorScheme == Constant.Dark) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark; + ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark; } // Initialize position @@ -666,16 +670,6 @@ namespace Flow.Launcher handled = true; } break; - case Win32Helper.WM_POWERBROADCAST: // Handle power broadcast messages - // https://learn.microsoft.com/en-us/windows/win32/power/wm-powerbroadcast - if (wParam.ToInt32() == Win32Helper.PBT_APMRESUMEAUTOMATIC) - { - // Fix for sound not playing after sleep / hibernate - // https://stackoverflow.com/questions/64805186/mediaplayer-doesnt-play-after-computer-sleeps - InitSoundEffects(); - } - handled = true; - break; } return IntPtr.Zero; @@ -687,31 +681,78 @@ namespace Flow.Launcher private void InitSoundEffects() { - if (_settings.WMPInstalled) + lock (_soundLock) { - animationSoundWMP?.Close(); - animationSoundWMP = new MediaPlayer(); - animationSoundWMP.Open(new Uri(AppContext.BaseDirectory + "Resources\\open.wav")); - } - else - { - animationSoundWPF?.Dispose(); - animationSoundWPF = new SoundPlayer(AppContext.BaseDirectory + "Resources\\open.wav"); - animationSoundWPF.Load(); + if (_settings.WMPInstalled) + { + _animationSoundWMP?.Close(); + _animationSoundWMP = new MediaPlayer(); + _animationSoundWMP.Open(new Uri(AppContext.BaseDirectory + "Resources\\open.wav")); + } + else + { + _animationSoundWPF?.Dispose(); + _animationSoundWPF = new SoundPlayer(AppContext.BaseDirectory + "Resources\\open.wav"); + _animationSoundWPF.Load(); + } } } private void SoundPlay() { - if (_settings.WMPInstalled) + lock (_soundLock) { - animationSoundWMP.Position = TimeSpan.Zero; - animationSoundWMP.Volume = _settings.SoundVolume / 100.0; - animationSoundWMP.Play(); + if (_settings.WMPInstalled) + { + _animationSoundWMP.Position = TimeSpan.Zero; + _animationSoundWMP.Volume = _settings.SoundVolume / 100.0; + _animationSoundWMP.Play(); + } + else + { + _animationSoundWPF.Play(); + } } - else + } + + private void RegisterSoundEffectsEvent() + { + // Fix for sound not playing after sleep / hibernate for both modern standby and legacy standby + // https://stackoverflow.com/questions/64805186/mediaplayer-doesnt-play-after-computer-sleeps + try { - animationSoundWPF.Play(); + Win32Helper.RegisterSleepModeListener(() => + { + if (Application.Current == null) + { + return; + } + + // We must run InitSoundEffects on UI thread because MediaPlayer is a DispatcherObject + if (!Application.Current.Dispatcher.CheckAccess()) + { + Application.Current.Dispatcher.Invoke(InitSoundEffects); + return; + } + + InitSoundEffects(); + }); + } + catch (Exception e) + { + App.API.LogException(ClassName, "Failed to register sound effect event", e); + } + } + + private static void UnregisterSoundEffectsEvent() + { + try + { + Win32Helper.UnregisterSleepModeListener(); + } + catch (Exception e) + { + App.API.LogException(ClassName, "Failed to unregister sound effect event", e); } } @@ -753,12 +794,12 @@ namespace Flow.Launcher private void UpdateNotifyIconText() { var menu = _contextMenu; - ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + + ((MenuItem)menu.Items[0]).Header = Localize.iconTrayOpen() + " (" + _settings.Hotkey + ")"; - ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); - ((MenuItem)menu.Items[2]).Header = App.API.GetTranslation("PositionReset"); - ((MenuItem)menu.Items[3]).Header = App.API.GetTranslation("iconTraySettings"); - ((MenuItem)menu.Items[4]).Header = App.API.GetTranslation("iconTrayExit"); + ((MenuItem)menu.Items[1]).Header = Localize.GameMode(); + ((MenuItem)menu.Items[2]).Header = Localize.PositionReset(); + ((MenuItem)menu.Items[3]).Header = Localize.iconTraySettings(); + ((MenuItem)menu.Items[4]).Header = Localize.iconTrayExit(); } private void InitializeContextMenu() @@ -768,31 +809,31 @@ namespace Flow.Launcher var openIcon = new FontIcon { Glyph = "\ue71e" }; var open = new MenuItem { - Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")", + Header = Localize.iconTrayOpen() + " (" + _settings.Hotkey + ")", Icon = openIcon }; var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" }; var gamemode = new MenuItem { - Header = App.API.GetTranslation("GameMode"), + Header = Localize.GameMode(), Icon = gamemodeIcon }; var positionresetIcon = new FontIcon { Glyph = "\ue73f" }; var positionreset = new MenuItem { - Header = App.API.GetTranslation("PositionReset"), + Header = Localize.PositionReset(), Icon = positionresetIcon }; var settingsIcon = new FontIcon { Glyph = "\ue713" }; var settings = new MenuItem { - Header = App.API.GetTranslation("iconTraySettings"), + Header = Localize.iconTraySettings(), Icon = settingsIcon }; var exitIcon = new FontIcon { Glyph = "\ue7e8" }; var exit = new MenuItem { - Header = App.API.GetTranslation("iconTrayExit"), + Header = Localize.iconTrayExit(), Icon = exitIcon }; @@ -802,8 +843,8 @@ namespace Flow.Launcher settings.Click += (o, e) => App.API.OpenSettingDialog(); exit.Click += (o, e) => Close(); - gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); - positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); + gamemode.ToolTip = Localize.GameModeToolTip(); + positionreset.ToolTip = Localize.PositionResetToolTip(); _contextMenu.Items.Add(open); _contextMenu.Items.Add(gamemode); @@ -1436,9 +1477,10 @@ namespace Flow.Launcher { _hwndSource?.Dispose(); _notifyIcon?.Dispose(); - animationSoundWMP?.Close(); - animationSoundWPF?.Dispose(); + _animationSoundWMP?.Close(); + _animationSoundWPF?.Dispose(); _viewModel.ActualApplicationThemeChanged -= ViewModel_ActualApplicationThemeChanged; + UnregisterSoundEffectsEvent(); } _disposed = true; diff --git a/Flow.Launcher/PluginUpdateWindow.xaml b/Flow.Launcher/PluginUpdateWindow.xaml index 04cd1f7bc..a4bb06431 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" /> - - + )_pluginJsonStorages[type]).Save(); } - + public void OpenDirectory(string directoryPath, string fileNameOrFilePath = null) { try @@ -394,24 +393,30 @@ namespace Flow.Launcher } catch (Win32Exception ex) when (ex.NativeErrorCode == 2) { - LogError(ClassName, "File Manager not found"); + LogException(ClassName, "File Manager not found", ex); ShowMsgError( - GetTranslation("fileManagerNotFoundTitle"), - string.Format(GetTranslation("fileManagerNotFound"), ex.Message) + Localize.fileManagerNotFoundTitle(), + Localize.fileManagerNotFound() ); } catch (Exception ex) { LogException(ClassName, "Failed to open folder", ex); ShowMsgError( - GetTranslation("errorTitle"), - string.Format(GetTranslation("folderOpenError"), ex.Message) + Localize.errorTitle(), + Localize.folderOpenError() ); } } private void OpenUri(Uri uri, bool? inPrivate = null, bool forceBrowser = false) { + if (uri.IsFile && !FilesFolders.FileOrLocationExists(uri.LocalPath)) + { + ShowMsgError(Localize.errorTitle(), Localize.fileNotFoundError(uri.LocalPath)); + return; + } + if (forceBrowser || uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) { var browserInfo = _settings.CustomBrowser; @@ -434,20 +439,26 @@ namespace Flow.Launcher var tabOrWindow = browserInfo.OpenInTab ? "tab" : "window"; LogException(ClassName, $"Failed to open URL in browser {tabOrWindow}: {path}, {inPrivate ?? browserInfo.EnablePrivate}, {browserInfo.PrivateArg}", e); ShowMsgError( - GetTranslation("errorTitle"), - GetTranslation("browserOpenError") + Localize.errorTitle(), + Localize.browserOpenError() ); } } else { - Process.Start(new ProcessStartInfo() + try { - FileName = uri.AbsoluteUri, - UseShellExecute = true - })?.Dispose(); - - return; + Process.Start(new ProcessStartInfo() + { + FileName = uri.AbsoluteUri, + UseShellExecute = true + })?.Dispose(); + } + catch (Exception e) + { + LogException(ClassName, $"Failed to open: {uri.AbsoluteUri}", e); + ShowMsgError(Localize.errorTitle(), e.Message); + } } } @@ -481,7 +492,7 @@ namespace Flow.Launcher OpenUri(appUri); } - public void ToggleGameMode() + public void ToggleGameMode() { _mainVM.ToggleGameMode(); } diff --git a/Flow.Launcher/ReleaseNotesWindow.xaml b/Flow.Launcher/ReleaseNotesWindow.xaml index f0bdbadda..6072f40f1 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 @@ namespace Flow.Launcher private void Window_Closed(object sender, EventArgs e) { - ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged; + ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged; } #endregion @@ -132,8 +132,8 @@ namespace Flow.Launcher RefreshButton.Visibility = Visibility.Visible; MarkdownViewer.Visibility = Visibility.Collapsed; App.API.ShowMsgError( - App.API.GetTranslation("checkNetworkConnectionTitle"), - App.API.GetTranslation("checkNetworkConnectionSubTitle")); + Localize.checkNetworkConnectionTitle(), + Localize.checkNetworkConnectionSubTitle()); } else { @@ -147,7 +147,6 @@ namespace Flow.Launcher private void Grid_SizeChanged(object sender, SizeChangedEventArgs e) { MarkdownScrollViewer.Height = e.NewSize.Height; - MarkdownScrollViewer.Width = e.NewSize.Width; } private void MarkdownViewer_MouseWheel(object sender, MouseWheelEventArgs e) diff --git a/Flow.Launcher/ReportWindow.xaml.cs b/Flow.Launcher/ReportWindow.xaml.cs index ae0767934..bb0ce0073 100644 --- a/Flow.Launcher/ReportWindow.xaml.cs +++ b/Flow.Launcher/ReportWindow.xaml.cs @@ -48,10 +48,10 @@ namespace Flow.Launcher _ => Constant.IssuesUrl }; - var paragraph = Hyperlink(App.API.GetTranslation("reportWindow_please_open_issue"), websiteUrl); - paragraph.Inlines.Add(string.Format(App.API.GetTranslation("reportWindow_upload_log"), log.FullName)); + var paragraph = Hyperlink(Localize.reportWindow_please_open_issue(), websiteUrl); + paragraph.Inlines.Add(Localize.reportWindow_upload_log(log.FullName)); paragraph.Inlines.Add("\n"); - paragraph.Inlines.Add(App.API.GetTranslation("reportWindow_copy_below")); + paragraph.Inlines.Add(Localize.reportWindow_copy_below()); ErrorTextbox.Document.Blocks.Add(paragraph); StringBuilder content = new StringBuilder(); diff --git a/Flow.Launcher/Resources/Controls/Card.xaml b/Flow.Launcher/Resources/Controls/Card.xaml deleted file mode 100644 index e3c5f8194..000000000 --- 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 6a70dded2..000000000 --- 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 f48bf4b6c..000000000 --- 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 b9588275c..000000000 --- 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 605934e80..000000000 --- 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 000000000..78985108c --- /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 a70c0f4ea..000000000 --- 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 f149951f0..000000000 --- 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 9ea550afd..000000000 --- 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 855cccdbd..000000000 --- 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 2ddcbdd0c..000000000 --- a/Flow.Launcher/Resources/Controls/InfoBar.xaml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - + + + + + +