diff --git a/.cm/gitstream.cm b/.cm/gitstream.cm new file mode 100644 index 000000000..767982e3b --- /dev/null +++ b/.cm/gitstream.cm @@ -0,0 +1,80 @@ +# -*- mode: yaml -*- +# This example configuration for provides basic automations to get started with gitStream. +# View the gitStream quickstart for more examples: https://docs.gitstream.cm/examples/ +manifest: + version: 1.0 + + +triggers: + exclude: + branch: + - l10n_dev + - dev + - r/([Dd]ependabot|[Rr]enovate)/ + + +automations: + # Add a label that indicates how many minutes it will take to review the PR. + estimated_time_to_review: + on: + - commit + if: + - true + run: + - action: add-label@v1 + args: + label: "{{ calc.etr }} min review" + color: {{ colors.red if (calc.etr >= 20) else ( colors.yellow if (calc.etr >= 5) else colors.green ) }} + # Post a comment that lists the best experts for the files that were modified. + explain_code_experts: + if: + - true + run: + - action: explain-code-experts@v1 + args: + gt: 10 + # Post a comment notifying that the PR contains a TODO statement. + review_todo_comments: + if: + - {{ source.diff.files | matchDiffLines(regex=r/^[+].*\b(TODO|todo)\b/) | some }} + run: + - action: add-comment@v1 + args: + comment: | + This PR contains a TODO statement. Please check to see if they should be removed. + # Post a comment that request a before and after screenshot + request_screenshot: + # Triggered for PRs that lack an image file or link to an image in the PR description + if: + - {{ not (has.screenshot_link or has.image_uploaded) }} + run: + - action: add-comment@v1 + args: + comment: | + Be a legend :trophy: by adding a before and after screenshot of the changes you made, especially if they are around UI/UX. + + +# +----------------------------------------------------------------------------+ +# | Custom Expressions | +# | https://docs.gitstream.cm/how-it-works/#custom-expressions | +# +----------------------------------------------------------------------------+ + +calc: + etr: {{ branch | estimatedReviewTime }} + +colors: + red: 'b60205' + yellow: 'fbca04' + green: '0e8a16' + +changes: + # Sum all the lines added/edited in the PR + additions: {{ branch.diff.files_metadata | map(attr='additions') | sum }} + # Sum all the line removed in the PR + deletions: {{ branch.diff.files_metadata | map(attr='deletions') | sum }} + # Calculate the ratio of new code + ratio: {{ (changes.additions / (changes.additions + changes.deletions)) * 100 | round(2) }} + +has: + screenshot_link: {{ pr.description | includes(regex=r/!\[.*\]\(.*(jpg|svg|png|gif|psd).*\)/) }} + image_uploaded: {{ pr.description | includes(regex=r/()|!\[image\]\(.*github\.com.*\)/) }} diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 02b486a02..6ba41e410 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -98,6 +98,7 @@ Português Português (Brasil) Italiano Slovenský +quicklook Tiếng Việt Droplex Preinstalled diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d9b39eb89..da4231f74 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,8 @@ updates: - package-ecosystem: "nuget" # See documentation for possible values directory: "/" # Location of package manifests schedule: - interval: "weekly" + interval: "daily" + open-pull-requests-limit: 3 ignore: - dependency-name: "squirrel-windows" reviewers: diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml new file mode 100644 index 000000000..5556c5ea8 --- /dev/null +++ b/.github/pr-labeler.yml @@ -0,0 +1,31 @@ +# The bot always updates the labels, add/remove as necessary [default: false] +alwaysReplace: false +# Treats the text and labels as case sensitive [default: true] +caseSensitive: false +# Array of labels to be applied to the PR [default: []] +customLabels: + # Finds the `text` within the PR title and body and applies the `label` + - text: 'bug' + label: 'bug' + - text: 'fix' + label: 'bug' + - text: 'dependabot' + label: 'bug' + - text: 'New Crowdin updates' + label: 'bug' + - text: 'New Crowdin updates' + label: 'kind/i18n' + - text: 'feature' + label: 'enhancement' + - text: 'add new' + label: 'enhancement' + - text: 'refactor' + label: 'enhancement' + - text: 'refactor' + label: 'Code Refactor' +# Search the body of the PR for the `text` [default: true] +searchBody: true +# Search the title of the PR for the `text` [default: true] +searchTitle: true +# Search for whole words only [default: false] +wholeWords: false diff --git a/.github/workflows/default_plugins.yml b/.github/workflows/default_plugins.yml index a0a816877..85acafae1 100644 --- a/.github/workflows/default_plugins.yml +++ b/.github/workflows/default_plugins.yml @@ -2,7 +2,7 @@ name: Publish Default Plugins on: push: - branches: ['dev'] + branches: ['master'] paths: ['Plugins/**'] workflow_dispatch: @@ -369,4 +369,4 @@ jobs: tag_name: "v${{steps.updated-version-windowssettings.outputs.prop}}" body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes. env: - GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }} diff --git a/.github/workflows/gitstream.yml b/.github/workflows/gitstream.yml new file mode 100644 index 000000000..0916572df --- /dev/null +++ b/.github/workflows/gitstream.yml @@ -0,0 +1,49 @@ +# Code generated by gitStream GitHub app - DO NOT EDIT + +name: gitStream workflow automation +run-name: | + /:\ gitStream: PR #${{ fromJSON(fromJSON(github.event.inputs.client_payload)).pullRequestNumber }} from ${{ github.event.inputs.full_repository }} + +on: + workflow_dispatch: + inputs: + client_payload: + description: The Client payload + required: true + full_repository: + description: the repository name include the owner in `owner/repo_name` format + required: true + head_ref: + description: the head sha + required: true + base_ref: + description: the base ref + required: true + installation_id: + description: the installation id + required: false + resolver_url: + description: the resolver url to pass results to + required: true + resolver_token: + description: Optional resolver token for resolver service + required: false + default: '' + +jobs: + gitStream: + timeout-minutes: 5 + runs-on: ubuntu-latest + name: gitStream workflow automation + steps: + - name: Evaluate Rules + uses: linear-b/gitstream-github-action@v2 + id: rules-engine + with: + full_repository: ${{ github.event.inputs.full_repository }} + head_ref: ${{ github.event.inputs.head_ref }} + base_ref: ${{ github.event.inputs.base_ref }} + client_payload: ${{ github.event.inputs.client_payload }} + installation_id: ${{ github.event.inputs.installation_id }} + resolver_url: ${{ github.event.inputs.resolver_url }} + resolver_token: ${{ github.event.inputs.resolver_token }} diff --git a/.github/workflows/pr_assignee.yml b/.github/workflows/pr_assignee.yml new file mode 100644 index 000000000..5be603df6 --- /dev/null +++ b/.github/workflows/pr_assignee.yml @@ -0,0 +1,17 @@ +name: Assign PR to creator + +on: + pull_request_target: + types: [opened] + branches-ignore: + - l10n_dev + +permissions: + pull-requests: write + +jobs: + automation: + runs-on: ubuntu-latest + steps: + - name: Assign PR to creator + uses: toshimaru/auto-author-assign@v2.1.1 diff --git a/.github/workflows/pr_milestone.yml b/.github/workflows/pr_milestone.yml new file mode 100644 index 000000000..e2365f554 --- /dev/null +++ b/.github/workflows/pr_milestone.yml @@ -0,0 +1,22 @@ +name: Set Milestone + +# Assigns the earliest created milestone that matches the below glob pattern. + +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + +jobs: + automation: + runs-on: ubuntu-latest + + steps: + - name: set-milestone + uses: andrefcdias/add-to-milestone@v1.3.0 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + milestone: "+([0-9]).+([0-9]).+([0-9])" + use-expression: true diff --git a/.github/workflows/top-ranking-issues.yml b/.github/workflows/top-ranking-issues.yml new file mode 100644 index 000000000..bddca4741 --- /dev/null +++ b/.github/workflows/top-ranking-issues.yml @@ -0,0 +1,25 @@ +name: Top-Ranking Issues +on: + schedule: + - cron: '0 0 */1 * *' + workflow_dispatch: + +jobs: + ShowAndLabelTopIssues: + name: Display and label top issues. + runs-on: ubuntu-latest + steps: + - name: Top Issues action + uses: rickstaa/top-issues-action@v1.3.101 + env: + github_token: ${{ secrets.GITHUB_TOKEN }} + with: + dashboard: true + dashboard_show_total_reactions: true + top_list_size: 10 + top_features: true + top_bugs: true + dashboard_title: Top-Ranking Issues 📈 + dashboard_label: ⭐ Dashboard + hide_dashboard_footer: true + top_issues: false diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index b58154dcb..2b570d2c0 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -9,16 +9,20 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; using System.Linq; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Configuration { public class Portable : IPortable { + private readonly IPublicAPI API = Ioc.Default.GetRequiredService(); + /// /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish /// /// - private UpdateManager NewUpdateManager() + private static UpdateManager NewUpdateManager() { var applicationFolderName = Constant.ApplicationDirectory .Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None) @@ -40,7 +44,7 @@ namespace Flow.Launcher.Core.Configuration #endif IndicateDeletion(DataLocation.PortableDataPath); - MessageBox.Show("Flow Launcher needs to restart to finish disabling portable mode, " + + API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " + "after the restart your portable data profile will be deleted and roaming data profile kept"); UpdateManager.RestartApp(Constant.ApplicationFileName); @@ -64,7 +68,7 @@ namespace Flow.Launcher.Core.Configuration #endif IndicateDeletion(DataLocation.RoamingDataPath); - MessageBox.Show("Flow Launcher needs to restart to finish enabling portable mode, " + + API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " + "after the restart your roaming data profile will be deleted and portable data profile kept"); UpdateManager.RestartApp(Constant.ApplicationFileName); @@ -77,41 +81,35 @@ namespace Flow.Launcher.Core.Configuration public void RemoveShortcuts() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup); } public void RemoveUninstallerEntry() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.RemoveUninstallerRegistryEntry(); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.RemoveUninstallerRegistryEntry(); } public void MoveUserDataFolder(string fromLocation, string toLocation) { - FilesFolders.CopyAll(fromLocation, toLocation); + FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s)); VerifyUserDataAfterMove(fromLocation, toLocation); } public void VerifyUserDataAfterMove(string fromLocation, string toLocation) { - FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation); + FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s)); } public void CreateShortcuts() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false); } public void CreateUninstallerEntry() @@ -125,18 +123,14 @@ namespace Flow.Launcher.Core.Configuration subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String); } - using (var portabilityUpdater = NewUpdateManager()) - { - _ = portabilityUpdater.CreateUninstallerRegistryEntry(); - } + using var portabilityUpdater = NewUpdateManager(); + _ = portabilityUpdater.CreateUninstallerRegistryEntry(); } - internal void IndicateDeletion(string filePathTodelete) + private static void IndicateDeletion(string filePathTodelete) { var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile); - using (var _ = File.CreateText(deleteFilePath)) - { - } + using var _ = File.CreateText(deleteFilePath); } /// @@ -157,13 +151,13 @@ namespace Flow.Launcher.Core.Configuration // delete it and prompt the user to pick the portable data location if (File.Exists(roamingDataDeleteFilePath)) { - FilesFolders.RemoveFolderIfExists(roamingDataDir); + FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s)); - if (MessageBox.Show("Flow Launcher has detected you enabled portable mode, " + + if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " + "would you like to move it to a different location?", string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) { - FilesFolders.OpenPath(Constant.RootDirectory); + FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s)); Environment.Exit(0); } @@ -172,9 +166,9 @@ namespace Flow.Launcher.Core.Configuration // delete it and notify the user about it. else if (File.Exists(portableDataDeleteFilePath)) { - FilesFolders.RemoveFolderIfExists(portableDataDir); + FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s)); - MessageBox.Show("Flow Launcher has detected you disabled portable mode, " + + API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " + "the relevant shortcuts and uninstaller entry have been created"); } } @@ -186,7 +180,7 @@ namespace Flow.Launcher.Core.Configuration if (roamingLocationExists && portableLocationExists) { - MessageBox.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " + + API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " + "{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.", DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs index 68be746f2..e9713564e 100644 --- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs @@ -1,5 +1,6 @@ using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Plugin; using System; using System.Collections.Generic; using System.Net; diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs index affd7c312..1f23c2f66 100644 --- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.ExternalPlugins { diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 40eb1be3e..bbb6cf638 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -1,17 +1,21 @@ -using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; -using Flow.Launcher.Plugin.SharedCommands; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Windows; using System.Windows.Forms; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Core.ExternalPlugins.Environments { public abstract class AbstractPluginEnvironment { + protected readonly IPublicAPI API = Ioc.Default.GetRequiredService(); + internal abstract string Language { get; } internal abstract string EnvName { get; } @@ -24,7 +28,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal virtual string FileDialogFilter => string.Empty; - internal abstract string PluginsSettingsFilePath { get; set; } + internal abstract string PluginsSettingsFilePath { get; set; } internal List PluginMetadataList; @@ -38,8 +42,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal IEnumerable Setup() { + // If no plugin is using the language, return empty list if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase))) + { return new List(); + } if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath)) { @@ -50,24 +57,56 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments return SetPathForPluginPairs(PluginsSettingsFilePath, Language); } - if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " + - $"will require {EnvName} to run. Would you like to download {EnvName}? " + - Environment.NewLine + Environment.NewLine + - "Click no if it's already installed, " + - $"and you will be prompted to select the folder that contains the {EnvName} executable", - string.Empty, MessageBoxButtons.YesNo) == DialogResult.No) + var noRuntimeMessage = string.Format( + API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"), + Language, + EnvName, + Environment.NewLine + ); + if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) { - var msg = $"Please select the {EnvName} executable"; - string selectedFile; + var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName); - selectedFile = GetFileFromDialog(msg, FileDialogFilter); + var selectedFile = GetFileFromDialog(msg, FileDialogFilter); if (!string.IsNullOrEmpty(selectedFile)) + { PluginsSettingsFilePath = selectedFile; - + } // Nothing selected because user pressed cancel from the file dialog window - if (string.IsNullOrEmpty(selectedFile)) - InstallEnvironment(); + else + { + var forceDownloadMessage = string.Format( + API.GetTranslation("runtimeExecutableInvalidChooseDownload"), + Language, + EnvName, + Environment.NewLine + ); + + // Let users select valid path or choose to download + while (string.IsNullOrEmpty(selectedFile)) + { + if (API.ShowMsgBox(forceDownloadMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + // Continue select file + selectedFile = GetFileFromDialog(msg, FileDialogFilter); + } + else + { + // User selected no, break the loop + break; + } + } + + if (!string.IsNullOrEmpty(selectedFile)) + { + PluginsSettingsFilePath = selectedFile; + } + else + { + InstallEnvironment(); + } + } } else { @@ -80,8 +119,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments } else { - MessageBox.Show( - $"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom)."); + API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language)); Log.Error("PluginsLoader", $"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.", $"{Language}Environment"); @@ -94,13 +132,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath) { - if (expectedPath == currentPath) - return; + if (expectedPath == currentPath) return; - FilesFolders.RemoveFolderIfExists(installedDirPath); + FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s)); InstallEnvironment(); - } internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata); @@ -112,13 +148,16 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments foreach (var metadata in PluginMetadataList) { if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase)) + { + metadata.AssemblyName = string.Empty; pluginPairs.Add(CreatePluginPair(filePath, metadata)); + } } return pluginPairs; } - private string GetFileFromDialog(string title, string filter = "") + private static string GetFileFromDialog(string title, string filter = "") { var dlg = new OpenFileDialog { @@ -132,7 +171,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments var result = dlg.ShowDialog(); return result == DialogResult.OK ? dlg.FileName : string.Empty; - } /// @@ -175,31 +213,33 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments else { if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName)) + { settings.PluginSettings.PythonExecutablePath = GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath); + } if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName)) + { settings.PluginSettings.NodeExecutablePath = GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath); + } } } private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName) { - if (string.IsNullOrEmpty(filePath)) - return false; + if (string.IsNullOrEmpty(filePath)) return false; // DataLocation.PortableDataPath returns the current portable path, this determines if an out // of date path is also a portable path. - var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}"; + var portableAppEnvLocation = Path.Combine("UserData", DataLocation.PluginEnvironments, pluginEnvironmentName); return filePath.Contains(portableAppEnvLocation); } private static bool IsUsingRoamingPath(string filePath) { - if (string.IsNullOrEmpty(filePath)) - return false; + if (string.IsNullOrEmpty(filePath)) return false; return filePath.StartsWith(DataLocation.RoamingDataPath); } @@ -209,8 +249,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments var index = filePath.IndexOf(DataLocation.PluginEnvironments); // get the substring after "Environments" because we can not determine it dynamically - var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count()); - return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}"; + 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 5676e12f5..fab5738de 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -1,10 +1,10 @@ -using Droplex; +using System.Collections.Generic; +using System.IO; +using Droplex; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; -using System.Collections.Generic; -using System.IO; namespace Flow.Launcher.Core.ExternalPlugins.Environments { @@ -22,13 +22,17 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal override string FileDialogFilter => "Python|pythonw.exe"; - internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; } + internal override string PluginsSettingsFilePath + { + get => PluginSettings.PythonExecutablePath; + set => PluginSettings.PythonExecutablePath = value; + } internal PythonEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal override void InstallEnvironment() { - FilesFolders.RemoveFolderIfExists(InstallPath); + FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); // Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and // uses Python plugin they need to custom install and use v3.8.9 diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs index 70341f711..8a4f527ba 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; -using Droplex; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin.SharedCommands; -using Flow.Launcher.Plugin; using System.IO; +using Droplex; using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Core.ExternalPlugins.Environments { @@ -19,13 +19,17 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); - internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } + internal override string PluginsSettingsFilePath + { + get => PluginSettings.NodeExecutablePath; + set => PluginSettings.NodeExecutablePath = value; + } internal TypeScriptEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal override void InstallEnvironment() { - FilesFolders.RemoveFolderIfExists(InstallPath); + FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait(); diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs index 11ed94d3f..61fd28376 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; -using Droplex; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin.SharedCommands; -using Flow.Launcher.Plugin; using System.IO; +using Droplex; using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Core.ExternalPlugins.Environments { @@ -19,13 +19,17 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); - internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } + internal override string PluginsSettingsFilePath + { + get => PluginSettings.NodeExecutablePath; + set => PluginSettings.NodeExecutablePath = value; + } internal TypeScriptV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal override void InstallEnvironment() { - FilesFolders.RemoveFolderIfExists(InstallPath); + FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait(); diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index 63f21c1d6..44d3ef0ff 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -1,8 +1,9 @@ -using Flow.Launcher.Infrastructure.Logger; -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.ExternalPlugins { @@ -17,11 +18,11 @@ namespace Flow.Launcher.Core.ExternalPlugins private static readonly SemaphoreSlim manifestUpdateLock = new(1); private static DateTime lastFetchedAt = DateTime.MinValue; - private static TimeSpan fetchTimeout = TimeSpan.FromMinutes(2); + private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2); public static List UserPlugins { get; private set; } - public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false) + public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) { try { @@ -31,18 +32,26 @@ namespace Flow.Launcher.Core.ExternalPlugins { var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false); - UserPlugins = results; - lastFetchedAt = DateTime.Now; + // If the results are empty, we shouldn't update the manifest because the results are invalid. + if (results.Count != 0) + { + UserPlugins = results; + lastFetchedAt = DateTime.Now; + + return true; + } } } catch (Exception e) { - Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e); + Ioc.Default.GetRequiredService().LogException(nameof(PluginsManifest), "Http request failed", e); } finally { manifestUpdateLock.Release(); } + + return false; } } } diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs deleted file mode 100644 index 79d6d7605..000000000 --- a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace Flow.Launcher.Core.ExternalPlugins -{ - public record UserPlugin - { - public string ID { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public string Author { get; set; } - public string Version { get; set; } - public string Language { get; set; } - public string Website { get; set; } - public string UrlDownload { get; set; } - public string UrlSourceCode { get; set; } - public string LocalInstallPath { get; set; } - public string IcoPath { get; set; } - public DateTime? LatestReleaseDate { get; set; } - public DateTime? DateAdded { get; set; } - - public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath); - } -} diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 18101ccf0..e9f199d00 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -54,11 +54,11 @@ - - - + + + - + diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 97c3c8981..88d595301 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -1,28 +1,16 @@ using Flow.Launcher.Core.Resource; -using Flow.Launcher.Infrastructure; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Microsoft.IO; using System.Windows; -using System.Windows.Controls; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; -using CheckBox = System.Windows.Controls.CheckBox; -using Control = System.Windows.Controls.Control; -using Orientation = System.Windows.Controls.Orientation; -using TextBox = System.Windows.Controls.TextBox; -using UserControl = System.Windows.Controls.UserControl; -using System.Windows.Documents; namespace Flow.Launcher.Core.Plugin { @@ -42,7 +30,7 @@ namespace Flow.Launcher.Core.Plugin private int RequestId { get; set; } private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json"); + private string SettingPath => Path.Combine(Context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "Settings.json"); public override List LoadContextMenus(Result selectedResult) { diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index f6e5e5879..779dcf887 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -1,32 +1,15 @@ using Flow.Launcher.Core.Resource; -using Flow.Launcher.Infrastructure; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using Microsoft.IO; -using System.Windows; -using System.Windows.Controls; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -using CheckBox = System.Windows.Controls.CheckBox; using Control = System.Windows.Controls.Control; -using Orientation = System.Windows.Controls.Orientation; -using TextBox = System.Windows.Controls.TextBox; -using UserControl = System.Windows.Controls.UserControl; -using System.Windows.Documents; -using static System.Windows.Forms.LinkLabel; -using Droplex; -using System.Windows.Forms; -using Microsoft.VisualStudio.Threading; namespace Flow.Launcher.Core.Plugin { @@ -34,7 +17,7 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable { protected PluginInitContext Context; public const string JsonRPC = "JsonRPC"; @@ -44,8 +27,9 @@ namespace Flow.Launcher.Core.Plugin private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, - Context.CurrentPluginMetadata.Name, "Settings.json"); + private string SettingDirectory => Context.CurrentPluginMetadata.PluginSettingsDirectoryPath; + + private string SettingPath => Path.Combine(SettingDirectory, "Settings.json"); public abstract List LoadContextMenus(Result selectedResult); @@ -155,6 +139,11 @@ namespace Flow.Launcher.Core.Plugin Settings?.Save(); } + public bool NeedCreateSettingPanel() + { + return Settings.NeedCreateSettingPanel(); + } + public Control CreateSettingPanel() { return Settings.CreateSettingPanel(); diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index 408d9785d..e0a217251 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -7,6 +8,8 @@ using System.Windows.Documents; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; +#nullable enable + namespace Flow.Launcher.Core.Plugin { public class JsonRPCPluginSettings @@ -16,51 +19,74 @@ namespace Flow.Launcher.Core.Plugin public required string SettingPath { get; init; } public Dictionary SettingControls { get; } = new(); - public IReadOnlyDictionary Inner => Settings; - protected ConcurrentDictionary Settings { get; set; } + public IReadOnlyDictionary Inner => Settings; + protected ConcurrentDictionary Settings { get; set; } = null!; public required IPublicAPI API { get; init; } - private JsonStorage> _storage; + private static readonly string ClassName = nameof(JsonRPCPluginSettings); - // maybe move to resource? - private static readonly Thickness settingControlMargin = new(0, 9, 18, 9); - private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9); - private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingDescMargin = new(0, 2, 0, 0); - private static readonly Thickness settingSepMargin = new(0, 0, 0, 2); + private JsonStorage> _storage = null!; + + private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin"); + private static readonly Thickness SettingPanelItemLeftMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftMargin"); + private static readonly Thickness SettingPanelItemTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemTopBottomMargin"); + private static readonly Thickness SettingPanelItemLeftTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftTopBottomMargin"); + private static readonly double SettingPanelTextBoxMinWidth = (double)Application.Current.FindResource("SettingPanelTextBoxMinWidth"); + private static readonly double SettingPanelPathTextBoxWidth = (double)Application.Current.FindResource("SettingPanelPathTextBoxWidth"); + private static readonly double SettingPanelAreaTextBoxMinHeight = (double)Application.Current.FindResource("SettingPanelAreaTextBoxMinHeight"); public async Task InitializeAsync() { - _storage = new JsonStorage>(SettingPath); - Settings = await _storage.LoadAsync(); - - if (Configuration == null) + if (Settings == null) { - return; + _storage = new JsonStorage>(SettingPath); + Settings = await _storage.LoadAsync(); + + // Because value type of settings dictionary is object which causes them to be JsonElement when loading from json files, + // we need to convert it to the correct type + foreach (var (key, value) in Settings) + { + if (value is not JsonElement jsonElement) continue; + + Settings[key] = jsonElement.ValueKind switch + { + JsonValueKind.String => jsonElement.GetString() ?? value, + JsonValueKind.True => jsonElement.GetBoolean(), + JsonValueKind.False => jsonElement.GetBoolean(), + JsonValueKind.Null => null, + _ => value + }; + } } + if (Configuration == null) return; + foreach (var (type, attributes) in Configuration.Body) { - if (attributes.Name == null) - { - continue; - } + // Skip if the setting does not have attributes or name + if (attributes?.Name == null) continue; - if (!Settings.ContainsKey(attributes.Name)) + // Skip if the setting does not have attributes or name + if (!NeedSaveInSettings(type)) continue; + + // If need save in settings, we need to make sure the setting exists in the settings file + if (Settings.ContainsKey(attributes.Name)) continue; + + if (type == "checkbox") + { + // If can parse the default value to bool, use it, otherwise use false + Settings[attributes.Name] = bool.TryParse(attributes.DefaultValue, out var value) && value; + } + else { Settings[attributes.Name] = attributes.DefaultValue; } } } - public void UpdateSettings(IReadOnlyDictionary settings) { - if (settings == null || settings.Count == 0) - return; + if (settings == null || settings.Count == 0) return; foreach (var (key, value) in settings) { @@ -71,19 +97,23 @@ namespace Flow.Launcher.Core.Plugin switch (control) { case TextBox textBox: - textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty); + var text = value as string ?? string.Empty; + textBox.Dispatcher.Invoke(() => textBox.Text = text); break; case PasswordBox passwordBox: - passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty); + var password = value as string ?? string.Empty; + passwordBox.Dispatcher.Invoke(() => passwordBox.Password = password); break; case ComboBox comboBox: comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); break; case CheckBox checkBox: - checkBox.Dispatcher.Invoke(() => - checkBox.IsChecked = value is bool isChecked - ? isChecked - : bool.Parse(value as string ?? string.Empty)); + var isChecked = value is bool boolValue + ? boolValue + // If can parse the default value to bool, use it, otherwise use false + : value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString) + && boolValueFromString; + checkBox.Dispatcher.Invoke(() =>checkBox.IsChecked = isChecked); break; } } @@ -94,322 +124,381 @@ namespace Flow.Launcher.Core.Plugin public async Task SaveAsync() { - await _storage.SaveAsync(); + try + { + await _storage.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e); + } } public void Save() { - _storage.Save(); + try + { + _storage.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e); + } + } + + public bool NeedCreateSettingPanel() + { + // If there are no settings or the settings configuration is empty, return null + return Settings != null && Configuration != null && Configuration.Body.Count != 0; } public Control CreateSettingPanel() { - if (Settings == null || Settings.Count == 0) - return new(); + // No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true + // if (!NeedCreateSettingPanel()) return null; - var settingWindow = new UserControl(); - var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; - - ColumnDefinition gridCol1 = new ColumnDefinition(); - ColumnDefinition gridCol2 = new ColumnDefinition(); - - gridCol1.Width = new GridLength(70, GridUnitType.Star); - gridCol2.Width = new GridLength(30, GridUnitType.Star); - mainPanel.ColumnDefinitions.Add(gridCol1); - mainPanel.ColumnDefinitions.Add(gridCol2); - settingWindow.Content = mainPanel; - int rowCount = 0; - - foreach (var (type, attribute) in Configuration.Body) + // Create main grid with two columns (Column 1: Auto, Column 2: *) + var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; + mainPanel.ColumnDefinitions.Add(new ColumnDefinition() { - Separator sep = new Separator(); - sep.VerticalAlignment = VerticalAlignment.Top; - sep.Margin = settingSepMargin; - sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ - var panel = new StackPanel + Width = new GridLength(0, GridUnitType.Auto) + }); + mainPanel.ColumnDefinitions.Add(new ColumnDefinition() + { + Width = new GridLength(1, GridUnitType.Star) + }); + + // Iterate over each setting and create one row for it + var rowCount = 0; + foreach (var (type, attributes) in Configuration!.Body) + { + // Skip if the setting does not have attributes or name + if (attributes?.Name == null) continue; + + // Add a new row to the main grid + mainPanel.RowDefinitions.Add(new RowDefinition() { - Orientation = Orientation.Vertical, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelPanelMargin - }; - - RowDefinition gridRow = new RowDefinition(); - mainPanel.RowDefinitions.Add(gridRow); - var name = new TextBlock() - { - Text = attribute.Label, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - - var desc = new TextBlock() - { - Text = attribute.Description, - FontSize = 12, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingDescMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - - desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); - - if (attribute.Description == null) /* if no description, hide */ - desc.Visibility = Visibility.Collapsed; - - - if (type != "textBlock") /* if textBlock, hide desc */ - { - panel.Children.Add(name); - panel.Children.Add(desc); - } - - - Grid.SetColumn(panel, 0); - Grid.SetRow(panel, rowCount); + Height = new GridLength(0, GridUnitType.Auto) + }); + // State controls for column 0 and 1 + StackPanel? panel = null; FrameworkElement contentControl; + // If the type is textBlock, separator, or checkbox, we do not need to create a panel + if (type != "textBlock" && type != "separator" && type != "checkbox") + { + // Create a panel to hold the label and description + panel = new StackPanel + { + Margin = SettingPanelItemTopBottomMargin, + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Center + }; + + // Create a text block for name + var name = new TextBlock() + { + Text = attributes.Label, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + // Create a text block for description + TextBlock? desc = null; + if (attributes.Description != null) + { + desc = new TextBlock() + { + Text = attributes.Description, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + desc.SetResourceReference(TextBlock.StyleProperty, "SettingPanelTextBlockDescriptionStyle"); // for theme change + } + + // Add the name and description to the panel + panel.Children.Add(name); + if (desc != null) panel.Children.Add(desc); + } + switch (type) { case "textBlock": - { - contentControl = new TextBlock { - Text = attribute.Description.Replace("\\r\\n", "\r\n"), - Margin = settingTextBlockMargin, - Padding = new Thickness(0, 0, 0, 0), - HorizontalAlignment = System.Windows.HorizontalAlignment.Left, - TextAlignment = TextAlignment.Left, - TextWrapping = TextWrapping.Wrap - }; + contentControl = new TextBlock + { + Text = attributes.Description?.Replace("\\r\\n", "\r\n") ?? string.Empty, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; - Grid.SetColumn(contentControl, 0); - Grid.SetColumnSpan(contentControl, 2); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "input": - { - var textBox = new TextBox() { - Text = Settings[attribute.Name] as string ?? string.Empty, - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; + var textBox = new TextBox() + { + MinWidth = SettingPanelTextBoxMinWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description + }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; + textBox.TextChanged += (_, _) => + { + Settings[attributes.Name] = textBox.Text; + }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = textBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "inputWithFileBtn": - { - var textBox = new TextBox() + case "inputWithFolderBtn": { - Margin = new Thickness(10, 0, 0, 0), - Text = Settings[attribute.Name] as string ?? string.Empty, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; + var textBox = new TextBox() + { + Width = SettingPanelPathTextBoxWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftMargin, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description + }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; + textBox.TextChanged += (_, _) => + { + Settings[attributes.Name] = textBox.Text; + }; - var Btn = new System.Windows.Controls.Button() - { - Margin = new Thickness(10, 0, 0, 0), Content = "Browse" - }; + var Btn = new Button() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftMargin, + Content = API.GetTranslation("select") + }; - var dockPanel = new DockPanel() { Margin = settingControlMargin }; + Btn.Click += (_, _) => + { + using System.Windows.Forms.CommonDialog dialog = type switch + { + "inputWithFolderBtn" => new System.Windows.Forms.FolderBrowserDialog(), + _ => new System.Windows.Forms.OpenFileDialog(), + }; - DockPanel.SetDock(Btn, Dock.Right); - dockPanel.Children.Add(Btn); - dockPanel.Children.Add(textBox); - contentControl = dockPanel; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK) + { + return; + } - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); + var path = dialog switch + { + System.Windows.Forms.FolderBrowserDialog folderDialog => folderDialog.SelectedPath, + System.Windows.Forms.OpenFileDialog fileDialog => fileDialog.FileName, + _ => throw new System.NotImplementedException() + }; - break; - } + textBox.Text = path; + Settings[attributes.Name] = path; + }; + + var stackPanel = new StackPanel() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + Orientation = Orientation.Horizontal + }; + + // Create a stack panel to wrap the button and text box + stackPanel.Children.Add(textBox); + stackPanel.Children.Add(Btn); + + contentControl = stackPanel; + + break; + } case "textarea": - { - var textBox = new TextBox() { - Height = 120, - Margin = settingControlMargin, - VerticalAlignment = VerticalAlignment.Center, - TextWrapping = TextWrapping.WrapWithOverflow, - AcceptsReturn = true, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - Text = Settings[attribute.Name] as string ?? string.Empty, - ToolTip = attribute.Description - }; + var textBox = new TextBox() + { + MinHeight = SettingPanelAreaTextBoxMinHeight, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + TextWrapping = TextWrapping.WrapWithOverflow, + AcceptsReturn = true, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description + }; - textBox.TextChanged += (sender, _) => - { - Settings[attribute.Name] = ((TextBox)sender).Text; - }; + textBox.TextChanged += (sender, _) => + { + Settings[attributes.Name] = ((TextBox)sender).Text; + }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = textBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "passwordBox": - { - var passwordBox = new PasswordBox() { - Margin = settingControlMargin, - Password = Settings[attribute.Name] as string ?? string.Empty, - PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; + var passwordBox = new PasswordBox() + { + MinWidth = SettingPanelTextBoxMinWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + Password = Settings[attributes.Name] as string ?? string.Empty, + PasswordChar = attributes.passwordChar == default ? '*' : attributes.passwordChar, + ToolTip = attributes.Description, + }; - passwordBox.PasswordChanged += (sender, _) => - { - Settings[attribute.Name] = ((PasswordBox)sender).Password; - }; + passwordBox.PasswordChanged += (sender, _) => + { + Settings[attributes.Name] = ((PasswordBox)sender).Password; + }; - contentControl = passwordBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = passwordBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "dropdown": - { - var comboBox = new System.Windows.Controls.ComboBox() { - ItemsSource = attribute.Options, - SelectedItem = Settings[attribute.Name], - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; + var comboBox = new ComboBox() + { + ItemsSource = attributes.Options, + SelectedItem = Settings[attributes.Name], + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + ToolTip = attributes.Description + }; - comboBox.SelectionChanged += (sender, _) => - { - Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; - }; + comboBox.SelectionChanged += (sender, _) => + { + Settings[attributes.Name] = (string)((ComboBox)sender).SelectedItem; + }; - contentControl = comboBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = comboBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; - } + break; + } case "checkbox": - var checkBox = new CheckBox { - IsChecked = - Settings[attribute.Name] is bool isChecked + // If can parse the default value to bool, use it, otherwise use false + var defaultValue = bool.TryParse(attributes.DefaultValue, out var value) && value; + var checkBox = new CheckBox + { + IsChecked = + Settings[attributes.Name] is bool isChecked ? isChecked - : bool.Parse(attribute.DefaultValue), - Margin = settingCheckboxMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; + : defaultValue, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + Content = attributes.Label, + ToolTip = attributes.Description + }; - checkBox.Click += (sender, _) => - { - Settings[attribute.Name] = ((CheckBox)sender).IsChecked; - }; + checkBox.Click += (sender, _) => + { + Settings[attributes.Name] = ((CheckBox)sender).IsChecked ?? defaultValue; + }; - contentControl = checkBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + contentControl = checkBox; - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - - break; + break; + } case "hyperlink": - var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url }; - - var linkbtn = new System.Windows.Controls.Button { - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - Margin = settingControlMargin - }; + var hyperlink = new Hyperlink + { + ToolTip = attributes.Description, + NavigateUri = attributes.url + }; - linkbtn.Content = attribute.urlLabel; + hyperlink.Inlines.Add(attributes.urlLabel); + hyperlink.RequestNavigate += (sender, e) => + { + API.OpenUrl(e.Uri); + e.Handled = true; + }; - contentControl = linkbtn; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); + var textBlock = new TextBlock() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; + textBlock.Inlines.Add(hyperlink); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); + contentControl = textBlock; - break; + break; + } + case "separator": + { + var sep = new Separator(); + + sep.SetResourceReference(Separator.StyleProperty, "SettingPanelSeparatorStyle"); + + contentControl = sep; + + break; + } default: continue; } - if (type != "textBlock") - SettingControls[attribute.Name] = contentControl; + // If type is textBlock or separator, we just add the content control to the main grid + if (panel == null) + { + // Add the content control to the column 0, row rowCount and columnSpan 2 of the main grid + mainPanel.Children.Add(contentControl); + Grid.SetColumn(contentControl, 0); + Grid.SetColumnSpan(contentControl, 2); + Grid.SetRow(contentControl, rowCount); + } + else + { + // Add the panel to the column 0 and row rowCount of the main grid + mainPanel.Children.Add(panel); + Grid.SetColumn(panel, 0); + Grid.SetRow(panel, rowCount); + + // Add the content control to the column 1 and row rowCount of the main grid + mainPanel.Children.Add(contentControl); + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + } + + // Add into SettingControls for settings storage if need + if (NeedSaveInSettings(type)) SettingControls[attributes.Name] = contentControl; - mainPanel.Children.Add(panel); - mainPanel.Children.Add(contentControl); rowCount++; } - return settingWindow; + // Wrap the main grid in a user control + return new UserControl() + { + Content = mainPanel + }; + } + + private static bool NeedSaveInSettings(string type) + { + return type != "textBlock" && type != "separator" && type != "hyperlink"; } } } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index 5a6633525..abe563c14 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -26,54 +26,33 @@ namespace Flow.Launcher.Core.Plugin protected override async Task ExecuteResultAsync(JsonRPCResult result) { - try - { - var res = await RPC.InvokeAsync(result.JsonRPCAction.Method, - argument: result.JsonRPCAction.Parameters); + var res = await RPC.InvokeAsync(result.JsonRPCAction.Method, + argument: result.JsonRPCAction.Parameters); - return res.Hide; - } - catch - { - return false; - } + return res.Hide; } private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); public override List LoadContextMenus(Result selectedResult) { - try - { - var res = JTF.Run(() => RPC.InvokeWithCancellationAsync("context_menu", - new object[] { selectedResult.ContextData })); + var res = JTF.Run(() => RPC.InvokeWithCancellationAsync("context_menu", + new object[] { selectedResult.ContextData })); - var results = ParseResults(res); + var results = ParseResults(res); - return results; - } - catch - { - return new List(); - } + return results; } public override async Task> QueryAsync(Query query, CancellationToken token) { - try - { - var res = await RPC.InvokeWithCancellationAsync("query", - new object[] { query, Settings.Inner }, - token); + var res = await RPC.InvokeWithCancellationAsync("query", + new object[] { query, Settings.Inner }, + token); - var results = ParseResults(res); + var results = ParseResults(res); - return results; - } - catch - { - return new List(); - } + return results; } @@ -133,10 +112,15 @@ namespace Flow.Launcher.Core.Plugin RPC.StartListening(); } - public virtual Task ReloadDataAsync() + public virtual async Task ReloadDataAsync() { - SetupJsonRPC(); - return Task.CompletedTask; + try + { + await RPC.InvokeAsync("reload_data", Context); + } + catch (RemoteMethodNotFoundException e) + { + } } public virtual async ValueTask DisposeAsync() diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs index b8bfee591..8df2ce9ed 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; @@ -121,10 +120,10 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models return _api.HttpGetStreamAsync(url, token); } - public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, + public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default) { - return _api.HttpDownloadAsync(url, filePath, token); + return _api.HttpDownloadAsync(url, filePath, reportProgress, token); } public void AddActionKeyword(string pluginId, string newActionKeyword) @@ -162,16 +161,29 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models _api.OpenDirectory(DirectoryPath, FileNameOrFilePath); } - public void OpenUrl(string url, bool? inPrivate = null) { _api.OpenUrl(url, inPrivate); } - public void OpenAppUri(string appUri) { _api.OpenAppUri(appUri); } + + public void BackToQueryResults() + { + _api.BackToQueryResults(); + } + + public void StartLoadingBar() + { + _api.StartLoadingBar(); + } + + public void StopLoadingBar() + { + _api.StopLoadingBar(); + } } } diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs index dd6517a7f..163f97046 100644 --- a/Flow.Launcher.Core/Plugin/PluginConfig.cs +++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.IO; @@ -9,7 +9,6 @@ using System.Text.Json; namespace Flow.Launcher.Core.Plugin { - internal abstract class PluginConfig { /// @@ -112,7 +111,7 @@ namespace Flow.Launcher.Core.Plugin metadata = JsonSerializer.Deserialize(File.ReadAllText(configPath)); metadata.PluginDirectory = pluginDirectory; // for plugins which doesn't has ActionKeywords key - metadata.ActionKeywords = metadata.ActionKeywords ?? new List { metadata.ActionKeyword }; + metadata.ActionKeywords ??= new List { metadata.ActionKeyword }; // for plugin still use old ActionKeyword metadata.ActionKeyword = metadata.ActionKeywords?[0]; } @@ -137,4 +136,4 @@ namespace Flow.Launcher.Core.Plugin return metadata; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index e7dfb31c0..aa6c54a94 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -1,18 +1,19 @@ -using Flow.Launcher.Core.ExternalPlugins; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; 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.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using ISavable = Flow.Launcher.Plugin.ISavable; using Flow.Launcher.Plugin.SharedCommands; -using System.Text.Json; +using ISavable = Flow.Launcher.Plugin.ISavable; namespace Flow.Launcher.Core.Plugin { @@ -27,11 +28,13 @@ namespace Flow.Launcher.Core.Plugin public static readonly HashSet GlobalPlugins = new(); public static readonly Dictionary NonGlobalPlugins = new(); - public static IPublicAPI API { private set; get; } + // 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 List _metadatas; - private static List _modifiedPlugins = new List(); + private static List _modifiedPlugins = new(); /// /// Directories that will hold Flow Launcher plugin directory @@ -51,7 +54,7 @@ namespace Flow.Launcher.Core.Plugin } /// - /// Save json and ISavable + /// Save json and ISavable /// public static void Save() { @@ -68,15 +71,20 @@ namespace Flow.Launcher.Core.Plugin { foreach (var pluginPair in AllPlugins) { - switch (pluginPair.Plugin) - { - case IDisposable disposable: - disposable.Dispose(); - break; - case IAsyncDisposable asyncDisposable: - await asyncDisposable.DisposeAsync(); - break; - } + await DisposePluginAsync(pluginPair); + } + } + + private static async Task DisposePluginAsync(PluginPair pluginPair) + { + switch (pluginPair.Plugin) + { + case IDisposable disposable: + disposable.Dispose(); + break; + case IAsyncDisposable asyncDisposable: + await asyncDisposable.DisposeAsync(); + break; } } @@ -90,6 +98,48 @@ namespace Flow.Launcher.Core.Plugin }).ToArray()); } + public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static async Task CloseExternalPreviewAsync() + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.ClosePreviewAsync(), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static bool UseExternalPreview() + { + return GetPluginsForInterface().Any(x => !x.Metadata.Disabled); + } + + public static bool AllowAlwaysPreview() + { + var plugin = GetPluginsForInterface().FirstOrDefault(x => !x.Metadata.Disabled); + + if (plugin is null) + return false; + + return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview(); + } + static PluginManager() { // validate user directory @@ -109,15 +159,33 @@ namespace Flow.Launcher.Core.Plugin Settings = settings; Settings.UpdatePluginSettings(_metadatas); AllPlugins = PluginsLoader.Plugins(_metadatas, Settings); + // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins + UpdatePluginDirectory(_metadatas); + } + + private static void UpdatePluginDirectory(List metadatas) + { + foreach (var metadata in metadatas) + { + if (AllowedLanguage.IsDotNet(metadata.Language)) + { + metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName); + metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.AssemblyName); + } + else + { + metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name); + metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.Name); + } + } } /// /// Call initialize for all plugins /// /// return the list of failed to init plugins or null for none - public static async Task InitializePluginsAsync(IPublicAPI api) + public static async Task InitializePluginsAsync() { - API = api; var failedPlugins = new ConcurrentQueue(); var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate @@ -163,9 +231,15 @@ namespace Flow.Launcher.Core.Plugin if (failedPlugins.Any()) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); - API.ShowMsg($"Fail to Init Plugins", - $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", - "", false); + API.ShowMsg( + API.GetTranslation("failedToInitializePluginsTitle"), + string.Format( + API.GetTranslation("failedToInitializePluginsMessage"), + failed + ), + "", + false + ); } } @@ -173,12 +247,10 @@ namespace Flow.Launcher.Core.Plugin { if (query is null) return Array.Empty(); - - if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword)) + + if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) return GlobalPlugins; - - - var plugin = NonGlobalPlugins[query.ActionKeyword]; + return new List { plugin @@ -229,7 +301,7 @@ namespace Flow.Launcher.Core.Plugin return results; } - public static void UpdatePluginMetadata(List results, PluginMetadata metadata, Query query) + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) { @@ -237,8 +309,8 @@ namespace Flow.Launcher.Core.Plugin r.PluginID = metadata.ID; r.OriginQuery = query; - // ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions - // Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level + // ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions + // Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level if (metadata.ActionKeywords.Count == 1) r.ActionKeywordAssigned = query.ActionKeyword; } @@ -256,7 +328,8 @@ namespace Flow.Launcher.Core.Plugin public static IEnumerable GetPluginsForInterface() where T : IFeatures { - return AllPlugins.Where(p => p.Plugin is T); + // Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup + return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty(); } public static List GetContextMenusForPlugin(Result result) @@ -312,7 +385,16 @@ namespace Flow.Launcher.Core.Plugin NonGlobalPlugins[newActionKeyword] = plugin; } + // Update action keywords and action keyword in plugin metadata plugin.Metadata.ActionKeywords.Add(newActionKeyword); + if (plugin.Metadata.ActionKeywords.Count > 0) + { + plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0]; + } + else + { + plugin.Metadata.ActionKeyword = string.Empty; + } } /// @@ -333,16 +415,15 @@ namespace Flow.Launcher.Core.Plugin if (oldActionkeyword != Query.GlobalPluginWildcardSign) NonGlobalPlugins.Remove(oldActionkeyword); - + // Update action keywords and action keyword in plugin metadata plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); - } - - public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword) - { - if (oldActionKeyword != newActionKeyword) + if (plugin.Metadata.ActionKeywords.Count > 0) { - AddActionKeyword(id, newActionKeyword); - RemoveActionKeyword(id, oldActionKeyword); + plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0]; + } + else + { + plugin.Metadata.ActionKeyword = string.Empty; } } @@ -373,37 +454,26 @@ namespace Flow.Launcher.Core.Plugin #region Public functions - public static bool PluginModified(string uuid) + public static bool PluginModified(string id) { - return _modifiedPlugins.Contains(uuid); + return _modifiedPlugins.Contains(id); } - - /// - /// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url, - /// unless it's a local path installation - /// - public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) + public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) { InstallPlugin(newVersion, zipFilePath, checkModified:false); - UninstallPlugin(existingVersion, removeSettings:false, checkModified:false); + await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false); _modifiedPlugins.Add(existingVersion.ID); } - /// - /// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation - /// public static void InstallPlugin(UserPlugin plugin, string zipFilePath) { InstallPlugin(plugin, zipFilePath, checkModified: true); } - /// - /// Uninstall a plugin. - /// - public static void UninstallPlugin(PluginMetadata plugin, bool removeSettings = true) + public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false) { - UninstallPlugin(plugin, removeSettings, true); + await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true); } #endregion @@ -421,7 +491,7 @@ namespace Flow.Launcher.Core.Plugin // Unzip plugin files to temp folder var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath); - + if(!plugin.IsFromLocalInstallPath) File.Delete(zipFilePath); @@ -444,20 +514,20 @@ namespace Flow.Launcher.Core.Plugin var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}"; var defaultPluginIDs = new List - { - "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark - "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator - "572be03c74c642baae319fc283e561a8", // Explorer - "6A122269676E40EB86EB543B945932B9", // PluginIndicator - "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager - "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller - "791FC278BA414111B8D1886DFE447410", // Program - "D409510CD0D2481F853690A07E6DC426", // Shell - "CEA08895D2544B019B2E9C5009600DF4", // Sys - "0308FD86DE0A4DEE8D62B9B535370992", // URL - "565B73353DBF4806919830B9202EE3BF", // WebSearch - "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings - }; + { + "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark + "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator + "572be03c74c642baae319fc283e561a8", // Explorer + "6A122269676E40EB86EB543B945932B9", // PluginIndicator + "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager + "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller + "791FC278BA414111B8D1886DFE447410", // Program + "D409510CD0D2481F853690A07E6DC426", // Shell + "CEA08895D2544B019B2E9C5009600DF4", // Sys + "0308FD86DE0A4DEE8D62B9B535370992", // URL + "565B73353DBF4806919830B9202EE3BF", // WebSearch + "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings + }; // Treat default plugin differently, it needs to be removable along with each flow release var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID) @@ -466,9 +536,17 @@ namespace Flow.Launcher.Core.Plugin var newPluginPath = Path.Combine(installDirectory, folderName); - FilesFolders.CopyAll(pluginFolderPath, newPluginPath); + FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s)); - Directory.Delete(tempFolderPluginPath, true); + try + { + if (Directory.Exists(tempFolderPluginPath)) + Directory.Delete(tempFolderPluginPath, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.InstallPlugin|Failed to delete temp folder {tempFolderPluginPath}", e); + } if (checkModified) { @@ -476,16 +554,63 @@ namespace Flow.Launcher.Core.Plugin } } - internal static void UninstallPlugin(PluginMetadata plugin, bool removeSettings, bool checkModified) + internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) { if (checkModified && PluginModified(plugin.ID)) { throw new ArgumentException($"Plugin {plugin.Name} has been modified"); } - if (removeSettings) + if (removePluginSettings || removePluginFromSettings) { - Settings.Plugins.Remove(plugin.ID); + // If we want to remove plugin from AllPlugins, + // we need to dispose them so that they can release file handles + // which can help FL to delete the plugin settings & cache folders successfully + var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID); + foreach (var pluginPair in pluginPairs) + { + await DisposePluginAsync(pluginPair); + } + } + + if (removePluginSettings) + { + // For dotnet plugins, we need to remove their PluginJsonStorage instance + if (AllowedLanguage.IsDotNet(plugin.Language)) + { + var method = API.GetType().GetMethod("RemovePluginSettings"); + method?.Invoke(API, new object[] { plugin.AssemblyName }); + } + + try + { + var pluginSettingsDirectory = plugin.PluginSettingsDirectoryPath; + if (Directory.Exists(pluginSettingsDirectory)) + Directory.Delete(pluginSettingsDirectory, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin settings folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), + string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); + } + } + + if (removePluginFromSettings) + { + try + { + var pluginCacheDirectory = plugin.PluginCacheDirectoryPath; + if (Directory.Exists(pluginCacheDirectory)) + Directory.Delete(pluginCacheDirectory, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin cache folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"), + string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); + } + Settings.RemovePluginSettings(plugin.ID); AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); } diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 0f2e4f996..495a4c1ab 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -3,7 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using System.Windows.Forms; +using System.Windows; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins.Environments; #pragma warning disable IDE0005 using Flow.Launcher.Infrastructure.Logger; @@ -49,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin return plugins; } - public static IEnumerable DotNetPlugins(List source) + private static IEnumerable DotNetPlugins(List source) { var erroredPlugins = new List(); @@ -73,9 +74,11 @@ namespace Flow.Launcher.Core.Plugin typeof(IAsyncPlugin)); plugin = Activator.CreateInstance(type) as IAsyncPlugin; + + metadata.AssemblyName = assembly.GetName().Name; } #if DEBUG - catch (Exception e) + catch (Exception) { throw; } @@ -111,7 +114,7 @@ namespace Flow.Launcher.Core.Plugin if (erroredPlugins.Count > 0) { - var errorPluginString = String.Join(Environment.NewLine, erroredPlugins); + var errorPluginString = string.Join(Environment.NewLine, erroredPlugins); var errorMessage = "The following " + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") @@ -119,33 +122,41 @@ namespace Flow.Launcher.Core.Plugin _ = Task.Run(() => { - MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + + Ioc.Default.GetRequiredService().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + $"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" + $"Please refer to the logs for more information", "", - MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBoxButton.OK, MessageBoxImage.Warning); }); } return plugins; } - public static IEnumerable ExecutablePlugins(IEnumerable source) + private static IEnumerable ExecutablePlugins(IEnumerable source) { return source .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) - .Select(metadata => new PluginPair + .Select(metadata => { - Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata + return new PluginPair + { + Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), + Metadata = metadata + }; }); } - public static IEnumerable ExecutableV2Plugins(IEnumerable source) + private static IEnumerable ExecutableV2Plugins(IEnumerable source) { return source .Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase)) - .Select(metadata => new PluginPair + .Select(metadata => { - Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata + return new PluginPair + { + Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), + Metadata = metadata + }; }); } } diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs index a14baf271..bae263157 100644 --- a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -82,10 +82,10 @@ namespace Flow.Launcher.Core.Plugin public override async ValueTask DisposeAsync() { + await base.DisposeAsync(); ClientProcess.Kill(true); await ClientProcess.WaitForExitAsync(); ClientProcess.Dispose(); - await base.DisposeAsync(); } } } diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 536e69b3d..e40b0330e 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.IO; using System.Text.Json; using System.Threading; @@ -25,14 +26,13 @@ namespace Flow.Launcher.Core.Plugin var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); _startInfo.EnvironmentVariables["PYTHONPATH"] = path; + // Prevent Python from writing .py[co] files. + // Because .pyc contains location infos which will prevent python portable. + _startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1"; _startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; _startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory; _startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory; - - - //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable - _startInfo.ArgumentList.Add("-B"); } protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) @@ -50,10 +50,53 @@ namespace Flow.Launcher.Core.Plugin // TODO: Async Action return Execute(_startInfo); } + public override async Task InitAsync(PluginInitContext context) { - _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); - _startInfo.ArgumentList.Add(""); + // Run .py files via `-c ` + if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) + { + var rootDirectory = context.CurrentPluginMetadata.PluginDirectory; + var libDirectory = Path.Combine(rootDirectory, "lib"); + var libPyWin32Directory = Path.Combine(libDirectory, "win32"); + var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib"); + var pluginDirectory = Path.Combine(rootDirectory, "plugin"); + + // This makes it easier for plugin authors to import their own modules. + // They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually. + // Instead of running the .py file directly, we pass the code we want to run as a CLI argument. + // This code sets sys.path for the plugin author and then runs the .py file via runpy. + _startInfo.ArgumentList.Add("-c"); + _startInfo.ArgumentList.Add( + $""" + import sys + sys.path.append(r'{rootDirectory}') + sys.path.append(r'{libDirectory}') + sys.path.append(r'{libPyWin32LibDirectory}') + sys.path.append(r'{libPyWin32Directory}') + sys.path.append(r'{pluginDirectory}') + + import runpy + runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__') + """ + ); + // Plugins always expect the JSON data to be in the third argument + // (we're always setting it as _startInfo.ArgumentList[2] = ...). + _startInfo.ArgumentList.Add(""); + } + // Run .pyz files as is + else + { + // No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now, + // but the plugins still expect data to be sent as the third argument, so we're keeping + // the flag here, even though it's not necessary anymore. + _startInfo.ArgumentList.Add("-B"); + _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + // Plugins always expect the JSON data to be in the third argument + // (we're always setting it as _startInfo.ArgumentList[2] = ...). + _startInfo.ArgumentList.Add(""); + } + await base.InitAsync(context); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; } diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index 5c36e0eea..8a9e1ff44 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -26,14 +26,45 @@ namespace Flow.Launcher.Core.Plugin var path = Path.Combine(Constant.ProgramDirectory, JsonRpc); StartInfo.EnvironmentVariables["PYTHONPATH"] = path; - - //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable - StartInfo.ArgumentList.Add("-B"); + StartInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1"; } public override async Task InitAsync(PluginInitContext context) { - StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + // Run .py files via `-c ` + if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) + { + var rootDirectory = context.CurrentPluginMetadata.PluginDirectory; + var libDirectory = Path.Combine(rootDirectory, "lib"); + var libPyWin32Directory = Path.Combine(libDirectory, "win32"); + var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib"); + var pluginDirectory = Path.Combine(rootDirectory, "plugin"); + var filePath = context.CurrentPluginMetadata.ExecuteFilePath; + + // This makes it easier for plugin authors to import their own modules. + // They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually. + // Instead of running the .py file directly, we pass the code we want to run as a CLI argument. + // This code sets sys.path for the plugin author and then runs the .py file via runpy. + StartInfo.ArgumentList.Add("-c"); + StartInfo.ArgumentList.Add( + $""" + import sys + sys.path.append(r'{rootDirectory}') + sys.path.append(r'{libDirectory}') + sys.path.append(r'{libPyWin32LibDirectory}') + sys.path.append(r'{libPyWin32Directory}') + sys.path.append(r'{pluginDirectory}') + + import runpy + runpy.run_path(r'{filePath}', None, '__main__') + """ + ); + } + // Run .pyz files as is + else + { + StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + } await base.InitAsync(context); } diff --git a/Flow.Launcher.Core/Resource/AvailableLanguages.cs b/Flow.Launcher.Core/Resource/AvailableLanguages.cs index 46c1ecb54..ecaecf646 100644 --- a/Flow.Launcher.Core/Resource/AvailableLanguages.cs +++ b/Flow.Launcher.Core/Resource/AvailableLanguages.cs @@ -28,7 +28,7 @@ namespace Flow.Launcher.Core.Resource public static Language Czech = new Language("cs", "čeština"); public static Language Arabic = new Language("ar", "اللغة العربية"); public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt"); - + public static Language Hebrew = new Language("he", "עברית"); public static List GetAvailableLanguages() { @@ -57,9 +57,43 @@ namespace Flow.Launcher.Core.Resource Turkish, Czech, Arabic, - Vietnamese + Vietnamese, + Hebrew }; return languages; } + + public static string GetSystemTranslation(string languageCode) + { + return languageCode switch + { + "en" => "System", + "zh-cn" => "系统", + "zh-tw" => "系統", + "uk-UA" => "Система", + "ru" => "Система", + "fr" => "Système", + "ja" => "システム", + "nl" => "Systeem", + "pl" => "System", + "da" => "System", + "de" => "System", + "ko" => "시스템", + "sr" => "Систем", + "pt-pt" => "Sistema", + "pt-br" => "Sistema", + "es" => "Sistema", + "es-419" => "Sistema", + "it" => "Sistema", + "nb-NO" => "System", + "sk" => "Systém", + "tr" => "Sistem", + "cs" => "Systém", + "ar" => "النظام", + "vi-vn" => "Hệ thống", + "he" => "מערכת", + _ => "System", + }; + } } } diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index 06eb868b8..ffa17ab4d 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -11,34 +11,61 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using System.Globalization; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Core.Resource { public class Internationalization { - public Settings Settings { get; set; } private const string Folder = "Languages"; + private const string DefaultLanguageCode = "en"; private const string DefaultFile = "en.xaml"; private const string Extension = ".xaml"; + private readonly Settings _settings; private readonly List _languageDirectories = new List(); private readonly List _oldResources = new List(); + private readonly string SystemLanguageCode; - public Internationalization() + public Internationalization(Settings settings) { - AddPluginLanguageDirectories(); - LoadDefaultLanguage(); - // we don't want to load /Languages/en.xaml twice - // so add flowlauncher language directory after load plugin language files + _settings = settings; AddFlowLauncherLanguageDirectory(); + SystemLanguageCode = GetSystemLanguageCodeAtStartup(); } - private void AddFlowLauncherLanguageDirectory() { var directory = Path.Combine(Constant.ProgramDirectory, Folder); _languageDirectories.Add(directory); } + private static string GetSystemLanguageCodeAtStartup() + { + var availableLanguages = AvailableLanguages.GetAvailableLanguages(); + + // Retrieve the language identifiers for the current culture. + // ChangeLanguage method overrides the CultureInfo.CurrentCulture, so this needs to + // be called at startup in order to get the correct lang code of system. + var currentCulture = CultureInfo.CurrentCulture; + var twoLetterCode = currentCulture.TwoLetterISOLanguageName; + var threeLetterCode = currentCulture.ThreeLetterISOLanguageName; + var fullName = currentCulture.Name; + + // Try to find a match in the available languages list + foreach (var language in availableLanguages) + { + var languageCode = language.LanguageCode; + + if (string.Equals(languageCode, twoLetterCode, StringComparison.OrdinalIgnoreCase) || + string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) || + string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase)) + { + return languageCode; + } + } + + return DefaultLanguageCode; + } private void AddPluginLanguageDirectories() { @@ -56,19 +83,65 @@ namespace Flow.Launcher.Core.Resource Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>"); } } + + LoadDefaultLanguage(); } private void LoadDefaultLanguage() { + // Removes language files loaded before any plugins were loaded. + // Prevents the language Flow started in from overwriting English if the user switches back to English + RemoveOldLanguageFiles(); LoadLanguage(AvailableLanguages.English); _oldResources.Clear(); } + /// + /// Initialize language. Will change app language and plugin language based on settings. + /// + public async Task InitializeLanguageAsync() + { + // Get actual language + var languageCode = _settings.Language; + if (languageCode == Constant.SystemLanguageCode) + { + languageCode = SystemLanguageCode; + } + + // Get language by language code and change language + var language = GetLanguageByLanguageCode(languageCode); + + // Add plugin language directories first so that we can load language files from plugins + AddPluginLanguageDirectories(); + + // Change language + await ChangeLanguageAsync(language); + } + + /// + /// Change language during runtime. Will change app language and plugin language & save settings. + /// + /// public void ChangeLanguage(string languageCode) { languageCode = languageCode.NonNull(); - Language language = GetLanguageByLanguageCode(languageCode); - ChangeLanguage(language); + + // Get actual language if language code is system + var isSystem = false; + if (languageCode == Constant.SystemLanguageCode) + { + languageCode = SystemLanguageCode; + isSystem = true; + } + + // Get language by language code and change language + var language = GetLanguageByLanguageCode(languageCode); + + // Change language + _ = ChangeLanguageAsync(language); + + // Save settings + _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } private Language GetLanguageByLanguageCode(string languageCode) @@ -86,34 +159,29 @@ namespace Flow.Launcher.Core.Resource } } - public void ChangeLanguage(Language language) + private async Task ChangeLanguageAsync(Language language) { - language = language.NonNull(); - - + // Remove old language files and load language RemoveOldLanguageFiles(); if (language != AvailableLanguages.English) { LoadLanguage(language); } + // Culture of main thread // Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode); CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; - // Raise event after culture is set - Settings.Language = language.LanguageCode; - _ = Task.Run(() => - { - UpdatePluginMetadataTranslations(); - }); + // Raise event for plugins after culture is set + await Task.Run(UpdatePluginMetadataTranslations); } public bool PromptShouldUsePinyin(string languageCodeToSet) { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); - if (Settings.ShouldUsePinyin) + if (_settings.ShouldUsePinyin) return false; if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW) @@ -123,7 +191,7 @@ namespace Flow.Launcher.Core.Resource // "Do you want to search with pinyin?" string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ; - if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) + if (Ioc.Default.GetRequiredService().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) return false; return true; @@ -140,11 +208,14 @@ namespace Flow.Launcher.Core.Resource private void LoadLanguage(Language language) { + var flowEnglishFile = Path.Combine(Constant.ProgramDirectory, Folder, DefaultFile); var dicts = Application.Current.Resources.MergedDictionaries; var filename = $"{language.LanguageCode}{Extension}"; var files = _languageDirectories .Select(d => LanguageFile(d, filename)) - .Where(f => !string.IsNullOrEmpty(f)) + // Exclude Flow's English language file since it's built into the binary, and there's no need to load + // it again from the file system. + .Where(f => !string.IsNullOrEmpty(f) && f != flowEnglishFile) .ToArray(); if (files.Length > 0) @@ -163,7 +234,9 @@ namespace Flow.Launcher.Core.Resource public List LoadAvailableLanguages() { - return AvailableLanguages.GetAvailableLanguages(); + var list = AvailableLanguages.GetAvailableLanguages(); + list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode))); + return list; } public string GetTranslation(string key) diff --git a/Flow.Launcher.Core/Resource/InternationalizationManager.cs b/Flow.Launcher.Core/Resource/InternationalizationManager.cs index 3d87626e6..5d718466c 100644 --- a/Flow.Launcher.Core/Resource/InternationalizationManager.cs +++ b/Flow.Launcher.Core/Resource/InternationalizationManager.cs @@ -1,26 +1,12 @@ -namespace Flow.Launcher.Core.Resource +using System; +using CommunityToolkit.Mvvm.DependencyInjection; + +namespace Flow.Launcher.Core.Resource { + [Obsolete("InternationalizationManager.Instance is obsolete. Use Ioc.Default.GetRequiredService() instead.")] public static class InternationalizationManager { - private static Internationalization instance; - private static object syncObject = new object(); - public static Internationalization Instance - { - get - { - if (instance == null) - { - lock (syncObject) - { - if (instance == null) - { - instance = new Internationalization(); - } - } - } - return instance; - } - } + => Ioc.Default.GetRequiredService(); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 61f0b18e0..e5980b62f 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -2,57 +2,88 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; +using System.Xml; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Interop; +using System.Windows.Controls.Primitives; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Media.Effects; +using System.Windows.Shell; +using System.Windows.Threading; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Microsoft.Win32; namespace Flow.Launcher.Core.Resource { public class Theme { + #region Properties & Fields + + public bool BlurEnabled { get; private set; } + + private const string ThemeMetadataNamePrefix = "Name:"; + private const string ThemeMetadataIsDarkPrefix = "IsDark:"; + private const string ThemeMetadataHasBlurPrefix = "HasBlur:"; + private const int ShadowExtraMargin = 32; - private readonly List _themeDirectories = new List(); + private readonly IPublicAPI _api; + private readonly Settings _settings; + private readonly List _themeDirectories = new(); private ResourceDictionary _oldResource; private string _oldTheme; - public Settings Settings { get; set; } private const string Folder = Constant.Themes; private const string Extension = ".xaml"; - private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder); - private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder); + private static string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder); + private static string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder); - public bool BlurEnabled { get; set; } + private Thickness _themeResizeBorderThickness; - private double mainWindowWidth; + #endregion - public Theme() + #region Constructor + + public Theme(IPublicAPI publicAPI, Settings settings) { + _api = publicAPI; + _settings = settings; + _themeDirectories.Add(DirectoryPath); _themeDirectories.Add(UserDirectoryPath); MakeSureThemeDirectoriesExist(); var dicts = Application.Current.Resources.MergedDictionaries; - _oldResource = dicts.First(d => + _oldResource = dicts.FirstOrDefault(d => { - if (d.Source == null) - return false; + if (d.Source == null) return false; var p = d.Source.AbsolutePath; - var dir = Path.GetDirectoryName(p).NonNull(); - var info = new DirectoryInfo(dir); - var f = info.Name; - var e = Path.GetExtension(p); - var found = f == Folder && e == Extension; - return found; + return p.Contains(Folder) && Path.GetExtension(p) == Extension; }); - _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + + if (_oldResource != null) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + else + { + Log.Error("Current theme resource not found. Initializing with default theme."); + _oldTheme = Constant.DefaultTheme; + }; + } + + #endregion + + #region Theme Resources + + public string GetCurrentTheme() + { + return _settings.Theme; } private void MakeSureThemeDirectoriesExist() @@ -70,68 +101,154 @@ namespace Flow.Launcher.Core.Resource } } - public bool ChangeTheme(string theme) - { - const string defaultTheme = Constant.DefaultTheme; - - string path = GetThemePath(theme); - try - { - if (string.IsNullOrEmpty(path)) - throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); - - // reload all resources even if the theme itself hasn't changed in order to pickup changes - // to things like fonts - UpdateResourceDictionary(GetResourceDictionary(theme)); - - Settings.Theme = theme; - - - //always allow re-loading default theme, in case of failure of switching to a new theme from default theme - if (_oldTheme != theme || theme == defaultTheme) - { - _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); - } - - BlurEnabled = IsBlurTheme(); - - if (Settings.UseDropShadowEffect && !BlurEnabled) - AddDropShadowEffectToCurrentTheme(); - - SetBlurForWindow(); - } - catch (DirectoryNotFoundException) - { - Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); - if (theme != defaultTheme) - { - MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme)); - ChangeTheme(defaultTheme); - } - return false; - } - catch (XamlParseException) - { - Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); - if (theme != defaultTheme) - { - MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme)); - ChangeTheme(defaultTheme); - } - return false; - } - return true; - } - private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) { - var dicts = Application.Current.Resources.MergedDictionaries; + // Add new resources + if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) + { + Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); + } + + // Remove old resources + if (_oldResource != null && _oldResource != dictionaryToUpdate && + Application.Current.Resources.MergedDictionaries.Contains(_oldResource)) + { + Application.Current.Resources.MergedDictionaries.Remove(_oldResource); + } - dicts.Remove(_oldResource); - dicts.Add(dictionaryToUpdate); _oldResource = dictionaryToUpdate; } + /// + /// Updates only the font settings and refreshes the UI. + /// + public void UpdateFonts() + { + try + { + // Load a ResourceDictionary for the specified theme. + var themeName = GetCurrentTheme(); + var dict = GetThemeResourceDictionary(themeName); + + // Apply font settings to the theme resource. + ApplyFontSettings(dict); + UpdateResourceDictionary(dict); + + // Must apply blur and drop shadow effects + _ = RefreshFrameAsync(); + } + catch (Exception e) + { + Log.Exception("Error occurred while updating theme fonts", e); + } + } + + /// + /// Loads and applies font settings to the theme resource. + /// + private void ApplyFontSettings(ResourceDictionary dict) + { + if (dict["QueryBoxStyle"] is Style queryBoxStyle && + dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) + { + var fontFamily = new FontFamily(_settings.QueryBoxFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); + + SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true); + SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemTitleStyle"] is Style resultItemStyle && + dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && + dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && + dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch); + + SetFontProperties(resultItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle && + dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultSubFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch); + + SetFontProperties(resultSubItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultSubItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + } + + /// + /// Applies font properties to a Style. + /// + private static void SetFontProperties(Style style, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, bool isTextBox) + { + // Remove existing font-related setters + if (isTextBox) + { + // First, find the setters to remove and store them in a list + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == Control.FontFamilyProperty || + setter.Property == Control.FontStyleProperty || + setter.Property == Control.FontWeightProperty || + setter.Property == Control.FontStretchProperty) + .ToList(); + + // Remove each found setter one by one + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + // Add New font setter + style.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); + + // Set caret brush (retain existing logic) + var caretBrushPropertyValue = style.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); + var foregroundPropertyValue = style.Setters.OfType().Where(x => x.Property.Name == "Foreground") + .Select(x => x.Value).FirstOrDefault(); + if (!caretBrushPropertyValue && foregroundPropertyValue != null) + style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue)); + } + else + { + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == TextBlock.FontFamilyProperty || + setter.Property == TextBlock.FontStyleProperty || + setter.Property == TextBlock.FontWeightProperty || + setter.Property == TextBlock.FontStretchProperty) + .ToList(); + + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(TextBlock.FontStretchProperty, fontStretch)); + } + } + private ResourceDictionary GetThemeResourceDictionary(string theme) { var uri = GetThemePath(theme); @@ -143,36 +260,34 @@ namespace Flow.Launcher.Core.Resource return dict; } - private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(Settings.Theme); - - public ResourceDictionary GetResourceDictionary(string theme) + private ResourceDictionary GetResourceDictionary(string theme) { var dict = GetThemeResourceDictionary(theme); - + if (dict["QueryBoxStyle"] is Style queryBoxStyle && dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) { - var fontFamily = new FontFamily(Settings.QueryBoxFont); - var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle); - var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.QueryBoxFontWeight); - var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.QueryBoxFontStretch); + var fontFamily = new FontFamily(_settings.QueryBoxFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); + queryBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + queryBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + queryBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); var caretBrushPropertyValue = queryBoxStyle.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); var foregroundPropertyValue = queryBoxStyle.Setters.OfType().Where(x => x.Property.Name == "Foreground") .Select(x => x.Value).FirstOrDefault(); if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling - queryBoxStyle.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue)); + queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue)); // Query suggestion box's font style is aligned with query box - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); } if (dict["ItemTitleStyle"] is Style resultItemStyle && @@ -180,14 +295,14 @@ namespace Flow.Launcher.Core.Resource dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) { - Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultFont)); - Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultFontStyle)); - Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultFontWeight)); - Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultFontStretch)); + Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultFont)); + Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle)); + Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight)); + Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch)); Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; Array.ForEach( - new[] { resultItemStyle, resultItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o + new[] { resultItemStyle, resultItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); } @@ -195,41 +310,61 @@ namespace Flow.Launcher.Core.Resource dict["ItemSubTitleStyle"] is Style resultSubItemStyle && dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) { - Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultSubFont)); - Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultSubFontStyle)); - Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultSubFontWeight)); - Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultSubFontStretch)); + Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultSubFont)); + Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle)); + Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight)); + Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch)); Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; Array.ForEach( - new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o + new[] { resultSubItemStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); } /* Ignore Theme Window Width and use setting */ var windowStyle = dict["WindowStyle"] as Style; - var width = Settings.WindowSize; - windowStyle.Setters.Add(new Setter(Window.WidthProperty, width)); - mainWindowWidth = (double)width; + var width = _settings.WindowSize; + windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, width)); return dict; } - private ResourceDictionary GetCurrentResourceDictionary( ) + private ResourceDictionary GetCurrentResourceDictionary() { - return GetResourceDictionary(Settings.Theme); + return GetResourceDictionary(GetCurrentTheme()); } - public List LoadAvailableThemes() + private ThemeData GetThemeDataFromPath(string path) { - List themes = new List(); - foreach (var themeDirectory in _themeDirectories) + using var reader = XmlReader.Create(path); + reader.Read(); + + var extensionlessName = Path.GetFileNameWithoutExtension(path); + + if (reader.NodeType is not XmlNodeType.Comment) + return new ThemeData(extensionlessName, extensionlessName); + + var commentLines = reader.Value.Trim().Split('\n').Select(v => v.Trim()); + + var name = extensionlessName; + bool? isDark = null; + bool? hasBlur = null; + foreach (var line in commentLines) { - themes.AddRange( - Directory.GetFiles(themeDirectory) - .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) - .ToList()); + if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + name = line[ThemeMetadataNamePrefix.Length..].Trim(); + } + else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase)) + { + isDark = bool.Parse(line[ThemeMetadataIsDarkPrefix.Length..].Trim()); + } + else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase)) + { + hasBlur = bool.Parse(line[ThemeMetadataHasBlurPrefix.Length..].Trim()); + } } - return themes.OrderBy(o => o).ToList(); + + return new ThemeData(extensionlessName, name, isDark, hasBlur); } private string GetThemePath(string themeName) @@ -246,6 +381,82 @@ namespace Flow.Launcher.Core.Resource return string.Empty; } + #endregion + + #region Load & Change + + public List LoadAvailableThemes() + { + List themes = new List(); + foreach (var themeDirectory in _themeDirectories) + { + var filePaths = Directory + .GetFiles(themeDirectory) + .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) + .Select(GetThemeDataFromPath); + themes.AddRange(filePaths); + } + + return themes.OrderBy(o => o.Name).ToList(); + } + + public bool ChangeTheme(string theme = null) + { + if (string.IsNullOrEmpty(theme)) + theme = GetCurrentTheme(); + + string path = GetThemePath(theme); + try + { + if (string.IsNullOrEmpty(path)) + throw new DirectoryNotFoundException($"Theme path can't be found <{path}>"); + + // Retrieve theme resource – always use the resource with font settings applied. + var resourceDict = GetResourceDictionary(theme); + + UpdateResourceDictionary(resourceDict); + + _settings.Theme = theme; + + //always allow re-loading default theme, in case of failure of switching to a new theme from default theme + if (_oldTheme != theme || theme == Constant.DefaultTheme) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + + BlurEnabled = IsBlurTheme(); + + // Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues + _ = RefreshFrameAsync(); + + return true; + } + catch (DirectoryNotFoundException) + { + Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); + if (theme != Constant.DefaultTheme) + { + _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme)); + ChangeTheme(Constant.DefaultTheme); + } + return false; + } + catch (XamlParseException) + { + Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); + if (theme != Constant.DefaultTheme) + { + _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme)); + ChangeTheme(Constant.DefaultTheme); + } + return false; + } + } + + #endregion + + #region Shadow Effect + public void AddDropShadowEffectToCurrentTheme() { var dict = GetCurrentResourceDictionary(); @@ -254,7 +465,7 @@ namespace Flow.Launcher.Core.Resource var effectSetter = new Setter { - Property = Border.EffectProperty, + Property = UIElement.EffectProperty, Value = new DropShadowEffect { Opacity = 0.3, @@ -264,15 +475,17 @@ namespace Flow.Launcher.Core.Resource } }; - var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; - if (marginSetter == null) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is not Setter marginSetter) { + var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin); marginSetter = new Setter() { - Property = Border.MarginProperty, - Value = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin), + Property = FrameworkElement.MarginProperty, + Value = margin, }; windowBorderStyle.Setters.Add(marginSetter); + + SetResizeBoarderThickness(margin); } else { @@ -283,6 +496,8 @@ namespace Flow.Launcher.Core.Resource baseMargin.Right + ShadowExtraMargin, baseMargin.Bottom + ShadowExtraMargin); marginSetter.Value = newMargin; + + SetResizeBoarderThickness(newMargin); } windowBorderStyle.Setters.Add(effectSetter); @@ -295,14 +510,12 @@ namespace Flow.Launcher.Core.Resource var dict = GetCurrentResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; - var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter; - var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; - - if (effectSetter != null) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == UIElement.EffectProperty) is Setter effectSetter) { windowBorderStyle.Setters.Remove(effectSetter); } - if (marginSetter != null) + + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is Setter marginSetter) { var currentMargin = (Thickness)marginSetter.Value; var newMargin = new Thickness( @@ -313,99 +526,383 @@ namespace Flow.Launcher.Core.Resource marginSetter.Value = newMargin; } + SetResizeBoarderThickness(null); + UpdateResourceDictionary(dict); } + public void SetResizeBorderThickness(WindowChrome windowChrome, bool fixedWindowSize) + { + if (fixedWindowSize) + { + windowChrome.ResizeBorderThickness = new Thickness(0); + } + else + { + windowChrome.ResizeBorderThickness = _themeResizeBorderThickness; + } + } + + // because adding drop shadow effect will change the margin of the window, + // we need to update the window chrome thickness to correct set the resize border + private void SetResizeBoarderThickness(Thickness? effectMargin) + { + var window = Application.Current.MainWindow; + if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome) + { + // Save the theme resize border thickness so that we can restore it if we change ResizeWindow setting + if (effectMargin == null) + { + _themeResizeBorderThickness = SystemParameters.WindowResizeBorderThickness; + } + else + { + _themeResizeBorderThickness = new Thickness( + effectMargin.Value.Left + SystemParameters.WindowResizeBorderThickness.Left, + effectMargin.Value.Top + SystemParameters.WindowResizeBorderThickness.Top, + effectMargin.Value.Right + SystemParameters.WindowResizeBorderThickness.Right, + effectMargin.Value.Bottom + SystemParameters.WindowResizeBorderThickness.Bottom); + } + + // Apply the resize border thickness to the window chrome + SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults); + } + } + + #endregion + #region Blur Handling - /* - Found on https://github.com/riverar/sample-win10-aeroglass - */ - private enum AccentState - { - ACCENT_DISABLED = 0, - ACCENT_ENABLE_GRADIENT = 1, - ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, - ACCENT_ENABLE_BLURBEHIND = 3, - ACCENT_INVALID_STATE = 4 - } - [StructLayout(LayoutKind.Sequential)] - private struct AccentPolicy + /// + /// Refreshes the frame to apply the current theme settings. + /// + public async Task RefreshFrameAsync() { - public AccentState AccentState; - public int AccentFlags; - public int GradientColor; - public int AnimationId; - } + await Application.Current.Dispatcher.InvokeAsync(() => + { + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, useDropShadowEffect) = GetActualValue(); - [StructLayout(LayoutKind.Sequential)] - private struct WindowCompositionAttributeData - { - public WindowCompositionAttribute Attribute; - public IntPtr Data; - public int SizeOfData; - } + // Remove OS minimizing/maximizing animation + // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); - private enum WindowCompositionAttribute - { - WCA_ACCENT_POLICY = 19 + // The timing of adding the shadow effect should vary depending on whether the theme is transparent. + if (BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + SetBlurForWindow(GetCurrentTheme(), backdropType); + + if (!BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + }, DispatcherPriority.Render); } - [DllImport("user32.dll")] - private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); /// /// Sets the blur for a window via SetWindowCompositionAttribute /// - public void SetBlurForWindow() + public async Task SetBlurForWindowAsync() { + await Application.Current.Dispatcher.InvokeAsync(() => + { + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, _) = GetActualValue(); + + SetBlurForWindow(GetCurrentTheme(), backdropType); + }, DispatcherPriority.Render); + } + + /// + /// Gets the actual backdrop type and drop shadow effect settings based on the current theme status. + /// + public (BackdropTypes BackdropType, bool UseDropShadowEffect) GetActualValue() + { + var backdropType = _settings.BackdropType; + var useDropShadowEffect = _settings.UseDropShadowEffect; + + // When changed non-blur theme, change to backdrop to none + if (!BlurEnabled) + { + backdropType = BackdropTypes.None; + } + + // Dropshadow on and control disabled.(user can't change dropshadow with blur theme) if (BlurEnabled) { - SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND); + useDropShadowEffect = true; + } + + return (backdropType, useDropShadowEffect); + } + + private void SetBlurForWindow(string theme, BackdropTypes backdropType) + { + var dict = GetResourceDictionary(theme); + if (dict == null) return; + + var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null; + if (windowBorderStyle == null) return; + + var mainWindow = Application.Current.MainWindow; + if (mainWindow == null) return; + + // Check if the theme supports blur + bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; + if (BlurEnabled && hasBlur && Win32Helper.IsBackdropSupported()) + { + // If the BackdropType is Mica or MicaAlt, set the windowborderstyle's background to transparent + if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt) + { + windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); + windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)))); + } + else if (backdropType == BackdropTypes.Acrylic) + { + windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); + windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Transparent))); + } + + // Apply the blur effect + Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType); + ColorizeWindow(theme, backdropType); } else { - SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED); + // Apply default style when Blur is disabled + Win32Helper.DWMSetBackdropForWindow(mainWindow, BackdropTypes.None); + ColorizeWindow(theme, backdropType); + } + + UpdateResourceDictionary(dict); + } + + private void AutoDropShadow(bool useDropShadowEffect) + { + SetWindowCornerPreference("Default"); + RemoveDropShadowEffectFromCurrentTheme(); + if (useDropShadowEffect) + { + if (BlurEnabled && Win32Helper.IsBackdropSupported()) + { + SetWindowCornerPreference("Round"); + } + else + { + SetWindowCornerPreference("Default"); + AddDropShadowEffectToCurrentTheme(); + } + } + else + { + if (BlurEnabled && Win32Helper.IsBackdropSupported()) + { + SetWindowCornerPreference("Default"); + } + else + { + RemoveDropShadowEffectFromCurrentTheme(); + } } } - private bool IsBlurTheme() + private static void SetWindowCornerPreference(string cornerType) { - if (Environment.OSVersion.Version >= new Version(6, 2)) + Window mainWindow = Application.Current.MainWindow; + if (mainWindow == null) + return; + + Win32Helper.DWMSetCornerPreferenceForWindow(mainWindow, cornerType); + } + + // Get Background Color from WindowBorderStyle when there not color for BG. + // for theme has not "LightBG" or "DarkBG" case. + private Color GetWindowBorderStyleBackground(string theme) + { + var Resources = GetThemeResourceDictionary(theme); + var windowBorderStyle = (Style)Resources["WindowBorderStyle"]; + + var backgroundSetter = windowBorderStyle.Setters + .OfType() + .FirstOrDefault(s => s.Property == Border.BackgroundProperty); + + if (backgroundSetter != null) { - var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); + // Background's Value is DynamicColor Case + var backgroundValue = backgroundSetter.Value; - if (resource is bool) - return (bool)resource; + if (backgroundValue is SolidColorBrush solidColorBrush) + { + return solidColorBrush.Color; // Return SolidColorBrush's Color + } + else if (backgroundValue is DynamicResourceExtension dynamicResource) + { + // When DynamicResource Extension it is, Key is resource's name. + var resourceKey = backgroundSetter.Value.ToString(); + // find key in resource and return color. + if (Resources.Contains(resourceKey)) + { + var colorResource = Resources[resourceKey]; + if (colorResource is SolidColorBrush colorBrush) + { + return colorBrush.Color; + } + else if (colorResource is Color color) + { + return color; + } + } + } + } + + return Colors.Transparent; // Default is transparent + } + + private void ApplyPreviewBackground(Color? bgColor = null) + { + if (bgColor == null) return; + + // Copy the existing WindowBorderStyle + var previewStyle = new Style(typeof(Border)); + if (Application.Current.Resources.Contains("WindowBorderStyle")) + { + if (Application.Current.Resources["WindowBorderStyle"] is Style originalStyle) + { + foreach (var setter in originalStyle.Setters.OfType()) + { + previewStyle.Setters.Add(new Setter(setter.Property, setter.Value)); + } + } + } + + // Apply background color (remove transparency in color) + // WPF does not allow the use of an acrylic brush within the window's internal area, + // so transparency effects are not applied to the preview. + Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B); + previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor))); + + // The blur theme keeps the corner round fixed (applying DWM code to modify it causes rendering issues). + // The non-blur theme retains the previously set WindowBorderStyle. + if (BlurEnabled) + { + previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5))); + previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1))); + } + Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle; + } + + private void ColorizeWindow(string theme, BackdropTypes backdropType) + { + var dict = GetThemeResourceDictionary(theme); + if (dict == null) return; + + var mainWindow = Application.Current.MainWindow; + if (mainWindow == null) return; + + // Check if the theme supports blur + bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; + + // SystemBG value check (Auto, Light, Dark) + string systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto + + // Check the user's ColorScheme setting + string colorScheme = _settings.ColorScheme; + + // Check system dark mode setting (read AppsUseLightTheme value) + int themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1); + bool isSystemDark = themeValue == 0; + + // Final decision on whether to use dark mode + bool useDarkMode = false; + + // If systemBG is not "Auto", prioritize it over ColorScheme and set the mode based on systemBG value + if (systemBG == "Dark") + { + useDarkMode = true; // Dark + } + else if (systemBG == "Light") + { + useDarkMode = false; // Light + } + else if (systemBG == "Auto") + { + // If systemBG is "Auto", decide based on ColorScheme + if (colorScheme == "Dark") + useDarkMode = true; + else if (colorScheme == "Light") + useDarkMode = false; + else + useDarkMode = isSystemDark; // Auto (based on system setting) + } + + // Apply DWM Dark Mode + Win32Helper.DWMSetDarkModeForWindow(mainWindow, useDarkMode); + + Color LightBG; + Color DarkBG; + + // Retrieve LightBG value (fallback to WindowBorderStyle background color if not found) + try + { + LightBG = dict.Contains("LightBG") ? (Color)dict["LightBG"] : GetWindowBorderStyleBackground(theme); + } + catch (Exception) + { + LightBG = GetWindowBorderStyleBackground(theme); + } + + // Retrieve DarkBG value (fallback to LightBG if not found) + try + { + DarkBG = dict.Contains("DarkBG") ? (Color)dict["DarkBG"] : LightBG; + } + catch (Exception) + { + DarkBG = LightBG; + } + + // Select background color based on ColorScheme and SystemBG + Color selectedBG = useDarkMode ? DarkBG : LightBG; + ApplyPreviewBackground(selectedBG); + + bool isBlurAvailable = hasBlur && Win32Helper.IsBackdropSupported(); // Windows 11 미만이면 hasBlur를 강제 false + + if (!isBlurAvailable) + { + mainWindow.Background = Brushes.Transparent; + } + else + { + // Only set the background to transparent if the theme supports blur + if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt) + { + mainWindow.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); + } + else + { + mainWindow.Background = new SolidColorBrush(selectedBG); + } + } + } + + private static bool IsBlurTheme() + { + if (!Win32Helper.IsBackdropSupported()) // Windows 11 미만이면 무조건 false return false; - } - return false; + var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); + + return resource is bool b && b; } - private void SetWindowAccent(Window w, AccentState state) - { - var windowHelper = new WindowInteropHelper(w); + #endregion - windowHelper.EnsureHandle(); + #region Classes - var accent = new AccentPolicy { AccentState = state }; - var accentStructSize = Marshal.SizeOf(accent); + public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null); - var accentPtr = Marshal.AllocHGlobal(accentStructSize); - Marshal.StructureToPtr(accent, accentPtr, false); - - var data = new WindowCompositionAttributeData - { - Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY, - SizeOfData = accentStructSize, - Data = accentPtr - }; - - SetWindowCompositionAttribute(windowHelper.Handle, ref data); - - Marshal.FreeHGlobal(accentPtr); - } #endregion } } diff --git a/Flow.Launcher.Core/Resource/ThemeManager.cs b/Flow.Launcher.Core/Resource/ThemeManager.cs deleted file mode 100644 index 71f9acaa5..000000000 --- a/Flow.Launcher.Core/Resource/ThemeManager.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Flow.Launcher.Core.Resource -{ - public class ThemeManager - { - private static Theme instance; - private static object syncObject = new object(); - - public static Theme Instance - { - get - { - if (instance == null) - { - lock (syncObject) - { - if (instance == null) - { - instance = new Theme(); - } - } - } - return instance; - } - } - } -} diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 3f64b273e..20b884b3d 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -4,10 +4,12 @@ using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using System.Windows; -using JetBrains.Annotations; -using Squirrel; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Resource; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Infrastructure; @@ -15,30 +17,33 @@ using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using System.Text.Json.Serialization; -using System.Threading; +using JetBrains.Annotations; +using Squirrel; namespace Flow.Launcher.Core { public class Updater { - public string GitHubRepository { get; } + public string GitHubRepository { get; init; } - public Updater(string gitHubRepository) + private readonly IPublicAPI _api; + + public Updater(IPublicAPI publicAPI, string gitHubRepository) { + _api = publicAPI; GitHubRepository = gitHubRepository; } private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1); - public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) + public async Task UpdateAppAsync(bool silentUpdate = true) { await UpdateLock.WaitAsync().ConfigureAwait(false); try { if (!silentUpdate) - api.ShowMsg(api.GetTranslation("pleaseWait"), - api.GetTranslation("update_flowlauncher_update_check")); + _api.ShowMsg(_api.GetTranslation("pleaseWait"), + _api.GetTranslation("update_flowlauncher_update_check")); using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false); @@ -48,18 +53,18 @@ namespace Flow.Launcher.Core var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); var currentVersion = Version.Parse(Constant.Version); - Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>"); + Log.Info($"|Updater.UpdateApp|Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>"); if (newReleaseVersion <= currentVersion) { if (!silentUpdate) - MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest")); + _api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest")); return; } if (!silentUpdate) - api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), - api.GetTranslation("update_flowlauncher_updating")); + _api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"), + _api.GetTranslation("update_flowlauncher_updating")); await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); @@ -67,10 +72,10 @@ namespace Flow.Launcher.Core if (DataLocation.PortableDataLocationInUse()) { - var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; - FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); - if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) - MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), + var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}"; + FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)); + if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s))) + _api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), DataLocation.PortableDataPath, targetDestination)); } @@ -83,7 +88,7 @@ namespace Flow.Launcher.Core Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); - if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) + if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) { UpdateManager.RestartApp(Constant.ApplicationFileName); } @@ -96,8 +101,8 @@ namespace Flow.Launcher.Core Log.Exception($"|Updater.UpdateApp|Error Occurred", e); if (!silentUpdate) - api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), - api.GetTranslation("update_flowlauncher_check_connection")); + _api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"), + _api.GetTranslation("update_flowlauncher_check_connection")); } finally { @@ -119,7 +124,7 @@ namespace Flow.Launcher.Core } // https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs - private async Task GitHubUpdateManagerAsync(string repository) + private static async Task GitHubUpdateManagerAsync(string repository) { var uri = new Uri(repository); var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; @@ -141,12 +146,22 @@ namespace Flow.Launcher.Core return manager; } - public string NewVersionTips(string version) + private static string NewVersionTips(string version) { - var translator = InternationalizationManager.Instance; + var translator = Ioc.Default.GetRequiredService(); var tips = string.Format(translator.GetTranslation("newVersionTips"), version); return tips; } + + private static string Formatted(T t) + { + var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions + { + WriteIndented = true + }); + + return formatted; + } } } diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index 8a95ee79f..13da9f79f 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -31,6 +31,8 @@ namespace Flow.Launcher.Infrastructure public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png"); public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png"); public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png"); + public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png"); + public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png"); public static string PythonPath; public static string NodePath; @@ -46,10 +48,13 @@ namespace Flow.Launcher.Infrastructure public const string Themes = "Themes"; public const string Settings = "Settings"; public const string Logs = "Logs"; + public const string Cache = "Cache"; public const string Website = "https://flowlauncher.com"; public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher"; public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher"; public const string Docs = "https://flowlauncher.com/docs"; + + public const string SystemLanguageCode = "system"; } } diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index 76695a4e3..1085cc833 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; +using Windows.Win32; namespace Flow.Launcher.Infrastructure { @@ -15,7 +15,20 @@ namespace Flow.Launcher.Infrastructure { var explorerWindow = GetActiveExplorer(); string locationUrl = explorerWindow?.LocationURL; - return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null; + return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null; + } + + /// + /// Get directory path from a file path + /// + private static string GetDirectoryPath(string path) + { + if (!path.EndsWith("\\")) + { + return path + "\\"; + } + + return path; } /// @@ -54,12 +67,6 @@ namespace Flow.Launcher.Infrastructure return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First; } - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); - - private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - /// /// 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. /// @@ -70,9 +77,9 @@ namespace Flow.Launcher.Infrastructure var index = 0; var numRemaining = hWnds.Count; - EnumWindows((wnd, _) => + PInvoke.EnumWindows((wnd, _) => { - var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32()); + var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd); if (searchIndex != -1) { z[searchIndex] = index; diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 7d6448c43..b91da7114 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -35,6 +35,10 @@ false + + + + @@ -49,13 +53,18 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Flow.Launcher.Infrastructure/Helper.cs b/Flow.Launcher.Infrastructure/Helper.cs index 864d796c7..b02d84ca7 100644 --- a/Flow.Launcher.Infrastructure/Helper.cs +++ b/Flow.Launcher.Infrastructure/Helper.cs @@ -1,19 +1,11 @@ #nullable enable using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization; namespace Flow.Launcher.Infrastructure { public static class Helper { - static Helper() - { - jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - } - /// /// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy /// @@ -36,55 +28,5 @@ namespace Flow.Launcher.Infrastructure throw new NullReferenceException(); } } - - public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory) - { - if (!Directory.Exists(dataDirectory)) - { - Directory.CreateDirectory(dataDirectory); - } - - foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory)) - { - var data = Path.GetFileName(bundledDataPath); - var dataPath = Path.Combine(dataDirectory, data.NonNull()); - if (!File.Exists(dataPath)) - { - File.Copy(bundledDataPath, dataPath); - } - else - { - var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc; - var time2 = new FileInfo(dataPath).LastWriteTimeUtc; - if (time1 != time2) - { - File.Copy(bundledDataPath, dataPath, true); - } - } - } - } - - public static void ValidateDirectory(string path) - { - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - } - - private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions - { - WriteIndented = true - }; - - public static string Formatted(this T t) - { - var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions - { - WriteIndented = true - }); - - return formatted; - } } } diff --git a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs index f847ab189..b2a140755 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs @@ -1,6 +1,11 @@ -using System; +using System; +using System.Diagnostics; using System.Runtime.InteropServices; using Flow.Launcher.Plugin; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Infrastructure.Hotkey { @@ -10,44 +15,45 @@ namespace Flow.Launcher.Infrastructure.Hotkey /// public unsafe class GlobalHotkey : IDisposable { - private static readonly IntPtr hookId; - - - + private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback; + private static readonly UnhookWindowsHookExSafeHandle hookId; + public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state); internal static Func hookedKeyboardCallback; - //Modifier key constants - private const int VK_SHIFT = 0x10; - private const int VK_CONTROL = 0x11; - private const int VK_ALT = 0x12; - private const int VK_WIN = 91; - static GlobalHotkey() { // Set the hook - hookId = InterceptKeys.SetHook(& LowLevelKeyboardProc); + hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL); + } + + private static UnhookWindowsHookExSafeHandle SetHook(HOOKPROC proc, WINDOWS_HOOK_ID hookId) + { + using var curProcess = Process.GetCurrentProcess(); + using var curModule = curProcess.MainModule; + return PInvoke.SetWindowsHookEx(hookId, proc, PInvoke.GetModuleHandle(curModule.ModuleName), 0); } public static SpecialKeyState CheckModifiers() { SpecialKeyState state = new SpecialKeyState(); - if ((InterceptKeys.GetKeyState(VK_SHIFT) & 0x8000) != 0) + if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) & 0x8000) != 0) { //SHIFT is pressed state.ShiftPressed = true; } - if ((InterceptKeys.GetKeyState(VK_CONTROL) & 0x8000) != 0) + if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) & 0x8000) != 0) { //CONTROL is pressed state.CtrlPressed = true; } - if ((InterceptKeys.GetKeyState(VK_ALT) & 0x8000) != 0) + if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) & 0x8000) != 0) { //ALT is pressed state.AltPressed = true; } - if ((InterceptKeys.GetKeyState(VK_WIN) & 0x8000) != 0) + if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_LWIN) & 0x8000) != 0 || + (PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_RWIN) & 0x8000) != 0) { //WIN is pressed state.WinPressed = true; @@ -56,33 +62,33 @@ namespace Flow.Launcher.Infrastructure.Hotkey return state; } - [UnmanagedCallersOnly] - private static IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam) + private static LRESULT HookKeyboardCallback(int nCode, WPARAM wParam, LPARAM lParam) { bool continues = true; if (nCode >= 0) { - if (wParam.ToUInt32() == (int)KeyEvent.WM_KEYDOWN || - wParam.ToUInt32() == (int)KeyEvent.WM_KEYUP || - wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYDOWN || - wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYUP) + if (wParam.Value == (int)KeyEvent.WM_KEYDOWN || + wParam.Value == (int)KeyEvent.WM_KEYUP || + wParam.Value == (int)KeyEvent.WM_SYSKEYDOWN || + wParam.Value == (int)KeyEvent.WM_SYSKEYUP) { if (hookedKeyboardCallback != null) - continues = hookedKeyboardCallback((KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), CheckModifiers()); + continues = hookedKeyboardCallback((KeyEvent)wParam.Value, Marshal.ReadInt32(lParam), CheckModifiers()); } } if (continues) { - return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam); + return PInvoke.CallNextHookEx(hookId, nCode, wParam, lParam); } - return (IntPtr)(-1); + + return new LRESULT(1); } public void Dispose() { - InterceptKeys.UnhookWindowsHookEx(hookId); + hookId.Dispose(); } ~GlobalHotkey() @@ -90,4 +96,4 @@ namespace Flow.Launcher.Infrastructure.Hotkey Dispose(); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs b/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs deleted file mode 100644 index d33bac34c..000000000 --- a/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace Flow.Launcher.Infrastructure.Hotkey -{ - internal static unsafe class InterceptKeys - { - public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam); - - private const int WH_KEYBOARD_LL = 13; - - public static IntPtr SetHook(delegate* unmanaged proc) - { - using (Process curProcess = Process.GetCurrentProcess()) - using (ProcessModule curModule = curProcess.MainModule) - { - return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); - } - } - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public static extern IntPtr SetWindowsHookEx(int idHook, delegate* unmanaged lpfn, IntPtr hMod, uint dwThreadId); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool UnhookWindowsHookEx(IntPtr hhk); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public static extern IntPtr GetModuleHandle(string lpModuleName); - - [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)] - public static extern short GetKeyState(int keyCode); - } -} \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs b/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs index 15e306883..95bb25837 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs @@ -1,3 +1,5 @@ +using Windows.Win32; + namespace Flow.Launcher.Infrastructure.Hotkey { public enum KeyEvent @@ -5,21 +7,21 @@ namespace Flow.Launcher.Infrastructure.Hotkey /// /// Key down /// - WM_KEYDOWN = 256, + WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN, /// /// Key up /// - WM_KEYUP = 257, + WM_KEYUP = (int)PInvoke.WM_KEYUP, /// /// System key up /// - WM_SYSKEYUP = 261, + WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP, /// /// System key down /// - WM_SYSKEYDOWN = 260 + WM_SYSKEYDOWN = (int)PInvoke.WM_SYSKEYDOWN } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 14b8eef4e..030aff7cf 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -8,6 +8,7 @@ using Flow.Launcher.Infrastructure.UserSettings; using System; using System.Threading; using Flow.Launcher.Plugin; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Infrastructure.Http { @@ -17,8 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http private static HttpClient client = new HttpClient(); - public static IPublicAPI API { get; set; } - static Http() { // need to be added so it would work on a win10 machine @@ -78,20 +77,55 @@ namespace Flow.Launcher.Infrastructure.Http } catch (UriFormatException e) { - API.ShowMsg("Please try again", "Unable to parse Http Proxy"); + Ioc.Default.GetRequiredService().ShowMsg("Please try again", "Unable to parse Http Proxy"); Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e); } } - public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) + public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default) { try { using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + if (response.StatusCode == HttpStatusCode.OK) { - await using var fileStream = new FileStream(filePath, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream, token); + var totalBytes = response.Content.Headers.ContentLength ?? -1L; + var canReportProgress = totalBytes != -1; + + if (canReportProgress && reportProgress != null) + { + await using var contentStream = await response.Content.ReadAsStreamAsync(token); + await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true); + + var buffer = new byte[8192]; + long totalRead = 0; + int read; + double progressValue = 0; + + reportProgress(0); + + while ((read = await contentStream.ReadAsync(buffer, token)) > 0) + { + await fileStream.WriteAsync(buffer.AsMemory(0, read), token); + totalRead += read; + + progressValue = totalRead * 100.0 / totalBytes; + + if (token.IsCancellationRequested) + return; + else + reportProgress(progressValue); + } + + if (progressValue < 100) + reportProgress(100); + } + else + { + await using var fileStream = new FileStream(filePath, FileMode.CreateNew); + await response.Content.CopyToAsync(fileStream, token); + } } else { diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 612f495be..c8d3ffbc4 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -5,12 +5,10 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.Imaging; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; -using static Flow.Launcher.Infrastructure.Http.Http; namespace Flow.Launcher.Infrastructure.Image { @@ -22,12 +20,12 @@ namespace Flow.Launcher.Infrastructure.Image private static readonly ConcurrentDictionary GuidToKey = new(); private static IImageHashGenerator _hashGenerator; private static readonly bool EnableImageHash = true; + public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon)); public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon)); public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon)); public const int SmallIconSize = 64; public const int FullIconSize = 256; - private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" }; public static async Task InitializeAsync() @@ -60,7 +58,7 @@ namespace Flow.Launcher.Infrastructure.Image }); } - public static async Task Save() + public static async Task SaveAsync() { await storageLock.WaitAsync(); @@ -70,12 +68,22 @@ namespace Flow.Launcher.Infrastructure.Image .Select(x => x.Key) .ToList()); } + catch (System.Exception e) + { + Log.Exception($"|ImageLoader.SaveAsync|Failed to save image cache to file", e); + } finally { storageLock.Release(); } } + public static async Task WaitSaveAsync() + { + await storageLock.WaitAsync(); + storageLock.Release(); + } + private static async Task> LoadStorageToConcurrentDictionaryAsync() { await storageLock.WaitAsync(); @@ -139,7 +147,7 @@ namespace Flow.Launcher.Infrastructure.Image return new ImageResult(image, ImageType.ImageFile); } - if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + if (path.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { var imageSource = new BitmapImage(new Uri(path)); imageSource.Freeze(); @@ -172,7 +180,7 @@ namespace Flow.Launcher.Infrastructure.Image private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult) { // Download image from url - await using var resp = await GetStreamAsync(uriResult); + await using var resp = await Http.Http.GetStreamAsync(uriResult); await using var buffer = new MemoryStream(); await resp.CopyToAsync(buffer); buffer.Seek(0, SeekOrigin.Begin); @@ -215,8 +223,16 @@ namespace Flow.Launcher.Infrastructure.Image type = ImageType.ImageFile; if (loadFullImage) { - image = LoadFullImage(path); - type = ImageType.FullImageFile; + try + { + image = LoadFullImage(path); + type = ImageType.FullImageFile; + } + catch (NotSupportedException) + { + image = Image; + type = ImageType.Error; + } } else { diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 247238bb6..b98ea50fe 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -1,12 +1,19 @@ using System; using System.Runtime.InteropServices; using System.IO; +using System.Windows; using System.Windows.Interop; using System.Windows.Media.Imaging; -using System.Windows; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.Graphics.Gdi; namespace Flow.Launcher.Infrastructure.Image { + /// + /// Subclass of + /// [Flags] public enum ThumbnailOptions { @@ -22,91 +29,13 @@ namespace Flow.Launcher.Infrastructure.Image { // Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows - private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93"; - - [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern int SHCreateItemFromParsingName( - [MarshalAs(UnmanagedType.LPWStr)] string path, - IntPtr pbc, - ref Guid riid, - [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem); - - [DllImport("gdi32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool DeleteObject(IntPtr hObject); - - [ComImport] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")] - internal interface IShellItem - { - void BindToHandler(IntPtr pbc, - [MarshalAs(UnmanagedType.LPStruct)]Guid bhid, - [MarshalAs(UnmanagedType.LPStruct)]Guid riid, - out IntPtr ppv); - - void GetParent(out IShellItem ppsi); - void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName); - void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); - void Compare(IShellItem psi, uint hint, out int piOrder); - }; - - internal enum SIGDN : uint - { - NORMALDISPLAY = 0, - PARENTRELATIVEPARSING = 0x80018001, - PARENTRELATIVEFORADDRESSBAR = 0x8001c001, - DESKTOPABSOLUTEPARSING = 0x80028000, - PARENTRELATIVEEDITING = 0x80031001, - DESKTOPABSOLUTEEDITING = 0x8004c000, - FILESYSPATH = 0x80058000, - URL = 0x80068000 - } - - internal enum HResult - { - Ok = 0x0000, - False = 0x0001, - InvalidArguments = unchecked((int)0x80070057), - OutOfMemory = unchecked((int)0x8007000E), - NoInterface = unchecked((int)0x80004002), - Fail = unchecked((int)0x80004005), - ExtractionFailed = unchecked((int)0x8004B200), - ElementNotFound = unchecked((int)0x80070490), - TypeElementNotFound = unchecked((int)0x8002802B), - NoObject = unchecked((int)0x800401E5), - Win32ErrorCanceled = 1223, - Canceled = unchecked((int)0x800704C7), - ResourceInUse = unchecked((int)0x800700AA), - AccessDenied = unchecked((int)0x80030005) - } - - [ComImportAttribute()] - [GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")] - [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IShellItemImageFactory - { - [PreserveSig] - HResult GetImage( - [In, MarshalAs(UnmanagedType.Struct)] NativeSize size, - [In] ThumbnailOptions flags, - [Out] out IntPtr phbm); - } - - [StructLayout(LayoutKind.Sequential)] - internal struct NativeSize - { - private int width; - private int height; - - public int Width { set { width = value; } } - public int Height { set { height = value; } } - }; + private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID; + private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200; public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options) { - IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); + HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); try { @@ -115,39 +44,61 @@ namespace Flow.Launcher.Infrastructure.Image finally { // delete HBitmap to avoid memory leaks - DeleteObject(hBitmap); + PInvoke.DeleteObject(hBitmap); } } - - private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options) - { - IShellItem nativeShellItem; - Guid shellItem2Guid = new Guid(IShellItem2Guid); - int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem); - if (retCode != 0) + private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options) + { + var retCode = PInvoke.SHCreateItemFromParsingName( + fileName, + null, + GUID_IShellItem, + out var nativeShellItem); + + if (retCode != HRESULT.S_OK) throw Marshal.GetExceptionForHR(retCode); - NativeSize nativeSize = new NativeSize + if (nativeShellItem is not IShellItemImageFactory imageFactory) { - Width = width, - Height = height - }; - - IntPtr hBitmap; - HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap); - - // if extracting image thumbnail and failed, extract shell icon - if (options == ThumbnailOptions.ThumbnailOnly && hr == HResult.ExtractionFailed) - { - hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, out hBitmap); + Marshal.ReleaseComObject(nativeShellItem); + nativeShellItem = null; + throw new InvalidOperationException("Failed to get IShellItemImageFactory"); } - Marshal.ReleaseComObject(nativeShellItem); + SIZE size = new SIZE + { + cx = width, + cy = height + }; - if (hr == HResult.Ok) return hBitmap; + HBITMAP hBitmap = default; + try + { + try + { + imageFactory.GetImage(size, (SIIGBF)options, &hBitmap); + } + catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly) + { + // Fallback to IconOnly if ThumbnailOnly fails + imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap); + } + catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly) + { + // Fallback to IconOnly if files cannot be found + imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap); + } + } + finally + { + if (nativeShellItem != null) + { + Marshal.ReleaseComObject(nativeShellItem); + } + } - throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr)); + return hBitmap; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index d4bd473ac..9f5d6725e 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -12,13 +12,13 @@ namespace Flow.Launcher.Infrastructure.Logger { public static class Log { - public const string DirectoryName = "Logs"; + public const string DirectoryName = Constant.Logs; public static string CurrentLogDirectory { get; } static Log() { - CurrentLogDirectory = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version); + CurrentLogDirectory = DataLocation.VersionLogDirectory; if (!Directory.Exists(CurrentLogDirectory)) { Directory.CreateDirectory(CurrentLogDirectory); @@ -48,17 +48,45 @@ namespace Flow.Launcher.Infrastructure.Logger configuration.AddTarget("file", fileTargetASyncWrapper); configuration.AddTarget("debug", debugTarget); + var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper) + { + RuleName = "file" + }; #if DEBUG - var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper); - var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget); + var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget) + { + RuleName = "debug" + }; configuration.LoggingRules.Add(debugRule); -#else - var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper); #endif configuration.LoggingRules.Add(fileRule); LogManager.Configuration = configuration; } + public static void SetLogLevel(LOGLEVEL level) + { + switch (level) + { + case LOGLEVEL.DEBUG: + UseDebugLogLevel(); + break; + default: + UseInfoLogLevel(); + break; + } + Info(nameof(Logger), $"Using log level: {level}."); + } + + private static void UseDebugLogLevel() + { + LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal); + } + + private static void UseInfoLogLevel() + { + LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal); + } + private static void LogFaultyFormat(string message) { var logger = LogManager.GetLogger("FaultyLogger"); @@ -206,4 +234,10 @@ namespace Flow.Launcher.Infrastructure.Logger LogInternal(message, LogLevel.Warn); } } + + public enum LOGLEVEL + { + DEBUG, + INFO + } } diff --git a/Flow.Launcher.Infrastructure/MonitorInfo.cs b/Flow.Launcher.Infrastructure/MonitorInfo.cs new file mode 100644 index 000000000..3221708c1 --- /dev/null +++ b/Flow.Launcher.Infrastructure/MonitorInfo.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System; +using System.Runtime.InteropServices; +using System.Windows; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Flow.Launcher.Infrastructure; + +/// +/// Contains full information about a display monitor. +/// Codes are edited from: . +/// +internal class MonitorInfo +{ + /// + /// Gets the display monitors (including invisible pseudo-monitors associated with the mirroring drivers). + /// + /// A list of display monitors + public static unsafe IList GetDisplayMonitors() + { + var monitorCount = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CMONITORS); + var list = new List(monitorCount); + var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) => + { + list.Add(new MonitorInfo(monitor, rect)); + return true; + }); + var dwData = new LPARAM(); + var hdc = new HDC(); + bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData); + if (!ok) + { + Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error()); + } + return list; + } + + /// + /// Gets the display monitor that is nearest to a given window. + /// + /// Window handle + /// The display monitor that is nearest to a given window, or null if no monitor is found. + public static unsafe MonitorInfo GetNearestDisplayMonitor(HWND hwnd) + { + var nearestMonitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); + MonitorInfo nearestMonitorInfo = null; + var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) => + { + if (monitor == nearestMonitor) + { + nearestMonitorInfo = new MonitorInfo(monitor, rect); + return false; + } + return true; + }); + var dwData = new LPARAM(); + var hdc = new HDC(); + bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData); + if (!ok) + { + Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error()); + } + return nearestMonitorInfo; + } + + private readonly HMONITOR _monitor; + + internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect) + { + RectMonitor = + new Rect(new Point(rect->left, rect->top), + new Point(rect->right, rect->bottom)); + _monitor = monitor; + var info = new MONITORINFOEXW() { monitorInfo = new MONITORINFO() { cbSize = (uint)sizeof(MONITORINFOEXW) } }; + GetMonitorInfo(monitor, ref info); + RectWork = + new Rect(new Point(info.monitorInfo.rcWork.left, info.monitorInfo.rcWork.top), + new Point(info.monitorInfo.rcWork.right, info.monitorInfo.rcWork.bottom)); + Name = new string(info.szDevice.AsSpan()).Replace("\0", "").Trim(); + } + + /// + /// Gets the name of the display. + /// + public string Name { get; } + + /// + /// Gets the display monitor rectangle, expressed in virtual-screen coordinates. + /// + /// + /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. + /// + public Rect RectMonitor { get; } + + /// + /// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates. + /// + /// + /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. + /// + public Rect RectWork { get; } + + /// + /// Gets if the monitor is the the primary display monitor. + /// + public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(IntPtr.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY); + + /// + public override string ToString() => $"{Name} {RectMonitor.Width}x{RectMonitor.Height}"; + + private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi) + { + fixed (MONITORINFOEXW* lpmiLocal = &lpmi) + { + var lpmiBase = (MONITORINFO*)lpmiLocal; + var __result = PInvoke.GetMonitorInfo(hMonitor, lpmiBase); + return __result; + } + } +} diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt new file mode 100644 index 000000000..363ecb9d0 --- /dev/null +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -0,0 +1,61 @@ +SHCreateItemFromParsingName +DeleteObject +IShellItem +IShellItemImageFactory +S_OK + +SetWindowsHookEx +UnhookWindowsHookEx +CallNextHookEx +GetModuleHandle +GetKeyState +VIRTUAL_KEY + +WM_KEYDOWN +WM_KEYUP +WM_SYSKEYDOWN +WM_SYSKEYUP + +EnumWindows + +DwmSetWindowAttribute +DWM_SYSTEMBACKDROP_TYPE +DWM_WINDOW_CORNER_PREFERENCE + +MAX_PATH +SystemParametersInfo + +SetForegroundWindow + +GetWindowLong +GetForegroundWindow +GetDesktopWindow +GetShellWindow +GetWindowRect +GetClassName +FindWindowEx +WINDOW_STYLE + +SetLastError +WINDOW_EX_STYLE + +GetSystemMetrics +EnumDisplayMonitors +MonitorFromWindow +GetMonitorInfo +MONITORINFOEXW + +WM_ENTERSIZEMOVE +WM_EXITSIZEMOVE + +GetKeyboardLayout +GetWindowThreadProcessId +ActivateKeyboardLayout +GetKeyboardLayoutList +PostMessage +WM_INPUTLANGCHANGEREQUEST +INPUTLANGCHANGE_FORWARD +LOCALE_TRANSIENT_KEYBOARD1 +LOCALE_TRANSIENT_KEYBOARD2 +LOCALE_TRANSIENT_KEYBOARD3 +LOCALE_TRANSIENT_KEYBOARD4 \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs new file mode 100644 index 000000000..1a72ab7a6 --- /dev/null +++ b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Windows.Win32; + +// Edited from: https://github.com/files-community/Files +internal static partial class PInvoke +{ + [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] + static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); + + [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] + static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); + + // NOTE: + // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa. + // For more info, visit https://github.com/microsoft/CsWin32/issues/882 + public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong) + { + return sizeof(nint) is 4 + ? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong) + : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); + } +} diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 10799c676..e12764ed3 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -6,6 +6,7 @@ using Flow.Launcher.Infrastructure.UserSettings; using ToolGood.Words.Pinyin; using System.Collections.Generic; using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Infrastructure { @@ -103,13 +104,21 @@ namespace Flow.Launcher.Infrastructure {"Ou", "ou"} }); - private static readonly ReadOnlyDictionary first = new(new Dictionary(){ {"Ch", "i"}, {"Sh", "u"}, {"Zh", "v"} }); + + public PinyinAlphabet() + { + Initialize(Ioc.Default.GetRequiredService()); + } + private void Initialize([NotNull] Settings settings) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + } private static readonly ReadOnlyDictionary second = new(new Dictionary() { diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index 2a439b8cc..a8d5f5d62 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -1,12 +1,8 @@ -using System; -using System.IO; -using System.Reflection; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters; -using System.Runtime.Serialization.Formatters.Binary; +using System.IO; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin.SharedCommands; using MemoryPack; namespace Flow.Launcher.Infrastructure.Storage @@ -16,19 +12,17 @@ namespace Flow.Launcher.Infrastructure.Storage /// Normally, it has better performance, but not readable /// /// - /// It utilize MemoryPack, which means the object must be MemoryPackSerializable - /// https://github.com/Cysharp/MemoryPack + /// It utilize MemoryPack, which means the object must be MemoryPackSerializable /// public class BinaryStorage { - const string DirectoryName = "Cache"; + public const string FileSuffix = ".cache"; - const string FileSuffix = ".cache"; - - public BinaryStorage(string filename) + // Let the derived class to set the file path + public BinaryStorage(string filename, string directoryPath = null) { - var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); - Helper.ValidateDirectory(directoryPath); + directoryPath ??= DataLocation.CacheDirectory; + FilesFolders.ValidateDirectory(directoryPath); FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); } @@ -58,14 +52,14 @@ namespace Flow.Launcher.Infrastructure.Storage } } - private async ValueTask DeserializeAsync(Stream stream, T defaultData) + private static async ValueTask DeserializeAsync(Stream stream, T defaultData) { try { var t = await MemoryPackSerializer.DeserializeAsync(stream); return t; } - catch (System.Exception e) + catch (System.Exception) { // Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); return defaultData; diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs index 865041fb3..8b4062b6b 100644 --- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs @@ -1,17 +1,51 @@ using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { public class FlowLauncherJsonStorage : JsonStorage where T : new() { + private static readonly string ClassName = "FlowLauncherJsonStorage"; + + // 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 FlowLauncherJsonStorage() { var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); - Helper.ValidateDirectory(directoryPath); + FilesFolders.ValidateDirectory(directoryPath); var filename = typeof(T).Name; FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); } + + public new void Save() + { + try + { + base.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e); + } + } + + public new async Task SaveAsync() + { + try + { + await base.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e); + } + } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 642250627..a3488124b 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text.Json; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { @@ -16,7 +17,7 @@ namespace Flow.Launcher.Infrastructure.Storage protected T? Data; // need a new directory name - public const string DirectoryName = "Settings"; + public const string DirectoryName = Constant.Settings; public const string FileSuffix = ".json"; protected string FilePath { get; init; } = null!; @@ -31,12 +32,13 @@ namespace Flow.Launcher.Infrastructure.Storage protected JsonStorage() { } + public JsonStorage(string filePath) { FilePath = filePath; DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path"); - - Helper.ValidateDirectory(DirectoryPath); + + FilesFolders.ValidateDirectory(DirectoryPath); } public async Task LoadAsync() @@ -97,6 +99,7 @@ namespace Flow.Launcher.Infrastructure.Storage return default; } } + private void RestoreBackup() { Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully"); @@ -179,25 +182,21 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { string serialized = JsonSerializer.Serialize(Data, - new JsonSerializerOptions - { - WriteIndented = true - }); + new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(TempFilePath, serialized); AtomicWriteSetting(); } + public async Task SaveAsync() { - var tempOutput = File.OpenWrite(TempFilePath); + await using var tempOutput = File.OpenWrite(TempFilePath); await JsonSerializer.SerializeAsync(tempOutput, Data, - new JsonSerializerOptions - { - WriteIndented = true - }); + new JsonSerializerOptions { WriteIndented = true }); AtomicWriteSetting(); } + private void AtomicWriteSetting() { if (!File.Exists(FilePath)) @@ -206,9 +205,9 @@ namespace Flow.Launcher.Infrastructure.Storage } else { - File.Replace(TempFilePath, FilePath, BackupFilePath); + var finalFilePath = new FileInfo(FilePath).LinkTarget ?? FilePath; + File.Replace(TempFilePath, finalFilePath, BackupFilePath); } } - } } diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index abe3f55b5..e8cbd70fb 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -1,17 +1,30 @@ using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { - public class PluginJsonStorage :JsonStorage where T : new() + public class PluginJsonStorage : JsonStorage where T : new() { + // Use assembly name to check which plugin is using this storage + public readonly string AssemblyName; + + private static readonly string ClassName = "PluginJsonStorage"; + + // 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 PluginJsonStorage() { // C# related, add python related below var dataType = typeof(T); - var assemblyName = dataType.Assembly.GetName().Name; - DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName); - Helper.ValidateDirectory(DirectoryPath); + AssemblyName = dataType.Assembly.GetName().Name; + DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName); + FilesFolders.ValidateDirectory(DirectoryPath); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); } @@ -20,6 +33,29 @@ namespace Flow.Launcher.Infrastructure.Storage { Data = data; } + + public new void Save() + { + try + { + base.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); + } + } + + public new async Task SaveAsync() + { + try + { + await base.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); + } + } } } - diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index 4929e4cd2..7045517f5 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -1,28 +1,35 @@ -using Flow.Launcher.Plugin.SharedModels; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin.SharedModels; using System; using System.Collections.Generic; using System.Linq; +using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher.Infrastructure { public class StringMatcher { - private readonly MatchOption _defaultMatchOption = new MatchOption(); + private readonly MatchOption _defaultMatchOption = new(); public SearchPrecisionScore UserSettingSearchPrecision { get; set; } private readonly IAlphabet _alphabet; - public StringMatcher(IAlphabet alphabet = null) + public StringMatcher(IAlphabet alphabet, Settings settings) + { + _alphabet = alphabet; + UserSettingSearchPrecision = settings.QuerySearchPrecision; + } + + // This is a workaround to allow unit tests to set the instance + public StringMatcher(IAlphabet alphabet) { _alphabet = alphabet; } - public static StringMatcher Instance { get; internal set; } - public static MatchResult FuzzySearch(string query, string stringToCompare) { - return Instance.FuzzyMatch(query, stringToCompare); + return Ioc.Default.GetRequiredService().FuzzyMatch(query, stringToCompare); } public MatchResult FuzzyMatch(string query, string stringToCompare) @@ -241,16 +248,16 @@ namespace Flow.Launcher.Infrastructure return false; } - private bool IsAcronymChar(string stringToCompare, int compareStringIndex) + private static bool IsAcronymChar(string stringToCompare, int compareStringIndex) => char.IsUpper(stringToCompare[compareStringIndex]) || compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]); - private bool IsAcronymNumber(string stringToCompare, int compareStringIndex) + private static bool IsAcronymNumber(string stringToCompare, int compareStringIndex) => stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9; // To get the index of the closest space which preceeds the first matching index - private int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) + private static int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) { var closestSpaceIndex = -1; diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index e294f52b8..5b948e450 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -25,8 +25,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings return false; } + public static string VersionLogDirectory => Path.Combine(LogDirectory, Constant.Version); + public static string LogDirectory => Path.Combine(DataDirectory(), Constant.Logs); + + public static readonly string CacheDirectory = Path.Combine(DataDirectory(), Constant.Cache); + public static readonly string SettingsDirectory = Path.Combine(DataDirectory(), Constant.Settings); public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins); - public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins); + public static readonly string ThemesDirectory = Path.Combine(DataDirectory(), Constant.Themes); + + public static readonly string PluginSettingsDirectory = Path.Combine(SettingsDirectory, Constant.Plugins); + public static readonly string PluginCacheDirectory = Path.Combine(DataDirectory(), Constant.Cache, Constant.Plugins); public const string PythonEnvironmentName = "Python"; public const string NodeEnvironmentName = "Node.js"; diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 98f4dccda..da92a3583 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings @@ -6,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings public class PluginsSettings : BaseModel { private string pythonExecutablePath = string.Empty; - public string PythonExecutablePath { - get { return pythonExecutablePath; } + public string PythonExecutablePath + { + get => pythonExecutablePath; set { pythonExecutablePath = value; @@ -18,7 +20,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings private string nodeExecutablePath = string.Empty; public string NodeExecutablePath { - get { return nodeExecutablePath; } + get => nodeExecutablePath; set { nodeExecutablePath = value; @@ -26,19 +28,32 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } - public Dictionary Plugins { get; set; } = new Dictionary(); + /// + /// Only used for serialization + /// + public Dictionary Plugins { get; set; } = new(); + /// + /// Update plugin settings with metadata. + /// FL will get default values from metadata first and then load settings to metadata + /// + /// Parsed plugin metadatas public void UpdatePluginSettings(List metadatas) { foreach (var metadata in metadatas) { - if (Plugins.ContainsKey(metadata.ID)) + if (Plugins.TryGetValue(metadata.ID, out var settings)) { - var settings = Plugins[metadata.ID]; - + // If settings exist, update settings & metadata value + // update settings values with metadata if (string.IsNullOrEmpty(settings.Version)) + { settings.Version = metadata.Version; + } + settings.DefaultActionKeywords = metadata.ActionKeywords; // metadata provides default values + settings.DefaultSearchDelayTime = metadata.SearchDelayTime; // metadata provides default values + // update metadata values with settings if (settings.ActionKeywords?.Count > 0) { metadata.ActionKeywords = settings.ActionKeywords; @@ -51,30 +66,65 @@ namespace Flow.Launcher.Infrastructure.UserSettings } metadata.Disabled = settings.Disabled; metadata.Priority = settings.Priority; + metadata.SearchDelayTime = settings.SearchDelayTime; } else { + // If settings does not exist, create a new one Plugins[metadata.ID] = new Plugin { ID = metadata.ID, Name = metadata.Name, Version = metadata.Version, - ActionKeywords = metadata.ActionKeywords, + DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values + ActionKeywords = metadata.ActionKeywords, // use default value Disabled = metadata.Disabled, - Priority = metadata.Priority + Priority = metadata.Priority, + DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values + SearchDelayTime = metadata.SearchDelayTime, // use default value }; } } } + + public Plugin GetPluginSettings(string id) + { + if (Plugins.TryGetValue(id, out var plugin)) + { + return plugin; + } + return null; + } + + public Plugin RemovePluginSettings(string id) + { + Plugins.Remove(id, out var plugin); + return plugin; + } } + public class Plugin { public string ID { get; set; } + public string Name { get; set; } + public string Version { get; set; } - public List ActionKeywords { get; set; } // a reference of the action keywords from plugin manager + + [JsonIgnore] + public List DefaultActionKeywords { get; set; } + + // a reference of the action keywords from plugin manager + public List ActionKeywords { get; set; } + public int Priority { get; set; } + [JsonIgnore] + public SearchDelayTime? DefaultSearchDelayTime { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public SearchDelayTime? SearchDelayTime { get; set; } + /// /// Used only to save the state of the plugin in settings /// diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 30e3b77be..05a08f97f 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -1,10 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; using System.Text.Json.Serialization; using System.Windows; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.ViewModel; @@ -13,7 +15,25 @@ namespace Flow.Launcher.Infrastructure.UserSettings { public class Settings : BaseModel, IHotkeySettings { - private string language = "en"; + private FlowLauncherJsonStorage _storage; + private StringMatcher _stringMatcher = null; + + public void SetStorage(FlowLauncherJsonStorage storage) + { + _storage = storage; + } + + public void Initialize() + { + _stringMatcher = Ioc.Default.GetRequiredService(); + } + + public void Save() + { + _storage.Save(); + } + + private string language = Constant.SystemLanguageCode; private string _theme = Constant.DefaultTheme; public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}"; public string OpenResultModifiers { get; set; } = KeyConstant.Alt; @@ -48,21 +68,23 @@ namespace Flow.Launcher.Infrastructure.UserSettings get => _theme; set { - if (value == _theme) - return; - _theme = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(MaxResultsToShow)); + if (value != _theme) + { + _theme = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(MaxResultsToShow)); + } } } - public bool UseDropShadowEffect { get; set; } = false; + public bool UseDropShadowEffect { get; set; } = true; + public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None; /* Appearance Settings. It should be separated from the setting later.*/ public double WindowHeightSize { get; set; } = 42; public double ItemHeightSize { get; set; } = 58; public double QueryBoxFontSize { get; set; } = 20; public double ResultItemFontSize { get; set; } = 16; - public double ResultSubItemFontSize { get; set; } = 13; + public double ResultSubItemFontSize { get; set; } = 13; public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name; public string QueryBoxFontStyle { get; set; } public string QueryBoxFontWeight { get; set; } @@ -90,7 +112,34 @@ namespace Flow.Launcher.Infrastructure.UserSettings public double SettingWindowHeight { get; set; } = 700; public double? SettingWindowTop { get; set; } = null; public double? SettingWindowLeft { get; set; } = null; - public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal; + public WindowState SettingWindowState { get; set; } = WindowState.Normal; + + bool _showPlaceholder { get; set; } = false; + public bool ShowPlaceholder + { + get => _showPlaceholder; + set + { + if (_showPlaceholder != value) + { + _showPlaceholder = value; + OnPropertyChanged(); + } + } + } + string _placeholderText { get; set; } = string.Empty; + public string PlaceholderText + { + get => _placeholderText; + set + { + if (_placeholderText != value) + { + _placeholderText = value; + OnPropertyChanged(); + } + } + } public int CustomExplorerIndex { get; set; } = 0; @@ -180,6 +229,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings } }; + [JsonConverter(typeof(JsonStringEnumConverter))] + public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO; /// /// when false Alphabet static service will always return empty results @@ -189,6 +240,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool UseDoublePinyin { get; set; } = true; //For developing public bool AlwaysPreview { get; set; } = false; + public bool AlwaysStartEn { get; set; } = false; private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular; @@ -199,8 +251,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings set { _querySearchPrecision = value; - if (StringMatcher.Instance != null) - StringMatcher.Instance.UserSettingSearchPrecision = value; + if (_stringMatcher != null) + _stringMatcher.UserSettingSearchPrecision = value; } } @@ -219,10 +271,26 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// public double CustomWindowTop { get; set; } = 0; - public bool KeepMaxResults { get; set; } = false; - public int MaxResultsToShow { get; set; } = 5; - public int ActivateTimes { get; set; } + /// + /// Fixed window size + /// + private bool _keepMaxResults { get; set; } = false; + public bool KeepMaxResults + { + get => _keepMaxResults; + set + { + if (_keepMaxResults != value) + { + _keepMaxResults = value; + OnPropertyChanged(); + } + } + } + public int MaxResultsToShow { get; set; } = 5; + + public int ActivateTimes { get; set; } public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection(); @@ -239,11 +307,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool EnableUpdateLog { get; set; } public bool StartFlowLauncherOnSystemStartup { get; set; } = false; + public bool UseLogonTaskForStartup { get; set; } = false; public bool HideOnStartup { get; set; } = true; bool _hideNotifyIcon { get; set; } public bool HideNotifyIcon { - get { return _hideNotifyIcon; } + get => _hideNotifyIcon; set { _hideNotifyIcon = value; @@ -253,6 +322,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool LeaveCmdOpen { get; set; } public bool HideWhenDeactivated { get; set; } = true; + public bool SearchQueryResultsWithDelay { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public SearchDelayTime SearchDelayTime { get; set; } = SearchDelayTime.Normal; + [JsonConverter(typeof(JsonStringEnumConverter))] public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor; @@ -275,7 +349,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings [JsonIgnore] public bool WMPInstalled { get; set; } = true; - // This needs to be loaded last by staying at the bottom public PluginsSettings PluginSettings { get; set; } = new PluginsSettings(); @@ -371,7 +444,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings { Selected, Empty, - Preserved + Preserved, + ActionKeywordPreserved, + ActionKeywordSelected } public enum ColorSchemes @@ -406,4 +481,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings Fast, Custom } + + public enum BackdropTypes + { + None, + Acrylic, + Mica, + MicaAlt + } } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs new file mode 100644 index 000000000..f9c548de8 --- /dev/null +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -0,0 +1,521 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using Flow.Launcher.Infrastructure.UserSettings; +using Microsoft.Win32; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Dwm; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using Windows.Win32.UI.WindowsAndMessaging; +using Point = System.Windows.Point; + +namespace Flow.Launcher.Infrastructure +{ + public static class Win32Helper + { + #region Blur Handling + + public static bool IsBackdropSupported() + { + // Mica and Acrylic only supported Windows 11 22000+ + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 22000; + } + + public static unsafe bool DWMSetCloakForWindow(Window window, bool cloak) + { + var cloaked = cloak ? 1 : 0; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_CLOAK, + &cloaked, + (uint)Marshal.SizeOf()).Succeeded; + } + + public static unsafe bool DWMSetBackdropForWindow(Window window, BackdropTypes backdrop) + { + var backdropType = backdrop switch + { + BackdropTypes.Acrylic => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TRANSIENTWINDOW, + BackdropTypes.Mica => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_MAINWINDOW, + BackdropTypes.MicaAlt => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TABBEDWINDOW, + _ => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_AUTO + }; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_SYSTEMBACKDROP_TYPE, + &backdropType, + (uint)Marshal.SizeOf()).Succeeded; + } + + public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMode) + { + var darkMode = useDarkMode ? 1 : 0; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &darkMode, + (uint)Marshal.SizeOf()).Succeeded; + } + + /// + /// + /// + /// + /// DoNotRound, Round, RoundSmall, Default + /// + public static unsafe bool DWMSetCornerPreferenceForWindow(Window window, string cornerType) + { + var preference = cornerType switch + { + "DoNotRound" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND, + "Round" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND, + "RoundSmall" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUNDSMALL, + "Default" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DEFAULT, + _ => throw new InvalidOperationException("Invalid corner type") + }; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, + &preference, + (uint)Marshal.SizeOf()).Succeeded; + } + + #endregion + + #region Wallpaper + + public static unsafe string GetWallpaperPath() + { + var wallpaperPtr = stackalloc char[(int)PInvoke.MAX_PATH]; + PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, PInvoke.MAX_PATH, + wallpaperPtr, + 0); + var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr); + + return wallpaper.ToString(); + } + + #endregion + + #region Window Foreground + + public static nint GetForegroundWindow() + { + return PInvoke.GetForegroundWindow().Value; + } + + public static bool SetForegroundWindow(Window window) + { + return PInvoke.SetForegroundWindow(GetWindowHandle(window)); + } + + public static bool SetForegroundWindow(nint handle) + { + return PInvoke.SetForegroundWindow(new(handle)); + } + + public static bool IsForegroundWindow(Window window) + { + return IsForegroundWindow(GetWindowHandle(window)); + } + + internal static bool IsForegroundWindow(HWND handle) + { + return handle.Equals(PInvoke.GetForegroundWindow()); + } + + #endregion + + #region Task Switching + + /// + /// Hide windows in the Alt+Tab window list + /// + /// To hide a window + public static void HideFromAltTab(Window window) + { + var hwnd = GetWindowHandle(window); + + var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + // Add TOOLWINDOW style, remove APPWINDOW style + var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; + + SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); + } + + /// + /// Restore window display in the Alt+Tab window list. + /// + /// To restore the displayed window + public static void ShowInAltTab(Window window) + { + var hwnd = GetWindowHandle(window); + + var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + // Remove the TOOLWINDOW style and add the APPWINDOW style. + var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; + + SetWindowStyle(GetWindowHandle(window), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); + } + + /// + /// Disable windows toolbar's control box + /// This will also disable system menu with Alt+Space hotkey + /// + public static void DisableControlBox(Window window) + { + var hwnd = GetWindowHandle(window); + + var style = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + + style &= ~(int)WINDOW_STYLE.WS_SYSMENU; + + SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style); + } + + private static int GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex) + { + var style = PInvoke.GetWindowLong(hWnd, nIndex); + if (style == 0 && Marshal.GetLastPInvokeError() != 0) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + return style; + } + + private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong) + { + PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error + + var result = PInvoke.SetWindowLongPtr(hWnd, nIndex, dwNewLong); + if (result == 0 && Marshal.GetLastPInvokeError() != 0) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + + return result; + } + + #endregion + + #region Window Fullscreen + + private const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass"; + private const string WINDOW_CLASS_WINTAB = "Flip3D"; + private const string WINDOW_CLASS_PROGMAN = "Progman"; + private const string WINDOW_CLASS_WORKERW = "WorkerW"; + + private static HWND _hwnd_shell; + private static HWND HWND_SHELL => + _hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow(); + + private static HWND _hwnd_desktop; + private static HWND HWND_DESKTOP => + _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow(); + + public static unsafe bool IsForegroundWindowFullscreen() + { + // Get current active window + var hWnd = PInvoke.GetForegroundWindow(); + if (hWnd.Equals(HWND.Null)) + { + return false; + } + + // If current active window is desktop or shell, exit early + if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)) + { + return false; + } + + string windowClass; + const int capacity = 256; + Span buffer = stackalloc char[capacity]; + int validLength; + fixed (char* pBuffer = buffer) + { + validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity); + } + + windowClass = buffer[..validLength].ToString(); + + // For Win+Tab (Flip3D) + if (windowClass == WINDOW_CLASS_WINTAB) + { + return false; + } + + PInvoke.GetWindowRect(hWnd, out var appBounds); + + // For console (ConsoleWindowClass), we have to check for negative dimensions + if (windowClass == WINDOW_CLASS_CONSOLE) + { + return appBounds.top < 0 && appBounds.bottom < 0; + } + + // For desktop (Progman or WorkerW, depends on the system), we have to check + if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW) + { + var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null); + hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView"); + if (hWndDesktop.Value != IntPtr.Zero) + { + return false; + } + } + + var monitorInfo = MonitorInfo.GetNearestDisplayMonitor(hWnd); + return (appBounds.bottom - appBounds.top) == monitorInfo.RectMonitor.Height && + (appBounds.right - appBounds.left) == monitorInfo.RectMonitor.Width; + } + + #endregion + + #region Pixel to DIP + + /// + /// Transforms pixels to Device Independent Pixels used by WPF + /// + /// current window, required to get presentation source + /// horizontal position in pixels + /// vertical position in pixels + /// point containing device independent pixels + public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY) + { + Matrix matrix; + var source = PresentationSource.FromVisual(visual); + if (source is not null) + { + matrix = source.CompositionTarget.TransformFromDevice; + } + else + { + using var src = new HwndSource(new HwndSourceParameters()); + matrix = src.CompositionTarget.TransformFromDevice; + } + + return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY)); + } + + #endregion + + #region WndProc + + public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE; + public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE; + + #endregion + + #region Window Handle + + internal static HWND GetWindowHandle(Window window, bool ensure = false) + { + var windowHelper = new WindowInteropHelper(window); + if (ensure) + { + windowHelper.EnsureHandle(); + } + return new(windowHelper.Handle); + } + + #endregion + + #region Keyboard Layout + + private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f + private const string EnglishLanguageTag = "en"; + + private static readonly string[] ImeLanguageTags = + { + "zh", // Chinese + "ja", // Japanese + "ko", // Korean + }; + + private const uint KeyboardLayoutLoWord = 0xFFFF; + + // Store the previous keyboard layout + private static HKL _previousLayout; + + /// + /// Switches the keyboard layout to English if available. + /// + /// If true, the current keyboard layout will be stored for later restoration. + /// Thrown when there's an error getting the window thread process ID. + public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious) + { + // Find an installed English layout + var enHKL = FindEnglishKeyboardLayout(); + + // No installed English layout found + if (enHKL == HKL.Null) return; + + // When application is exiting, the Application.Current will be null + if (Application.Current == null) return; + + // Get the FL main window + var hwnd = GetWindowHandle(Application.Current.MainWindow, true); + if (hwnd == HWND.Null) return; + + // Check if the FL main window is the current foreground window + if (!IsForegroundWindow(hwnd)) + { + var result = PInvoke.SetForegroundWindow(hwnd); + if (!result) throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // Get the current foreground window thread ID + var threadId = PInvoke.GetWindowThreadProcessId(hwnd); + if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + + // If the current layout has an IME mode, disable it without switching to another layout. + // This is needed because for languages with IME mode, Flow Launcher just temporarily disables + // the IME mode instead of switching to another layout. + var currentLayout = PInvoke.GetKeyboardLayout(threadId); + var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord; + foreach (var imeLangTag in ImeLanguageTags) + { + var langTag = GetLanguageTag(currentLangId); + if (langTag.StartsWith(imeLangTag, StringComparison.OrdinalIgnoreCase)) return; + } + + // Backup current keyboard layout + if (backupPrevious) _previousLayout = currentLayout; + + // Switch to English layout + PInvoke.ActivateKeyboardLayout(enHKL, 0); + } + + /// + /// Restores the previously backed-up keyboard layout. + /// If it wasn't backed up or has already been restored, this method does nothing. + /// + public static void RestorePreviousKeyboardLayout() + { + if (_previousLayout == HKL.Null) return; + + var hwnd = PInvoke.GetForegroundWindow(); + if (hwnd == HWND.Null) return; + + PInvoke.PostMessage( + hwnd, + PInvoke.WM_INPUTLANGCHANGEREQUEST, + PInvoke.INPUTLANGCHANGE_FORWARD, + _previousLayout.Value + ); + + _previousLayout = HKL.Null; + } + + /// + /// Finds an installed English keyboard layout. + /// + /// + /// + private static unsafe HKL FindEnglishKeyboardLayout() + { + // Get the number of keyboard layouts + int count = PInvoke.GetKeyboardLayoutList(0, null); + if (count <= 0) return HKL.Null; + + // Get all keyboard layouts + var handles = new HKL[count]; + fixed (HKL* h = handles) + { + var result = PInvoke.GetKeyboardLayoutList(count, h); + if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // Look for any English keyboard layout + foreach (var hkl in handles) + { + // The lower word contains the language identifier + var langId = (uint)hkl.Value & KeyboardLayoutLoWord; + var langTag = GetLanguageTag(langId); + + // Check if it's an English layout + if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase)) + { + return hkl; + } + } + + return HKL.Null; + } + + /// + /// Returns the + /// + /// BCP 47 language tag + /// + /// of the current input language. + /// + /// + /// Edited from: https://github.com/dotnet/winforms + /// + private static string GetLanguageTag(uint langId) + { + // We need to convert the language identifier to a language tag, because they are deprecated and may have a + // transient value. + // https://learn.microsoft.com/globalization/locale/other-locale-names#lcid + // https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks + // + // It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect + // language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID" + // instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet). + // + // Try to extract proper language tag from registry as a workaround approved by a Windows team. + // https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949 + // + // NOTE: this logic may break in future versions of Windows since it is not documented. + if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD2 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD3 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD4) + { + using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath); + if (key?.GetValue("Languages") is string[] languages) + { + foreach (string language in languages) + { + using var subKey = key.OpenSubKey(language); + if (subKey?.GetValue("TransientLangId") is int transientLangId + && transientLangId == langId) + { + return language; + } + } + } + } + + return CultureInfo.GetCultureInfo((int)langId).Name; + } + + #endregion + + #region Notification + + public static bool IsNotificationSupported() + { + // Notifications only supported on Windows 10 19041+ + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 19041; + } + + #endregion + } +} diff --git a/Flow.Launcher.Plugin/ActionContext.cs b/Flow.Launcher.Plugin/ActionContext.cs index e31c8e31d..9e05bbd06 100644 --- a/Flow.Launcher.Plugin/ActionContext.cs +++ b/Flow.Launcher.Plugin/ActionContext.cs @@ -51,6 +51,9 @@ namespace Flow.Launcher.Plugin (WinPressed ? ModifierKeys.Windows : ModifierKeys.None); } + /// + /// Default object with all keys not pressed. + /// public static readonly SpecialKeyState Default = new () { CtrlPressed = false, ShiftPressed = false, diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 7b0880b67..1472813b8 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -14,10 +14,10 @@ - 4.3.0 - 4.3.0 - 4.3.0 - 4.3.0 + 4.4.0 + 4.4.0 + 4.4.0 + 4.4.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -57,17 +57,25 @@ - + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs new file mode 100644 index 000000000..cc4f94c56 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// This interface is for plugins that wish to provide file preview (external preview) + /// via a third party app instead of the default preview. + /// + public interface IAsyncExternalPreview : IFeatures + { + /// + /// Method for opening/showing the preview. + /// + /// The file path to open the preview for + /// Whether to send a toast message notification on failure for the user + public Task OpenPreviewAsync(string path, bool sendFailToast = true); + + /// + /// Method for closing/hiding the preview. + /// + public Task ClosePreviewAsync(); + + /// + /// Method for switching the preview to the next file result. + /// This requires the external preview be already open/showing + /// + /// The file path to switch the preview for + /// Whether to send a toast message notification on failure for the user + public Task SwitchPreviewAsync(string path, bool sendFailToast = true); + + /// + /// Allows the preview plugin to override the AlwaysPreview setting. Typically useful if plugin's preview does not + /// fully work well with being shown together when the query window appears with results. + /// When AlwaysPreview setting is on and this is set to false, the preview will not be shown when query + /// window appears with results, instead the internal preview will be shown. + /// + /// + public bool AllowAlwaysPreview(); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index c95a8ce7b..eeb3f5de3 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using System.Windows; namespace Flow.Launcher.Plugin { @@ -16,7 +17,8 @@ namespace Flow.Launcher.Plugin public interface IPublicAPI { /// - /// Change Flow.Launcher query + /// Change Flow.Launcher query. + /// When current results are from context menu or history, it will go back to query results before changing query. /// /// query text /// @@ -150,7 +152,6 @@ namespace Flow.Launcher.Plugin /// public void RemoveGlobalKeyboardCallback(Func callback); - /// /// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses /// @@ -180,19 +181,24 @@ namespace Flow.Launcher.Plugin /// /// URL to download file /// path to save downloaded file + /// + /// Action to report progress. The input of the action is the progress value which is a double value between 0 and 100. + /// It will be called if url support range request and the reportProgress is not null. + /// /// place to store file /// Task showing the progress - Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default); + Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default); /// - /// Add ActionKeyword for specific plugin + /// Add ActionKeyword and update action keyword metadata for specific plugin + /// Before adding, please check if action keyword is already assigned by /// /// ID for plugin that needs to add action keyword /// The actionkeyword that is supposed to be added void AddActionKeyword(string pluginId, string newActionKeyword); /// - /// Remove ActionKeyword for specific plugin + /// Remove ActionKeyword and update action keyword metadata for specific plugin /// /// ID for plugin that needs to remove action keyword /// The actionkeyword that is supposed to be removed @@ -294,9 +300,106 @@ namespace Flow.Launcher.Plugin /// /// Reloads the query. - /// This method should run + /// When current results are from context menu or history, it will go back to query results before requerying. /// /// Choose the first result after reload if true; keep the last selected result if false. Default is true. public void ReQuery(bool reselect = true); + + /// + /// Back to the query results. + /// This method should run when selected item is from context menu or history. + /// + public void BackToQueryResults(); + + /// + /// Displays a standardised Flow message box. + /// + /// The message of the message box. + /// The caption of the message box. + /// Specifies which button or buttons to display. + /// Specifies the icon to display. + /// Specifies the default result of the message box. + /// Specifies which message box button is clicked by the user. + public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK); + + /// + /// Displays a standardised Flow progress box. + /// + /// The caption of the progress box. + /// + /// Time-consuming task function, whose input is the action to report progress. + /// The input of the action is the progress value which is a double value between 0 and 100. + /// If there are any exceptions, this action will be null. + /// + /// When user cancel the progress, this action will be called. + /// + public Task ShowProgressBoxAsync(string caption, Func, Task> reportProgressAsync, Action cancelProgress = null); + + /// + /// Start the loading bar in main window + /// + public void StartLoadingBar(); + + /// + /// Stop the loading bar in main window + /// + public void StopLoadingBar(); + + /// + /// Update the plugin manifest + /// + /// + /// FL has multiple urls to download the plugin manifest. Set this to true to only use the primary url. + /// + /// + /// True if the manifest is updated successfully, false otherwise + public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default); + + /// + /// Get the plugin manifest + /// + /// + public IReadOnlyList GetPluginManifest(); + + /// + /// Check if the plugin has been modified. + /// If this plugin is updated, installed or uninstalled and users do not restart the app, + /// it will be marked as modified + /// + /// Plugin id + /// + public bool PluginModified(string id); + + /// + /// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url, + /// unless it's a local path installation + /// + /// The metadata of the old plugin to update + /// The new plugin to update + /// + /// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed. + /// + /// + public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath); + + /// + /// Install a plugin. By default will remove the zip file if installation is from url, + /// unless it's a local path installation + /// + /// The plugin to install + /// + /// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed. + /// + public void InstallPlugin(UserPlugin plugin, string zipFilePath); + + /// + /// Uninstall a plugin + /// + /// The metadata of the plugin to uninstall + /// + /// Plugin has their own settings. If this is set to true, the plugin settings will be removed. + /// + /// + public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs index fd21460ac..aa4e4a56d 100644 --- a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs +++ b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs @@ -4,17 +4,42 @@ using System.Threading; namespace Flow.Launcher.Plugin { + /// + /// Interface for plugins that want to manually update their results + /// public interface IResultUpdated : IFeatures { + /// + /// Event that is triggered when the results are updated + /// event ResultUpdatedEventHandler ResultsUpdated; } + /// + /// Delegate for the ResultsUpdated event + /// + /// + /// public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e); + /// + /// Event arguments for the ResultsUpdated event + /// public class ResultUpdatedEventArgs : EventArgs { + /// + /// List of results that should be displayed + /// public List Results; + + /// + /// Query that triggered the update + /// public Query Query; + + /// + /// Token that can be used to cancel the update + /// public CancellationToken Token { get; init; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs index d5ffba20b..f034243c3 100644 --- a/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs +++ b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs @@ -2,8 +2,15 @@ namespace Flow.Launcher.Plugin { + /// + /// This interface is used to create settings panel for .Net plugins + /// public interface ISettingProvider { + /// + /// Create settings panel control for .Net plugins + /// + /// Control CreateSettingPanel(); } } diff --git a/Flow.Launcher.Plugin/NativeMethods.txt b/Flow.Launcher.Plugin/NativeMethods.txt new file mode 100644 index 000000000..e3e2b705e --- /dev/null +++ b/Flow.Launcher.Plugin/NativeMethods.txt @@ -0,0 +1,3 @@ +EnumThreadWindows +GetWindowText +GetWindowTextLength \ No newline at end of file diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs index f040752bd..a42e3930c 100644 --- a/Flow.Launcher.Plugin/PluginInitContext.cs +++ b/Flow.Launcher.Plugin/PluginInitContext.cs @@ -5,10 +5,18 @@ /// public class PluginInitContext { + /// + /// Default constructor. + /// public PluginInitContext() { } + /// + /// Constructor. + /// + /// + /// public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api) { CurrentPluginMetadata = currentPluginMetadata; diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index b4e06913e..1496765ce 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -4,24 +4,77 @@ using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { + /// + /// Plugin metadata + /// public class PluginMetadata : BaseModel { - private string _pluginDirectory; + /// + /// Plugin ID. + /// public string ID { get; set; } - public string Name { get; set; } - public string Author { get; set; } - public string Version { get; set; } - public string Language { get; set; } - public string Description { get; set; } - public string Website { get; set; } - public bool Disabled { get; set; } - public string ExecuteFilePath { get; private set;} + /// + /// Plugin name. + /// + public string Name { get; set; } + + /// + /// Plugin author. + /// + public string Author { get; set; } + + /// + /// Plugin version. + /// + public string Version { get; set; } + + /// + /// Plugin language. + /// See + /// + public string Language { get; set; } + + /// + /// Plugin description. + /// + public string Description { get; set; } + + /// + /// Plugin website. + /// + public string Website { get; set; } + + /// + /// Whether plugin is disabled. + /// + public bool Disabled { get; set; } + + /// + /// Plugin execute file path. + /// + public string ExecuteFilePath { get; private set; } + + /// + /// Plugin execute file name. + /// public string ExecuteFileName { get; set; } + /// + /// Plugin assembly name. + /// Only available for .Net plugins. + /// + [JsonIgnore] + public string AssemblyName { get; internal set; } + + private string _pluginDirectory; + + /// + /// Plugin source directory. + /// public string PluginDirectory { - get { return _pluginDirectory; } + get => _pluginDirectory; internal set { _pluginDirectory = value; @@ -30,28 +83,78 @@ namespace Flow.Launcher.Plugin } } + /// + /// The first action keyword of plugin. + /// public string ActionKeyword { get; set; } + /// + /// All action keywords of plugin. + /// public List ActionKeywords { get; set; } - public string IcoPath { get; set;} - - public override string ToString() - { - return Name; - } + /// + /// Hide plugin keyword setting panel. + /// + public bool HideActionKeywordPanel { get; set; } + /// + /// Plugin search delay time. Null means use default search delay time. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public SearchDelayTime? SearchDelayTime { get; set; } = null; + + /// + /// Plugin icon path. + /// + public string IcoPath { get; set;} + + /// + /// Plugin priority. + /// [JsonIgnore] public int Priority { get; set; } /// - /// Init time include both plugin load time and init time + /// Init time include both plugin load time and init time. /// [JsonIgnore] public long InitTime { get; set; } + + /// + /// Average query time. + /// [JsonIgnore] public long AvgQueryTime { get; set; } + + /// + /// Query count. + /// [JsonIgnore] public int QueryCount { get; set; } + + /// + /// The path to the plugin settings directory which is not validated. + /// It is used to store plugin settings files and data files. + /// When plugin is deleted, FL will ask users whether to keep its settings. + /// If users do not want to keep, this directory will be deleted. + /// + public string PluginSettingsDirectoryPath { get; internal set; } + + /// + /// The path to the plugin cache directory which is not validated. + /// It is used to store cache files. + /// When plugin is deleted, this directory will be deleted as well. + /// + public string PluginCacheDirectoryPath { get; internal set; } + + /// + /// Convert to string. + /// + /// + public override string ToString() + { + return Name; + } } } diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs index 7bf634691..f2c14d70c 100644 --- a/Flow.Launcher.Plugin/PluginPair.cs +++ b/Flow.Launcher.Plugin/PluginPair.cs @@ -1,21 +1,37 @@ namespace Flow.Launcher.Plugin { + /// + /// Plugin instance and plugin metadata + /// public class PluginPair { + /// + /// Plugin instance + /// public IAsyncPlugin Plugin { get; internal set; } + + /// + /// Plugin metadata + /// public PluginMetadata Metadata { get; internal set; } - - + /// + /// Convert to string + /// + /// public override string ToString() { return Metadata.Name; } + /// + /// Compare by plugin metadata ID + /// + /// + /// public override bool Equals(object obj) { - PluginPair r = obj as PluginPair; - if (r != null) + if (obj is PluginPair r) { return string.Equals(r.Metadata.ID, Metadata.ID); } @@ -25,6 +41,10 @@ } } + /// + /// Get hash code + /// + /// public override int GetHashCode() { var hashcode = Metadata.ID?.GetHashCode() ?? 0; diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index b41675a1a..913dc31ae 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -1,23 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { + /// + /// Represents a query that is sent to a plugin. + /// public class Query { - public Query() { } - - [Obsolete("Use the default Query constructor.")] - public Query(string rawQuery, string search, string[] terms, string[] searchTerms, string actionKeyword = "") - { - Search = search; - RawQuery = rawQuery; - SearchTerms = searchTerms; - ActionKeyword = actionKeyword; - } - /// /// Raw query, this includes action keyword if it has /// We didn't recommend use this property directly. You should always use Search property. @@ -51,10 +40,9 @@ namespace Flow.Launcher.Plugin public const string TermSeparator = " "; /// - /// User can set multiple action keywords seperated by ';' + /// User can set multiple action keywords seperated by whitespace /// - public const string ActionKeywordSeparator = ";"; - + public const string ActionKeywordSeparator = TermSeparator; /// /// Wildcard action keyword. Plugins using this value will be queried on every search. @@ -67,13 +55,13 @@ namespace Flow.Launcher.Plugin /// public string ActionKeyword { get; init; } - [JsonIgnore] /// /// Splits by spaces and returns the first item. /// /// /// returns an empty string when does not have enough items. /// + [JsonIgnore] public string FirstSearch => SplitSearch(0); [JsonIgnore] diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index ea79386b3..910485438 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -13,7 +12,6 @@ namespace Flow.Launcher.Plugin /// public class Result { - private string _pluginDirectory; private string _icoPath; @@ -70,7 +68,8 @@ namespace Flow.Launcher.Plugin && !string.IsNullOrEmpty(PluginDirectory) && !Path.IsPathRooted(value) && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) - && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { _icoPath = Path.Combine(PluginDirectory, value); } @@ -156,27 +155,6 @@ namespace Flow.Launcher.Plugin } } - /// - public override bool Equals(object obj) - { - var r = obj as Result; - - var equality = string.Equals(r?.Title, Title) && - string.Equals(r?.SubTitle, SubTitle) && - string.Equals(r?.AutoCompleteText, AutoCompleteText) && - string.Equals(r?.CopyText, CopyText) && - string.Equals(r?.IcoPath, IcoPath) && - TitleHighlightData == r.TitleHighlightData; - - return equality; - } - - /// - public override int GetHashCode() - { - return HashCode.Combine(Title, SubTitle, AutoCompleteText, CopyText, IcoPath); - } - /// public override string ToString() { @@ -205,6 +183,16 @@ namespace Flow.Launcher.Plugin TitleHighlightData = TitleHighlightData, OriginQuery = OriginQuery, PluginDirectory = PluginDirectory, + ContextData = ContextData, + PluginID = PluginID, + TitleToolTip = TitleToolTip, + SubTitleToolTip = SubTitleToolTip, + PreviewPanel = PreviewPanel, + ProgressBar = ProgressBar, + ProgressBarColor = ProgressBarColor, + Preview = Preview, + AddSelectedCount = AddSelectedCount, + RecordKey = RecordKey }; } @@ -262,6 +250,24 @@ namespace Flow.Launcher.Plugin /// public PreviewInfo Preview { get; set; } = PreviewInfo.Default; + /// + /// Determines if the user selection count should be added to the score. This can be useful when set to false to allow the result sequence order to be the same everytime instead of changing based on selection. + /// + public bool AddSelectedCount { get; set; } = true; + + /// + /// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users. + /// + public const int MaxScore = int.MaxValue; + + /// + /// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records. + /// This can be useful when your plugin will change the Title or SubTitle of the result dynamically. + /// If the plugin does not specific this, FL just uses Title and SubTitle to identify this result. + /// Note: Because old data does not have this key, we should use null as the default value for consistency. + /// + public string RecordKey { get; set; } = null; + /// /// Info of the preview section of a /// @@ -270,12 +276,12 @@ namespace Flow.Launcher.Plugin /// /// Full image used for preview panel /// - public string PreviewImagePath { get; set; } + public string PreviewImagePath { get; set; } = null; /// /// Determines if the preview image should occupy the full width of the preview panel. /// - public bool IsMedia { get; set; } + public bool IsMedia { get; set; } = false; /// /// Result description text that is shown at the bottom of the preview panel. @@ -283,12 +289,17 @@ namespace Flow.Launcher.Plugin /// /// When a value is not set, the will be used. /// - public string Description { get; set; } + public string Description { get; set; } = null; /// /// Delegate to get the preview panel's image /// - public IconDelegate PreviewDelegate { get; set; } + public IconDelegate PreviewDelegate { get; set; } = null; + + /// + /// File path of the result. For third-party programs providing external preview. + /// + public string FilePath { get; set; } = null; /// /// Default instance of @@ -299,6 +310,7 @@ namespace Flow.Launcher.Plugin Description = null, IsMedia = false, PreviewDelegate = null, + FilePath = null, }; } } diff --git a/Flow.Launcher.Plugin/SearchDelayTime.cs b/Flow.Launcher.Plugin/SearchDelayTime.cs new file mode 100644 index 000000000..ae1daabe0 --- /dev/null +++ b/Flow.Launcher.Plugin/SearchDelayTime.cs @@ -0,0 +1,32 @@ +namespace Flow.Launcher.Plugin; + +/// +/// Enum for search delay time +/// +public enum SearchDelayTime +{ + /// + /// Very long search delay time. 250ms. + /// + VeryLong, + + /// + /// Long search delay time. 200ms. + /// + Long, + + /// + /// Normal search delay time. 150ms. Default value. + /// + Normal, + + /// + /// Short search delay time. 100ms. + /// + Short, + + /// + /// Very short search delay time. 50ms. + /// + VeryShort +} diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index dd8c4b112..1de5841a5 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -21,7 +21,8 @@ namespace Flow.Launcher.Plugin.SharedCommands /// /// /// - public static void CopyAll(this string sourcePath, string targetPath) + /// + public static void CopyAll(this string sourcePath, string targetPath, Func messageBoxExShow = null) { // Get the subdirectories for the specified directory. DirectoryInfo dir = new DirectoryInfo(sourcePath); @@ -54,7 +55,7 @@ namespace Flow.Launcher.Plugin.SharedCommands foreach (DirectoryInfo subdir in dirs) { string temppath = Path.Combine(targetPath, subdir.Name); - CopyAll(subdir.FullName, temppath); + CopyAll(subdir.FullName, temppath, messageBoxExShow); } } catch (Exception) @@ -62,8 +63,9 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG throw; #else - MessageBox.Show(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath)); - RemoveFolderIfExists(targetPath); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath)); + RemoveFolderIfExists(targetPath, messageBoxExShow); #endif } @@ -75,8 +77,9 @@ namespace Flow.Launcher.Plugin.SharedCommands /// /// /// + /// /// - public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath) + public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath, Func messageBoxExShow = null) { try { @@ -96,7 +99,8 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG throw; #else - MessageBox.Show(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath)); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath)); return false; #endif } @@ -107,7 +111,8 @@ namespace Flow.Launcher.Plugin.SharedCommands /// Deletes a folder if it exists /// /// - public static void RemoveFolderIfExists(this string path) + /// + public static void RemoveFolderIfExists(this string path, Func messageBoxExShow = null) { try { @@ -119,7 +124,8 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG throw; #else - MessageBox.Show(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path)); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path)); #endif } } @@ -148,7 +154,8 @@ namespace Flow.Launcher.Plugin.SharedCommands /// Open a directory window (using the OS's default handler, usually explorer) /// /// - public static void OpenPath(string fileOrFolderPath) + /// + public static void OpenPath(string fileOrFolderPath, Func messageBoxExShow = null) { var psi = new ProcessStartInfo { @@ -166,7 +173,8 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG throw; #else - MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath)); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath)); #endif } } @@ -177,7 +185,8 @@ namespace Flow.Launcher.Plugin.SharedCommands /// File path /// Working directory /// Open as Administrator - public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false) + /// + public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false, Func messageBoxExShow = null) { var psi = new ProcessStartInfo { @@ -196,7 +205,8 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG throw; #else - MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", filePath)); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", filePath)); #endif } } @@ -308,5 +318,51 @@ namespace Flow.Launcher.Plugin.SharedCommands { return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; } + + /// + /// Validates a directory, creating it if it doesn't exist + /// + /// + public static void ValidateDirectory(string path) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + + /// + /// Validates a data directory, synchronizing it by ensuring all files from a bundled source directory exist in it. + /// If files are missing or outdated, they are copied from the bundled directory to the data directory. + /// + /// + /// + public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory) + { + if (!Directory.Exists(dataDirectory)) + { + Directory.CreateDirectory(dataDirectory); + } + + foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory)) + { + var data = Path.GetFileName(bundledDataPath); + if (data == null) continue; + var dataPath = Path.Combine(dataDirectory, data); + if (!File.Exists(dataPath)) + { + File.Copy(bundledDataPath, dataPath); + } + else + { + var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc; + var time2 = new FileInfo(dataPath).LastWriteTimeUtc; + if (time1 != time2) + { + File.Copy(bundledDataPath, dataPath, true); + } + } + } + } } } diff --git a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs index a7744ffac..752c85933 100644 --- a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs +++ b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs @@ -6,6 +6,9 @@ using System.Linq; namespace Flow.Launcher.Plugin.SharedCommands { + /// + /// Contains methods to open a search in a new browser window or tab. + /// public static class SearchWeb { private static string GetDefaultBrowserPath() @@ -106,4 +109,4 @@ namespace Flow.Launcher.Plugin.SharedCommands } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs index 49f78b458..288222d4f 100644 --- a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs +++ b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs @@ -2,21 +2,32 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Runtime.InteropServices; -using System.Text; using System.Threading; +using Windows.Win32; +using Windows.Win32.Foundation; namespace Flow.Launcher.Plugin.SharedCommands { + /// + /// Contains methods for running shell commands + /// public static class ShellCommand { + /// + /// Delegate for EnumThreadWindows + /// + /// + /// + /// public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam); - [DllImport("user32.dll")] static extern bool EnumThreadWindows(uint threadId, EnumThreadDelegate lpfn, IntPtr lParam); - [DllImport("user32.dll")] static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount); - [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hwnd); private static bool containsSecurityWindow; + /// + /// Runs a windows command using the provided ProcessStartInfo + /// + /// + /// public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo) { processStartInfo.Verb = "RunAsUser"; @@ -28,6 +39,7 @@ namespace Flow.Launcher.Plugin.SharedCommands CheckSecurityWindow(); Thread.Sleep(25); } + while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open { containsSecurityWindow = false; @@ -42,24 +54,42 @@ namespace Flow.Launcher.Plugin.SharedCommands { ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads; for (int i = 0; i < ptc.Count; i++) - EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero); + PInvoke.EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero); } - private static bool CheckSecurityThread(IntPtr hwnd, IntPtr lParam) + private static BOOL CheckSecurityThread(HWND hwnd, LPARAM lParam) { if (GetWindowTitle(hwnd) == "Windows Security") containsSecurityWindow = true; return true; } - private static string GetWindowTitle(IntPtr hwnd) + private static unsafe string GetWindowTitle(HWND hwnd) { - StringBuilder sb = new StringBuilder(GetWindowTextLength(hwnd) + 1); - GetWindowText(hwnd, sb, sb.Capacity); - return sb.ToString(); + var capacity = PInvoke.GetWindowTextLength(hwnd) + 1; + int length; + Span buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity]; + fixed (char* pBuffer = buffer) + { + // If the window has no title bar or text, if the title bar is empty, + // or if the window or control handle is invalid, the return value is zero. + length = PInvoke.GetWindowText(hwnd, pBuffer, capacity); + } + + return buffer[..length].ToString(); } - public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", string arguments = "", string verb = "", bool createNoWindow = false) + /// + /// Runs a windows command using the provided ProcessStartInfo + /// + /// + /// + /// + /// + /// + /// + public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", + string arguments = "", string verb = "", bool createNoWindow = false) { var info = new ProcessStartInfo { diff --git a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs index 5144eb61d..36677d4bb 100644 --- a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs +++ b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs @@ -2,14 +2,29 @@ namespace Flow.Launcher.Plugin.SharedModels { + /// + /// Represents the result of a match operation. + /// public class MatchResult { + /// + /// Initializes a new instance of the class. + /// + /// + /// public MatchResult(bool success, SearchPrecisionScore searchPrecision) { Success = success; SearchPrecision = searchPrecision; } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore) { Success = success; @@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels RawScore = rawScore; } + /// + /// Whether the match operation was successful. + /// public bool Success { get; set; } /// @@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels /// private int _rawScore; + /// + /// The raw calculated search score without any search precision filtering applied. + /// public int RawScore { get { return _rawScore; } @@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels /// public List MatchData { get; set; } + /// + /// The search precision score used to filter the search results. + /// public SearchPrecisionScore SearchPrecision { get; set; } + /// + /// Determines if the search precision score is met. + /// + /// public bool IsSearchPrecisionScoreMet() { return IsSearchPrecisionScoreMet(_rawScore); @@ -63,10 +91,24 @@ namespace Flow.Launcher.Plugin.SharedModels } } + /// + /// Represents the search precision score used to filter search results. + /// public enum SearchPrecisionScore { + /// + /// The highest search precision score. + /// Regular = 50, + + /// + /// The medium search precision score. + /// Low = 20, + + /// + /// The lowest search precision score. + /// None = 0 } } diff --git a/Flow.Launcher.Plugin/UserPlugin.cs b/Flow.Launcher.Plugin/UserPlugin.cs new file mode 100644 index 000000000..74a16b83d --- /dev/null +++ b/Flow.Launcher.Plugin/UserPlugin.cs @@ -0,0 +1,80 @@ +using System; + +namespace Flow.Launcher.Plugin +{ + /// + /// User Plugin Model for Flow Launcher + /// + public record UserPlugin + { + /// + /// Unique identifier of the plugin + /// + public string ID { get; set; } + + /// + /// Name of the plugin + /// + public string Name { get; set; } + + /// + /// Description of the plugin + /// + public string Description { get; set; } + + /// + /// Author of the plugin + /// + public string Author { get; set; } + + /// + /// Version of the plugin + /// + public string Version { get; set; } + + /// + /// Allow language of the plugin + /// + public string Language { get; set; } + + /// + /// Website of the plugin + /// + public string Website { get; set; } + + /// + /// URL to download the plugin + /// + public string UrlDownload { get; set; } + + /// + /// URL to the source code of the plugin + /// + public string UrlSourceCode { get; set; } + + /// + /// Local path where the plugin is installed + /// + public string LocalInstallPath { get; set; } + + /// + /// Icon path of the plugin + /// + public string IcoPath { get; set; } + + /// + /// The date when the plugin was last updated + /// + public DateTime? LatestReleaseDate { get; set; } + + /// + /// The date when the plugin was added to the local system + /// + public DateTime? DateAdded { get; set; } + + /// + /// Indicates whether the plugin is installed from a local path + /// + public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath); + } +} diff --git a/Flow.Launcher.Test/FilesFoldersTest.cs b/Flow.Launcher.Test/FilesFoldersTest.cs index 3dead9918..2621fc2da 100644 --- a/Flow.Launcher.Test/FilesFoldersTest.cs +++ b/Flow.Launcher.Test/FilesFoldersTest.cs @@ -1,5 +1,6 @@ using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; +using NUnit.Framework.Legacy; namespace Flow.Launcher.Test { @@ -35,7 +36,7 @@ namespace Flow.Launcher.Test [TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)] public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) { - Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); + ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); } // Equality @@ -47,7 +48,7 @@ namespace Flow.Launcher.Test [TestCase(@"c:\foo", @"c:\foo\", true)] public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) { - Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult)); + ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult)); } } } diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index fd967de4a..0241a374e 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -49,12 +49,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index d7f143218..090719642 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; @@ -21,6 +22,8 @@ namespace Flow.Launcher.Test private const string MicrosoftSqlServerManagementStudio = "Microsoft SQL Server Management Studio"; private const string VisualStudioCode = "Visual Studio Code"; + private readonly IAlphabet alphabet = null; + public List GetSearchStrings() => new List { @@ -34,7 +37,7 @@ namespace Flow.Launcher.Test OneOneOneOne }; - public List GetPrecisionScores() + public static List GetPrecisionScores() { var listToReturn = new List(); @@ -59,7 +62,7 @@ namespace Flow.Launcher.Test }; var results = new List(); - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); foreach (var str in sources) { results.Add(new Result @@ -71,20 +74,20 @@ namespace Flow.Launcher.Test results = results.Where(x => x.Score > 0).OrderByDescending(x => x.Score).ToList(); - Assert.IsTrue(results.Count == 3); - Assert.IsTrue(results[0].Title == "Inste"); - Assert.IsTrue(results[1].Title == "Install Package"); - Assert.IsTrue(results[2].Title == "file open in browser-test"); + ClassicAssert.IsTrue(results.Count == 3); + ClassicAssert.IsTrue(results[0].Title == "Inste"); + ClassicAssert.IsTrue(results[1].Title == "Install Package"); + ClassicAssert.IsTrue(results[2].Title == "file open in browser-test"); } [TestCase("Chrome")] public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString) { var compareString = "Can have rum only in my glass"; - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore; - Assert.True(scoreResult == 0); + ClassicAssert.True(scoreResult == 0); } [TestCase("chr")] @@ -97,7 +100,7 @@ namespace Flow.Launcher.Test string searchTerm) { var results = new List(); - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); foreach (var str in GetSearchStrings()) { results.Add(new Result @@ -125,7 +128,7 @@ namespace Flow.Launcher.Test Debug.WriteLine("###############################################"); Debug.WriteLine(""); - Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); + ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); } } @@ -147,11 +150,11 @@ namespace Flow.Launcher.Test string queryString, string compareString, int expectedScore) { // When, Given - var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; + var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore; // Should - Assert.AreEqual(expectedScore, rawScore, + ClassicAssert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}"); } @@ -181,7 +184,7 @@ namespace Flow.Launcher.Test bool expectedPrecisionResult) { // When - var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore}; + var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore}; // Given var matchResult = matcher.FuzzyMatch(queryString, compareString); @@ -190,12 +193,12 @@ namespace Flow.Launcher.Test Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); Debug.WriteLine( - $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); + $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); // Should - Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), $"Query: {queryString}{Environment.NewLine} " + $"Compare: {compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + @@ -232,7 +235,7 @@ namespace Flow.Launcher.Test bool expectedPrecisionResult) { // When - var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore}; + var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore}; // Given var matchResult = matcher.FuzzyMatch(queryString, compareString); @@ -241,12 +244,12 @@ namespace Flow.Launcher.Test Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); Debug.WriteLine( - $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); + $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); // Should - Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), $"Query:{queryString}{Environment.NewLine} " + $"Compare:{compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + @@ -260,7 +263,7 @@ namespace Flow.Launcher.Test string queryString, string compareString1, string compareString2) { // When - var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; + var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; // Given var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); @@ -277,7 +280,7 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); // Should - Assert.True(compareString1Result.Score > compareString2Result.Score, + ClassicAssert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -293,7 +296,7 @@ namespace Flow.Launcher.Test string queryString, string compareString1, string compareString2) { // When - var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular }; + var matcher = new StringMatcher(alphabet) { UserSettingSearchPrecision = SearchPrecisionScore.Regular }; // Given var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); @@ -310,7 +313,7 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); // Should - Assert.True(compareString1Result.Score > compareString2Result.Score, + ClassicAssert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -323,7 +326,7 @@ namespace Flow.Launcher.Test string secondName, string secondDescription, string secondExecutableName) { // Act - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore; var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore; var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore; @@ -336,7 +339,7 @@ namespace Flow.Launcher.Test var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max(); // Assert - Assert.IsTrue(firstScore > secondScore, + ClassicAssert.IsTrue(firstScore > secondScore, $"Query: \"{queryString}\"{Environment.NewLine} " + $"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -358,9 +361,9 @@ namespace Flow.Launcher.Test public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString, int desiredScore) { - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); var score = matcher.FuzzyMatch(queryString, compareString).Score; - Assert.IsTrue(score == desiredScore, + ClassicAssert.IsTrue(score == desiredScore, $@"Query: ""{queryString}"" CompareString: ""{compareString}"" Score: {score} diff --git a/Flow.Launcher.Test/HttpTest.cs b/Flow.Launcher.Test/HttpTest.cs index e72ad7a67..4f135978a 100644 --- a/Flow.Launcher.Test/HttpTest.cs +++ b/Flow.Launcher.Test/HttpTest.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using NUnit.Framework; +using NUnit.Framework.Legacy; using System; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.Http; @@ -16,16 +17,16 @@ namespace Flow.Launcher.Test proxy.Enabled = true; proxy.Server = "127.0.0.1"; - Assert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}")); - Assert.IsNull(Http.WebProxy.Credentials); + ClassicAssert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}")); + ClassicAssert.IsNull(Http.WebProxy.Credentials); proxy.UserName = "test"; - Assert.NotNull(Http.WebProxy.Credentials); - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName); - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, ""); + ClassicAssert.NotNull(Http.WebProxy.Credentials); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, ""); proxy.Password = "test password"; - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password); } } } diff --git a/Flow.Launcher.Test/PluginLoadTest.cs b/Flow.Launcher.Test/PluginLoadTest.cs index d6ba48f19..2cc05f95a 100644 --- a/Flow.Launcher.Test/PluginLoadTest.cs +++ b/Flow.Launcher.Test/PluginLoadTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; using System.Collections.Generic; @@ -15,37 +16,37 @@ namespace Flow.Launcher.Test // Given var duplicateList = new List { - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.1" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.2" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "ABC0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "ABC0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" @@ -56,11 +57,11 @@ namespace Flow.Launcher.Test (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); // Then - Assert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); - Assert.True(unique.Count() == 1); + ClassicAssert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); + ClassicAssert.True(unique.Count == 1); - Assert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); - Assert.True(duplicates.Count() == 6); + ClassicAssert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); + ClassicAssert.True(duplicates.Count == 6); } [Test] @@ -69,12 +70,12 @@ namespace Flow.Launcher.Test // Given var duplicateList = new List { - new PluginMetadata + new() { ID = "CEA0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" @@ -85,8 +86,8 @@ namespace Flow.Launcher.Test (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); // Then - Assert.True(unique.Count() == 0); - Assert.True(duplicates.Count() == 2); + ClassicAssert.True(unique.Count == 0); + ClassicAssert.True(duplicates.Count == 2); } } } diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 80cb74729..420da266d 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -5,12 +5,10 @@ using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo; using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; +using NUnit.Framework.Legacy; using System; -using System.Collections.Generic; using System.IO; using System.Runtime.Versioning; -using System.Threading; -using System.Threading.Tasks; using static Flow.Launcher.Plugin.Explorer.Search.SearchManager; namespace Flow.Launcher.Test.Plugins @@ -22,28 +20,6 @@ namespace Flow.Launcher.Test.Plugins [TestFixture] public class ExplorerTest { -#pragma warning disable CS1998 // async method with no await (more readable to leave it async to match the tested signature) - private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken) - { - return new List(); - } -#pragma warning restore CS1998 - - private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token) - { - return new List - { - new Result - { - Title = "Result 1" - }, - new Result - { - Title = "Result 2" - } - }; - } - private bool PreviousLocationExistsReturnsTrue(string dummyString) => true; private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false; @@ -57,7 +33,7 @@ namespace Flow.Launcher.Test.Plugins var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath); // Then - Assert.IsTrue(result == expectedString, + ClassicAssert.IsTrue(result == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual: {result}{Environment.NewLine}"); } @@ -74,7 +50,7 @@ namespace Flow.Launcher.Test.Plugins var queryString = queryConstructor.Directory(folderPath); // Then - Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), + ClassicAssert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), $"Expected string: {expectedString}{Environment.NewLine} " + $"Actual string was: {queryString}{Environment.NewLine}"); } @@ -94,7 +70,7 @@ namespace Flow.Launcher.Test.Plugins var queryString = queryConstructor.Directory(folderPath, userSearchString); // Then - Assert.AreEqual(expectedString, queryString); + ClassicAssert.AreEqual(expectedString, queryString); } [SupportedOSPlatform("windows7.0")] @@ -105,7 +81,7 @@ namespace Flow.Launcher.Test.Plugins const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch; // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -128,7 +104,7 @@ namespace Flow.Launcher.Test.Plugins var resultString = queryConstructor.FilesAndFolders(userSearchString); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } @@ -138,13 +114,13 @@ namespace Flow.Launcher.Test.Plugins string querySearchString, string expectedString) { // Given - var queryConstructor = new QueryConstructor(new Settings()); + _ = new QueryConstructor(new Settings()); //When var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString); // Then - Assert.IsTrue(resultString == expectedString, + ClassicAssert.IsTrue(resultString == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } @@ -162,12 +138,12 @@ namespace Flow.Launcher.Test.Plugins var resultString = queryConstructor.FileContent(userSearchString); // Then - Assert.IsTrue(resultString == expectedString, + ClassicAssert.IsTrue(resultString == expectedString, $"Expected query string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } - public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue() + public static void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue() { // Given var query = new Query @@ -181,7 +157,7 @@ namespace Flow.Launcher.Test.Plugins var result = searchManager.IsFileContentSearch(query.ActionKeyword); // Then - Assert.IsTrue(result, + ClassicAssert.IsTrue(result, $"Expected True for file content search. {Environment.NewLine} " + $"Actual result was: {result}{Environment.NewLine}"); } @@ -206,7 +182,7 @@ namespace Flow.Launcher.Test.Plugins var result = FilesFolders.IsLocationPathString(querySearchString); //Then - Assert.IsTrue(result == expectedResult, + ClassicAssert.IsTrue(result == expectedResult, $"Expected query search string check result is: {expectedResult} {Environment.NewLine} " + $"Actual check result is {result} {Environment.NewLine}"); @@ -233,7 +209,7 @@ namespace Flow.Launcher.Test.Plugins var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path); //Then - Assert.IsTrue(previousDirectoryPath == expectedString, + ClassicAssert.IsTrue(previousDirectoryPath == expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {previousDirectoryPath} {Environment.NewLine}"); } @@ -246,7 +222,7 @@ namespace Flow.Launcher.Test.Plugins var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); //Then - Assert.IsTrue(returnedPath == expectedString, + ClassicAssert.IsTrue(returnedPath == expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {returnedPath} {Environment.NewLine}"); } @@ -260,7 +236,7 @@ namespace Flow.Launcher.Test.Plugins var resultString = QueryConstructor.RecursiveDirectoryConstraint(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -274,7 +250,7 @@ namespace Flow.Launcher.Test.Plugins var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")] @@ -305,7 +281,7 @@ namespace Flow.Launcher.Test.Plugins var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")] @@ -334,7 +310,7 @@ namespace Flow.Launcher.Test.Plugins var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")] @@ -366,7 +342,7 @@ namespace Flow.Launcher.Test.Plugins var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")] @@ -398,7 +374,7 @@ namespace Flow.Launcher.Test.Plugins var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase(@"c:\foo", @"c:\foo", true)] @@ -420,7 +396,7 @@ namespace Flow.Launcher.Test.Plugins }; // When, Then - Assert.AreEqual(expectedResult, comparator.Equals(result1, result2)); + ClassicAssert.AreEqual(expectedResult, comparator.Equals(result1, result2)); } [TestCase(@"c:\foo\", @"c:\foo\")] @@ -444,7 +420,7 @@ namespace Flow.Launcher.Test.Plugins var hash2 = comparator.GetHashCode(result2); // When, Then - Assert.IsTrue(hash1 == hash2); + ClassicAssert.IsTrue(hash1 == hash2); } [TestCase(@"%appdata%", true)] @@ -461,7 +437,7 @@ namespace Flow.Launcher.Test.Plugins var result = EnvironmentVariables.HasEnvironmentVar(path); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } } } diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index 3d05e5679..497f874e7 100644 --- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -1,12 +1,11 @@ -using NUnit.Framework; +using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; using System.Threading.Tasks; using System.IO; using System.Threading; using System.Text; -using System.Text.Json; -using System.Linq; using System.Collections.Generic; namespace Flow.Launcher.Test.Plugins @@ -40,13 +39,13 @@ namespace Flow.Launcher.Test.Plugins Search = resultText }, default); - Assert.IsNotNull(results); + ClassicAssert.IsNotNull(results); foreach (var result in results) { - Assert.IsNotNull(result); - Assert.IsNotNull(result.AsyncAction); - Assert.IsNotNull(result.Title); + ClassicAssert.IsNotNull(result); + ClassicAssert.IsNotNull(result.AsyncAction); + ClassicAssert.IsNotNull(result.Title); } } @@ -56,35 +55,11 @@ namespace Flow.Launcher.Test.Plugins new JsonRPCQueryResponseModel(0, new List()), new JsonRPCQueryResponseModel(0, new List { - new JsonRPCResult + new() { Title = "Test1", SubTitle = "Test2" } }) }; - - [TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))] - public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference) - { - var camelText = JsonSerializer.Serialize(reference, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - - var pascalText = JsonSerializer.Serialize(reference); - - var results1 = await QueryAsync(new Query { Search = camelText }, default); - var results2 = await QueryAsync(new Query { Search = pascalText }, default); - - Assert.IsNotNull(results1); - Assert.IsNotNull(results2); - - foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result)) - { - Assert.AreEqual(result1, result2); - Assert.AreEqual(result1, referenceResult); - - Assert.IsNotNull(result1); - Assert.IsNotNull(result1.AsyncAction); - } - } - } } diff --git a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs index 7ccac5bd5..0dd1fe489 100644 --- a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs @@ -1,7 +1,8 @@ using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Plugin.Url; -namespace Flow.Launcher.Test +namespace Flow.Launcher.Test.Plugins { [TestFixture] public class UrlPluginTest @@ -10,23 +11,23 @@ namespace Flow.Launcher.Test public void URLMatchTest() { var plugin = new Main(); - Assert.IsTrue(plugin.IsURL("http://www.google.com")); - Assert.IsTrue(plugin.IsURL("https://www.google.com")); - Assert.IsTrue(plugin.IsURL("http://google.com")); - Assert.IsTrue(plugin.IsURL("www.google.com")); - Assert.IsTrue(plugin.IsURL("google.com")); - Assert.IsTrue(plugin.IsURL("http://localhost")); - Assert.IsTrue(plugin.IsURL("https://localhost")); - Assert.IsTrue(plugin.IsURL("http://localhost:80")); - Assert.IsTrue(plugin.IsURL("https://localhost:80")); - Assert.IsTrue(plugin.IsURL("http://110.10.10.10")); - Assert.IsTrue(plugin.IsURL("110.10.10.10")); - Assert.IsTrue(plugin.IsURL("ftp://110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("http://www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("https://www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("http://google.com")); + ClassicAssert.IsTrue(plugin.IsURL("www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("google.com")); + ClassicAssert.IsTrue(plugin.IsURL("http://localhost")); + ClassicAssert.IsTrue(plugin.IsURL("https://localhost")); + ClassicAssert.IsTrue(plugin.IsURL("http://localhost:80")); + ClassicAssert.IsTrue(plugin.IsURL("https://localhost:80")); + ClassicAssert.IsTrue(plugin.IsURL("http://110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("ftp://110.10.10.10")); - Assert.IsFalse(plugin.IsURL("wwww")); - Assert.IsFalse(plugin.IsURL("wwww.c")); - Assert.IsFalse(plugin.IsURL("wwww.c")); + ClassicAssert.IsFalse(plugin.IsURL("wwww")); + ClassicAssert.IsFalse(plugin.IsURL("wwww.c")); + ClassicAssert.IsFalse(plugin.IsURL("wwww.c")); } } } diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index aa0c8da12..c8ac17748 100644 --- a/Flow.Launcher.Test/QueryBuilderTest.cs +++ b/Flow.Launcher.Test/QueryBuilderTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; @@ -17,17 +18,17 @@ namespace Flow.Launcher.Test Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); - Assert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); - Assert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); - Assert.AreEqual(">", q.ActionKeyword); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); + ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); + ClassicAssert.AreEqual(">", q.ActionKeyword); - Assert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match."); + ClassicAssert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match."); - Assert.AreEqual("ping", q.FirstSearch); - Assert.AreEqual("google.com", q.SecondSearch); - Assert.AreEqual("-n", q.ThirdSearch); + ClassicAssert.AreEqual("ping", q.FirstSearch); + ClassicAssert.AreEqual("google.com", q.SecondSearch); + ClassicAssert.AreEqual("-n", q.ThirdSearch); - Assert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); + ClassicAssert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); } [Test] @@ -40,11 +41,11 @@ namespace Flow.Launcher.Test Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); - Assert.AreEqual("> ping google.com -n 20 -6", q.Search); - Assert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); - Assert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); - Assert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); - Assert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search); + ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); + ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); + ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); + ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); } [Test] @@ -52,13 +53,13 @@ namespace Flow.Launcher.Test { Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary()); - Assert.AreEqual("file.txt file2 file3", q.Search); - Assert.AreEqual("", q.ActionKeyword); + ClassicAssert.AreEqual("file.txt file2 file3", q.Search); + ClassicAssert.AreEqual("", q.ActionKeyword); - Assert.AreEqual("file.txt", q.FirstSearch); - Assert.AreEqual("file2", q.SecondSearch); - Assert.AreEqual("file3", q.ThirdSearch); - Assert.AreEqual("file2 file3", q.SecondToEndSearch); + ClassicAssert.AreEqual("file.txt", q.FirstSearch); + ClassicAssert.AreEqual("file2", q.SecondSearch); + ClassicAssert.AreEqual("file3", q.ThirdSearch); + ClassicAssert.AreEqual("file2 file3", q.SecondToEndSearch); } } } diff --git a/Flow.Launcher/ActionKeywords.xaml.cs b/Flow.Launcher/ActionKeywords.xaml.cs index 371b2dd07..d403d4847 100644 --- a/Flow.Launcher/ActionKeywords.xaml.cs +++ b/Flow.Launcher/ActionKeywords.xaml.cs @@ -2,6 +2,9 @@ using Flow.Launcher.Core.Resource; using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; +using Flow.Launcher.Core; +using System.Linq; +using System.Collections.Generic; namespace Flow.Launcher { @@ -31,20 +34,59 @@ namespace Flow.Launcher private void btnDone_OnClick(object sender, RoutedEventArgs _) { - var oldActionKeyword = plugin.Metadata.ActionKeywords[0]; - var newActionKeyword = tbAction.Text.Trim(); - newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*"; - - if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword)) + var oldActionKeywords = plugin.Metadata.ActionKeywords; + + var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList(); + newActionKeywords.RemoveAll(string.IsNullOrEmpty); + newActionKeywords = newActionKeywords.Distinct().ToList(); + + newActionKeywords = newActionKeywords.Count > 0 ? newActionKeywords : new() { Query.GlobalPluginWildcardSign }; + + var addedActionKeywords = newActionKeywords.Except(oldActionKeywords).ToList(); + var removedActionKeywords = oldActionKeywords.Except(newActionKeywords).ToList(); + if (!addedActionKeywords.Any(App.API.ActionKeywordAssigned)) { - pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword); - Close(); + if (oldActionKeywords.Count != newActionKeywords.Count) + { + ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords); + return; + } + + var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList(); + var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList(); + + if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords)) + { + // User just changes the sequence of action keywords + var msg = translater.GetTranslation("newActionKeywordsSameAsOld"); + MessageBoxEx.Show(msg); + } + else + { + ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords); + } } else { string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned"); - MessageBox.Show(msg); + App.API.ShowMsgBox(msg); } } + + private void ReplaceActionKeyword(string id, IReadOnlyList removedActionKeywords, IReadOnlyList addedActionKeywords) + { + foreach (var actionKeyword in removedActionKeywords) + { + App.API.RemoveActionKeyword(id, actionKeyword); + } + foreach (var actionKeyword in addedActionKeywords) + { + App.API.AddActionKeyword(id, actionKeyword); + } + + // Update action keywords text and close window + pluginViewModel.OnActionKeywordsChanged(); + Close(); + } } } diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index 76a613c3f..565bbe3c7 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -4,7 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="http://schemas.modernwpf.com/2019" ShutdownMode="OnMainWindowClose" - Startup="OnStartupAsync"> + Startup="OnStartup"> @@ -20,12 +20,17 @@ + + + + + - + diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 83870837a..81938612c 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core; using Flow.Launcher.Core.Configuration; using Flow.Launcher.Core.ExternalPlugins.Environments; @@ -14,91 +15,172 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher { public partial class App : IDisposable, ISingleInstanceApp { - public static PublicAPIInstance API { get; private set; } - private const string Unique = "Flow.Launcher_Unique_Application_Mutex"; + #region Public Properties + + public static IPublicAPI API { get; private set; } + + #endregion + + #region Private Fields + private static bool _disposed; - private Settings _settings; - private MainViewModel _mainVM; - private SettingWindowViewModel _settingsVM; - private readonly Updater _updater = new Updater(Flow.Launcher.Properties.Settings.Default.GithubRepo); - private readonly Portable _portable = new Portable(); - private readonly PinyinAlphabet _alphabet = new PinyinAlphabet(); - private StringMatcher _stringMatcher; + private MainWindow _mainWindow; + private readonly MainViewModel _mainVM; + private readonly Settings _settings; + + // To prevent two disposals running at the same time. + private static readonly object _disposingLock = new(); + + #endregion + + #region Constructor + + public App() + { + // Initialize settings + try + { + var storage = new FlowLauncherJsonStorage(); + _settings = storage.Load(); + _settings.SetStorage(storage); + _settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled(); + } + catch (Exception e) + { + ShowErrorMsgBoxAndFailFast("Cannot load setting storage, please check local data directory", e); + return; + } + + // Configure the dependency injection container + try + { + var host = Host.CreateDefaultBuilder() + .UseContentRoot(AppContext.BaseDirectory) + .ConfigureServices(services => services + .AddSingleton(_ => _settings) + .AddSingleton(sp => new Updater(sp.GetRequiredService(), Launcher.Properties.Settings.Default.GithubRepo)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + ).Build(); + Ioc.Default.ConfigureServices(host.Services); + } + catch (Exception e) + { + ShowErrorMsgBoxAndFailFast("Cannot configure dependency injection container, please open new issue in Flow.Launcher", e); + return; + } + + // Initialize the public API and Settings first + try + { + API = Ioc.Default.GetRequiredService(); + _settings.Initialize(); + _mainVM = Ioc.Default.GetRequiredService(); + } + catch (Exception e) + { + ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); + return; + } + + // Local function + static void ShowErrorMsgBoxAndFailFast(string message, Exception e) + { + // Firstly show users the message + MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + + // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. + Environment.FailFast(message, e); + } + } + + #endregion + + #region Main [STAThread] public static void Main() { - if (SingleInstance.InitializeAsFirstInstance(Unique)) + if (SingleInstance.InitializeAsFirstInstance()) { - using (var application = new App()) - { - application.InitializeComponent(); - application.Run(); - } + using var application = new App(); + application.InitializeComponent(); + application.Run(); } } - private async void OnStartupAsync(object sender, StartupEventArgs e) + #endregion + + #region App Events + +#pragma warning disable VSTHRD100 // Avoid async void methods + + private async void OnStartup(object sender, StartupEventArgs e) { await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => { - _portable.PreStartCleanUpAfterPortabilityUpdate(); + // Because new message box api uses MessageBoxEx window, + // if it is created and closed before main window is created, it will cause the application to exit. + // So set to OnExplicitShutdown to prevent the application from shutting down before main window is created + Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; - Log.Info( - "|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------"); + Log.SetLogLevel(_settings.LogLevel); + + Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate(); + + Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------"); Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}"); + RegisterAppDomainExceptions(); RegisterDispatcherUnhandledException(); var imageLoadertask = ImageLoader.InitializeAsync(); - _settingsVM = new SettingWindowViewModel(_updater, _portable); - _settings = _settingsVM.Settings; - _settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled(); - AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); - _alphabet.Initialize(_settings); - _stringMatcher = new StringMatcher(_alphabet); - StringMatcher.Instance = _stringMatcher; - _stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision; - PluginManager.LoadPlugins(_settings.PluginSettings); - _mainVM = new MainViewModel(_settings); - API = new PublicAPIInstance(_settingsVM, _mainVM); + // Register ResultsUpdated event after all plugins are loaded + Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); - Http.API = API; Http.Proxy = _settings.Proxy; - await PluginManager.InitializePluginsAsync(API); + await PluginManager.InitializePluginsAsync(); + + // Change language after all plugins are initialized because we need to update plugin title based on their api + // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future + await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + await imageLoadertask; - var window = new MainWindow(_settings, _mainVM); + _mainWindow = new MainWindow(); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); - Current.MainWindow = window; + Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; - // todo temp fix for instance code logic - // load plugin before change language, because plugin language also needs be changed - InternationalizationManager.Instance.Settings = _settings; - InternationalizationManager.Instance.ChangeLanguage(_settings.Language); - - HotKeyMapper.Initialize(_mainVM); - // main windows needs initialized before theme change because of blur settings - ThemeManager.Instance.Settings = _settings; - ThemeManager.Instance.ChangeTheme(_settings.Theme); + Ioc.Default.GetRequiredService().ChangeTheme(); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -108,11 +190,12 @@ namespace Flow.Launcher AutoUpdates(); API.SaveAppAllSettings(); - Log.Info( - "|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); + Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------"); }); } +#pragma warning restore VSTHRD100 // Avoid async void methods + private void AutoStartup() { // we try to enable auto-startup on first launch, or reenable if it was removed @@ -121,20 +204,25 @@ namespace Flow.Launcher { try { - Helper.AutoStartup.Enable(); + if (_settings.UseLogonTaskForStartup) + { + Helper.AutoStartup.EnableViaLogonTask(); + } + else + { + Helper.AutoStartup.EnableViaRegistry(); + } } catch (Exception e) { // 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; - Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), - e.Message); + API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message); } } } - //[Conditional("RELEASE")] private void AutoUpdates() { _ = Task.Run(async () => @@ -143,20 +231,38 @@ namespace Flow.Launcher { // check update every 5 hours var timer = new PeriodicTimer(TimeSpan.FromHours(5)); - await _updater.UpdateAppAsync(API); + await Ioc.Default.GetRequiredService().UpdateAppAsync(); while (await timer.WaitForNextTickAsync()) // check updates on startup - await _updater.UpdateAppAsync(API); + await Ioc.Default.GetRequiredService().UpdateAppAsync(); } }); } + #endregion + + #region Register Events + private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose(); - Current.Exit += (s, e) => Dispose(); - Current.SessionEnding += (s, e) => Dispose(); + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Process Exit"); + Dispose(); + }; + + Current.Exit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Application Exit"); + Dispose(); + }; + + Current.SessionEnding += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Session Ending"); + Dispose(); + }; } /// @@ -177,20 +283,68 @@ namespace Flow.Launcher AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } - public void Dispose() + #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) { - // if sessionending is called, exit proverbially be called when log off / shutdown - // but if sessionending is not called, exit won't be called when log off / shutdown - if (!_disposed) + // Prevent two disposes at the same time. + lock (_disposingLock) { - API.SaveAppAllSettings(); + if (!disposing) + { + return; + } + + if (_disposed) + { + return; + } + + // If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close() + // Accessing _mainWindow?.Dispatcher will cause the application stuck + // So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher + if (!_mainWindow.CanClose) + { + return; + } + _disposed = true; } + + Stopwatch.Normal("|App.Dispose|Dispose cost", () => + { + Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); + + if (disposing) + { + // Dispose needs to be called on the main Windows thread, + // since some resources owned by the thread need to be disposed. + _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); + _mainVM?.Dispose(); + } + + Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); + }); } + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + #region ISingleInstanceApp + public void OnSecondAppStarted() { - _mainVM.Show(); + Ioc.Default.GetRequiredService().Show(); } + + #endregion } } diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml index 068afda15..0171e6d79 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml @@ -32,14 +32,11 @@ - - - /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance( string uniqueName ) + public static bool InitializeAsFirstInstance() { // Build unique application Id and the IPC channel name. - string applicationIdentifier = uniqueName + Environment.UserName; + string applicationIdentifier = InstanceMutexName + Environment.UserName; - string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); // Create mutex based on unique application Id to check if this is the first instance of the application. - bool firstInstance; - singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance); + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); if (firstInstance) { - _ = CreateRemoteService(channelName); + _ = CreateRemoteServiceAsync(channelName); return true; } else { - _ = SignalFirstInstance(channelName); + _ = SignalFirstInstanceAsync(channelName); return false; } } @@ -257,84 +79,31 @@ namespace Flow.Launcher.Helper /// public static void Cleanup() { - singleInstanceMutex?.ReleaseMutex(); + SingleInstanceMutex?.ReleaseMutex(); } #endregion #region Private Methods - /// - /// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved. - /// - /// List of command line arg strings. - private static IList GetCommandLineArgs( string uniqueApplicationName ) - { - string[] args = null; - - try - { - // The application was not clickonce deployed, get args from standard API's - args = Environment.GetCommandLineArgs(); - } - catch (NotSupportedException) - { - - // The application was clickonce deployed - // Clickonce deployed apps cannot recieve traditional commandline arguments - // As a workaround commandline arguments can be written to a shared location before - // the app is launched and the app can obtain its commandline arguments from the - // shared location - string appFolderPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName); - - string cmdLinePath = Path.Combine(appFolderPath, "cmdline.txt"); - if (File.Exists(cmdLinePath)) - { - try - { - using (TextReader reader = new StreamReader(cmdLinePath, Encoding.Unicode)) - { - args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd()); - } - - File.Delete(cmdLinePath); - } - catch (IOException) - { - } - } - } - - if (args == null) - { - args = new string[] { }; - } - - return new List(args); - } - /// /// Creates a remote server pipe for communication. /// Once receives signal from client, will activate first instance. /// /// Application's IPC channel name. - private static async Task CreateRemoteService(string channelName) + private static async Task CreateRemoteServiceAsync(string channelName) { - using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In)) + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) { - while(true) - { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - if (Application.Current != null) - { - // Do an asynchronous call to ActivateFirstInstance function - Application.Current.Dispatcher.Invoke(ActivateFirstInstance); - } - // Disconect client - pipeServer.Disconnect(); - } + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); + + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); } } @@ -345,25 +114,13 @@ namespace Flow.Launcher.Helper /// /// Command line arguments for the second instance, passed to the first instance to take appropriate action. /// - private static async Task SignalFirstInstance(string channelName) + private static async Task SignalFirstInstanceAsync(string channelName) { // Create a client pipe connected to server - using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) - { - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } - } + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); - /// - /// Callback for activating first instance of the application. - /// - /// Callback argument. - /// Always null. - private static object ActivateFirstInstanceCallback(object o) - { - ActivateFirstInstance(); - return null; + // Connect to the available pipe + await pipeClient.ConnectAsync(0); } /// diff --git a/Flow.Launcher/Helper/SingletonWindowOpener.cs b/Flow.Launcher/Helper/SingletonWindowOpener.cs index b5c2d8b55..5282b61f9 100644 --- a/Flow.Launcher/Helper/SingletonWindowOpener.cs +++ b/Flow.Launcher/Helper/SingletonWindowOpener.cs @@ -10,16 +10,29 @@ public static class SingletonWindowOpener { var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.GetType() == typeof(T)) ?? (T)Activator.CreateInstance(typeof(T), args); - + // Fix UI bug // Add `window.WindowState = WindowState.Normal` // If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar // Not sure why this works tho // Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`) // https://stackoverflow.com/a/59719760/4230390 - window.WindowState = WindowState.Normal; - window.Show(); - + // Ensure the window is not minimized before showing it + if (window.WindowState == WindowState.Minimized) + { + window.WindowState = WindowState.Normal; + } + + // Ensure the window is visible + if (!window.IsVisible) + { + window.Show(); + } + else + { + window.Activate(); // Bring the window to the foreground if already open + } + window.Focus(); return (T)window; diff --git a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs index e08e227cc..77bebe2d3 100644 --- a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs +++ b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs @@ -1,47 +1,122 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Runtime.InteropServices; -using System.Text; +using System.Windows; using System.Windows.Media; +using System.Windows.Media.Imaging; +using Flow.Launcher.Infrastructure; using Microsoft.Win32; namespace Flow.Launcher.Helper; public static class WallpaperPathRetrieval { - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern Int32 SystemParametersInfo(UInt32 action, - Int32 uParam, StringBuilder vParam, UInt32 winIni); - private static readonly UInt32 SPI_GETDESKWALLPAPER = 0x73; - private static int MAX_PATH = 260; + private const int MaxCacheSize = 3; + private static readonly Dictionary<(string, DateTime), ImageBrush> WallpaperCache = new(); + private static readonly object CacheLock = new(); - public static string GetWallpaperPath() + public static Brush GetWallpaperBrush() { - var wallpaper = new StringBuilder(MAX_PATH); - SystemParametersInfo(SPI_GETDESKWALLPAPER, MAX_PATH, wallpaper, 0); + // Invoke the method on the UI thread + if (!Application.Current.Dispatcher.CheckAccess()) + { + return Application.Current.Dispatcher.Invoke(GetWallpaperBrush); + } - var str = wallpaper.ToString(); - if (string.IsNullOrEmpty(str)) - return null; + try + { + var wallpaperPath = Win32Helper.GetWallpaperPath(); + if (string.IsNullOrEmpty(wallpaperPath) || !File.Exists(wallpaperPath)) + { + App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Wallpaper path is invalid: {wallpaperPath}"); + var wallpaperColor = GetWallpaperColor(); + return new SolidColorBrush(wallpaperColor); + } - return str; + // Since the wallpaper file name can be the same (TranscodedWallpaper), + // we need to add the last modified date to differentiate them + var dateModified = File.GetLastWriteTime(wallpaperPath); + lock (CacheLock) + { + WallpaperCache.TryGetValue((wallpaperPath, dateModified), out var cachedWallpaper); + if (cachedWallpaper != null) + { + 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; + + if (originalWidth == 0 || originalHeight == 0) + { + App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}"); + return new SolidColorBrush(Colors.Transparent); + } + + // Calculate the scaling factor to fit the image within 800x600 while preserving aspect ratio + var widthRatio = 800.0 / originalWidth; + var heightRatio = 600.0 / originalHeight; + var scaleFactor = Math.Min(widthRatio, heightRatio); + var decodedPixelWidth = (int)(originalWidth * scaleFactor); + var decodedPixelHeight = (int)(originalHeight * scaleFactor); + + // Set DecodePixelWidth and DecodePixelHeight to resize the image while preserving aspect ratio + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(wallpaperPath); + bitmap.DecodePixelWidth = decodedPixelWidth; + bitmap.DecodePixelHeight = decodedPixelHeight; + bitmap.EndInit(); + bitmap.Freeze(); // Make the bitmap thread-safe + var wallpaperBrush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill }; + wallpaperBrush.Freeze(); // Make the brush thread-safe + + // Manage cache size + lock (CacheLock) + { + if (WallpaperCache.Count >= MaxCacheSize) + { + // Remove the oldest wallpaper from the cache + var oldestCache = WallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault(); + if (oldestCache != default) + { + WallpaperCache.Remove(oldestCache); + } + } + + WallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush); + return wallpaperBrush; + } + } + catch (Exception ex) + { + App.API.LogException(nameof(WallpaperPathRetrieval), "Error retrieving wallpaper", ex); + return new SolidColorBrush(Colors.Transparent); + } } - public static Color GetWallpaperColor() + private static Color GetWallpaperColor() { - RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", true); + RegistryKey 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(new[] { ' ' }, 3).Select(byte.Parse).ToList(); return Color.FromRgb(parts[0], parts[1], parts[2]); } - catch + catch (Exception ex) { + App.API.LogException(nameof(WallpaperPathRetrieval), "Error parsing wallpaper color", ex); } } + return Colors.Transparent; } } diff --git a/Flow.Launcher/Helper/WindowsInteropHelper.cs b/Flow.Launcher/Helper/WindowsInteropHelper.cs deleted file mode 100644 index 89fbec967..000000000 --- a/Flow.Launcher/Helper/WindowsInteropHelper.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Text; -using System.Windows; -using System.Windows.Forms; -using System.Windows.Interop; -using System.Windows.Media; -using Point = System.Windows.Point; - -namespace Flow.Launcher.Helper; - -public class WindowsInteropHelper -{ - private const int GWL_STYLE = -16; //WPF's Message code for Title Bar's Style - private const int WS_SYSMENU = 0x80000; //WPF's Message code for System Menu - private static IntPtr _hwnd_shell; - private static IntPtr _hwnd_desktop; - - //Accessors for shell and desktop handlers - //Will set the variables once and then will return them - private static IntPtr HWND_SHELL - { - get - { - return _hwnd_shell != IntPtr.Zero ? _hwnd_shell : _hwnd_shell = GetShellWindow(); - } - } - private static IntPtr HWND_DESKTOP - { - get - { - return _hwnd_desktop != IntPtr.Zero ? _hwnd_desktop : _hwnd_desktop = GetDesktopWindow(); - } - } - - [DllImport("user32.dll", SetLastError = true)] - internal static extern int GetWindowLong(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll")] - internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); - - [DllImport("user32.dll")] - internal static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - internal static extern IntPtr GetDesktopWindow(); - - [DllImport("user32.dll")] - internal static extern IntPtr GetShellWindow(); - - [DllImport("user32.dll", SetLastError = true)] - internal static extern int GetWindowRect(IntPtr hwnd, out RECT rc); - - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] - internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); - - [DllImport("user32.DLL")] - public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); - - - const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass"; - const string WINDOW_CLASS_WINTAB = "Flip3D"; - const string WINDOW_CLASS_PROGMAN = "Progman"; - const string WINDOW_CLASS_WORKERW = "WorkerW"; - - public static bool IsWindowFullscreen() - { - //get current active window - IntPtr hWnd = GetForegroundWindow(); - - if (hWnd.Equals(IntPtr.Zero)) - { - return false; - } - - //if current active window is desktop or shell, exit early - if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)) - { - return false; - } - - StringBuilder sb = new StringBuilder(256); - GetClassName(hWnd, sb, sb.Capacity); - string windowClass = sb.ToString(); - - //for Win+Tab (Flip3D) - if (windowClass == WINDOW_CLASS_WINTAB) - { - return false; - } - - RECT appBounds; - GetWindowRect(hWnd, out appBounds); - - //for console (ConsoleWindowClass), we have to check for negative dimensions - if (windowClass == WINDOW_CLASS_CONSOLE) - { - return appBounds.Top < 0 && appBounds.Bottom < 0; - } - - //for desktop (Progman or WorkerW, depends on the system), we have to check - if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW) - { - IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null); - hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView"); - if (!hWndDesktop.Equals(IntPtr.Zero)) - { - return false; - } - } - - Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds; - return (appBounds.Bottom - appBounds.Top) == screenBounds.Height && (appBounds.Right - appBounds.Left) == screenBounds.Width; - } - - /// - /// disable windows toolbar's control box - /// this will also disable system menu with Alt+Space hotkey - /// - public static void DisableControlBox(Window win) - { - var hwnd = new WindowInteropHelper(win).Handle; - SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU); - } - - /// - /// Transforms pixels to Device Independent Pixels used by WPF - /// - /// current window, required to get presentation source - /// horizontal position in pixels - /// vertical position in pixels - /// point containing device independent pixels - public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY) - { - Matrix matrix; - var source = PresentationSource.FromVisual(visual); - if (source is not null) - { - matrix = source.CompositionTarget.TransformFromDevice; - } - else - { - using var src = new HwndSource(new HwndSourceParameters()); - matrix = src.CompositionTarget.TransformFromDevice; - } - return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY)); - } - - - [StructLayout(LayoutKind.Sequential)] - public struct RECT - { - public int Left; - public int Top; - public int Right; - public int Bottom; - } -} diff --git a/Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs b/Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs index 2eec6c89b..9d2046972 100644 --- a/Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs +++ b/Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs @@ -6,6 +6,6 @@ internal static class WindowsMediaPlayerHelper internal static bool IsWindowsMediaPlayerInstalled() { using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MediaPlayer"); - return key.GetValue("Installation Directory") != null; + return key?.GetValue("Installation Directory") != null; } } diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index a42bde7c9..678280bba 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -4,25 +4,16 @@ using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher { public partial class HotkeyControl { - public IHotkeySettings HotkeySettings { - get { return (IHotkeySettings)GetValue(HotkeySettingsProperty); } - set { SetValue(HotkeySettingsProperty, value); } - } - - public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register( - nameof(HotkeySettings), - typeof(IHotkeySettings), - typeof(HotkeyControl), - new PropertyMetadata() - ); public string WindowTitle { get { return (string)GetValue(WindowTitleProperty); } set { SetValue(WindowTitleProperty, value); } @@ -71,8 +62,7 @@ namespace Flow.Launcher return; } - hotkeyControl.SetKeysToDisplay(new HotkeyModel(hotkeyControl.Hotkey)); - hotkeyControl.CurrentHotkey = new HotkeyModel(hotkeyControl.Hotkey); + hotkeyControl.RefreshHotkeyInterface(hotkeyControl.Hotkey); } @@ -90,17 +80,132 @@ namespace Flow.Launcher } - public static readonly DependencyProperty HotkeyProperty = DependencyProperty.Register( - nameof(Hotkey), - typeof(string), + public static readonly DependencyProperty TypeProperty = DependencyProperty.Register( + nameof(Type), + typeof(HotkeyType), typeof(HotkeyControl), - new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged) + new FrameworkPropertyMetadata(HotkeyType.None, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged) ); + public HotkeyType Type + { + get { return (HotkeyType)GetValue(TypeProperty); } + set { SetValue(TypeProperty, value); } + } + + public enum HotkeyType + { + None, + // Custom query hotkeys + CustomQueryHotkey, + // Settings hotkeys + Hotkey, + PreviewHotkey, + OpenContextMenuHotkey, + SettingWindowHotkey, + CycleHistoryUpHotkey, + CycleHistoryDownHotkey, + SelectPrevPageHotkey, + SelectNextPageHotkey, + AutoCompleteHotkey, + AutoCompleteHotkey2, + SelectPrevItemHotkey, + SelectPrevItemHotkey2, + SelectNextItemHotkey, + SelectNextItemHotkey2 + } + + // We can initialize settings in static field because it has been constructed in App constuctor + // and it will not construct settings instances twice + private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + + private string hotkey = string.Empty; public string Hotkey { - get { return (string)GetValue(HotkeyProperty); } - set { SetValue(HotkeyProperty, value); } + get + { + return Type switch + { + // Custom query hotkeys + HotkeyType.CustomQueryHotkey => hotkey, + // Settings hotkeys + HotkeyType.Hotkey => _settings.Hotkey, + HotkeyType.PreviewHotkey => _settings.PreviewHotkey, + HotkeyType.OpenContextMenuHotkey => _settings.OpenContextMenuHotkey, + HotkeyType.SettingWindowHotkey => _settings.SettingWindowHotkey, + HotkeyType.CycleHistoryUpHotkey => _settings.CycleHistoryUpHotkey, + HotkeyType.CycleHistoryDownHotkey => _settings.CycleHistoryDownHotkey, + HotkeyType.SelectPrevPageHotkey => _settings.SelectPrevPageHotkey, + HotkeyType.SelectNextPageHotkey => _settings.SelectNextPageHotkey, + HotkeyType.AutoCompleteHotkey => _settings.AutoCompleteHotkey, + HotkeyType.AutoCompleteHotkey2 => _settings.AutoCompleteHotkey2, + HotkeyType.SelectPrevItemHotkey => _settings.SelectPrevItemHotkey, + HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2, + HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey, + HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2, + _ => throw new System.NotImplementedException("Hotkey type not set") + }; + } + set + { + switch (Type) + { + // Custom query hotkeys + case HotkeyType.CustomQueryHotkey: + // We just need to store it in a local field + // because we will save to settings in other place + hotkey = value; + break; + // Settings hotkeys + case HotkeyType.Hotkey: + _settings.Hotkey = value; + break; + case HotkeyType.PreviewHotkey: + _settings.PreviewHotkey = value; + break; + case HotkeyType.OpenContextMenuHotkey: + _settings.OpenContextMenuHotkey = value; + break; + case HotkeyType.SettingWindowHotkey: + _settings.SettingWindowHotkey = value; + break; + case HotkeyType.CycleHistoryUpHotkey: + _settings.CycleHistoryUpHotkey = value; + break; + case HotkeyType.CycleHistoryDownHotkey: + _settings.CycleHistoryDownHotkey = value; + break; + case HotkeyType.SelectPrevPageHotkey: + _settings.SelectPrevPageHotkey = value; + break; + case HotkeyType.SelectNextPageHotkey: + _settings.SelectNextPageHotkey = value; + break; + case HotkeyType.AutoCompleteHotkey: + _settings.AutoCompleteHotkey = value; + break; + case HotkeyType.AutoCompleteHotkey2: + _settings.AutoCompleteHotkey2 = value; + break; + case HotkeyType.SelectPrevItemHotkey: + _settings.SelectPrevItemHotkey = value; + break; + case HotkeyType.SelectNextItemHotkey: + _settings.SelectNextItemHotkey = value; + break; + case HotkeyType.SelectPrevItemHotkey2: + _settings.SelectPrevItemHotkey2 = value; + break; + case HotkeyType.SelectNextItemHotkey2: + _settings.SelectNextItemHotkey2 = value; + break; + default: + throw new System.NotImplementedException("Hotkey type not set"); + } + + // After setting the hotkey, we need to refresh the interface + RefreshHotkeyInterface(Hotkey); + } } public HotkeyControl() @@ -108,7 +213,15 @@ namespace Flow.Launcher InitializeComponent(); HotkeyList.ItemsSource = KeysToDisplay; - SetKeysToDisplay(CurrentHotkey); + + // We should not call RefreshHotkeyInterface here because DependencyProperty is not set yet + // And it will be called in OnHotkeyChanged event or Hotkey setter later + } + + private void RefreshHotkeyInterface(string hotkey) + { + SetKeysToDisplay(new HotkeyModel(hotkey)); + CurrentHotkey = new HotkeyModel(hotkey); } private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => @@ -133,7 +246,7 @@ namespace Flow.Launcher HotKeyMapper.RemoveHotkey(Hotkey); } - var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, HotkeySettings, WindowTitle); + var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle); await dialog.ShowAsync(); switch (dialog.ResultType) { @@ -154,7 +267,16 @@ namespace Flow.Launcher { if (triggerValidate) { - bool hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture); + bool hotkeyAvailable = false; + // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157 + if (keyModel.ToString() == "LWin" || keyModel.ToString() == "RWin") + { + hotkeyAvailable = true; + } + else + { + hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture); + } if (!hotkeyAvailable) { diff --git a/Flow.Launcher/HotkeyControlDialog.xaml.cs b/Flow.Launcher/HotkeyControlDialog.xaml.cs index a7b99f670..2f8c5eb26 100644 --- a/Flow.Launcher/HotkeyControlDialog.xaml.cs +++ b/Flow.Launcher/HotkeyControlDialog.xaml.cs @@ -3,9 +3,12 @@ using System.Collections.ObjectModel; using System.Linq; using System.Windows; using System.Windows.Input; +using ChefKeys; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using ModernWpf.Controls; @@ -15,7 +18,7 @@ namespace Flow.Launcher; public partial class HotkeyControlDialog : ContentDialog { - private IHotkeySettings _hotkeySettings; + private static readonly IHotkeySettings _hotkeySettings = Ioc.Default.GetRequiredService(); private Action? _overwriteOtherHotkey; private string DefaultHotkey { get; } public string WindowTitle { get; } @@ -33,7 +36,9 @@ public partial class HotkeyControlDialog : ContentDialog public string ResultValue { get; private set; } = string.Empty; public static string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none"); - public HotkeyControlDialog(string hotkey, string defaultHotkey, IHotkeySettings hotkeySettings, string windowTitle = "") + private static bool isOpenFlowHotkey; + + public HotkeyControlDialog(string hotkey, string defaultHotkey, string windowTitle = "") { WindowTitle = windowTitle switch { @@ -42,10 +47,17 @@ public partial class HotkeyControlDialog : ContentDialog }; DefaultHotkey = defaultHotkey; CurrentHotkey = new HotkeyModel(hotkey); - _hotkeySettings = hotkeySettings; SetKeysToDisplay(CurrentHotkey); InitializeComponent(); + + // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157 + isOpenFlowHotkey = _hotkeySettings.RegisteredHotkeys + .Any(x => x.DescriptionResourceKey == "flowlauncherHotkey" + && x.Hotkey.ToString() == hotkey); + + ChefKeysManager.StartMenuEnableBlocking = true; + ChefKeysManager.Start(); } private void Reset(object sender, RoutedEventArgs routedEventArgs) @@ -61,12 +73,18 @@ public partial class HotkeyControlDialog : ContentDialog private void Cancel(object sender, RoutedEventArgs routedEventArgs) { + ChefKeysManager.StartMenuEnableBlocking = false; + ChefKeysManager.Stop(); + ResultType = EResultType.Cancel; Hide(); } private void Save(object sender, RoutedEventArgs routedEventArgs) { + ChefKeysManager.StartMenuEnableBlocking = false; + ChefKeysManager.Stop(); + if (KeysToDisplay.Count == 1 && KeysToDisplay[0] == EmptyHotkey) { ResultType = EResultType.Delete; @@ -85,6 +103,9 @@ public partial class HotkeyControlDialog : ContentDialog //when alt is pressed, the real key should be e.SystemKey Key key = e.Key == Key.System ? e.SystemKey : e.Key; + if (ChefKeysManager.StartMenuBlocked && key.ToString() == ChefKeysManager.StartMenuSimulatedKey) + return; + SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers(); var hotkeyModel = new HotkeyModel( @@ -168,8 +189,13 @@ public partial class HotkeyControlDialog : ContentDialog } } - private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => - hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); + private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) + { + if (isOpenFlowHotkey && (hotkey.ToString() == "LWin" || hotkey.ToString() == "RWin")) + return true; + + return hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); + } private void Overwrite(object sender, RoutedEventArgs e) { diff --git a/Flow.Launcher/Images/Error.png b/Flow.Launcher/Images/Error.png new file mode 100644 index 000000000..4bdcd80f9 Binary files /dev/null and b/Flow.Launcher/Images/Error.png differ diff --git a/Flow.Launcher/Images/Exclamation.png b/Flow.Launcher/Images/Exclamation.png new file mode 100644 index 000000000..a9d99362c Binary files /dev/null and b/Flow.Launcher/Images/Exclamation.png differ diff --git a/Flow.Launcher/Images/Information.png b/Flow.Launcher/Images/Information.png new file mode 100644 index 000000000..779e22dc1 Binary files /dev/null and b/Flow.Launcher/Images/Information.png differ diff --git a/Flow.Launcher/Images/Question.png b/Flow.Launcher/Images/Question.png new file mode 100644 index 000000000..6ca093f6d Binary files /dev/null and b/Flow.Launcher/Images/Question.png differ diff --git a/Flow.Launcher/Images/app_error.png b/Flow.Launcher/Images/app_error.png index 70599f504..a5664e263 100644 Binary files a/Flow.Launcher/Images/app_error.png and b/Flow.Launcher/Images/app_error.png differ diff --git a/Flow.Launcher/Images/image.png b/Flow.Launcher/Images/image.png index 9f26517e8..ea610046b 100644 Binary files a/Flow.Launcher/Images/image.png and b/Flow.Launcher/Images/image.png differ diff --git a/Flow.Launcher/Images/warning.png b/Flow.Launcher/Images/warning.png deleted file mode 100644 index 8d29625ee..000000000 Binary files a/Flow.Launcher/Images/warning.png and /dev/null differ diff --git a/Flow.Launcher/Languages/ar.xaml b/Flow.Launcher/Languages/ar.xaml index 5a361d478..a57179da6 100644 --- a/Flow.Launcher/Languages/ar.xaml +++ b/Flow.Launcher/Languages/ar.xaml @@ -1,420 +1,466 @@  + + + Flow اكتشف أنك قمت بتثبيت إضافات {0}، والتي ستتطلب تشغيل {1}. هل ترغب في تحميل {1}؟ + {2}{2} + انقر فوق لا إذا كان مثبتاً بالفعل، وسوف يطلب منك تحديد المجلد الذي يحتوي على {1} القابل للتنفيذ + + الرجاء اختيار الملف التنفيذي لـ {0} + تعذر تعيين مسار الملف التنفيذي لـ {0}، يرجى المحاولة من إعدادات Flow (قم بالتمرير إلى الأسفل). + فشل في تهيئة الإضافات + الإضافات: {0} - فشل في التحميل وسيتم تعطيلها، يرجى الاتصال بمطور الإضافة للحصول على المساعدة + - Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + فشل في تسجيل مفتاح التشغيل السريع "{0}". قد يكون المفتاح مستخدمًا من قبل برنامج آخر. قم بتغيير المفتاح، أو قم بإغلاق البرنامج الآخر. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher - Could not start {0} - Invalid Flow Launcher plugin file format - Set as topmost in this query - Cancel topmost in this query - Execute query: {0} - Last execution time: {0} - Open - Settings - About - Exit - Close - Copy - Cut - Paste - Undo - Select All - File - Folder - Text - Game Mode - Suspend the use of Hotkeys. - Position Reset - Reset search window position + تعذر بدء {0} + تنسيق ملف إضافة Flow Launcher غير صالح + تعيين كأعلى أولوية في هذا الاستعلام + إلغاء أعلى أولوية في هذا الاستعلام + تنفيذ الاستعلام: {0} + آخر وقت تنفيذ: {0} + فتح + الإعدادات + حول + خروج + إغلاق + نسخ + قص + لصق + تراجع + اختيار الكل + ملف + مجلد + نص + وضع اللعب + تعليق استخدام مفاتيح التشغيل السريع. + إعادة تعيين الموقع + إعادة تعيين موضع نافذة البحث - Settings - General - Portable Mode - Store all settings and user data in one folder (Useful when used with removable drives or cloud services). - Start Flow Launcher on system startup - Error setting launch on startup - Hide Flow Launcher when focus is lost - Do not show new version notifications - Search Window Position - Remember Last Position - Monitor with Mouse Cursor - Monitor with Focused Window - Primary Monitor - Custom Monitor - Search Window Position on Monitor - Center - Center Top - Left Top - Right Top - Custom Position - Language - Last Query Style - Show/Hide previous results when Flow Launcher is reactivated. - Preserve Last Query - Select last Query - Empty last Query - Maximum results shown - You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. - Ignore hotkeys in fullscreen mode - Disable Flow Launcher activation when a full screen application is active (Recommended for games). - Default File Manager - Select the file manager to use when opening the folder. - Default Web Browser - Setting for New Tab, New Window, Private Mode. - Python Path - Node.js Path - Please select the Node.js executable - Please select pythonw.exe - Always Start Typing in English Mode - Temporarily change your input method to English mode when activating Flow. - Auto Update - Select - Hide Flow Launcher on startup - Hide tray icon - When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. - Query Search Precision - Changes minimum match score required for results. - Search with Pinyin - Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese. - Always Preview - Always open preview panel when Flow activates. Press {0} to toggle preview. - Shadow effect is not allowed while current theme has blur effect enabled + الإعدادات + عام + وضع المحمول + تخزين جميع الإعدادات وبيانات المستخدم في مجلد واحد (مفيد عند استخدام الأقراص القابلة للإزالة أو الخدمات السحابية). + تشغيل Flow Launcher عند بدء تشغيل النظام + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler + خطأ في إعداد التشغيل عند بدء التشغيل + إخفاء Flow Launcher عند فقدان التركيز + عدم عرض إشعارات الإصدار الجديد + موضع نافذة البحث + تذكر آخر موقع + الشاشة مع مؤشر الماوس + الشاشة مع النافذة المركزة + الشاشة الرئيسية + شاشة مخصصة + موضع نافذة البحث على الشاشة + الوسط + وسط الأعلى + أعلى اليسار + أعلى اليمين + موضع مخصص + اللغة + نمط الاستعلام الأخير + عرض/إخفاء النتائج السابقة عند إعادة تنشيط Flow Launcher. + حفظ الاستعلام الأخير + اختيار الاستعلام الأخير + تفريغ الاستعلام الأخير + Preserve Last Action Keyword + Select Last Action Keyword + ارتفاع ثابت للنافذة + ارتفاع النافذة غير قابل للتعديل عن طريق السحب. + الحد الأقصى للنتائج المعروضة + يمكنك أيضًا تعديل هذا بسرعة باستخدام CTRL+Plus وCTRL+Minus. + تجاهل مفاتيح التشغيل السريع في وضع ملء الشاشة + تعطيل تنشيط Flow Launcher عند تشغيل تطبيق بملء الشاشة (موصى به للألعاب). + مدير الملفات الافتراضي + اختيار مدير الملفات لاستخدامه عند فتح المجلد. + متصفح الويب الافتراضي + إعدادات لعلامة تبويب جديدة، نافذة جديدة، وضع الخصوصية. + مسار Python + مسار Node.js + يرجى اختيار الملف التنفيذي لـ Node.js + يرجى اختيار pythonw.exe + دائمًا ابدأ الكتابة بوضع الإنجليزية + تغيير مؤقت لطريقة الإدخال إلى وضع الإنجليزية عند تنشيط Flow. + التحديث التلقائي + اختر + إخفاء Flow Launcher عند بدء التشغيل + يتم إخفاء نافذة بحث Flow Launcher في العلبة بعد بدء التشغيل. + إخفاء أيقونة العلبة + عندما تكون الأيقونة مخفية من العلبة، يمكن فتح قائمة الإعدادات عن طريق النقر بزر الماوس الأيمن على نافذة البحث. + دقة البحث في الاستعلام + تغيير الحد الأدنى لدرجة التطابق المطلوبة للنتائج. + لا شيء + منخفضة + عادية + البحث باستخدام Pinyin + السماح باستخدام Pinyin للبحث. Pinyin هو نظام التهجئة الروماني القياسي لترجمة الصينية. + دائمًا معاينة + فتح لوحة المعاينة دائمًا عند تنشيط Flow. اضغط على {0} للتبديل بين المعاينة وعدمها. + تأثير الظل غير مسموح به بينما يتم تمكين تأثير التمويه في السمة الحالية - Search Plugin - Ctrl+F to search plugins - No results found - Please try a different search. - Plugin - Plugins - Find more plugins - On - Off - Action keyword Setting - Action keyword - Current action keyword - New action keyword - Change Action Keywords - Current Priority - New Priority - Priority - Change Plugin Results Priority - Plugin Directory - by - Init time: - Query time: - Version - Website - Uninstall - + البحث عن إضافة + Ctrl+F للبحث عن الإضافات + لم يتم العثور على نتائج + يرجى تجربة بحث مختلف. + إضافة + الإضافات + البحث عن المزيد من الإضافات + تشغيل + إيقاف + إعداد كلمة الفعل + كلمة الفعل + كلمة الفعل الحالية + كلمة فعل جديدة + تغيير كلمات الفعل + الأولوية الحالية + أولوية جديدة + الأولوية + تغيير أولوية نتائج الإضافة + دليل الإضافات + بواسطة + وقت التهيئة: + وقت الاستعلام: + الإصدار + الموقع الإلكتروني + إلغاء التثبيت + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually - Plugin Store - New Release - Recently Updated - Plugins - Installed - Refresh - Install - Uninstall - Update - Plugin already installed - New Version - This plugin has been updated within the last 7 days - New Update is Available - - + متجر الإضافات + إصدار جديد + تحديثات حديثة + لا توجد إضافات + مثبتة + تحديث + تثبيت + إلغاء التثبيت + تحديث + الإضافة مثبتة بالفعل + إصدار جديد + تم تحديث هذه الإضافة في آخر 7 أيام + يتوفر تحديث جديد - Theme - Appearance - Theme Gallery - How to create a theme - Hi There - Explorer - Search for files, folders and file contents - WebSearch - Search the web with different search engine support - Program - Launch programs as admin or a different user - ProcessKiller - Terminate unwanted processes - Query Box Font - Result Item Font - Window Mode - Opacity - Theme {0} not exists, fallback to default theme - Fail to load theme {0}, fallback to default theme - Theme Folder - Open Theme Folder - Color Scheme - System Default - Light - Dark - Sound Effect - Play a small sound when the search window opens - Sound Effect Volume - Adjust the volume of the sound effect - Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume. - Animation - Use Animation in UI - Animation Speed - The speed of the UI animation - Slow - Medium - Fast - Custom - Clock - Date + السمة + المظهر + معرض السمات + كيفية إنشاء سمة + مرحباً + المستكشف + البحث عن الملفات والمجلدات ومحتويات الملفات + البحث في الويب + البحث في الويب باستخدام محركات بحث مختلفة + البرنامج + تشغيل البرامج كمسؤول أو كمستخدم مختلف + مُنهي العمليات + إنهاء العمليات غير المرغوب فيها + ارتفاع شريط البحث + ارتفاع العنصر + خط صندوق الاستعلام + خط عنوان النتيجة + خط العنوان الفرعي للنتيجة + إعادة التعيين + تخصيص + وضع النافذة + الشفافية + السمة {0} غير موجودة، العودة إلى السمة الافتراضية + فشل في تحميل السمة {0}، العودة إلى السمة الافتراضية + مجلد السمة + فتح مجلد السمة + نظام الألوان + الافتراضي للنظام + فاتح + داكن + تأثير الصوت + تشغيل صوت صغير عند فتح نافذة البحث + مستوى صوت تأثير الصوت + تعديل مستوى صوت تأثير الصوت + مشغل الوسائط غير متاح ويتطلبه Flow لضبط مستوى الصوت. يرجى التحقق من التثبيت إذا كنت بحاجة إلى تعديل مستوى الصوت. + الرسوم المتحركة + استخدام الرسوم المتحركة في واجهة المستخدم + سرعة الرسوم المتحركة + سرعة الرسوم المتحركة في واجهة المستخدم + بطيء + متوسط + سريع + مخصص + الساعة + التاريخ + هذه السمة تدعم الوضعين (فاتح/داكن). + هذه السمة تدعم الخلفية الضبابية الشفافة. - Hotkey - Hotkeys - Open Flow Launcher - Enter shortcut to show/hide Flow Launcher. - Toggle Preview - Enter shortcut to show/hide preview in search window. - Hotkey Presets - List of currently registered hotkeys - Open Result Modifier Key - Select a modifier key to open selected result via keyboard. - Show Hotkey - Show result selection hotkey with results. - Auto Complete - Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page - Cycle Previous Query - Cycle Next Query - Open Context Menu - Open Setting Window - Copy File Path - Toggle Game Mode - Toggle History - Open Containing Folder - Run As Admin - Refresh Search Results - Reload Plugins Data - Quick Adjust Window Width - Quick Adjust Window Height - Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. - Custom Query Hotkeys - Custom Query Shortcuts - Built-in Shortcuts - Query - Shortcut - Expansion - Description - Delete - Edit - Add - None - Please select an item - Are you sure you want to delete {0} plugin hotkey? - Are you sure you want to delete shortcut: {0} with expansion {1}? - Get text from clipboard. - Get path from active explorer. - Query window shadow effect - 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+]. - Use Segoe Fluent Icons - Use Segoe Fluent Icons for query results where supported - Press Key + مفتاح الاختصار + مفاتيح الاختصار + فتح Flow Launcher + أدخل اختصارًا لإظهار/إخفاء Flow Launcher. + تبديل المعاينة + أدخل اختصارًا لإظهار/إخفاء المعاينة في نافذة البحث. + إعدادات مفاتيح الاختصار + قائمة مفاتيح الاختصار المسجلة حاليًا + مفتاح تعديل فتح النتيجة + اختر مفتاح تعديل لفتح النتيجة المحددة عبر لوحة المفاتيح. + عرض مفتاح الاختصار + عرض مفتاح اختصار تحديد النتائج مع النتائج. + الإكمال التلقائي + تشغيل الإكمال التلقائي للعناصر المحددة. + تحديد العنصر التالي + تحديد العنصر السابق + الصفحة التالية + الصفحة السابقة + التنقل إلى الاستعلام السابق + التنقل إلى الاستعلام التالي + فتح قائمة السياق + فتح قائمة السياق الأصلية + فتح نافذة الإعدادات + نسخ مسار الملف + تبديل وضع اللعبة + تبديل السجل + فتح المجلد المحتوي + تشغيل كمسؤول + تحديث نتائج البحث + إعادة تحميل بيانات الإضافات + تعديل عرض النافذة بسرعة + تعديل ارتفاع النافذة بسرعة + استخدم عند الحاجة إلى إعادة تحميل الإضافات وتحديث بياناتها الحالية. + يمكنك إضافة مفتاح اختصار آخر لهذه الوظيفة. + مفاتيح اختصار الاستعلام المخصص + اختصارات الاستعلام المخصص + الاختصارات المدمجة + استعلام + اختصار + توسيع + الوصف + حذف + تعديل + إضافة + بلا + يرجى اختيار عنصر + هل أنت متأكد أنك تريد حذف مفتاح اختصار الإضافة {0} + هل أنت متأكد أنك تريد حذف الاختصار: {0} مع التوسيع {1}؟ + استخراج النص من الحافظة. + استخراج المسار من المستكشف النشط. + تأثير ظل نافذة الاستعلام + تأثير الظل يستهلك كمية كبيرة من وحدة المعالجة الرسومية (GPU). لا يوصى به إذا كان أداء الكمبيوتر محدودًا. + عرض النافذة + يمكنك أيضًا تعديل هذا بسرعة باستخدام Ctrl+[ و Ctrl+]. + استخدام أيقونات Segoe Fluent + استخدام أيقونات Segoe Fluent لنتائج الاستعلام حيثما كان مدعومًا + اضغط على المفتاح - HTTP Proxy - Enable HTTP Proxy - HTTP Server - Port - User Name - Password - Test Proxy - Save - Server field can't be empty - Port field can't be empty - Invalid port format - Proxy configuration saved successfully - Proxy configured correctly - Proxy connection failed + بروكسي HTTP + تمكين بروكسي HTTP + خادم HTTP + المنفذ + اسم المستخدم + كلمة المرور + اختبار البروكسي + حفظ + حقل الخادم لا يمكن أن يكون فارغًا + حقل المنفذ لا يمكن أن يكون فارغًا + تنسيق المنفذ غير صحيح + تم حفظ إعدادات البروكسي بنجاح + تم تكوين البروكسي بشكل صحيح + فشل الاتصال بالبروكسي - About - Website + حول + الموقع الإلكتروني GitHub - Docs - Version - Icons - You have activated Flow Launcher {0} times - Check for Updates - Become A Sponsor - New version {0} is available, would you like to restart Flow Launcher to use the update? - Check updates failed, please check your connection and proxy settings to api.github.com. + الوثائق + الإصدار + الأيقونات + لقد قمت بتفعيل Flow Launcher {0} مرات + التحقق من التحديثات + كن راعيا + الإصدار الجديد {0} متاح، هل ترغب في إعادة تشغيل Flow Launcher لاستخدام التحديث + فشل التحقق من التحديثات، يرجى التحقق من الاتصال وإعدادات البروكسي لـ api.github.com. - Download updates failed, please check your connection and proxy settings to github-cloud.s3.amazonaws.com, - or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually. + فشل تنزيل التحديثات، يرجى التحقق من الاتصال وإعدادات البروكسي لـ github-cloud.s3.amazonaws.com، + أو الذهاب إلى https://github.com/Flow-Launcher/Flow.Launcher/releases لتنزيل التحديثات يدويًا. - Release Notes - Usage Tips - DevTools - Setting Folder - Log Folder - Clear Logs - Are you sure you want to delete all logs? - Wizard + ملاحظات الإصدار + نصائح الاستخدام + أدوات المطورين + مجلد الإعدادات + مجلد السجلات + مسح السجلات + هل أنت متأكد أنك تريد حذف جميع السجلات؟ + معالج الترحيب + موقع بيانات المستخدم + يتم حفظ إعدادات المستخدم والإضافات المثبتة في مجلد بيانات المستخدم. قد يختلف هذا الموقع اعتمادًا على ما إذا كان في وضع النقل أم لا. + فتح المجلد + Log Level + Debug + Info - Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". - File Manager - Profile Name - File Manager Path - Arg For Folder - Arg For File + اختر مدير الملفات + يرجى تحديد موقع ملف مدير الملفات الذي تستخدمه وإضافة الحجج حسب الحاجة. يمثل "%d" مسار الدليل المفتوح، ويستخدمه الحقل "الحجة للمجلد" للأوامر التي تفتح أدلة محددة. يمثل "%f" مسار الملف المفتوح، ويستخدمه الحقل "الحجة للملف" للأوامر التي تفتح ملفات محددة. + على سبيل المثال، إذا كان مدير الملفات يستخدم أمرًا مثل "totalcmd.exe /A c:\windows" لفتح دليل c:\windows، فإن مسار مدير الملفات سيكون totalcmd.exe، وحجة المجلد ستكون /A "%d". قد تحتاج بعض مديري الملفات مثل QTTabBar فقط إلى توفير مسار، في هذه الحالة استخدم "%d" كمسار مدير الملفات واترك باقي الحقول فارغة. + مدير الملفات + اسم الملف الشخصي + مسار مدير الملفات + حجة للمجلد + حجة للملف - Default Web Browser - 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 + متصفح الويب الافتراضي + الإعداد الافتراضي يتبع إعداد متصفح النظام الافتراضي. إذا تم تحديده بشكل منفصل، يستخدم Flow ذلك المتصفح. + المتصفح + اسم المتصفح + مسار المتصفح + نافذة جديدة + تبويب جديد + الوضع الخاص - 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. إذا كنت تريد أن تكون النتائج أقل من نتائج أي إضافة أخرى، قدم رقمًا سالبًا. + يرجى تقديم عدد صحيح صالح للأولوية! - Old Action Keyword - New Action Keyword - Cancel - Done - Can't find specified plugin - New Action Keyword can't be empty - This new Action Keyword is already assigned to another plugin, please choose a different one - Success - Completed successfully - Enter the action keyword you like to use to start the plugin. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords. + كلمة المفتاح القديمة + كلمة المفتاح الجديدة + إلغاء + تم + لا يمكن العثور على الإضافة المحددة + كلمة المفتاح الجديدة لا يمكن أن تكون فارغة + تم تعيين كلمة المفتاح الجديدة هذه إلى إضافة أخرى، يرجى اختيار كلمة أخرى + نجاح + اكتمل بنجاح + أدخل كلمة المفتاح التي ترغب في استخدامها لبدء الإضافة. استخدم * إذا كنت لا ترغب في تحديد أي كلمة، وسيتم تشغيل الإضافة بدون كلمات مفتاحية. - Custom Query Hotkey - Press a custom hotkey to open Flow Launcher and input the specified query automatically. - Preview - Hotkey is unavailable, please select a new hotkey - Invalid plugin hotkey - Update - 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. + مفتاح اختصار الاستعلام المخصص + اضغط على مفتاح اختصار مخصص لفتح Flow Launcher وإدخال الاستعلام المحدد تلقائيًا. + معاينة + مفتاح الاختصار غير متاح، يرجى اختيار مفتاح اختصار جديد + مفتاح اختصار غير صالح للإضافة + تحديث + ربط مفتاح الاختصار + مفتاح الاختصار الحالي غير متاح. + تم حجز هذا المفتاح لـ "{0}" ولا يمكن استخدامه. يرجى اختيار مفتاح اختصار آخر. + يتم استخدام هذا المفتاح بالفعل من قبل "{0}". إذا ضغطت على "استبدال"، سيتم إزالته من "{0}". + اضغط على المفاتيح التي تريد استخدامها لهذه الوظيفة. - Custom Query Shortcut - Enter a shortcut that automatically expands to the specified query. - A shortcut is expanded when it exactly matches the query. + اختصار الاستعلام المخصص + أدخل اختصارًا يتوسع تلقائيًا إلى الاستعلام المحدد. + يتوسع الاختصار عندما يتطابق تمامًا مع الاستعلام. -If you add an '@' prefix while inputting a shortcut, it matches any position in the query. Builtin shortcuts match any position in a query. +إذا أضفت بادئة "@" أثناء إدخال الاختصار، فإنه يتطابق مع أي موضع في الاستعلام. تتطابق الاختصارات المدمجة مع أي موضع في الاستعلام. - Shortcut already exists, please enter a new Shortcut or edit the existing one. - Shortcut and/or its expansion is empty. + الاختصار موجود بالفعل، يرجى إدخال اختصار جديد أو تعديل الموجود. + الاختصار و/أو توسيعه فارغ. - Save - Overwrite - Cancel - Reset - Delete + حفظ + استبدال + إلغاء + إعادة تعيين + حذف + حسناً + نعم + لا + الخلفية - Version - Time - Please tell us how application crashed so we can fix it - Send Report - Cancel - General - Exceptions - Exception Type - Source - Stack Trace - Sending - Report sent successfully - Failed to send report - Flow Launcher got an error + الإصدار + الوقت + يرجى إخبارنا بكيفية تعطل التطبيق حتى نتمكن من إصلاحه + إرسال التقرير + إلغاء + عام + الاستثناءات + نوع الاستثناء + المصدر + تتبع المكدس + جارٍ الإرسال + تم إرسال التقرير بنجاح + فشل في إرسال التقرير + حدث خطأ في Flow Launcher + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message - Please wait... + يرجى الانتظار... - Checking for new update - You already have the latest Flow Launcher version - Update found - Updating... + التحقق من التحديث الجديد + أنت بالفعل تستخدم أحدث إصدار من Flow Launcher + تم العثور على تحديث + جارٍ التحديث... - 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 - New Flow Launcher release {0} is now available - An error occurred while trying to install software updates - Update - Cancel - Update Failed - Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com. - This upgrade will restart Flow Launcher - Following files will be updated - Update files - Update description + تحديث جديد + الإصدار الجديد من Flow Launcher {0} متاح الآن + حدث خطأ أثناء محاولة تثبيت تحديثات البرنامج + تحديث + إلغاء + فشل التحديث + تحقق من اتصالك وجرب تحديث إعدادات الوكيل إلى github-cloud.s3.amazonaws.com. + سيقوم هذا التحديث بإعادة تشغيل Flow Launcher + الملفات التالية سيتم تحديثها + تحديث الملفات + وصف التحديث - Skip - Welcome to Flow Launcher - Hello, this is the first time you are running Flow Launcher! - Before starting, this wizard will assist in setting up Flow Launcher. You can skip this if you wish. Please choose a language - Search and run all files and applications on your PC - Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse. - Flow Launcher starts with the hotkey below, go ahead and try it out now. To change it, click on the input and press the desired hotkey on the keyboard. - Hotkeys - Action Keyword and Commands - Search the web, launch applications or run various functions through Flow Launcher plugins. Certain functions start with an action keyword, and if necessary, they can be used without action keywords. Try the queries below in Flow Launcher. - Let's Start Flow Launcher - Finished. Enjoy Flow Launcher. Don't forget the hotkey to start :) + تخطي + مرحبًا بك في Flow Launcher + مرحبًا، هذه هي المرة الأولى التي تشغل فيها Flow Launcher! + قبل البدء، سيساعدك هذا المعالج في إعداد Flow Launcher. يمكنك تخطي هذه الخطوة إذا أردت. يرجى اختيار اللغة + ابحث وشغل جميع الملفات والتطبيقات على جهاز الكمبيوتر الخاص بك + ابحث عن كل شيء من التطبيقات، الملفات، الإشارات المرجعية، YouTube، Twitter والمزيد. كل ذلك من خلال لوحة المفاتيح الخاصة بك دون لمس الفأرة. + يبدأ Flow Launcher بمفتاح الاختصار أدناه، جربه الآن. لتغييره، انقر على الإدخال واضغط على مفتاح الاختصار المطلوب على لوحة المفاتيح. + مفاتيح الاختصار + كلمات المفتاح والأوامر + ابحث في الويب، شغل التطبيقات أو نفذ وظائف مختلفة من خلال إضافات Flow Launcher. تبدأ بعض الوظائف بكلمة مفتاح، وإذا لزم الأمر، يمكن استخدامها بدون كلمات مفتاح. جرب الاستعلامات أدناه في Flow Launcher. + لنبدأ مع Flow Launcher + تم الانتهاء. استمتع باستخدام Flow Launcher. لا تنسَ مفتاح الاختصار لبدء الاستخدام :) - Back / Context Menu - Item Navigation - Open Context Menu - Open Containing Folder - Run as Admin / Open Folder in Default File Manager - Query History - Back to Result in Context Menu - Autocomplete - Open / Run Selected Item - Open Setting Window - Reload Plugin Data + الرجوع / قائمة السياق + التنقل بين العناصر + فتح قائمة السياق + فتح المجلد المحتوي + تشغيل كمسؤول / فتح المجلد في مدير الملفات الافتراضي + سجل الاستعلامات + الرجوع إلى النتائج في قائمة السياق + الإكمال التلقائي + فتح / تشغيل العنصر المحدد + فتح نافذة الإعدادات + إعادة تحميل بيانات الإضافات - Select first result - Select last result - Run current query again - Open result - Open result #{0} + اختر النتيجة الأولى + اختر النتيجة الأخيرة + تشغيل الاستعلام الحالي مرة أخرى + فتح النتيجة + فتح النتيجة #{0} - Weather - Weather in Google Result + الطقس + الطقس في نتائج Google > ping 8.8.8.8 - Shell Command - s Bluetooth - Bluetooth in Windows Settings + أمر Shell + إعدادات Bluetooth + Bluetooth في إعدادات Windows sn - Sticky Notes + الملاحظات اللاصقة + + حجم الملف + تم الإنشاء + آخر تعديل diff --git a/Flow.Launcher/Languages/cs.xaml b/Flow.Launcher/Languages/cs.xaml index 1e387b2bc..92721b70a 100644 --- a/Flow.Launcher/Languages/cs.xaml +++ b/Flow.Launcher/Languages/cs.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + Nepodařilo se zaregistrovat hotkey "{0}". Klávesová zkratka může být používána jiným programem. Změňte na jinou klávesu nebo ukončíte jiný program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Nepodařilo se spustit {0} Neplatný typ souboru pluginu aplikace Flow Launcher @@ -33,6 +45,8 @@ Přenosný režim Ukládat všechna nastavení a uživatelská data v jedné složce (Užitečné při užití s přenosnými zařízeními). Spustit Flow Launcher při spuštění systému + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Při nastavování spouštění došlo k chybě Skrýt Flow Launcher při vykliknutí Nezobrazovat oznámení o nové verzi @@ -54,6 +68,10 @@ Zachovat poslední dotaz Vybrat poslední dotaz Smazat poslední dotaz + Preserve Last Action Keyword + Select Last Action Keyword + Fixed Window Height + The window height is not adjustable by dragging. Počet zobrazených výsledků Toto nastavení můžete také rychle upravit pomocí CTRL + Plus a CTRL + Minus. Ignorovat klávesové zkratky v režimu celé obrazovky @@ -71,10 +89,14 @@ Automatické aktualizace Vybrat Skrýt Flow Launcher při spuštění + Flow Launcher search window is hidden in the tray after starting up. Skrýt ikonu v systémové liště Pokud je ikona v oznamovací oblasti skrytá, nastavení lze otevřít kliknutím pravým tlačítkem myši na okno vyhledávání. Přesnost vyhledávání Změní minimální skóre shody, které je nutné pro zobrazení výsledků. + None + Low + Regular Vyhledávání pomocí pchin-jin Umožňuje vyhledávání pomocí pchin-jin. Pchin-jin je systém zápisu čínského jazyka pomocí písmen latinky. Vždy zobrazit náhled @@ -107,7 +129,8 @@ Verze Webová stránka Odinstalovat - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Obchod s pluginy @@ -124,8 +147,6 @@ Tento plugin byl aktualizován během posledních 7 dní Nová aktualizace je k dispozici - - Motiv Vzhled @@ -140,8 +161,13 @@ Spustit programy jako administrátor nebo jiný uživatel ProcessKiller Ukončit nežádoucí procesy + Search Bar Height + Item Height Písmo vyhledávacího pole - Písmo výsledků + Result Title Font + Result Subtitle Font + Reset + Customize Režim okna Neprůhlednost Motiv {0} neexistuje, použije se výchozí motiv @@ -167,6 +193,8 @@ Vlastní Hodiny Datum + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. Klávesová zkratka @@ -190,6 +218,7 @@ Cycle Previous Query Cycle Next Query Otevřít kontextovou nabídku + Open Native Context Menu Otevřít okno s nastavením Copy File Path Toggle Game Mode @@ -266,11 +295,17 @@ Vymazat logy Opravdu chcete odstranit všechny logy? Průvodce + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info Vybrat správce souborů - Zadejte umístění souboru správce souborů, který používáte, a v případě potřeby přidejte argumenty. Výchozí argumenty jsou "%d" a cesta se zadává v tomto umístění. Pokud je například požadován příkaz jako "totalcmd.exe /A c:\windows", argument je /A "%d". - "%f" je argument, který představuje cestu k souboru. Používá se ke zvýraznění názvu souboru/složky při otevření konkrétního umístění souboru ve správci souborů třetí strany. Tento argument je k dispozici pouze v položce "Arg. pro soubor". Pokud správce souborů tuto funkci nemá, můžete použít "%d". + 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 fileds blank. Správce souborů Jméno profilu Cesta k správci souborů @@ -333,6 +368,10 @@ Pokud před zkratku při zadávání přidáte znak "@", bude odpovíd Zrušit Reset Smazat + Dobře + Yes + No + Pozadí Verze @@ -349,6 +388,9 @@ Pokud před zkratku při zadávání přidáte znak "@", bude odpovíd Hlášení bylo úspěšně odesláno Nepodařilo se odeslat hlášení Flow Launcher zaznamenal chybu + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Počkejte prosím... @@ -417,4 +459,8 @@ Pokud před zkratku při zadávání přidáte znak "@", bude odpovíd sn Označené poznámky + + File Size + Created + Last Modified diff --git a/Flow.Launcher/Languages/da.xaml b/Flow.Launcher/Languages/da.xaml index 9f4ff0491..629173f74 100644 --- a/Flow.Launcher/Languages/da.xaml +++ b/Flow.Launcher/Languages/da.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Kunne ikke starte {0} Ugyldigt Flow Launcher plugin filformat @@ -33,6 +45,8 @@ Portable Mode Store all settings and user data in one folder (Useful when used with removable drives or cloud services). Start Flow Launcher ved system start + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Skjul Flow Launcher ved mistet fokus Vis ikke notifikationer om nye versioner @@ -54,6 +68,10 @@ Preserve Last Query Select last Query Empty last Query + Preserve Last Action Keyword + Select Last Action Keyword + Fixed Window Height + The window height is not adjustable by dragging. Maksimum antal resultater vist You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. Ignorer genvejstaster i fuldskærmsmode @@ -71,10 +89,14 @@ Autoopdatering Vælg Skjul Flow Launcher ved opstart + Flow Launcher search window is hidden in the tray after starting up. Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. Query Search Precision Changes minimum match score required for results. + None + Low + Regular Search with Pinyin Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese. Always Preview @@ -107,7 +129,8 @@ Version Website Uninstall - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Plugin Store @@ -124,8 +147,6 @@ This plugin has been updated within the last 7 days New Update is Available - - Tema Appearance @@ -140,8 +161,13 @@ Launch programs as admin or a different user ProcessKiller Terminate unwanted processes + Search Bar Height + Item Height Søgefelt skrifttype - Resultat skrifttype + Result Title Font + Result Subtitle Font + Reset + Customize Vindue mode Gennemsigtighed Theme {0} not exists, fallback to default theme @@ -167,6 +193,8 @@ Custom Clock Date + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. Genvejstast @@ -190,6 +218,7 @@ Cycle Previous Query Cycle Next Query Open Context Menu + Open Native Context Menu Open Setting Window Copy File Path Toggle Game Mode @@ -266,11 +295,17 @@ Clear Logs Are you sure you want to delete all logs? Wizard + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". + 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 fileds blank. File Manager Profile Name File Manager Path @@ -333,6 +368,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Annuller Reset Slet + OK + Yes + No + Background Version @@ -349,6 +388,9 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Rapport sendt korrekt Kunne ikke sende rapport Flow Launcher fik en fejl + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Please wait... @@ -417,4 +459,8 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in sn Sticky Notes + + File Size + Created + Last Modified diff --git a/Flow.Launcher/Languages/de.xaml b/Flow.Launcher/Languages/de.xaml index 84cce5a47..8a7e3498a 100644 --- a/Flow.Launcher/Languages/de.xaml +++ b/Flow.Launcher/Languages/de.xaml @@ -1,18 +1,30 @@  + + + Flow hat festgestellt, dass Sie {0} Plug-ins installiert haben, welche {1} zum Ausführen erfordern. Möchten Sie {1} herunterladen? + {2}{2} + Klicken Sie auf „Nein“, wenn es bereits installiert ist, und Sie werden aufgefordert, den Ordner auszuwählen, der die ausführbare Datei {1} enthält + + Bitte wählen Sie die ausführbare Datei {0} aus + Der Pfad zur ausführbaren Datei {0} kann nicht festgelegt werden. Bitte versuchen Sie es in den Einstellungen von Flow (scrollen Sie nach unten). + Plug-ins können nicht initialisiert werden + Plug-ins: {0} - nicht geladen werden und wird deaktiviert, bitte kontaktiere Sie den Ersteller des Plug-ins für Hilfe + - Fehler beim Registrieren des Hotkeys "{0}". Der Hotkey wird möglicherweise von einem anderen Programm verwendet. Wechseln Sie zu einem anderen Hotkey oder beenden Sie das andere Programm. + Hotkey "{0}" konnte nicht registriert werden. Der Hotkey ist möglicherweise von einem anderen Programm in Verwendung. Wechseln Sie zu einem anderen Hotkey oder beenden Sie das andere Programm. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher - Kann {0} nicht starten - Fehlerhaftes Flow Launcher-Plugin Dateiformat - In dieser Abfrage als oberstes setzen + Konnte nicht gestartet werden {0} + Flow Launcher Plug-in-Dateiformat ungültig + In dieser Abfrage als oberstes festlegen In dieser Abfrage oberstes abbrechen - Abfrage ausführen:{0} - Letzte Ausführungszeit:{0} + Abfrage ausführen: {0} + Letzte Ausführungszeit: {0} Öffnen Einstellungen Über - Schließen + Beenden Schließen Kopieren Ausschneiden @@ -23,390 +35,420 @@ Ordner Text Spielmodus - Hotkeys deaktivieren. + Aussetzen der Verwendung von Hotkeys. Position zurücksetzen - Position des Suchfensters zurücksetzen + Position des Suchfensters zurücksetze Einstellungen Allgemein Portabler Modus - Speichern Sie alle Einstellungen und Benutzerdaten in einem Ordner (nützlich bei Verwendung mit Wechseldatenträgern oder Clouddiensten). - Starte Flow Launcher bei Systemstart - Fehler beim Speichern von Autostart bei Systemstart - Verstecke Flow Launcher wenn der Fokus verloren geht - Zeige keine Nachricht wenn eine neue Version vorhanden ist - Suchfenster Position + Speichern Sie alle Einstellungen und Benutzerdaten in einem Ordner (nützlich bei Verwendung von Wechsellaufwerken oder Cloud-Diensten). + Flow Launcher bei Systemstart starten + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler + Fehler bei Einstellungsstart bei Start + Flow Launcher ausblenden, wenn Fokus verloren geht + Versionsbenachrichtigungen nicht zeigen + Position des Suchfensters Letzte Position merken - Bildschirm mit Mauszeiger - Bildschirm mit fokussiertem Fenster - Primärer Bildschirm - Benutzerdefinierter Bildschirm - Suchfenster Position auf Bildschirm - Mittig - Oben mittig + Monitor mit Mauscursor + Monitor mit fokussiertem Fenster + Primärer Monitor + Benutzerdefinierter Monitor + Position des Suchfensters auf Monitor + Zentriert + Oben zentriert Links oben Rechts oben Benutzerdefinierte Position Sprache - Abfragestil auswählen - Vorherige Ergebnisse ein-/ausblenden, wenn Flow Launcher wieder aktiviert wird. + Letzter Abfragestil + Vorherige Ergebnisse zeigen/ausblenden, wenn Flow Launcher reaktiviert wird. Letzte Abfrage beibehalten Letzte Abfrage auswählen Letzte Abfrage leeren - Maximale Anzahl Ergebnissen - Kann mit CTRL+Plus und CTRL+Minus schnell festgelegt werden. - Ignoriere Tastenkombination wenn Fenster im Vollbildmodus ist - Deaktiviere Flow Launcher, wenn eine Vollbildanwendung aktiv ist (Empfohlen für Spiele). - Standard-Dateimanager - Wählen Sie den Dateimanager, der beim Öffnen des Ordners verwendet werden soll. - Standardbrowser - Einstellung für neuen Tab, neues Fenster und dem Privatmodus. + Letztes Aktions-Schlüsselwort beibehalten + Letztes Aktions-Schlüsselwort auswählen + Feste Fensterhöhe + Die Fensterhöhe ist durch Ziehen nicht anpassbar. + Maximal gezeigte Ergebnisse + Sie können dies auch unter Verwendung von STRG+Plus und STRG+Minus schnell anpassen. + Hotkeys im Vollbildmodus ignorieren + Aktivierung von Flow Launcher deaktivieren, wenn eine Vollbildanwendung aktiv ist (empfohlen für Spiele). + Dateimanager per Default + Wählen Sie den Dateimanager aus, der beim Öffnen des Ordners verwendet werden soll. + Webbrowser per Default + Einstellung für Neuer Tab, Neues Fenster, Privater Modus. Python-Pfad Node.js-Pfad - Bitte wählen Sie das Programm Node.js aus + Bitte wählen Sie die ausführbare Datei Node.js aus Bitte wählen Sie pythonw.exe aus - Starte Eingabe immer im englischen Modus - Eingabemethode temporär auf Englisch wechseln beim Aktivieren von Flow. - Automatische Aktualisierung + Tippen immer im englischen Modus starten + Ändern Sie Ihre Eingabemethode temporär in den englischen Modus, wenn Sie Flow aktivieren. + Auto-Update Auswählen - Verstecke Flow Launcher bei Systemstart - Statusleistensymbol ausblenden - When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. + Flow Launcher beim Start ausblenden + Flow Launcher-Suchfenster wird nach dem Start in der Taskleiste ausgeblendet. + Tray-Icon ausblenden + Wenn das Icon in der Taskleiste ausgeblendet ist, kann das Menü Einstellungen durch einen Rechtsklick auf das Suchfenster geöffnet werden. Suchgenauigkeit abfragen - Erforderliche Suchergebnisse. - Pinyin aktivieren + Ändert den für Ergebnisse erforderlichen Mindestübereinstimmungswert. + Keine + Gering + Regelmäßig + Suche mit Pinyin Ermöglicht die Verwendung von Pinyin für die Suche. Pinyin ist das Standardsystem der romanisierten Schreibweise für die Übersetzung von chinesischen Texten. - Always Preview - Always open preview panel when Flow activates. Press {0} to toggle preview. - Der Schatteneffekt ist nicht zulässig, wenn das aktuelle Thema den Weichzeichneffekt aktiviert hat + Immer Vorschau + Vorschau-Panel immer öffnen, wenn Flow aktiviert ist. Drücken Sie {0}, um Vorschau umzuschalten. + Schatteneffekt ist nicht erlaubt, während das aktuelle Theme den Unschärfe-Effekt aktiviert hat - Search Plugin - Ctrl+F to search plugins - No results found - Please try a different search. - Erweiterung - Erweiterung - Suche nach weiteren Plugins - Aktivieren - Deaktivieren - Aktionswort Einstellung - Aktionsschlüsselwörter - Aktuelles Aktionswort - Neues Aktionswort - Aktionswörter ändern + Plug-in suchen + Strg+F, um Plug-ins zu suchen + Keine Ergebnisse gefunden + Bitte versuchen Sie eine andere Suche. + Plug-in + Plug-ins + Mehr Plug-ins finden + Ein + Aus + Aktions-Schlüsselwort-Einstellung + Aktions-Schlüsselwort + Aktuelles Action-Schlüsselwort + Neues Aktions-Schlüsselwort + Aktions-Schlüsselwörter ändern Aktuelle Priorität Neue Priorität Priorität - Change Plugin Results Priority - Pluginordner + Priorität der Plug-in-Ergebnisse ändern + Plug-in-Verzeichnis von - Initialisierungszeit: + Init-Zeit: Abfragezeit: Version - Webseite + Website Deinstallieren - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually - Erweiterungen laden - New Release - Recently Updated - Erweiterungen - Installed - Aktualisieren + Plug-in-Store + Neues Release + Kürzlich aktualisiert + Plug-ins + Installiert + Refresh Installieren Deinstallieren Aktualisieren - Plugin ist bereits installiert - New Version - Dieses Plugin wurde innerhalb der letzten 7 Tage aktualisiert - New Update is Available - - + Plug-in bereits installiert + Neue Version + Dieses Plug-in ist innerhalb der letzten 7 Tage aktualisiert worden + Neues Update ist verfügbar - Design - Appearance - Suche nach weiteren Themes - Wie man ein Design erstellt - Hallo! + Theme + Erscheinungsbild + Theme-Galerie + Wie man ein Theme erstellt + Hallo zusammen Explorer - Search for files, folders and file contents - WebSearch - Search the web with different search engine support + Suche nach Dateien, Ordnern und Dateiinhalten + Websuche + Suche im Web mit Unterstützung verschiedener Suchmaschinen Programm - Launch programs as admin or a different user + Programme als Admin oder ein anderer Benutzer starten ProcessKiller - Terminate unwanted processes - Abfragebox Schriftart - Ergebnis Schriftart + Unerwünschte Prozesse beenden + Suchleistenhöhe + Elementhöhe + Schriftart der Abfragebox + Schriftart des Ergebnistitels + Schriftart des Ergebnis-Untertitels + Zurücksetzen + Individuell anpassen Fenstermodus - Transparenz - Das Design {0} existiert nicht, deshalb wird das Standard-Template aktiviert - Laden des Designs {0} fehlgeschlagen, das Standard-Template wird aktiviert - Themenordner öffnen - Themenordner öffnen + Opazität + Theme {0} ist nicht vorhanden, Fallback auf Default-Theme + Theme {0} konnte nicht geladen werden, Fallback auf Default-Theme + Theme-Ordner + Theme-Ordner öffnen Farbschema Systemvorgabe Hell Dunkel Soundeffekt - Ton abspielen, wenn das Suchfenster geöffnet wird - Sound Effect Volume - Adjust the volume of the sound effect - Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume. + Einen kurzen Sound abspielen, wenn das Suchfenster geöffnet wird + Lautstärke der Soundeffekte + Lautstärke des Soundeffekts anpassen + Windows Media Player ist nicht verfügbar und ist für die Lautstärkeregelung von Flow erforderlich. Bitte überprüfen Sie Ihre Installation, wenn Sie die Lautstärke anpassen müssen. Animation - Animationen in der Oberfläche verwenden - Animation Speed - The speed of the UI animation + Animation in UI verwenden + Animationsgeschwindigkeit + Die Geschwindigkeit der UI-Animation Langsam Mittel Schnell Benutzerdefiniert Uhr - Date + Datum + Dieses Theme unterstützt zwei Modi (hell/dunkel). + Dieses Theme unterstützt Unschärfe und transparenten Hintergrund. - Tastenkombination - Tastenkombination - Open Flow Launcher - Verknüpfung eingeben, um Flow Launcher anzuzeigen/auszublenden. - Toggle Preview - Enter shortcut to show/hide preview in search window. - Hotkey Presets - List of currently registered hotkeys - Öffnen Sie die Ergebnismodifikatoren - Wählen Sie eine Modifikatortaste, um das ausgewählte Ergebnis über die Tastatur zu öffnen. - Hotkey anzeigen - Hotkey für die Ergebnisauswahl mit Ergebnissen anzeigen. - Auto Complete - Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page - Cycle Previous Query - Cycle Next Query + Hotkey + Hotkeys + Flow Launcher öffnen + Shortcut eingeben, um Flow Launcher anzuzeigen/auszublenden. + Vorschau umschalten + Shortcut eingeben, um Vorschau im Suchfenster anzuzeigen/auszublenden. + Hotkey-Presets + Liste der derzeit registrierten Hotkeys + Ergebnis-Modifkator-Taste öffnen + Wählen Sie eine Modifikator-Taste aus, um das ausgewählte Ergebnis via Tastatur zu öffnen. + Hotkey zeigen + Hotkey für die Ergebnisauswahl mit Ergebnissen zeigen. + Auto-Vervollständigung + Führt die Autovervollständigung für die ausgewählten Elemente aus. + Nächstes Element auswählen + Vorheriges Element auswählen + Nächste Seite + Vorherige Seite + Vorherige Abfrage durchblättern + Nächste Abfrage durchblättern Kontextmenü öffnen + Natives Kontextmenü öffnen Einstellungsfenster öffnen - Copy File Path - Gott Modus - Toggle History - Öffne beinhaltenden Ordner - Run As Admin - Refresh Search Results - Reload Plugins Data - Quick Adjust Window Width - Quick Adjust Window Height - Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. - Benutzerdefinierte Abfrage Tastenkombination - Custom Query Shortcut - Built-in Shortcut + Dateipfad kopieren + Spielmodus umschalten + Historie umschalten + Enthaltenden Ordner öffnen + Als Admin ausführen + Suchergebnisse auffrischen + Plug-in-Daten neu laden + Fensterbreite schnell anpassen + Fensterhöhe schnell anpassen + Verwenden, wenn Plug-ins ihre vorhandenen Daten neu laden und aktualisieren müssen. + Sie können einen weiteren Hotkey für diese Funktion hinzufügen. + Benutzerdefinierte Abfrage-Hotkeys + Benutzerdefinierte Abfrage-Shortcuts + Integrierte Shortcuts Abfrage Shortcut - Expansion + Erweiterung Beschreibung Löschen Bearbeiten Hinzufügen - None - Bitte einen Eintrag auswählen - Wollen Sie die {0} Plugin Tastenkombination wirklich löschen? - Are you sure you want to delete shortcut: {0} with expansion {1}? - Get text from clipboard. - Get path from active explorer. - Schatteneffekt im Abfragefenster - Der Schatteneffekt beansprucht die GPU stark. Nicht empfohlen, wenn die Leistung deines Computers begrenzt ist. - Fenstergröße Breite - You can also quickly adjust this by using Ctrl+[ and Ctrl+]. - Segoe Fluent Icons verwenden - Segoe Fluent Icons für Abfrageergebnisse verwenden, sofern unterstützt - Press Key + Keine + Bitte wählen Sie ein Element aus + Sind Sie sicher, dass Sie den {0} Plug-in-Hotkey löschen wollen? + Sind Sie sicher, dass Sie den Shortcut {0} mit der Erweiterung {1} löschen wollen? + Text aus Zwischenablage abrufen. + Pfad aus aktivem Explorer abrufen. + Schatteneffekt in Abfragefenster + Der Schatteneffekt beansprucht die GPU stark. Nicht empfohlen, wenn die Performance Ihres Computers begrenzt ist. + Fensterbreite Größe + Sie können dies auch schnell durch die Verwendung von Strg+[ und Strg+] anpassen. + Segoe Fluent-Icons verwenden + Segoe Fluent-Icons für Abfrageergebnisse verwenden, wo unterstützt + Taste drücken HTTP-Proxy - Aktiviere HTTP Proxy + HTTP-Proxy aktivieren HTTP-Server Port Benutzername Passwort - Teste Proxy + Proxy testen Speichern - Server darf nicht leer sein - Server Port darf nicht leer sein - Falsches Port Format - Proxy wurde erfolgreich gespeichert - Proxy ist korrekt - Verbindung zum Proxy fehlgeschlagen + Feld Server darf nicht leer sein + Feld Port darf nicht leer sein + Port-Format ungültig + Proxy-Konfiguration erfolgreich gespeichert + Proxy korrekt konfiguriert + Proxy-Verbindung fehlgeschlagen Über - Webseite + Website GitHub - Dokumentation + Docs Version Icons Sie haben Flow Launcher {0} mal aktiviert - Nach Aktuallisierungen Suchen - Become A Sponsor - Eine neue Version ({0}) ist vorhanden. Bitte starten Sie Flow Launcher neu. + Nach Updates suchen + Ein Sponsor werden + Neue Version {0} ist verfügbar. Möchten Sie Flow Launcher neu starten, um das Update zu verwenden? Überprüfung der Updates fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindungs- und Proxy-Einstellungen zu api.github.com. Das Herunterladen von Updates ist fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindungs- und Proxy-Einstellungen zu github-cloud.s3.amazonaws.com, oder gehen Sie zu https://github.com/Flow-Launcher/Flow.Launcher/releases, um Updates manuell herunterzuladen. Versionshinweise - Verwendungshinweise - Entwicklerwerkzeuge - Einstellungsordner - Protokoll-Verzeichnis - Protokolldateien leeren - Sind Sie sicher, dass Sie alle Protokolle löschen möchten? + Tipps zur Nutzung + DevTools + Ordner »Settings« + Ordner »Logs« + Logs löschen + Sind Sie sicher, dass Sie alle Logs löschen wollen? Assistent + Speicherort für Benutzerdaten + Benutzereinstellungen und installierte Plug-ins werden im Ordner für Benutzerdaten gespeichert. Dieser Speicherort kann variieren, je nachdem, ob sich das Programm im portablen Modus befindet oder nicht. + Ordner öffnen + Log Level + Debug + Info Dateimanager auswählen - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". - Datei-Manager + Bitte geben Sie den Dateiort des von Ihnen verwendeten Dateimanagers an und fügen Sie bei Bedarf Argumente hinzu. Das „%d“ repräsentiert den dafür zu öffnenden Verzeichnispfad, der vom Feld Arg for Folder und für Befehle zum Öffnen bestimmter Verzeichnisse verwendet wird. Das „%f“ repräsentiert den dafür zu öffnenden Dateipfad, der vom Feld Arg for File und für Befehle zum Öffnen bestimmter Dateien verwendet wird. + Zum Beispiel, wenn der Dateimanager einen Befehl wie „totalcmd.exe /A c:\windows“ verwendet, um das Verzeichnis c:\windows zu öffnen, lautet der Dateimanager-Pfad „totalcmd.exe“ und der Arg for Folder „/A %d“. Bestimmte Dateimanager wie QTTabBar kann nur die Angabe eines Pfades erfordern, in diesem Fall verwenden Sie „%d“ als den Dateimanager-Pfad und lassen den Rest der Felder blank. + Dateimanager Profilname Dateimanager-Pfad - Argumente für Ordner - Argumente für Datei + Arg For Folder + Arg For File - Standard-Webbrowser - Die Standardeinstellung verwendet die Browser-Standardeinstellung des Betriebssystems. Falls angegeben, verwendet Flow diesen Browser. + Webbrowser per Default + Die Defaulteinstellung folgt der Default-Browsereinstellung des Betriebssystems. Wenn separat angegeben, verwendet Flow diesen Browser. Browser Browser-Name - Browserpfad + Browser-Pfad Neues Fenster Neuer Tab Privater Modus Priorität ändern - Je höher die Zahl, desto höher wird das Ergebnis gewertet. Versuchen Sie es mit einer 5. Sollen die Ergebnisse tiefer sein als diejenigen anderer Plugins, verwenden Sie eine negative Zahl - Bitte eine gültige Ganzzahl angeben für die Priorität! + Je höher die Zahl ist, desto höher wird das Ergebnis eingestuft. Versuchen Sie, den Wert auf 5 einzustellen. Wenn Sie möchten, dass die Ergebnisse niedriger sind als jedes andere Plug-in, geben Sie eine negative Zahl ein + Bitte geben Sie eine gültige ganze Zahl für Priorität an! - Altes Aktionsschlüsselwort - Neues Aktionsschlüsselwort + Altes Aktions-Schlüsselwort + Neues Aktions-Schlüsselwort Abbrechen Fertig - Kann das angegebene Plugin nicht finden - Neues Aktionsschlüsselwort darf nicht leer sein - Aktionsschlüsselwort ist schon bei einem anderen Plugin in verwendung. Bitte stellen Sie ein anderes Aktionsschlüsselwort ein. - Erfolgreich + Das angegebene Plug-in kann nicht gefunden werden + Neues Aktions-Schlüsselwort darf nicht leer sein + Dieses neue Aktions-Schlüsselwort ist bereits einem anderen Plug-in zugewiesen, bitte wählen Sie ein anderes + Erfolg Erfolgreich abgeschlossen - Benutzen Sie * wenn Sie ein Aktionsschlüsselwort definieren wollen. + Geben Sie das Aktions-Schlüsselwort ein, das Sie verwenden möchten, um das Plug-in zu starten. Verwenden Sie *, wenn Sie keines angeben möchten, und das Plug-in wird ohne irgendwelche Aktions-Schlüsselwörter ausgelöst. - Benutzerdefinierte Abfrage Tastenkombination - Press a custom hotkey to open Flow Launcher and input the specified query automatically. + Benutzerdefinierter Abfrage-Hotkey + Drücken Sie einen benutzerdefinierten Hotkey, um Flow Launcher zu öffnen und die angegebene Abfrage automatisch einzugeben. Vorschau - Tastenkombination ist nicht verfügbar, bitte wähle eine andere Tastenkombination - Ungültige Plugin Tastenkombination + Hotkey ist nicht verfügbar, bitte wählen Sie einen neuen Hotkey aus + Plug-in-Hotkey ungültig Aktualisieren - 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. + Bindung Hotkey + Aktueller Hotkey ist nicht verfügbar. + Dieser Hotkey ist für "{0}" reserviert und kann nicht verwendet werden. Bitte wählen Sie einen anderen Hotkey. + Dieser Hotkey ist bereits in Verwendung von "{0}". Wenn Sie "Überschreiben" drücken, wird dieser aus "{0}" entfernt. + Drücken Sie die Tasten, die Sie für diese Funktion verwenden möchten. - Custom Query Shortcut - Enter a shortcut that automatically expands to the specified query. - A shortcut is expanded when it exactly matches the query. + Benutzerdefinierter Abfrage-Shortcut + Geben Sie einen Shortcut ein, der sich automatisch auf die angegebene Abfrage erweitert. + Ein Shortcut wird erweitert, wenn dieser genau mit der Abfrage übereinstimmt. -If you add an '@' prefix while inputting a shortcut, it matches any position in the query. Builtin shortcuts match any position in a query. +Wenn Sie bei der Eingabe eines Shortcuts ein '@'-Präfix hinzufügen, stimmt dieser mit jeder beliebigen Position in der Abfrage überein. Integrierte Shortcuts stimmen mit jeder Position in einer Abfrage überein. - Shortcut already exists, please enter a new Shortcut or edit the existing one. - Shortcut and/or its expansion is empty. + Shortcut ist bereits vorhanden, bitte geben Sie einen neuen Shortcut ein oder bearbeiten Sie den vorhandenen. + Shortcut und/oder dessen Erweiterung ist leer. Speichern - Overwrite + Überschreiben Abbrechen - Reset + Zurücksetzen Löschen + OK + Ja + Nein + Hintergrund Version Zeit - Bitte teilen Sie uns mit, wie die Anwendung abgestürzt ist, damit wir den Fehler beheben können. - Sende Report + Bitte teilen Sie uns mit, wie die Anwendung abgestürzt ist, damit wir den Fehler beheben können + Bericht senden Abbrechen Allgemein - Fehler - Fehlertypen + Ausnahmen + Ausnahme-Typ Quelle - Stapelüberwachung - Sende - Report erfolgreich - Report fehlgeschlagen + Stapelverfolgung + Senden + Bericht erfolgreich gesendet + Bericht konnte nicht gesendet werden Flow Launcher hat einen Fehler + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message - Bitte warten... + Bitte warten Sie ... - Nach Updates suchen! - Sie haben bereits die neuste Version + Prüfung auf neues Update + Sie haben bereits die neueste Version von Flow Launcher Update gefunden Wird aktualisiert ... - Flow Launcher konnte deine Profildaten nicht in die neue Updateversion verschieben. + Flow Launcher war nicht in der Lage, Ihre Benutzerprofildaten auf die neue Update-Version zu übertragen. Bitte verschieben Sie Ihren Profildatenordner manuell von {0} nach {1} Neues Update - V{0} von Flow Launcher ist verfügbar - Es ist ein Fehler während der Installation der Aktualisierung aufgetreten. + Neues Release {0} von Flow Launcher ist jetzt verfügbar + Beim Versuch, Software-Updates zu installieren, ist ein Fehler aufgetreten Aktualisieren Abbrechen - Aktualisierung fehlgeschlagen - Überprüfen Sie Ihre Internetverbindung und aktualisieren Sie die Proxy-Einstellungen um auf github-cloud.s3.amazonaws.com zugreifen zu können. - Diese Aktualisierung wird Flow Launcher neu starten + Update fehlgeschlagen + Überprüfen Sie Ihre Verbindung und versuchen Sie, die Proxy-Einstellungen auf github-cloud.s3.amazonaws.com zu aktualisieren. + Dieses Upgrade wird Flow Launcher neu starten Folgende Dateien werden aktualisiert - Aktualisiere Dateien - Aktualisierungbeschreibung + Daten aktualisieren + Update-Beschreibung Überspringen - Willkommen im Flow Launcher - Hallo, dies ist das erste Mal, dass Sie den Flow Launcher verwenden! - Vor dem Start hilft dieser Assistent beim Einrichten des Flow Launchers. Sie können dies überspringen, wenn Sie möchten. Bitte wählen Sie eine Sprache aus - Suchen und starten Sie alle Dateien sowie Anwendungen auf Ihrem PC - Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse. - Flow Launcher starts with the hotkey below, go ahead and try it out now. To change it, click on the input and press the desired hotkey on the keyboard. - Tastenkombination + Willkommen bei Flow Launcher + Hallo, dies ist das erste Mal, dass Sie Flow Launcher ausführen! + Bevor Sie beginnen, hilft dieser Assistent bei der Einrichtung von Flow Launcher. Sie können dies überspringen, wenn Sie möchten. Bitte wählen Sie eine Sprache + Alle Dateien und Anwendungen auf Ihrem PC suchen und ausführen + Durchsuchen Sie alles von Anwendungen, Dateien, Lesezeichen, YouTube, Twitter und vielem mehr. Alles bequem über Ihre Tastatur, ohne die Maus zu berühren. + Flow Launcher startet mit dem unten stehenden Hotkey, probieren Sie ihn gleich aus. Um ihn zu ändern, klicken Sie auf die Eingabe und drücken Sie den gewünschten Hotkey auf der Tastatur. + Hotkeys Aktions-Schlüsselwort und Befehle - Search the web, launch applications or run various functions through Flow Launcher plugins. Certain functions start with an action keyword, and if necessary, they can be used without action keywords. Try the queries below in Flow Launcher. - Starte Flow-Launcher - Fertig. Genießen Sie Flow Launcher. Die Tastenkombination nicht vergessen, um Flow Launcher zu starten :) + Suchen Sie im Web, starten Sie Anwendungen oder führen Sie verschiedene Funktionen über Flow Launcher-Plug-ins aus. Bestimmte Funktionen beginnen mit einem Aktions-Schlüsselwort und diese können bei Bedarf auch ohne Aktions-Schlüsselwörter verwendet werden. Probieren Sie die Abfragen unten in Flow Launcher aus. + Lassen Sie uns Flow Launcher starten + Beendet. Viel Spaß mit Flow Launcher. Vergessen Sie nicht den Hotkey zum Starten :) Zurück / Kontextmenü - Element Navigation + Element-Navigation Kontextmenü öffnen - Öffne beinhaltenden Ordner - Als Admin ausführen / Ordner im Standard-Dateimanager öffnen - Verlauf durchsuchen - Zurück zum Resultat im Kontextmenü + Enthaltenden Ordner öffnen + Als Admin ausführen / Ordner im Default-Dateimanager öffnen + Historie abfragen + Zurück zu Ergebnis in Kontextmenü Autovervollständigung Ausgewähltes Element öffnen / ausführen Einstellungsfenster öffnen - Plugin-Daten neu laden + Plug-in-Daten neu laden - Select first result - Select last result - Run current query again - Open result - Open result #{0} + Erstes Ergebnis auswählen + Letztes Ergebnis auswählen + Aktuelle Abfrage erneut ausführen + Ergebnis öffnen + Ergebnis öffnen #{0} Wetter Wetter in Google-Ergebnis @@ -414,7 +456,11 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Shell-Befehl s Bluetooth Bluetooth in Windows-Einstellungen - nt - Notizen + sn + Haftnotizen + + Dateigröße + Erstellt + Zuletzt geändert diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 28b516757..24ab3cf94 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -2,8 +2,25 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + + Your selected {0} executable is invalid. + {2}{2} + Click yes if you would like select the {0} executable agian. Click no if you would like to download {1} + + 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 + Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Could not start {0} Invalid Flow Launcher plugin file format @@ -27,7 +44,7 @@ Game Mode Suspend the use of Hotkeys. Position Reset - Reset search window position + Type here to search Settings @@ -35,6 +52,8 @@ Portable Mode Store all settings and user data in one folder (Useful when used with removable drives or cloud services). Start Flow Launcher on system startup + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Hide Flow Launcher when focus is lost Do not show new version notifications @@ -56,8 +75,8 @@ Preserve Last Query Select last Query Empty last Query - Fixed Window Height - The window height will not be resizeable by dragging + Preserve Last Action Keyword + Select Last Action Keyword Maximum results shown You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. Ignore hotkeys in fullscreen mode @@ -75,6 +94,7 @@ Auto Update Select Hide Flow Launcher on startup + Flow Launcher search window is hidden in the tray after starting up. Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. Query Search Precision @@ -87,6 +107,15 @@ Always Preview Always open preview panel when Flow activates. Press {0} to toggle preview. Shadow effect is not allowed while current theme has blur effect enabled + Search Delay + Delay for a while to search when typing. This reduces interface jumpiness and result load. + Default Search Delay Time + Plugin default delay time after which search results appear when typing is stopped. + Very long + Long + Normal + Short + Very short Search Plugin @@ -103,6 +132,8 @@ Current action keyword New action keyword Change Action Keywords + Plugin seach delay time + Change Plugin Seach Delay Time Current Priority New Priority Priority @@ -114,7 +145,11 @@ Version Website Uninstall - + 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 + Default Plugin Store @@ -131,8 +166,6 @@ This plugin has been updated within the last 7 days New Update is Available - - Theme Appearance @@ -179,7 +212,20 @@ Custom Clock Date - + Backdrop Type + Backdrop supported starting from Windows 11 build 22000 and above + None + Acrylic + Mica + Mica Alt + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. + Show placeholder + Display placeholder when query is empty + Placeholder text + Change placeholder text. Input empty will use: {0} + Fixed Window Size + The window size is not adjustable by dragging. Hotkey @@ -203,6 +249,7 @@ Cycle Previous Query Cycle Next Query Open Context Menu + Open Native Context Menu Open Setting Window Copy File Path Toggle Game Mode @@ -278,12 +325,21 @@ Log Folder Clear Logs Are you sure you want to delete all logs? + Clear Caches + Are you sure you want to delete all caches? + Failed to clear part of folders and files. Please see log file for more information Wizard + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". + 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 fileds blank. File Manager Profile Name File Manager Path @@ -313,9 +369,16 @@ Can't find specified plugin New Action Keyword can't be empty This new Action Keyword is already assigned to another plugin, please choose a different one + This new Action Keyword is the same as old, please choose a different one Success Completed successfully - Enter the action keyword you like to use to start the plugin. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords. + 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 + Select the search delay time you like to use for the plugin. Select "{0}" if you don't want to specify any, and the plugin will use default search delay time. + Current search delay time + New search delay time Custom Query Hotkey @@ -344,6 +407,10 @@ Cancel Reset Delete + OK + Yes + No + Background Version @@ -360,6 +427,9 @@ Report sent successfully Failed to send report Flow Launcher got an error + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Please wait... diff --git a/Flow.Launcher/Languages/es-419.xaml b/Flow.Launcher/Languages/es-419.xaml index aa3536f80..1ab69727b 100644 --- a/Flow.Launcher/Languages/es-419.xaml +++ b/Flow.Launcher/Languages/es-419.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher No se pudo iniciar {0} Formato de archivo de plugin Flow Launcher inválido @@ -33,6 +45,8 @@ Modo portable Almacena todos los ajustes y datos de usuario en una sola carpeta (útil cuando se utiliza con unidades extraíbles o servicios en la nube). Iniciar Flow Launcher al arrancar el sistema + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Ocultar Flow Launcher cuando se pierde el enfoque No mostrar notificaciones de nuevas versiones @@ -54,6 +68,10 @@ Conservar última consulta Seleccionar última consulta Borrar última consulta + Preserve Last Action Keyword + Select Last Action Keyword + Fixed Window Height + The window height is not adjustable by dragging. Máximo de resultados mostrados You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. Ignorar atajos de teclado en modo pantalla completa @@ -71,10 +89,14 @@ Actualización automática Seleccionar Ocultar Flow Launcher al arrancar el sistema + Flow Launcher search window is hidden in the tray after starting up. Ocultar icono de la bandeja When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. Precisión de la búsqueda Cambia la puntuación mínima de similitud requerida para resultados. + None + Low + Regular Search with Pinyin Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese. Always Preview @@ -107,7 +129,8 @@ Versión Sitio web Uninstall - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Tienda de Plugins @@ -124,8 +147,6 @@ This plugin has been updated within the last 7 days New Update is Available - - Tema Appearance @@ -140,8 +161,13 @@ Launch programs as admin or a different user ProcessKiller Terminate unwanted processes + Search Bar Height + Item Height Fuente del cuadro de consulta - Fuente de los resultados + Result Title Font + Result Subtitle Font + Reset + Customize Modo Ventana Opacidad Tema {0} no existe, se usará al tema predeterminado @@ -167,6 +193,8 @@ Custom Clock Date + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. Tecla Rápida @@ -190,6 +218,7 @@ Cycle Previous Query Cycle Next Query Open Context Menu + Open Native Context Menu Open Setting Window Copy File Path Toggle Game Mode @@ -266,11 +295,17 @@ Clear Logs Are you sure you want to delete all logs? Asistente + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info Seleccionar Gestor de Archivos - Por favor, especifique la ubicación del gestor de archivos que utiliza y añada argumentos si es necesario. Los argumentos por defecto son "%d", y se introduce una ruta en esa ubicación. Por ejemplo, si se requiere un comando como "totalcmd.exe /A c:\windows", el argumento es /A "%d". - "%f" es un argumento que representa la ruta del archivo. Se utiliza para enfatizar el nombre de archivo/carpeta al abrir una ubicación específica de archivo en un gestor de archivos de terceros. Este argumento sólo está disponible en el elemento "Arg para Archivo". Si el gestor de archivos no tiene esa función, puede utilizar "%d". + 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 fileds blank. Gestor de Archivos Nombre de Perfil Ruta del Gestor de Archivos @@ -333,6 +368,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Cancelar Reset Eliminar + OK + Yes + No + Background Versión @@ -349,6 +388,9 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Informe enviado correctamente Error al enviar el informe Flow Launcher ha tenido un error + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Por favor espere... @@ -417,4 +459,8 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in sn Notas adhesivas + + File Size + Created + Last Modified diff --git a/Flow.Launcher/Languages/es.xaml b/Flow.Launcher/Languages/es.xaml index a0d8e00c3..4cba4c8a7 100644 --- a/Flow.Launcher/Languages/es.xaml +++ b/Flow.Launcher/Languages/es.xaml @@ -1,7 +1,19 @@  + + + Flow ha detectado que tiene instalado(s) {0} complemento(s), que necesita(n) {1} para funcionar. ¿Desea descargar {1}? + {2}{2} + Haga clic en no si ya está instalado, y seleccione la carpeta que contiene el ejecutable {1} + + Por favor, seleccione el ejecutable {0} + No se puede establecer la ruta del ejecutable {0}, por favor inténtelo desde la configuración de Flow (desplácese hacia abajo). + Fallo al iniciar los complementos + Complemento: {0} - no se pudo cargar y se desactivará, póngase en contacto con el creador del complemento para obtener ayuda + No se ha podido registrar el atajo de teclado "{0}". El atajo de teclado puede estar siendo utilizado por otro programa. Seleccione un atajo de teclado diferente o salga del otro programa. + No se ha podido anular el registro de la tecla de acceso rápido «{0}». Inténtelo de nuevo o consulte el registro para obtener más detalles Flow Launcher No se ha podido iniciar {0} Formato de archivo del complemento de Flow Launcher no válido @@ -33,6 +45,8 @@ Modo Portable Guarda toda la configuración y datos de usuario en una carpeta (Útil cuando se utiliza con unidades extraíbles o servicios en la nube). Cargar Flow Launcher al iniciar el sistema + Usar la tarea de inicio de sesión en lugar de la entrada de inicio para una experiencia de inicio más rápida + Después de la desinstalación, es necesario eliminar manualmente la tarea (Flow.Launcher Startup) mediante el Programador de Tareas Error de configuración de arranque al iniciar Ocultar Flow Launcher cuando se pierde el foco No mostrar notificaciones de nuevas versiones @@ -54,8 +68,12 @@ Mantener la última consulta Seleccionar la última consulta Limpiar la última consulta + Conservar última palabra clave de acción + Seleccionar última palabra clave de acción + Altura de la ventana fija + La altura de la ventana no se puede ajustar arrastrando el ratón. Número máximo de resultados mostrados - También puede ajustarse rápidamente usando Ctrl+Más y Ctrl+Menos. + También puede ajustarse rápidamente usando Ctrl+Más(+) y Ctrl+Menos(-). Ignorar atajos de teclado en modo pantalla completa No permite activar Flow Launcher con aplicaciones a pantalla completa (Recomendado para juegos). Administrador de archivos predeterminado @@ -71,14 +89,18 @@ Actualización automática Seleccionar Ocultar Flow Launcher al inicio + La ventana de búsqueda de Flow Launcher se oculta en la bandeja del sistema tras el arranque. Ocultar icono en la bandeja del sistema Cuando el icono está oculto en la bandeja del sistema, se puede abrir el menú de configuración haciendo clic con el botón derecho en la ventana de búsqueda. Precisión de búsqueda en consultas Cambia la puntuación mínima requerida para la coincidencia de los resultados. + Ninguna + Baja + Normal Buscar con Pinyin Permite utilizar Pinyin para la búsqueda. Pinyin es el sistema estándar de ortografía romanizado para traducir chino. Mostrar siempre vista previa - Muestra siempre el panel de vista previa al iniciar Flow. Pulse {0} para mostrar/ocultar la vista previa. + Muestra siempre el panel de vista previa al iniciar Flow. Pulsar {0} para mostrar/ocultar la vista previa. El efecto de sombra no está permitido si el tema actual tiene activado el efecto de desenfoque @@ -107,10 +129,11 @@ Versión Sitio web Desinstalar - + Fallo al eliminar la configuración del complemento + Complementos: {0} - Fallo al eliminar los archivos de configuración del complemento, por favor elimínelos manualmente - Tienda de complementos + Tienda complementos Nuevo(s) lanzamiento(s) Actualizado(s) recientemente Complementos @@ -124,8 +147,6 @@ Este complemento ha sido actualizado en los últimos 7 días Nueva actualización disponible - - Tema Apariencia @@ -140,8 +161,13 @@ Iniciar programas como administrador o como usuario diferente Eliminar Procesos Terminar procesos no deseados + Altura de la barra de búsqueda + Altura del elemento Fuente del texto del cuadro de consulta - Fuente del texto de los resultados + Fuente del título del resultado + Fuente del subtítulo del resultado + Restablecer + Personaliza Modo Ventana Opacidad El tema {0} no existe, activando el tema por defecto @@ -156,7 +182,7 @@ Reproduce un pequeño sonido cuando se abre el cuadro de búsqueda Volumen del efecto de sonido Ajusta el volumen del efecto de sonido - 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 no está disponible y es necesario para el ajuste del volumen de Flow. Por favor, compruebe su instalación si necesita ajustar el volumen. Animación Usa animación en la Interfaz de Usuario Velocidad de animación @@ -167,29 +193,32 @@ Personalizada Reloj Fecha + Este tema soporta dos modos (claro/oscuro). + Este tema soporta fondo transparente desenfocado. Atajo de teclado Atajos de teclado - Abrir Flow Launcher - Introduzca el atajo de teclado para mostrar/ocultar Flow Launcher. - Cambiar vista previa - Introduzca el acceso directo para mostrar/ocultar la vista previa en la ventana de búsqueda. - Ajustes preestablecidos de atajos de teclado + Abrir/Cerrar Flow Launcher + Introduzca el atajo de teclado para abrir/cerrar Flow Launcher. + Mostrar/Ocultar vista previa + Introduzca el atajo de teclado para mostrar/ocultar la vista previa en la ventana de búsqueda. + Atajos de teclado preestablecidos Lista de atajos de teclado actualmente registrados Tecla modificadora para abrir resultado Seleccione una tecla modificadora para abrir el resultado seleccionado con el teclado. Mostrar atajo de teclado - Muestra atajo de teclado de selección junto a los resultados. + Muestra atajo de teclado de apertura junto a los resultados. Completar automáticamente - Ejecuta completar automáticamente para los elementos seleccionados. + Ejecuta la función 'completar automáticamente' para los elementos seleccionados. Seleccionar siguiente elemento Seleccionar elemento anterior - Siguiente página - Página anterior - Cycle Previous Query - Cycle Next Query + Seleccionar siguiente página + Seleccionar página anterior + Recuperar una tras otra la consulta anterior + Recuperar una tras otra la siguiente consulta Abrir menú contextual + Abrir menú contextual nativo Abrir ventana de configuración Copiar ruta del archivo Cambiar a Modo Juego @@ -198,8 +227,8 @@ Ejecutar como administrador Actualizar resultados de búsqueda Recargar datos de complementos - Ajuste rápido de la anchuira de la ventana - Ajuste rápido de la altura de la ventana + Ajustar rápidamente anchura de la ventana + Ajustar rápidamente altura de la ventana Se utiliza cuando se requiere que los complementos recarguen y actualicen sus datos existentes. Se puede añadir otro atajo de teclado más para esta función. Atajos de teclado de consulta personalizada @@ -214,16 +243,16 @@ Añadir Ninguno Por favor, seleccione un elemento - ¿Está seguro que desea eliminar el atajo de teclado de consulta personalizada {0}? + ¿Está seguro de que desea eliminar el atajo de teclado de consulta personalizada {0}? ¿Está seguro de que desea eliminar el acceso directo: {0} con la expansión {1}? Obtiene el texto del portapapeles. Obtiene la ruta del explorador activo. Efecto de sombra de la ventana de consultas - El efecto de sombra hace un uso sustancial de la GPU. No se recomienda para ordenadores de rendimiento limitado. + Este efecto hace un uso sustancial de la GPU. No se recomienda para ordenadores de bajo rendimiento. Tamaño del ancho de la ventana También puede ajustarse rápidamente usando Ctrl+[ y Ctrl+]. - Usar iconos Segoe Fluent - Usa iconos Segoe Fluent para los resultados de la consulta cuando sean compatibles + Iconos Segoe Fluent + Utiliza iconos Segoe Fluent para los resultados de la consulta cuando sean compatibles Pulsar Tecla @@ -266,11 +295,17 @@ Eliminar registros ¿Está seguro de que desea eliminar todos los registros? Asistente + Ubicación de datos del usuario + La configuración del usuario y los complementos instalados se guardan en la carpeta de datos del usuario. Esta ubicación puede variar dependiendo de si está en modo portable o no. + Abrir carpeta + Nivel de registro + Depurar + Información Seleccionar administrador de archivos - Por favor, especifique la ubicación del administrador de archivos que desea utilizar y añada argumentos si es necesario. El argumento por defecto es "%d", introduciendo una ruta en esa ubicación. Por ejemplo, si se requiere un comando como "totalcmd.exe /A c:\windows", el argumento es /A "%d". - "%f" es un argumento que representa la ruta del archivo. Se utiliza para especificar el nombre de archivo/carpeta al abrir una ubicación específica con administradores de archivos de terceros. Este argumento sólo está disponible en el elemento "Argumentos del archivo". Si el administrador de archivos no tiene esa función, puede utilizar "%d". + Especifique la ubicación del archivo del administrador de archivos que está utilizando y añada los argumentos necesarios. El argumento "%d" representa la ruta del directorio a abrir, utilizada por el campo Argumentos de la carpeta y por comandos que abren directorios específicos. El "%f" representa la ruta del archivo a abrir, utilizada por el campo Argumentos del archivo y por comandos que abren archivos específicos. + Por ejemplo, si el administrador de archivos utiliza un comando como "totalcmd.exe /A c:\windows" para abrir el directorio c:\windows, la ruta del administrador de archivos será totalcmd.exe, y los Argumentos de la carpeta serán /A "%d". Ciertos administradores de archivos como QTTabBar pueden requerir solo la ruta, en este caso utilice "%d" como la ruta del administrador de archivos y deje el resto de los campos en blanco. Administrador de archivos Nombre del perfil Ruta del administrador de archivos @@ -333,6 +368,10 @@ Si añade un prefijo "@" al introducir un acceso directo, éste coinci Cancelar Restablecer Eliminar + Aceptar + Si + No + Fondo Versión @@ -349,6 +388,9 @@ Si añade un prefijo "@" al introducir un acceso directo, éste coinci Informe enviado correctamente No se ha podido enviar el informe Flow Launcher ha tenido un error + Por favor, abra un nuevo tema en + 1. Subir archivo de registro: {0} + 2. Copiar el siguiente mensaje de excepción Por favor espere... @@ -417,4 +459,8 @@ Si añade un prefijo "@" al introducir un acceso directo, éste coinci sn Notas adhesivas + + Tamaño + Creado + Modificado diff --git a/Flow.Launcher/Languages/fr.xaml b/Flow.Launcher/Languages/fr.xaml index de3d849e9..d0d1d010d 100644 --- a/Flow.Launcher/Languages/fr.xaml +++ b/Flow.Launcher/Languages/fr.xaml @@ -1,7 +1,19 @@  + + + Flow a détecté que vous avez installé {0} plugins, qui nécessiteront {1} pour fonctionner. Souhaitez-vous télécharger {1} ? + {2}{2} + Cliquez sur non s'il est déjà installé, et vous serez invité à sélectionner le dossier qui contient l'exécutable {1} + + Veuillez sélectionner l'exécutable {0} + Impossible de définir {0} comme chemin d'accès vers l'exécutable. Veuillez essayer à partir des paramètres de Flow (défiler vers le bas). + Échec de l'initialisation des plugins + Plugins : {0} - n'ont pas pu être chargés et doivent être désactivés, veuillez contacter le créateur du plugin pour obtenir de l'aide + Échec lors de l'enregistrement du raccourci : {0} + Échec de la réinitialisation du raccourci "{0}". Veuillez réessayer ou consulter le journal pour plus de détails Flow Launcher Impossible de lancer {0} Le format de fichier n'est pas un plugin Flow Launcher valide @@ -33,6 +45,8 @@ Mode Portable Stocker tous les paramètres et données utilisateur dans un seul dossier (Pratique en cas d'utilisation de disques amovibles ou de services cloud). Lancer Flow Launcher au démarrage du système + Utilisez la tâche de connexion au lieu de l'entrée de démarrage pour une expérience de démarrage plus rapide + Après une désinstallation, vous devez supprimer manuellement cette tâche (Flow.Launcher Startup) via le planificateur de tâches Erreur lors de la configuration du lancement au démarrage Cacher Flow Launcher lors de la perte de focus Ne pas afficher le message de mise à jour pour les nouvelles versions @@ -54,6 +68,10 @@ Conserver la dernière recherche Sélectionner la dernière recherche Ne pas afficher la dernière recherche + Conserver le mot clé de la dernière action + Sélectionnez le mot clé de la dernière action + Hauteur de fenêtre fixe + La hauteur de la fenêtre n'est pas réglable par glissement. Résultats maximums à afficher Vous pouvez également ajuster ce paramètre en utilisant CTRL+Plus ou CTRL+Moins. Ignore les raccourcis lorsqu'une application est en plein écran @@ -71,10 +89,14 @@ Mettre à jour automatiquement Sélectionner Cacher Flow Launcher au démarrage + La fenêtre de recherche de Flow Launcher est cachée dans la barre d’état après le démarrage. Masquer l'icône de la barre des tâches Lorsque l'icône est cachée dans la barre de tâches, le menu Paramètres peut être ouvert en faisant un clic droit sur la barre de recherche. Précision de la recherche Modifie le score de correspondance minimum requis pour les résultats. + Aucun + Faible + Normal Devrait utiliser Pinyin Permet d'utiliser Pinyin pour la recherche. Pinyin est le système standard d'orthographe romanisée pour la traduction du chinois Toujours prévisualiser @@ -107,7 +129,8 @@ Version Site Web Désinstaller - + Échec de la suppression des paramètres du plugin + Plugins : {0} - Échec de la suppression des fichiers de configuration des plugins, veuillez les supprimer manuellement Magasin des Plugins @@ -124,8 +147,6 @@ Cette extension a été mis à jour au cours des 7 derniers jours Une nouvelle mise à jour est disponible - - Thèmes Apparence @@ -140,8 +161,13 @@ Lancez des programmes en tant qu'administrateur ou un utilisateur différent Tueur de processus Terminer les processus non désirés + Hauteur de la barre de recherche + Hauteur de l'objet Police (barre de recherche) - Police (liste des résultats) + Police du titre du résultat + Police des sous-titres du résultat + Réinitialiser + Personnaliser Mode fenêtré Opacité Le thème {0} n'existe pas, retour au thème par défaut @@ -156,7 +182,7 @@ Jouer un petit son lorsque la fenêtre de recherche s'ouvre Volume de l'effet sonore Ajuster le volume de l'effet sonore - 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 n'est pas disponible et est requis pour le réglage du volume de Flow. Veuillez vérifier votre installation si vous avez besoin de régler le volume. Animation Utiliser l'animation dans l'interface Vitesse d'animation @@ -167,6 +193,8 @@ Personnalisé Heure Date + Ce thème prend en charge deux modes (clair/sombre). + Ce thème prend en charge l'arrière-plan flou et transparent. Raccourcis @@ -190,6 +218,7 @@ Cycle de requête précédente Cycle de requête suivante Ouvrir le Menu Contextuel + Ouvrir le menu contextuel natif Ouvrir la Fenêtre des Réglages Copier le chemin du fichier Basculer le mode de jeu @@ -265,11 +294,17 @@ Effacer le journal Êtes-vous sûr de vouloir supprimer tous les journaux ? Assistant + Emplacement des données utilisateur + Les paramètres utilisateur et les plugins installés sont enregistrés dans le dossier des données utilisateur. Cet emplacement peut varier selon que vous soyez en mode portable ou non. + Ouvrir le dossier + Niveau de journalisation + Débogage + Info Sélectionner le gestionnaire de fichiers - Veuillez spécifier l'emplacement du fichier du gestionnaire de fichiers que vous utilisez et ajouter des arguments si nécessaire. Les arguments par défaut sont "%d", et un chemin est entré à cet endroit. Par exemple, si une commande est requise comme "totalcmd.exe /A c:\windows", l'argument est /A "%d". - "%f" est un argument qui représente le chemin du fichier. Il est utilisé pour souligner le nom du fichier/dossier lors de l'ouverture d'un emplacement de fichier spécifique dans le gestionnaire de fichiers tiers. Cet argument n'est disponible que dans l'élément "Arguments pour le fichier". Si le gestionnaire de fichiers n'a pas cette fonction, vous pouvez utiliser "%d". + Veuillez spécifier l'emplacement du fichier de l'explorateur de fichiers que vous utilisez et ajouter des arguments si nécessaire. Le "%d" représente le chemin du répertoire à ouvrir, utilisé par le champ Arg for Folder et pour les commandes ouvrant des répertoires spécifiques. Le "%f" représente le chemin du fichier à ouvrir, utilisé par le champ Arg for File et pour les commandes ouvrant des fichiers spécifiques. + Par exemple, si l'explorateur de fichiers utilise une commande telle que "totalcmd.exe /A c:\windows" pour ouvrir le répertoire c:\windows, le chemin de l'explorateur de fichiers sera totalcmd.exe et l'argument Arg For Folder sera /A "%d"". Certains explorateurs de fichiers comme QTTabBar peuvent simplement nécessiter qu'un chemin soit fourni, dans ce cas, utilisez "%d" comme chemin de l'explorateur de fichiers et laissez le reste des fichiers vides. Gestionnaire de fichiers Nom du profil Chemin du gestionnaire de fichiers @@ -295,7 +330,7 @@ Ancien mot-clé d'action Nouveau mot-clé d'action Annuler - Termin + Terminé Impossible de trouver le module spécifi Le nouveau mot-clé d'action doit être spécifi Le nouveau mot-clé d'action a été assigné à un autre module, veuillez en choisir un autre @@ -332,6 +367,10 @@ Si vous ajoutez un préfixe "@" lors de la saisie d'un raccourci, celu Annuler Réinitialiser Supprimer + Ok + Oui + Non + Arrière-plan Version @@ -348,6 +387,9 @@ Si vous ajoutez un préfixe "@" lors de la saisie d'un raccourci, celu Signalement envoy Échec de l'envoi du signalement Flow Launcher a rencontré une erreur + Veuillez ouvrir un nouveau ticket dans + 1. Télécharger le fichier journal : {0} + 2. Copiez le message d’exception ci-dessous Veuillez patienter... @@ -416,4 +458,8 @@ Si vous ajoutez un préfixe "@" lors de la saisie d'un raccourci, celu sn Pense-bêtes + + Taille du fichier + Créé + Dernière modification diff --git a/Flow.Launcher/Languages/he.xaml b/Flow.Launcher/Languages/he.xaml new file mode 100644 index 000000000..c912052f1 --- /dev/null +++ b/Flow.Launcher/Languages/he.xaml @@ -0,0 +1,466 @@ + + + + + Flow זיהה שהתקנת את התוסף {0}, אשר דורש את {1} כדי לפעול. האם תרצה להוריד את {1}? + {2}{2} + אם זה כבר מותקן, לחץ על 'לא' ותתבקש לבחור את התיקיה המכילה את קובץ ההפעלה {1} + + אנא בחר את קובץ ההפעלה {0} + לא ניתן להגדיר נתיב הפעלה {0}, אנא נסה שוב בהגדרות Flow (גלול עד למטה). + נכשל בהפעלת תוספים + תוספים: {0} - נכשלו בטעינה ויושבתו, אנא צור קשר עם יוצרי התוספים לקבלת עזרה + + + רישום מקש הקיצור "{0}" נכשל. ייתכן שמקש הקיצור נמצא בשימוש על ידי תוכנה אחרת. שנה למקש קיצור אחר, או צא מהתוכנה האחרת. + ביטול הרישום של מקש קיצור "{0}" נכשל. אנא נסה שוב או עיין ביומן הרישום לפרטים נוספים + Flow Launcher + לא ניתן היה להפעיל את {0} + פורמט קובץ תוסף Flow Launcher לא חוקי + הגדר כגבוה ביותר בשאילתה זו + בטל העלאה בשאילתה זו + בצע שאילתה: {0} + זמן ביצוע אחרון: {0} + פתח + הגדרות + אודות + יציאה + סגור + העתק + גזור + הדבק + בטל + בחר הכל + קובץ + תיקייה + טקסט + מצב משחק + השהה את השימוש במקשי קיצור. + איפוס מיקום + אפס את מיקום חלון החיפוש + + + הגדרות + כללי + מצב נייד + אחסן את כל ההגדרות ונתוני המשתמש בתיקייה אחת (שימושי בשימוש עם כוננים נשלפים או שירותי ענן). + הפעל את Flow Launcher בעת הפעלת Window + השתמש במשימת כניסה במקום בכניסה בעת האתחול, לחוויית הפעלה מהירה יותר + לאחר הסרת ההתקנה, עליך להסיר ידנית משימה זו (Flow.Launcher Startup) דרך מתזמן המשימות + שגיאה בהגדרת ההפעלה בעת הפעלת windows + הסתר את Flow Launcher כאשר הוא אינו החלון הפעיל + אל תציג התראות על גרסה חדשה + מיקום חלון החיפוש + זכור את המיקום האחרון + Monitor with Mouse Cursor + Monitor with Focused Window + צג ראשי + צג מותאם אישית + Search Window Position on Monitor + מרכז + מרכז עליון + שמאל עליון + ימין עליון + Custom Position + שפה + סגנון שאילתה אחרונה + הצג/הסתר תוצאות קודמות כאשר Flow Launcher מופעל מחדש. + שמור את השאילתה האחרונה + בחר שאילתא אחרונה + נקה שאילתא אחרונה + שמור מילת מפתח לפעולה האחרונה + בחר מילת מפתח לפעולה האחרונה + גובה חלון קבוע + גובה החלון אינו ניתן להתאמה באמצעות גרירה. + כמות תוצאות מרבית + ניתן גם להתאים במהירות באמצעות CTRL+פלוס ו-CTRL+מינוס. + התעלם מקיצורי מקשים במצב מסך מלא + השבת את הפעלת Flow Launcher כאשר יישום מסך מלא פעיל (מומלץ למשחקים). + מנהל הקבצים המוגדר כברירת מחדל + בחר את מנהל הקבצים לשימוש בעת פתיחת תיקיה. + דפדפן ברירת מחדל + הגדרה ללשונית חדשה, חלון חדש, מצב פרטי. + נתיב Python + נתיב Node.js + בחר את קובץ ההפעלה של Node.js + בחר את pythonw.exe + תמיד התחל להקליד במצב אנגלית + שנה זמנית את שיטת הקלט שלך למצב אנגלית בעת הפעלת Flow. + עדכון אוטומטי + בחר + הסתר את Flow Launcher בהפעלת המחשב + חלון החיפוש של Flow Launcher מוסתר במגש לאחר ההפעלה. + הסתר אייקון מגש + כאשר האייקון מוסתר מהמגש, ניתן לפתוח את תפריט ההגדרות על ידי לחיצה ימנית על חלון החיפוש. + דיוק חיפוש שאילתה + משנה את ציון ההתאמה המינימלי הנדרש לתוצאות. + ללא + נמוך + Regular + Search with Pinyin + Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese. + הצג תמיד תצוגה מקדימה + פתח תמיד את לוח התצוגה המקדימה כאשר Flow מופעל. הקש על {0} כדי להחליף את התצוגה המקדימה. + לא ניתן להחיל אפקט צל כאשר העיצוב הנוכחי מוגדר לאפקט טשטוש + + + חפש תוסף + Ctrl+F לחיפוש תוסף + לא נמצאו תוצאות + Please try a different search. + תוסף + תוספים + מצא תוספים נוספים + פועל + כבוי + הגדרת מילת מפתח לפעולה + מילת מפתח לפעולה + מילת מפתח נוכחית לפעולה + מילת מפתח חדשה לפעולה + שנה מילות מפתח לפעולה + עדיפות נוכחית + עדיפות חדשה + עדיפות + שנה עדיפות תוצאות תוסף + ספריית תוספים + מאת + זמן פתיחה: + זמן שאילתא: + גרסה + אתר + הסר התקנה + נכשל בהסרת הגדרות התוסף + תוספים: {0} - נכשל בהסרת קבצי הגדרות התוסף, יש להסירם ידנית + + + חנות תוספים + שחרור חדש + עודכן לאחרונה + תוספים + מותקן + רענן + התקן + הסר התקנה + עדכון + התוסף כבר מותקן + גרסה חדשה + תוסף זה עודכן במהלך 7 הימים האחרונים + עדכון חדש זמין + + + ערכת נושא + מראה + גלריית ערכות נושא + איך ליצור ערכת נושא + שלום + סייר + חפש קבצים, תיקיות ובתוכן הקבצים + חיפוש באינטרנט + Search the web with different search engine support + תוכנה + הפעל תוכנות כמנהל או כמשתמש אחר + ProcessKiller + הפסקת תהליכים לא רצויים + גובה סרגל החיפוש + גובה פריט + גופן תיבת שאילתות + גופן הכותרת לתוצאה + גופן כותרת המשנה לתוצאה + אפס + התאם אישית + מצב חלון + שקיפות + ערכת הנושא {0} אינה קיימת, חוזר לערכת ברירת המחדל + נכשל בטעינת העיצוב {0}, חוזר לערכת ברירת המחדל + תיקיית ערכת נושא + פתח תיקיית ערכת נושא + ערכת צבעים + ברירת המחדל של המערכת + בהיר + כהה + אפקט צליל + השמע צליל קטן כאשר חלון החיפוש נפתח + עוצמת אפקט הקול + התאם את עוצמת אפקט הקול + Windows Media Player אינו זמין, והוא נדרש להתאמת עוצמת הקול של Flow. אנא בדוק את ההתקנה שלך אם אתה צריך להתאים את עוצמת הקול. + אנימציה + השתמש באנימציה בממשק המשתמש + מהירות אנימציה + מהירות האנימציה של ממשק המשתמש + איטי + בינוני + מהיר + מותאם אישית + שעון + תאריך + ערכת נושא זאת תומך בשני מצבים (בהיר/כהה). + ערכת נושא זו תומכת בטשטוש רקע שקוף. + + + מקש קיצור + מקשי קיצור + פתח את Flow Launcher + הזן קיצור דרך להצגה/הסתרה של Flow Launcher. + הצג/הסתר תצוגה מקדימה + הזן קיצור דרך להצגה/הסתרה של התצוגה המקדימה בחלון החיפוש. + קיצורי דרך מוגדרים + רשימת קיצורי הדרך הרשומים כעת + מקש משני לפתיחת תוצאה + בחר מקש משני לפתיחת התוצאה שנבחרה דרך המקלדת. + הצג מקש קיצור + הצג מקש קיצור לבחירת תוצאה עם התוצאות. + השלמה אוטומטית + מבצע השלמה אוטומטית לפריטים שנבחרו. + בחר את הפריט הבא + בחר את הפריט הקודם + הדף הבא + הדף הקודם + עבור לשאילתה הקודמת + עבור לשאילתה הבאה + פתח תפריט הקשר + פתח תפריט הקשר מקומי + פתח חלון הגדרות + העתק את נתיב הקובץ + הפעל או כבה מצב משחק + הפעל או כבה היסטוריה + פתח תיקייה מכילה + הרץ כמנהל + רענן תוצאות חיפוש + טען מחדש נתוני תוספים + כוונון מהיר של רוחב החלון + כוונון מהיר של גובה החלון + השתמש כאשר יש צורך בטעינה מחדש של תוספים ובעדכון הנתונים שלהם. + באפשרותך להוסיף מקש קיצור נוסף לפעולה זו. + מקשי קיצור לשאילתות מותאמות אישית + קיצורי דרך לשאילתות מותאמות אישית + קיצורי דרך מובנים + שאילתה + קיצור דרך + הרחבה + תיאור + מחק + ערוך + הוסף + ללא + אנא בחר פריט + האם אתה בטוח שברצונך למחוק את מקש הקיצור של התוסף {0}? + האם אתה בטוח שברצונך למחוק את הקיצור: {0} עם ההרחבה {1}? + קבל טקסט מהלוח. + קבל נתיב מסייר הקבצים הפעיל. + אפקט צל לחלון השאילתה + לאפקט הצל יש שימוש ניכר ב-GPU. לא מומלץ אם ביצועי המחשב שלך מוגבלים. + רוחב החלון + ניתן גם להתאים במהירות באמצעות Ctrl+[ ו-Ctrl+] + השתמש ב-Segoe Fluent Icons + השתמש ב-Segoe Fluent Icons לתוצאות חיפוש כאשר נתמך + הקש על מקש + + + HTTP Proxy + הפעל HTTP Proxy + שרת HTTP + Port + שם משתמש + סיסמא + בדוק Proxy + שמור + שדה השרת לא יכול להיות ריק + שדה ה-Port לא יכול להיות ריק + פורמט ה-Port לא תקין + תצורת ה-Proxy נשמרה בהצלחה + ה-Proxy הוגדר בהצלחה + החיבור ל- Proxy נכשל + + + אודות + אתר אינטרנט + Github + תיעוד + גרסה + סמלים + הפעלת את Flow Launcher {0} פעמים + בדוק עדכונים + תן חסות + גרסה חדשה {0} זמינה, האם ברצונך להפעיל מחדש את Flow Launcher כדי להשתמש בעדכון? + בדיקת העדכונים נכשלה, אנא בדוק את הגדרות החיבור ואת הגדרות ה-Proxy שלך לכתובת api.github.com. + + הורדת העדכונים נכשלה, אנא בדוק את הגדרות החיבור ואת ה-proxy שלך אל github-cloud.s3.amazonaws.com, + או עבור אל https://github.com/Flow-Launcher/Flow.Launcher/releases כדי להוריד עדכונים באופן ידני. + + הערות שחרור + טיפים לשימוש + DevTools + תיקיית ההגדרות + תיקיית יומני רישום + נקה יומני רישום + האם אתה בטוח שברצונך למחוק את כל היומנים? + אשף + מיקום נתוני משתמש + הגדרות המשתמש והתוספים המותקנים נשמרים בתיקיית נתוני המשתמש. מיקום זה עשוי להשתנות אם התוכנה במצב נייד. + פתח תיקיה + Log Level + Debug + Info + + + בחר מנהל קבצים + אנא ציין את מיקום הקובץ של מנהל הקבצים שבו אתה משתמש והוסף ארגומנטים כנדרש. "%d" מייצג את נתיב התיקייה שיש לפתוח, ומשמש בשדה ארגומנט לתיקייה ובפקודות לפתיחת תיקיות מסוימות. "%f" מייצג את נתיב הקובץ שיש לפתוח, ומשמש בשדה ארגומנט לקובץ ובפקודות לפתיחת קבצים מסוימים. + לדוגמה, אם מנהל הקבצים משתמש בפקודה כגון "totalcmd.exe /A c:\windows" כדי לפתוח את התיקייה c:\windows, נתיב מנהל הקבצים יהיה totalcmd.exe, והארגומנט לתיקייה יהיה /A "%d". מנהלי קבצים מסוימים, כגון QTTabBar, עשויים לדרוש רק ציון נתיב, במקרה כזה השתמש ב-"%d" כנתיב מנהל הקבצים והשאר את שאר השדות ריקים. + מנהל קבצים + שם פרופיל + נתיב מנהל קבצים + ארגומנט לתיקייה + ארגומנט לקובץ + + + דפדפן ברירת מחדל + ההגדרה המוגדרת כברירת מחדל עוקבת אחר הדפדפן המוגדר כברירת מחדל במערכת ההפעלה. אם צוין דפדפן אחר, Flow Launcher ישתמש בו. + דפדפן + שם דפדפן + נתיב דפדפן + חלון חדש + כרטיסייה חדשה + מצב פרטיות + + + שנה עדיפות + ככל שהמספר גבוה יותר, התוצאה תדורג גבוה יותר ברשימת החיפוש. נסה להגדיר את הערך ל-5. אם ברצונך שהתוצאות יופיעו אחרי תוצאות של כל תוסף אחר, הזן מספר שלילי. + אנא הזן מספר שלם תקף עבור העדיפות! + + + מילת פעולה ישנה + מילת פעולה חדשה + ביטול + בוצע + לא ניתן למצוא את התוסף שצוין + מילת הפעולה החדשה לא יכולה להיות ריקה + מילת הפעולה החדשה כבר מוקצה לתוסף אחר, אנא בחר אחת שונה + הצליח + הושלם בהצלחה + הזן את מילת הפעולה שברצונך להשתמש בה להפעלת התוסף. השתמש ב-* אם אינך רוצה לציין מילה כלשהי, והתוסף יופעל ללא צורך במילת פעולה. + + + מקש קיצור לשאילתה מותאמת אישית + הקש על מקש קיצור מותאם אישית כדי לפתוח את Flow Launcher ולהזין את השאילתה שצוינה באופן אוטומטי. + תצוגה מקדימה + מקש הקיצור אינו זמין, אנא בחר מקש קיצור חדש + מקש קיצור לא חוקי לתוסף + עדכון + שיוך מקש קיצור + מקש הקיצור הנוכחי אינו זמין. + מקש קיצור זה שמור עבור "{0}" ואינו ניתן לשימוש. אנא בחר מקש קיצור אחר. + מקש קיצור זה כבר נמצא בשימוש על ידי "{0}". אם תלחץ על "החלף", הוא יוסר מ-"{0}". + הקש על המקשים שברצונך להשתמש בהם עבור פעולה זו. + + + קיצור דרך לשאילתה מותאמת אישית + הזן קיצור דרך שיוחלף אוטומטית בשאילתה שצוינה. + קיצור דרך מוחלף כאשר הוא תואם בדיוק לשאילתה. + +אם תוסיף תחילית '@' בעת הזנת קיצור דרך, הוא יתאים לכל מיקום בשאילתה. קיצורי דרך מובנים מתאימים לכל מיקום בשאילתה. + + קיצור דרך כבר קיים, אנא הזן קיצור דרך חדש או ערוך את הקיים. + קיצור הדרך ו/או ההרחבה שלו ריקים. + + + שמור + שכתב + ביטול + אפס + מחק + אישור + כן + לא + רקע + + + גרסה + זמן + אנא תאר כיצד האפליקציה קרסה כדי שנוכל לתקן זאת + שלח דיווח + ביטול + כללי + חריגים + סוג החריגה + מקור + מעקב מחסנית + שולח + הדוח נשלח בהצלחה + שליחת הדוח נכשלה + אירעה שגיאה ב-Flow Launcher + אנא פתח דיווח חדש ב + 1. העלה קובץ יומן: {0} + 2. העתק את הודעת החריגה למטה + + + אנא המתן... + + + בודק עדכון חדש + כבר מותקנת אצלך הגרסה העדכנית של Flow Launcher + עדכון נמצא + מעדכן... + + Flow Launcher לא הצליח להעביר את נתוני פרופיל המשתמש שלך לגרסת העדכון החדשה. + אנא העבר ידנית את תיקיית נתוני הפרופיל שלך מ-{0} אל-{1} + + עדכון חדש + גרסה חדשה {0} של Flow Launcher זמינה כעת + אירעה שגיאה במהלך ניסיון התקנת עדכוני התוכנה + עדכון + ביטול + העדכון נכשל + בדוק את החיבור שלך ונסה לעדכן את הגדרות הפרוקסי ל-github-cloud.s3.amazonaws.com. + שדרוג זה יאתחל את Flow Launcher + הקבצים הבאים יעודכנו + עדכן קבצים + עדכן תיאור + + + דלג + ברוך הבא אל Flow Launcher + שלום, זו הפעם הראשונה שבה Flow Launcher מופעל! + לפני שתתחיל, אשף זה יסייע בהגדרת Flow Launcher. אתה יכול לדלג על שלב זה. בחר שפה + חפש והפעל את כל הקבצים והיישומים במחשב שלך + חפש הכל מיישומים, קבצים, סימניות, YouTube, ועד טוויטר ועוד. הכל מהנוחות של המקלדת מבלי לגעת בעכבר. + ניתן להפעיל את Flow Launcherבקיצור המקש שלמטה, קדימה נסה אותו כעת! כדי לשנות אותו, לחץ על מקש הקיצור הרצוי במקלדת. + מקשי קיצור + מילת מפתח ופקודות פעולה + חפש באינטרנט, הפעל אפליקציות או הפעל פונקציות שונות באמצעות תוספים של Flow Launcher. פונקציות מסוימות מתחילות במילת מפתח פעולה, ובמידת הצורך, ניתן להשתמש בהן ללא מילות מפתח פעולה. נסה את השאילתות למטה ב-Flow Launcher. + בואו נתחיל עם Flow Launcher + סיימנו. תהנה מ-Flow Launcher. אל תשכח את מקש הקיצור כדי להתחיל :) + + + + חזור / תפריט הקשר + ניווט בין פריטים + פתח תפריט הקשר + פתח תיקייה מכילה + הפעל כמנהל / פתח תיקייה במנהל הקבצים ברירת מחדל + היסטוריית שאילתות + חזור לתוצאה בתפריט הקשר + השלמה אוטומטית + פתח / הפעל פריט נבחר + פתח חלון הגדרות + טען מחדש נתוני תוסף + + בחר בתוצאה הראשונה + בחר בתוצאה האחרונה + הפעל מחדש את השאילתה הנוכחית + פתח תוצאה + פתח תוצאה #{0} + + מזג אוויר + מזג אוויר מתוצאות Google + > ping 8.8.8.8 + פקודת Shell + s Bluetooth + Bluetooth בהגדרות Windows + sn + פתקים נדבקים + + + גודל קובץ + נוצר + תאריך שינוי אחרון + diff --git a/Flow.Launcher/Languages/it.xaml b/Flow.Launcher/Languages/it.xaml index 6d7a42f45..e4d4d3e2c 100644 --- a/Flow.Launcher/Languages/it.xaml +++ b/Flow.Launcher/Languages/it.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + Registrazione del tasto di scelta rapida "{0}" non riuscita. Il tasto di scelta rapida potrebbe essere in uso da un altro programma. Passa a un altro tasto di scelta rapida o esci da un altro programma. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Avvio fallito {0} Formato file plugin non valido @@ -33,6 +45,8 @@ Modalità portatile Memorizzare tutte le impostazioni e i dati dell'utente in un'unica cartella (utile se utilizzato con unità rimovibili o servizi cloud). Avvia Wow all'avvio di Windows + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Errore nell'impostazione del lancio all'avvio Nascondi Flow Launcher quando perde il focus Non mostrare le notifiche per una nuova versione @@ -54,6 +68,10 @@ Conserva ultima ricerca Seleziona ultima ricerca Cancella ultima ricerca + Preserve Last Action Keyword + Select Last Action Keyword + Altezza Finestra Fissa + L'altezza della finestra non si può regolare trascinando. Numero massimo di risultati mostrati È anche possibile regolarlo rapidamente utilizzando CTRL+Più e CTRL+Meno. Ignora i tasti di scelta rapida in applicazione a schermo pieno @@ -71,10 +89,14 @@ Aggiornamento automatico Seleziona Nascondi Flow Launcher all'avvio + La finestra di ricerca di Flow Launcher è nascosta nelle applicazioni nascoste dopo l'avvio. Nascondi Icona nell'Area di Notifica Quando l'icona è nascosta dal menu delle icone nascoste, il menu Impostazioni può essere aperto facendo clic con il tasto destro del mouse sulla finestra di ricerca. Precisione di ricerca delle query Modifica il punteggio minimo richiesto per i risultati. + Vuoto + Bassa + Normale Dovrebbe usare il Pinyin Consente di utilizzare il Pinyin per la ricerca. Il Pinyin è il sistema standard di ortografia romanizzata per la traduzione del cinese. Mostra Sempre Anteprima @@ -107,7 +129,8 @@ Versione Sito Web Disinstalla - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Negozio dei Plugin @@ -124,8 +147,6 @@ Questo plugin è stato aggiornato negli ultimi 7 giorni Nuovo aggiornamento disponibile - - Tema Aspetto @@ -140,8 +161,13 @@ Avvia programmi come amministratore o un altro utente ProcessKiller Termina i processi indesiderati + Altezza Barra Di Ricerca + Altezza Oggetto Font campo di ricerca - Font campo risultati + Font del Titolo del Risultato + Font del Sottotitolo del Risultato + Resetta + Personalizza Modalità finestra Opacità Il tema {0} non esiste, si ritorna al tema predefinito @@ -156,7 +182,7 @@ Riproduce un piccolo suono all'apertura della finestra di ricerca Volume Effetti Sonori Regola il volume degli effetti sonori - 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 non è disponibile ed è richiesto per regolare il volume da Flow. Si prega di controllare l'installazione per regolare il volume. Animazione Usa l'animazione nell'interfaccia utente Velocità di animazione @@ -167,6 +193,8 @@ Personalizzato Orologio Data + Questo tema supporta due (chiaro/scuro) varianti. + Questo tema supporta lo sfondo trasparente blurrato. Tasti scelta rapida @@ -187,9 +215,10 @@ Seleziona Elemento Precedente Pagina Successiva Pagina Precedente - Cycle Previous Query - Cycle Next Query + Cicla Query Precedente + Cicla Query Successiva Apri il menu di scelta rapida + Apri il Menu Contestuale Nativo Aprire la finestra delle impostazioni Copia Percorso File Attiva/Disattiva Modalità Di Gioco @@ -266,11 +295,17 @@ Cancella i log Sei sicuro di voler cancellare tutti i log? Wizard + Posizione Dati Utente + Le impostazioni dell'utente e i plugin installati sono salvati nella cartella dati utente. Questa posizione può variare se è in modalità portable o no. + Apri Cartella + Log Level + Debug + Info Seleziona Gestore File - Specificare la posizione del gestore file che si sta utilizzando e aggiungere argomenti se necessario. Gli argomenti di default sono "%d", e un percorso è inserito in quella posizione. Per esempio, se è richiesto un comando come "totalcmd.exe /A c:\windows", l'argomento è /A "%d". - "%f" è un argomento che rappresenta il percorso del file. Viene usato per sottolineare il nome del file/cartella quando si apre una posizione specifica del file in file manager di terze parti. Questo argomento è disponibile solo nell'elemento "Arg per File". Se il file manager non dispone di tale funzione, è possibile utilizzare "%d". + 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 fileds blank. Gestore File Nome Profilo Percorso Gestore File @@ -333,6 +368,10 @@ Se si aggiunge un prefisso '@' mentre si inserisce una scorciatoia, corrisponde Annulla Resetta Cancella + OK + + No + Sfondo Versione @@ -349,6 +388,9 @@ Se si aggiunge un prefisso '@' mentre si inserisce una scorciatoia, corrisponde Rapporto inviato correttamente Invio rapporto fallito Flow Launcher ha riportato un errore + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Attendere prego... @@ -417,4 +459,8 @@ Se si aggiunge un prefisso '@' mentre si inserisce una scorciatoia, corrisponde sn Sticky Notes + + Dimensione File + Creato + Ultima modifica diff --git a/Flow.Launcher/Languages/ja.xaml b/Flow.Launcher/Languages/ja.xaml index 018be0d59..28c334667 100644 --- a/Flow.Launcher/Languages/ja.xaml +++ b/Flow.Launcher/Languages/ja.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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}" の登録に失敗しました。このホットキーは別のプログラムで使用されている可能性があります。別のホットキーに変更するか、このホットキーを使用しているプログラムを終了してください。 + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher {0}の起動に失敗しました Flow Launcherプラグインの形式が正しくありません @@ -33,6 +45,8 @@ ポータブルモード すべての設定とユーザーデータを1つのフォルダに保存します(リムーバブルドライブやクラウドサービスで使用する場合に便利です)。 スタートアップ時にFlow Launcherを起動する + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup フォーカスを失った時にFlow Launcherを隠す 最新版が入手可能であっても、アップグレードメッセージを表示しない @@ -54,6 +68,10 @@ 前回のクエリを保存 前回のクエリを選択 前回のクエリを消去 + Preserve Last Action Keyword + Select Last Action Keyword + Fixed Window Height + The window height is not adjustable by dragging. 結果の最大表示件数 CTRL+PlusとCTRL+Minusを使用すれば、簡単に調整することもできます。 ウィンドウがフルスクリーン時にホットキーを無効にする @@ -71,10 +89,14 @@ 自動更新 選択 起動時にFlow Launcherを隠す + Flow Launcher search window is hidden in the tray after starting up. トレイアイコンを隠す トレイアイコンが非表示になっているときは、検索ウィンドウを右クリックすることで設定メニューを開くことができます。 クエリ検索精度 表示する結果に必要な一致スコアの最小値を変更します。 + None + Low + Regular ピンインによる検索 ピンインを使用して検索できるようにします。ピンインは、中国語をローマ字表記するための標準的な表記体系です。 常にプレビューする @@ -107,7 +129,8 @@ バージョン ウェブサイト アンインストール - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually プラグインストア @@ -124,8 +147,6 @@ This plugin has been updated within the last 7 days 新しいアップデートが利用可能です - - テーマ 外観 @@ -140,8 +161,13 @@ 管理者または別のユーザーとしてプログラムを起動します プロセスキラー 不要なプロセスを終了します + Search Bar Height + Item Height 検索ボックスのフォント - 検索結果一覧のフォント + Result Title Font + Result Subtitle Font + Reset + Customize ウィンドウモード 透過度 テーマ {0} が存在しません、デフォルトのテーマに戻します。 @@ -167,6 +193,8 @@ カスタム 時刻 日付 + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. ホットキー @@ -190,6 +218,7 @@ Cycle Previous Query Cycle Next Query Open Context Menu + Open Native Context Menu Open Setting Window Copy File Path Toggle Game Mode @@ -266,11 +295,17 @@ Clear Logs Are you sure you want to delete all logs? Wizard + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". + 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 fileds blank. File Manager Profile Name File Manager Path @@ -333,6 +368,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Reset 削除 + Update + Yes + No + バックグラウンド バージョン @@ -349,6 +388,9 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in クラッシュレポートの送信に成功しました クラッシュレポートの送信に失敗しました Flow Launcherにエラーが発生しました + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Please wait... @@ -417,4 +459,8 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in sn Sticky Notes + + File Size + Created + Last Modified diff --git a/Flow.Launcher/Languages/ko.xaml b/Flow.Launcher/Languages/ko.xaml index c3bb574b9..d9da26bcb 100644 --- a/Flow.Launcher/Languages/ko.xaml +++ b/Flow.Launcher/Languages/ko.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher {0}을 실행할 수 없습니다. Flow Launcher 플러그인 파일 형식이 유효하지 않습니다. @@ -33,27 +45,33 @@ 포터블 모드 모든 설정이 폴더안에 들어갑니다. USB 드라이브나 클라우드로 사용 가능합니다. 시스템 시작 시 Flow Launcher 실행 + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup 포커스 잃으면 Flow Launcher 숨김 새 버전 알림 끄기 검색 창 위치 - Remember Last Position - Monitor with Mouse Cursor - Monitor with Focused Window - Primary Monitor - Custom Monitor - Search Window Position on Monitor - Center - Center Top - Left Top - Right Top - Custom Position + 마지막 위치 기억 + 마우스 위치 모니터 + 선택된 창 위치 모니터 + 주 모니터 + 사용자 지정 모니터 + 모니터 내 검색창 위치 + 중앙 + 중앙 상단 + 좌측 상단 + 우측 상단 + 사용자 지정 위치 언어 마지막 쿼리 스타일 쿼리박스를 열었을 때 쿼리 처리 방식 직전 쿼리에 계속 입력 직전 쿼리 내용 선택 직전 쿼리 지우기 + Preserve Last Action Keyword + Select Last Action Keyword + 창 높이 고정 + 드래그로 창 높이를 조정하지 않습니다. 표시할 결과 수 Ctrl + '+'키와 Ctrl + '-'키로도 빠르게 조정할 수 있습니다 전체화면 모드에서는 단축키 무시 @@ -62,8 +80,8 @@ 폴더를 열 때 사용할 파일관리자를 선택하세요. 기본 웹 브라우저 새 탭, 새 창, 사생활 보호 모드 - Python Path - Node.js Path + Python 경로 + Node.js 경로 Please select the Node.js executable Please select pythonw.exe 항상 영어입력 모드에서 시작 @@ -71,10 +89,14 @@ 자동 업데이트 선택 시작 시 Flow Launcher 숨김 + Flow Launcher search window is hidden in the tray after starting up. 트레이 아이콘 숨기기 트레이에서 아이콘을 숨길 경우, 검색창 우클릭으로 설정창을 열 수 있습니다. 쿼리 검색 정밀도 검색 결과에 필요한 최소 매치 점수를 변경합니다. + 없음 + 낮음 + 보통 항상 Pinyin 사용 Pinyin을 사용하여 검색할 수 있습니다. Pinyin (병음) 은 로마자 중국어 입력 방식입니다. 항상 미리보기 @@ -107,7 +129,8 @@ 버전 웹사이트 제거 - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually 플러그인 스토어 @@ -124,8 +147,6 @@ 이 플러그인은 최근 7일 사이 업데이트 되었습니다 새 업데이트 설치 가능 - - 테마 외관 @@ -140,8 +161,13 @@ Launch programs as admin or a different user ProcessKiller Terminate unwanted processes + 검색창 높이 + 결과 항목 높이 쿼리 상자 글꼴 - 결과 항목 글꼴 + 결과 제목 글꼴 + 결과 부제목 글꼴 + 초기화 + 사용자 지정 윈도우 모드 투명도 {0} 테마가 존재하지 않습니다. 기본 테마로 변경합니다. @@ -154,54 +180,57 @@ 어둡게 소리 효과 검색창을 열 때 작은 소리를 재생합니다. - Sound Effect Volume - Adjust the volume of the sound effect - Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume. + 효과음 크기 + 소리 효과의 음량을 조절합니다. + 사운드 볼륨 조절에 필요한 Windows Media Player가 설치되어 있지 않습니다. 볼륨 조절 기능이 필요한 경우 설치 여부를 확인하세요. 애니메이션 일부 UI에 애니메이션을 사용합니다. - Animation Speed - The speed of the UI animation - Slow - Medium - Fast - Custom + 애니메이션 속도 + UI 애니메이션의 속도 + 느림 + 보통 + 빠름 + 사용자 정의 시계 날짜 + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. 단축키 단축키 - Open Flow Launcher + Flow Launcher 열기 Flow Launcher를 열 때 사용할 단축키를 입력하세요. - Toggle Preview + 미리보기 전환 미리보기 패널을 켜고 끌 때 사용할 단축키를 입력하세요. - Hotkey Presets + 단축키 프리셋 List of currently registered hotkeys 결과 선택 단축키 결과 항목을 선택하는 단축키입니다. 단축키 표시 결과창에서 결과 선택 단축키를 표시합니다. - Auto Complete + 자동 완성 Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page - Cycle Previous Query - Cycle Next Query + 다음 항목 선택 + 이전 항목 선택 + 다음 페이지 + 이전 페이지 + 이전 쿼리로 전환 + 다음 쿼리로 전환 콘텍스트 메뉴 열기 + Open Native Context Menu 설정창 열기 - Copy File Path - Toggle Game Mode - Toggle History + 파일 경로 복사 + 게임 모드 전환 + 기록창 켜기/끄기 포함된 폴더 열기 - Run As Admin + 관리자로 실행 Refresh Search Results - Reload Plugins Data - Quick Adjust Window Width - Quick Adjust Window Height + 플러그인 데이터 새로고침 + 빠른 창 넓이 조정 + 빠른 창 높이 조정 Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. + 단축키를 하나 더 추가할 수 있습니다. 사용자지정 쿼리 단축키 사용자 지정 쿼리 단축어 내장 단축어 @@ -212,7 +241,7 @@ 삭제 편집 추가 - None + 없음 항목을 선택하세요. {0} 플러그인 단축키를 삭제하시겠습니까? Are you sure you want to delete shortcut: {0} with expansion {1}? @@ -266,11 +295,17 @@ 로그 삭제 정말 모든 로그를 삭제하시겠습니까? 마법사 + 사용자 데이터 위치 + 사용자 설정과 설치된 플러그인은 사용자 데이터 폴더에 저장됩니다. 이 위치는 휴대용 모드 활성화 여부에 따라 달라질 수 있습니다. + 폴더 열기 + Log Level + Debug + Info 파일관리자 선택 - 사용하려는 파일관리자를 선택하고 필요한 경우 인수를 추가하세요. 기본 인수는 "%d" 이며 해당 위치에 경로가 입력됩니다. 예를들어 "totalcmd.exe /A c:\windows"와 같은 명령이 필요한 경우, 인수는 /A "%d" 입니다. - "%f"는 특정 파일의 경로를 나타냅니다. 파일관리자에서 선택한 파일/폴더의 위치를 강조하는 기능에서 사용됩니다. 이 인수는 "파일경로 인수" 항목에서만 사용할 수 있습니다. 파일관리자에 해당 기능이 없거나 잘 모를 경우 "%d" 인수를 사용할 수 있습니다. + 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 fileds blank. 파일관리자 프로필 이름 파일관리자 경로 @@ -278,7 +313,7 @@ 파일경로 인수 - 기본 웹 브라우저r + 기본 웹 브라우저 기본 설정은 OS의 기본 브라우저 설정을 따릅니다. 특정 브라우저를 지정할 경우, Flow는 해당 브라우저를 사용합니다. 브라우저 브라우저 이름 @@ -333,6 +368,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in 취소 Reset 삭제 + 확인 + Yes + No + 배경 버전 @@ -349,6 +388,9 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in 보고서를 정상적으로 보냈습니다. 보고서를 보내지 못했습니다. Flow Launcher에 문제가 발생했습니다. + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message 잠시 기다려주세요... @@ -404,7 +446,7 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Select first result Select last result - Run current query again + 현재 쿼리 재실행 Open result Open result #{0} @@ -417,4 +459,8 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in 스메 스티커 메모 + + 파일 크기 + 만든 날짜 + 수정한 날짜 diff --git a/Flow.Launcher/Languages/nb.xaml b/Flow.Launcher/Languages/nb.xaml index 5a361d478..a37a204e1 100644 --- a/Flow.Launcher/Languages/nb.xaml +++ b/Flow.Launcher/Languages/nb.xaml @@ -1,420 +1,466 @@  + + + Flow oppdaget at du har installert {0} programtillegg, noe som krever {1} for å kjøre. Vil du laste ned {1}? + {2}{2} + Klikk nei hvis det allerede er installert, og du vil bli bedt om å velge mappen som inneholder {1} kjørbar fil + + Velg den kjørbare filen for {0} + Kan ikke angi {0} kjørbar bane, prøv fra Flows innstillinger (bla ned til bunnen). + Mislykkes i å initialisere programtillegg + Programtillegg: {0} - mislykkes å lastes inn og vil bli deaktivert, vennligst kontakt utvikleren av programtillegget for hjelp + - Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Kan ikke registrere hurtigtasten "{0}". Hurtigtasten kan være i bruk av et annet program. Endre til en annen hurtigtast, eller avslutt et annet program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher - Could not start {0} - Invalid Flow Launcher plugin file format - Set as topmost in this query - Cancel topmost in this query - Execute query: {0} - Last execution time: {0} - Open - Settings - About - Exit - Close - Copy - Cut - Paste - Undo - Select All - File - Folder - Text - Game Mode - Suspend the use of Hotkeys. - Position Reset - Reset search window position + Kunne ikke starte {0} + Ugyldig Flow Launcher programtillegg filformat + Sett som høyest i denne spørringen + Kanseller høyest i denne spørringen + Kjør spørring: {0} + Siste utførelsestid: {0} + Åpne + Innstillinger + Om + Avslutt + Lukk + Kopier + Klipp ut + Lim inn + Angre + Velg alle + Fil + Mappe + Tekst + Spillmodus + Stopp bruken av hurtigtaster. + Tilbakestilling av posisjon + Tilbakestill posisjonen til søkevinduet - Settings - General - Portable Mode - Store all settings and user data in one folder (Useful when used with removable drives or cloud services). - Start Flow Launcher on system startup - Error setting launch on startup - Hide Flow Launcher when focus is lost - Do not show new version notifications - Search Window Position - Remember Last Position - Monitor with Mouse Cursor - Monitor with Focused Window - Primary Monitor - Custom Monitor - Search Window Position on Monitor - Center - Center Top - Left Top - Right Top - Custom Position - Language - Last Query Style - Show/Hide previous results when Flow Launcher is reactivated. - Preserve Last Query - Select last Query - Empty last Query - Maximum results shown - You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. - Ignore hotkeys in fullscreen mode - Disable Flow Launcher activation when a full screen application is active (Recommended for games). - Default File Manager - Select the file manager to use when opening the folder. - Default Web Browser - Setting for New Tab, New Window, Private Mode. - Python Path - Node.js Path - Please select the Node.js executable - Please select pythonw.exe - Always Start Typing in English Mode - Temporarily change your input method to English mode when activating Flow. - Auto Update - Select - Hide Flow Launcher on startup - Hide tray icon - When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. - Query Search Precision - Changes minimum match score required for results. - Search with Pinyin - Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese. - Always Preview - Always open preview panel when Flow activates. Press {0} to toggle preview. - Shadow effect is not allowed while current theme has blur effect enabled + Innstillinger + Generelt + Portabel modus + Lagre alle innstillinger og brukerdata i en mappe (nyttig når man bruker flyttbare stasjoner eller skytjenester). + Start Flow Launcher ved systemoppstart + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler + Feil ved å sette kjør ved oppstart + Skjul Flow Launcher når fokus forsvinner + Ikke vis varsler om nye versjoner + Posisjon til søkevindu + Husk siste posisjon + Skjerm med musepekeren + Skjerm med fokusert vindu + Primær skjerm + Tilpasset skjerm + Søkevindusposisjon på skjermen + Midten + Midtstill toppen + Øverst til venstre + Øverst til høyre + Egendefinert posisjon + Språk + Siste stil for spørring + Vis/Skjul forrige resultater når Flow Launcher aktiveres på nytt. + Bevar siste spørring + Velg siste spørring + Tøm siste spørring + Preserve Last Action Keyword + Select Last Action Keyword + Fast vindushøyde + Vindushøyden kan ikke justeres ved å dra. + Maksimalt antall resultater vist + Du kan også raskt justere dette ved å bruke CTRL+Plus og CTRL+Minus. + Ignorer hurtigtaster i fullskjermmodus + Deaktiver Flow Launcher aktivering når et fullskjermprogram er aktivt (anbefalt for spill). + Standard filbehandler + Velg filbehandleren du vil bruke når du åpner mappen. + Standard nettleser + Innstilling for ny fane, nytt vindu, privat modus. + Python-sti + Node.js-sti + Vennligst velg Node.js-kjørbar fil + Vennligst velg pythonw.exe + Begynn alltid å skrive i engelsk modus + Bytt inndatametode midlertidig til engelsk modus når du aktiverer Flow. + Automatisk oppdatering + Velg + Skjul Flow Launcher ved oppstart + Søkevinduet for Flow Launcher er skjult i systemstatusfeltet etter oppstart. + Skjul systemstatusfeltikon + Når ikonet er skjult fra systemstatusfeltet, kan Innstillinger-menyen åpnes ved å høyreklikke på søkevinduet. + Presisjon for spørringssøk + Endrer minimum samsvarsresultat som kreves for resultater. + Ingen + Lav + Vanlig + Søk med Pinyin + Tillater bruk av Pinyin for å søke. Pinyin er standardsystemet for romanisert stavemåte for å oversette kinesisk. + Alltid forhåndsvisning + Åpne alltid forhåndsvisningspanel når Flow aktiveres. Trykk på {0} for å velge forhåndsvisning. + Skyggeeffekt er ikke tillatt mens gjeldende tema har uskarphet-effekt aktivert - Search Plugin - Ctrl+F to search plugins - No results found - Please try a different search. - Plugin - Plugins - Find more plugins - On - Off - Action keyword Setting - Action keyword - Current action keyword - New action keyword - Change Action Keywords - Current Priority - New Priority - Priority - Change Plugin Results Priority - Plugin Directory - by - Init time: - Query time: - Version - Website - Uninstall - + Søk etter programtillegg + Ctrl+F for å søke programtillegg + Fant ingen resultater + Vennligst prøv et annet søk. + Programtillegg + Programtillegg + Finn flere programtillegg + + Av + Innstilling av handlingsnøkkelord + Handlingsnøkkelord + Nåværende handlingsnøkkelord + Nytt handlingsnøkkelord + Endre handlingsnøkkelord + Gjeldende prioritet + Ny prioritet + Prioritet + Endre prioritet for programtilleggresultater + Mappe for programtillegg + av + Innbygd tid: + Spørringstid: + Versjon + Nettsted + Avinstaller + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually - Plugin Store - New Release - Recently Updated - Plugins - Installed - Refresh - Install - Uninstall - Update - Plugin already installed - New Version - This plugin has been updated within the last 7 days - New Update is Available - - + Programtillegg butikk + Ny utgivelse + Nylig oppdatert + Programtillegg + Installert + Oppfrisk + Installer + Avinstaller + Oppdater + Programtillegg er allerede installert + Ny versjon + Dette programtillegget er oppdatert i løpet av de siste 7 dagene + Ny oppdatering er tilgjengelig - Theme - Appearance - Theme Gallery - How to create a theme - Hi There - Explorer - Search for files, folders and file contents - WebSearch - Search the web with different search engine support + Drakt + Utseende + Tema Galleri + Hvordan lage et tema + Hei der + Utforsker + Søk etter filer, mapper og filinnhold + Nettsøk + Søk på nettet med forskjellig støtte for søkemotorer Program - Launch programs as admin or a different user - ProcessKiller - Terminate unwanted processes - Query Box Font - Result Item Font - Window Mode - Opacity - Theme {0} not exists, fallback to default theme - Fail to load theme {0}, fallback to default theme - Theme Folder - Open Theme Folder - Color Scheme - System Default - Light - Dark - Sound Effect - Play a small sound when the search window opens - Sound Effect Volume - Adjust the volume of the sound effect - Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume. - Animation - Use Animation in UI - Animation Speed - The speed of the UI animation - Slow - Medium - Fast - Custom - Clock - Date + Start programmer som administrator eller en annen bruker + ProsessTerminerer + Avslutt uønskede prosesser + Søkefeltets høyde + Elementhøyde + Spørreboks skrift + Skrift for resultattittel + Skrift for resultatundertittel + Tilbakestill + Tilpass + Vindumodus + Ugjennomsiktighet + Tema {0} finnes ikke, tilbakestilling til standardtema + Kunne ikke laste inn tema {0}, tilbakestilling til standardtema + Tema-mappe + Åpne temamappen + Fargevalg + Systemstandard + Lys + Mørk + Lydeeffekt + Spill av en liten lyd når søkevinduet åpnes + Lydeffektvolum + Juster volumet til lydeffekten + Windows Media Player er ikke tilgjengelig og er nødvendig for Flows volumjustering. Vennligst sjekk installasjonen din hvis du trenger å justere volumet. + Animasjon + Bruk animasjon i brukergrensesnitt + Animasjonshastighet + Hastigheten på grensesnittanimasjonen + Sakte + Middels + Rask + Egendefinert + Klokke + Dato + Dette temaet støtter to (lys/mørk) moduser. + Dette temaet støtter uskarp gjennomsiktig bakgrunn. - Hotkey - Hotkeys - Open Flow Launcher - Enter shortcut to show/hide Flow Launcher. - Toggle Preview - Enter shortcut to show/hide preview in search window. - Hotkey Presets - List of currently registered hotkeys - Open Result Modifier Key - Select a modifier key to open selected result via keyboard. - Show Hotkey - Show result selection hotkey with results. - Auto Complete - Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page - Cycle Previous Query - Cycle Next Query - Open Context Menu - Open Setting Window - Copy File Path - Toggle Game Mode - Toggle History - Open Containing Folder - Run As Admin - Refresh Search Results - Reload Plugins Data - Quick Adjust Window Width - Quick Adjust Window Height - Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. - Custom Query Hotkeys - Custom Query Shortcuts - Built-in Shortcuts - Query - Shortcut - Expansion - Description - Delete - Edit - Add - None - Please select an item - Are you sure you want to delete {0} plugin hotkey? - Are you sure you want to delete shortcut: {0} with expansion {1}? - Get text from clipboard. - Get path from active explorer. - Query window shadow effect - 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+]. - Use Segoe Fluent Icons - Use Segoe Fluent Icons for query results where supported - Press Key + Hurtigtast + Hurtigtaster + Åpne Flow Launcher + Skriv inn snarvei til å vise/skjule Flow Launcher. + Vis/skjul forhåndsvisning + Skriv inn snarvei til å vise/skjule forhåndsvisning i søkevinduet. + Hurtigtast forhåndsinnstillinger + Liste over registrerte hurtigtaster + Åpne resultatendringstast + Velg en modifikasjonstast for å åpne valgt resultat via tastatur. + Vis hurtigtast + Vis resultatvalg hurtigtast med resultater. + Automatisk fullføring + Kjører autofullføring for de valgte elementene. + Velg neste element + Velg forrige element + Neste side + Forrige side + Bla gjennom forrige spørring + Bla gjennom neste spørring + Åpne hurtigmeny + Åpne innebygd hurtigmeny + Åpne innstillingsvindu + Kopier filbane + Vis/Skjul spillmodus + Veksle historikk + Åpne innholdsmappen + Kjør som administrator + Oppdater søkeresultater + Last programtilleggdata på nytt + Hurtigjuster vindusbredden + Hurtigjuster vindushøyden + Bruk når du krever programtillegg for å laste inn og oppdatere eksisterende data på nytt. + Du kan legge til en hurtigtast til for denne funksjonen. + Egendefinerte hurtigtaster for spørring + Egendefinerte snarveier for spørring + Innebygde snarveier + Spørring + Snarvei + Utvidelse + Beskrivelse + Slett + Rediger + Legg til + Ingen + Velg et element + Er du sikker på at du vil slette {0} programtillegg hurtigtast? + Er du sikker på at du vil slette snarvei: {0} med utvidelse {1}? + Hent tekst fra utklippstavlen. + Hent sti fra aktiv utforsker. + Vindusskyggeeffekt for spørringer + Skyggeeffekt har en betydelig bruk av GPU. Anbefales ikke hvis datamaskinens ytelse er begrenset. + Vindusbredde størrelse + Du kan også raskt justere dette ved å bruke Ctrl+[ og Ctrl+]. + Bruk Segoe Fluent ikoner + Bruk Segoe Fluent Icons for spørreresultater der det støttes + Trykk tast - HTTP Proxy - Enable HTTP Proxy - HTTP Server + HTTP proxy + Aktiver HTTP proxy + HTTP-tjener Port - User Name - Password - Test Proxy - Save - Server field can't be empty - Port field can't be empty - Invalid port format - Proxy configuration saved successfully - Proxy configured correctly - Proxy connection failed + Brukernavn + Passord + Test-proxy + Lagre + Tjenerfelt kan ikke være tomt + Portfelt kan ikke være tomt + Ugyldig portformat + Proxy-konfigurasjonen ble lagret + Proxy konfigurert riktig + Proxy-tilkobling mislyktes - About - Website + Om + Nettsted GitHub - Docs - Version - Icons - You have activated Flow Launcher {0} times - Check for Updates - Become A Sponsor - New version {0} is available, would you like to restart Flow Launcher to use the update? - Check updates failed, please check your connection and proxy settings to api.github.com. + Dokumenter + Versjon + Ikoner + Du har aktivert Flow Launcher {0} ganger + Se etter oppdateringer + Bli en sponsor + Ny versjon {0} er tilgjengelig, vil du starte Flow Launcher på nytt for å bruke oppdateringen? + Sjekk oppdateringer mislyktes, vennligst sjekk tilkoblingen og proxy-innstillingene til api.github.com. - Download updates failed, please check your connection and proxy settings to github-cloud.s3.amazonaws.com, - or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually. + Nedlasting av oppdateringer mislyktes, vennligst sjekk tilkoblings- og mellomtjenerinnstillingene til github-cloud.s3.amazonaws.com, + eller gå til https://github.com/Flow-Launcher/Flow.Launcher/releases for å laste ned manuelt. - Release Notes - Usage Tips + Versjonsmerknader + Tips om bruk DevTools - Setting Folder - Log Folder - Clear Logs - Are you sure you want to delete all logs? - Wizard + Innstillingsmappe + Loggmappe + Tøm logger + Er du sikker på at du vil slette alle loggene? + Veiviser + Plassering av brukerdata + Brukerinnstillinger og installerte programtillegg lagres i brukerens datamappe. Denne plasseringen kan variere avhengig av om den er i portabel modus eller ikke. + Åpne mappe + Log Level + Debug + Info - Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". - File Manager - Profile Name - File Manager Path - Arg For Folder - Arg For File + Velg filbehandler + Vennligst spesifiser filplasseringen til filbehandleren du bruker, og legg til argumenter etter behov. "%d" representerer katalogbanen som skal åpnes for, brukt av Arg for mappe-feltet og for kommandoer som åpner spesifikke kataloger. "%f" representerer filbanen som skal åpnes for, brukt av Arg for fil-feltet og for kommandoer som åpner spesifikke filer. + For eksempel, hvis filbehandleren bruker en kommando som "totalcmd.exe /A c:windows" for å åpne c:windows-katalogen, vil filbehandlingsbanen bli totalcmd.exe, og Arg For Folder vil være /A "%d". Enkelte filbehandlere som QTTabBar kan bare kreve at en bane oppgis, i dette tilfellet bruker du "%d" som filbehandlingsbane og lar resten av feltene stå tomme. + Filbehandler + Profilnavn + Filbehandler sti + Arg for mappe + Arg for fil - Default Web Browser - 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 + Standard nettleser + Standardinnstillingen følger operativsystemets standard nettleserinnstilling. Hvis det er angitt separat, bruker Flow denne nettleseren. + Nettleser + Nettlesernavn + Sti til nettleser + Nytt vindu + Ny fane + Privat modus - 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! + Endre prioritet + Større nummer, jo høyere vil resultatet bli rangert. Prøv å sette det som 5. Ønsker du at resultatene skal være lavere enn noen andre programtillegg, gi et negativt tall + Angi et gyldig heltall for prioritet! - Old Action Keyword - New Action Keyword - Cancel - Done - Can't find specified plugin - New Action Keyword can't be empty - This new Action Keyword is already assigned to another plugin, please choose a different one - Success - Completed successfully - Enter the action keyword you like to use to start the plugin. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords. + Gammelt nøkkelord for handling + Nytt nøkkelord for handling + Avbryt + Utført + Kan ikke finne spesifisert programtillegg + Nytt handlingsnøkkelord kan ikke være tom + Det nye nøkkelordet for handling er allerede tilordnet et annet programtillegg, velg et annet + Vellykket + Fullført vellykket + Skriv inn handlingsnøkkelord du vil bruke for å starte programtillegget. Bruk * hvis du ikke ønsker å spesifisere noen, og utvidelsen vil bli utløst uten noen handlingsord. - Custom Query Hotkey - Press a custom hotkey to open Flow Launcher and input the specified query automatically. - Preview - Hotkey is unavailable, please select a new hotkey - Invalid plugin hotkey - Update - 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. + Hurtigtast for egendefinert spørring + Trykk på en egendefinert hurtigtast for å åpne Flow Launcher og skrive inn den angitte spørringen automatisk. + Forhåndsvis + Hurtigtast er utilgjengelig, vennligst velg en ny hurtigtast + Ugyldig hurtigtast for programtillegg + Oppdater + Binding av hurtigtast + Nåværende hurtigtast er utilgjengelig. + Denne hurtigtasten er reservert for "{0}" og kan ikke brukes. Velg en annen hurtigtast. + Denne hurtigtasten er allerede i bruk av "{0}". Hvis du trykker "Overskriv" vil den bli fjernet fra "{0}". + Trykk på tastene du vil bruke for denne funksjonen. - Custom Query Shortcut - Enter a shortcut that automatically expands to the specified query. - A shortcut is expanded when it exactly matches the query. + Snarvei for egendefinert spørring + Skriv inn en snarvei som automatisk utvides til den angitte spørringen. + En snarvei utvides når den samsvarer nøyaktig med spørringen. -If you add an '@' prefix while inputting a shortcut, it matches any position in the query. Builtin shortcuts match any position in a query. +Hvis du legger til et @-prefiks mens du legger inn en snarvei, samsvarer det med en hvilken som helst posisjon i spørringen. Innebygde snarveier samsvarer med alle posisjoner i en spørring. - Shortcut already exists, please enter a new Shortcut or edit the existing one. - Shortcut and/or its expansion is empty. + Snarveien eksisterer allerede, skriv inn en ny snarvei eller rediger den eksisterende. + Snarvei og/eller utvidelsen er tom. - Save - Overwrite - Cancel - Reset - Delete + Lagre + Overskriv + Avbryt + Tilbakestill + Slett + OK + Ja + Nei + Bakgrunn - Version - Time - Please tell us how application crashed so we can fix it - Send Report - Cancel - General - Exceptions - Exception Type - Source - Stack Trace - Sending - Report sent successfully - Failed to send report - Flow Launcher got an error + Versjon + Tid + Fortell oss hvordan programmet krasjet slik at vi kan fikse det + Send rapport + Avbryt + Generelt + Unntak + Type unntak + Kilde + Stabel spor + Sender + Rapporten ble sendt + Kunne ikke sende rapport + Flow Launcher fikk en feil + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message - Please wait... + Vennligst vent... - Checking for new update - You already have the latest Flow Launcher version - Update found - Updating... + Ser etter nye oppdateringer + Du har allerede siste versjon av Flow Launcher + Oppdatering funnet + Oppdaterer... - 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 kunne ikke flytte brukerprofildataene dine til den nye oppdateringsversjonen. + Du må manuelt flytte profildatamappen fra {0} til {1} - New Update - New Flow Launcher release {0} is now available - An error occurred while trying to install software updates - Update - Cancel - Update Failed - Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com. - This upgrade will restart Flow Launcher - Following files will be updated - Update files - Update description + Ny oppdatering + Ny versjon av Flow Launcher {0} er nå tilgjengelig + Det oppstod en feil under forsøket på å installere programvareoppdateringer + Oppdater + Avbryt + Oppdatering mislyktes + Sjekk din tilkobling og prøv å oppdatere proxy-innstillingene til github-cloud.s3.amazonaws.com. + Denne oppgraderingen vil starte Flow Launcher på nytt + Følgende filer vil bli oppdatert + Oppdater filer + Beskrivelse av oppdatering - Skip - Welcome to Flow Launcher - Hello, this is the first time you are running Flow Launcher! - Before starting, this wizard will assist in setting up Flow Launcher. You can skip this if you wish. Please choose a language - Search and run all files and applications on your PC - Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse. - Flow Launcher starts with the hotkey below, go ahead and try it out now. To change it, click on the input and press the desired hotkey on the keyboard. - Hotkeys - Action Keyword and Commands - Search the web, launch applications or run various functions through Flow Launcher plugins. Certain functions start with an action keyword, and if necessary, they can be used without action keywords. Try the queries below in Flow Launcher. - Let's Start Flow Launcher - Finished. Enjoy Flow Launcher. Don't forget the hotkey to start :) + Hopp over + Velkommen til Flow Launcher + Hei, dette er første gang du kjører Flow Launcher! + Før du starter, vil denne veiviseren hjelpe deg å sette opp Flow Launcher. Du kan hoppe over dette hvis du vil. Vennligst velg et språk + Søk og kjør alle filer og programmer på PCen din + Søk alt fra programmer, filer, bokmerker, YouTube, Twitter og mer. Alt fra komforten til tastaturet uten å røre musen. + Flow Launcher starter med hurtigtasten nedenfor, fortsett og prøv den nå. For å endre det, klikk på inndata og trykk på ønsket hurtigtast på tastaturet. + Hurtigtaster + Nøkkelord og kommandoer for handling + Søk på nett, starte programmer eller kjør forskjellige funksjoner gjennom programtillegg for Flow Launcher. Enkelte funksjoner begynner med et handlings-nøkkelord, og kan om nødvendig brukes uten handlings-søkeord. Prøv spørringene under i Flow Launcher. + La oss starte Flow Launcher + Fullført. Nyt Flow Launcher. Ikke glem hurtigtasten for å starte :) - Back / Context Menu - Item Navigation - Open Context Menu - Open Containing Folder - Run as Admin / Open Folder in Default File Manager - Query History - Back to Result in Context Menu - Autocomplete - Open / Run Selected Item - Open Setting Window - Reload Plugin Data + Tilbake / Hurtigmeny + Elementnavigering + Åpne hurtigmeny + Åpne innholdsmappen + Kjør som admin / åpne mappe i standard filbehandling + Spørrehistorikk + Tilbake til resultat i hurtigmenyen + Autofullfør + Åpne / Kjør valgte element + Åpne innstillingsvindu + Last programtilleggdata på nytt - Select first result - Select last result - Run current query again - Open result - Open result #{0} + Velg første resultat + Velg siste resultat + Kjør gjeldende spørring på nytt + Åpne resultat + Åpne resultat #{0} - Weather - Weather in Google Result + Vær + Været i Google Resultat > ping 8.8.8.8 - Shell Command + Skallkommando s Bluetooth - Bluetooth in Windows Settings + Bluetooth i Windows innstillinger sn Sticky Notes + + Filstørrelse + Opprettet + Sist endret diff --git a/Flow.Launcher/Languages/nl.xaml b/Flow.Launcher/Languages/nl.xaml index dedc8ff1b..64adccd94 100644 --- a/Flow.Launcher/Languages/nl.xaml +++ b/Flow.Launcher/Languages/nl.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + - Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Sneltoets "{0}" registreren. De sneltoets kan in gebruik zijn door een ander programma. Verander naar een andere sneltoets of sluit een ander programma. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Kan {0} niet starten Ongeldige Flow Launcher plugin bestandsextensie @@ -33,6 +45,8 @@ Draagbare Modus Alle instellingen en gebruikersgegevens opslaan in één map (Nuttig bij het gebruik van verwijderbare schijven of cloud services). Start Flow Launcher als systeem opstart + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Fout bij het instellen van uitvoeren bij opstarten Verberg Flow Launcher als focus verloren is Laat geen nieuwe versie notificaties zien @@ -42,51 +56,59 @@ Monitor met Gefocust Venster Primaire Monitor Aangepaste Monitor - Search Window Position on Monitor - Center - Center Top - Left Top - Right Top - Custom Position + Zoek vensterpositie op scherm + Midden + Midden boven + Links boven + Rechts boven + Aangepaste Positie Taal Laatste Query Style Toon/Verberg vorige resultaten wanneer Flow Launcher wordt gereactiveerd. Behoud laatste zoekopdracht Selecteer laatste zoekopdracht Laatste zoekopdracht verwijderen + Preserve Last Action Keyword + Select Last Action Keyword + Vaste venster hoogte + De vensterhoogte is niet aanpasbaar door te slepen. Laat maximale resultaten zien - You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. - Negeer sneltoetsen in fullscreen mode + Je kunt dit ook snel aanpassen met CTRL+Plus en CTRL+Minus. + Negeer sneltoetsen in vol scherm modus De activatie van Flow Launcher uitschakelen als er een applicatie actief is die zich in een volledig scherm bevind (Aanbevolen voor spellen). Standaard Bestandsbeheerder Selecteer de bestandsbeheerder voor het openen van de map. Standaard webbrowser Instelling voor Nieuw tabblad, Nieuw Venster, Privémodus. - Python Path - Node.js Path - Please select the Node.js executable - Please select pythonw.exe - Always Start Typing in English Mode - Temporarily change your input method to English mode when activating Flow. + Python pad + Node.js pad + Selecteer de Node.js uitvoerbare + Selecteer pythonw.exe + Begin altijd met typen in Engelse modus + Verander tijdelijk je invoermethode in de Engelse modus bij het activeren van Flow. Automatische Update Selecteer Verberg Flow Launcher als systeem opstart + Flow Launcher zoekvenster is verborgen in het systeemvak na het opstarten. Systeemvakpictogram verbergen - When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. + Wanneer het pictogram verborgen is in het vak, kan het instellingenmenu worden geopend door de rechtermuisknop op het zoekvenster te klikken. Zoekopdracht nauwkeurigheid Wijzigt de minimale overeenkomst-score die vereist is voor resultaten. + Geen + Laag + Normaal Zou Pinyin moeten gebruiken Zorgt ervoor dat Pinyin gebruikt kan worden om te zoeken. Pinyin is het standaard systeem van geromaniseerde spelling voor het vertalen van Chinees. - Always Preview - Always open preview panel when Flow activates. Press {0} to toggle preview. + Altijd voorbeeld + Open altijd het voorbeeld paneel wanneer Flow activeert. Druk op {0} om voorbeeld te schakelen. Schaduw effect is niet toegestaan omdat het huidige thema een vervagingseffect heeft - Search Plugin - Ctrl+F to search plugins - No results found - Please try a different search. - Plugin + Plug-ins zoeken + Ctrl+F om plug-ins te zoeken + Geen zoekresultaten + Probeer een andere zoekopdracht. + Plug-in Plugins Zoek meer plugins Aan @@ -99,49 +121,53 @@ Huidige Prioriteit Nieuwe Prioriteit Prioriteit - Change Plugin Results Priority + Wijzig Plug-in Resultaten Prioriteit Plugin map door Init tijd: Query tijd: Versie Website - Uninstall - + Verwijderen + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Plugin Winkel - New Release - Recently Updated + Nieuwe Versie + Recent bijgewerkt Plugins - Installed + Geïnstalleerd Vernieuwen Installeren - Uninstall - Update - Plugin already installed - New Version - This plugin has been updated within the last 7 days - New Update is Available - - + Verwijderen + Bijwerken + Plug-in is al geïnstalleerd + Nieuwe Versie + Deze plug-in is in de laatste 7 dagen bijgewerkt + Nieuwe update beschikbaar Thema - Appearance + Uiterlijk Zoek meer thema´s Hoe maak je een thema Hallo daar - Explorer - Search for files, folders and file contents - WebSearch - Search the web with different search engine support - Program - Launch programs as admin or a different user - ProcessKiller - Terminate unwanted processes + Verkenner + Zoek naar bestanden, mappen en bestandsinhoud + WebZoeken + Zoek op het web met verschillende zoekmachine ondersteuning + Programma + Programma's starten als admin of een andere gebruiker + Procesdoder + Ongewenste processen beëindigen + Zoekbalk hoogte + Item hoogte Query Box lettertype - Resultaat Item lettertype + Resultaat titel lettertype + Result Subtitle Font + Herstellen + Aanpassen Venster Modus Ondoorzichtigheid Thema {0} bestaat niet, terugvallen op het standaardthema @@ -161,12 +187,14 @@ Animatie gebruiken in UI Animation Speed The speed of the UI animation - Slow - Medium - Fast - Custom - Clock - Date + Langzaam + Normaal + Snel + Aangepast + Klok + Datum + Dit thema ondersteunt twee (licht/donker) modi. + This theme supports Blur Transparent Background. Sneltoets @@ -175,29 +203,30 @@ Voer snelkoppeling in om Flow Launcher te tonen/verbergen. Toggle Preview Enter shortcut to show/hide preview in search window. - Hotkey Presets - List of currently registered hotkeys + Sneltoets voorinstellingen + Lijst met momenteel geregistreerde sneltoetsen Open resultaatmodificatoren Kies een aanpassingstoets om het geselecteerde resultaat te openen via het toetsenbord. Sneltoets weergeven Show result selection hotkey with results. - Auto Complete - Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page + Automatisch Aanvullen + Voert automatisch aanvullen uit voor de geselecteerde items. + Selecteer Volgend Item + Selecteer Vorig Item + Volgende Pagina + Vorige Pagina Ga naar vorige zoekopdracht Ga naar volgende zoekopdracht - Open Context Menu - Open Setting Window - Copy File Path + Open Contextmenu + Open Origineel Contextmenu + Open Instellingenvenster + Kopieer Bestandspad Toggle Game Mode Toggle History Open Containing Folder - Run As Admin - Refresh Search Results - Reload Plugins Data + Uitvoeren Als Administrator + Vernieuw Zoekresultaten + Herlaad Gegevens Plugins Snel vensterbreedte aanpassen Snel vensterhoogte aanpassen Use when require plugins to reload and update their existing data. @@ -246,51 +275,57 @@ Over Website GitHub - Docs + Documentatie Versie - Icons + Pictogrammen U heeft Flow Launcher {0} keer opgestart Zoek naar Updates - Become A Sponsor + Sponsor worden Nieuwe versie {0} beschikbaar, start Flow Launcher opnieuw op - Check updates failed, please check your connection and proxy settings to api.github.com. + Controleren op updates mislukt, controleer uw verbinding en proxy-instellingen voor api.github.com. - Download updates failed, please check your connection and proxy settings to github-cloud.s3.amazonaws.com, - or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually. + Downloaden is mislukt, controleer uw verbinding en proxy-instellingen voor github-cloud.s3.amazonaws.com, + of ga naar https://github.com/Flow-Launcher/Flow.Launcher/releases om handmatig updates te downloaden. Release Notes - Usage Tips - DevTools - Setting Folder - Log Folder - Clear Logs - Are you sure you want to delete all logs? + Gebruiks tips + Ontwikkelaars Tools + Instellingen map + Log Map + Logbestanden wissen + Weet u zeker dat u alle logbestanden wilt verwijderen? Wizard + Gegevenslocatie van gebruiker + Gebruikersinstellingen en geïnstalleerde plug-ins worden opgeslagen in de gebruikersgegevensmap. Deze locatie kan variëren afhankelijk van of het in draagbare modus is of niet. + Map openen + Log Level + Debug + Info - Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". - File Manager - Profile Name - File Manager Path - Arg For Folder - Arg For File + Bestandsbeheerder selecteren + 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 fileds blank. + Bestandsbeheerder + Profielnaam + Bestandsbeheerder pad + Arg voor map + Arg voor bestand Standaard webbrowser - 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 + De standaardinstelling volgt de standaardinstelling van de OS standaardinstellingen. Indien apart opgegeven, Flow gebruikt die browser. + Webbrowser + Browser Naam + Browser pad + Nieuw Venster + Nieuw tabblad + Privé modus - 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! + Prioriteit wijzigen + Groter getal, hoe hoger het resultaat zal worden gerangschikt. Probeer het in te stellen als 5. Als u wilt dat de resultaten lager zijn dan welke andere plug-in dan ook, geef een negatief getal op + Geef een geldig geheel getal voor de prioriteit! Oude actie sneltoets @@ -301,38 +336,42 @@ Nieuwe actie sneltoets moet ingevuld worden Nieuwe actie sneltoets is toegewezen aan een andere plugin, wijs een nieuwe actie sneltoets aan Succesvol - Completed successfully + Succesvol afgerond Gebruik * wanneer je geen nieuwe actie sneltoets wilt specificeren Custom Query Sneltoets - Press a custom hotkey to open Flow Launcher and input the specified query automatically. + Druk op een aangepaste sneltoets om Flow Launcher te openen en de opgegeven query automatisch in te voeren. Voorbeeld Sneltoets is niet beschikbaar, selecteer een nieuwe sneltoets Ongeldige plugin sneltoets - Update - 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. + Bijwerken + Sneltoets koppelen + Huidige sneltoets is niet beschikbaar. + Deze sneltoets is gereserveerd voor "{0}" en kan niet worden gebruikt. Kies een andere sneltoets. + Deze sneltoets is al in gebruik door "{0}". Als u op "Overschrijven" klikt, zal deze verwijderd worden uit "{0}". + Druk op de toetsen die u wilt gebruiken voor deze functie. - Custom Query Shortcut - Enter a shortcut that automatically expands to the specified query. - A shortcut is expanded when it exactly matches the query. + Aangepaste Query Snelkoppeling + Voer een snelkoppeling in die automatisch wordt uitgebreid naar de opgegeven zoekopdracht. + Een snelkoppeling wordt uitgebreid wanneer deze precies overeenkomt met de query. -If you add an '@' prefix while inputting a shortcut, it matches any position in the query. Builtin shortcuts match any position in a query. +Als u een '@' voorvoegsel toevoegt tijdens het invoeren van een snelkoppeling, matcht het met elke positie in de zoekopdracht. Ingebouwde snelkoppelingen komen overeen met elke positie in een query. - Shortcut already exists, please enter a new Shortcut or edit the existing one. - Shortcut and/or its expansion is empty. + Snelkoppeling bestaat al, vul een nieuwe snelkoppeling in of pas de bestaande aan. + Snelkoppeling en/of uitbreiding is leeg. Opslaan - Overwrite + Overschrijf Annuleer - Reset + Herstellen Verwijder + OK + Yes + No + Background Versie @@ -349,6 +388,9 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Rapport succesvol verzonden Verzenden van rapport mislukt Flow Launcher heeft een error + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Please wait... @@ -417,4 +459,8 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in sn Sticky Notes + + Bestandsgrootte + Gemaakt + Laatst gewijzigd diff --git a/Flow.Launcher/Languages/pl.xaml b/Flow.Launcher/Languages/pl.xaml index 03da99d91..06204395c 100644 --- a/Flow.Launcher/Languages/pl.xaml +++ b/Flow.Launcher/Languages/pl.xaml @@ -1,7 +1,19 @@  + + + Flow wykrył, że zainstalowano {0} wtyczek, które będą wymagać {1} do działania. Czy chcesz pobrać {1}? +{2}{2} +Kliknij "nie", jeśli jest już zainstalowany. Zostaniesz wtedy poproszony o wybranie folderu zawierającego plik wykonywalny {1} + + Wybierz plik wykonywalny {0} + Nie można ustawić ścieżki do pliku wykonywalnego {0}. Spróbuj ponownie w ustawieniach Flow (przewiń na sam dół). + Nie udało się zainicjować wtyczek + Wtyczki: {0} – nie udało się ich wczytać i zostaną wyłączone. Skontaktuj się z twórcą wtyczki, aby uzyskać pomoc + - Nie udało się zarejestrować skrótu klawiszowego "{0}". Klucz skrótu może być używany przez inny program. Zmień skrót klawiszowy lub wyjdź z innego programu. + Nie udało się zarejestrować skrótu klawiszowego „{0}”. Skrót może być używany przez inny program. Zmień skrót na inny lub zamknij program, który go używa. + Nie udało się wyrejestrować skrótu „{0}”. Spróbuj ponownie lub sprawdź szczegóły w dzienniku Flow Launcher Nie udało się uruchomić: {0} Niepoprawny format pliku wtyczki @@ -33,16 +45,18 @@ Tryb przenośny Przechowuj wszystkie ustawienia i dane użytkownika w jednym folderze (Przydatne, gdy używane na dyskach wymiennych lub usługach chmurowych). Uruchamiaj Flow Launcher przy starcie systemu + Użyj zadania logowania zamiast wpisu autostartu, aby przyspieszyć uruchamianie + Po odinstalowaniu musisz ręcznie usunąć to zadanie (Flow.Launcher Startup) za pomocą Harmonogramu zadań Błąd uruchamiania ustawień przy starcie Ukryj okno Flow Launcher kiedy przestanie ono być aktywne Nie pokazuj powiadomienia o nowej wersji - Pozycja Okna Wyszukiwania + Pozycja okna wyszukiwania Zapamiętaj Ostatnią Pozycję Monitoruj kursorem myszy Monitor z Dostosowanym Oknem Monitor główny Monitor Niestandardowy - Pozycja Okna Wyszukiwania na Monitorze + Pozycja okna wyszukiwania na monitorze Wyśrodkowane Środek Góra Lewa Góra @@ -54,15 +68,19 @@ Zachowaj ostatnie zapytanie Wybierz ostatnie zapytanie Puste ostatnie zapytanie + Zachowaj ostatnie słowo kluczowe akcji + Wybierz ostatnie słowo kluczowe akcji + Stała wysokość okna + Wysokość okna nie jest regulowana poprzez przeciąganie. Maksymalna liczba wyników Możesz to również szybko dostosować używając CTRL+Plus i CTRL+Minus. - Ignoruj skróty klawiszowe w trybie pełnego ekranu - Wyłącz aktywowanie Flow Launcher, gdy uruchomiona jest aplikacja pełnoekranowa (Zalecane dla gier). + Ignoruj skróty klawiszowe w trybie pełnoekranowym + Wyłącz aktywację Flow Launcher, gdy aktywna jest aplikacja pełnoekranowa (zalecane dla gier). Domyślny menedżer plików Wybierz menedżer plików używany do otwierania folderów. Domyślna przeglądarka Ustawienie dla nowej karty, nowego okna i trybu prywatnego. - Python Path + Ścieżka Python Ścieżka Node.js Wybierz plik wykonywalny Node.js Wybierz pythonw.exe @@ -71,13 +89,17 @@ Automatyczne aktualizacje Wybierz Uruchamiaj Flow Launcher zminimalizowany + Okno wyszukiwania Flow Launcher jest ukryte w zasobniku systemowym po uruchomieniu. Ukryj ikonę zasobnika - Gdy ikona jest ukryta w zasobniku, menu Ustawienia można otworzyć, klikając prawym przyciskiem myszy okno wyszukiwania. + Gdy ikona jest ukryta w zasobniku systemowym, menu Ustawienia można otworzyć, klikając prawym przyciskiem myszy na oknie wyszukiwania. Precyzja wyszukiwania zapytań Zmienia minimalny wynik dopasowania wymagany do uzyskania wyników. + Brak + Niska + Standardowa Szukaj z Pinyin Umożliwia wyszukiwanie przy użyciu Pinyin. Pinyin to standardowy system pisowni zromanizowanej służący do tłumaczenia języka chińskiego. - Zawsze Podgląd + Zawsze podgląd Zawsze otwieraj panel podglądu, gdy aktywowany jest Flow. Naciśnij {0}, aby przełączyć podgląd. Efekt cienia jest niedozwolony, gdy bieżący motyw ma włączony efekt rozmycia @@ -107,7 +129,8 @@ Wersja Strona Odinstalowywanie - + Nie udało się usunąć ustawień wtyczki + Wtyczki: {0} – nie udało się usunąć plików ustawień wtyczek, usuń je ręcznie Sklep z wtyczkami @@ -124,8 +147,6 @@ Ta wtyczka została zaktualizowana w ciągu ostatnich 7 dni Dostępna jest nowa aktualizacja - - Skórka Wygląd @@ -140,23 +161,28 @@ Uruchamiaj programy jako administrator lub inny użytkownik ZabijProces Zakończ niechciane procesy + Wysokość paska wyszukiwania + Wysokość elementu Czcionka okna zapytania - Czcionka okna wyników + Czcionka tytułu wyniku + Czcionka podtytułu wyniku + Resetuj + Personalizuj Tryb w oknie Przeźroczystość Motyw {0} nie istnieje, powróć do domyślnego motywu Nie można załadować motywu {0}, wróć do motywu domyślnego - Folder Motywu - Otwórz folder motywu + Folder motywów + Otwórz folder motywów Schemat kolorów Domyślne ustawienie systemowe Jasny Ciemny Efekty dźwiękowe Odtwarzaj krótki dźwięk po otwarciu okna wyszukiwania - Sound Effect Volume - Adjust the volume of the sound effect - Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume. + Głośność efektów dźwiękowych + Dostosuj głośność efektów dźwiękowych + Windows Media Player jest niedostępny, a jest wymagany do regulacji głośności w Flow. Sprawdź swoją instalację, jeśli potrzebujesz dostosować głośność. Animacja Użyj animacji w interfejsie użytkownika Szybkość animacji @@ -167,64 +193,67 @@ Niestandardowa Zegar Data + Ten motyw obsługuje dwa tryby (jasny/ciemny). + Ten motyw obsługuje rozmyte przezroczyste tło. Skrót klawiszowy Skrót klawiszowy - Open Flow Launcher + Otwórz Flow Launcher Wprowadź skrót, aby pokazać/ukryć Flow Launcher. - Toggle Preview + Przełącz podgląd Wprowadź skrót, aby pokazać/ukryć podgląd w oknie wyszukiwania. - Hotkey Presets - List of currently registered hotkeys + Szablony skrótów + Lista zarejestrowanych skrótów klawiszowych Modyfikatory klawiszów otwierających wyniki - Select a modifier key to open selected result via keyboard. + Wybierz klawisz modyfikujący, aby otworzyć wybrany wynik za pomocą klawiatury. Pokaż skrót klawiszowy - Show result selection hotkey with results. - Auto Complete - Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page - Cycle Previous Query - Cycle Next Query - Open Context Menu - Open Setting Window - Copy File Path - Toggle Game Mode - Toggle History + Pokaż skrót wyboru wyniku wraz z wynikami. + Autouzupełnienie + Uruchamia autouzupełnianie dla wybranych elementów. + Wybierz następny element + Wybierz poprzedni element + Następna Strona + Poprzednia strona + Przejdź do poprzedniego zapytania + Przejdź do następnego zapytania + Otwórz menu kontekstowe + Otwórz natywne menu kontekstowe + Otwórz okno ustawień + Kopiuj ścieżkę pliku + Przełącz tryb gry + Przełącz historię Otwórz folder zawierający - Run As Admin - Refresh Search Results - Reload Plugins Data - Quick Adjust Window Width - Quick Adjust Window Height - Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. + Uruchom jako Administrator + Odśwież wyniki wyszukiwania + Odśwież dane wtyczek + Szybka regulacja szerokości okna + Szybka regulacja szerokości okna + Używaj, gdy wymagane jest odświeżenie wtyczek i zaktualizowanie ich istniejących danych. + Możesz dodać jeszcze jeden skrót klawiszowy dla tej funkcji. Skrót klawiszowy niestandardowych zapytań Custom Query Shortcut Built-in Shortcut - Query - Shortcut - Expansion + Zapytanie + Skrót + Rozwinięcie Opis Usuń Edytuj Dodaj - None + Brak Musisz coś wybrać Czy jesteś pewien że chcesz usunąć skrót klawiszowy {0} wtyczki? - Are you sure you want to delete shortcut: {0} with expansion {1}? - Get text from clipboard. - Get path from active explorer. - Query window shadow effect - 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+]. - Use Segoe Fluent Icons - Use Segoe Fluent Icons for query results where supported - Press Key + Czy na pewno chcesz usunąć skrót: {0} z rozszerzeniem {1}? + Pobierz tekst ze schowka. + Pobierz ścieżkę z aktywnego eksploratora. + Efekt cienia okna zapytania + Efekt cienia znacząco obciąża GPU. Niezalecane, jeśli wydajność twojego komputera jest ograniczona. + Szerokość okna + Możesz to również szybko dostosować, używając Ctrl+[ i Ctrl+]. + Użyj ikon Segoe Fluent + Użyj ikon Segoe Fluent dla wyników wyszukiwania, gdzie jest to obsługiwane + Naciśnij klawisz Serwer proxy HTTP @@ -244,33 +273,39 @@ O programie - Website + Strona internetowa GitHub - Docs + Dokumentacja Wersja - Icons + Ikony Uaktywniłeś Flow Launcher {0} razy Szukaj aktualizacji - Become A Sponsor + Zostań sponsorem Nowa wersja {0} jest dostępna, uruchom ponownie Flow Launcher - Check updates failed, please check your connection and proxy settings to api.github.com. + Sprawdzenie aktualizacji nie powiodło się. Sprawdź swoje połączenie i ustawienia proxy dla api.github.com. - Download updates failed, please check your connection and proxy settings to github-cloud.s3.amazonaws.com, - or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually. + Pobieranie aktualizacji nie powiodło się. Sprawdź połączenie internetowe i ustawienia proxy dla github-cloud.s3.amazonaws.com + lub przejdź do https://github.com/Flow-Launcher/Flow.Launcher/releases, aby pobrać aktualizacje ręcznie. Zmiany - Usage Tips - DevTools - Setting Folder - Log Folder - Clear Logs - Are you sure you want to delete all logs? - Wizard + Wskazówki użycia + Narzędzia deweloperskie + Folder ustawień + Folder dziennika + Wyczyść logi + Czy na pewno chcesz usunąć wszystkie logi? + Kreator + Lokalizacja danych użytkownika + Ustawienia użytkownika i zainstalowane wtyczki są zapisywane w folderze danych użytkownika. Ta lokalizacja może się różnić w zależności od tego, czy aplikacja jest w trybie przenośnym, czy nie. + Otwórz folder + Log Level + Debug + Info - Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" jest argumentem reprezentującym ścieżkę do pliku. Służy do podkreślenia nazwy pliku/folderu podczas otwierania określonej lokalizacji pliku w menedżerze plików innych firm. Ten argument jest dostępny tylko w pozycji "Arg dla pliku". Jeśli menedżer plików nie ma tej funkcji, możesz użyć "%d". + Wybierz menedżer plików + Proszę określić lokalizację pliku menedżera plików, którego używasz i dodać argumenty według potrzeb. Symbol "%d" reprezentuje ścieżkę katalogu do otwarcia, używaną w polu Arg dla Folderu oraz dla poleceń otwierających konkretne katalogi. Symbol "%f" reprezentuje ścieżkę pliku do otwarcia, używaną w polu Arg dla Pliku oraz dla poleceń otwierających konkretne pliki. + Na przykład, jeśli menedżer plików używa polecenia takiego jak „totalcmd.exe /A c:\windows" do otwarcia katalogu c:\windows, Ścieżka Menedżera Plików będzie totalcmd.exe, a Argument dla Folderu będzie /A "%d". Niektóre menedżery plików, takie jak QTTabBar, mogą wymagać jedynie podania ścieżki; w takim przypadku użyj "%d" jako Ścieżki Menedżera Plików, a pozostałe pola pozostaw puste. Menadżer plików Nazwa profilu Ścieżka menedżera plików @@ -311,11 +346,11 @@ Skrót klawiszowy jest niedostępny, musisz podać inny skrót klawiszowy Niepoprawny skrót klawiszowy Aktualizuj - 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. + Przypisywanie skrótów + Bieżący skrót klawiszowy jest niedostępny. + Ten skrót klawiszowy jest zarezerwowany dla "{0}" i nie może być użyty. Proszę wybrać inny skrót. + Ten skrót klawiszowy jest już używany przez "{0}". Jeśli naciśniesz "Nadpisz", zostanie on usunięty z "{0}". + Naciśnij klawisze, których chcesz użyć dla tej funkcji. Niestandardowy skrót zapytania @@ -329,10 +364,14 @@ Jeśli dodasz prefiks '@' podczas wprowadzania skrótu, będzie on pasował do d Zapisz - Overwrite + Nadpisz Anuluj - Reset - Usu + Zresetuj + Usuń + Aktualizuj + Tak + Nie + Tło Wersja @@ -349,6 +388,9 @@ Jeśli dodasz prefiks '@' podczas wprowadzania skrótu, będzie on pasował do d Raport wysłany pomyślnie Nie udało się wysłać raportu W programie Flow Launcher wystąpił błąd + Otwórz nowe zgłoszenie w + 1. Prześlij plik dziennika: {0} + 2. Skopiuj poniższą wiadomość wyjątku Proszę czekać... @@ -379,8 +421,8 @@ Jeśli dodasz prefiks '@' podczas wprowadzania skrótu, będzie on pasował do d Witamy w Flow Launcher Witaj, po raz pierwszy uruchamiasz Flow Launcher! Przed rozpoczęciem ten kreator pomoże skonfigurować Flow Launcher. Jeśli chcesz, możesz to pominąć. Proszę wybierz język - Wyszukiwanie i uruchamianie wszystkich plików i aplikacji na PC - Przeszukuj wszystko, od aplikacji, plików, zakładek, YouTube, X i nie tylko. Wszystko to z komfortowej klawiatury, bez konieczności dotykania myszy. + Wyszukuj i uruchamiaj pliki oraz aplikacje na komputerze + Wyszukuj wszystko – aplikacje, pliki, zakładki, YouTube, Twitter i nie tylko. Wszystko wygodnie z klawiatury, bez używania myszy. Flow Launcher uruchamia się za pomocą poniższego skrótu klawiszowego, śmiało i wypróbuj go teraz. Aby to zmienić, kliknij dane wejściowe i naciśnij żądany klawisz skrótu na klawiaturze. Skróty klawiszowe Słowo kluczowe akcji i polecenia @@ -391,30 +433,34 @@ Jeśli dodasz prefiks '@' podczas wprowadzania skrótu, będzie on pasował do d Powrót / Menu kontekstowe - Nawigacja pozycji + Nawigacja po elementach Otwórz menu kontekstowe Otwórz folder zawierający Uruchom jako administrator / Otwórz folder w domyślnym menedżerze plików Historia zapytań - Back to Result in Context Menu - Autocomplete - Open / Run Selected Item - Open Setting Window - Reload Plugin Data + Powrót do wyniku w menu kontekstowym + Autouzupełnianie + Otwórz / Uruchom zaznaczony element + Otwórz okno ustawień + Odśwież dane wtyczek - Select first result - Select last result - Run current query again - Open result - Open result #{0} + Wybierz pierwszy wynik + Wybierz ostatni wynik + Uruchom ponownie bieżące zapytanie + Otwórz wynik + Otwórz wynik #{0} - Weather - Weather in Google Result + Pogoda + Pogoda w wynikach Google > ping 8.8.8.8 - Shell Command + Polecenie powłoki s Bluetooth - Bluetooth in Windows Settings + Bluetooth w ustawieniach Windows sn - Sticky Notes + Podręczne notatki + + Rozmiar pliku + Utworzono + Ostatnia modyfikacja diff --git a/Flow.Launcher/Languages/pt-br.xaml b/Flow.Launcher/Languages/pt-br.xaml index 562f43b96..62293b1a1 100644 --- a/Flow.Launcher/Languages/pt-br.xaml +++ b/Flow.Launcher/Languages/pt-br.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + Falha em registrar a tecla de atalho "{0}". A combinação pode estar em uso por outro programa. Mude para uma tecla de atalho diferente, ou encerre o outro programa. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Não foi possível iniciar {0} Formato de plugin Flow Launcher inválido @@ -33,6 +45,8 @@ Modo Portátil Armazene todas as configurações e dados do usuário em uma pasta (útil quando usado com unidades removíveis ou serviços em nuvem). Iniciar Flow Launcher com inicialização do sistema + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Erro ao ativar início com o sistema Esconder Flow Launcher quando foco for perdido Não mostrar notificações de novas versões @@ -54,6 +68,10 @@ Preservar Última Consulta Selecionar última consulta Limpar última consulta + Preserve Last Action Keyword + Select Last Action Keyword + Fixed Window Height + The window height is not adjustable by dragging. Máximo de resultados mostrados Você também pode ajustar isso rapidamente usando CTRL+Mais e CTRL+Menos. Ignorar atalhos em tela cheia @@ -71,10 +89,14 @@ Atualizar Automaticamente Selecionar Esconder Flow Launcher na inicialização + Flow Launcher search window is hidden in the tray after starting up. Ocultar ícone da bandeja Quando o ícone não está na bandeja, o menu de Configurações pode ser aberto ao clicar na janela de busca com o botão direito do mouse. Precisão de Busca da Consulta Altera a pontuação de match mínima exigida para resultados. + None + Low + Regular Buscar com Pinyin Permite o uso de Pinyin para busca. Pinyin é o sistema padrão de escrita romanizada para traduzir chinês. Sempre Pré-visualizar @@ -107,7 +129,8 @@ Versão Site Desinstalar - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Loja de Plugins @@ -124,8 +147,6 @@ Este plugin foi atualizado nos últimos 7 dias Nova Atualização Disponível - - Tema Aparência @@ -140,8 +161,13 @@ Inicie programas como administrador ou como um usuário diferente Matador de Processos Termine processos indesejados + Search Bar Height + Item Height Fonte da caixa de Consulta - Fonte do Resultado + Result Title Font + Result Subtitle Font + Reset + Customize Modo Janela Opacidade Tema {0} não existe, retorne para o tema padrão @@ -167,6 +193,8 @@ Personalizado Relógio Data + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. Atalho @@ -190,6 +218,7 @@ Cycle Previous Query Cycle Next Query Abrir Menu de Contexto + Open Native Context Menu Abrir Janela de Configurações Copy File Path Toggle Game Mode @@ -266,11 +295,17 @@ Limpar Registros Tem certeza que quer excluir todos os registros? Assistente + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info Selecione o Gerenciador de Arquivos - Por favor, especifique a localização do arquivo do gerenciador de arquivos que você está usando e adicione argumentos se necessário. Os argumentos padrões são "%d", e um caminho é digitado naquela localização. Por exemplo, se um comando é necessário tal qual "totalcmd.exe /A c:\windows", o argumento é /A "%d". - "%f" é um argumento que representa o caminho do arquivo. É utilizado para enfatizar o nome da pasta/arquivo ao abrir uma localização de arquivo específica em um gerenciador de arquivo de terceiros. Esse argumento só está disponível no item "Arg para Arquivo". Se o gerenciador de arquivos não tem essa função, você pode usar "%d". + 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 fileds blank. Gerenciador de Arquivos Nome do Perfil Caminho do Gerenciador de Arquivos @@ -333,6 +368,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Cancelar Reset Apagar + OK + Yes + No + Plano de fundo Versão @@ -349,6 +388,9 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Relatório enviado com sucesso Falha ao enviar relatório Flow Launcher apresentou um erro + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Por favor, aguarde... @@ -417,4 +459,8 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in sn Notas Autoadesivas + + File Size + Created + Last Modified diff --git a/Flow.Launcher/Languages/pt-pt.xaml b/Flow.Launcher/Languages/pt-pt.xaml index 58f3aba43..bad37f688 100644 --- a/Flow.Launcher/Languages/pt-pt.xaml +++ b/Flow.Launcher/Languages/pt-pt.xaml @@ -1,7 +1,19 @@  + + + Flow Launcher detetou que tem instalou os plugins {0}, que necessitam de {1} para serem executados. Gostaria de descarregar {1}? + {2}{2} + Clique "Não" se já tiver instalado e, de seguida, ser-lhe-á solicitada a pasta que contém o executável {1}. + + Por favor, selecione o executável {0} + Não foi possível definir o caminho do executável {0}. Experimente definir o caminho nas definições (desloque até ao fundo). + Falha ao iniciar os plugins + Plugin: {0} - não foi possível iniciar e será desativado. Contacte o criador do plugin para obter ajuda. + Falha ao registar a tecla de atalho "{0}". A tecla de atalho pode estar a ser usada por outra aplicação. Utilize uma tecla de atalho diferente ou feche o outro programa. + Falha ao cancelar a atribuição da tecla de atalho "{0}". Tente novamente ou consulte o registo para mais detalhes. Flow Launcher Não foi possível iniciar {0} Formato do ficheiro inválido como plugin @@ -33,6 +45,8 @@ Modo portátil Guardar todas as definições e dados do utilizador numa pasta (indicado se utilizar discos amovíveis ou serviços cloud) Iniciar Flow Launcher ao arrancar o sistema + Utilizar tarefa de arranque em vez de uma entrada de arranque para uma experiência mais rápida + Se desinstalar a aplicação, tem que remover manualmente a tarefa (Flow.Launcher Startup) no agendamento de tarefas Erro ao definir para iniciar ao arrancar Ocultar Flow Launcher ao perder o foco Não notificar acerca de novas versões @@ -54,6 +68,10 @@ Manter última consulta Selecionar última consulta Limpar última consulta + Manter palavra-chave da última ação + Selecionar palavra-chave da última ação + Altura fixa de janela + Não é possível ajustar o tamanho da janela por arrasto. Número máximo de resultados Também pode ajustar rapidamente através do atalho Ctrl+ e Ctrl-. Ignorar teclas de atalho se em ecrã completo @@ -71,10 +89,14 @@ Atualização automática Selecionar Ocultar Flow Launcher ao arrancar + Ocultar caixa de pesquisa na bandeja após iniciar Flow Launcher. Ocultar ícone na bandeja Se o ícone da bandeja estiver oculto, pode abrir as Definições com um clique com o botão direito do rato na caixa de pesquisa. Precisão da consulta Altera a precisão mínima necessário para obter resultados + Nenhuma + Baixa + Normal Pesquisar com Pinyin Permite a utilização de Pinyin para pesquisar. Pinyin é um sistema normalizado de ortografia romanizada para tradução de mandarim. Pré-visualizar sempre @@ -107,7 +129,8 @@ Versão Site Desinstalar - + Falha ao remover as definições do plugin + Plugin: {0} - Falha ao remover o ficheiro de definições do plugin. Experimente remover manualmente. Loja de plugins @@ -124,8 +147,6 @@ Este plugin foi atualizado nos últimos 7 dias Atualização disponível - - Tema Aparência @@ -140,8 +161,13 @@ Iniciar programas como administrador ou utilizador Terminador de processos Terminar processos indesejados + Altura da barra de pesquisa + Altura do item Tipo de letra da consulta - Tipo de letra dos resultados + Tipo de letra dos títulos + Tipo de letra dos subtítulos + Repor + Personalizar Modo da janela Opacidade O tema {0} não existe e será utilizado o tema padrão @@ -167,6 +193,8 @@ Personalizada Relógio Data + Este tema tem suporte a dois modos (claro/escuro). + Este tema tem suporte a fundo transparente desfocado. Tecla de atalho @@ -190,6 +218,7 @@ Ir para consulta anterior Ir para consulta seguinte Abrir menu de contexto + Abrir menu de contexto nativo Abrir janela de definições Copiar caminho do ficheiro Comutar modo de jogo @@ -265,11 +294,17 @@ Limpar registos Tem a certeza de que deseja remover todos os registos? Assistente + Localização dos dados do utilizador + As definições e os plugins instalados são guardados na pasta de dados do utilizador. A localização pode variar, tendo em conta se a aplicação está instalada ou no modo portátil + Abrir pasta + Log Level + Debug + Info Selecione o gestor de ficheiros - Especifique a localização do executável do gestor de ficheiros e, eventualmente, alguns argumentos. Os argumentos padrão são "%d" e o caminho é introduzido nesse local. Por exemplo, se necessitar de um comando como "totalcmd.exe /A c:\windows", o argumento é /A "%d". - "%f" é o argumento que representa o caminho do ficheiro. É utilizado para dar ênfase ao nome do ficheiro ou da pasta se utilizar um gestor de ficheiros não nativo. Este argumento apenas está disponível para o item "Argumento para ficheiro". Se o seu gestor de ficheiros não possuir esta funcionalidade, pode utilizar "%d". + Por favor, especifique a localização do executável do seu gestor de ficheiros e adicione os argumentos necessários. "%d" representa o caminho do diretório a abrir, usado pelo argumento do campo Pasta e para comandos que abrem diretórios específicos. "%f" representa o caminho do ficheiro a abrir, usado pelo argumento do campo Ficheiro e para comandos que abrem ficheiros específicos. + Por exemplo, se o gestor de ficheiros utilizar o comando "totalcmd.exe /A c:\windows" para abrir o diretório c:\windows , o caminho para o gestor de ficheiros será totalcmd. exe e os argumentos para a Pasta serão /A "%d". Alguns gestores de ficheiros, como QTTabBar podem apenas exigir que especifique o caminho. Para estes, deve utilizar "%d" como caminho para o gestor de ficheiros e deixar o resto dos campos em branco. Gestor de ficheiros Nome do perfil Caminho do gestor de ficheiros @@ -332,6 +367,10 @@ Se adicionar o prefixo '@' durante a introdução do atalho, será utilizada qua Cancelar Repor Eliminar + OK + Sim + Não + Fundo Versão @@ -348,6 +387,9 @@ Se adicionar o prefixo '@' durante a introdução do atalho, será utilizada qua Relatório enviado com sucesso Falha ao enviar o relatório Ocorreu um erro + Abra um relatório de erro em + 1. Carregue o ficheiro de registos: {0} + 2. Copie a mensagem abaixo Por favor aguarde... @@ -379,8 +421,8 @@ Queira por favor mover a pasta do seu perfil de {0} para {1} Esta é a primeira vez que está a utilizar Flow Launcher! Antes de utilizar a aplicação, este assistente ajuda a configurar Flow Launcher. Caso pretenda, pode ignorar este passo. Por favor escolha um idioma. Pesquise ficheiros/pastas e execute aplicações no seu computador - Pode pesquisar aplicações, ficheiros, marcadores, YouTube, Twitter e muito mais. Tudo isto é efetuado através do teclado, dispensando a utilização do rato - Flow Launcher é iniciado com a tecla de atalho abaixo. Experimente. Para alterar esta tecla de atalho, clique no valor e escolha a combinação de teclas a utilizar + Pode pesquisar aplicações, ficheiros, marcadores, YouTube, Twitter e muito mais. Tudo isto é efetuado através do teclado, dispensando a utilização do rato. + Flow Launcher é iniciado com a tecla de atalho abaixo indicada. Para alterar esta tecla de atalho, clique no valor e escolha a combinação de teclas a utilizar. Teclas de atalho Palavras-chave e comandos Pesquise na Web, inicie aplicações e execute funções com os nossos plugins. Algumas ações são invocadas com palavras-chave mas, se quiser, podem ser invocadas sem essas palavras-chave. Teste as consultas abaixo para experimentar. @@ -416,4 +458,8 @@ Queira por favor mover a pasta do seu perfil de {0} para {1} sn Sticky Notes + + Tamanho do ficheiro + Criado + Última modificação diff --git a/Flow.Launcher/Languages/ru.xaml b/Flow.Launcher/Languages/ru.xaml index 739252da7..a56a770da 100644 --- a/Flow.Launcher/Languages/ru.xaml +++ b/Flow.Launcher/Languages/ru.xaml @@ -1,10 +1,19 @@ - - + + + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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}". Возможно, оно используется другой программой. Измените сочетание клавиш или закройте другую программу. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Не удалось запустить {0} Недопустимый формат файла плагина Flow Launcher @@ -36,6 +45,8 @@ Портативный режим Храните все настройки и данные пользователя в одной папке (полезно при использовании со съёмными дисками или облачными сервисами). Запускать Flow Launcher при запуске системы + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Ошибка настройки запуска при запуске Скрывать Flow Launcher, если потерян фокуc Не отображать сообщение об обновлении, когда доступна новая версия @@ -57,6 +68,10 @@ Сохранение последнего запроса Выбор последнего запроса Очистить последний запрос + Preserve Last Action Keyword + Select Last Action Keyword + Фиксированная высота окна + The window height is not adjustable by dragging. Максимальное количество результатов Вы также можете быстро настроить это с помощью CTRL+плюс и CTRL+минус. Игнорировать горячие клавиши в полноэкранном режиме @@ -74,13 +89,14 @@ Автообновление Выбор Скрыть Flow Launcher при запуске + Flow Launcher search window is hidden in the tray after starting up. Скрыть значок в трее Когда значок скрыт в трее, меню настройки можно открыть, щёлкнув правой кнопкой мыши на окне поиска. Точность поиска запросов Изменение минимального количества совпадений при поиске, необходимого для получения результатов. - Нет - Низкая - Обычная + None + Low + Regular Поиск с использованием пиньинь Позволяет использовать пиньинь для поиска. Пиньинь - это стандартная система латинизированной орфографии для перевода китайского языка. Всегда предпросмотр @@ -113,7 +129,8 @@ Версия Веб-сайт Удалить - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Магазин плагинов @@ -130,8 +147,6 @@ Этот плагин был обновлён за последние 7 дней Доступно новое обновление - - Тема Внешний вид @@ -146,8 +161,13 @@ Запуск программ от имени администратора или другого пользователя Удалятор процессов Завершение нежелательных процессов + Search Bar Height + Item Height Шрифт запросов - Шрифт результатов + Result Title Font + Result Subtitle Font + Reset + Customize Оконный режим Прозрачность Тема «{0}» не существует, откат к теме по умолчанию @@ -173,6 +193,8 @@ Своя Часы Дата + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. Горячая клавиша @@ -196,6 +218,7 @@ Cycle Previous Query Cycle Next Query Открыть контекстное меню + Open Native Context Menu Открыть окно настроек Copy File Path Toggle Game Mode @@ -272,11 +295,17 @@ Очистить журнал Вы уверены, что хотите удалить все журналы? Мастер + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info Выбор менеджера файлов - Укажите расположение файла в файловом менеджере, который вы используете, и добавьте аргументы, если необходимо. По умолчанию аргументами являются «%d», и путь вводится в этом месте. Например, если требуется команда, такая как «totalcmd.exe /A c:\windows», аргументом будет /A «%d». - «%f» - это аргумент, представляющий путь к файлу. Он используется для подчёркивания имени файла/папки при открытии определённого местоположения файла в стороннем файловом менеджере. Этот аргумент доступен только в пункте «Аргумент для файла». Если файловый менеджер не имеет такой функции, вы можете использовать «%d». + 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 fileds blank. Файловый менеджер Имя профиля Путь к файловому менеджеру @@ -339,6 +368,10 @@ Отменить Reset Удалить + OK + Yes + No + Фон Версия @@ -355,6 +388,9 @@ Отчёт успешно отправлен Не удалось отправить отчёт Произошёл сбой в Flow Launcher + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Пожалуйста, подождите... @@ -423,4 +459,8 @@ Команда sn Заметки + + File Size + Created + Last Modified diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml index 3f1d39f0c..332528b2b 100644 --- a/Flow.Launcher/Languages/sk.xaml +++ b/Flow.Launcher/Languages/sk.xaml @@ -1,7 +1,19 @@  + + + Flow zistil, že máte nainštalované {0} pluginy, ktoré vyžadujú na spustenie {1}. Chcete stiahnuť {1}? + {2}{2} + Ak už ho máte nainštalovaný, kliknite na nie, a budete vyzvaný na zadanie cesty k priečinku spustiteľného súboru {1} + + Vyberte spustiteľný súbor {0} + Nie je možné nastaviť cestu ku spustiteľnému súboru {0}, skúste to v nastaveniach Flowu (prejdite nadol). + Nepodarilo sa inicializovať pluginy + Pluginy: {0} – nepodarilo sa načítať a mal by byť zakázaný, na pomoc kontaktujte autora pluginu + Nepodarilo sa zaregistrovať klávesovú skratku "{0}". Klávesová skratka môže byť používaná iným programom. Zmeňte klávesovú skratku na inú alebo ukončite iný program. + Nepodarilo sa registrovať klávesovú skratku "{0}". Skúste to znova alebo si pozrite podrobnosti v denníku Flow Launcher Nepodarilo sa spustiť {0} Neplatný formát súboru pre plugin Flow Launchera @@ -33,6 +45,8 @@ Prenosný režim Uloží všetky nastavenia a používateľské údaje do jedného priečinka (Užitočné pri vymeniteľných diskoch a cloudových službách). Spustiť Flow Launcher pri spustení systému + Pre rýchlejšie spustenie použiť úlohu pri prihlásení namiesto položky po spustení + Po odinštalovaní musíte úlohu manuálne odstrániť (Flow.Launcher Startup) cez Plánovač úloh Chybné nastavenie spustenia pri spustení Schovať Flow Launcher po strate fokusu Nezobrazovať upozornenia na novú verziu @@ -54,6 +68,10 @@ Ponechať Označiť Vymazať + Ponechať posledný akčný príkaz + Označiť posledný akčný príkaz + Pevná výška okna + Výška okna sa nedá nastaviť ťahaním. Maximum výsledkov Túto hodnotu môžete rýchlo upraviť aj pomocou klávesových skratiek CTRL + znamienko plus (+) a CTRL + znamienko mínus (-). Ignorovať klávesové skratky v režime na celú obrazovku @@ -71,10 +89,14 @@ Automatická aktualizácia Vybrať Schovať Flow Launcher po spustení + Vyhľadávacie okno Flow launchera sa po spustení skryje v oblasti oznámení. Schovať ikonu z oblasti oznámení Keď je ikona skrytá z oblasti oznámení, nastavenia možno otvoriť kliknutím pravým tlačidlom myši na okno vyhľadávania. Presnosť vyhľadávania Mení minimálne skóre zhody potrebné na zobrazenie výsledkov. + Žiadna + Nízka + Normálna Vyhľadávanie pomocou pchin-jin Umožňuje vyhľadávanie pomocou pchin-jin. Pchin-jin je systém zápisu čínskeho jazyka pomocou písmen latinky. Vždy zobraziť náhľad @@ -107,7 +129,8 @@ Verzia Webstránka Odinštalovať - + Nepodarilo sa odstrániť nastavenia pluginu + Pluginy: {0} – Nepodarilo sa odstrániť súbory s nastaveniami pluginu, odstráňte ich manuálne Repozitár pluginov @@ -124,8 +147,6 @@ Tento plugin bol aktualizovaný za posledných 7 dní K dispozícii je nová aktualizácia - - Motív Vzhľad @@ -140,8 +161,13 @@ Spúšťanie programov ako správca alebo iný používateľ ProcessKiller Ukončenie nežiaducich procesov + Výška vyhľadávacieho panela + Výška položky Písmo vyhľadávacieho poľa - Písmo výsledkov + Písmo nadpisu výsledku + Písmo podnadpisu výsledku + Resetovať + Prispôsobiť Režim okno Nepriehľadnosť Motív {0} neexistuje, použije sa predvolený motív @@ -167,6 +193,8 @@ Vlastné Hodiny Dátum + Tento motív podporuje 2 režimy (svetlý/tmavý). + Tento motív podporuje rozostrenie priehľadného pozadia. Klávesové skratky @@ -190,6 +218,7 @@ Prejsť na predchádzajúci dopyt Prejsť na nasledujúci dopyt Otvoriť kontextovú ponuku + Otvoriť natívnu kontextovú ponuku Otvoriť okno s nastaveniami Kopírovať cestu k súboru Prepnúť herný režim @@ -266,11 +295,17 @@ Vymazať logy Naozaj chcete odstrániť všetky logy? Sprievodca + Cesta k používateľskému priečinku + Nastavenia používateľa a nainštalované pluginy sa ukladajú do používateľského priečinka. Toto umiestnenie sa môže líšiť v závislosti od toho, či je v prenosnom režime alebo nie. + Otvoriť priečinok + Úroveň logovania + Debug + Info Vyberte správcu súborov - Zadajte umiestnenie súboru správcu súborov, ktorý používate, a v prípade potreby pridajte argumenty. Predvolené argumenty sú "%d" a cesta sa zadáva na tomto mieste. Napríklad, ak sa vyžaduje príkaz, ako napríklad "totalcmd.exe /A c:\windows", argument je /A "%d". - "%f" je argument, ktorý predstavuje cestu k súboru. Používa sa na zvýraznenie názvu súboru/priečinka pri otváraní konkrétneho umiestnenia súboru v správcovi súborov tretej strany. Tento argument je k dispozícii len v položke "Arg. pre súbor". Ak správca súborov nemá túto funkciu, môžete použiť "%d". + 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 @@ -333,6 +368,10 @@ Ak pri zadávaní skratky pred ňu pridáte "@", bude sa zhodovať s Zrušiť Resetovať Odstrániť + Aktualizovať + Áno + Nie + Pozadie Verzia @@ -349,6 +388,9 @@ Ak pri zadávaní skratky pred ňu pridáte "@", bude sa zhodovať s Hlásenie bolo úspešne odoslané Odoslanie hlásenia zlyhalo Flow Launcher zaznamenal chybu + Prosím, otvorte nové issue na + 1. Nahrajte súbor logu: {0} + 2. Skopírujte nižšie uvedenú správu o výnimke Čakajte, prosím... @@ -417,4 +459,8 @@ Ak pri zadávaní skratky pred ňu pridáte "@", bude sa zhodovať s sn Sticky Notes + + Veľkosť súboru + Vytvorené + Upravené diff --git a/Flow.Launcher/Languages/sr.xaml b/Flow.Launcher/Languages/sr.xaml index 335a7deb7..b244c4660 100644 --- a/Flow.Launcher/Languages/sr.xaml +++ b/Flow.Launcher/Languages/sr.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Neuspešno pokretanje {0} Nepravilni Flow Launcher plugin format datoteke @@ -33,6 +45,8 @@ Portable Mode Store all settings and user data in one folder (Useful when used with removable drives or cloud services). Pokreni Flow Launcher pri podizanju sistema + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup Sakri Flow Launcher kada se izgubi fokus Ne prikazuj obaveštenje o novoj verziji @@ -54,6 +68,10 @@ Sačuvaj poslednji Upit Selektuj poslednji Upit Isprazni poslednji Upit + Preserve Last Action Keyword + Select Last Action Keyword + Fixed Window Height + The window height is not adjustable by dragging. Maksimum prikazanih rezultata You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. Ignoriši prečice u fullscreen režimu @@ -71,10 +89,14 @@ Auto ažuriranje Izaberi Sakrij Flow Launcher pri podizanju sistema + Flow Launcher search window is hidden in the tray after starting up. Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. Query Search Precision Changes minimum match score required for results. + None + Low + Regular Search with Pinyin Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese. Always Preview @@ -107,7 +129,8 @@ Verzija Website Uninstall - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Plugin Store @@ -124,8 +147,6 @@ This plugin has been updated within the last 7 days New Update is Available - - Tema Appearance @@ -140,8 +161,13 @@ Launch programs as admin or a different user ProcessKiller Terminate unwanted processes + Search Bar Height + Item Height Font upita - Font rezultata + Result Title Font + Result Subtitle Font + Reset + Customize Režim prozora Neprozirnost Theme {0} not exists, fallback to default theme @@ -167,6 +193,8 @@ Custom Clock Date + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. Prečica @@ -190,6 +218,7 @@ Cycle Previous Query Cycle Next Query Open Context Menu + Open Native Context Menu Open Setting Window Copy File Path Toggle Game Mode @@ -266,11 +295,17 @@ Clear Logs Are you sure you want to delete all logs? Wizard + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". + 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 fileds blank. File Manager Profile Name File Manager Path @@ -333,6 +368,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Otkaži Reset Obriši + OK + Yes + No + Background Verzija @@ -349,6 +388,9 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Izveštaj uspešno poslat Izveštaj neuspešno poslat Flow Launcher je dobio grešku + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Please wait... @@ -417,4 +459,8 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in sn Sticky Notes + + File Size + Created + Last Modified diff --git a/Flow.Launcher/Languages/tr.xaml b/Flow.Launcher/Languages/tr.xaml index 8a54a9b8a..5d550ebed 100644 --- a/Flow.Launcher/Languages/tr.xaml +++ b/Flow.Launcher/Languages/tr.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + - Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + "{0}" kısayolunu atama başarısız oldu. Kısayolu başka bir program kullanıyorsa kapatmayı deneyin veya kısayolu değiştirin. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher {0} başlatılamıyor Geçersiz Flow Launcher eklenti dosyası formatı @@ -17,218 +29,235 @@ Kopyala Kes Yapıştır - Undo - Select All + Geri Al + Tümünü Seç Dosya Klasör Yazı Oyun Modu - Kısayol Tuşlarının kullanımını durdurun. - Position Reset - Reset search window position + Kısayol tuşlarının kullanımını durdurun. + Pencere Konumunu Sıfırla + Arama penceresinin konumunu sıfırla Ayarlar Genel Taşınabilir Mod Tüm ayarları ve kullanıcı verilerini tek bir klasörde saklayın (Çıkarılabilir sürücüler veya bulut hizmetleri ile kullanıldığında kullanışlıdır). - Flow Launcher'u başlangıçta başlat - Error setting launch on startup - Odak pencereden ayrıldığında Flow Launcher'u gizle + Sistem ile Başlat + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler + Sistemle başlatma ayarı başarısız oldu + Odak Pencereden Ayrıldığında Gizle Güncelleme bildirimlerini gösterme - Search Window Position - Remember Last Position - Monitor with Mouse Cursor - Monitor with Focused Window - Primary Monitor - Custom Monitor - Search Window Position on Monitor - Center - Center Top - Left Top - Right Top - Custom Position + Pencere Konumu + Son Konumu Hatırla + Fare İmlecinin Bulunduğu Monitör + Aktif Pencerenin Bulunduğu Monitör + Birincil Monitör + Seçilen Monitör + Seçili Monitördeki Pencere Konumu + Orta + Orta Üst + Sol Üst + Sağ Üst + Özel Konum Dil - Pencere açıldığında - Flow Launcher yeniden etkinleştirildiğinde önceki sonuçları göster/gizle. - Son sorguyu sakla - Son sorguyu sakla ve tümünü seç - Sorgu kutusunu temizle - Maksimum sonuç sayısı - You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. - Tam ekran modunda kısayol tuşunu gözardı et - Tam ekran bir uygulama etkinken Flow Launcher etkinleştirmesini devre dışı bırakın (Oyunlar için önerilir). - Varsayılan Dosya Yöneticisi - Klasör açarken kullanılacak dosya yöneticisini seçin. - Varsayılan Tarayıcısı - Yeni Sekme, Yeni Pencere, Gizli Mod için Ayar. - Python Path - Node.js Path - Please select the Node.js executable - Please select pythonw.exe - Always Start Typing in English Mode - Temporarily change your input method to English mode when activating Flow. + Pencere Açıldığında + Flow Launcher etkinleştirildiğinde bir önceki sonuçları göster/gizle. + Son Sorguyu Sakla + Son Sorguyu Sakla ve Tümünü Seç + Sorgu Kutusunu Temizle + Preserve Last Action Keyword + Select Last Action Keyword + Sabit Pencere Yükseliği + Pencere yüksekliği sürükleme ile ayarlanamaz. + Maksimum Sonuç Sayısı + Bunu CTRL + ve CTRL - kısayollarıyla da ayarlayabilirsiniz. + Tam Ekran Modunda Kısayol Tuşunu Gözardı Et + Tam ekran bir uygulama etkinken Flow Launcher etkinleştirmesini devre dışı bırak (Oyunlar için önerilir). + Dosya Yönetici + Klasörleri açarken kullanılacak dosya yöneticisini. + İnternet Tarayıcı + Websiteleri açmak için kullanılacak tarayıcı, yeni sekme/pencere ve gizli mod ile açma seçenekleri. + Python Kurulumu + Node.js Kurulumu + node.exe dosyasını seçin + pythonw.exe dosyasını seçin + İngilizce Klayve Düzeni Kullan + Arama yaparken klayve düzenini geçici olarak İngilizce yap. Otomatik Güncelle Seç - Başlangıçta Flow Launcher'u gizle - Sistem çekmecesi simgesini gizle - When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. - Sorgu Arama Hassasiyeti - Sonuçlar için gereken minimum maç puanını değiştirir. - Pinyin kullanılmalı - Arama yapmak için Pinyin'in kullanılmasına izin verir. Pinyin, Çince'yi çevirmek için standart romanlaştırılmış yazım sistemidir. - Always Preview - Always open preview panel when Flow activates. Press {0} to toggle preview. - Mevcut temada bulanıklık efekti etkinken gölge efektine izin verilmez + Başlangıçta Pencereyi Gizle + Arama kutusu başlangıçtan sonra sistem tepsisinde gizlenir. + Sistem Tepsisinden Gizle + Arama penceresine sağ tıklayarak ayarları açabilirsiniz. + Arama Hassasiyeti + Sonuçlar için gereken minimum eşleşme puanını ayarla. + Hiçbiri + Düşük + Normal + Pinyin + Arama yapmak için Pinyin'in kullanılmasına izin ver. Pinyin, Çince'yi çevirmek için standart romanlaştırılmış yazım sistemidir. + Önizleme + Önizleme panelini her zaman aç. Önizlemeyi bu ayardan bağımsız {0} kısayolu ile açıp kapatabilirsiniz. + Mevcut temada bulanıklık efekti etkinken gölgelendirme efektine izin verilmez - Search Plugin - Ctrl+F to search plugins - No results found - Please try a different search. + Eklenti Ara + Ctrl+F ile arayın + Arama ile eşleşen bir eklenti bulunamadı + Farklı bir arama sorgusu deneyin. Eklenti Eklenti Daha fazla eklenti bul Açık Devre Dışı - Anahtar sözcüğü Ayar eylemi + Anahtar kelimeyi ayarla Anahtar Kelimeler - Varsayılan anahtar kelime eylemi - Yeni anahtar kelime eylemi - Anahtar kelime eylemini değiştir + Geçerli anahtar kelime + Yeni anahtar kelime + Anahtar kelimeyi değiştir Mevcut öncelik Yeni Öncelik Öncelik - Change Plugin Results Priority + Arama Sonuçlarındaki Önceliğini Ayarlayın Eklenti Klasörü - Yapımcı: + Geliştirici: Açılış Süresi: Sorgu Süresi: Sürüm İnternet Sitesi Kaldır - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Eklenti Mağazası - New Release - Recently Updated + Yeni Sürüm + Yakın Zamanda Güncellendi Eklentiler - Installed + Yüklü Yenile - Install + Yükle Kaldır Güncelle - Plugin already installed - New Version - This plugin has been updated within the last 7 days - New Update is Available - - + Eklenti zaten yüklü + Yeni Sürüm + Bu eklenti son 7 gün içerisinde güncellenmiş. + Yeni Bir Güncelleme Mevcut Temalar - Appearance + Görünüm Daha fazla tema bul - Nasıl bir tema yaratılır + Tema oluşturma Merhaba - Explorer - Search for files, folders and file contents - WebSearch - Search the web with different search engine support + Dosya Gezgini + Klasör, dosya ve dosya içeriklerini arayın + Web'de Arama + Birden fazla arama motoruyla web'de arayın Program - Launch programs as admin or a different user - ProcessKiller - Terminate unwanted processes + Programları yönetici veya başka bir kullanıcı hesabından çalıştırın + İşlemSonlandırıcı + İstenmeyen işlemleri sonlandırın + Arama Kutusu Yüksekliği + Öğe Yüksekliği Pencere Yazı Tipi - Sonuç Yazı Tipi + Arama Sonuçları Yazı Tipi + Arama Sonuçları Yazı Tipi + Sıfırla + Kişiselleştir Pencere Modu Saydamlık {0} isimli tema bulunamadı, varsayılan temaya dönülüyor. {0} isimli tema yüklenirken hata oluştu, varsayılan temaya dönülüyor. - Tema klasörü + Tema Klasörü Tema Klasörünü Aç - Renk düzeni + Renk Düzeni Sistem Varsayılanı Aydınlık Koyu Ses Efekti Arama penceresi açıldığında küçük bir ses oynat - Sound Effect Volume - Adjust the volume of the sound effect - Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume. - Animasyon - Arayüzde Animasyon Kullan - Animation Speed - The speed of the UI animation - Slow - Medium - Fast - Custom - Clock - Date + Ses Düzeyi + Ses efektinin seviyesini ayarla + Bu ayarın çalışması için Windows Media Player gerekli. Sisteminizde yüklü olduğundan emin olun. + Animasyonlar + Arayüzde animasyonlar kullan + Animasyon Hızı + Arayüz animasyonlarının hızı. + Yavaş + Orta + Hızlı + Özel + Saat + Tarih + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. Kısayol Tuşu Kısayol Tuşu - Open Flow Launcher - Enter shortcut to show/hide Flow Launcher. - Toggle Preview - Enter shortcut to show/hide preview in search window. - Hotkey Presets - List of currently registered hotkeys - Açık Sonuç Değiştiricileri - Select a modifier key to open selected result via keyboard. + Flow Launcher'ı Aç + Flow Launcher'ı açıp kapatmak için kullanılacak kısayol. + Önizleme Kısayolu + Arama sırasında önizlemeyi açıp kapatmak için kullanılacak kısayol. + Kısayol Ön Ayarları + Kayıtlı kısayolların listesi + Arama Sonuçlarını Açma Kısayolu + Arama sonuçlarını klavye ile açmak için bir anahtar tuş seçin. Kısayol Tuşunu Göster - Show result selection hotkey with results. - Auto Complete - Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page - Cycle Previous Query - Cycle Next Query - Open Context Menu - Open Setting Window - Copy File Path - Toggle Game Mode - Toggle History - Open Containing Folder - Run As Admin - Refresh Search Results - Reload Plugins Data - Quick Adjust Window Width - Quick Adjust Window Height - Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. + Arama sonuçlarının yanında açma kısayolunu göster. + Otomatik Tamamla + Seçilen öğeler için otomatik tamamlamayı çalıştırır. + Sonraki Öğeyi Seç + Bir Önceki Öğeyi Seç + Sonraki Sayfa + Bir Önceki Sayfa + Bir Önceki Sorguya Geç + Sonraki Sorguya Geç + Bağlam Menüsünü Aç + Open Native Context Menu + Ayarlar Penceresini Aç + Dosya Yolunu Kopyala + Oyun Modunu Aç/Kapat + Geçmişe Kaydetmeyi Aç/Kapat + Dosya Konumunu Aç + Yönetici Olarak Çalıştır + Arama Sonuçlarını Yenile + Eklenti Verilerini Yenile + Pencere Genişliğini Ayarla + Pencere Yüksekliğini Ayarla + Eklentilerin mevcut verilerini güncellemeleri gerektirdiğinde kullanın. + Bu işlev için bir veya birden fazla kısayol atayabilirsiniz. Özel Sorgu Kısayolları - Custom Query Shortcut - Built-in Shortcut - Query - Shortcut - Expansion + Özel Kısaltmalar + Dahili Kısaltmalar + Sorgu + Kısaltma + Sorgu Açıklama Sil Düzenle Ekle - None + Hiçbiri Lütfen bir öğe seçin - {0} eklentisi için olan kısayolu silmek istediğinize emin misiniz? - Are you sure you want to delete shortcut: {0} with expansion {1}? - Get text from clipboard. - Get path from active explorer. - Query window shadow effect - 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+]. - Use Segoe Fluent Icons - Use Segoe Fluent Icons for query results where supported - Press Key + {0} kısayolunu silmek istediğinize emin misiniz? + {0}: {1} kısaltmayı silmek istediğinize emin misiniz? + Kopyalanan metin + Dosya gezginindeki aktif dizin + Gölgelendirme Efekti + Önemli ölçüde grafik işlemci kullanır. Bilgisayarınızın performansı sınırlıysa önerilmez. + Pencere Genişliği + Bunu Ctrl+[ ve Ctrl+] kısayollarıyla da ayarlayabilirsiniz. + Segoe Fluent Simgeleri + Arama sonuçlarında mümkünse Segoe Fluent simgelerini kullan. + Tuşa basın Vekil Sunucu - HTTP vekil sunucuyu etkinleştir. + HTTP Vekil Sunucuyu Etkinleştir Sunucu Adresi Port Kullanıcı Adı @@ -244,53 +273,59 @@ Hakkında - Website + Websitesi GitHub - Docs + Dokümantasyon Sürüm - Icons - Şu ana kadar Flow Launcher'u {0} kez aktifleştirdiniz. + Kullanılan Simgeler + Şu ana kadar Flow Launcher'ı {0} kez aktifleştirdiniz. Güncellemeleri Kontrol Et - Become A Sponsor - Uygulamanın yeni sürümü ({0}) mevcut, Lütfen Flow Launcher'u yeniden başlatın. + Sponsor Olun + Uygulamanın yeni sürümü ({0}) mevcut, Lütfen Flow Launcher'ı yeniden başlatın. Güncelleme kontrolü başarısız oldu. Lütfen bağlantınız ve vekil sunucu ayarlarınızın api.github.com adresine ulaşabilir olduğunu kontrol edin. Güncellemenin yüklenmesi başarısız oldu. Lütfen bağlantınız ve vekil sunucu ayarlarınızın github-cloud.s3.amazonaws.com adresine ulaşabilir olduğunu kontrol edin ya da https://github.com/Flow-Launcher/Flow.Launcher/releases adresinden güncellemeyi elle indirin. Sürüm Notları - Usage Tips - DevTools - Setting Folder - Log Folder - Clear Logs - Are you sure you want to delete all logs? - Wizard + İpuçları + Geliştirici Araçları + Ayarlar Klasörü + Günlük Klasörü + Günlükleri Temizle + Tüm günlük kayıtlarını silmek istediğinize emin misiniz? + Kurulum Sihirbazı + Kullanıcı Verisi Dizini + Kullanıcı ayarları ve yüklü eklentiler bu klasörde saklanır. Klasörün konumu taşınabilir moda bağlı olarak değişebilir. + Klasörü Aç + Log Level + Debug + Info - Select File Manager - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". - File Manager - Profile Name - File Manager Path - Arg For Folder - Arg For File + Dosya Yöneticisi Seçenekleri + 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 fileds blank. + Dosya Yöneticisi + Profil Adı + Dosya Yöneticisi Yolu + Klasör Açarken + Dosya Açarken - Varsayılan İnternet Tarayıcısı - 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 + İnternet Tarayıcı Seçenekleri + "Default" ayarı sisteminizdeki varsayılan internet tarayıcıyı kullanır. + Tarayıcı + Profil Adı + Tarayıcı Yolu + Yeni Pencere + Yeni Sekme + Gizli Mod için Bağımsız Değişken - 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! + Önceliği Ayarla + Sayı ne kadar büyükse, eklenti arama sonuçlarında da o kadar yüksekte sıralanır. Eğer eklentinin sağladığı sonuçların en altta yer almasını istiyorsanız negatif bir sayı girin. + Geçerli bir tam sayı girin! Eski Anahtar Kelime @@ -299,45 +334,47 @@ Tamam Belirtilen eklenti bulunamadı Yeni anahtar kelime boş olamaz - Yeni anahtar kelime başka bir eklentiye atanmış durumda. Lütfen başka bir anahtar kelime seçin + Yeni anahtar kelime başka bir eklentiye atanmış durumda. Lütfen başka bir anahtar kelime seçin. Başarılı - Completed successfully - Anahtar kelime belirlemek istemiyorsanız * kullanın + Başarıyla tamamlandı + Herhangi bir anahtar kelime belirlemek istemiyorsanız * kullanın Özel Sorgu Kısayolları - Press a custom hotkey to open Flow Launcher and input the specified query automatically. + Flow Launcher'ı açıp otomatik olarak girdiğiniz sorguyu aratması için bir kısayol atayın. Önizleme - Kısayol tuşu kullanılabilir değil, lütfen başka bir kısayol tuşu seçin + Kısayol tuşu kullanılamıyor, lütfen başka bir kombinasyon girin. Geçersiz eklenti kısayol tuşu Güncelle - 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. + Kısayol Atanıyor + Kullanılamıyor + Bu kısayol "{0}" için ayrılmıştır, lütfen başka bir kısayol deneyin. + Bu kısayol zaten "{0}" için kullanılıyor. Eğer "Üstüne Yaz"'ı seçerseniz, "{0}" sorgusu bu kısayol ile kullanılamayacak. + Bu işleve atamak istediğiniz kısayol tuşlarına basın. - Custom Query Shortcut - Enter a shortcut that automatically expands to the specified query. - A shortcut is expanded when it exactly matches the query. - -If you add an '@' prefix while inputting a shortcut, it matches any position in the query. Builtin shortcuts match any position in a query. + Özel Kısaltmalar + Yazdığınızda otomatik olarak belirtilen sorguya tamamlanacak bir kısaltma girin. + Kısaltmanın sorgunun herhangi bir yerinde çalışması için başına @ ekleyin. - Shortcut already exists, please enter a new Shortcut or edit the existing one. - Shortcut and/or its expansion is empty. + Anahtar kelime zaten mevcut. Yeni bir kısaltma girin veya mevcut kısaltmayı düzenleyin. + Kısaltma ve/veya sorgu eksik. Kaydet - Overwrite + Üzerine Yaz İptal - Reset + Sıfırla Sil + Güncelle + Yes + No + Arka plan Sürüm Tarih - Sorunu çözebilmemiz için lütfen uygulamanın ne yaparken çöktüğünü belirtin. + Sorunu çözebilmemiz için lütfen uygulamanın ne yaparken çöktüğünü belirtin Raporu Gönder İptal Genel @@ -348,73 +385,80 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in Gönderiliyor Hata raporu başarıyla gönderildi Hata raporu gönderimi başarısız oldu - Flow Launcher'ta bir hata oluştu + Flow Launcher'da bir hata oluştu + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message - Please wait... + Lütfen bekleyin... - Checking for new update - You already have the latest Flow Launcher version - Update found - Updating... + Güncellemeler denetleniyor + Güncel sürümü kullanıyorsunuz + Güncelleme bulundu + Güncelleniyor... - 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 kullanıcı profilinizi güncel sürüme taşımakta başarısız oldu. + Lütfen {0} klasörünü {1}'e taşıyın - New Update - Flow Launcher'un yeni bir sürümü ({0}) mevcut + Yeni Güncelleme + Flow Launcher'ın yeni bir sürümü ({0}) mevcut Güncellemelerin kurulması sırasında bir hata oluştu Güncelle İptal - Update Failed - Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com. - Bu güncelleme Flow Launcher'u yeniden başlatacaktır - Aşağıdaki dosyalar güncelleştirilecektir + Güncelleme Başarısız Oldu + Güncelleme kontrolü başarısız oldu. Lütfen bağlantınız ve vekil sunucu ayarlarınızın github-cloud.s3.amazonaws.com adresine ulaşabilir olduğunu kontrol edin. + Güncelleme işlemi Flow Launcher'ı yeniden başlatacak + Aşağıdaki dosyalar güncelleştirilecek Güncellenecek dosyalar Güncelleme açıklaması - Skip - Welcome to Flow Launcher - Hello, this is the first time you are running Flow Launcher! - Before starting, this wizard will assist in setting up Flow Launcher. You can skip this if you wish. Please choose a language - Search and run all files and applications on your PC - Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse. - Flow Launcher starts with the hotkey below, go ahead and try it out now. To change it, click on the input and press the desired hotkey on the keyboard. - Hotkeys - Action Keyword and Commands - Search the web, launch applications or run various functions through Flow Launcher plugins. Certain functions start with an action keyword, and if necessary, they can be used without action keywords. Try the queries below in Flow Launcher. - Let's Start Flow Launcher - Finished. Enjoy Flow Launcher. Don't forget the hotkey to start :) + Atla + Flow Launcher Sihirbazına Hoşgeldiniz + Merhaba, görünüşe göre Flow Launcher'ı ilk defa çalıştırıyorsunuz! + Başlamadan önce, bu sihirbaz Flow Launcher'ı kişiselleştirmenize yardım edecek. İsterseniz bu adımı atlayabilirsiniz. + Tüm Dosya ve Uygulamalarınıza Kolayca Erişin + Uygulamalardan dosyalara, yer imlerine, YouTube'a, Twitter'a ve daha da fazlasına kadar her şeyi arayın. Hepsi klavyenizin rahatlığında, fareye hiç dokunmadan. + Flow Launcher aşağıdaki kısayol tuşu ile başlar, ne duruyorsunuz deneyin hadi! Değiştirmek için üzerine tıklayın ve istediğiniz kısayol tuşuna basın. + Kısayol Tuşları + Anahtar Kelimeler ve Komutlar + Eklentiler aracılığıyla web'de arama yapın, uygulamaları başlatın veya çeşitli işlevleri çalıştırın. Bazı işlevler varsayılan olarak bir anahtar sözcüğü gerektirir, ayarlardan kaldırılırsa anahtar sözcüğü olmadan da kullanılabilirler. Aşağıdaki sorguları Flow Launcher'da deneyin. + Hepsi Bu + Flow Launcher'ın keyfini çıkarın. Başlatma kısayolunu sakın unutmayın :) - Back / Context Menu - Item Navigation - Open Context Menu - Open Containing Folder - Run as Admin / Open Folder in Default File Manager - Query History - Back to Result in Context Menu - Autocomplete - Open / Run Selected Item - Open Setting Window - Reload Plugin Data + Geri/Bağlam Menüsü + Öğeler Arasında Gezinme + Bağlam Menüsünü Aç + Dosya Konumunu Aç + Yönetici Olarak Çalıştır + Sorgu Geçmişine Göz At + Bağlam Menüsünden Arama Sonuçlarına Dön + Otomatik Tamamla + Seçili Öğeyi Aç + Ayarları Aç + Eklenti Verisini Yenile - Select first result - Select last result - Run current query again - Open result - Open result #{0} + İlk Arama Sonucunu Seç + Son Arama Sonucunu Seç + Geçerli Sorguyu Yenile + Arama sonucunu aç + #{0} arama sonucunu aç - Weather - Weather in Google Result + Hava Durumu + Google aramadan hava durumu > ping 8.8.8.8 - Shell Command + Kabuk Komutu s Bluetooth - Bluetooth in Windows Settings + Bluetooth ayarları sn - Sticky Notes + Yapışkan Notlar + + Dosya Boyutu + Oluşturuldu + Değiştirme Tarihi diff --git a/Flow.Launcher/Languages/uk-UA.xaml b/Flow.Launcher/Languages/uk-UA.xaml index 88f88b7cb..95a746a51 100644 --- a/Flow.Launcher/Languages/uk-UA.xaml +++ b/Flow.Launcher/Languages/uk-UA.xaml @@ -1,7 +1,19 @@  + + + Flow виявив, що ви встановили плагіни {0}, для запуску яких потрібно {1}. Бажаєте завантажити {1}? + {2}{2} + Клацніть Ні, якщо воно вже встановлене, і вам буде запропоновано вибрати теку, яка містить виконуваник {1} + + Будласка оберіть виконуваник {0} + Не вдається встановити шлях до виконуваника {0}, будласка спробуйте в налаштуваннях Flow (прокрутіть вниз до кінця). + Невдача ініціалізації плагінів + Плагіни: {0} - не вдалося підвантажити і буде вимкнено, зверніться за допомогою до автора плагіна + Не вдалося зареєструвати гарячу клавішу "{0}". Можливо, гаряча клавіша використовується іншою програмою. Змініть її на іншу гарячу клавішу або вийдіть з програми, де вона використовується. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Не вдалося запустити {0} Невірний формат файлу плагіна Flow Launcher @@ -33,6 +45,8 @@ Портативний режим Зберігати всі налаштування і дані користувача в одній теці (буде корисно при видаленні дисків або хмарних сервісах). Запускати Flow Launcher при запуску системи + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Помилка запуску налаштування під час запуску Сховати Flow Launcher, якщо втрачено фокус Не повідомляти про доступні нові версії @@ -54,6 +68,10 @@ Зберегти останній запит Вибрати останній запит Очистити останній запит + Preserve Last Action Keyword + Select Last Action Keyword + Фіксована висота вікна + Висота вікна не регулюється перетягуванням. Максимальна кількість результатів Ви також можете швидко налаштувати цей параметр за допомогою клавіш CTRL+Плюс чи CTRL+Мінус. Ігнорувати гарячі клавіші в повноекранному режимі @@ -71,10 +89,14 @@ Автоматичне оновлення Вибрати Сховати Flow Launcher при запуску системи + Вікно пошуку Flow Launcher після запуску ховається в треї. Приховати значок в системному лотку Коли піктограму приховано з трею, меню налаштувань можна відкрити, клацнувши правою кнопкою миші у вікні пошуку. Точність пошуку запитів Змінює мінімальний бал збігів, необхідних для результатів. + Нема + Мало + Звичайно Використовувати піньїнь Дозволяє використовувати пінїнь для пошуку. Піньїнь - це стандартна система написання для перекладу китайської. Завжди переглядати @@ -107,7 +129,8 @@ Версія Сайт Видалити - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Магазин плагінів @@ -124,8 +147,6 @@ Цей плагін було оновлено протягом останніх 7 днів Доступне нове оновлення - - Тема Зовнішній вигляд @@ -140,8 +161,13 @@ Запуск програм від імені адміністратора або іншого користувача Процес-кілер Припинення небажаних процесів + Висота рядка пошуку + Висота елемента Шрифт запитів - Шрифт результатів + Шрифт заголовка результату + Шрифт підзаголовка результату + Скинути + Підлаштувати Віконний режим Прозорість Тема {0} не існує, повернення до теми за замовчуванням @@ -156,7 +182,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. Будласка перевірте вашу інсталяцію, якщо вам потрібно відрегулювати гучність. Анімація Використовувати анімацію в інтерфейсі Швидкість анімації @@ -167,41 +193,44 @@ Користувацька Годинник Дата + This theme supports two(light/dark) modes. + Ця тема підтримує розмитий прозорий фон. Гаряча клавіша Гарячі клавіші - Open Flow Launcher + Відкрити Flow Launcher Введіть скорочення, щоб показати/приховати Flow Launcher. - Toggle Preview + Перемкнути поперегляд Введіть скорочення, щоб показати/приховати попередній перегляд у вікні пошуку. - Hotkey Presets - List of currently registered hotkeys + Пресети галавіш + Список поточних зареєстрованих галавіш Відкрити ключ зміни результатів Виберіть ключ модифікатора для відкриття вибраних результатів за допомогою клавіатури. Показати гарячу клавішу Показати гарячу клавішу вибору результату з результатами. - Auto Complete - Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page - Cycle Previous Query - Cycle Next Query + Автозавершення + Запускає автозавершення для вибраних елементів. + Виберіть наступний елемент + Виберіть наступний елемент + Наступна сторінка + Попередня сторінка + Циклувати попередній запит + Циклувати наступний запит Відкрити контекстне меню + Відкрити рідне контекстне меню Відкрити вікно налаштувань - Copy File Path + Копіювати шлях файлу Перемкнути режим гри - Toggle History + Перемкнути історію Відкрийте папку, що містить файл - Run As Admin - Refresh Search Results - Reload Plugins Data - Quick Adjust Window Width - Quick Adjust Window Height - Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. + Запустити від імені адміністратора + Оновити результати пошуку + Перезавантажити дані плагінів + Швидке регулювання ширини вікна + Швидке регулювання висоти вікна + Використовуйте, коли плагінам потрібно перезавантажити та оновити наявні дані. + Ви можете додати ще одну галавішу для цієї функції. Задані гарячі клавіші для запитів Користувацькі скорочення запитів Вбудовані скорочення @@ -212,7 +241,7 @@ Видалити Редагувати Додати - None + Нема Спочатку виберіть елемент Ви впевнені, що хочете видалити гарячу клавішу ({0}) плагіну? Ви впевнені, що хочете видалити скорочення: {0} з розширенням {1}? @@ -266,11 +295,17 @@ Очистити журнали Ви впевнені, що хочете видалити всі журнали? Чаклун + Розташування даних користувача + Налаштування користувача та встановлені плагіни зберігаються у теці даних користувача. Це місце може змінюватися залежно від того, чи перебуває програма в портативному режимі, чи ні. + Відкрити теку + Log Level + Debug + Info Виберіть файловий менеджер - Будь ласка, вкажіть розташування файлу у файловому менеджері, який ви використовуєте, і додайте аргументи, якщо це необхідно. За замовчуванням аргументами є "%d", і шлях вводиться у вказаному місці. Наприклад, якщо потрібно виконати команду типу "totalcmd.exe /A c:\windows", аргументом буде /A "%d". - "%f" - це аргумент, який представляє шлях до файлу. Він використовується для виділення імені файлу/теки при відкритті певного місця розташування файлу у сторонньому файловому менеджері. Цей аргумент доступний лише у пункті "Аргумент для файлу". Якщо файловий менеджер не має такої функції, ви можете використовувати "%d". + 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 fileds blank. Файловий менеджер Ім'я профілю Шлях до файлового менеджера @@ -311,11 +346,11 @@ Гаряча клавіша недоступна. Будь ласка, вкажіть нову Недійсна гаряча клавіша плагіна Оновити - 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. + Прив'язка галавіші + Поточна галавіша недоступна. + Ця галавіша зарезервована для «{0}» і не може бути використана. Будласка, виберіть іншу галавішу. + Ця галавіша вже використовується «{0}». Якщо ви натиснете «Перезаписати», її буде вилучено з «{0}». + Натисніть клавіші, які ви хочете використовувати для цієї функції. Власне скорочення запиту @@ -329,10 +364,14 @@ Зберегти - Overwrite + Перезаписати Скасувати - Reset + Скинути Видалити + Добре + Так + Ні + Тло Версія @@ -349,6 +388,9 @@ Звіт успішно відправлено Не вдалося відправити звіт Стався збій в додатку Flow Launcher + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Будь ласка, зачекайте... @@ -402,11 +444,11 @@ Відкрити вікно налаштувань Перезавантажити дані плагінів - Select first result - Select last result - Run current query again - Open result - Open result #{0} + Вибрати перший результат + Вибрати останній результат + Виконати поточний запит знову + Відкрити результат + Відкрити результат #{0} Погода Погода в результатах Google @@ -417,4 +459,8 @@ sn Липкі нотатки + + Розмір файлу + Створено + Востаннє змінено diff --git a/Flow.Launcher/Languages/vi.xaml b/Flow.Launcher/Languages/vi.xaml index dbb8b9a05..17e421e16 100644 --- a/Flow.Launcher/Languages/vi.xaml +++ b/Flow.Launcher/Languages/vi.xaml @@ -1,8 +1,20 @@  + + + Flow phát hiện bạn đã cài plugin {0}, plugin này cần {1} để chạy. Bạn có muốn tải {1}? + {2}{2} + Hãy chọn không nếu nó đã được cài đặt, và bạn sẽ được hỏi chọn thư mục chứa chương trình thực thi {1} + + Please select the {0} executable + 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 + Không thể đăng ký phím nóng "{0}". Phím nóng có thể được sử dụng bởi một chương trình khác. Chuyển sang phím nóng khác hoặc thoát khỏi chương trình khác. - Trình khởi chạy luồng + Failed to unregister hotkey "{0}". Please try again or see log for details + Flow Launcher Không thể khởi động {0} Định dạng tệp plugin Flow Launcher không chính xác Đặt ở vị trí trên cùng trong truy vấn này @@ -33,6 +45,8 @@ Chế độ Portabler Lưu trữ tất cả cài đặt và dữ liệu người dùng trong một thư mục (hữu ích khi sử dụng với thiết bị lưu trữ di động hoặc dịch vụ đám mây). Khởi động Flow Launcher khi khởi động hệ thống + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Không lưu được tính năng tự khởi động khi khởi động hệ thống Ẩn Flow Launcher khi mất tiêu điểm Không hiển thị thông báo khi có phiên bản mới @@ -54,6 +68,10 @@ Giữ lại truy vấn cuối cùng Chọn truy vấn cuối cùng Trống truy vấn cuối cùng + Preserve Last Action Keyword + Select Last Action Keyword + Giữ nguyên chiều cao cửa sổ + Chiều cao cửa sổ không thể thay đổi bằng cách kéo. Số kết quả tối đa Có thể thiết lập nhanh chóng bằng CTRL+Plus và CTRL+Minus. Bỏ qua phím nóng khi cửa sổ ở chế độ toàn màn hình @@ -70,11 +88,15 @@ Tạm thời chuyển phương thức nhập sang tiếng Anh khi kích hoạt Flow. Cập nhật tự động Chọn - Ẩn Trình khởi chạy luồng khi khởi động + Ẩn Flow Launcher khi khởi động + Flow Launcher sẽ ẩn trong khay hệ thống sau khi khởi động. Ẩn biểu tượng thanh trạng thái Khi biểu tượng bị ẩn khỏi khay, bạn có thể mở menu Cài đặt bằng cách nhấp chuột phải vào cửa sổ tìm kiếm. Độ chính xác của tìm kiếm truy vấn Kết quả tìm kiếm bắt buộc. + Không + Thấp + Bình thường Hoạt động bính âm Cho phép bạn sử dụng Bính âm để tìm kiếm. Bính âm là hệ thống ký hiệu La Mã tiêu chuẩn để dịch văn bản tiếng Trung. Luôn xem trước @@ -107,7 +129,8 @@ Phiên bản Trang web Gỡ cài đặt - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Tải tiện ích mở rộng @@ -124,8 +147,6 @@ Plugin này đã được cập nhật trong vòng 7 ngày qua Đã có bản cập nhật mới - - Giao Diện Giao diện @@ -140,8 +161,13 @@ Khởi chạy chương trình với tư cách quản trị viên hoặc người dùng khác Buộc Tắt Tiến Trình Chấm dứt các tiến trình không mong muốn + Chiều cao thanh tìm kiếm + Item Height Phông chữ hộp truy vấn - Phông chữ kết quả + Result Title Font + Result Subtitle Font + Đặt lại + Customize chế độ cửa sổ độ mờ Chủ đề {0} không tồn tại nên mẫu mặc định được kích hoạt @@ -167,6 +193,8 @@ Tùy chỉnh Giờ Ngày + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. Phím tắt @@ -190,18 +218,19 @@ Cycle Previous Query Cycle Next Query Mở menu ngữ cảnh + Open Native Context Menu Mở cửa sổ cài đặt Chép đường dẫn tập tin Chuyển đổi chế độ trò chơi Chuyển đổi lịch sử Mở thư mục chứa Chạy với quyền admin - Refresh Search Results + Làm mới kết quả tìm kiếm Dữ liệu plugin không tải - Quick Adjust Window Width - Quick Adjust Window Height - Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. + Điều chỉnh nhanh độ rộng cửa sổ + Điều chỉnh nhanh chiều cao cửa sổ + Sử dụng khi yêu cầu plugin tải lại và cập nhật dữ liệu hiện có của chúng. + Bạn có thể thêm một phím nóng nữa cho chức năng này. Phím tắt truy vấn tùy chỉnh Lối tắt truy vấn tùy chỉnh Phím tắt tích hợp @@ -268,11 +297,17 @@ Xóa tệp nhật ký Bạn có chắc chắn muốn xóa tất cả nhật ký không? Wizard + Vị trí dữ liệu người dùng + Thiết đặt người dùng và plugin đã cài đặt sẽ được lưu trong thư mục dữ liệu người dùng. Vị trí này có thể thay đổi tùy thuộc vào việc nó có ở chế độ di động hay không. + Mở thư mục + Log Level + Debug + Info Chọn trình quản lý tệp - Vui lòng chỉ định vị trí tệp của trình quản lý tệp mà bạn đang sử dụng và thêm đối số nếu cần. Đối số mặc định là "%d" và một đường dẫn được nhập tại vị trí đó. Ví dụ: Nếu một lệnh được yêu cầu như "totalcmd.exe /A c:\windows", đối số là /A "%d". - "%f" là một đối số đại diện cho đường dẫn tệp. Nó được sử dụng để nhấn mạnh tên tệp/thư mục khi mở một vị trí tệp cụ thể trong trình quản lý tệp của bên thứ 3. Đối số này chỉ có sẵn trong phần "Arg for File" mục. Nếu trình quản lý tệp không có chức năng đó, bạn có thể sử dụng "%d". + 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 fileds blank. Trình quản lý ngày tháng Tên hồ sơ Đường dẫn quản lý tệp @@ -314,7 +349,7 @@ Tổ hợp phím plugin không hợp lệ Cập nhật Binding Hotkey - Current hotkey is unavailable. + Phím nóng hiện tại không có sẵn. 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. @@ -337,6 +372,10 @@ Hủy Đặt lại Xóa + OK + + Không + Nền Phiên bản @@ -353,6 +392,9 @@ Đã gửi báo cáo thành công Báo cáo lỗi Trình khởi chạy luồng có lỗi + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message Cảnh báo nhỏ... @@ -408,11 +450,11 @@ Mở cửa sổ cài đặt Dữ liệu plugin không tải - Select first result - Select last result - Run current query again - Open result - Open result #{0} + Chọn kết quả đầu tiên + Chọn kết quả cuối cùng + Chạy lại truy vấn hiện tại + Mở kết quả + Mở kết quả #{0} Thời Tiết Thời tiết từ kết quả của Google @@ -423,4 +465,8 @@ sn Ghi chú + + Kích thước tệp + Đã tạo + Sửa đổi lần cuối diff --git a/Flow.Launcher/Languages/zh-cn.xaml b/Flow.Launcher/Languages/zh-cn.xaml index dd24444b0..d9966d757 100644 --- a/Flow.Launcher/Languages/zh-cn.xaml +++ b/Flow.Launcher/Languages/zh-cn.xaml @@ -1,7 +1,19 @@  + + + Flow 检测到您已安装 {0} 个插件,需要 {1} 才能运行。是否要下载 {1}? + {2}{2} + 如果已安装,请单击“否”,系统将提示您选择包含 {1} 可执行文件的文件夹 + + 请选择 {0} 可执行文件 + 无法设置 {0} 可执行路径,请尝试从 Flow 的设置中设置(向下滚动到底部)。 + 无法初始化插件 + 插件:{0} - 无法加载并将被禁用,请联系插件创建者寻求帮助 + 无法注册热键“{0}”。该热键可能正在被其他程序使用。更改为不同的热键,或退出其他程序。 + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher 启动命令 {0} 失败 无效的 Flow Launcher 插件文件格式 @@ -23,7 +35,7 @@ 目录 文本 游戏模式 - 暂停使用快捷键。 + 暂停使用热键。 重置位置 重置搜索窗口位置 @@ -33,6 +45,8 @@ 便携模式 将所有设置和用户数据存储在一个文件夹中 (可用于可移除驱动器或云服务)。 开机自启 + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler 设置开机自启时出错 失去焦点时自动隐藏 Flow Launcher 不显示新版本提示 @@ -54,6 +68,10 @@ 保留上次搜索关键字 选择上次搜索关键字 清空上次搜索关键字 + Preserve Last Action Keyword + Select Last Action Keyword + 固定窗口高度 + 窗口高度不能通过拖动来调整。 最大结果显示个数 您也可以通过使用 CTRL+ "+" 和 CTRL+ "-" 来快速调整它。 全屏模式下忽略热键 @@ -71,10 +89,14 @@ 自动更新 选择 系统启动时不显示主窗口 + Flow Launcher 启动后搜索窗口隐藏在托盘中。 隐藏任务栏图标 任务栏图标被隐藏时,右键点击搜索窗口即可打开设置菜单。 查询搜索精度 更改匹配成功所需的最低分数。 + + + 常规 使用拼音搜索 允许使用拼音进行搜索。 始终打开预览 @@ -107,7 +129,8 @@ 版本 官方网站 卸载 - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually 插件商店 @@ -124,8 +147,6 @@ 此插件在过去7天内有更新 有可用的更新 - - 主题 外观 @@ -140,8 +161,13 @@ 以管理员或其他用户身份启动程序 进程杀手 终止不需要的进程 + 搜索栏高度 + 项目高度 查询框字体 - 结果项字体 + 结果标题字体 + 结果字幕字体 + 重置 + 自定义 窗口模式 透明度 无法找到主题 {0} ,切换为默认主题 @@ -156,7 +182,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 媒体播放器不可用,需要它来调节 Flow 的音量。如果您需要调节音量,请检查您的安装。 动画 启用动画 动画速度 @@ -167,41 +193,44 @@ 自定义 时钟 日期 + 该主题支持两种(浅色/深色)模式。 + 该主题支持模糊透明背景。 热键 热键 - Open Flow Launcher + 打开 Flow Launcher 输入显示/隐藏 Flow Launcher 的快捷键。 - Toggle Preview + 切换预览 输入在搜索窗口中开启/关闭预览的快捷键。 - Hotkey Presets - List of currently registered hotkeys + 热键预设 + 当前注册的热键列表 打开结果快捷键修饰符 选择一个用以打开搜索结果的按键修饰符。 显示热键 - 显示用于打开结果的快捷键。 - Auto Complete - Runs autocomplete for the selected items. - Select Next Item - Select Previous Item - Next Page - Previous Page - Cycle Previous Query - Cycle Next Query + 显示用于打开结果的热键。 + 自动完成 + 对所选项目运行自动完成功能。 + 选择下一个项目 + 选择上一个项目 + 下一页 + 上一页 + 循环上一个查询 + 循环下一个查询 打开菜单目录 + 打开本机上下文菜单 打开设置窗口 - Copy File Path + 复制文件路径 切换游戏模式 - Toggle History + 切换历史记录 打开所在目录 - Run As Admin - Refresh Search Results - Reload Plugins Data - Quick Adjust Window Width - Quick Adjust Window Height - Use when require plugins to reload and update their existing data. - You can add one more hotkey for this function. + 以管理员身份运行 + 刷新搜索结果 + 重新加载插件数据 + 快速调整窗口宽度 + 快速调整窗口高度 + 当需要插件重新加载并更新其现有数据时使用。 + 您可以为此功能添加一个热键。 自定义查询热键 自定义查询捷径 内置捷径 @@ -212,7 +241,7 @@ 删除 编辑 添加 - None + 请选择一项 你确定要删除插件 {0} 的热键吗? 你确定要删除捷径 {0} (展开为 {1})? @@ -266,11 +295,17 @@ 清除日志 你确定要删除所有的日志吗? 向导 + 用户数据位置 + 用户设置和已安装的插件保存在用户数据文件夹中。此位置可能因是否处于便携模式而异。 + 打开文件夹 + Log Level + Debug + Info 默认文件管理器 - 请指定文件管理器的文件位置,并在必要时修改参数。 默认参数是%d",作为占位符代表文件路径。 例如命令 “totalcmd.exe /A c:\winds”,参数是 /A "%d”。 - %f是一个表示文件路径的参数。 它用于在第三方文件管理器中打开特定文件位置时强调文件/文件夹名称。 此参数仅在“选中文件参数”项目中可用。 如果文件管理器没有该功能,“%d” 仍然可用。 + 请指定您使用的文件管理器的文件位置并根据需要添加参数。“%d”表示要打开的目录路径,由文件夹字段的参数和打开特定目录的命令使用。“%f”表示要打开的文件路径,由文件字段的参数和打开特定文件的命令使用。 + 例如,如果文件管理器使用诸如“totalcmd.exe /A c:\windows”之类的命令来打开 c:\windows 目录,则文件管理器路径将为 totalcmd.exe,文件夹参数将为 /A "%d"。某些文件管理器(如 QTTabBar)可能只需要提供路径,在本例中,使用“%d”作为文件管理器路径,其余字段留空。 文件管理器 档案名称 文件管理器路径 @@ -311,11 +346,11 @@ 热键不可用,请选择一个新的热键 插件热键不合法 更新 - 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. + 绑定热键 + 当前热键不可用。 + 此热键为“{0}”保留,无法使用。请选择其他热键。 + 此热键已被“{0}”使用。如果按“覆盖”,则会将其从“{0}”中删除。 + 按下您想要用于此功能的键。 自定义查询捷径 @@ -329,10 +364,14 @@ 保存 - Overwrite + 覆盖 取消 - Reset + 重置 删除 + 更新 + + + 背景 版本 @@ -349,6 +388,9 @@ 发送成功 发送失败 Flow Launcher 出错啦 + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message 请稍等... @@ -382,11 +424,11 @@ 搜索并运行您PC上的文件和应用程序 搜索所有应用程序、 文件、 书签、 YouTube、 Twitter等。所有都只需要键盘而不需要鼠标。 Flow Launcher 默认使用下面的快捷键激活。 要更改它,请点击输入并按键盘上所需的热键。 - 快捷键 + 热键 动作关键词和命令 通过 Flow Launcher 插件搜索网站、启动应用程序或运行各种功能。某些功能使用一个动作关键词激活,如有必要,它们也可以在没有动作关键词的情况下使用。欢迎尝试以下的查询语句。 开始使用 Flow Launcher - 完成了!享受 Flow Launcher。不要忘记激活快捷键 :) + 大功告成。尽情使用 Flow Launcher 吧。别忘了用热键启动 :) @@ -402,11 +444,11 @@ 打开设置窗口 重新加载插件数据 - Select first result - Select last result - Run current query again - Open result - Open result #{0} + 选择第一个结果 + 选择最后一个结果 + 再次运行当前查询 + 打开结果 + 打开结果 #{0} 天气 谷歌天气结果 @@ -417,4 +459,8 @@ sn 便笺 + + 文件大小 + 创建于 + 最后修改于 diff --git a/Flow.Launcher/Languages/zh-tw.xaml b/Flow.Launcher/Languages/zh-tw.xaml index 3b0f19638..01b667e72 100644 --- a/Flow.Launcher/Languages/zh-tw.xaml +++ b/Flow.Launcher/Languages/zh-tw.xaml @@ -1,7 +1,19 @@  + + + Flow detected you have installed {0} plugins, which will require {1} to run. Would you like to download {1}? + {2}{2} + Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable + + Please select the {0} executable + 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 + Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher 啟動命令 {0} 失敗 無效的 Flow Launcher 外掛格式 @@ -33,6 +45,8 @@ 便攜模式 將所有設定和使用者資料存儲在一個資料夾中(當與可移動磁碟或雲服務一起使用時很有用)。 開機時啟動 + Use logon task instead of startup entry for faster startup experience + After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler Error setting launch on startup 失去焦點時自動隱藏 Flow Launcher 不顯示新版本提示 @@ -54,6 +68,10 @@ 保留上一個查詢 選擇上一個查詢 清空上次搜尋關鍵字 + Preserve Last Action Keyword + Select Last Action Keyword + Fixed Window Height + The window height is not adjustable by dragging. 最大結果顯示個數 You can also quickly adjust this by using CTRL+Plus and CTRL+Minus. 全螢幕模式下忽略快捷鍵 @@ -71,10 +89,14 @@ 自動更新 選擇 啟動時不顯示主視窗 + Flow Launcher search window is hidden in the tray after starting up. 隱藏任務欄圖示 當圖示從系統列隱藏時,可以透過在搜尋視窗上按右鍵來開啟設定選單。 查詢搜尋精確度 更改結果所需的最低匹配分數。 + None + Low + Regular 拼音搜尋 允許使用拼音來搜尋。拼音是將中文轉換為羅馬字母拼寫的標準系統。 一律預覽 @@ -107,7 +129,8 @@ 版本 官方網站 解除安裝 - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually 插件商店 @@ -124,8 +147,6 @@ This plugin has been updated within the last 7 days New Update is Available - - 主題 外觀 @@ -140,8 +161,13 @@ 以系統管理員或其他使用者啟用應用程式 ProcessKiller Terminate unwanted processes + Search Bar Height + Item Height 查詢框字體 - 結果項字體 + Result Title Font + Result Subtitle Font + Reset + Customize 視窗模式 透明度 找不到主題 {0} ,將回到預設主題 @@ -167,6 +193,8 @@ Custom 時鐘 日期 + This theme supports two(light/dark) modes. + This theme supports Blur Transparent Background. 快捷鍵 @@ -190,6 +218,7 @@ Cycle Previous Query Cycle Next Query 打開選單 + Open Native Context Menu 打開視窗設定 Copy File Path Toggle Game Mode @@ -266,11 +295,17 @@ 清除日誌 請確認要刪除所有日誌嗎? 嚮導 + User Data Location + User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not. + Open Folder + Log Level + Debug + Info 選擇檔案管理器 - Please specify the file location of the file manager you using and add arguments if necessary. The default arguments are "%d", and a path is entered at that location. For example, If a command is required such as "totalcmd.exe /A c:\windows", argument is /A "%d". - "%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d". + 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 fileds blank. 檔案管理器 檔案名稱 檔案管理器路徑 @@ -333,6 +368,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in 取消 Reset 刪除 + 更新 + Yes + No + 背景 版本 @@ -349,6 +388,9 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in 傳送成功 傳送失敗 Flow Launcher 出錯啦 + Please open new issue in + 1. Upload log file: {0} + 2. Copy below exception message 請稍後... @@ -417,4 +459,8 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in sn 便利貼 + + File Size + Created + Last Modified diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index e1970f5ba..31bc2ba50 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -6,8 +6,6 @@ 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:svgc="http://sharpvectors.codeplex.com/svgc/" - xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:ui="http://schemas.modernwpf.com/2019" xmlns:vm="clr-namespace:Flow.Launcher.ViewModel" Name="FlowMainWindow" @@ -19,19 +17,20 @@ AllowDrop="True" AllowsTransparency="True" Background="Transparent" + Closed="OnClosed" Closing="OnClosing" Deactivated="OnDeactivated" Icon="Images/app.png" - Initialized="OnInitialized" Left="{Binding Settings.WindowLeft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Loaded="OnLoaded" LocationChanged="OnLocationChanged" - Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" PreviewKeyDown="OnKeyDown" PreviewKeyUp="OnKeyUp" + PreviewMouseMove="OnPreviewMouseMove" ResizeMode="CanResize" ShowInTaskbar="False" SizeToContent="Height" + SourceInitialized="OnSourceInitialized" Topmost="True" Visibility="{Binding MainWindowVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" WindowStartupLocation="Manual" @@ -39,7 +38,7 @@ mc:Ignorable="d"> - + @@ -216,9 +215,17 @@ - + + - + @@ -290,7 +298,9 @@ + Opacity="{Binding ClockPanelOpacity}" + Style="{DynamicResource ClockPanel}" + Visibility="{Binding ClockPanelVisibility}"> @@ -318,6 +328,7 @@ Name="SearchIcon" Margin="0" Data="{DynamicResource SearchIconImg}" + Opacity="{Binding SearchIconOpacity}" Stretch="Fill" Style="{DynamicResource SearchIconStyle}" Visibility="{Binding SearchIconVisibility}" /> @@ -332,7 +343,6 @@ HorizontalAlignment="Center" VerticalAlignment="Bottom" StrokeThickness="2" - Style="{DynamicResource PendingLineStyle}" Visibility="{Binding ProgressBarVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" X1="-100" X2="0" @@ -340,7 +350,7 @@ Y2="0" /> - + + + + + + + + - - - + + + + - - - - - - - - - - - - + + + + + - + diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 22799a63d..30afe67a1 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,270 +1,247 @@ using System; using System.ComponentModel; +using System.Linq; +using System.Media; using System.Threading.Tasks; using System.Windows; -using System.Windows.Input; -using System.Windows.Media.Animation; using System.Windows.Controls; -using System.Windows.Forms; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Shapes; +using System.Windows.Shell; +using System.Windows.Threading; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.ViewModel; -using Screen = System.Windows.Forms.Screen; -using DragEventArgs = System.Windows.DragEventArgs; -using KeyEventArgs = System.Windows.Input.KeyEventArgs; -using NotifyIcon = System.Windows.Forms.NotifyIcon; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.Image; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; -using System.Windows.Threading; -using System.Windows.Data; +using Flow.Launcher.ViewModel; using ModernWpf.Controls; -using Key = System.Windows.Input.Key; -using System.Media; using DataObject = System.Windows.DataObject; -using System.Windows.Media; -using System.Windows.Interop; -using System.Runtime.InteropServices; +using Key = System.Windows.Input.Key; +using MouseButtons = System.Windows.Forms.MouseButtons; +using NotifyIcon = System.Windows.Forms.NotifyIcon; +using Screen = System.Windows.Forms.Screen; namespace Flow.Launcher { - public partial class MainWindow + public partial class MainWindow : IDisposable { - #region Private Fields + #region Public Property - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern IntPtr SetForegroundWindow(IntPtr hwnd); - - private readonly Storyboard _progressBarStoryboard = new Storyboard(); - private bool isProgressBarStoryboardPaused; - private Settings _settings; - private NotifyIcon _notifyIcon; - private ContextMenu contextMenu = new ContextMenu(); - private MainViewModel _viewModel; - private bool _animating; - private bool isArrowKeyPressed = false; - - private MediaPlayer animationSoundWMP; - private SoundPlayer animationSoundWPF; + // Window Event: Close Event + public bool CanClose { get; set; } = false; #endregion - public MainWindow(Settings settings, MainViewModel mainVM) - { - DataContext = mainVM; - _viewModel = mainVM; - _settings = settings; + #region Private Fields - InitializeComponent(); - InitializePosition(); + // Dependency Injection + private readonly Settings _settings; + private readonly Theme _theme; - InitSoundEffects(); + // Window Notify Icon + private NotifyIcon _notifyIcon; - DataObject.AddPastingHandler(QueryTextBox, OnPaste); + // Window Context Menu + private readonly ContextMenu _contextMenu = new(); + private readonly MainViewModel _viewModel; - this.Loaded += (_, _) => - { - var handle = new WindowInteropHelper(this).Handle; - var win = HwndSource.FromHwnd(handle); - win.AddHook(WndProc); - }; - } + // Window Event: Key Event + private bool _isArrowKeyPressed = false; - DispatcherTimer timer = new DispatcherTimer - { - Interval = new TimeSpan(0, 0, 0, 0, 500), - IsEnabled = false - }; + // Window Sound Effects + private MediaPlayer animationSoundWMP; + private SoundPlayer animationSoundWPF; + + // Window WndProc + private HwndSource _hwndSource; + private int _initialWidth; + private int _initialHeight; + + // Window Animation + private const double DefaultRightMargin = 66; //* this value from base.xaml + private bool _isClockPanelAnimating = false; + + // IDisposable + private bool _disposed = false; + + #endregion + + #region Constructor public MainWindow() { + _settings = Ioc.Default.GetRequiredService(); + _theme = Ioc.Default.GetRequiredService(); + _viewModel = Ioc.Default.GetRequiredService(); + DataContext = _viewModel; + InitializeComponent(); + UpdatePosition(); + + InitSoundEffects(); + DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste); } - private const int WM_ENTERSIZEMOVE = 0x0231; - private const int WM_EXITSIZEMOVE = 0x0232; - private int _initialWidth; - private int _initialHeight; - private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + #endregion + + #region Window Event + +#pragma warning disable VSTHRD100 // Avoid async void methods + + private void OnSourceInitialized(object sender, EventArgs e) { - if (msg == WM_ENTERSIZEMOVE) - { - _initialWidth = (int)Width; - _initialHeight = (int)Height; - handled = true; - } - if (msg == WM_EXITSIZEMOVE) - { - if ( _initialHeight != (int)Height) - { - OnResizeEnd(); - } - if (_initialWidth != (int)Width) - { - FlowMainWindow.SizeToContent = SizeToContent.Height; - } - handled = true; - } - return IntPtr.Zero; + var handle = Win32Helper.GetWindowHandle(this, true); + _hwndSource = HwndSource.FromHwnd(handle); + _hwndSource.AddHook(WndProc); + Win32Helper.HideFromAltTab(this); + Win32Helper.DisableControlBox(this); } - private void OnResizeEnd() + private async void OnLoaded(object sender, RoutedEventArgs _) { - int shadowMargin = 0; - if (_settings.UseDropShadowEffect) + // Check first launch + if (_settings.FirstLaunch) { - shadowMargin = 32; + // Set First Launch to false + _settings.FirstLaunch = false; + + // Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None + if (Win32Helper.IsBackdropSupported()) _settings.BackdropType = BackdropTypes.Acrylic; + + // Save settings + App.API.SaveAppAllSettings(); + + // Show Welcome Window + var welcomeWindow = new WelcomeWindow(); + welcomeWindow.Show(); } - if (!_settings.KeepMaxResults) + // Initialize place holder + SetupPlaceholderText(); + _viewModel.PlaceholderText = _settings.PlaceholderText; + + // Hide window if need + UpdatePosition(); + if (_settings.HideOnStartup) { - var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize; - - if (itemCount < 2) - { - _settings.MaxResultsToShow = 2; - } - else - { - _settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount)); - } + _viewModel.Hide(); } - FlowMainWindow.SizeToContent = SizeToContent.Height; - _viewModel.MainWindowWidth = Width; - } - - private void OnCopy(object sender, ExecutedRoutedEventArgs e) - { - var result = _viewModel.Results.SelectedItem?.Result; - if (QueryTextBox.SelectionLength == 0 && result != null) + else { - string copyText = result.CopyText; - App.API.CopyToClipboard(copyText, directCopy: true); + _viewModel.Show(); } - else if (!string.IsNullOrEmpty(QueryTextBox.Text)) - { - App.API.CopyToClipboard(QueryTextBox.SelectedText, showDefaultNotification: false); - } - } - private void OnPaste(object sender, DataObjectPastingEventArgs e) - { - var isText = e.SourceDataObject.GetDataPresent(System.Windows.DataFormats.UnicodeText, true); - if (isText) - { - var text = e.SourceDataObject.GetData(System.Windows.DataFormats.UnicodeText) as string; - text = text.Replace(Environment.NewLine, " "); - DataObject data = new DataObject(); - data.SetData(System.Windows.DataFormats.UnicodeText, text); - e.DataObject = data; - } - } - - private async void OnClosing(object sender, CancelEventArgs e) - { - _notifyIcon.Visible = false; - App.API.SaveAppAllSettings(); - e.Cancel = true; - await PluginManager.DisposePluginsAsync(); - Notification.Uninstall(); - Environment.Exit(0); - } - - private void OnInitialized(object sender, EventArgs e) - { - } - private void OnLoaded(object sender, RoutedEventArgs _) - { - // MouseEventHandler - PreviewMouseMove += MainPreviewMouseMove; - CheckFirstLaunch(); - HideStartup(); - // show notify icon when flowlauncher is hidden + // Show notify icon when flowlauncher is hidden InitializeNotifyIcon(); - InitializeColorScheme(); - WindowsInteropHelper.DisableControlBox(this); + + // Initialize color scheme + if (_settings.ColorScheme == Constant.Light) + { + ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light; + } + else if (_settings.ColorScheme == Constant.Dark) + { + ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark; + } + + // Initialize position InitProgressbarAnimation(); - InitializePosition(); - PreviewReset(); - // since the default main window visibility is visible - // so we need set focus during startup + + // Force update position + UpdatePosition(); + + // Refresh frame + await _theme.RefreshFrameAsync(); + + // Initialize resize mode after refreshing frame + SetupResizeMode(); + + // Reset preview + _viewModel.ResetPreview(); + + // Since the default main window visibility is visible, so we need set focus during startup QueryTextBox.Focus(); + // Set the initial state of the QueryTextBoxCursorMovedToEnd property + // Without this part, when shown for the first time, switching the context menu does not move the cursor to the end. + _viewModel.QueryTextCursorMovedToEnd = false; + + // Initialize hotkey mapper after window is loaded + HotKeyMapper.Initialize(); + + // View model property changed event _viewModel.PropertyChanged += (o, e) => { switch (e.PropertyName) { case nameof(MainViewModel.MainWindowVisibilityStatus): - { - Dispatcher.Invoke(() => { - if (_viewModel.MainWindowVisibilityStatus) + Dispatcher.Invoke(() => { - if (_settings.UseSound) + if (_viewModel.MainWindowVisibilityStatus) { - SoundPlay(); - } - UpdatePosition(); - PreviewReset(); - Activate(); - QueryTextBox.Focus(); - _settings.ActivateTimes++; - if (!_viewModel.LastQuerySelected) - { - QueryTextBox.SelectAll(); - _viewModel.LastQuerySelected = true; - } + // Play sound effect before activing the window + if (_settings.UseSound) + { + SoundPlay(); + } - if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused) - { - _progressBarStoryboard.Begin(ProgressBar, true); - isProgressBarStoryboardPaused = false; - } + // Update position & Activate + UpdatePosition(); + Activate(); - if (_settings.UseAnimation) - WindowAnimator(); - } - else if (!isProgressBarStoryboardPaused) - { - _progressBarStoryboard.Stop(ProgressBar); - isProgressBarStoryboardPaused = true; - } - }); - break; - } - case nameof(MainViewModel.ProgressBarVisibility): - { - Dispatcher.Invoke(() => - { - if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused) - { - _progressBarStoryboard.Stop(ProgressBar); - isProgressBarStoryboardPaused = true; - } - else if (_viewModel.MainWindowVisibilityStatus && - isProgressBarStoryboardPaused) - { - _progressBarStoryboard.Begin(ProgressBar, true); - isProgressBarStoryboardPaused = false; - } - }); - break; - } + // Reset preview + _viewModel.ResetPreview(); + + // Select last query if need + if (!_viewModel.LastQuerySelected) + { + QueryTextBox.SelectAll(); + _viewModel.LastQuerySelected = true; + } + + // Focus query box + QueryTextBox.Focus(); + + // Play window animation + if (_settings.UseAnimation) + { + WindowAnimation(); + } + + // Update activate times + _settings.ActivateTimes++; + } + }); + break; + } case nameof(MainViewModel.QueryTextCursorMovedToEnd): if (_viewModel.QueryTextCursorMovedToEnd) { - MoveQueryTextToEnd(); + // QueryTextBox seems to be update with a DispatcherPriority as low as ContextIdle. + // To ensure QueryTextBox is up to date with QueryText from the View, we need to Dispatch with such a priority + Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length); _viewModel.QueryTextCursorMovedToEnd = false; } break; case nameof(MainViewModel.GameModeStatus): - _notifyIcon.Icon = _viewModel.GameModeStatus ? Properties.Resources.gamemode : Properties.Resources.app; + _notifyIcon.Icon = _viewModel.GameModeStatus + ? Properties.Resources.gamemode + : Properties.Resources.app; break; } }; + // Settings property changed event _settings.PropertyChanged += (o, e) => { switch (e.PropertyName) @@ -284,301 +261,279 @@ namespace Flow.Launcher case nameof(Settings.WindowTop): Top = _settings.WindowTop; break; + case nameof(Settings.ShowPlaceholder): + SetupPlaceholderText(); + break; + case nameof(Settings.PlaceholderText): + _viewModel.PlaceholderText = _settings.PlaceholderText; + break; + case nameof(Settings.KeepMaxResults): + SetupResizeMode(); + break; } }; + + // QueryTextBox.Text change detection (modified to only work when character count is 1 or higher) + QueryTextBox.TextChanged += (s, e) => UpdateClockPanelVisibility(); + + // Detecting ContextMenu.Visibility changes + DependencyPropertyDescriptor + .FromProperty(VisibilityProperty, typeof(ContextMenu)) + .AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility()); + + // Detect History.Visibility changes + DependencyPropertyDescriptor + .FromProperty(VisibilityProperty, typeof(StackPanel)) + .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private void InitializePosition() + private async void OnClosing(object sender, CancelEventArgs e) + { + if (!CanClose) + { + _notifyIcon.Visible = false; + App.API.SaveAppAllSettings(); + e.Cancel = true; + await ImageLoader.WaitSaveAsync(); + await PluginManager.DisposePluginsAsync(); + Notification.Uninstall(); + // After plugins are all disposed, we can close the main window + CanClose = true; + // Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event + Application.Current.Shutdown(); + } + } + + private void OnClosed(object sender, EventArgs e) + { + try + { + _hwndSource.RemoveHook(WndProc); + } + catch (Exception) + { + // Ignored + } + + _hwndSource = null; + } + + private void OnLocationChanged(object sender, EventArgs e) { if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { - Top = _settings.WindowTop; - Left = _settings.WindowLeft; - } - else - { - var screen = SelectedScreen(); - switch (_settings.SearchWindowAlign) - { - case SearchWindowAligns.Center: - Left = HorizonCenter(screen); - Top = VerticalCenter(screen); - break; - case SearchWindowAligns.CenterTop: - Left = HorizonCenter(screen); - Top = 10; - break; - case SearchWindowAligns.LeftTop: - Left = HorizonLeft(screen); - Top = 10; - break; - case SearchWindowAligns.RightTop: - Left = HorizonRight(screen); - Top = 10; - break; - case SearchWindowAligns.Custom: - Left = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; - Top = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y + _settings.CustomWindowTop).Y; - break; - } - } - - } - - private void UpdateNotifyIconText() - { - var menu = contextMenu; - ((MenuItem)menu.Items[0]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")"; - ((MenuItem)menu.Items[1]).Header = InternationalizationManager.Instance.GetTranslation("GameMode"); - ((MenuItem)menu.Items[2]).Header = InternationalizationManager.Instance.GetTranslation("PositionReset"); - ((MenuItem)menu.Items[3]).Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"); - ((MenuItem)menu.Items[4]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"); - - } - - private void InitializeNotifyIcon() - { - _notifyIcon = new NotifyIcon - { - Text = Infrastructure.Constant.FlowLauncherFullName, - Icon = Constant.Version == "1.0.0" ? Properties.Resources.dev : Properties.Resources.app, - Visible = !_settings.HideNotifyIcon - }; - - var openIcon = new FontIcon - { - Glyph = "\ue71e" - }; - var open = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")", - Icon = openIcon - }; - var gamemodeIcon = new FontIcon - { - Glyph = "\ue7fc" - }; - var gamemode = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("GameMode"), - Icon = gamemodeIcon - }; - var positionresetIcon = new FontIcon - { - Glyph = "\ue73f" - }; - var positionreset = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("PositionReset"), - Icon = positionresetIcon - }; - var settingsIcon = new FontIcon - { - Glyph = "\ue713" - }; - var settings = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"), - Icon = settingsIcon - }; - var exitIcon = new FontIcon - { - Glyph = "\ue7e8" - }; - var exit = new MenuItem - { - Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"), - Icon = exitIcon - }; - - open.Click += (o, e) => _viewModel.ToggleFlowLauncher(); - gamemode.Click += (o, e) => _viewModel.ToggleGameMode(); - positionreset.Click += (o, e) => PositionReset(); - settings.Click += (o, e) => App.API.OpenSettingDialog(); - exit.Click += (o, e) => Close(); - - gamemode.ToolTip = InternationalizationManager.Instance.GetTranslation("GameModeToolTip"); - positionreset.ToolTip = InternationalizationManager.Instance.GetTranslation("PositionResetToolTip"); - - contextMenu.Items.Add(open); - contextMenu.Items.Add(gamemode); - contextMenu.Items.Add(positionreset); - contextMenu.Items.Add(settings); - contextMenu.Items.Add(exit); - - _notifyIcon.MouseClick += (o, e) => - { - switch (e.Button) - { - case MouseButtons.Left: - _viewModel.ToggleFlowLauncher(); - break; - case MouseButtons.Right: - - contextMenu.IsOpen = true; - // Get context menu handle and bring it to the foreground - if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource) - { - _ = SetForegroundWindow(hwndSource.Handle); - } - contextMenu.Focus(); - break; - } - }; - } - - private void CheckFirstLaunch() - { - if (_settings.FirstLaunch) - { - _settings.FirstLaunch = false; - PluginManager.API.SaveAppAllSettings(); - OpenWelcomeWindow(); + _settings.WindowLeft = Left; + _settings.WindowTop = Top; } } - private void OpenWelcomeWindow() + private async void OnDeactivated(object sender, EventArgs e) { - var WelcomeWindow = new WelcomeWindow(_settings); - WelcomeWindow.Show(); - } - - private async void PositionReset() - { - _viewModel.Show(); - await Task.Delay(300); // If don't give a time, Positioning will be weird. - var screen = SelectedScreen(); - Left = HorizonCenter(screen); - Top = VerticalCenter(screen); - } - - private void InitProgressbarAnimation() - { - var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); - var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth + 0, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); - Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)")); - Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)")); - _progressBarStoryboard.Children.Add(da); - _progressBarStoryboard.Children.Add(da1); - _progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever; - _viewModel.ProgressBarVisibility = Visibility.Hidden; - isProgressBarStoryboardPaused = true; - } - - public void WindowAnimator() - { - if (_animating) - return; - - isArrowKeyPressed = true; - _animating = true; - UpdatePosition(); - - Storyboard windowsb = new Storyboard(); - Storyboard clocksb = new Storyboard(); - Storyboard iconsb = new Storyboard(); - CircleEase easing = new CircleEase(); - easing.EasingMode = EasingMode.EaseInOut; - - var animationLength = _settings.AnimationSpeed switch - { - AnimationSpeeds.Slow => 560, - AnimationSpeeds.Medium => 360, - AnimationSpeeds.Fast => 160, - _ => _settings.CustomAnimationLength - }; - - var WindowOpacity = new DoubleAnimation - { - From = 0, - To = 1, - Duration = TimeSpan.FromMilliseconds(animationLength * 2 / 3), - FillBehavior = FillBehavior.Stop - }; - - var WindowMotion = new DoubleAnimation - { - From = Top + 10, - To = Top, - Duration = TimeSpan.FromMilliseconds(animationLength * 2 / 3), - FillBehavior = FillBehavior.Stop - }; - var IconMotion = new DoubleAnimation - { - From = 12, - To = 0, - EasingFunction = easing, - Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop - }; - - var ClockOpacity = new DoubleAnimation - { - From = 0, - To = 1, - EasingFunction = easing, - Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop - }; - double TargetIconOpacity = SearchIcon.Opacity; // Animation Target Opacity from Style - var IconOpacity = new DoubleAnimation - { - From = 0, - To = TargetIconOpacity, - EasingFunction = easing, - Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop - }; - - double right = ClockPanel.Margin.Right; - var thicknessAnimation = new ThicknessAnimation - { - From = new Thickness(0, 12, right, 0), - To = new Thickness(0, 0, right, 0), - EasingFunction = easing, - Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop - }; - - Storyboard.SetTargetProperty(ClockOpacity, new PropertyPath(OpacityProperty)); - Storyboard.SetTargetName(thicknessAnimation, "ClockPanel"); - Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath(MarginProperty)); - Storyboard.SetTarget(WindowOpacity, this); - Storyboard.SetTargetProperty(WindowOpacity, new PropertyPath(Window.OpacityProperty)); - Storyboard.SetTargetProperty(WindowMotion, new PropertyPath(Window.TopProperty)); - Storyboard.SetTargetProperty(IconMotion, new PropertyPath(TopProperty)); - Storyboard.SetTargetProperty(IconOpacity, new PropertyPath(OpacityProperty)); - - clocksb.Children.Add(thicknessAnimation); - clocksb.Children.Add(ClockOpacity); - windowsb.Children.Add(WindowOpacity); - windowsb.Children.Add(WindowMotion); - iconsb.Children.Add(IconMotion); - iconsb.Children.Add(IconOpacity); - - windowsb.Completed += (_, _) => _animating = false; _settings.WindowLeft = Left; _settings.WindowTop = Top; - isArrowKeyPressed = false; - if (QueryTextBox.Text.Length == 0) + _viewModel.ClockPanelOpacity = 0.0; + _viewModel.SearchIconOpacity = 0.0; + + // This condition stops extra hide call when animator is on, + // which causes the toggling to occasional hide instead of show. + if (_viewModel.MainWindowVisibilityStatus) { - clocksb.Begin(ClockPanel); + // Need time to initialize the main query window animation. + // This also stops the mainwindow from flickering occasionally after Settings window is opened + // and always after Settings window is closed. + if (_settings.UseAnimation) + { + await Task.Delay(100); + } + + if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible) + { + _viewModel.Hide(); + } } - iconsb.Begin(SearchIcon); - windowsb.Begin(FlowMainWindow); } + private void OnKeyDown(object sender, KeyEventArgs e) + { + var specialKeyState = GlobalHotkey.CheckModifiers(); + switch (e.Key) + { + case Key.Down: + _isArrowKeyPressed = true; + _viewModel.SelectNextItemCommand.Execute(null); + e.Handled = true; + break; + case Key.Up: + _isArrowKeyPressed = true; + _viewModel.SelectPrevItemCommand.Execute(null); + e.Handled = true; + break; + case Key.PageDown: + _viewModel.SelectNextPageCommand.Execute(null); + e.Handled = true; + break; + case Key.PageUp: + _viewModel.SelectPrevPageCommand.Execute(null); + e.Handled = true; + break; + case Key.Right: + if (_viewModel.QueryResultsSelected() + && QueryTextBox.CaretIndex == QueryTextBox.Text.Length + && !string.IsNullOrEmpty(QueryTextBox.Text)) + { + _viewModel.LoadContextMenuCommand.Execute(null); + e.Handled = true; + } + break; + case Key.Left: + if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0) + { + _viewModel.EscCommand.Execute(null); + e.Handled = true; + } + break; + case Key.Back: + if (specialKeyState.CtrlPressed) + { + if (_viewModel.QueryResultsSelected() + && QueryTextBox.Text.Length > 0 + && QueryTextBox.CaretIndex == QueryTextBox.Text.Length) + { + var queryWithoutActionKeyword = + QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; + + if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) + { + _viewModel.BackspaceCommand.Execute(null); + e.Handled = true; + } + } + } + break; + default: + break; + } + } + + private void OnKeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Up || e.Key == Key.Down) + { + _isArrowKeyPressed = false; + } + } + + private void OnPreviewMouseMove(object sender, MouseEventArgs e) + { + if (_isArrowKeyPressed) + { + e.Handled = true; // Ignore Mouse Hover when press Arrowkeys + } + } + +#pragma warning restore VSTHRD100 // Avoid async void methods + + #endregion + + #region Window Boarder Event + + private void OnMouseDown(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left) DragMove(); + } + + #endregion + + #region Window Context Menu Event + +#pragma warning disable VSTHRD100 // Avoid async void methods + + private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs e) + { + _viewModel.Hide(); + + if (_settings.UseAnimation) + await Task.Delay(100); + + App.API.OpenSettingDialog(); + } + +#pragma warning restore VSTHRD100 // Avoid async void methods + + #endregion + + #region Window WndProc + + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + if (msg == Win32Helper.WM_ENTERSIZEMOVE) + { + _initialWidth = (int)Width; + _initialHeight = (int)Height; + + handled = true; + } + else if (msg == Win32Helper.WM_EXITSIZEMOVE) + { + if (_initialHeight != (int)Height) + { + if (!_settings.KeepMaxResults) + { + // Get shadow margin + var shadowMargin = 0; + var (_, useDropShadowEffect) = _theme.GetActualValue(); + if (useDropShadowEffect) + { + shadowMargin = 32; + } + + // Calculate max results to show + var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize; + if (itemCount < 2) + { + _settings.MaxResultsToShow = 2; + } + else + { + _settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount)); + } + } + + SizeToContent = SizeToContent.Height; + } + + if (_initialWidth != (int)Width) + { + if (!_settings.KeepMaxResults) + { + // Update width + _viewModel.MainWindowWidth = Width; + } + + SizeToContent = SizeToContent.Height; + } + + handled = true; + } + + return IntPtr.Zero; + } + + #endregion + + #region Window Sound Effects + private void InitSoundEffects() { if (_settings.WMPInstalled) { animationSoundWMP = new MediaPlayer(); - animationSoundWMP.Open(new Uri(AppDomain.CurrentDomain.BaseDirectory + "Resources\\open.wav")); + animationSoundWMP.Open(new Uri(AppContext.BaseDirectory + "Resources\\open.wav")); } else { - animationSoundWPF = new SoundPlayer(AppDomain.CurrentDomain.BaseDirectory + "Resources\\open.wav"); + animationSoundWPF = new SoundPlayer(AppContext.BaseDirectory + "Resources\\open.wav"); } } @@ -596,82 +551,167 @@ namespace Flow.Launcher } } - private void OnMouseDown(object sender, MouseButtonEventArgs e) + #endregion + + #region Window Notify Icon + + private void InitializeNotifyIcon() { - if (e.ChangedButton == MouseButton.Left) DragMove(); - } - - private void OnPreviewDragOver(object sender, DragEventArgs e) - { - e.Handled = true; - } - - private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs e) - { - _viewModel.Hide(); - - if (_settings.UseAnimation) - await Task.Delay(100); - - App.API.OpenSettingDialog(); - } - - private async void OnDeactivated(object sender, EventArgs e) - { - _settings.WindowLeft = Left; - _settings.WindowTop = Top; - //This condition stops extra hide call when animator is on, - // which causes the toggling to occasional hide instead of show. - if (_viewModel.MainWindowVisibilityStatus) + _notifyIcon = new NotifyIcon { - // Need time to initialize the main query window animation. - // This also stops the mainwindow from flickering occasionally after Settings window is opened - // and always after Settings window is closed. - if (_settings.UseAnimation) - await Task.Delay(100); + Text = Constant.FlowLauncherFullName, + Icon = Constant.Version == "1.0.0" ? Properties.Resources.dev : Properties.Resources.app, + Visible = !_settings.HideNotifyIcon + }; + var openIcon = new FontIcon { Glyph = "\ue71e" }; + var open = new MenuItem + { + Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")", + Icon = openIcon + }; + var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" }; + var gamemode = new MenuItem + { + Header = App.API.GetTranslation("GameMode"), + Icon = gamemodeIcon + }; + var positionresetIcon = new FontIcon { Glyph = "\ue73f" }; + var positionreset = new MenuItem + { + Header = App.API.GetTranslation("PositionReset"), + Icon = positionresetIcon + }; + var settingsIcon = new FontIcon { Glyph = "\ue713" }; + var settings = new MenuItem + { + Header = App.API.GetTranslation("iconTraySettings"), + Icon = settingsIcon + }; + var exitIcon = new FontIcon { Glyph = "\ue7e8" }; + var exit = new MenuItem + { + Header = App.API.GetTranslation("iconTrayExit"), + Icon = exitIcon + }; - if (_settings.HideWhenDeactivated) + open.Click += (o, e) => _viewModel.ToggleFlowLauncher(); + gamemode.Click += (o, e) => _viewModel.ToggleGameMode(); + positionreset.Click += (o, e) => _ = PositionResetAsync(); + settings.Click += (o, e) => App.API.OpenSettingDialog(); + exit.Click += (o, e) => Close(); + + gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); + positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); + + _contextMenu.Items.Add(open); + _contextMenu.Items.Add(gamemode); + _contextMenu.Items.Add(positionreset); + _contextMenu.Items.Add(settings); + _contextMenu.Items.Add(exit); + + _notifyIcon.MouseClick += (o, e) => + { + switch (e.Button) { - _viewModel.Hide(); + case MouseButtons.Left: + _viewModel.ToggleFlowLauncher(); + break; + case MouseButtons.Right: + + _contextMenu.IsOpen = true; + // Get context menu handle and bring it to the foreground + if (PresentationSource.FromVisual(_contextMenu) is HwndSource hwndSource) + { + Win32Helper.SetForegroundWindow(hwndSource.Handle); + } + + _contextMenu.Focus(); + break; + } + }; + } + + private void UpdateNotifyIconText() + { + var menu = _contextMenu; + ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("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"); + } + + #endregion + + #region Window Position + + private void UpdatePosition() + { + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + InitializePosition(); + InitializePosition(); + } + + private async Task PositionResetAsync() + { + _viewModel.Show(); + await Task.Delay(300); // If don't give a time, Positioning will be weird. + var screen = SelectedScreen(); + Left = HorizonCenter(screen); + Top = VerticalCenter(screen); + } + + private void InitializePosition() + { + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + InitializePositionInner(); + InitializePositionInner(); + return; + + void InitializePositionInner() + { + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) + { + Top = _settings.WindowTop; + Left = _settings.WindowLeft; + } + else + { + var screen = SelectedScreen(); + switch (_settings.SearchWindowAlign) + { + case SearchWindowAligns.Center: + Left = HorizonCenter(screen); + Top = VerticalCenter(screen); + break; + case SearchWindowAligns.CenterTop: + Left = HorizonCenter(screen); + Top = 10; + break; + case SearchWindowAligns.LeftTop: + Left = HorizonLeft(screen); + Top = 10; + break; + case SearchWindowAligns.RightTop: + Left = HorizonRight(screen); + Top = 10; + break; + case SearchWindowAligns.Custom: + Left = Win32Helper.TransformPixelsToDIP(this, + screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; + Top = Win32Helper.TransformPixelsToDIP(this, 0, + screen.WorkingArea.Y + _settings.CustomWindowTop).Y; + break; + } } } } - private void UpdatePosition() + private Screen SelectedScreen() { - if (_animating) - return; - InitializePosition(); - } - - private void OnLocationChanged(object sender, EventArgs e) - { - if (_animating) - return; - if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) - { - _settings.WindowLeft = Left; - _settings.WindowTop = Top; - } - } - - public void HideStartup() - { - UpdatePosition(); - if (_settings.HideOnStartup) - { - _viewModel.Hide(); - } - else - { - _viewModel.Show(); - } - } - - public Screen SelectedScreen() - { - Screen screen = null; - switch(_settings.SearchWindowScreen) + Screen screen; + switch (_settings.SearchWindowScreen) { case SearchWindowScreens.Cursor: screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); @@ -680,7 +720,7 @@ namespace Flow.Launcher screen = Screen.PrimaryScreen; break; case SearchWindowScreens.Focus: - IntPtr foregroundWindowHandle = WindowsInteropHelper.GetForegroundWindow(); + var foregroundWindowHandle = Win32Helper.GetForegroundWindow(); screen = Screen.FromHandle(foregroundWindowHandle); break; case SearchWindowScreens.Custom: @@ -693,142 +733,323 @@ namespace Flow.Launcher screen = Screen.AllScreens[0]; break; } + return screen ?? Screen.AllScreens[0]; } - public double HorizonCenter(Screen screen) + private double HorizonCenter(Screen screen) { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); var left = (dip2.X - ActualWidth) / 2 + dip1.X; return left; } - public double VerticalCenter(Screen screen) + private double VerticalCenter(Screen screen) { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); + var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); var top = (dip2.Y - QueryTextBox.ActualHeight) / 4 + dip1.Y; return top; } - public double HorizonRight(Screen screen) + private double HorizonRight(Screen screen) { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); var left = (dip1.X + dip2.X - ActualWidth) - 10; return left; } - public double HorizonLeft(Screen screen) + private double HorizonLeft(Screen screen) { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); var left = dip1.X + 10; return left; } - /// - /// Register up and down key - /// todo: any way to put this in xaml ? - /// - private void OnKeyDown(object sender, KeyEventArgs e) + #endregion + + #region Window Animation + + private void InitProgressbarAnimation() { - var specialKeyState = GlobalHotkey.CheckModifiers(); - switch (e.Key) + var progressBarStoryBoard = new Storyboard(); + + var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, + new Duration(new TimeSpan(0, 0, 0, 0, 1600))); + var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth + 0, + new Duration(new TimeSpan(0, 0, 0, 0, 1600))); + Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)")); + Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)")); + progressBarStoryBoard.Children.Add(da); + progressBarStoryBoard.Children.Add(da1); + progressBarStoryBoard.RepeatBehavior = RepeatBehavior.Forever; + + da.Freeze(); + da1.Freeze(); + + const string progressBarAnimationName = "ProgressBarAnimation"; + var beginStoryboard = new BeginStoryboard { - case Key.Down: - isArrowKeyPressed = true; - _viewModel.SelectNextItemCommand.Execute(null); - e.Handled = true; - break; - case Key.Up: - isArrowKeyPressed = true; - _viewModel.SelectPrevItemCommand.Execute(null); - e.Handled = true; - break; - case Key.PageDown: - _viewModel.SelectNextPageCommand.Execute(null); - e.Handled = true; - break; - case Key.PageUp: - _viewModel.SelectPrevPageCommand.Execute(null); - e.Handled = true; - break; - case Key.Right: - if (_viewModel.SelectedIsFromQueryResults() - && QueryTextBox.CaretIndex == QueryTextBox.Text.Length - && !string.IsNullOrEmpty(QueryTextBox.Text)) - { - _viewModel.LoadContextMenuCommand.Execute(null); - e.Handled = true; - } - break; - case Key.Left: - if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0) - { - _viewModel.EscCommand.Execute(null); - e.Handled = true; - } - break; - case Key.Back: - if (specialKeyState.CtrlPressed) - { - if (_viewModel.SelectedIsFromQueryResults() - && QueryTextBox.Text.Length > 0 - && QueryTextBox.CaretIndex == QueryTextBox.Text.Length) - { - var queryWithoutActionKeyword = - QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; + Name = progressBarAnimationName, Storyboard = progressBarStoryBoard + }; + + var stopStoryboard = new StopStoryboard() + { + BeginStoryboardName = progressBarAnimationName + }; - if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) - { - _viewModel.BackspaceCommand.Execute(null); - e.Handled = true; - } - } - } - break; - default: - break; + var trigger = new Trigger + { + Property = VisibilityProperty, Value = Visibility.Visible + }; + trigger.EnterActions.Add(beginStoryboard); + trigger.ExitActions.Add(stopStoryboard); - } + var progressStyle = new Style(typeof(Line)) + { + BasedOn = FindResource("PendingLineStyle") as Style + }; + progressStyle.RegisterName(progressBarAnimationName, beginStoryboard); + progressStyle.Triggers.Add(trigger); + + ProgressBar.Style = progressStyle; + + _viewModel.ProgressBarVisibility = Visibility.Hidden; } - private void OnKeyUp(object sender, KeyEventArgs e) + + private void WindowAnimation() { - if (e.Key == Key.Up || e.Key == Key.Down) + _isArrowKeyPressed = true; + + var clocksb = new Storyboard(); + var iconsb = new Storyboard(); + var easing = new CircleEase { EasingMode = EasingMode.EaseInOut }; + + var animationLength = _settings.AnimationSpeed switch { - isArrowKeyPressed = false; + AnimationSpeeds.Slow => 560, + AnimationSpeeds.Medium => 360, + AnimationSpeeds.Fast => 160, + _ => _settings.CustomAnimationLength + }; + + var IconMotion = new DoubleAnimation + { + From = 12, + To = 0, + EasingFunction = easing, + Duration = TimeSpan.FromMilliseconds(animationLength), + FillBehavior = FillBehavior.HoldEnd + }; + + var ClockOpacity = new DoubleAnimation + { + From = 0, + To = 1, + EasingFunction = easing, + Duration = TimeSpan.FromMilliseconds(animationLength), + FillBehavior = FillBehavior.HoldEnd + }; + + var TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0); + + var IconOpacity = new DoubleAnimation + { + From = 0, + To = TargetIconOpacity, + EasingFunction = easing, + Duration = TimeSpan.FromMilliseconds(animationLength), + FillBehavior = FillBehavior.HoldEnd + }; + + var rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right; + + var thicknessAnimation = new ThicknessAnimation + { + From = new Thickness(0, 12, rightMargin, 0), + To = new Thickness(0, 0, rightMargin, 0), + EasingFunction = easing, + Duration = TimeSpan.FromMilliseconds(animationLength), + FillBehavior = FillBehavior.HoldEnd + }; + + Storyboard.SetTargetProperty(ClockOpacity, new PropertyPath(OpacityProperty)); + Storyboard.SetTarget(ClockOpacity, ClockPanel); + + Storyboard.SetTargetName(thicknessAnimation, "ClockPanel"); + Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath(MarginProperty)); + + Storyboard.SetTarget(IconMotion, SearchIcon); + Storyboard.SetTargetProperty(IconMotion, new PropertyPath(TopProperty)); + + Storyboard.SetTarget(IconOpacity, SearchIcon); + Storyboard.SetTargetProperty(IconOpacity, new PropertyPath(OpacityProperty)); + + clocksb.Children.Add(thicknessAnimation); + clocksb.Children.Add(ClockOpacity); + iconsb.Children.Add(IconMotion); + iconsb.Children.Add(IconOpacity); + + _settings.WindowLeft = Left; + _isArrowKeyPressed = false; + + clocksb.Begin(ClockPanel); + iconsb.Begin(SearchIcon); + } + + private void UpdateClockPanelVisibility() + { + if (QueryTextBox == null || ContextMenu == null || History == null || ClockPanel == null) + { + return; + } + + // ✅ Initialize animation length & duration + var animationLength = _settings.AnimationSpeed switch + { + AnimationSpeeds.Slow => 560, + AnimationSpeeds.Medium => 360, + AnimationSpeeds.Fast => 160, + _ => _settings.CustomAnimationLength + }; + var animationDuration = TimeSpan.FromMilliseconds(animationLength * 2 / 3); + + // ✅ Conditions for showing ClockPanel (No query input & ContextMenu, History are closed) + var shouldShowClock = QueryTextBox.Text.Length == 0 && + ContextMenu.Visibility != Visibility.Visible && + History.Visibility != Visibility.Visible; + + // ✅ 1. When ContextMenu opens, immediately set Visibility.Hidden (force hide without animation) + if (ContextMenu.Visibility == Visibility.Visible) + { + _viewModel.ClockPanelVisibility = Visibility.Hidden; + _viewModel.ClockPanelOpacity = 0.0; // Set to 0 in case Opacity animation affects it + return; + } + + // ✅ 2. When ContextMenu is closed, keep it Hidden if there's text in the query (remember previous state) + else if (QueryTextBox.Text.Length > 0) + { + _viewModel.ClockPanelVisibility = Visibility.Hidden; + _viewModel.ClockPanelOpacity = 0.0; + return; + } + + // ✅ Prevent multiple animations + if (_isClockPanelAnimating) + { + return; + } + + // ✅ 3. When hiding ClockPanel (apply fade-out animation) + if ((!shouldShowClock) && _viewModel.ClockPanelVisibility == Visibility.Visible) + { + _isClockPanelAnimating = true; + + var fadeOut = new DoubleAnimation + { + From = 1.0, + To = 0.0, + Duration = animationDuration, + FillBehavior = FillBehavior.HoldEnd + }; + + fadeOut.Completed += (s, e) => + { + _viewModel.ClockPanelVisibility = Visibility.Hidden; // ✅ Completely hide after animation + _isClockPanelAnimating = false; + }; + + ClockPanel.BeginAnimation(OpacityProperty, fadeOut); + } + + // ✅ 4. When showing ClockPanel (apply fade-in animation) + else if (shouldShowClock && _viewModel.ClockPanelVisibility != Visibility.Visible) + { + _isClockPanelAnimating = true; + + _viewModel.ClockPanelVisibility = Visibility.Visible; // ✅ Set Visibility to Visible first + + var fadeIn = new DoubleAnimation + { + From = 0.0, + To = 1.0, + Duration = animationDuration, + FillBehavior = FillBehavior.HoldEnd + }; + + fadeIn.Completed += (s, e) => _isClockPanelAnimating = false; + + ClockPanel.BeginAnimation(OpacityProperty, fadeIn); } } - private void MainPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) + private static double GetOpacityFromStyle(Style style, double defaultOpacity = 1.0) { - if (isArrowKeyPressed) + if (style == null) { - e.Handled = true; // Ignore Mouse Hover when press Arrowkeys + return defaultOpacity; } - } - public void PreviewReset() - { - _viewModel.ResetPreview(); + + foreach (Setter setter in style.Setters.Cast()) + { + if (setter.Property == OpacityProperty) + { + return setter.Value is double opacity ? opacity : defaultOpacity; + } + } + + return defaultOpacity; } - private void MoveQueryTextToEnd() + private static Thickness GetThicknessFromStyle(Style style, Thickness defaultThickness) { - // QueryTextBox seems to be update with a DispatcherPriority as low as ContextIdle. - // To ensure QueryTextBox is up to date with QueryText from the View, we need to Dispatch with such a priority - Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length); + if (style == null) + { + return defaultThickness; + } + + foreach (Setter setter in style.Setters.Cast()) + { + if (setter.Property == MarginProperty) + { + return setter.Value is Thickness thickness ? thickness : defaultThickness; + } + } + + return defaultThickness; } - public void InitializeColorScheme() + #endregion + + #region QueryTextBox Event + + private void QueryTextBox_OnCopy(object sender, ExecutedRoutedEventArgs e) { - if (_settings.ColorScheme == Constant.Light) + var result = _viewModel.Results.SelectedItem?.Result; + if (QueryTextBox.SelectionLength == 0 && result != null) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light; + string copyText = result.CopyText; + App.API.CopyToClipboard(copyText, directCopy: true); } - else if (_settings.ColorScheme == Constant.Dark) + else if (!string.IsNullOrEmpty(QueryTextBox.Text)) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark; + App.API.CopyToClipboard(QueryTextBox.SelectedText, showDefaultNotification: false); + } + } + + private void QueryTextBox_OnPaste(object sender, DataObjectPastingEventArgs e) + { + var isText = e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true); + if (isText) + { + var text = e.SourceDataObject.GetData(DataFormats.UnicodeText) as string; + text = text.Replace(Environment.NewLine, " "); + DataObject data = new DataObject(); + data.SetData(DataFormats.UnicodeText, text); + e.DataObject = data; } } @@ -836,9 +1057,102 @@ namespace Flow.Launcher { if (_viewModel.QueryText != QueryTextBox.Text) { - BindingExpression be = QueryTextBox.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty); + BindingExpression be = QueryTextBox.GetBindingExpression(TextBox.TextProperty); be.UpdateSource(); } } + + private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e) + { + e.Handled = true; + } + + #endregion + + #region Placeholder + + private void SetupPlaceholderText() + { + if (_settings.ShowPlaceholder) + { + QueryTextBox.TextChanged += QueryTextBox_TextChanged; + QueryTextSuggestionBox.TextChanged += QueryTextSuggestionBox_TextChanged; + SetPlaceholderText(); + } + else + { + QueryTextBox.TextChanged -= QueryTextBox_TextChanged; + QueryTextSuggestionBox.TextChanged -= QueryTextSuggestionBox_TextChanged; + QueryTextPlaceholderBox.Visibility = Visibility.Collapsed; + } + } + + private void QueryTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + SetPlaceholderText(); + } + + private void QueryTextSuggestionBox_TextChanged(object sender, TextChangedEventArgs e) + { + SetPlaceholderText(); + } + + private void SetPlaceholderText() + { + var queryText = QueryTextBox.Text; + var suggestionText = QueryTextSuggestionBox.Text; + QueryTextPlaceholderBox.Visibility = string.IsNullOrEmpty(queryText) && string.IsNullOrEmpty(suggestionText) ? Visibility.Visible : Visibility.Collapsed; + } + + #endregion + + #region Resize Mode + + private void SetupResizeMode() + { + ResizeMode = _settings.KeepMaxResults ? ResizeMode.NoResize : ResizeMode.CanResize; + if (WindowChrome.GetWindowChrome(this) is WindowChrome windowChrome) + { + _theme.SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults); + } + } + + #endregion + + #region Search Delay + + private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) + { + var textBox = (TextBox)sender; + _viewModel.QueryText = textBox.Text; + _viewModel.Query(_settings.SearchQueryResultsWithDelay); + } + + #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _hwndSource?.Dispose(); + _notifyIcon?.Dispose(); + } + + _disposed = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion } } diff --git a/Flow.Launcher/MessageBoxEx.xaml b/Flow.Launcher/MessageBoxEx.xaml new file mode 100644 index 000000000..a0e5ffaf8 --- /dev/null +++ b/Flow.Launcher/MessageBoxEx.xaml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/ReportWindow.xaml.cs b/Flow.Launcher/ReportWindow.xaml.cs index a535dfb3e..6fe90783e 100644 --- a/Flow.Launcher/ReportWindow.xaml.cs +++ b/Flow.Launcher/ReportWindow.xaml.cs @@ -43,20 +43,21 @@ namespace Flow.Launcher var log = directory.GetFiles().OrderByDescending(f => f.LastWriteTime).First(); var websiteUrl = exception switch - { - FlowPluginException pluginException =>GetIssuesUrl(pluginException.Metadata.Website), - _ => Constant.IssuesUrl - }; - + { + FlowPluginException pluginException =>GetIssuesUrl(pluginException.Metadata.Website), + _ => Constant.IssuesUrl + }; - var paragraph = Hyperlink("Please open new issue in: ", websiteUrl); - paragraph.Inlines.Add($"1. upload log file: {log.FullName}\n"); - paragraph.Inlines.Add($"2. copy below exception message"); + var paragraph = Hyperlink(App.API.GetTranslation("reportWindow_please_open_issue"), websiteUrl); + paragraph.Inlines.Add(string.Format(App.API.GetTranslation("reportWindow_upload_log"), log.FullName)); + paragraph.Inlines.Add("\n"); + paragraph.Inlines.Add(App.API.GetTranslation("reportWindow_copy_below")); ErrorTextbox.Document.Blocks.Add(paragraph); StringBuilder content = new StringBuilder(); content.AppendLine(ErrorReporting.RuntimeInfo()); content.AppendLine(ErrorReporting.DependenciesInfo()); + content.AppendLine(); content.AppendLine($"Date: {DateTime.Now.ToString(CultureInfo.InvariantCulture)}"); content.AppendLine("Exception:"); content.AppendLine(exception.ToString()); @@ -65,10 +66,12 @@ namespace Flow.Launcher ErrorTextbox.Document.Blocks.Add(paragraph); } - private Paragraph Hyperlink(string textBeforeUrl, string url) + private static Paragraph Hyperlink(string textBeforeUrl, string url) { - var paragraph = new Paragraph(); - paragraph.Margin = new Thickness(0); + var paragraph = new Paragraph + { + Margin = new Thickness(0) + }; var link = new Hyperlink { @@ -79,10 +82,16 @@ namespace Flow.Launcher link.Click += (s, e) => SearchWeb.OpenInBrowserTab(url); paragraph.Inlines.Add(textBeforeUrl); + paragraph.Inlines.Add(" "); paragraph.Inlines.Add(link); paragraph.Inlines.Add("\n"); return paragraph; } + + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + { + Close(); + } } } diff --git a/Flow.Launcher/Resources/Controls/Card.xaml b/Flow.Launcher/Resources/Controls/Card.xaml index c29a5f602..33c1299a9 100644 --- a/Flow.Launcher/Resources/Controls/Card.xaml +++ b/Flow.Launcher/Resources/Controls/Card.xaml @@ -20,21 +20,21 @@ - - + + - + - + - + - - + + @@ -73,7 +73,7 @@ @@ -91,7 +91,7 @@ @@ -107,8 +107,8 @@ - - + + @@ -120,11 +120,11 @@ @@ -1642,6 +1643,80 @@ + + 0:0:0.033 @@ -1657,7 +1732,7 @@ - + @@ -5510,4 +5585,31 @@ + + + 70,13.5,18,13.5 + + 9,0,0,0 + 0,0,9,0 + 0,4.5,0,4.5 + + 9,4.5,0,4.5 + 0,4.5,9,4.5 + + + + 180 + 240 + 150 + + diff --git a/Flow.Launcher/Resources/Dark.xaml b/Flow.Launcher/Resources/Dark.xaml index a910108e8..ec089b378 100644 --- a/Flow.Launcher/Resources/Dark.xaml +++ b/Flow.Launcher/Resources/Dark.xaml @@ -7,16 +7,17 @@ xmlns:sys="clr-namespace:System;assembly=mscorlib"> - + - - - - + + + + - - + + + #198F8F8F @@ -57,6 +58,9 @@ + + + @@ -115,6 +119,8 @@ + + @@ -1605,18 +1611,23 @@ - - + + - - - + + + + + + + + True False diff --git a/Flow.Launcher/Resources/Light.xaml b/Flow.Launcher/Resources/Light.xaml index 906430343..aa6da9fb2 100644 --- a/Flow.Launcher/Resources/Light.xaml +++ b/Flow.Launcher/Resources/Light.xaml @@ -7,17 +7,18 @@ xmlns:sys="clr-namespace:System;assembly=mscorlib"> - + - - + + - - - #198F8F8F + + + + #0C000000 @@ -50,6 +51,9 @@ + + + @@ -106,6 +110,7 @@ + @@ -1608,9 +1613,11 @@ - - + + + + True diff --git a/Flow.Launcher/Resources/Pages/WelcomePage1.xaml b/Flow.Launcher/Resources/Pages/WelcomePage1.xaml index 1728195bd..32fdb62fc 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage1.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage1.xaml @@ -110,8 +110,8 @@ - - + + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage1.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage1.xaml.cs index 98fb47288..880bfd9bc 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage1.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage1.xaml.cs @@ -1,8 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Windows.Navigation; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Core.Resource; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.ViewModel; namespace Flow.Launcher.Resources.Pages { @@ -10,10 +11,10 @@ namespace Flow.Launcher.Resources.Pages { protected override void OnNavigatedTo(NavigationEventArgs e) { - if (e.ExtraData is Settings settings) - Settings = settings; - else - throw new ArgumentException("Unexpected Navigation Parameter for Settings"); + Settings = Ioc.Default.GetRequiredService(); + // Sometimes the navigation is not triggered by button click, + // so we need to reset the page number + Ioc.Default.GetRequiredService().PageNum = 1; InitializeComponent(); } private Internationalization _translater => InternationalizationManager.Instance; @@ -37,4 +38,4 @@ namespace Flow.Launcher.Resources.Pages } } -} \ No newline at end of file +} diff --git a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml index 6c6fcbb62..f62cec3bf 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml @@ -60,7 +60,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal"> - + @@ -114,8 +114,7 @@ Margin="0,8,0,0" ChangeHotkey="{Binding SetTogglingHotkeyCommand}" DefaultHotkey="Alt+Space" - Hotkey="{Binding Settings.Hotkey}" - HotkeySettings="{Binding Settings}" + Type="Hotkey" ValidateKeyGesture="True" WindowTitle="{DynamicResource flowlauncherHotkey}" /> diff --git a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml.cs index 7dfb85a83..786b4d506 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml.cs @@ -1,12 +1,11 @@ using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.UserSettings; -using System; -using System.Windows; -using System.Windows.Media; using System.Windows.Navigation; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.ViewModel; +using System.Windows.Media; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Resources.Pages { @@ -16,11 +15,10 @@ namespace Flow.Launcher.Resources.Pages protected override void OnNavigatedTo(NavigationEventArgs e) { - if (e.ExtraData is Settings settings) - Settings = settings; - else - throw new ArgumentException("Unexpected Parameter setting."); - + Settings = Ioc.Default.GetRequiredService(); + // Sometimes the navigation is not triggered by button click, + // so we need to reset the page number + Ioc.Default.GetRequiredService().PageNum = 2; InitializeComponent(); } @@ -29,5 +27,10 @@ namespace Flow.Launcher.Resources.Pages { HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey); } + + public Brush PreviewBackground + { + get => WallpaperPathRetrieval.GetWallpaperBrush(); + } } } diff --git a/Flow.Launcher/Resources/Pages/WelcomePage3.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage3.xaml.cs index 9051e7c27..f59b65c1c 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage3.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage3.xaml.cs @@ -1,6 +1,7 @@ -using System; -using System.Windows.Navigation; +using System.Windows.Navigation; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.ViewModel; namespace Flow.Launcher.Resources.Pages { @@ -8,10 +9,10 @@ namespace Flow.Launcher.Resources.Pages { protected override void OnNavigatedTo(NavigationEventArgs e) { - if (e.ExtraData is Settings settings) - Settings = settings; - else if(Settings is null) - throw new ArgumentException("Unexpected Navigation Parameter for Settings"); + Settings = Ioc.Default.GetRequiredService(); + // Sometimes the navigation is not triggered by button click, + // so we need to reset the page number + Ioc.Default.GetRequiredService().PageNum = 3; InitializeComponent(); } diff --git a/Flow.Launcher/Resources/Pages/WelcomePage4.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage4.xaml.cs index 11bbcd6ed..4c83f3a83 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage4.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage4.xaml.cs @@ -1,5 +1,6 @@ -using Flow.Launcher.Infrastructure.UserSettings; -using System; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.ViewModel; using System.Windows.Navigation; namespace Flow.Launcher.Resources.Pages @@ -8,10 +9,10 @@ namespace Flow.Launcher.Resources.Pages { protected override void OnNavigatedTo(NavigationEventArgs e) { - if (e.ExtraData is Settings settings) - Settings = settings; - else - throw new ArgumentException("Unexpected Navigation Parameter for Settings"); + Settings = Ioc.Default.GetRequiredService(); + // Sometimes the navigation is not triggered by button click, + // so we need to reset the page number + Ioc.Default.GetRequiredService().PageNum = 4; InitializeComponent(); } diff --git a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml index c898ac9a0..3df4b506e 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml @@ -58,10 +58,10 @@ - + - - + + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs index abb805303..95d7ff1a0 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs @@ -1,9 +1,10 @@ -using System; -using System.Windows; +using System.Windows; using System.Windows.Navigation; using Flow.Launcher.Infrastructure.UserSettings; using Microsoft.Win32; using Flow.Launcher.Infrastructure; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.ViewModel; namespace Flow.Launcher.Resources.Pages { @@ -15,10 +16,10 @@ namespace Flow.Launcher.Resources.Pages protected override void OnNavigatedTo(NavigationEventArgs e) { - if (e.ExtraData is Settings settings) - Settings = settings; - else - throw new ArgumentException("Unexpected Navigation Parameter for Settings"); + Settings = Ioc.Default.GetRequiredService(); + // Sometimes the navigation is not triggered by button click, + // so we need to reset the page number + Ioc.Default.GetRequiredService().PageNum = 5; InitializeComponent(); } diff --git a/Flow.Launcher/Resources/SettingWindowStyle.xaml b/Flow.Launcher/Resources/SettingWindowStyle.xaml index 2c73094c7..fc9246aa3 100644 --- a/Flow.Launcher/Resources/SettingWindowStyle.xaml +++ b/Flow.Launcher/Resources/SettingWindowStyle.xaml @@ -8,6 +8,8 @@ + + F1 M512,512z M0,0z M448,256C448,150,362,64,256,64L256,448C362,448,448,362,448,256z M0,256A256,256,0,1,1,512,256A256,256,0,1,1,0,256z + @@ -123,8 +166,8 @@ x:Name="Spacer" Width="Auto" Height="Auto" - Margin="0,10,5,0" - Padding="0,0,0,0" + Margin="0 10 5 0" + Padding="0 0 0 0" BorderBrush="Transparent" BorderThickness="0"> @@ -180,7 +223,7 @@ Grid.Column="0" Width="4" Height="18" - Margin="0,11,0,11" + Margin="0 11 0 11" Fill="{DynamicResource ToggleSwitchFillOn}" RadiusX="2" RadiusY="2" @@ -188,7 +231,7 @@ - + - + @@ -255,7 +298,7 @@ - + @@ -267,7 +310,7 @@ - + @@ -284,10 +327,10 @@ - \ No newline at end of file + diff --git a/Flow.Launcher/SearchDelayTimeWindow.xaml b/Flow.Launcher/SearchDelayTimeWindow.xaml new file mode 100644 index 000000000..1c7ca58d0 --- /dev/null +++ b/Flow.Launcher/SearchDelayTimeWindow.xaml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SearchDelayTimeWindow.xaml.cs b/Flow.Launcher/SearchDelayTimeWindow.xaml.cs new file mode 100644 index 000000000..4a3c9f5a7 --- /dev/null +++ b/Flow.Launcher/SearchDelayTimeWindow.xaml.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Windows; +using Flow.Launcher.Plugin; +using Flow.Launcher.SettingPages.ViewModels; +using Flow.Launcher.ViewModel; +using static Flow.Launcher.SettingPages.ViewModels.SettingsPaneGeneralViewModel; + +namespace Flow.Launcher; + +public partial class SearchDelayTimeWindow : Window +{ + private readonly PluginViewModel _pluginViewModel; + + public SearchDelayTimeWindow(PluginViewModel pluginViewModel) + { + InitializeComponent(); + _pluginViewModel = pluginViewModel; + } + + private void SearchDelayTimeWindow_OnLoaded(object sender, RoutedEventArgs e) + { + tbSearchDelayTimeTips.Text = string.Format(App.API.GetTranslation("searchDelayTime_tips"), + App.API.GetTranslation("default")); + tbOldSearchDelayTime.Text = _pluginViewModel.SearchDelayTimeText; + var searchDelayTimes = DropdownDataGeneric.GetValues("SearchDelayTime"); + SearchDelayTimeData selected = null; + // Because default value is SearchDelayTime.VeryShort, we need to get selected value before adding default value + if (_pluginViewModel.PluginSearchDelayTime != null) + { + selected = searchDelayTimes.FirstOrDefault(x => x.Value == _pluginViewModel.PluginSearchDelayTime); + } + // Add default value to the beginning of the list + // When _pluginViewModel.PluginSearchDelayTime equals null, we will select this + searchDelayTimes.Insert(0, new SearchDelayTimeData { Display = App.API.GetTranslation("default"), LocalizationKey = "default" }); + selected ??= searchDelayTimes.FirstOrDefault(); + cbDelay.ItemsSource = searchDelayTimes; + cbDelay.SelectedItem = selected; + cbDelay.Focus(); + } + + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + { + Close(); + } + + private void btnDone_OnClick(object sender, RoutedEventArgs _) + { + // Update search delay time + var selected = cbDelay.SelectedItem as SearchDelayTimeData; + SearchDelayTime? changedValue = selected?.LocalizationKey != "default" ? selected.Value : null; + _pluginViewModel.PluginSearchDelayTime = changedValue; + + // Update search delay time text and close window + _pluginViewModel.OnSearchDelayTimeChanged(); + Close(); + } +} diff --git a/Flow.Launcher/SelectBrowserWindow.xaml b/Flow.Launcher/SelectBrowserWindow.xaml index d8807dbef..c12879a04 100644 --- a/Flow.Launcher/SelectBrowserWindow.xaml +++ b/Flow.Launcher/SelectBrowserWindow.xaml @@ -28,14 +28,11 @@ - - - @@ -51,7 +50,18 @@ - + + + + + + + - + public partial class PluginsManagerSettings { - private readonly SettingsViewModel viewModel; - internal PluginsManagerSettings(SettingsViewModel viewModel) { InitializeComponent(); - this.viewModel = viewModel; - - this.DataContext = viewModel; + DataContext = viewModel; } } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index 438a407e2..df5a2c784 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -6,7 +6,7 @@ "Name": "Plugins Manager", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Author": "Jeremy Wu", - "Version": "3.1.1", + "Version": "3.2.4", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj index 876bac1e7..0c501b2d9 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj @@ -13,6 +13,7 @@ false false en + true @@ -51,7 +52,13 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ar.xaml index c4cc85463..4104bd757 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ar.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ar.xaml @@ -1,11 +1,11 @@  - Process Killer - Kill running processes from Flow Launcher + قاتل العمليات + اقتل العمليات قيد التشغيل من Flow Launcher - kill all instances of "{0}" - kill {0} processes - kill all instances + قتل جميع الأمثلة من "{0}" + قتل عمليات {0} + قتل جميع الأمثلة diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/de.xaml index c27892d51..8697818dc 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/de.xaml @@ -1,11 +1,11 @@  - Prozesskiller - Kill running processes from Flow Launcher + Process Killer + Laufende Prozesse aus Flow Launcher beenden - kill all instances of "{0}" - kill {0} processes - kill all instances + Alle Instanzen von "{0}" beenden + {0} Prozesse beenden + Alle Instanzen beenden diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml index e7a136114..ea6e54fef 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml @@ -1,6 +1,7 @@ - + Process Killer Kill running processes from Flow Launcher @@ -9,4 +10,6 @@ kill {0} processes kill all instances + Put processes with visible windows on the top + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/he.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/he.xaml new file mode 100644 index 000000000..018435eca --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/he.xaml @@ -0,0 +1,11 @@ + + + + מנהל תהליכים + סגור תהליכים פעילים מתוך Flow Launcher + + סגור את כל המופעים של "{0}" + סגור {0} תהליכים + סגור את כל המופעים + + diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nb.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nb.xaml index c4cc85463..f06121887 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nb.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nb.xaml @@ -1,11 +1,11 @@  - Process Killer - Kill running processes from Flow Launcher + Prosessterminerer + Terminer kjørende prosesser fra Flow Launcher - kill all instances of "{0}" - kill {0} processes - kill all instances + terminer alle forekomster av "{0}" + terminer {0} processes + terminer alle forekomstene diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pl.xaml index c4cc85463..a111f8776 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pl.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pl.xaml @@ -1,11 +1,11 @@  - Process Killer - Kill running processes from Flow Launcher + Zamykacz procesów + Zamykaj uruchomione procesy z poziomu Flow Launcher - kill all instances of "{0}" - kill {0} processes - kill all instances + zamknij wszystkie instancje "{0}" + zamknij {0} procesów + zamknij wszystkie instancje diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index be2a2dd66..4f5d1becd 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -1,18 +1,27 @@ +using System; using System.Collections.Generic; using System.Linq; -using Flow.Launcher.Infrastructure; +using System.Windows.Controls; +using Flow.Launcher.Plugin.ProcessKiller.ViewModels; +using Flow.Launcher.Plugin.ProcessKiller.Views; namespace Flow.Launcher.Plugin.ProcessKiller { - public class Main : IPlugin, IPluginI18n, IContextMenu + public class Main : IPlugin, IPluginI18n, IContextMenu, ISettingProvider { - private ProcessHelper processHelper = new ProcessHelper(); + private readonly ProcessHelper processHelper = new(); private static PluginInitContext _context; + internal Settings Settings; + + private SettingsViewModel _viewModel; + public void Init(PluginInitContext context) { _context = context; + Settings = context.API.LoadSettingJsonStorage(); + _viewModel = new SettingsViewModel(Settings); } public List Query(Query query) @@ -48,7 +57,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller { foreach (var p in similarProcesses) { - processHelper.TryKill(p); + processHelper.TryKill(_context, p); } return true; @@ -62,16 +71,72 @@ namespace Flow.Launcher.Plugin.ProcessKiller private List CreateResultsFromQuery(Query query) { - string termToSearch = query.Search; - var processlist = processHelper.GetMatchingProcesses(termToSearch); - - if (!processlist.Any()) + // Get all non-system processes + var allPocessList = processHelper.GetMatchingProcesses(); + if (!allPocessList.Any()) { return null; } - var results = new List(); + // Filter processes based on search term + var searchTerm = query.Search; + var processlist = new List(); + var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); + if (string.IsNullOrWhiteSpace(searchTerm)) + { + foreach (var p in allPocessList) + { + var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p); + if (processWindowTitle.TryGetValue(p.Id, out var windowTitle)) + { + // Add score to prioritize processes with visible windows + // And use window title for those processes + processlist.Add(new ProcessResult(p, Settings.PutVisibleWindowProcessesTop ? 200 : 0, windowTitle, null, progressNameIdTitle)); + } + else + { + processlist.Add(new ProcessResult(p, 0, progressNameIdTitle, null, progressNameIdTitle)); + } + } + } + else + { + foreach (var p in allPocessList) + { + var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p); + + if (processWindowTitle.TryGetValue(p.Id, out var windowTitle)) + { + // Get max score from searching process name, window title and process id + var windowTitleMatch = _context.API.FuzzySearch(searchTerm, windowTitle); + var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle); + var score = Math.Max(windowTitleMatch.Score, processNameIdMatch.Score); + if (score > 0) + { + // Add score to prioritize processes with visible windows + // And use window title for those processes + if (Settings.PutVisibleWindowProcessesTop) + { + score += 200; + } + processlist.Add(new ProcessResult(p, score, windowTitle, + score == windowTitleMatch.Score ? windowTitleMatch : null, progressNameIdTitle)); + } + } + else + { + var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle); + var score = processNameIdMatch.Score; + if (score > 0) + { + processlist.Add(new ProcessResult(p, score, progressNameIdTitle, processNameIdMatch, progressNameIdTitle)); + } + } + } + } + + var results = new List(); foreach (var pr in processlist) { var p = pr.Process; @@ -79,28 +144,30 @@ namespace Flow.Launcher.Plugin.ProcessKiller results.Add(new Result() { IcoPath = path, - Title = p.ProcessName + " - " + p.Id, + Title = pr.Title, + TitleToolTip = pr.Tooltip, SubTitle = path, - TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData, + TitleHighlightData = pr.TitleMatch?.MatchData, Score = pr.Score, ContextData = p.ProcessName, AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}", Action = (c) => { - processHelper.TryKill(p); + processHelper.TryKill(_context, p); // Re-query to refresh process list - _context.API.ChangeQuery(query.RawQuery, true); + _context.API.ReQuery(); return true; } }); } + // Order results by process name for processes without visible windows var sortedResults = results.OrderBy(x => x.Title).ToList(); // When there are multiple results AND all of them are instances of the same executable // add a quick option to kill them all at the top of the results. var firstResult = sortedResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.SubTitle)); - if (processlist.Count > 1 && !string.IsNullOrEmpty(termToSearch) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle)) + if (processlist.Count > 1 && !string.IsNullOrEmpty(searchTerm) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle)) { sortedResults.Insert(1, new Result() { @@ -112,10 +179,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller { foreach (var p in processlist) { - processHelper.TryKill(p.Process); + processHelper.TryKill(_context, p.Process); } // Re-query to refresh process list - _context.API.ChangeQuery(query.RawQuery, true); + _context.API.ReQuery(); return true; } }); @@ -123,5 +190,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller return sortedResults; } + + public Control CreateSettingPanel() + { + return new SettingsControl(_viewModel); + } } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt new file mode 100644 index 000000000..13bf27932 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt @@ -0,0 +1,7 @@ +QueryFullProcessImageName +OpenProcess +EnumWindows +GetWindowTextLength +GetWindowText +IsWindowVisible +GetWindowThreadProcessId \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 0acc39fbb..4c07341ec 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -1,17 +1,18 @@ -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Logger; +using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Text; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Threading; namespace Flow.Launcher.Plugin.ProcessKiller { internal class ProcessHelper { - private readonly HashSet _systemProcessList = new HashSet() + private readonly HashSet _systemProcessList = new() { "conhost", "svchost", @@ -29,46 +30,96 @@ namespace Flow.Launcher.Plugin.ProcessKiller "explorer" }; - private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower()); + private const string FlowLauncherProcessName = "Flow.Launcher"; + + private bool IsSystemProcessOrFlowLauncher(Process p) => + _systemProcessList.Contains(p.ProcessName.ToLower()) || + string.Equals(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase); /// - /// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm + /// Get title based on process name and id /// - public List GetMatchingProcesses(string searchTerm) + public static string GetProcessNameIdTitle(Process p) { - var processlist = new List(); + var sb = new StringBuilder(); + sb.Append(p.ProcessName); + sb.Append(" - "); + sb.Append(p.Id); + return sb.ToString(); + } + + /// + /// Returns a Process for evey running non-system process + /// + public List GetMatchingProcesses() + { + var processlist = new List(); foreach (var p in Process.GetProcesses()) { - if (IsSystemProcess(p)) continue; + if (IsSystemProcessOrFlowLauncher(p)) continue; - if (string.IsNullOrWhiteSpace(searchTerm)) - { - // show all non-system processes - processlist.Add(new ProcessResult(p, 0)); - } - else - { - var score = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + p.Id).Score; - if (score > 0) - { - processlist.Add(new ProcessResult(p, score)); - } - } + processlist.Add(p); } return processlist; } + /// + /// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title. + /// + public static unsafe Dictionary GetProcessesWithNonEmptyWindowTitle() + { + var processDict = new Dictionary(); + PInvoke.EnumWindows((hWnd, _) => + { + var windowTitle = GetWindowTitle(hWnd); + if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd)) + { + uint processId = 0; + var result = PInvoke.GetWindowThreadProcessId(hWnd, &processId); + if (result == 0u || processId == 0u) + { + return false; + } + + var process = Process.GetProcessById((int)processId); + if (!processDict.ContainsKey((int)processId)) + { + processDict.Add((int)processId, windowTitle); + } + } + + return true; + }, IntPtr.Zero); + + return processDict; + } + + private static unsafe string GetWindowTitle(HWND hwnd) + { + var capacity = PInvoke.GetWindowTextLength(hwnd) + 1; + int length; + Span buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity]; + fixed (char* pBuffer = buffer) + { + // If the window has no title bar or text, if the title bar is empty, + // or if the window or control handle is invalid, the return value is zero. + length = PInvoke.GetWindowText(hwnd, pBuffer, capacity); + } + + return buffer[..length].ToString(); + } + /// /// Returns all non-system processes whose file path matches the given processPath /// public IEnumerable GetSimilarProcesses(string processPath) { - return Process.GetProcesses().Where(p => !IsSystemProcess(p) && TryGetProcessFilename(p) == processPath); + return Process.GetProcesses().Where(p => !IsSystemProcessOrFlowLauncher(p) && TryGetProcessFilename(p) == processPath); } - public void TryKill(Process p) + public void TryKill(PluginInitContext context, Process p) { try { @@ -80,47 +131,37 @@ namespace Flow.Launcher.Plugin.ProcessKiller } catch (Exception e) { - Log.Exception($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e); + context.API.LogException($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e); } } - public string TryGetProcessFilename(Process p) + public unsafe string TryGetProcessFilename(Process p) { try { - int capacity = 2000; - StringBuilder builder = new StringBuilder(capacity); - IntPtr ptr = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, p.Id); - if (!QueryFullProcessImageName(ptr, 0, builder, ref capacity)) + var handle = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, (uint)p.Id); + if (handle.Value == IntPtr.Zero) { - return String.Empty; + return string.Empty; } - return builder.ToString(); + using var safeHandle = new SafeProcessHandle(handle.Value, true); + uint capacity = 2000; + Span buffer = new char[capacity]; + fixed (char* pBuffer = buffer) + { + if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, (PWSTR)pBuffer, ref capacity)) + { + return string.Empty; + } + + return buffer[..(int)capacity].ToString(); + } } catch { - return ""; + return string.Empty; } } - - [Flags] - private enum ProcessAccessFlags : uint - { - QueryLimitedInformation = 0x00001000 - } - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool QueryFullProcessImageName( - [In] IntPtr hProcess, - [In] int dwFlags, - [Out] StringBuilder lpExeName, - ref int lpdwSize); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern IntPtr OpenProcess( - ProcessAccessFlags processAccess, - bool bInheritHandle, - int processId); } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs index 03856677e..146c9c92c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs @@ -1,17 +1,27 @@ using System.Diagnostics; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.ProcessKiller { internal class ProcessResult { - public ProcessResult(Process process, int score) + public ProcessResult(Process process, int score, string title, MatchResult match, string tooltip) { Process = process; Score = score; + Title = title; + TitleMatch = match; + Tooltip = tooltip; } public Process Process { get; } public int Score { get; } + + public string Title { get; } + + public MatchResult TitleMatch { get; } + + public string Tooltip { get; } } -} \ No newline at end of file +} diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs new file mode 100644 index 000000000..916bc6a39 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs @@ -0,0 +1,7 @@ +namespace Flow.Launcher.Plugin.ProcessKiller +{ + public class Settings + { + public bool PutVisibleWindowProcessesTop { get; set; } = false; + } +} diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs new file mode 100644 index 000000000..bacf1ba08 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs @@ -0,0 +1,18 @@ +namespace Flow.Launcher.Plugin.ProcessKiller.ViewModels +{ + public class SettingsViewModel + { + public Settings Settings { get; set; } + + public SettingsViewModel(Settings settings) + { + Settings = settings; + } + + public bool PutVisibleWindowProcessesTop + { + get => Settings.PutVisibleWindowProcessesTop; + set => Settings.PutVisibleWindowProcessesTop = value; + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml new file mode 100644 index 000000000..d15d6c3e0 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs new file mode 100644 index 000000000..a066ab6a9 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs @@ -0,0 +1,17 @@ +using System.Windows.Controls; +using Flow.Launcher.Plugin.ProcessKiller.ViewModels; + +namespace Flow.Launcher.Plugin.ProcessKiller.Views; + +public partial class SettingsControl : UserControl +{ + /// + /// Interaction logic for SettingsControl.xaml + /// + public SettingsControl(SettingsViewModel viewModel) + { + InitializeComponent(); + + DataContext = viewModel; + } +} diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json index c1760d1cd..956c4b4e1 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json @@ -4,7 +4,7 @@ "Name":"Process Killer", "Description":"Kill running processes from Flow", "Author":"Flow-Launcher", - "Version":"3.0.4", + "Version":"3.0.8", "Language":"csharp", "Website":"https://github.com/Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller", "IcoPath":"Images\\app.png", diff --git a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml.cs index be8b768bd..6385ab364 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml.cs @@ -32,7 +32,7 @@ namespace Flow.Launcher.Plugin.Program var (modified, msg) = ViewModel.AddOrUpdate(); if (modified == false && msg != null) { - MessageBox.Show(msg); // Invalid + ViewModel.API.ShowMsgBox(msg); // Invalid return; } DialogResult = modified; diff --git a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj index 132c0c705..99c1a12e9 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj +++ b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj @@ -52,6 +52,10 @@ PreserveNewest + + + + @@ -60,7 +64,11 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/ar.xaml index dda427996..69ca16b69 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/ar.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/ar.xaml @@ -2,92 +2,96 @@ - Reset Default - Delete - Edit - Add - Name - Enable - Enabled - Disable - Status - Enabled - Disabled - Location - All Programs - File Type - Reindex - Indexing - Index Sources - Options - UWP Apps - When enabled, Flow will load UWP Applications - Start Menu - When enabled, Flow will load programs from the start menu - Registry - When enabled, Flow will load programs from the registry - PATH - When enabled, Flow will load programs from the PATH environment variable - Hide app path - For executable files such as UWP or lnk, hide the file path from being visible - Search in Program Description - Flow will search program's description - Suffixes - Max Depth + إعادة الوضع الإفتراضي + حذف + تعديل + إضافة + الاسم + تفعيل + مُفعّل + تعطيل + الحالة + مُفعّل + مُعطّل + الموقع + كل البرامج + نوع الملف + إعادة فهرسة + الفهرسة + مصادر الفهرسة + الخيارات + تطبيقات UWP + عند التمكين، سيقوم Flow بتحميل تطبيقات UWP + قائمة ابدأ + عند التمكين، سيقوم Flow بتحميل البرامج من قائمة ابدأ + سجل النظام + عند التمكين، سيقوم Flow بتحميل البرامج من سجل النظام + مسار النظام + عند التمكين، سيقوم Flow بتحميل البرامج من متغير بيئة PATH + إخفاء مسار التطبيق + بالنسبة للملفات القابلة للتنفيذ مثل UWP أو lnk، قم بإخفاء مسار الملف من أن يكون مرئيًا + إخفاء برامج إلغاء التثبيت + إخفاء البرامج التي تحمل أسماء برامج إلغاء تثبيت شائعة، مثل unins000.exe + البحث في وصف البرنامج + سيقوم Flow بالبحث في وصف البرنامج + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list + اللاحقات + أقصى عمق - Directory - Browse - File Suffixes: - Maximum Search Depth (-1 is unlimited): + الدليل + تصفح + لاحقات الملفات: + أقصى عمق للبحث (-1 غير محدود): - Please select a program source - Are you sure you want to delete the selected program sources? - Another program source with the same location already exists. + الرجاء تحديد مصدر برنامج + هل أنت متأكد أنك تريد حذف مصادر البرامج المحددة؟ + مصدر برنامج آخر بنفس الموقع موجود بالفعل. - Program Source - Edit directory and status of this program source. + مصدر البرنامج + قم بتحرير دليل وحالة مصدر البرنامج هذا. - Update - Program Plugin will only index files with selected suffixes and .url files with selected protocols. - Successfully updated file suffixes - File suffixes can't be empty - Protocols can't be empty + تحديث + يقوم برنامج Plugin بفهرسة الملفات ذات اللاحقات المحددة وملفات .url ذات البروتوكولات المحددة فقط. + تم تحديث لاحقات الملفات بنجاح + لا يمكن أن تكون لاحقات الملفات فارغة + لا يمكن أن تكون البروتوكولات فارغة - File Suffixes - URL Protocols - Steam Games - Epic Games + لاحقات الملفات + بروتوكولات URL + ألعاب Steam + ألعاب Epic Http/Https - Custom URL Protocols - Custom File Suffixes + بروتوكولات URL مخصصة + أنواع ملفات مخصصة - Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py) + أدخل لاحقات الملفات التي تريد فهرستها. يجب فصل اللاحقات بـ ';'. (مثال>bat;py) - Insert protocols of .url files you want to index. Protocols should be separated by ';', and should end with "://". (ex>ftp://;mailto://) + أدخل بروتوكولات ملفات .url التي تريد فهرستها. يجب فصل البروتوكولات بـ ';'، ويجب أن تنتهي بـ "://". (مثال>ftp://;mailto://) - Run As Different User - Run As Administrator - Open containing folder - Disable this program from displaying - Open target folder + التشغيل كمستخدم مختلف + التشغيل كمسؤول + فتح المجلد المحتوي + تعطيل عرض هذا البرنامج + فتح المجلد الهدف - Program - Search programs in Flow Launcher + البرنامج + البحث عن البرامج في Flow Launcher - Invalid Path + مسار غير صالح - Customized Explorer - Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. - Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. + مستكشف مخصص + وسيطات + يمكنك تخصيص المستكشف المستخدم لفتح مجلد الحاوية عن طريق إدخال المتغير البيئي للمستكشف الذي تريد استخدامه. سيكون من المفيد استخدام CMD لاختبار ما إذا كان المتغير البيئي متاحًا. + أدخل الوسيطات المخصصة التي تريد إضافتها لمستكشفك المخصص. %s للدليل الأصل، %f للمسار الكامل (الذي يعمل فقط مع win32). راجع موقع المستكشف الإلكتروني للحصول على التفاصيل. - Success - Error - Successfully disabled this program from displaying in your query - This app is not intended to be run as administrator - Unable to run {0} + نجاح + خطأ + تم تعطيل عرض هذا البرنامج في استعلامك بنجاح + لم يتم تصميم هذا التطبيق للتشغيل كمسؤول + تعذر تشغيل {0} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/cs.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/cs.xaml index 9a703d3de..1c70c6b6f 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/cs.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/cs.xaml @@ -30,8 +30,12 @@ Pokud je tato možnost povolena, Flow načte programy z proměnné prostředí PATH Skrýt cestu k aplikaci U spustitelných souborů, jako jsou UWP nebo odkazy, nezobrazujte cestu k souborům + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Povolit popis programu Flow bude vyhledávat v popisu programu + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Přípony Max. hloubka @@ -80,7 +84,7 @@ Vlastní Průzkumník Arg - Umístění úvodní složky můžete upravit vložením proměnných prostředí, které chcete použít. Dostupnost proměnných prostředí můžete otestovat pomocí příkazového řádku. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Zadejte argumenty, které chcete přidat pro správce souborů. %s pro nadřazenou složku, %f pro úplnou cestu (funguje pouze pro win32). Podrobnosti naleznete na webové stránce správce souborů. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/da.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/da.xaml index 676eb76cb..0f39f5d56 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/da.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/da.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable Hide app path For executable files such as UWP or lnk, hide the file path from being visible + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffixes Max Depth @@ -80,7 +84,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/de.xaml index 293dbc9c8..662765760 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/de.xaml @@ -2,7 +2,7 @@ - Reset Default + Auf Default zurücksetzen Löschen Bearbeiten Hinzufügen @@ -12,82 +12,86 @@ Deaktivieren Status Aktiviert - Disabled - Speicherort + Deaktiviert + Ort Alle Programme - File Type - erneut Indexieren - Indexieren - Index Sources - Options - UWP Apps - When enabled, Flow will load UWP Applications - Start Menu - When enabled, Flow will load programs from the start menu + Dateityp + Neu indizieren + Indexierung + Indexquellen + Optionen + UWP-Apps + Wenn aktiviert, wird Flow UWP-Anwendungen laden + Startmenü + Wenn aktiviert, wird Flow Programme aus dem Startmenü laden Registry - When enabled, Flow will load programs from the registry + Wenn aktiviert, wird Flow Programme aus der Registry laden PATH - When enabled, Flow will load programs from the PATH environment variable - App-Pfad verstecken - For executable files such as UWP or lnk, hide the file path from being visible - Search in Program Description - Flow will search program's description - Endungen + Wenn aktiviert, wird Flow Programme aus der PATH-Umgebungsvariable laden + App-Pfad ausblenden + Für ausführbare Dateien wie UWP oder lnk den Dateipfad nicht mehr sichtbar machen + Uninstaller ausblenden + Blendet Programme mit gängigen Uninstaller-Namen aus, wie unins000.exe + In Programmbeschreibung suchen + Flow wird in Programmbeschreibung suchen + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list + Suffixe Maximale Tiefe - Verzeichnis: + Verzeichnis Durchsuchen - Dateiendungen: + Dateisuffixe: Maximale Suchtiefe (-1 ist unlimitiert): - Bitte wähle eine Programmquelle - Are you sure you want to delete the selected program sources? - Another program source with the same location already exists. + Bitte wählen Sie eine Programmquelle aus + Sind Sie sicher, dass Sie die ausgewählten Programmquellen löschen wollen? + Eine andere Programmquelle mit dem gleichen Ort ist bereits vorhanden. - Program Source - Edit directory and status of this program source. + Programmquelle + Verzeichnis und Status dieser Programmquelle bearbeiten. Aktualisieren - Program Plugin will only index files with selected suffixes and .url files with selected protocols. - Dateiendungen wurden erfolgreich aktualisiert - Dateiendungen dürfen nicht leer sein - Protocols can't be empty + Das Programm Plug-in indiziert nur Dateien mit ausgewählten Suffixen und .url-Dateien mit ausgewählten Protokollen. + Dateisuffixe erfolgreich aktualisiert + Dateisuffixe dürfen nicht leer sein + Protokolle dürfen nicht leer sein - Indexiere Dateiendungen - URL Protocols + Dateisuffixe + URL-Protokolle Steam Games Epic Games Http/Https - Custom URL Protocols - Custom File Suffixes + Benutzerdefinierte URL-Protokolle + Benutzerdefinierte Dateisuffixe - Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py) + Fügen Sie Suffixe ein, die Sie indizieren möchten, ein. Suffixe sollten durch ';' getrennt werden. (z.B.>bat;py) - Insert protocols of .url files you want to index. Protocols should be separated by ';', and should end with "://". (ex>ftp://;mailto://) + Fügen Sie Protokolle von .url-Dateien ein, die Sie indizieren möchten. Protokolle sollten durch ';' getrennt werden und mit "://" enden. (ex>ftp://;mailto://) - Run As Different User + Als anderer Benutzer ausführen Als Administrator ausführen Enthaltenden Ordner öffnen - Disable this program from displaying - Open target folder + Dieses Programm von der Anzeige deaktivieren + Zielordner öffnen Programm - Suche Programme mit Flow Launcher + Programme in Flow Launcher suchen - Ungültiger Pfad + Pfad ungültig - Customized Explorer - Argumente - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. - Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. + Individuell angepasster Explorer + Args + Sie können den zum Öffnen des Containerordners verwendeten Explorer individuell anpassen, indem Sie die Umgebungsvariable des von Ihnen verwendenden Explorers eingeben. Es ist sinnvoll, CMD zu verwenden, um zu testen, ob die Umgebungsvariable verfügbar ist. + Geben Sie die benutzerdefinierten Args ein, die Sie für Ihren individuell angepassten Explorer hinzufügen möchten. %s für übergeordnetes Verzeichnis, %f für vollständigen Pfad (was nur für Win32 funktioniert). Details finden Sie auf der Website des Explorers. - Erfolgreich - Error - Successfully disabled this program from displaying in your query - This app is not intended to be run as administrator - Unable to run {0} + Erfolg + Fehler + Dieses Programm wurde erfolgreich von der Anzeige in Ihrer Abfrage deaktiviert + Diese App ist nicht zur Ausführung als Administrator gedacht + {0} kann nicht ausgeführt werden diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml index 25ceac3bb..790c9d2c6 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml @@ -36,6 +36,8 @@ Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffixes Max Depth @@ -84,7 +86,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/es-419.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/es-419.xaml index 375dcd041..b928bd1ce 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/es-419.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/es-419.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable Hide app path For executable files such as UWP or lnk, hide the file path from being visible + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffixes Max Depth @@ -80,7 +84,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/es.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/es.xaml index d4eccc8ac..57b5c94b7 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/es.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/es.xaml @@ -26,12 +26,16 @@ Cuando esté activado, Flow cargará los programas desde el menú de inicio Registro Cuando esté activado, Flow cargará los programas desde el registro - RUTA (PATH) + PATH Cuando esté activado, Flow cargará los programas desde la variable de entorno PATH Ocultar ruta de aplicación - Para los archivos ejecutables como UWP o lnk, oculta la ruta del archivo para que no sea visible + Oculta la ruta de los archivos ejecutables, como UWP o lnk + Ocultar desinstaladores + Oculta nombres comunes de programas de desinstalación, como unins000.exe Buscar en la descripción del programa - Flow buscará la descripción del programa + Flow buscará en la descripción del programa + Ocultar aplicaciones duplicadas + Ocultar programas Win32 duplicados que ya están en la lista UWP Extensiones Profundidad máxima @@ -41,7 +45,7 @@ Profundidad de búsqueda máxima (-1 es sin límite): Por favor, seleccione la ruta del programa - ¿Está seguro que desea eliminar las fuentes del programa seleccionadas? + ¿Está seguro de que desea eliminar las fuentes del programa seleccionadas? Ya existe otra fuente de programa con la misma ubicación. Fuente de Programa @@ -80,7 +84,7 @@ Explorador personalizado Argumentos - Puede personalizar el explorador utilizado para abrir la carpeta contenedor introduciendo la Variable de entorno del explorador que desea utilizar. Será útil usar CMD para probar si la Variable de entorno está disponible. + Puede personalizar el explorador utilizado para abrir la carpeta contenedora introduciendo la Variable de Entorno del explorador que desea utilizar. Para comprobar si la Variable de Entorno está disponible, puede utilizar CMD. Introduzca los argumentos personalizados que desea agregar para el explorador personalizado. %s para carpeta contenedora, %f para ruta completa (solo funciona para win32). Consulte la página web del explorador para obtener más detalles. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/fr.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/fr.xaml index d8b3dc5dd..7cccd5a42 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/fr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/fr.xaml @@ -30,8 +30,12 @@ Lorsque cette option est activée, Flow chargera les programmes à partir de la variable d'environnement PATH. Masquer le chemin de l'application Pour les fichiers exécutables tels que UWP ou lnk, masquez le chemin d'accès pour ne pas être visible + Masquer les désinstallateurs + Masque les programmes portant des noms de désinstallateurs courants, tels que unins000.exe Rechercher dans la description du programme Flow cherchera la description du programme + Masquer les applications dupliquées + Masquer les programmes Win32 dupliqués qui sont déjà dans la liste UWP Suffixes Profondeur max. @@ -69,11 +73,11 @@ Exécuter en tant qu'utilisateur différent Exécuter en tant qu'administrateur - Ouvrir le répertoire - Désactiver l'affichage de ce programme + Ouvrir l'emplacement du fichier + Masquer ce programme des résultats Ouvrir le répertoire cible - Programme + Programmes Rechercher des programmes dans Flow Launcher Chemin d'accès invalide diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/he.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/he.xaml new file mode 100644 index 000000000..0e8b2f1d5 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/he.xaml @@ -0,0 +1,97 @@ + + + + + איפוס לברירת מחדל + מחק + ערוך + הוסף + שם + הפעל + מופעל + השבת + סטטוס + מופעל + מושבת + מיקום + כל התוכניות + סוג קובץ + בצע אינדוקס מחדש + מבצע אינדוקס + מקורות אינדוקס + אפשרויות + אפליקציות UWP + כאשר האפשרות מופעלת, Flow יטען אפליקציות UWP + תפריט התחלה + כאשר האפשרות מופעלת, Flow יטען תוכניות מתפריט ההתחלה + רישום + כאשר האפשרות מופעלת, Flow יטען תוכניות מהרישום + משתנה PATH + כאשר האפשרות מופעלת, Flow יטען תוכניות מהמשתנה PATH + הסתר נתיב אפליקציה + לקבצי הפעלה כמו UWP או lnk, הסתר את נתיב הקובץ מהתצוגה + הסתר מסירי התקנה + מסתיר תוכניות עם שמות מסירי התקנה נפוצים, כגון unins000.exe + חיפוש בתיאור התוכנית + Flow יחפש בתיאור התוכנית + הסתר אפליקציות כפולות + הסתר תוכניות Win32 כפולות שכבר קיימות ברשימת UWP + סיומות + עומק מקסימלי + + תיקייה + עיון + סיומות קבצים: + עומק חיפוש מקסימלי (-1 ללא הגבלה): + + אנא בחר מקור תוכנה + האם אתה בטוח שברצונך למחוק את מקורות התוכניות שנבחרו? + מקור תוכנה נוסף עם אותו מיקום כבר קיים. + + מקור תוכנה + ערוך את התיקייה והסטטוס של מקור תוכנה זה. + + עדכון + תוסף התוכנה יאנדקס רק קבצים עם סיומות נבחרות וקובצי .url עם פרוטוקולים נבחרים. + סיומות הקבצים עודכנו בהצלחה + סיומות הקבצים לא יכולות להיות ריקות + פרוטוקולים לא יכולים להיות ריקים + + סיומות קבצים + פרוטוקולי URL + משחקי Steam + משחקי Epic + Http/Https + פרוטוקולי URL מותאמים אישית + סיומות קבצים מותאמות אישית + + הכנס סיומות קבצים שברצונך לאנדקס. סיומות יש להפריד באמצעות ';'. (לדוגמה: bat;py) + + + הכנס פרוטוקולים של קובצי .url שברצונך לאנדקס. יש להפריד פרוטוקולים באמצעות ';', ולסיים ב- "://". (לדוגמה: ftp://;mailto://) + + + הפעל כמשתמש אחר + הפעל כמנהל + פתח תיקייה מכילה + השבת הצגת תוכנה זו + פתח תיקיית יעד + + תוכנה + חיפוש תוכניות ב-Flow Launcher + + נתיב לא חוקי + + סייר מותאם אישית + ארגומנטים + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + הזן את הארגומנטים שברצונך להוסיף לסייר המותאם אישית שלך. %s עבור ספריית האב, %f עבור הנתיב המלא (זמין רק עבור win32). בדוק באתר הסייר לפרטים נוספים. + + + הצליח + שגיאה + התוכנית הוסרה מהתצוגה בהצלחה + אפליקציה זו אינה מיועדת להפעלה כמנהל + לא ניתן להפעיל את {0} + + diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/it.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/it.xaml index e73c8d24e..cf5de9bab 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/it.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/it.xaml @@ -2,7 +2,7 @@ - Reset Default + Ripristina predefiniti Cancella Modifica Aggiungi @@ -10,28 +10,32 @@ Abilita Abilitato Disabilita - Status + Stato Abilitato - Disabled + Disabilitato Posizione Tutti i programmi - File Type + Tipo File Reindicizza Indicizzando - Index Sources - Options - UWP Apps - When enabled, Flow will load UWP Applications - Start Menu + Fonti dell'Indice + Opzioni + Applicazioni UWP + Quando abilitato, Flow caricherà applicazioni UWP + Menu Start Se abilitato, Flow caricherà i programmi dal Menu Start - Registry + Registro Se abilitato, Flow caricherà i programmi dal Registro PATH - When enabled, Flow will load programs from the PATH environment variable + Quando abilitato, Flow caricherà il programma dalla variabile d'ambiente PATH Nascondi percorso app Per i file eseguibili come UWP o lnk, nascondere il percorso del file + Nascondi programmi di disinstallazione + Nasconde programmi con nomi comuni di disinstallatori, come unins000.exe Cerca nella Descrizione del Programma - Flow will search program's description + Flow cercherà nella descrizione del programma + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffissi Profondità max @@ -80,7 +84,7 @@ Esplora Risorse personalizzato Parametri - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Inserisci i parametri personalizzati che vuoi aggiungere per il tuo Esplora Risorse personalizzato. %s per la cartella superiore, %f per il percorso completo (che funziona solo per win32). Controlla il sito dell'Esplora Risorse per i dettagli. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/ja.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/ja.xaml index 6c35d7df1..1f1dd4d37 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/ja.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/ja.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable Hide app path For executable files such as UWP or lnk, hide the file path from being visible + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffixes Max Depth @@ -80,7 +84,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/ko.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/ko.xaml index 2857194c7..affa7567f 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/ko.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/ko.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable 앱 경로 숨김 UWP나 Lnk 같이 실행 가능한 프로그램인 경우 경로를 표시하지 않습니다 + Uninstaller 숨기기 + Unins000처럼 일반적으로 사용되는 설치 삭제(Uninstaller) 프로그램의 이름을 숨깁니다. 프로그램 설명 검색 Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list 확장자 최대 깊이 @@ -80,7 +84,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/nb.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/nb.xaml index dda427996..3fa53aba8 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/nb.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/nb.xaml @@ -2,92 +2,96 @@ - Reset Default - Delete - Edit - Add - Name - Enable - Enabled - Disable + Tilbakestill standard + Slett + Rediger + Legg til + Navn + Aktiver + Aktivert + Deaktiver Status - Enabled - Disabled - Location - All Programs - File Type - Reindex - Indexing - Index Sources - Options - UWP Apps - When enabled, Flow will load UWP Applications - Start Menu - When enabled, Flow will load programs from the start menu - Registry - When enabled, Flow will load programs from the registry - PATH - When enabled, Flow will load programs from the PATH environment variable - Hide app path - For executable files such as UWP or lnk, hide the file path from being visible - Search in Program Description - Flow will search program's description - Suffixes - Max Depth + Aktivert + Deaktivert + Plassering + Alle programmer + Filtype + Indekser på nytt + Indekserer + Indeks kilder + Alternativer + UWP-apper + Når aktivert, vil Flow laste UWP-applikasjoner + Startmeny + Når aktivert, vil Flow laste programmer fra startmenyen + Registeret + Når aktivert, vil Flow laste programmer fra registeret + BANE + Når aktivert, vil Flow laste programmer fra BANE-miljøvariabelen + Skjul app-banen + For kjørbare filer som for eksempel UWP eller lnk, skjul filbanen fra å være synlig + Skjul avinstalleringsprogrammer + Skjuler programmer med samme navn til avinstalleringer, for eksempel unins000.exe + Søk i programbeskrivelse + Flow vil søke i programmets beskrivelse + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list + Suffikser + Maks dybde - Directory - Browse - File Suffixes: - Maximum Search Depth (-1 is unlimited): + Katalog + Bla + Filendelser: + Maks søkedybde (-1 er ubegrenset): - Please select a program source - Are you sure you want to delete the selected program sources? - Another program source with the same location already exists. + Vennligst velg en programkilde + Er du sikker på at du vil slette de valgte programkildene? + Det finnes allerede en annen programkilde med samme plassering. - Program Source - Edit directory and status of this program source. + Programkilde + Rediger katalog og status for denne programkilden. - Update - Program Plugin will only index files with selected suffixes and .url files with selected protocols. - Successfully updated file suffixes - File suffixes can't be empty - Protocols can't be empty + Oppdater + Programtillegg vil bare indeksere filer med valgte endelser og .url filer med valgte protokoller. + Vellykket oppdatering av filendelser + Filendelser kan ikke være tomme + Protokoller kan ikke være tomme - File Suffixes - URL Protocols - Steam Games - Epic Games + Filendelser + URL-protokoller + Steam-spill + Epic-spill Http/Https - Custom URL Protocols - Custom File Suffixes + Egendefinerte URL-protokoller + Egendefinerte filendelser - Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py) + Sett inn filendelser du vil indeksere. Suffikser skal skilles med ';'. (eks>bat;py) - Insert protocols of .url files you want to index. Protocols should be separated by ';', and should end with "://". (ex>ftp://;mailto://) + Sett inn protokoller for .url-filer du vil indeksere. Protokoller skal skilles med ';', og skal slutte med "://". (eks>ftp://;mailto://) - Run As Different User - Run As Administrator - Open containing folder - Disable this program from displaying - Open target folder + Kjør som en annen bruker + Kjør som administrator + Åpne inneholdende mappe + Deaktiver visningen av dette programmet + Åpne målmappe Program - Search programs in Flow Launcher + Søk etter programmer i Flow Launcher - Invalid Path + Ugyldig bane - Customized Explorer - Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. - Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. + Tilpasset utforsker + Argumenter + Du kan tilpasse utforskeren som brukes til å åpne beholdermappen, ved å skrive inn miljøvariabelen for utforskeren du vil bruke. Det vil være nyttig å bruke CMD for å teste om miljøvariabelen er tilgjengelig. + Skriv inn de tilpassede argumentene du vil legge til for din tilpassede utforsker. %s for overordnet katalog, %f for full bane (som bare fungerer for win32). Sjekk utforskerens nettsted for detaljer. - Success - Error - Successfully disabled this program from displaying in your query - This app is not intended to be run as administrator - Unable to run {0} + Vellykket + Feil + Deaktiverte dette programmet fra å vises i spørringen din + Denne appen er ikke ment å kjøre som administrator + Kan ikke kjøre {0} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/nl.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/nl.xaml index 972cbe8d6..8a86dc511 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/nl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/nl.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable Hide app path For executable files such as UWP or lnk, hide the file path from being visible + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffixes Max Depth @@ -80,7 +84,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/pl.xaml index d651fb0b9..8a163de7a 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/pl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/pl.xaml @@ -2,36 +2,40 @@ - Reset Default + Przywróć domyślne Usuń Edytuj Dodaj - Name + Nazwa Aktywne - Enabled - Disable + Aktywny + Wyłącz Status - Enabled - Disabled + Aktywny + Wyłączony Lokalizacja - All Programs - File Type + Wszystkie programy + Typ pliku Re-indeksuj Indeksowanie - Index Sources - Options - UWP Apps - When enabled, Flow will load UWP Applications - Start Menu - When enabled, Flow will load programs from the start menu - Registry - When enabled, Flow will load programs from the registry + Źródła indeksu + Opcje + Aplikacje UWP + Po włączeniu Flow załaduje aplikacje UWP + Menu Start + Gdy opcja ta jest włączona, Flow będzie ładować programy z menu Start + Rejestr + Gdy opcja ta jest włączona, Flow będzie ładować programy z rejestru PATH - When enabled, Flow will load programs from the PATH environment variable - Hide app path - For executable files such as UWP or lnk, hide the file path from being visible - Search in Program Description - Flow will search program's description + Gdy opcja ta jest włączona, Flow będzie ładować programy ze zmiennej środowiskowej PATH + Ukryj ścieżkę aplikacji + Dla plików wykonywalnych, takich jak UWP lub lnk, ukryj widoczną ścieżkę pliku + Ukryj deinstalatory + Ukrywa programy z typowymi nazwami deinstalatorów, takimi jak unins000.exe + Szukaj w opisie programu + Flow będzie przeszukiwać opisy programów + Ukryj zduplikowane aplikacje + Ukryj zduplikowane programy Win32, które znajdują się już na liście UWP Rozszerzenia Maksymalna głębokość @@ -41,53 +45,53 @@ Maksymalna głębokość wyszukiwania (-1 to nieograniczona): Musisz wybrać katalog programu - Are you sure you want to delete the selected program sources? - Another program source with the same location already exists. + Czy na pewno chcesz usunąć wybrane źródła programów? + Inne źródło programu z tą samą lokalizacją już istnieje. - Program Source - Edit directory and status of this program source. + Źródło programu + Edytuj katalog i status tego źródła programu. Aktualizuj - Program Plugin will only index files with selected suffixes and .url files with selected protocols. + Wtyczka programu będzie indeksować tylko pliki z wybranymi sufiksami oraz pliki .url z wybranymi protokołami. Pomyślnie zaktualizowano rozszerzenia plików Musisz podać rozszerzenia plików - Protocols can't be empty + Protokoły nie mogą być puste Rozszerzenia indeksowanych plików - URL Protocols - Steam Games + Protokoły URL + Gry Steam Epic Games Http/Https - Custom URL Protocols - Custom File Suffixes + Niestandardowe protokoły URL + Niestandardowe rozszerzenia plików - Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py) + Wpisz rozszerzenia plików, które chcesz indeksować. Rozszerzenia powinny być oddzielone średnikiem ';'. (np. bat;py) - Insert protocols of .url files you want to index. Protocols should be separated by ';', and should end with "://". (ex>ftp://;mailto://) + Wpisz protokoły plików .url, które chcesz indeksować. Protokoły powinny być oddzielone średnikiem ';' i kończyć się "://". (np. ftp://;mailto://) - Run As Different User + Uruchom jako inny użytkownik Uruchom jako administrator - Open containing folder - Disable this program from displaying - Open target folder + Otwórz folder nadrzędny + Wyłącz wyświetlanie tego programu + Otwórz folder docelowy Programy Szukaj i uruchamiaj programy z poziomu Flow Launchera - Invalid Path + Nieprawidłowa ścieżka - Customized Explorer + Spersonalizowany Eksplorator Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. - Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. + Możesz dostosować eksplorator używany do otwierania folderu nadrzędnego, wprowadzając zmienną środowiskową wybranego eksploratora. Warto użyć wiersza poleceń (CMD), aby sprawdzić, czy zmienna środowiskowa jest dostępna. + Wprowadź niestandardowe argumenty, które chcesz dodać do spersonalizowanego eksploratora. %s dla katalogu nadrzędnego, %f dla pełnej ścieżki (działa tylko dla win32). Sprawdź stronę internetową eksploratora, aby uzyskać szczegółowe informacje. Sukces - Error - Successfully disabled this program from displaying in your query - This app is not intended to be run as administrator - Unable to run {0} + Błąd + Pomyślnie wyłączono wyświetlanie tego programu w zapytaniu + Ta aplikacja nie jest przeznaczona do uruchomienia jako administrator + Nie można uruchomić {0} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/pt-br.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/pt-br.xaml index f1cb7d7a2..bab077683 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/pt-br.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/pt-br.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable Hide app path For executable files such as UWP or lnk, hide the file path from being visible + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffixes Max Depth @@ -80,7 +84,7 @@ Customized Explorer Parâmetros - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/pt-pt.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/pt-pt.xaml index 0832c2150..c7b394593 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/pt-pt.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/pt-pt.xaml @@ -30,8 +30,12 @@ Se ativa, Flow Launcher irá carregar os programas da variável de ambiente PATH Ocultar caminho da aplicação Para ficheiros executáveis, tais como UWP ou lnk, ocultar o caminho do ficheiro + Ocultar desinstaladores + Ocultar programas com nomes de desinstalador como, por exemplo, unins000.exe Pesquisar na descrição dos programas Flow irá pesquisar na descrição do programa + Ocultar aplicações duplicadas + Ocultar aplicações Win32 duplicadas que existem lista UWP Sufixos Profundidade máxima diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/ru.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/ru.xaml index ebc30e161..8cb62137e 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/ru.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/ru.xaml @@ -30,8 +30,12 @@ При включении Flow будет загружать программы из переменной среды PATH Скрыть путь к приложению Для исполняемых файлов, таких как UWP или lnk, скрыть путь к файлу от просмотра + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Поиск в описании программы Flow будет искать в описании программ + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Суффиксы Макс. глубина @@ -80,7 +84,7 @@ Настраиваемый проводник Аргументы - Вы можете настроить проводник, используемый для открытия папки контейнера, введя переменную окружения проводника, который вы хотите использовать. Будет полезно использовать командную строку для проверки доступности переменной среды. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Введите настраиваемые аргументы, которые вы хотите добавить для вашего настраиваемого проводника. %s для родительского каталога, %f для полного пути (работает только для win32). Подробности смотрите на сайте проводника. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/sk.xaml index a669d256b..ee0705b96 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/sk.xaml @@ -30,8 +30,12 @@ Ak je táto možnosť povolená, Flow načíta programy z premennej prostredia PATH Skryť cestu k aplikácii Pre spustiteľné súbory ako sú UWP alebo odkazy nezobrazovať cestu k súborom + Schovať odinštalátory + Schovať odinštalačné programy s bežnými názvami ako unins000.exe Povoliť popis programu Flow bude vyhľadávať v popise programu + Schovať duplicitné aplikácie + Schovať duplicitné programy WIn32, ktoré sú už v zozname UWP Prípony Max. hĺbka @@ -71,7 +75,7 @@ Spustiť ako správca Otvoriť umiestnenie priečinka Zakázať zobrazovanie tohto programu - Otvoriť cieľový pričinok + Otvoriť cieľový priečinok Program Vyhľadávanie programov vo Flow Launcheri diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/sr.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/sr.xaml index 6d995e1ab..a69d7e96b 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/sr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/sr.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable Hide app path For executable files such as UWP or lnk, hide the file path from being visible + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffixes Max Depth @@ -80,7 +84,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/tr.xaml index 675c9f132..d46de0592 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/tr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/tr.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable Hide app path For executable files such as UWP or lnk, hide the file path from being visible + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Uzantılar Derinlik @@ -80,7 +84,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/uk-UA.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/uk-UA.xaml index 344695d1e..8e8d55f47 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/uk-UA.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/uk-UA.xaml @@ -30,8 +30,12 @@ Якщо увімкнено, Flow буде завантажувати програми зі змінної середовища PATH Приховати шлях до програми Для виконуваних файлів, таких як UWP або lnk, приховати шлях до файлу, щоб його не було видно + Приховати деінсталятори + Приховує програми з поширеними назвами деінсталяторів, наприклад, unins000.exe Пошук в описі програми Flow буде шукати опис програми + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Суфікси Максимальна глибина @@ -80,7 +84,7 @@ Кастомізований провідник Аргументи - Ви можете налаштувати провідник, який використовується для відкриття теки контейнера, ввівши змінну середовища провідника, який ви хочете використовувати. Буде корисно скористатися командою CMD для перевірки доступності змінної середовища. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Введіть спеціальні аргументи, які ви хочете додати до вашого провідника. %s для батьківського каталогу, %f для повного шляху (працює лише для win32). Докладнішу інформацію можна знайти на веб-сайті провідника. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/vi.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/vi.xaml index 7ec467566..21a3981c5 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/vi.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/vi.xaml @@ -30,8 +30,12 @@ Khi được bật, Flow sẽ tải các chương trình từ menu bắt đầu Ẩn đường dẫn ứng dụng Đối với các tệp thực thi như UWP hoặc lnk, hãy ẩn đường dẫn tệp để không hiển thị + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Tìm kiếm trong Mô tả chương trình Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Hậu tố Độ sâu tối đa: @@ -42,10 +46,10 @@ Hãy chọn một nguồn dữ liệu Bạn có chắc chắn là muốn xóa các đặt hàng đã chọn? - Another program source with the same location already exists. + Đã tồn tại một nguồn chương trình khác có cùng vị trí. - Program Source - Edit directory and status of this program source. + Nguồn chương trình + Chỉnh sửa thư mục và trạng thái của nguồn chương trình này. Cập nhật Program Plugin will only index files with selected suffixes and .url files with selected protocols. @@ -71,7 +75,7 @@ Chạy với quyền quản trị Mở thư mục chứa Vô hiệu hóa chương trình này hiển thị - Open target folder + Mở thư mục đích Chương trình Tìm kiếm chương trình trong Flow Launcher @@ -80,7 +84,7 @@ Trình khám phá tùy chỉnh Đối số - Bạn có thể tùy chỉnh trình thám hiểm được sử dụng để mở thư mục vùng chứa bằng cách nhập Biến môi trường của trình khám phá mà bạn muốn sử dụng. Sẽ rất hữu ích khi sử dụng CMD để kiểm tra xem Biến môi trường có sẵn hay không. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Nhập các đối số tùy chỉnh mà bạn muốn thêm cho trình khám phá tùy chỉnh của mình. %s cho thư mục mẹ, %f cho đường dẫn đầy đủ (chỉ hoạt động với win32). Kiểm tra trang web của nhà thám hiểm để biết chi tiết. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/zh-cn.xaml index 324bd3057..6d5c0079f 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/zh-cn.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/zh-cn.xaml @@ -30,8 +30,12 @@ 启用时搜索 PATH 环境变量中的程序 隐藏应用路径 隐藏诸如UWP,lnk 等可执行文件的路径 + 隐藏卸载程序 + 隐藏具有常见卸载程序名称的程序,例如 unins000.exe 启用程序描述 Flow 将搜索程序描述 + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list 后缀 最大深度 @@ -71,7 +75,7 @@ 以管理员身份运行 打开文件所在文件夹 禁止显示该程序 - Open target folder + 打开目标文件夹 程序 在 Flow Launcher 中搜索程序 @@ -80,7 +84,7 @@ 自定义资源管理器 参数 - 您可以通过输入要使用的资源管理器的环境变量来自定义用于打开文件夹的资源管理器。 使用CMD来测试环境变量是否可用。 + 您可以通过输入要使用的资源管理器的环境变量来自定义用于打开容器文件夹的资源管理器。使用 CMD 测试环境变量是否可用将很有用。 输入要为自定义资源管理器添加的自定义参数。 %s代表父目录,%f代表完整路径(仅适用于win32)。 检查资源管理器的网站以获取详细信息。 diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/zh-tw.xaml index 7c72b1aa3..fd0bd427a 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/zh-tw.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/zh-tw.xaml @@ -30,8 +30,12 @@ When enabled, Flow will load programs from the PATH environment variable Hide app path For executable files such as UWP or lnk, hide the file path from being visible + Hide uninstallers + Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list 副檔名 最大深度 @@ -80,7 +84,7 @@ Customized Explorer Args - You can customized the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. + You can customize the explorer used for opening the container folder by inputing the Environmental Variable of the explorer you want to use. It will be useful to use CMD to test whether the Environmental Variable is available. Enter the customized args you want to add for your customized explorer. %s for parent directory, %f for full path (which only works for win32). Check the explorer's website for details. diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 8bf1830e3..73b4ae9e6 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.Program.Programs; using Flow.Launcher.Plugin.Program.Views; using Flow.Launcher.Plugin.Program.Views.Models; +using Flow.Launcher.Plugin.SharedCommands; using Microsoft.Extensions.Caching.Memory; using Path = System.IO.Path; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; @@ -41,9 +44,38 @@ namespace Flow.Launcher.Plugin.Program "uninst000.exe", "uninstall.exe" }; - // For cases when the uninstaller is named like "Uninstall Program Name.exe" - private const string CommonUninstallerPrefix = "uninstall"; - private const string CommonUninstallerSuffix = ".exe"; + private static readonly string[] commonUninstallerPrefixs = + { + "uninstall",//en + "卸载",//zh-cn + "卸載",//zh-tw + "видалити",//uk-UA + "удалить",//ru + "désinstaller",//fr + "アンインストール",//ja + "deïnstalleren",//nl + "odinstaluj",//pl + "afinstallere",//da + "deinstallieren",//de + "삭제",//ko + "деинсталирај",//sr + "desinstalar",//pt-pt + "desinstalar",//pt-br + "desinstalar",//es + "desinstalar",//es-419 + "disinstallare",//it + "avinstallere",//nb-NO + "odinštalovať",//sk + "kaldır",//tr + "odinstalovat",//cs + "إلغاء التثبيت",//ar + "gỡ bỏ",//vi-vn + "הסרה"//he + }; + private const string ExeUninstallerSuffix = ".exe"; + private const string InkUninstallerSuffix = ".lnk"; + + private static readonly string WindowsAppPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsApps"); static Main() { @@ -60,15 +92,35 @@ namespace Flow.Launcher.Plugin.Program var result = await cache.GetOrCreateAsync(query.Search, async entry => { var resultList = await Task.Run(() => - _win32s.Cast() - .Concat(_uwps) - .AsParallel() - .WithCancellation(token) - .Where(HideUninstallersFilter) - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, Context.API)) - .Where(r => r?.Score > 0) - .ToList()); + { + try + { + // Collect all UWP Windows app directories + var uwpsDirectories = _settings.HideDuplicatedWindowsApp ? _uwps + .Where(uwp => !string.IsNullOrEmpty(uwp.Location)) // Exclude invalid paths + .Where(uwp => uwp.Location.StartsWith(WindowsAppPath, StringComparison.OrdinalIgnoreCase)) // Keep system apps + .Select(uwp => uwp.Location.TrimEnd('\\')) // Remove trailing slash + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() : null; + + return _win32s.Cast() + .Concat(_uwps) + .AsParallel() + .WithCancellation(token) + .Where(HideUninstallersFilter) + .Where(p => HideDuplicatedWindowsAppFilter(p, uwpsDirectories)) + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, Context.API)) + .Where(r => r?.Score > 0) + .ToList(); + } + catch (OperationCanceledException) + { + Log.Debug("|Flow.Launcher.Plugin.Program.Main|Query operation cancelled"); + return emptyResults; + } + + }, token); resultList = resultList.Any() ? resultList : emptyResults; @@ -85,10 +137,50 @@ namespace Flow.Launcher.Plugin.Program { if (!_settings.HideUninstallers) return true; if (program is not Win32 win32) return true; + + // First check the executable path var fileName = Path.GetFileName(win32.ExecutablePath); - return !commonUninstallerNames.Contains(fileName, StringComparer.OrdinalIgnoreCase) && - !(fileName.StartsWith(CommonUninstallerPrefix, StringComparison.OrdinalIgnoreCase) && - fileName.EndsWith(CommonUninstallerSuffix, StringComparison.OrdinalIgnoreCase)); + // For cases when the uninstaller is named like "uninst.exe" + if (commonUninstallerNames.Contains(fileName, StringComparer.OrdinalIgnoreCase)) return false; + // For cases when the uninstaller is named like "Uninstall Program Name.exe" + foreach (var prefix in commonUninstallerPrefixs) + { + if (fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && + fileName.EndsWith(ExeUninstallerSuffix, StringComparison.OrdinalIgnoreCase)) + return false; + } + + // Second check the lnk path + if (!string.IsNullOrEmpty(win32.LnkResolvedPath)) + { + var inkFileName = Path.GetFileName(win32.FullPath); + // For cases when the uninstaller is named like "Uninstall Program Name.ink" + foreach (var prefix in commonUninstallerPrefixs) + { + if (inkFileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && + inkFileName.EndsWith(InkUninstallerSuffix, StringComparison.OrdinalIgnoreCase)) + return false; + } + } + + return true; + } + + private static bool HideDuplicatedWindowsAppFilter(IProgram program, string[] uwpsDirectories) + { + if (uwpsDirectories == null || uwpsDirectories.Length == 0) return true; + if (program is UWPApp) return true; + + var location = program.Location.TrimEnd('\\'); // Ensure trailing slash + if (string.IsNullOrEmpty(location)) + return true; // Keep if location is invalid + + if (!location.StartsWith(WindowsAppPath, StringComparison.OrdinalIgnoreCase)) + return true; // Keep if not a Windows app + + // Check if the any Win32 executable directory contains UWP Windows app location matches + return !uwpsDirectories.Any(uwpDirectory => + location.StartsWith(uwpDirectory, StringComparison.OrdinalIgnoreCase)); } public async Task InitAsync(PluginInitContext context) @@ -99,9 +191,61 @@ namespace Flow.Launcher.Plugin.Program await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", async () => { - _win32Storage = new BinaryStorage("Win32"); + FilesFolders.ValidateDirectory(Context.CurrentPluginMetadata.PluginCacheDirectoryPath); + + static void MoveFile(string sourcePath, string destinationPath) + { + if (!File.Exists(sourcePath)) + { + return; + } + + if (File.Exists(destinationPath)) + { + try + { + File.Delete(sourcePath); + } + catch (Exception) + { + // Ignore, we will handle next time we start the plugin + } + return; + } + + var destinationDirectory = Path.GetDirectoryName(destinationPath); + if (!Directory.Exists(destinationDirectory) && (!string.IsNullOrEmpty(destinationDirectory))) + { + try + { + Directory.CreateDirectory(destinationDirectory); + } + catch (Exception) + { + // Ignore, we will handle next time we start the plugin + } + } + try + { + File.Move(sourcePath, destinationPath); + } + catch (Exception) + { + // Ignore, we will handle next time we start the plugin + } + } + + // Move old cache files to the new cache directory + var oldWin32CacheFile = Path.Combine(DataLocation.CacheDirectory, $"Win32.cache"); + var newWin32CacheFile = Path.Combine(Context.CurrentPluginMetadata.PluginCacheDirectoryPath, $"Win32.cache"); + MoveFile(oldWin32CacheFile, newWin32CacheFile); + var oldUWPCacheFile = Path.Combine(DataLocation.CacheDirectory, $"UWP.cache"); + var newUWPCacheFile = Path.Combine(Context.CurrentPluginMetadata.PluginCacheDirectoryPath, $"UWP.cache"); + MoveFile(oldUWPCacheFile, newUWPCacheFile); + + _win32Storage = new BinaryStorage("Win32", Context.CurrentPluginMetadata.PluginCacheDirectoryPath); _win32s = await _win32Storage.TryLoadAsync(Array.Empty()); - _uwpStorage = new BinaryStorage("UWP"); + _uwpStorage = new BinaryStorage("UWP", Context.CurrentPluginMetadata.PluginCacheDirectoryPath); _uwps = await _uwpStorage.TryLoadAsync(Array.Empty()); }); Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); @@ -203,6 +347,7 @@ namespace Flow.Launcher.Plugin.Program Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), Context.API.GetTranslation( "flowlauncher_plugin_program_disable_dlgtitle_success_message")); + Context.API.ReQuery(); return false; }, IcoPath = "Images/disable.png", diff --git a/Plugins/Flow.Launcher.Plugin.Program/NativeMethods.txt b/Plugins/Flow.Launcher.Plugin.Program/NativeMethods.txt new file mode 100644 index 000000000..ecd547dff --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/NativeMethods.txt @@ -0,0 +1,10 @@ +SHGetLocalizedName +LoadString +LoadLibraryEx +FreeLibrary +ExpandEnvironmentStrings +S_OK +SLGP_FLAGS +WIN32_FIND_DATAW +SLR_FLAGS +IShellLinkW \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml.cs index 31565c8b0..55d44bdc2 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml.cs @@ -39,14 +39,14 @@ namespace Flow.Launcher.Plugin.Program if (suffixes.Length == 0 && UseCustomSuffixes) { string warning = context.API.GetTranslation("flowlauncher_plugin_program_suffixes_cannot_empty"); - MessageBox.Show(warning); + context.API.ShowMsgBox(warning); return; } if (protocols.Length == 0 && UseCustomProtocols) { string warning = context.API.GetTranslation("flowlauncher_plugin_protocols_cannot_empty"); - MessageBox.Show(warning); + context.API.ShowMsgBox(warning); return; } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs index 78c66d604..a77b2ace8 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs @@ -1,97 +1,17 @@ using System; -using System.Text; using System.Runtime.InteropServices; -using Accessibility; using System.Runtime.InteropServices.ComTypes; using Flow.Launcher.Plugin.Program.Logger; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using Windows.Win32.Storage.FileSystem; namespace Flow.Launcher.Plugin.Program.Programs { class ShellLinkHelper { - [Flags()] - public enum SLGP_FLAGS - { - SLGP_SHORTPATH = 0x1, - SLGP_UNCPRIORITY = 0x2, - SLGP_RAWPATH = 0x4 - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - public struct WIN32_FIND_DATAW - { - public uint dwFileAttributes; - public long ftCreationTime; - public long ftLastAccessTime; - public long ftLastWriteTime; - public uint nFileSizeHigh; - public uint nFileSizeLow; - public uint dwReserved0; - public uint dwReserved1; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] - public string cFileName; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] - public string cAlternateFileName; - } - - [Flags()] - public enum SLR_FLAGS - { - SLR_NO_UI = 0x1, - SLR_ANY_MATCH = 0x2, - SLR_UPDATE = 0x4, - SLR_NOUPDATE = 0x8, - SLR_NOSEARCH = 0x10, - SLR_NOTRACK = 0x20, - SLR_NOLINKINFO = 0x40, - SLR_INVOKE_MSI = 0x80 - } - - + // Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW - /// The IShellLink interface allows Shell links to be created, modified, and resolved - [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")] - interface IShellLinkW - { - /// Retrieves the path and file name of a Shell link object - void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, ref WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags); - /// Retrieves the list of item identifiers for a Shell link object - void GetIDList(out IntPtr ppidl); - /// Sets the pointer to an item identifier list (PIDL) for a Shell link object. - void SetIDList(IntPtr pidl); - /// Retrieves the description string for a Shell link object - void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); - /// Sets the description for a Shell link object. The description can be any application-defined string - void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); - /// Retrieves the name of the working directory for a Shell link object - void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); - /// Sets the name of the working directory for a Shell link object - void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); - /// Retrieves the command-line arguments associated with a Shell link object - void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); - /// Sets the command-line arguments for a Shell link object - void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); - /// Retrieves the hot key for a Shell link object - void GetHotkey(out short pwHotkey); - /// Sets a hot key for a Shell link object - void SetHotkey(short wHotkey); - /// Retrieves the show command for a Shell link object - void GetShowCmd(out int piShowCmd); - /// Sets the show command for a Shell link object. The show command sets the initial show state of the window. - void SetShowCmd(int iShowCmd); - /// Retrieves the location (path and index) of the icon for a Shell link object - void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, - int cchIconPath, out int piIcon); - /// Sets the location (path and index) of the icon for a Shell link object - void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); - /// Sets the relative path to the Shell link object - void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); - /// Attempts to find the target of a Shell link, even if it has been moved or renamed - void Resolve(ref Accessibility._RemotableHandle hwnd, SLR_FLAGS fFlags); - /// Sets the path and file name of a Shell link object - void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); - } - [ComImport(), Guid("00021401-0000-0000-C000-000000000046")] public class ShellLink { @@ -102,29 +22,43 @@ namespace Flow.Launcher.Plugin.Program.Programs public string arguments = string.Empty; // Retrieve the target path using Shell Link - public string retrieveTargetPath(string path) + public unsafe string retrieveTargetPath(string path) { var link = new ShellLink(); const int STGM_READ = 0; ((IPersistFile)link).Load(path, STGM_READ); - var hwnd = new _RemotableHandle(); - ((IShellLinkW)link).Resolve(ref hwnd, 0); + var hwnd = new HWND(IntPtr.Zero); + ((IShellLinkW)link).Resolve(hwnd, 0); const int MAX_PATH = 260; - StringBuilder buffer = new StringBuilder(MAX_PATH); + Span buffer = stackalloc char[MAX_PATH]; var data = new WIN32_FIND_DATAW(); - ((IShellLinkW)link).GetPath(buffer, buffer.Capacity, ref data, SLGP_FLAGS.SLGP_SHORTPATH); - var target = buffer.ToString(); + var target = string.Empty; + try + { + fixed (char* bufferPtr = buffer) + { + ((IShellLinkW)link).GetPath((PWSTR)bufferPtr, MAX_PATH, &data, (uint)SLGP_FLAGS.SLGP_SHORTPATH); + target = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString(); + } + } + catch (COMException e) + { + ProgramLogger.LogException($"|IShellLinkW|retrieveTargetPath|{path}" + + "|Error occurred while getting program arguments", e); + } // To set the app description - if (!String.IsNullOrEmpty(target)) + if (!string.IsNullOrEmpty(target)) { try { - buffer = new StringBuilder(MAX_PATH); - ((IShellLinkW)link).GetDescription(buffer, MAX_PATH); - description = buffer.ToString(); + fixed (char* bufferPtr = buffer) + { + ((IShellLinkW)link).GetDescription(bufferPtr, MAX_PATH); + description = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString(); + } } catch (COMException e) { @@ -134,15 +68,17 @@ namespace Flow.Launcher.Plugin.Program.Programs e); } - buffer.Clear(); - ((IShellLinkW)link).GetArguments(buffer, MAX_PATH); - arguments = buffer.ToString(); + fixed (char* bufferPtr = buffer) + { + ((IShellLinkW)link).GetArguments(bufferPtr, MAX_PATH); + arguments = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString(); + } } - + // To release unmanaged memory Marshal.ReleaseComObject(link); return target; - } + } } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs index 4f344d89e..fac3ab181 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLocalization.cs @@ -1,8 +1,9 @@ using System; using System.IO; using System.Runtime.InteropServices; -using System.Text; - +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.LibraryLoader; namespace Flow.Launcher.Plugin.Program.Programs { @@ -13,51 +14,41 @@ namespace Flow.Launcher.Plugin.Program.Programs /// public static class ShellLocalization { - internal const uint DONTRESOLVEDLLREFERENCES = 0x00000001; - internal const uint LOADLIBRARYASDATAFILE = 0x00000002; - - [DllImport("shell32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] - internal static extern int SHGetLocalizedName(string pszPath, StringBuilder pszResModule, ref int cch, out int pidsRes); - - [DllImport("user32.dll", EntryPoint = "LoadStringW", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] - internal static extern int LoadString(IntPtr hModule, int resourceID, StringBuilder resourceValue, int len); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, EntryPoint = "LoadLibraryExW")] - internal static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern int FreeLibrary(IntPtr hModule); - - [DllImport("kernel32.dll", EntryPoint = "ExpandEnvironmentStringsW", CharSet = CharSet.Unicode, ExactSpelling = true)] - internal static extern uint ExpandEnvironmentStrings(string lpSrc, StringBuilder lpDst, int nSize); - /// /// Returns the localized name of a shell item. /// /// Path to the shell item (e. g. shortcut 'File Explorer.lnk'). /// The localized name as string or . - public static string GetLocalizedName(string path) + public static unsafe string GetLocalizedName(string path) { - StringBuilder resourcePath = new StringBuilder(1024); - StringBuilder localizedName = new StringBuilder(1024); - int len, id; - len = resourcePath.Capacity; + const int capacity = 1024; + Span buffer = new char[capacity]; // If there is no resource to localize a file name the method returns a non zero value. - if (SHGetLocalizedName(path, resourcePath, ref len, out id) == 0) + fixed (char* bufferPtr = buffer) { - _ = ExpandEnvironmentStrings(resourcePath.ToString(), resourcePath, resourcePath.Capacity); - IntPtr hMod = LoadLibraryEx(resourcePath.ToString(), IntPtr.Zero, DONTRESOLVEDLLREFERENCES | LOADLIBRARYASDATAFILE); - if (hMod != IntPtr.Zero) + var result = PInvoke.SHGetLocalizedName(path, bufferPtr, capacity, out var id); + if (result != HRESULT.S_OK) { - if (LoadString(hMod, id, localizedName, localizedName.Capacity) != 0) - { - string lString = localizedName.ToString(); - _ = FreeLibrary(hMod); - return lString; - } + return string.Empty; + } - _ = FreeLibrary(hMod); + var resourcePathStr = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString(); + _ = PInvoke.ExpandEnvironmentStrings(resourcePathStr, bufferPtr, capacity); + using var handle = PInvoke.LoadLibraryEx(resourcePathStr, + LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES | LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_DATAFILE); + if (handle.IsInvalid) + { + return string.Empty; + } + + // not sure about the behavior of Pinvoke.LoadString, so we clear the buffer before using it (so it must be a null-terminated string) + buffer.Clear(); + + if (PInvoke.LoadString(handle, (uint)id, bufferPtr, capacity) != 0) + { + var lString = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString(); + return lString; } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs index fb24f64d7..b2aad63b3 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs @@ -121,6 +121,7 @@ namespace Flow.Launcher.Plugin.Program public bool EnableRegistrySource { get; set; } = true; public bool EnablePathSource { get; set; } = false; public bool EnableUWP { get; set; } = true; + public bool HideDuplicatedWindowsApp { get; set; } = false; internal const char SuffixSeparator = ';'; } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml index e5ca6967e..5c0ba8d0b 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml @@ -8,123 +8,111 @@ DataContext="{Binding RelativeSource={RelativeSource Self}}" mc:Ignorable="d"> - + - + - + + ToolTip="{DynamicResource flowlauncher_plugin_program_index_uwp_tooltip}" + Visibility="{Binding ShowUWPCheckbox, Converter={StaticResource BooleanToVisibilityConverter}}" /> - - - + + + - + + + + + + + + + + + + + + + + + + + + +