diff --git a/.cm/gitstream.cm b/.cm/gitstream.cm new file mode 100644 index 00000000000..dda0822fcf7 --- /dev/null +++ b/.cm/gitstream.cm @@ -0,0 +1,77 @@ +# -*- 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 + + +automations: + # Add a label that indicates how many minutes it will take to review the PR. + estimated_time_to_review: + 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/allow.txt b/.github/actions/spelling/allow.txt index 00cc67ea00f..a36a6af3efa 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -3,3 +3,6 @@ https ssh ubuntu runcount +Firefox +Português +Português (Brasil) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index c4b1ee8494d..6ba41e410ea 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -74,6 +74,7 @@ WCA_ACCENT_POLICY HGlobal dopusrt firefox +Firefox msedge svgc ime @@ -97,6 +98,8 @@ Português Português (Brasil) Italiano Slovenský +quicklook +Tiếng Việt Droplex Preinstalled errormetadatafile diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml new file mode 100644 index 00000000000..5556c5ea8ac --- /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/gitstream.yml b/.github/workflows/gitstream.yml new file mode 100644 index 00000000000..0916572df26 --- /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 00000000000..af6daff02b7 --- /dev/null +++ b/.github/workflows/pr_assignee.yml @@ -0,0 +1,19 @@ +name: Assign PR to creator + +# Due to GitHub token limitation, only able to assign org members not authors from forks. +# https://github.com/thomaseizinger/assign-pr-creator-action/issues/3 + +on: + pull_request: + types: [opened] + branches-ignore: + - l10n_dev + +jobs: + automation: + runs-on: ubuntu-latest + steps: + - name: Assign PR to creator + uses: thomaseizinger/assign-pr-creator-action@v1.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr_milestone.yml b/.github/workflows/pr_milestone.yml new file mode 100644 index 00000000000..b343a39cc8f --- /dev/null +++ b/.github/workflows/pr_milestone.yml @@ -0,0 +1,19 @@ +name: Set Milestone + +# Assigns the earliest created milestone that matches the below glob pattern. + +on: + pull_request: + types: [opened] + +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/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 40eb1be3eae..30e812c6f05 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Windows.Forms; +using Flow.Launcher.Core.Resource; namespace Flow.Launcher.Core.ExternalPlugins.Environments { @@ -50,14 +51,15 @@ internal IEnumerable Setup() 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( + InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"), + Language, + EnvName, + Environment.NewLine + ); + if (MessageBox.Show(noRuntimeMessage, string.Empty, MessageBoxButtons.YesNo) == DialogResult.No) { - var msg = $"Please select the {EnvName} executable"; + var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName); string selectedFile; selectedFile = GetFileFromDialog(msg, FileDialogFilter); @@ -80,8 +82,7 @@ internal IEnumerable Setup() } else { - MessageBox.Show( - $"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom)."); + MessageBox.Show(string.Format(InternationalizationManager.Instance.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"); diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs index 64c4cd627d2..79d6d7605e3 100644 --- a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs +++ b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Flow.Launcher.Core.ExternalPlugins { @@ -13,9 +13,11 @@ public record UserPlugin 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 18101ccf04e..940ce7a849a 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/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index 46c72624a97..5a6633525e7 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -139,11 +139,20 @@ public virtual Task ReloadDataAsync() return Task.CompletedTask; } - public virtual ValueTask DisposeAsync() + public virtual async ValueTask DisposeAsync() { - RPC?.Dispose(); - ErrorStream?.Dispose(); - return ValueTask.CompletedTask; + try + { + await RPC.InvokeAsync("close"); + } + catch (RemoteMethodNotFoundException e) + { + } + finally + { + RPC?.Dispose(); + ErrorStream?.Dispose(); + } } } } diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 7f4d4ea35cf..91cb36a0e3c 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -13,6 +13,7 @@ using ISavable = Flow.Launcher.Plugin.ISavable; using Flow.Launcher.Plugin.SharedCommands; using System.Text.Json; +using Flow.Launcher.Core.Resource; namespace Flow.Launcher.Core.Plugin { @@ -51,7 +52,7 @@ private static void DeletePythonBinding() } /// - /// Save json and ISavable + /// Save json and ISavable /// public static void Save() { @@ -90,6 +91,48 @@ public static async Task ReloadDataAsync() }).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 @@ -160,12 +203,21 @@ public static async Task InitializePluginsAsync(IPublicAPI api) } } + InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface()); + InternationalizationManager.Instance.ChangeLanguage(InternationalizationManager.Instance.Settings.Language); + 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( + InternationalizationManager.Instance.GetTranslation("failedToInitializePluginsTitle"), + string.Format( + InternationalizationManager.Instance.GetTranslation("failedToInitializePluginsMessage"), + failed + ), + "", + false + ); } } @@ -173,11 +225,11 @@ public static ICollection ValidPluginsForQuery(Query query) { if (query is null) return Array.Empty(); - + if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword)) return GlobalPlugins; - - + + var plugin = NonGlobalPlugins[query.ActionKeyword]; return new List { @@ -237,8 +289,8 @@ public static void UpdatePluginMetadata(List results, PluginMetadata met 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 +308,8 @@ public static PluginPair GetPluginForId(string id) 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) @@ -380,7 +433,8 @@ public static bool PluginModified(string uuid) /// - /// Update a plugin to new version, from a zip file. Will Delete zip after updating. + /// 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) { @@ -390,11 +444,11 @@ public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVe } /// - /// Install a plugin. Will Delete zip after updating. + /// 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, true); + InstallPlugin(plugin, zipFilePath, checkModified: true); } /// @@ -420,7 +474,9 @@ internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool c // Unzip plugin files to temp folder var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath); - File.Delete(zipFilePath); + + if(!plugin.IsFromLocalInstallPath) + File.Delete(zipFilePath); var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath); diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs index a14baf271b7..bae2631573d 100644 --- a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -82,10 +82,10 @@ public override async Task ReloadDataAsync() 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/Resource/AvailableLanguages.cs b/Flow.Launcher.Core/Resource/AvailableLanguages.cs index b6d394d1184..46c1ecb54f1 100644 --- a/Flow.Launcher.Core/Resource/AvailableLanguages.cs +++ b/Flow.Launcher.Core/Resource/AvailableLanguages.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Flow.Launcher.Core.Resource { @@ -27,6 +27,8 @@ internal static class AvailableLanguages public static Language Turkish = new Language("tr", "Türkçe"); 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 List GetAvailableLanguages() { @@ -54,7 +56,8 @@ public static List GetAvailableLanguages() Slovak, Turkish, Czech, - Arabic + Arabic, + Vietnamese }; return languages; } diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index acc693ed527..f6f35589d8d 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -25,10 +25,6 @@ public class Internationalization public Internationalization() { - AddPluginLanguageDirectories(); - LoadDefaultLanguage(); - // we don't want to load /Languages/en.xaml twice - // so add flowlauncher language directory after load plugin language files AddFlowLauncherLanguageDirectory(); } @@ -40,9 +36,9 @@ private void AddFlowLauncherLanguageDirectory() } - private void AddPluginLanguageDirectories() + internal void AddPluginLanguageDirectories(IEnumerable plugins) { - foreach (var plugin in PluginManager.GetPluginsForInterface()) + foreach (var plugin in plugins) { var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location; var dir = Path.GetDirectoryName(location); @@ -56,10 +52,15 @@ private void AddPluginLanguageDirectories() 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(); } @@ -96,13 +97,10 @@ public void ChangeLanguage(Language language) { LoadLanguage(language); } - // Culture of this thread - // Use CreateSpecificCulture to preserve possible user-override settings in Windows + // 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; - // App domain - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode); - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.DefaultThreadCurrentCulture; // Raise event after culture is set Settings.Language = language.LanguageCode; @@ -143,11 +141,14 @@ private void RemoveOldLanguageFiles() 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) @@ -193,7 +194,7 @@ private void UpdatePluginMetadataTranslations() { p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); - pluginI18N.OnCultureInfoChanged(CultureInfo.DefaultThreadCurrentCulture); + pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); } catch (Exception e) { diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 96338cf6a1a..c3a3e9891e7 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Xml; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; @@ -17,6 +18,10 @@ namespace Flow.Launcher.Core.Resource { public class Theme { + 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(); @@ -79,14 +84,14 @@ public bool ChangeTheme(string theme) { 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) { @@ -148,7 +153,7 @@ private ResourceDictionary GetThemeResourceDictionary(string theme) public ResourceDictionary GetResourceDictionary(string theme) { var dict = GetThemeResourceDictionary(theme); - + if (dict["QueryBoxStyle"] is Style queryBoxStyle && dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) { @@ -176,8 +181,6 @@ public ResourceDictionary GetResourceDictionary(string theme) } if (dict["ItemTitleStyle"] is Style resultItemStyle && - dict["ItemSubTitleStyle"] is Style resultSubItemStyle && - dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle && dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) @@ -189,9 +192,25 @@ public ResourceDictionary GetResourceDictionary(string theme) Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; Array.ForEach( - new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o + new[] { resultItemStyle, resultItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); } + + if ( + 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[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; + Array.ForEach( + 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; @@ -205,17 +224,53 @@ private ResourceDictionary GetCurrentResourceDictionary( ) return GetResourceDictionary(Settings.Theme); } - public List LoadAvailableThemes() + public List LoadAvailableThemes() { - List themes = new List(); + List themes = new List(); foreach (var themeDirectory in _themeDirectories) { - themes.AddRange( - Directory.GetFiles(themeDirectory) - .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) - .ToList()); + var filePaths = Directory + .GetFiles(themeDirectory) + .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) + .Select(GetThemeDataFromPath); + themes.AddRange(filePaths); } - return themes.OrderBy(o => o).ToList(); + + return themes.OrderBy(o => o.Name).ToList(); + } + + private ThemeData GetThemeDataFromPath(string path) + { + 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) + { + if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim(); + } + else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase)) + { + isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim()); + } + else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase)) + { + hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim()); + } + } + + return new ThemeData(extensionlessName, name, isDark, hasBlur); } private string GetThemePath(string themeName) @@ -393,5 +448,7 @@ private void SetWindowAccent(Window w, AccentState state) Marshal.FreeHGlobal(accentPtr); } #endregion + + public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null); } } diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index 7a95b52d5e0..8a95ee79f77 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -7,6 +7,7 @@ namespace Flow.Launcher.Infrastructure public static class Constant { public const string FlowLauncher = "Flow.Launcher"; + public const string FlowLauncherFullName = "Flow Launcher"; public const string Plugins = "Plugins"; public const string PluginMetadataFileName = "plugin.json"; diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 8310917322a..3a8e07b4a0f 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -49,16 +49,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - diff --git a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs index 93705943651..25bc75a56c1 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs @@ -6,7 +6,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey { - public class HotkeyModel + public record struct HotkeyModel { public bool Alt { get; set; } public bool Shift { get; set; } @@ -17,8 +17,7 @@ public class HotkeyModel private static readonly Dictionary specialSymbolDictionary = new Dictionary { - {Key.Space, "Space"}, - {Key.Oem3, "~"} + { Key.Space, "Space" }, { Key.Oem3, "~" } }; public ModifierKeys ModifierKeys @@ -30,18 +29,22 @@ public ModifierKeys ModifierKeys { modifierKeys |= ModifierKeys.Alt; } + if (Shift) { modifierKeys |= ModifierKeys.Shift; } + if (Win) { modifierKeys |= ModifierKeys.Windows; } + if (Ctrl) { modifierKeys |= ModifierKeys.Control; } + return modifierKeys; } } @@ -66,31 +69,37 @@ private void Parse(string hotkeyString) { return; } + List keys = hotkeyString.Replace(" ", "").Split('+').ToList(); if (keys.Contains("Alt")) { Alt = true; keys.Remove("Alt"); } + if (keys.Contains("Shift")) { Shift = true; keys.Remove("Shift"); } + if (keys.Contains("Win")) { Win = true; keys.Remove("Win"); } + if (keys.Contains("Ctrl")) { Ctrl = true; keys.Remove("Ctrl"); } + if (keys.Count == 1) { string charKey = keys[0]; - KeyValuePair? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey); + KeyValuePair? specialSymbolPair = + specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey); if (specialSymbolPair.Value.Value != null) { CharKey = specialSymbolPair.Value.Key; @@ -103,7 +112,6 @@ private void Parse(string hotkeyString) } catch (ArgumentException) { - } } } @@ -111,33 +119,39 @@ private void Parse(string hotkeyString) public override string ToString() { - List keys = new List(); - if (Ctrl) + return string.Join(" + ", EnumerateDisplayKeys()); + } + + public IEnumerable EnumerateDisplayKeys() + { + if (Ctrl && CharKey is not (Key.LeftCtrl or Key.RightCtrl)) { - keys.Add("Ctrl"); + yield return "Ctrl"; } - if (Alt) + + if (Alt && CharKey is not (Key.LeftAlt or Key.RightAlt)) { - keys.Add("Alt"); + yield return "Alt"; } - if (Shift) + + if (Shift && CharKey is not (Key.LeftShift or Key.RightShift)) { - keys.Add("Shift"); + yield return "Shift"; } - if (Win) + + if (Win && CharKey is not (Key.LWin or Key.RWin)) { - keys.Add("Win"); + yield return "Win"; } if (CharKey != Key.None) { - keys.Add(specialSymbolDictionary.ContainsKey(CharKey) - ? specialSymbolDictionary[CharKey] - : CharKey.ToString()); + yield return specialSymbolDictionary.TryGetValue(CharKey, out var value) + ? value + : CharKey.ToString(); } - return string.Join(" + ", keys); } - + /// /// Validate hotkey /// @@ -164,11 +178,13 @@ public bool Validate(bool validateKeyGestrue = false) { KeyGesture keyGesture = new KeyGesture(CharKey, ModifierKeys); } - catch (System.Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException) + catch (System.Exception e) when + (e is NotSupportedException || e is InvalidEnumArgumentException) { return false; } } + if (ModifierKeys == ModifierKeys.None) { return !IsPrintableCharacter(CharKey); @@ -206,18 +222,6 @@ private static bool IsPrintableCharacter(Key key) key == Key.Decimal; } - public override bool Equals(object obj) - { - if (obj is HotkeyModel other) - { - return ModifierKeys == other.ModifierKeys && CharKey == other.CharKey; - } - else - { - return false; - } - } - public override int GetHashCode() { return HashCode.Combine(ModifierKeys, CharKey); diff --git a/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs b/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs new file mode 100644 index 00000000000..448a70d191c --- /dev/null +++ b/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Flow.Launcher.Infrastructure.Hotkey; + +/// +/// Interface that you should implement in your settings class to be able to pass it to +/// Flow.Launcher.HotkeyControlDialog. It allows the dialog to display the hotkeys that have already been +/// registered, and optionally provide a way to unregister them. +/// +public interface IHotkeySettings +{ + /// + /// A list of hotkeys that have already been registered. The dialog will display these hotkeys and provide a way to + /// unregister them. + /// + public List RegisteredHotkeys { get; } +} diff --git a/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs b/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs new file mode 100644 index 00000000000..b6a10fc3075 --- /dev/null +++ b/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs @@ -0,0 +1,119 @@ +using System; + +namespace Flow.Launcher.Infrastructure.Hotkey; + +#nullable enable + +/// +/// Represents a hotkey that has been registered. Used in Flow.Launcher.HotkeyControlDialog via +/// and to display errors if user tries to register a hotkey +/// that has already been registered, and optionally provides a way to unregister the hotkey. +/// +public record RegisteredHotkeyData +{ + /// + /// representation of this hotkey. + /// + public HotkeyModel Hotkey { get; } + + /// + /// String key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + public string DescriptionResourceKey { get; } + + /// + /// Array of values that will replace {0}, {1}, {2}, etc. in the localized string found via + /// . + /// + public object?[] DescriptionFormatVariables { get; } = Array.Empty(); + + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that + /// this hotkey can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public Action? RemoveHotkey { get; } + + /// + /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in + /// descriptionResourceKey doesn't need any arguments for string.Format. If it does, + /// use one of the other constructors. + /// + /// + /// The hotkey this class will represent. + /// Example values: F1, Ctrl+Shift+Enter + /// + /// + /// The key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey + /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action? removeHotkey = null) + { + Hotkey = new HotkeyModel(hotkey); + DescriptionResourceKey = descriptionResourceKey; + RemoveHotkey = removeHotkey; + } + + /// + /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in + /// descriptionResourceKey needs exactly one argument for string.Format. + /// + /// + /// The hotkey this class will represent. + /// Example values: F1, Ctrl+Shift+Enter + /// + /// + /// The key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + /// + /// The value that will replace {0} in the localized string found via description. + /// + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey + /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public RegisteredHotkeyData( + string hotkey, string descriptionResourceKey, object? descriptionFormatVariable, Action? removeHotkey = null + ) + { + Hotkey = new HotkeyModel(hotkey); + DescriptionResourceKey = descriptionResourceKey; + DescriptionFormatVariables = new[] { descriptionFormatVariable }; + RemoveHotkey = removeHotkey; + } + + /// + /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in + /// needs multiple arguments for string.Format. + /// + /// + /// The hotkey this class will represent. + /// Example values: F1, Ctrl+Shift+Enter + /// + /// + /// The key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + /// + /// Array of values that will replace {0}, {1}, {2}, etc. + /// in the localized string found via description. + /// + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey + /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public RegisteredHotkeyData( + string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables, Action? removeHotkey = null + ) + { + Hotkey = new HotkeyModel(hotkey); + DescriptionResourceKey = descriptionResourceKey; + DescriptionFormatVariables = descriptionFormatVariables; + RemoveHotkey = removeHotkey; + } +} diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index 55545b9a732..ddbab4ef0b1 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -3,33 +3,23 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using System.Windows.Media; -using FastCache; -using FastCache.Services; +using BitFaster.Caching.Lfu; namespace Flow.Launcher.Infrastructure.Image { - public class ImageUsage - { - public int usage; - public ImageSource imageSource; - - public ImageUsage(int usage, ImageSource image) - { - this.usage = usage; - imageSource = image; - } - } - public class ImageCache { private const int MaxCached = 150; - public void Initialize(Dictionary<(string, bool), int> usage) + private ConcurrentLfu<(string, bool), ImageSource> CacheManager { get; set; } = new(MaxCached); + + public void Initialize(IEnumerable<(string, bool)> usage) { - foreach (var key in usage.Keys) + foreach (var key in usage) { - Cached.Save(key, new ImageUsage(usage[key], null), TimeSpan.MaxValue, MaxCached); + CacheManager.AddOrUpdate(key, null); } } @@ -37,48 +27,42 @@ public void Initialize(Dictionary<(string, bool), int> usage) { get { - if (!Cached.TryGet((path, isFullImage), out var value)) - { - return null; - } - - value.Value.usage++; - return value.Value.imageSource; + return CacheManager.TryGet((path, isFullImage), out var value) ? value : null; } set { - if (Cached.TryGet((path, isFullImage), out var cached)) - { - cached.Value.imageSource = value; - cached.Value.usage++; - } - - Cached.Save((path, isFullImage), new ImageUsage(0, value), TimeSpan.MaxValue, - MaxCached); + CacheManager.AddOrUpdate((path, isFullImage), value); } } + public async ValueTask GetOrAddAsync(string key, + Func<(string, bool), Task> valueFactory, + bool isFullImage = false) + { + return await CacheManager.GetOrAddAsync((key, isFullImage), valueFactory); + } + public bool ContainsKey(string key, bool isFullImage) { - return Cached.TryGet((key, isFullImage), out _); + return CacheManager.TryGet((key, isFullImage), out _); } public bool TryGetValue(string key, bool isFullImage, out ImageSource image) { - if (Cached.TryGet((key, isFullImage), out var value)) + if (CacheManager.TryGet((key, isFullImage), out var value)) { - image = value.Value.imageSource; - value.Value.usage++; + image = value; return image != null; } + image = null; return false; } public int CacheSize() { - return CacheManager.TotalCount<(string, bool), ImageUsage>(); + return CacheManager.Count; } /// @@ -86,14 +70,14 @@ public int CacheSize() /// public int UniqueImagesInCache() { - return CacheManager.EnumerateEntries<(string, bool), ImageUsage>().Select(x => x.Value.imageSource) + return CacheManager.Select(x => x.Value) .Distinct() .Count(); } - public IEnumerable> EnumerateEntries() + public IEnumerable> EnumerateEntries() { - return CacheManager.EnumerateEntries<(string, bool), ImageUsage>(); + return CacheManager; } } } diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 75c2a4ec989..612f495be64 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -5,6 +5,7 @@ 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; @@ -17,7 +18,7 @@ public static class ImageLoader { private static readonly ImageCache ImageCache = new(); private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1); - private static BinaryStorage> _storage; + private static BinaryStorage> _storage; private static readonly ConcurrentDictionary GuidToKey = new(); private static IImageHashGenerator _hashGenerator; private static readonly bool EnableImageHash = true; @@ -31,12 +32,12 @@ public static class ImageLoader public static async Task InitializeAsync() { - _storage = new BinaryStorage>("Image"); + _storage = new BinaryStorage>("Image"); _hashGenerator = new ImageHashGenerator(); var usage = await LoadStorageToConcurrentDictionaryAsync(); - ImageCache.Initialize(usage.ToDictionary(x => x.Key, x => x.Value)); + ImageCache.Initialize(usage); foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon }) { @@ -49,7 +50,7 @@ public static async Task InitializeAsync() { await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () => { - foreach (var ((path, isFullImage), _) in usage) + foreach (var (path, isFullImage) in usage) { await LoadAsync(path, isFullImage); } @@ -66,9 +67,8 @@ public static async Task Save() try { await _storage.SaveAsync(ImageCache.EnumerateEntries() - .ToDictionary( - x => x.Key, - x => x.Value.usage)); + .Select(x => x.Key) + .ToList()); } finally { @@ -76,14 +76,12 @@ await _storage.SaveAsync(ImageCache.EnumerateEntries() } } - private static async Task> LoadStorageToConcurrentDictionaryAsync() + private static async Task> LoadStorageToConcurrentDictionaryAsync() { await storageLock.WaitAsync(); try { - var loaded = await _storage.TryLoadAsync(new Dictionary<(string, bool), int>()); - - return new ConcurrentDictionary<(string, bool), int>(loaded); + return await _storage.TryLoadAsync(new List<(string, bool)>()); } finally { diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index a679643fd92..2a439b8cce5 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -67,7 +67,7 @@ private async ValueTask DeserializeAsync(Stream stream, T defaultData) } catch (System.Exception e) { - Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); + // Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); return defaultData; } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 274f88dc674..0c7de10fd78 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -4,13 +4,14 @@ using System.Drawing; using System.Text.Json.Serialization; using System.Windows; +using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.ViewModel; namespace Flow.Launcher.Infrastructure.UserSettings { - public class Settings : BaseModel + public class Settings : BaseModel, IHotkeySettings { private string language = "en"; private string _theme = Constant.DefaultTheme; @@ -20,6 +21,18 @@ public class Settings : BaseModel public bool ShowOpenResultHotkey { get; set; } = true; public double WindowSize { get; set; } = 580; public string PreviewHotkey { get; set; } = $"F1"; + public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab"; + public string AutoCompleteHotkey2 { get; set; } = $""; + public string SelectNextItemHotkey { get; set; } = $"Tab"; + public string SelectNextItemHotkey2 { get; set; } = $""; + public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab"; + public string SelectPrevItemHotkey2 { get; set; } = $""; + public string SelectNextPageHotkey { get; set; } = $"PageUp"; + public string SelectPrevPageHotkey { get; set; } = $"PageDown"; + public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O"; + public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; + public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; + public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; public string Language { @@ -42,7 +55,14 @@ public string Theme OnPropertyChanged(nameof(MaxResultsToShow)); } } - public bool UseDropShadowEffect { get; set; } = false; + public bool UseDropShadowEffect { get; set; } = true; + + /* 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 string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name; public string QueryBoxFontStyle { get; set; } public string QueryBoxFontWeight { get; set; } @@ -51,6 +71,10 @@ public string Theme public string ResultFontStyle { get; set; } public string ResultFontWeight { get; set; } public string ResultFontStretch { get; set; } + public string ResultSubFont { get; set; } = FontFamily.GenericSansSerif.Name; + public string ResultSubFontStyle { get; set; } + public string ResultSubFontWeight { get; set; } + public string ResultSubFontStretch { get; set; } public bool UseGlyphIcons { get; set; } = true; public bool UseAnimation { get; set; } = true; public bool UseSound { get; set; } = true; @@ -64,8 +88,8 @@ public string Theme public double SettingWindowWidth { get; set; } = 1000; public double SettingWindowHeight { get; set; } = 700; - public double SettingWindowTop { get; set; } - public double SettingWindowLeft { get; set; } + public double? SettingWindowTop { get; set; } = null; + public double? SettingWindowLeft { get; set; } = null; public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal; public int CustomExplorerIndex { get; set; } = 0; @@ -161,35 +185,21 @@ public CustomBrowserViewModel CustomBrowser /// when false Alphabet static service will always return empty results /// public bool ShouldUsePinyin { get; set; } = false; + public bool AlwaysPreview { get; set; } = false; + public bool AlwaysStartEn { get; set; } = false; + private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular; [JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))] - public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular; - - [JsonIgnore] - public string QuerySearchPrecisionString + public SearchPrecisionScore QuerySearchPrecision { - get { return QuerySearchPrecision.ToString(); } + get => _querySearchPrecision; set { - try - { - var precisionScore = (SearchPrecisionScore)Enum - .Parse(typeof(SearchPrecisionScore), value); - - QuerySearchPrecision = precisionScore; - StringMatcher.Instance.UserSettingSearchPrecision = precisionScore; - } - catch (ArgumentException e) - { - Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e); - - QuerySearchPrecision = SearchPrecisionScore.Regular; - StringMatcher.Instance.UserSettingSearchPrecision = SearchPrecisionScore.Regular; - - throw; - } + _querySearchPrecision = value; + if (StringMatcher.Instance != null) + StringMatcher.Instance.UserSettingSearchPrecision = value; } } @@ -197,17 +207,18 @@ public string QuerySearchPrecisionString public double WindowLeft { get; set; } public double WindowTop { get; set; } - + /// /// Custom left position on selected monitor /// public double CustomWindowLeft { get; set; } = 0; - + /// /// Custom top position on selected monitor /// public double CustomWindowTop { get; set; } = 0; - + + public bool KeepMaxResults { get; set; } = false; public int MaxResultsToShow { get; set; } = 5; public int ActivateTimes { get; set; } @@ -219,7 +230,7 @@ public string QuerySearchPrecisionString [JsonIgnore] public ObservableCollection BuiltinShortcuts { get; set; } = new() { - new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), + new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) }; @@ -243,7 +254,7 @@ public bool HideNotifyIcon [JsonConverter(typeof(JsonStringEnumConverter))] public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor; - + [JsonConverter(typeof(JsonStringEnumConverter))] public SearchWindowAligns SearchWindowAlign { get; set; } = SearchWindowAligns.Center; @@ -260,9 +271,99 @@ public bool HideNotifyIcon public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium; public int CustomAnimationLength { get; set; } = 360; + [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(); + + [JsonIgnore] + public List RegisteredHotkeys + { + get + { + var list = FixedHotkeys(); + + // Customizeable hotkeys + if(!string.IsNullOrEmpty(Hotkey)) + list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = "")); + if(!string.IsNullOrEmpty(PreviewHotkey)) + list.Add(new(PreviewHotkey, "previewHotkey", () => PreviewHotkey = "")); + if(!string.IsNullOrEmpty(AutoCompleteHotkey)) + list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = "")); + if(!string.IsNullOrEmpty(AutoCompleteHotkey2)) + list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = "")); + if(!string.IsNullOrEmpty(SelectNextItemHotkey)) + list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = "")); + if(!string.IsNullOrEmpty(SelectNextItemHotkey2)) + list.Add(new(SelectNextItemHotkey2, "SelectNextItemHotkey", () => SelectNextItemHotkey2 = "")); + if(!string.IsNullOrEmpty(SelectPrevItemHotkey)) + list.Add(new(SelectPrevItemHotkey, "SelectPrevItemHotkey", () => SelectPrevItemHotkey = "")); + if(!string.IsNullOrEmpty(SelectPrevItemHotkey2)) + list.Add(new(SelectPrevItemHotkey2, "SelectPrevItemHotkey", () => SelectPrevItemHotkey2 = "")); + if(!string.IsNullOrEmpty(SettingWindowHotkey)) + list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = "")); + if(!string.IsNullOrEmpty(OpenContextMenuHotkey)) + list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = "")); + if(!string.IsNullOrEmpty(SelectNextPageHotkey)) + list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = "")); + if(!string.IsNullOrEmpty(SelectPrevPageHotkey)) + list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = "")); + if (!string.IsNullOrEmpty(CycleHistoryUpHotkey)) + list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = "")); + if (!string.IsNullOrEmpty(CycleHistoryDownHotkey)) + list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = "")); + + // Custom Query Hotkeys + foreach (var customPluginHotkey in CustomPluginHotkeys) + { + if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey)) + list.Add(new(customPluginHotkey.Hotkey, "customQueryHotkey", () => customPluginHotkey.Hotkey = "")); + } + + return list; + } + } + + private List FixedHotkeys() + { + return new List + { + new("Up", "HotkeyLeftRightDesc"), + new("Down", "HotkeyLeftRightDesc"), + new("Left", "HotkeyUpDownDesc"), + new("Right", "HotkeyUpDownDesc"), + new("Escape", "HotkeyESCDesc"), + new("F5", "ReloadPluginHotkey"), + new("Alt+Home", "HotkeySelectFirstResult"), + new("Alt+End", "HotkeySelectLastResult"), + new("Ctrl+R", "HotkeyRequery"), + new("Ctrl+H", "ToggleHistoryHotkey"), + new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"), + new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"), + new("Ctrl+OemPlus", "QuickHeightHotkey"), + new("Ctrl+OemMinus", "QuickHeightHotkey"), + new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"), + new("Shift+Enter", "OpenContextMenuHotkey"), + new("Enter", "HotkeyRunDesc"), + new("Ctrl+Enter", "OpenContainFolderHotkey"), + new("Alt+Enter", "HotkeyOpenResult"), + new("Ctrl+F12", "ToggleGameModeHotkey"), + new("Ctrl+Shift+C", "CopyFilePathHotkey"), + + new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1), + new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2), + new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3), + new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4), + new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5), + new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6), + new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7), + new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8), + new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9), + new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10) + }; + } } public enum LastQueryMode @@ -278,7 +379,7 @@ public enum ColorSchemes Light, Dark } - + public enum SearchWindowScreens { RememberLastLaunchLocation, @@ -287,7 +388,7 @@ public enum SearchWindowScreens Primary, Custom } - + public enum SearchWindowAligns { Center, diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 7b0880b676a..c4eb1c81d3a 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 @@ -67,7 +67,7 @@ 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 00000000000..cc4f94c569c --- /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/Result.cs b/Flow.Launcher.Plugin/Result.cs index fc6c5d185d8..9b42b102176 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -204,7 +204,7 @@ public Result Clone() Score = Score, TitleHighlightData = TitleHighlightData, OriginQuery = OriginQuery, - PluginDirectory = PluginDirectory + PluginDirectory = PluginDirectory, }; } @@ -258,7 +258,7 @@ public ValueTask ExecuteAsync(ActionContext context) public string ProgressBarColor { get; set; } = "#26a0da"; /// - /// Contains data used to populate the the preview section of this result. + /// Contains data used to populate the preview section of this result. /// public PreviewInfo Preview { get; set; } = PreviewInfo.Default; @@ -270,12 +270,12 @@ public record PreviewInfo /// /// 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 +283,17 @@ public record PreviewInfo /// /// 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 +304,7 @@ public record PreviewInfo Description = null, IsMedia = false, PreviewDelegate = null, + FilePath = null, }; } } diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 4137ca8d076..dd8c4b11232 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; #pragma warning disable IDE0005 using System.Windows; #pragma warning restore IDE0005 @@ -200,6 +201,24 @@ public static void OpenFile(string filePath, string workingDir = "", bool asAdmi } } + /// + /// This checks whether a given string is a zip file path. + /// By default does not check if the zip file actually exist on disk, can do so by + /// setting checkFileExists = true. + /// + public static bool IsZipFilePath(string querySearchString, bool checkFileExists = false) + { + if (IsLocationPathString(querySearchString) && querySearchString.Split('.').Last() == "zip") + { + if (checkFileExists) + return FileExists(querySearchString); + + return true; + } + + return false; + } + /// /// This checks whether a given string is a directory path or network location string. /// It does not check if location actually exists. diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index fd967de4aca..9fd8681ddeb 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -50,11 +50,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index e9d37433f4e..80cb74729fc 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -191,11 +191,15 @@ public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileConte [TestCase(@"\c:\", false)] [TestCase(@"cc:\", false)] [TestCase(@"\\\SomeNetworkLocation\", false)] + [TestCase(@"\\SomeNetworkLocation\", true)] [TestCase("RandomFile", false)] [TestCase(@"c:\>*", true)] [TestCase(@"c:\>", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation", true)] + [TestCase(@"c:\SomeLocation\SomeOtherLocation\SomeFile.exe", true)] + [TestCase(@"\\SomeNetworkLocation\SomeFile.exe", true)] + public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) { // When, Given diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln index 13e98833a9a..e44b23232fb 100644 --- a/Flow.Launcher.sln +++ b/Flow.Launcher.sln @@ -54,6 +54,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Scripts\post_build.ps1 = Scripts\post_build.ps1 README.md = README.md SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs + Settings.XamlStyler = Settings.XamlStyler EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Shell", "Plugins\Flow.Launcher.Plugin.Shell\Flow.Launcher.Plugin.Shell.csproj", "{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}" diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index b8e2a1cfe48..13e943c95ef 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -24,7 +24,8 @@ - + + diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index f4a17761f00..4d1adc6cd51 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -62,6 +62,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => _settingsVM = new SettingWindowViewModel(_updater, _portable); _settings = _settingsVM.Settings; + _settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled(); AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); @@ -70,6 +71,9 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => StringMatcher.Instance = _stringMatcher; _stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision; + InternationalizationManager.Instance.Settings = _settings; + InternationalizationManager.Instance.ChangeLanguage(_settings.Language); + PluginManager.LoadPlugins(_settings.PluginSettings); _mainVM = new MainViewModel(_settings); @@ -80,7 +84,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => await PluginManager.InitializePluginsAsync(API); await imageLoadertask; - + var window = new MainWindow(_settings, _mainVM); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); @@ -90,10 +94,6 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => HotKeyMapper.Initialize(_mainVM); - // 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); // main windows needs initialized before theme change because of blur settings ThemeManager.Instance.Settings = _settings; ThemeManager.Instance.ChangeTheme(_settings.Theme); diff --git a/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs b/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs index fabd01a24d1..ed94771f04e 100644 --- a/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs +++ b/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs @@ -44,7 +44,7 @@ values[2] is not string queryText || // Check if Text will be larger than our QueryTextBox Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch); // TODO: Obsolete warning? - var ft = new FormattedText(queryTextBox.Text, CultureInfo.DefaultThreadCurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black); + var ft = new FormattedText(queryTextBox.Text, CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black); var offset = queryTextBox.Padding.Right; diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml index 1113cb24de0..068afda15b4 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml @@ -2,10 +2,12 @@ x:Class="Flow.Launcher.CustomQueryHotkeySetting" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:flowlauncher="clr-namespace:Flow.Launcher" Title="{DynamicResource customeQueryHotkeyTitle}" Width="530" Background="{DynamicResource PopuBGColor}" + DataContext="{Binding RelativeSource={RelativeSource Self}}" Foreground="{DynamicResource PopupTextColor}" Icon="Images\app.png" MouseDown="window_MouseDown" @@ -60,94 +62,76 @@ - + + + + + + + + + + + + + + + - - + Margin="10" + HorizontalAlignment="Left" + VerticalAlignment="Center" + FontSize="14" + Text="{DynamicResource hotkey}" /> + - - - - - - - - - - - - - - - - - - - - - + diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 3da66caeb01..a42bde7c9cd 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -1,141 +1,204 @@ -using System; +#nullable enable + +using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Media; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Plugin; -using System.Threading; namespace Flow.Launcher { - public partial class HotkeyControl : UserControl + public partial class HotkeyControl { - public HotkeyModel CurrentHotkey { get; private set; } - public bool CurrentHotkeyAvailable { get; private set; } + 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); } + } - public event EventHandler HotkeyChanged; + public static readonly DependencyProperty WindowTitleProperty = DependencyProperty.Register( + nameof(WindowTitle), + typeof(string), + typeof(HotkeyControl), + new PropertyMetadata(string.Empty) + ); /// /// Designed for Preview Hotkey and KeyGesture. /// - public bool ValidateKeyGesture { get; set; } = false; + public static readonly DependencyProperty ValidateKeyGestureProperty = DependencyProperty.Register( + nameof(ValidateKeyGesture), + typeof(bool), + typeof(HotkeyControl), + new PropertyMetadata(default(bool)) + ); + + public bool ValidateKeyGesture + { + get { return (bool)GetValue(ValidateKeyGestureProperty); } + set { SetValue(ValidateKeyGestureProperty, value); } + } + + public static readonly DependencyProperty DefaultHotkeyProperty = DependencyProperty.Register( + nameof(DefaultHotkey), + typeof(string), + typeof(HotkeyControl), + new PropertyMetadata(default(string)) + ); + + public string DefaultHotkey + { + get { return (string)GetValue(DefaultHotkeyProperty); } + set { SetValue(DefaultHotkeyProperty, value); } + } + + private static void OnHotkeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not HotkeyControl hotkeyControl) + { + return; + } + + hotkeyControl.SetKeysToDisplay(new HotkeyModel(hotkeyControl.Hotkey)); + hotkeyControl.CurrentHotkey = new HotkeyModel(hotkeyControl.Hotkey); + } + + + public static readonly DependencyProperty ChangeHotkeyProperty = DependencyProperty.Register( + nameof(ChangeHotkey), + typeof(ICommand), + typeof(HotkeyControl), + new PropertyMetadata(default(ICommand)) + ); + + public ICommand? ChangeHotkey + { + get { return (ICommand)GetValue(ChangeHotkeyProperty); } + set { SetValue(ChangeHotkeyProperty, value); } + } + - protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty); + public static readonly DependencyProperty HotkeyProperty = DependencyProperty.Register( + nameof(Hotkey), + typeof(string), + typeof(HotkeyControl), + new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged) + ); + + public string Hotkey + { + get { return (string)GetValue(HotkeyProperty); } + set { SetValue(HotkeyProperty, value); } + } public HotkeyControl() { InitializeComponent(); + + HotkeyList.ItemsSource = KeysToDisplay; + SetKeysToDisplay(CurrentHotkey); } - private CancellationTokenSource hotkeyUpdateSource; + private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => + hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); - private void TbHotkey_OnPreviewKeyDown(object sender, KeyEventArgs e) - { - hotkeyUpdateSource?.Cancel(); - hotkeyUpdateSource?.Dispose(); - hotkeyUpdateSource = new(); - var token = hotkeyUpdateSource.Token; - e.Handled = true; + public string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none"); - //when alt is pressed, the real key should be e.SystemKey - Key key = e.Key == Key.System ? e.SystemKey : e.Key; + public ObservableCollection KeysToDisplay { get; set; } = new(); - SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers(); + public HotkeyModel CurrentHotkey { get; private set; } = new(false, false, false, false, Key.None); - var hotkeyModel = new HotkeyModel( - specialKeyState.AltPressed, - specialKeyState.ShiftPressed, - specialKeyState.WinPressed, - specialKeyState.CtrlPressed, - key); - if (hotkeyModel.Equals(CurrentHotkey)) + public void GetNewHotkey(object sender, RoutedEventArgs e) + { + OpenHotkeyDialog(); + } + + private async Task OpenHotkeyDialog() + { + if (!string.IsNullOrEmpty(Hotkey)) { - return; + HotKeyMapper.RemoveHotkey(Hotkey); } - _ = Dispatcher.InvokeAsync(async () => + var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, HotkeySettings, WindowTitle); + await dialog.ShowAsync(); + switch (dialog.ResultType) { - await Task.Delay(500, token); - if (!token.IsCancellationRequested) - await SetHotkeyAsync(hotkeyModel); - }); + case HotkeyControlDialog.EResultType.Cancel: + SetHotkey(Hotkey); + return; + case HotkeyControlDialog.EResultType.Save: + SetHotkey(dialog.ResultValue); + break; + case HotkeyControlDialog.EResultType.Delete: + Delete(); + break; + } } - public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true) - { - tbHotkey.Text = keyModel.ToString(); - tbHotkey.Select(tbHotkey.Text.Length, 0); + private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true) + { if (triggerValidate) { bool hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture); - CurrentHotkeyAvailable = hotkeyAvailable; - SetMessage(hotkeyAvailable); - OnHotkeyChanged(); - - var token = hotkeyUpdateSource.Token; - await Task.Delay(500, token); - if (token.IsCancellationRequested) - return; - if (CurrentHotkeyAvailable) + if (!hotkeyAvailable) { - CurrentHotkey = keyModel; - // To trigger LostFocus - FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); - Keyboard.ClearFocus(); + return; } + + Hotkey = keyModel.ToString(); + SetKeysToDisplay(CurrentHotkey); + ChangeHotkey?.Execute(keyModel); } else { - CurrentHotkey = keyModel; + Hotkey = keyModel.ToString(); + ChangeHotkey?.Execute(keyModel); } } - - public Task SetHotkeyAsync(string keyStr, bool triggerValidate = true) - { - return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate); - } - - private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); - public new bool IsFocused => tbHotkey.IsFocused; - - private void tbHotkey_LostFocus(object sender, RoutedEventArgs e) + public void Delete() { - tbHotkey.Text = CurrentHotkey?.ToString() ?? ""; - tbHotkey.Select(tbHotkey.Text.Length, 0); + if (!string.IsNullOrEmpty(Hotkey)) + HotKeyMapper.RemoveHotkey(Hotkey); + Hotkey = ""; + SetKeysToDisplay(new HotkeyModel(false, false, false, false, Key.None)); } - private void tbHotkey_GotFocus(object sender, RoutedEventArgs e) + private void SetKeysToDisplay(HotkeyModel? hotkey) { - ResetMessage(); - } - - private void ResetMessage() - { - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("flowlauncherPressHotkey"); - tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B"); - } + KeysToDisplay.Clear(); - private void SetMessage(bool hotkeyAvailable) - { - if (!hotkeyAvailable) + if (hotkey == null || hotkey == default(HotkeyModel)) { - tbMsg.Foreground = new SolidColorBrush(Colors.Red); - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable"); + KeysToDisplay.Add(EmptyHotkey); + return; } - else + + foreach (var key in hotkey.Value.EnumerateDisplayKeys()!) { - tbMsg.Foreground = new SolidColorBrush(Colors.Green); - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success"); + KeysToDisplay.Add(key); } - tbMsg.Visibility = Visibility.Visible; + } + + public void SetHotkey(string? keyStr, bool triggerValidate = true) + { + SetHotkey(new HotkeyModel(keyStr), triggerValidate); } } } diff --git a/Flow.Launcher/HotkeyControlDialog.xaml b/Flow.Launcher/HotkeyControlDialog.xaml new file mode 100644 index 00000000000..9a5872f4dc1 --- /dev/null +++ b/Flow.Launcher/HotkeyControlDialog.xaml @@ -0,0 +1,169 @@ + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml.cs b/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml.cs new file mode 100644 index 00000000000..9b19ffd8626 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml.cs @@ -0,0 +1,63 @@ +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; + +namespace Flow.Launcher.Resources.Controls +{ + public partial class HotkeyDisplay : UserControl + { + public enum DisplayType + { + Default, + Small + } + + public HotkeyDisplay() + { + InitializeComponent(); + //List stringList =e.NewValue.Split('+').ToList(); + Values = new ObservableCollection(); + KeysControl.ItemsSource = Values; + } + + public string Keys + { + get { return (string)GetValue(KeysProperty); } + set { SetValue(KeysProperty, value); } + } + + public static readonly DependencyProperty KeysProperty = + DependencyProperty.Register(nameof(Keys), typeof(string), typeof(HotkeyDisplay), + new PropertyMetadata(string.Empty, keyChanged)); + + public DisplayType Type + { + get { return (DisplayType)GetValue(TypeProperty); } + set { SetValue(TypeProperty, value); } + } + + public static readonly DependencyProperty TypeProperty = + DependencyProperty.Register(nameof(Type), typeof(DisplayType), typeof(HotkeyDisplay), + new PropertyMetadata(DisplayType.Default)); + + private static void keyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as UserControl; + if (null == control) return; // This should not be possible + + var newValue = e.NewValue as string; + if (null == newValue) return; + + if (d is not HotkeyDisplay hotkeyDisplay) + return; + + hotkeyDisplay.Values.Clear(); + foreach (var key in newValue.Split('+')) + { + hotkeyDisplay.Values.Add(key); + } + } + + public ObservableCollection Values { get; set; } + } +} diff --git a/Flow.Launcher/Resources/Controls/HyperLink.xaml b/Flow.Launcher/Resources/Controls/HyperLink.xaml new file mode 100644 index 00000000000..9ea550afd51 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/HyperLink.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs b/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs new file mode 100644 index 00000000000..855cccdbd60 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs @@ -0,0 +1,39 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Navigation; + +namespace Flow.Launcher.Resources.Controls; + +public partial class HyperLink : UserControl +{ + public static readonly DependencyProperty UriProperty = DependencyProperty.Register( + nameof(Uri), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string)) + ); + + public string Uri + { + get => (string)GetValue(UriProperty); + set => SetValue(UriProperty, value); + } + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register( + nameof(Text), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string)) + ); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public HyperLink() + { + InitializeComponent(); + } + + private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + App.API.OpenUrl(e.Uri); + e.Handled = true; + } +} diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml new file mode 100644 index 00000000000..ed3c2969060 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs new file mode 100644 index 00000000000..dfa03a2043f --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs @@ -0,0 +1,9 @@ +namespace Flow.Launcher.Resources.Controls; + +public partial class InstalledPluginDisplay +{ + public InstalledPluginDisplay() + { + InitializeComponent(); + } +} diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml new file mode 100644 index 00000000000..83a771728b6 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml.cs b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml.cs new file mode 100644 index 00000000000..f8d0afe61cd --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace Flow.Launcher.Resources.Controls; + +public partial class InstalledPluginDisplayBottomData : UserControl +{ + public InstalledPluginDisplayBottomData() + { + InitializeComponent(); + } +} diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml new file mode 100644 index 00000000000..ff2f14c4b2d --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml @@ -0,0 +1,47 @@ + + + + +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs new file mode 100644 index 00000000000..de6a4df5ee8 --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs @@ -0,0 +1,29 @@ +using System; +using System.Windows.Navigation; +using Flow.Launcher.SettingPages.ViewModels; + +namespace Flow.Launcher.SettingPages.Views; + +public partial class SettingsPaneAbout +{ + private SettingsPaneAboutViewModel _viewModel = null!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (!IsInitialized) + { + if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings, Updater: { } updater }) + throw new ArgumentException("Settings are required for SettingsPaneAbout."); + _viewModel = new SettingsPaneAboutViewModel(settings, updater); + DataContext = _viewModel; + InitializeComponent(); + } + base.OnNavigatedTo(e); + } + + private void OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + App.API.OpenUrl(e.Uri.AbsoluteUri); + e.Handled = true; + } +} diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml new file mode 100644 index 00000000000..30e065b1601 --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs new file mode 100644 index 00000000000..dfb4a7eaf6c --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs @@ -0,0 +1,66 @@ +using System; +using System.ComponentModel; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Navigation; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.SettingPages.ViewModels; +using Flow.Launcher.ViewModel; + +namespace Flow.Launcher.SettingPages.Views; + +public partial class SettingsPanePluginStore +{ + private SettingsPanePluginStoreViewModel _viewModel = null!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (!IsInitialized) + { + if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings }) + throw new ArgumentException($"Settings are required for {nameof(SettingsPanePluginStore)}."); + _viewModel = new SettingsPanePluginStoreViewModel(); + DataContext = _viewModel; + InitializeComponent(); + } + _viewModel.PropertyChanged += ViewModel_PropertyChanged; + base.OnNavigatedTo(e); + } + + private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SettingsPanePluginStoreViewModel.FilterText)) + { + ((CollectionViewSource)FindResource("PluginStoreCollectionView")).View.Refresh(); + } + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + _viewModel.PropertyChanged -= ViewModel_PropertyChanged; + base.OnNavigatingFrom(e); + } + + private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return; + PluginStoreFilterTextbox.Focus(); + } + + private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + PluginManager.API.OpenUrl(e.Uri.AbsoluteUri); + e.Handled = true; + } + + private void PluginStoreCollectionView_OnFilter(object sender, FilterEventArgs e) + { + if (e.Item is not PluginStoreItemViewModel plugin) + { + e.Accepted = false; + return; + } + + e.Accepted = _viewModel.SatisfiesFilter(plugin); + } +} diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml new file mode 100644 index 00000000000..37079a46fef --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs new file mode 100644 index 00000000000..d48505c3dbe --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs @@ -0,0 +1,30 @@ +using System; +using System.Windows.Input; +using System.Windows.Navigation; +using Flow.Launcher.SettingPages.ViewModels; + +namespace Flow.Launcher.SettingPages.Views; + +public partial class SettingsPanePlugins +{ + private SettingsPanePluginsViewModel _viewModel = null!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (!IsInitialized) + { + if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings }) + throw new ArgumentException("Settings are required for SettingsPaneHotkey."); + _viewModel = new SettingsPanePluginsViewModel(settings); + DataContext = _viewModel; + InitializeComponent(); + } + base.OnNavigatedTo(e); + } + + private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return; + PluginFilterTextbox.Focus(); + } +} diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml new file mode 100644 index 00000000000..768abbf978a --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -590,2739 +196,72 @@ Grid.Row="0" Width="50" Height="50" + RenderOptions.BitmapScalingMode="HighQuality" Source="images/app.png" /> - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - -  - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - + + + + - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - -  - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - + diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs index 7301be130e8..4cc125fa4af 100644 --- a/Flow.Launcher/SettingWindow.xaml.cs +++ b/Flow.Launcher/SettingWindow.xaml.cs @@ -1,538 +1,190 @@ -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; +using System; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Input; +using System.Windows.Interop; +using Flow.Launcher.Core; +using Flow.Launcher.Core.Configuration; using Flow.Launcher.Helper; -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using Flow.Launcher.SettingPages.Views; using Flow.Launcher.ViewModel; -using ModernWpf; using ModernWpf.Controls; -using System; -using System.ComponentModel; -using System.IO; -using System.Windows; -using System.Windows.Data; -using System.Windows.Forms; -using System.Windows.Input; -using System.Windows.Interop; -using System.Windows.Navigation; -using Button = System.Windows.Controls.Button; -using Control = System.Windows.Controls.Control; -using KeyEventArgs = System.Windows.Input.KeyEventArgs; -using MessageBox = System.Windows.MessageBox; using TextBox = System.Windows.Controls.TextBox; -using ThemeManager = ModernWpf.ThemeManager; - -namespace Flow.Launcher -{ - public partial class SettingWindow - { - public readonly IPublicAPI API; - private Settings settings; - private SettingWindowViewModel viewModel; - - public SettingWindow(IPublicAPI api, SettingWindowViewModel viewModel) - { - settings = viewModel.Settings; - DataContext = viewModel; - this.viewModel = viewModel; - API = api; - InitializePosition(); - InitializeComponent(); - - } - - #region General - - private void OnLoaded(object sender, RoutedEventArgs e) - { - RefreshMaximizeRestoreButton(); - // Fix (workaround) for the window freezes after lock screen (Win+L) - // https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf - HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource; - HwndTarget hwndTarget = hwndSource.CompositionTarget; - hwndTarget.RenderMode = RenderMode.SoftwareOnly; - - pluginListView = (CollectionView)CollectionViewSource.GetDefaultView(Plugins.ItemsSource); - pluginListView.Filter = PluginListFilter; - - pluginStoreView = (CollectionView)CollectionViewSource.GetDefaultView(StoreListBox.ItemsSource); - pluginStoreView.Filter = PluginStoreFilter; - - viewModel.PropertyChanged += new PropertyChangedEventHandler(SettingsWindowViewModelChanged); - - InitializePosition(); - } - - private void SettingsWindowViewModelChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(viewModel.ExternalPlugins)) - { - pluginStoreView = (CollectionView)CollectionViewSource.GetDefaultView(StoreListBox.ItemsSource); - pluginStoreView.Filter = PluginStoreFilter; - pluginStoreView.Refresh(); - } - } - - private void OnSelectPythonPathClick(object sender, RoutedEventArgs e) - { - var selectedFile = viewModel.GetFileFromDialog( - InternationalizationManager.Instance.GetTranslation("selectPythonExecutable"), - "Python|pythonw.exe"); - - if (!string.IsNullOrEmpty(selectedFile)) - settings.PluginSettings.PythonExecutablePath = selectedFile; - } - private void OnSelectNodePathClick(object sender, RoutedEventArgs e) - { - var selectedFile = viewModel.GetFileFromDialog( - InternationalizationManager.Instance.GetTranslation("selectNodeExecutable")); - - if (!string.IsNullOrEmpty(selectedFile)) - settings.PluginSettings.NodeExecutablePath = selectedFile; - } - - private void OnSelectFileManagerClick(object sender, RoutedEventArgs e) - { - SelectFileManagerWindow fileManagerChangeWindow = new SelectFileManagerWindow(settings); - fileManagerChangeWindow.ShowDialog(); - } - - private void OnSelectDefaultBrowserClick(object sender, RoutedEventArgs e) - { - var browserWindow = new SelectBrowserWindow(settings); - browserWindow.ShowDialog(); - } - - #endregion - - #region Hotkey - - private void OnHotkeyControlLoaded(object sender, RoutedEventArgs e) - { - _ = HotkeyControl.SetHotkeyAsync(viewModel.Settings.Hotkey, false); - } - - private void OnHotkeyControlFocused(object sender, RoutedEventArgs e) - { - HotKeyMapper.RemoveHotkey(settings.Hotkey); - } - - private void OnHotkeyControlFocusLost(object sender, RoutedEventArgs e) - { - if (HotkeyControl.CurrentHotkeyAvailable) - { - HotKeyMapper.SetHotkey(HotkeyControl.CurrentHotkey, HotKeyMapper.OnToggleHotkey); - HotKeyMapper.RemoveHotkey(settings.Hotkey); - settings.Hotkey = HotkeyControl.CurrentHotkey.ToString(); - } - else - { - HotKeyMapper.SetHotkey(new HotkeyModel(settings.Hotkey), HotKeyMapper.OnToggleHotkey); - } - } - - private void OnPreviewHotkeyControlLoaded(object sender, RoutedEventArgs e) - { - _ = PreviewHotkeyControl.SetHotkeyAsync(settings.PreviewHotkey, false); - } - - private void OnPreviewHotkeyControlFocusLost(object sender, RoutedEventArgs e) - { - if (PreviewHotkeyControl.CurrentHotkeyAvailable) - { - settings.PreviewHotkey = PreviewHotkeyControl.CurrentHotkey.ToString(); - } - } - - private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e) - { - var item = viewModel.SelectedCustomPluginHotkey; - if (item == null) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); - return; - } - - string deleteWarning = - string.Format(InternationalizationManager.Instance.GetTranslation("deleteCustomHotkeyWarning"), - item.Hotkey); - if ( - MessageBox.Show(deleteWarning, InternationalizationManager.Instance.GetTranslation("delete"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) - { - settings.CustomPluginHotkeys.Remove(item); - HotKeyMapper.RemoveHotkey(item.Hotkey); - } - } - - private void OnEditCustomHotkeyClick(object sender, RoutedEventArgs e) - { - var item = viewModel.SelectedCustomPluginHotkey; - if (item != null) - { - CustomQueryHotkeySetting window = new CustomQueryHotkeySetting(this, settings); - window.UpdateItem(item); - window.ShowDialog(); - } - else - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); - } - } - - private void OnAddCustomHotkeyClick(object sender, RoutedEventArgs e) - { - new CustomQueryHotkeySetting(this, settings).ShowDialog(); - } - - #endregion - - #region Plugin - - private void OnPluginToggled(object sender, RoutedEventArgs e) - { - var id = viewModel.SelectedPlugin.PluginPair.Metadata.ID; - // used to sync the current status from the plugin manager into the setting to keep consistency after save - settings.PluginSettings.Plugins[id].Disabled = viewModel.SelectedPlugin.PluginPair.Metadata.Disabled; - } - - private void OnPluginPriorityClick(object sender, RoutedEventArgs e) - { - if (sender is Control { DataContext: PluginViewModel pluginViewModel }) - { - PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(pluginViewModel.PluginPair.Metadata.ID, pluginViewModel); - priorityChangeWindow.ShowDialog(); - } - } - - #endregion - - #region Proxy - - private void OnTestProxyClick(object sender, RoutedEventArgs e) - { // TODO: change to command - var msg = viewModel.TestProxy(); - MessageBox.Show(msg); // TODO: add message box service - } - - #endregion - - private void OnCheckUpdates(object sender, RoutedEventArgs e) - { - viewModel.UpdateApp(); // TODO: change to command - } - - private void OnRequestNavigate(object sender, RequestNavigateEventArgs e) - { - API.OpenUrl(e.Uri.AbsoluteUri); - e.Handled = true; - } - - private void OnClosed(object sender, EventArgs e) - { - settings.SettingWindowState = WindowState; - settings.SettingWindowTop = Top; - settings.SettingWindowLeft = Left; - viewModel.Save(); - API.SavePluginSettings(); - } - - private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e) - { - Close(); - } - - private void OpenThemeFolder(object sender, RoutedEventArgs e) - { - PluginManager.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes)); - } - - private void OpenSettingFolder(object sender, RoutedEventArgs e) - { - PluginManager.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Settings)); - } - - private void OpenWelcomeWindow(object sender, RoutedEventArgs e) - { - var WelcomeWindow = new WelcomeWindow(settings); - WelcomeWindow.ShowDialog(); - } - private void OpenLogFolder(object sender, RoutedEventArgs e) - { - viewModel.OpenLogFolder(); - } - private void ClearLogFolder(object sender, RoutedEventArgs e) - { - var confirmResult = MessageBox.Show( - InternationalizationManager.Instance.GetTranslation("clearlogfolderMessage"), - InternationalizationManager.Instance.GetTranslation("clearlogfolder"), - MessageBoxButton.YesNo); - - if (confirmResult == MessageBoxResult.Yes) - { - viewModel.ClearLogFolder(); - } - } +namespace Flow.Launcher; - private void OnExternalPluginInstallClick(object sender, RoutedEventArgs e) - { - if (sender is not Button { DataContext: PluginStoreItemViewModel plugin } button) - { - return; - } - - if (storeClickedButton != null) - { - FlyoutService.GetFlyout(storeClickedButton).Hide(); - } - - viewModel.DisplayPluginQuery($"install {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); - } - - private void OnExternalPluginUninstallClick(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) - { - var name = viewModel.SelectedPlugin.PluginPair.Metadata.Name; - viewModel.DisplayPluginQuery($"uninstall {name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); - } - } - - private void OnExternalPluginUninstallClick(object sender, RoutedEventArgs e) - { - if (storeClickedButton != null) - { - FlyoutService.GetFlyout(storeClickedButton).Hide(); - } +public partial class SettingWindow +{ + private readonly IPublicAPI _api; + private readonly Settings _settings; + private readonly SettingWindowViewModel _viewModel; - if (sender is Button { DataContext: PluginStoreItemViewModel plugin }) - viewModel.DisplayPluginQuery($"uninstall {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); + public SettingWindow(IPublicAPI api, SettingWindowViewModel viewModel) + { + _settings = viewModel.Settings; + DataContext = viewModel; + _viewModel = viewModel; + _api = api; + InitializePosition(); + InitializeComponent(); + NavView.SelectedItem = NavView.MenuItems[0]; /* Set First Page */ + } - } + private void OnLoaded(object sender, RoutedEventArgs e) + { + RefreshMaximizeRestoreButton(); + // Fix (workaround) for the window freezes after lock screen (Win+L) + // https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf + HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource; + HwndTarget hwndTarget = hwndSource.CompositionTarget; + hwndTarget.RenderMode = RenderMode.Default; + + InitializePosition(); + } - private void OnExternalPluginUpdateClick(object sender, RoutedEventArgs e) - { - if (storeClickedButton != null) - { - FlyoutService.GetFlyout(storeClickedButton).Hide(); - } - if (sender is Button { DataContext: PluginStoreItemViewModel plugin }) - viewModel.DisplayPluginQuery($"update {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); + private void OnClosed(object sender, EventArgs e) + { + _settings.SettingWindowState = WindowState; + _settings.SettingWindowTop = Top; + _settings.SettingWindowLeft = Left; + _viewModel.Save(); + _api.SavePluginSettings(); + } - } + private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e) + { + Close(); + } - private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */ + private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */ + { + if (Keyboard.FocusedElement is not TextBox textBox) { - if (Keyboard.FocusedElement is not TextBox textBox) - { - return; - } - var tRequest = new TraversalRequest(FocusNavigationDirection.Next); - textBox.MoveFocus(tRequest); + return; } + var tRequest = new TraversalRequest(FocusNavigationDirection.Next); + textBox.MoveFocus(tRequest); + } - private void ColorSchemeSelectedIndexChanged(object sender, EventArgs e) - => ThemeManager.Current.ApplicationTheme = settings.ColorScheme switch - { - Constant.Light => ApplicationTheme.Light, - Constant.Dark => ApplicationTheme.Dark, - Constant.System => null, - _ => ThemeManager.Current.ApplicationTheme - }; + /* Custom TitleBar */ - /* Custom TitleBar */ + private void OnMinimizeButtonClick(object sender, RoutedEventArgs e) + { + WindowState = WindowState.Minimized; + } - private void OnMinimizeButtonClick(object sender, RoutedEventArgs e) + private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e) + { + WindowState = WindowState switch { - WindowState = WindowState.Minimized; - } + WindowState.Maximized => WindowState.Normal, + _ => WindowState.Maximized + }; + } - private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e) - { - WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } + private void OnCloseButtonClick(object sender, RoutedEventArgs e) + { + Close(); + } - private void OnCloseButtonClick(object sender, RoutedEventArgs e) + private void RefreshMaximizeRestoreButton() + { + if (WindowState == WindowState.Maximized) { - - Close(); + MaximizeButton.Visibility = Visibility.Collapsed; + RestoreButton.Visibility = Visibility.Visible; } - - private void RefreshMaximizeRestoreButton() + else { - if (WindowState == WindowState.Maximized) - { - maximizeButton.Visibility = Visibility.Collapsed; - restoreButton.Visibility = Visibility.Visible; - } - else - { - maximizeButton.Visibility = Visibility.Visible; - restoreButton.Visibility = Visibility.Collapsed; - } + MaximizeButton.Visibility = Visibility.Visible; + RestoreButton.Visibility = Visibility.Collapsed; } + } - private void Window_StateChanged(object sender, EventArgs e) - { - RefreshMaximizeRestoreButton(); - } + private void Window_StateChanged(object sender, EventArgs e) + { + RefreshMaximizeRestoreButton(); + } - #region Shortcut + public void InitializePosition() + { + var previousTop = _settings.SettingWindowTop; + var previousLeft = _settings.SettingWindowLeft; - private void OnDeleteCustomShortCutClick(object sender, RoutedEventArgs e) + if (previousTop == null || previousLeft == null || !IsPositionValid(previousTop.Value, previousLeft.Value)) { - viewModel.DeleteSelectedCustomShortcut(); + Top = WindowTop(); + Left = WindowLeft(); } - - private void OnEditCustomShortCutClick(object sender, RoutedEventArgs e) + else { - if (viewModel.EditSelectedCustomShortcut()) - { - customShortcutView.Items.Refresh(); - } + Top = previousTop.Value; + Left = previousLeft.Value; } + WindowState = _settings.SettingWindowState; + } - private void OnAddCustomShortCutClick(object sender, RoutedEventArgs e) + private bool IsPositionValid(double top, double left) + { + foreach (var screen in Screen.AllScreens) { - viewModel.AddCustomShortcut(); - } - - #endregion + var workingArea = screen.WorkingArea; - private CollectionView pluginListView; - private CollectionView pluginStoreView; - - private bool PluginListFilter(object item) - { - if (string.IsNullOrEmpty(pluginFilterTxb.Text)) - return true; - if (item is PluginViewModel model) + if (left >= workingArea.Left && left < workingArea.Right && + top >= workingArea.Top && top < workingArea.Bottom) { - return StringMatcher.FuzzySearch(pluginFilterTxb.Text, model.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet(); - } - return false; - } - - private bool PluginStoreFilter(object item) - { - if (string.IsNullOrEmpty(pluginStoreFilterTxb.Text)) return true; - if (item is PluginStoreItemViewModel model) - { - return StringMatcher.FuzzySearch(pluginStoreFilterTxb.Text, model.Name).IsSearchPrecisionScoreMet() - || StringMatcher.FuzzySearch(pluginStoreFilterTxb.Text, model.Description).IsSearchPrecisionScoreMet(); - } - return false; - } - - private string lastPluginListSearch = ""; - private string lastPluginStoreSearch = ""; - - private void RefreshPluginListEventHandler(object sender, RoutedEventArgs e) - { - if (pluginFilterTxb.Text != lastPluginListSearch) - { - lastPluginListSearch = pluginFilterTxb.Text; - pluginListView.Refresh(); - } - } - - private void RefreshPluginStoreEventHandler(object sender, RoutedEventArgs e) - { - if (pluginStoreFilterTxb.Text != lastPluginStoreSearch) - { - lastPluginStoreSearch = pluginStoreFilterTxb.Text; - pluginStoreView.Refresh(); - } - } - - private void PluginFilterTxb_OnKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - RefreshPluginListEventHandler(sender, e); - } - - private void PluginStoreFilterTxb_OnKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - RefreshPluginStoreEventHandler(sender, e); - } - - private void OnPluginSettingKeydown(object sender, KeyEventArgs e) - { - if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && e.Key == Key.F) - pluginFilterTxb.Focus(); - } - - private void PluginStore_OnKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.F && (Keyboard.Modifiers & ModifierKeys.Control) != 0) - { - pluginStoreFilterTxb.Focus(); } } + return false; + } - public void InitializePosition() - { - if (settings.SettingWindowTop >= 0 && settings.SettingWindowLeft >= 0) - { - Top = settings.SettingWindowTop; - Left = settings.SettingWindowLeft; - } - else - { - Top = WindowTop(); - Left = WindowLeft(); - } - WindowState = settings.SettingWindowState; - } + private double WindowLeft() + { + var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); + var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var left = (dip2.X - this.ActualWidth) / 2 + dip1.X; + return left; + } - public double WindowLeft() - { - var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); - var left = (dip2.X - this.ActualWidth) / 2 + dip1.X; - return left; - } + private double WindowTop() + { + var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); + var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); + var top = (dip2.Y - this.ActualHeight) / 2 + dip1.Y - 20; + return top; + } - public double WindowTop() + private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) + { + var paneData = new PaneData(_settings, _viewModel.Updater, _viewModel.Portable); + if (args.IsSettingsSelected) { - var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); - var top = (dip2.Y - this.ActualHeight) / 2 + dip1.Y - 20; - return top; + ContentFrame.Navigate(typeof(SettingsPaneGeneral), paneData); } - - private Button storeClickedButton; - - private void StoreListItem_Click(object sender, RoutedEventArgs e) + else { - if (sender is not Button button) - return; + var selectedItem = (NavigationViewItem)args.SelectedItem; + if (selectedItem == null) return; - storeClickedButton = button; - - var flyout = FlyoutService.GetFlyout(button); - flyout.Closed += (_, _) => + var pageType = selectedItem.Name switch { - storeClickedButton = null; + nameof(General) => typeof(SettingsPaneGeneral), + nameof(Plugins) => typeof(SettingsPanePlugins), + nameof(PluginStore) => typeof(SettingsPanePluginStore), + nameof(Theme) => typeof(SettingsPaneTheme), + nameof(Hotkey) => typeof(SettingsPaneHotkey), + nameof(Proxy) => typeof(SettingsPaneProxy), + nameof(About) => typeof(SettingsPaneAbout), + _ => typeof(SettingsPaneGeneral) }; - - } - - private void PluginStore_GotFocus(object sender, RoutedEventArgs e) - { - Keyboard.Focus(pluginStoreFilterTxb); - } - - private void Plugin_GotFocus(object sender, RoutedEventArgs e) - { - Keyboard.Focus(pluginFilterTxb); + ContentFrame.Navigate(pageType, paneData); } } + + public record PaneData(Settings Settings, Updater Updater, IPortable Portable); } diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml index 2ba3a592966..b96cb661e5e 100644 --- a/Flow.Launcher/Themes/Base.xaml +++ b/Flow.Launcher/Themes/Base.xaml @@ -28,8 +28,8 @@ - - + + @@ -69,7 +69,7 @@ @@ -115,18 +117,20 @@ - + @@ -135,10 +139,11 @@ - + + @@ -188,7 +193,7 @@ @@ -312,7 +317,7 @@ x:Name="PART_VerticalScrollBar" Grid.Row="0" Grid.Column="0" - Margin="0,0,0,0" + Margin="0 0 0 0" HorizontalAlignment="Right" AutomationProperties.AutomationId="VerticalScrollBar" Cursor="Arrow" @@ -368,22 +373,7 @@ - + @@ -413,16 +403,30 @@ + @@ -495,7 +500,7 @@ @@ -503,7 +508,7 @@ x:Key="ClockPanelPosition" BasedOn="{StaticResource BaseClockPanelPosition}" TargetType="{x:Type Canvas}"> - + - \ No newline at end of file + diff --git a/Flow.Launcher/Themes/BlurWhite.xaml b/Flow.Launcher/Themes/BlurWhite.xaml index 4406724b8ec..a13f3bfcc77 100644 --- a/Flow.Launcher/Themes/BlurWhite.xaml +++ b/Flow.Launcher/Themes/BlurWhite.xaml @@ -1,4 +1,9 @@ - + @@ -96,7 +101,7 @@ TargetType="{x:Type Rectangle}"> - + - - - - - - - - - - - - - #f1f1f1 - - - - - - - - - - 5 - 10 0 10 0 - 0 0 0 10 - - - - - - - - \ No newline at end of file diff --git a/Flow.Launcher/Themes/Circle Light.xaml b/Flow.Launcher/Themes/Circle Light.xaml deleted file mode 100644 index e52e3a9570f..00000000000 --- a/Flow.Launcher/Themes/Circle Light.xaml +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - - - - - - - - - - - #5046e5 - - - - - - - - - - 8 - 10 0 10 0 - 0 0 0 10 - - - - - - - - diff --git a/Flow.Launcher/Themes/Circle System.xaml b/Flow.Launcher/Themes/Circle System.xaml index 2b2ce7ca33d..343e7c5bc00 100644 --- a/Flow.Launcher/Themes/Circle System.xaml +++ b/Flow.Launcher/Themes/Circle System.xaml @@ -1,3 +1,8 @@ + - + @@ -27,7 +32,7 @@ x:Key="QuerySuggestionBoxStyle" BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}" TargetType="{x:Type TextBox}"> - + @@ -72,7 +77,7 @@ TargetType="{x:Type Rectangle}"> - + @@ -150,7 +155,7 @@ x:Key="ClockPanel" BasedOn="{StaticResource ClockPanel}" TargetType="{x:Type StackPanel}"> - + - \ No newline at end of file + diff --git a/Flow.Launcher/Themes/Cyan Dark.xaml b/Flow.Launcher/Themes/Cyan Dark.xaml index 60bc090022f..106b1b6d90e 100644 --- a/Flow.Launcher/Themes/Cyan Dark.xaml +++ b/Flow.Launcher/Themes/Cyan Dark.xaml @@ -27,7 +27,7 @@ x:Key="ItemGlyph" BasedOn="{StaticResource BaseGlyphStyle}" TargetType="{x:Type TextBlock}"> - + - + \ No newline at end of file diff --git a/Flow.Launcher/Themes/Discord Dark.xaml b/Flow.Launcher/Themes/Discord Dark.xaml index 5315c7644a1..9e39ee5bdaf 100644 --- a/Flow.Launcher/Themes/Discord Dark.xaml +++ b/Flow.Launcher/Themes/Discord Dark.xaml @@ -1,4 +1,4 @@ - @@ -189,4 +189,4 @@ TargetType="{x:Type TextBlock}"> - + \ No newline at end of file diff --git a/Flow.Launcher/Themes/Dracula.xaml b/Flow.Launcher/Themes/Dracula.xaml index d150e7355fe..eb8cc9557fb 100644 --- a/Flow.Launcher/Themes/Dracula.xaml +++ b/Flow.Launcher/Themes/Dracula.xaml @@ -28,7 +28,6 @@ x:Key="QuerySuggestionBoxStyle" BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}" TargetType="{x:Type TextBox}"> - @@ -98,7 +97,7 @@ diff --git a/Flow.Launcher/Themes/Pink.xaml b/Flow.Launcher/Themes/Pink.xaml index dc97e432055..d6f9813d001 100644 --- a/Flow.Launcher/Themes/Pink.xaml +++ b/Flow.Launcher/Themes/Pink.xaml @@ -2,45 +2,46 @@ - 0 0 0 4 + 0 0 0 0 - #cc1081 + #0e172c \ No newline at end of file diff --git a/Flow.Launcher/Themes/SlimLight.xaml b/Flow.Launcher/Themes/SlimLight.xaml index dc08eec3003..078b07048c3 100644 --- a/Flow.Launcher/Themes/SlimLight.xaml +++ b/Flow.Launcher/Themes/SlimLight.xaml @@ -179,15 +179,28 @@ BasedOn="{StaticResource BaseClockBox}" TargetType="{x:Type TextBlock}"> - + + + + + + + + - - - - - - - - - - - - - - - #ccd0d4 - - - - - - - - - - - - - - - diff --git a/Flow.Launcher/Themes/Win11System.xaml b/Flow.Launcher/Themes/Win10System.xaml similarity index 89% rename from Flow.Launcher/Themes/Win11System.xaml rename to Flow.Launcher/Themes/Win10System.xaml index 3025f9a07e2..4f8ea553290 100644 --- a/Flow.Launcher/Themes/Win11System.xaml +++ b/Flow.Launcher/Themes/Win10System.xaml @@ -1,3 +1,8 @@ + - + - - + + diff --git a/Flow.Launcher/Themes/Win11Dark.xaml b/Flow.Launcher/Themes/Win11Dark.xaml deleted file mode 100644 index 5abb96cce0d..00000000000 --- a/Flow.Launcher/Themes/Win11Dark.xaml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - 0 0 0 8 - - - - - - - - - - - - - - - #198F8F8F - - - - - - - - - - - - - - - diff --git a/Flow.Launcher/Themes/Win11Light.xaml b/Flow.Launcher/Themes/Win11Light.xaml index e6f376f8b34..4fe3c0495c4 100644 --- a/Flow.Launcher/Themes/Win11Light.xaml +++ b/Flow.Launcher/Themes/Win11Light.xaml @@ -1,72 +1,77 @@ + + - 0 0 0 8 + + + - + TargetType="{x:Type Window}" /> + - #198F8F8F - - - - + + + + + + + + 5 + 10 0 10 0 + 0 10 0 10 + diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 9b799e5824f..6c17e21f0d2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -19,8 +19,6 @@ using System.Text; using System.Threading.Channels; using ISavable = Flow.Launcher.Plugin.ISavable; -using System.IO; -using System.Collections.Specialized; using CommunityToolkit.Mvvm.Input; using System.Globalization; using System.Windows.Input; @@ -42,6 +40,7 @@ public partial class MainViewModel : BaseModel, ISavable private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; private readonly FlowLauncherJsonStorage _topMostRecordStorage; private readonly History _history; + private int lastHistoryIndex = 1; private readonly UserSelectedRecord _userSelectedRecord; private readonly TopMostRecord _topMostRecord; @@ -71,6 +70,21 @@ public MainViewModel(Settings settings) case nameof(Settings.WindowSize): OnPropertyChanged(nameof(MainWindowWidth)); break; + case nameof(Settings.WindowHeightSize): + OnPropertyChanged(nameof(MainWindowHeight)); + break; + case nameof(Settings.QueryBoxFontSize): + OnPropertyChanged(nameof(QueryBoxFontSize)); + break; + case nameof(Settings.ItemHeightSize): + OnPropertyChanged(nameof(ItemHeightSize)); + break; + case nameof(Settings.ResultItemFontSize): + OnPropertyChanged(nameof(ResultItemFontSize)); + break; + case nameof(Settings.ResultSubItemFontSize): + OnPropertyChanged(nameof(ResultSubItemFontSize)); + break; case nameof(Settings.AlwaysStartEn): OnPropertyChanged(nameof(StartWithEnglishMode)); break; @@ -80,6 +94,42 @@ public MainViewModel(Settings settings) case nameof(Settings.PreviewHotkey): OnPropertyChanged(nameof(PreviewHotkey)); break; + case nameof(Settings.AutoCompleteHotkey): + OnPropertyChanged(nameof(AutoCompleteHotkey)); + break; + case nameof(Settings.CycleHistoryUpHotkey): + OnPropertyChanged(nameof(CycleHistoryUpHotkey)); + break; + case nameof(Settings.CycleHistoryDownHotkey): + OnPropertyChanged(nameof(CycleHistoryDownHotkey)); + break; + case nameof(Settings.AutoCompleteHotkey2): + OnPropertyChanged(nameof(AutoCompleteHotkey2)); + break; + case nameof(Settings.SelectNextItemHotkey): + OnPropertyChanged(nameof(SelectNextItemHotkey)); + break; + case nameof(Settings.SelectNextItemHotkey2): + OnPropertyChanged(nameof(SelectNextItemHotkey2)); + break; + case nameof(Settings.SelectPrevItemHotkey): + OnPropertyChanged(nameof(SelectPrevItemHotkey)); + break; + case nameof(Settings.SelectPrevItemHotkey2): + OnPropertyChanged(nameof(SelectPrevItemHotkey2)); + break; + case nameof(Settings.SelectNextPageHotkey): + OnPropertyChanged(nameof(SelectNextPageHotkey)); + break; + case nameof(Settings.SelectPrevPageHotkey): + OnPropertyChanged(nameof(SelectPrevPageHotkey)); + break; + case nameof(Settings.OpenContextMenuHotkey): + OnPropertyChanged(nameof(OpenContextMenuHotkey)); + break; + case nameof(Settings.SettingWindowHotkey): + OnPropertyChanged(nameof(SettingWindowHotkey)); + break; } }; @@ -93,15 +143,21 @@ public MainViewModel(Settings settings) ContextMenu = new ResultsViewModel(Settings) { - LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview }; Results = new ResultsViewModel(Settings) { - LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview }; History = new ResultsViewModel(Settings) { - LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview }; _selectedResults = Results; @@ -226,6 +282,32 @@ public void ReQuery(bool reselect) } } + [RelayCommand] + public void ReverseHistory() + { + if (_history.Items.Count > 0) + { + ChangeQueryText(_history.Items[_history.Items.Count - lastHistoryIndex].Query.ToString()); + if (lastHistoryIndex < _history.Items.Count) + { + lastHistoryIndex++; + } + } + } + + [RelayCommand] + public void ForwardHistory() + { + if (_history.Items.Count > 0) + { + ChangeQueryText(_history.Items[_history.Items.Count - lastHistoryIndex].Query.ToString()); + if (lastHistoryIndex > 1) + { + lastHistoryIndex--; + } + } + } + [RelayCommand] private void LoadContextMenu() { @@ -316,6 +398,7 @@ private async Task OpenResultAsync(string index) { _userSelectedRecord.Add(result); _history.Add(result.OriginQuery.RawQuery); + lastHistoryIndex = 1; } if (hideWindow) @@ -324,6 +407,10 @@ private async Task OpenResultAsync(string index) } } + #endregion + + #region BasicCommands + [RelayCommand] private void OpenSetting() { @@ -342,6 +429,13 @@ private void SelectFirstResult() SelectedResults.SelectFirstResult(); } + [RelayCommand] + private void SelectLastResult() + { + SelectedResults.SelectLastResult(); + } + + [RelayCommand] private void SelectPrevPage() { @@ -357,7 +451,18 @@ private void SelectNextPage() [RelayCommand] private void SelectPrevItem() { - SelectedResults.SelectPrevResult(); + if (_history.Items.Count > 0 + && QueryText == string.Empty + && SelectedIsFromQueryResults()) + { + lastHistoryIndex = 1; + ReverseHistory(); + } + else + { + SelectedResults.SelectPrevResult(); + } + } [RelayCommand] @@ -384,7 +489,7 @@ public void ToggleGameMode() { GameModeStatus = !GameModeStatus; } - + [RelayCommand] public void CopyAlternative() { @@ -403,7 +508,6 @@ public void CopyAlternative() public Settings Settings { get; } public string ClockText { get; private set; } public string DateText { get; private set; } - public CultureInfo Culture => CultureInfo.DefaultThreadCurrentCulture; private async Task RegisterClockAndDateUpdateAsync() { @@ -412,9 +516,9 @@ private async Task RegisterClockAndDateUpdateAsync() while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { if (Settings.UseClock) - ClockText = DateTime.Now.ToString(Settings.TimeFormat, Culture); + ClockText = DateTime.Now.ToString(Settings.TimeFormat, CultureInfo.CurrentCulture); if (Settings.UseDate) - DateText = DateTime.Now.ToString(Settings.DateFormat, Culture); + DateText = DateTime.Now.ToString(Settings.DateFormat, CultureInfo.CurrentCulture); } } @@ -442,17 +546,9 @@ public string QueryText [RelayCommand] private void IncreaseWidth() { - if (MainWindowWidth + 100 > 1920 || Settings.WindowSize == 1920) - { - Settings.WindowSize = 1920; - } - else - { - Settings.WindowSize += 100; - Settings.WindowLeft -= 50; - } - - OnPropertyChanged(); + Settings.WindowSize += 100; + Settings.WindowLeft -= 50; + OnPropertyChanged(nameof(MainWindowWidth)); } [RelayCommand] @@ -468,7 +564,7 @@ private void DecreaseWidth() Settings.WindowSize -= 100; } - OnPropertyChanged(); + OnPropertyChanged(nameof(MainWindowWidth)); } [RelayCommand] @@ -489,52 +585,6 @@ private void DecreaseMaxResult() Settings.MaxResultsToShow -= 1; } - [RelayCommand] - public void TogglePreview() - { - if (!PreviewVisible) - { - ShowPreview(); - } - else - { - HidePreview(); - } - } - - private void ShowPreview() - { - ResultAreaColumn = 1; - PreviewVisible = true; - Results.SelectedItem?.LoadPreviewImage(); - } - - private void HidePreview() - { - ResultAreaColumn = 3; - PreviewVisible = false; - } - - public void ResetPreview() - { - if (Settings.AlwaysPreview == true) - { - ShowPreview(); - } - else - { - HidePreview(); - } - } - - private void UpdatePreview() - { - if (PreviewVisible) - { - Results.SelectedItem?.LoadPreviewImage(); - } - } - /// /// we need move cursor to end when we manually changed query /// but we don't want to move cursor to end when query is updated from TextBox @@ -574,12 +624,27 @@ private ResultsViewModel SelectedResults get { return _selectedResults; } set { + var isReturningFromContextMenu = ContextMenuSelected(); _selectedResults = value; if (SelectedIsFromQueryResults()) { ContextMenu.Visibility = Visibility.Collapsed; History.Visibility = Visibility.Collapsed; - ChangeQueryText(_queryTextBeforeLeaveResults); + + // QueryText setter (used in ChangeQueryText) runs the query again, resetting the selected + // result from the one that was selected before going into the context menu to the first result. + // The code below correctly restores QueryText and puts the text caret at the end without + // running the query again when returning from the context menu. + if (isReturningFromContextMenu) + { + _queryText = _queryTextBeforeLeaveResults; + OnPropertyChanged(nameof(QueryText)); + QueryTextCursorMovedToEnd = true; + } + else + { + ChangeQueryText(_queryTextBeforeLeaveResults); + } } else { @@ -620,40 +685,260 @@ private ResultsViewModel SelectedResults public double MainWindowWidth { get => Settings.WindowSize; - set => Settings.WindowSize = value; + set + { + if (!MainWindowVisibilityStatus) return; + Settings.WindowSize = value; + } + } + + public double MainWindowHeight + { + get => Settings.WindowHeightSize; + set => Settings.WindowHeightSize = value; + } + + public double QueryBoxFontSize + { + get => Settings.QueryBoxFontSize; + set => Settings.QueryBoxFontSize = value; + } + + public double ItemHeightSize + { + get => Settings.ItemHeightSize; + set => Settings.ItemHeightSize = value; + } + + public double ResultItemFontSize + { + get => Settings.ResultItemFontSize; + set => Settings.ResultItemFontSize = value; + } + + public double ResultSubItemFontSize + { + get => Settings.ResultSubItemFontSize; + set => Settings.ResultSubItemFontSize = value; } public string PluginIconPath { get; set; } = null; public string OpenResultCommandModifiers => Settings.OpenResultModifiers; - public string PreviewHotkey + public string VerifyOrSetDefaultHotkey(string hotkey, string defaultHotkey) { - get + try { - // TODO try to patch issue #1755 - // Added in v1.14.0, remove after v1.16.0. - try - { - var converter = new KeyGestureConverter(); - var key = (KeyGesture)converter.ConvertFromString(Settings.PreviewHotkey); - } - catch (Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException) - { - Settings.PreviewHotkey = "F1"; - } - - return Settings.PreviewHotkey; + var converter = new KeyGestureConverter(); + var key = (KeyGesture)converter.ConvertFromString(hotkey); + } + catch (Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException) + { + return defaultHotkey; } + + return hotkey; } + public string PreviewHotkey => VerifyOrSetDefaultHotkey(Settings.PreviewHotkey, "F1"); + public string AutoCompleteHotkey => VerifyOrSetDefaultHotkey(Settings.AutoCompleteHotkey, "Ctrl+Tab"); + public string AutoCompleteHotkey2 => VerifyOrSetDefaultHotkey(Settings.AutoCompleteHotkey2, ""); + public string SelectNextItemHotkey => VerifyOrSetDefaultHotkey(Settings.SelectNextItemHotkey, "Tab"); + public string SelectNextItemHotkey2 => VerifyOrSetDefaultHotkey(Settings.SelectNextItemHotkey2, ""); + public string SelectPrevItemHotkey => VerifyOrSetDefaultHotkey(Settings.SelectPrevItemHotkey, "Shift+Tab"); + public string SelectPrevItemHotkey2 => VerifyOrSetDefaultHotkey(Settings.SelectPrevItemHotkey2, ""); + public string SelectNextPageHotkey => VerifyOrSetDefaultHotkey(Settings.SelectNextPageHotkey, ""); + public string SelectPrevPageHotkey => VerifyOrSetDefaultHotkey(Settings.SelectPrevPageHotkey, ""); + public string OpenContextMenuHotkey => VerifyOrSetDefaultHotkey(Settings.OpenContextMenuHotkey, "Ctrl+O"); + public string SettingWindowHotkey => VerifyOrSetDefaultHotkey(Settings.SettingWindowHotkey, "Ctrl+I"); + public string CycleHistoryUpHotkey => VerifyOrSetDefaultHotkey(Settings.CycleHistoryUpHotkey, "Alt+Up"); + public string CycleHistoryDownHotkey => VerifyOrSetDefaultHotkey(Settings.CycleHistoryDownHotkey, "Alt+Down"); + + public string Image => Constant.QueryTextBoxIconImagePath; public bool StartWithEnglishMode => Settings.AlwaysStartEn; + + #endregion + + #region Preview + + public bool InternalPreviewVisible + { + get + { + if (ResultAreaColumn == ResultAreaColumnPreviewShown) + return true; + + if (ResultAreaColumn == ResultAreaColumnPreviewHidden) + return false; +#if DEBUG + throw new NotImplementedException("ResultAreaColumn should match ResultAreaColumnPreviewShown/ResultAreaColumnPreviewHidden value"); +#else + Log.Error("MainViewModel", "ResultAreaColumnPreviewHidden/ResultAreaColumnPreviewShown int value not implemented", "InternalPreviewVisible"); +#endif + return false; + } + } + + private static readonly int ResultAreaColumnPreviewShown = 1; - public bool PreviewVisible { get; set; } = false; + private static readonly int ResultAreaColumnPreviewHidden = 3; - public int ResultAreaColumn { get; set; } = 1; + public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown; + + // This is not a reliable indicator of whether external preview is visible due to the + // ability of manually closing/exiting the external preview program which, does not inform flow that + // preview is no longer available. + public bool ExternalPreviewVisible { get; set; } = false; + + private void ShowPreview() + { + var useExternalPreview = PluginManager.UseExternalPreview(); + + switch (useExternalPreview) + { + case true + when CanExternalPreviewSelectedResult(out var path): + // Internal preview may still be on when user switches to external + if (InternalPreviewVisible) + HideInternalPreview(); + OpenExternalPreview(path); + break; + + case true + when !CanExternalPreviewSelectedResult(out var _): + if (ExternalPreviewVisible) + CloseExternalPreview(); + ShowInternalPreview(); + break; + + case false: + ShowInternalPreview(); + break; + } + } + + private void HidePreview() + { + if (PluginManager.UseExternalPreview()) + CloseExternalPreview(); + + if (InternalPreviewVisible) + HideInternalPreview(); + } + + [RelayCommand] + private void TogglePreview() + { + if (InternalPreviewVisible || ExternalPreviewVisible) + { + HidePreview(); + } + else + { + ShowPreview(); + } + } + + private void ToggleInternalPreview() + { + if (!InternalPreviewVisible) + { + ShowInternalPreview(); + } + else + { + HideInternalPreview(); + } + } + + private void OpenExternalPreview(string path, bool sendFailToast = true) + { + _ = PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false); + ExternalPreviewVisible = true; + } + + private void CloseExternalPreview() + { + _ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false); + ExternalPreviewVisible = false; + } + + private void SwitchExternalPreview(string path, bool sendFailToast = true) + { + _ = PluginManager.SwitchExternalPreviewAsync(path,sendFailToast).ConfigureAwait(false); + } + + private void ShowInternalPreview() + { + ResultAreaColumn = ResultAreaColumnPreviewShown; + Results.SelectedItem?.LoadPreviewImage(); + } + + private void HideInternalPreview() + { + ResultAreaColumn = ResultAreaColumnPreviewHidden; + } + + public void ResetPreview() + { + switch (Settings.AlwaysPreview) + { + case true + when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path): + OpenExternalPreview(path); + break; + + case true: + ShowInternalPreview(); + break; + + case false: + HidePreview(); + break; + } + } + + private void UpdatePreview() + { + switch (PluginManager.UseExternalPreview()) + { + case true + when CanExternalPreviewSelectedResult(out var path): + if (ExternalPreviewVisible) + { + SwitchExternalPreview(path, false); + } + else if (InternalPreviewVisible) + { + HideInternalPreview(); + OpenExternalPreview(path); + } + break; + + case true + when !CanExternalPreviewSelectedResult(out var _): + if (ExternalPreviewVisible) + { + CloseExternalPreview(); + ShowInternalPreview(); + } + break; + + case false + when InternalPreviewVisible: + Results.SelectedItem?.LoadPreviewImage(); + break; + } + } + + private bool CanExternalPreviewSelectedResult(out string path) + { + path = Results.SelectedItem?.Result?.Preview.FilePath; + return !string.IsNullOrEmpty(path); + } #endregion @@ -1071,11 +1356,15 @@ public void Show() public async void Hide() { + lastHistoryIndex = 1; // Trick for no delay MainWindowOpacity = 0; lastContextMenuResult = new Result(); lastContextMenuResults = new List(); + if (ExternalPreviewVisible) + CloseExternalPreview(); + if (!SelectedIsFromQueryResults()) { SelectedResults = Results; diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs index bc98efabc86..38b5bec6561 100644 --- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -1,12 +1,17 @@ using System; +using System.Linq; +using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; +using SemanticVersioning; +using Version = SemanticVersioning.Version; namespace Flow.Launcher.ViewModel { - public class PluginStoreItemViewModel : BaseModel + public partial class PluginStoreItemViewModel : BaseModel { + private PluginPair PluginManagerData => PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"); public PluginStoreItemViewModel(UserPlugin plugin) { _plugin = plugin; @@ -26,19 +31,13 @@ public PluginStoreItemViewModel(UserPlugin plugin) public string IcoPath => _plugin.IcoPath; public bool LabelInstalled => PluginManager.GetPluginForId(_plugin.ID) != null; - public bool LabelUpdate => LabelInstalled && VersionConvertor(_plugin.Version) > VersionConvertor(PluginManager.GetPluginForId(_plugin.ID).Metadata.Version); + public bool LabelUpdate => LabelInstalled && new Version(_plugin.Version) > new Version(PluginManager.GetPluginForId(_plugin.ID).Metadata.Version); internal const string None = "None"; internal const string RecentlyUpdated = "RecentlyUpdated"; internal const string NewRelease = "NewRelease"; internal const string Installed = "Installed"; - public Version VersionConvertor(string version) - { - Version ResultVersion = new Version(version); - return ResultVersion; - } - public string Category { get @@ -60,5 +59,13 @@ public string Category return category; } } + + [RelayCommand] + private void ShowCommandQuery(string action) + { + var actionKeyword = PluginManagerData.Metadata.ActionKeywords.Any() ? PluginManagerData.Metadata.ActionKeywords[0] + " " : String.Empty; + App.API.ChangeQuery($"{actionKeyword}{action} {_plugin.Name}"); + App.API.ShowMainWindow(); + } } } diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index d2919507db0..4ce8bd4706f 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -1,4 +1,5 @@ -using System.Windows; +using System.Linq; +using System.Windows; using System.Windows.Media; using Flow.Launcher.Plugin; using Flow.Launcher.Infrastructure.Image; @@ -6,6 +7,7 @@ using System.Windows.Controls; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Resource; +using Flow.Launcher.Resources.Controls; namespace Flow.Launcher.ViewModel { @@ -26,6 +28,21 @@ public PluginPair PluginPair } } + private string PluginManagerActionKeyword + { + get + { + var keyword = PluginManager + .GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7") + .Metadata.ActionKeywords.FirstOrDefault(); + return keyword switch + { + null or "*" => string.Empty, + _ => keyword + }; + } + } + private async void LoadIconAsync() { @@ -46,7 +63,11 @@ public ImageSource Image public bool PluginState { get => !PluginPair.Metadata.Disabled; - set => PluginPair.Metadata.Disabled = !value; + set + { + PluginPair.Metadata.Disabled = !value; + PluginSettingsObject.Disabled = !value; + } } public bool IsExpanded { @@ -62,11 +83,19 @@ public bool IsExpanded private Control _settingControl; private bool _isExpanded; + + private Control _bottomPart1; + public Control BottomPart1 => IsExpanded ? _bottomPart1 ??= new InstalledPluginDisplayKeyword() : null; + + private Control _bottomPart2; + public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; + + public bool HasSettingControl => PluginPair.Plugin is ISettingProvider; public Control SettingControl => IsExpanded ? _settingControl ??= PluginPair.Plugin is not ISettingProvider settingProvider - ? new Control() + ? null : settingProvider.CreateSettingPanel() : null; private ImageSource _image = ImageLoader.MissingImage; @@ -78,6 +107,7 @@ public Control SettingControl public string InitAndQueryTime => InternationalizationManager.Instance.GetTranslation("plugin_init_time") + " " + PluginPair.Metadata.InitTime + "ms, " + InternationalizationManager.Instance.GetTranslation("plugin_query_time") + " " + PluginPair.Metadata.AvgQueryTime + "ms"; public string ActionKeywordsText => string.Join(Query.ActionKeywordSeparator, PluginPair.Metadata.ActionKeywords); public int Priority => PluginPair.Metadata.Priority; + public Infrastructure.UserSettings.Plugin PluginSettingsObject { get; set; } public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword) { @@ -88,13 +118,14 @@ public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword public void ChangePriority(int newPriority) { PluginPair.Metadata.Priority = newPriority; + PluginSettingsObject.Priority = newPriority; OnPropertyChanged(nameof(Priority)); } [RelayCommand] private void EditPluginPriority() { - PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(PluginPair.Metadata.ID, this); + PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(PluginPair. Metadata.ID, this); priorityChangeWindow.ShowDialog(); } @@ -106,6 +137,19 @@ private void OpenPluginDirectory() PluginManager.API.OpenDirectory(directory); } + [RelayCommand] + private void OpenSourceCodeLink() + { + PluginManager.API.OpenUrl(PluginPair.Metadata.Website); + } + + [RelayCommand] + private void OpenDeletePluginWindow() + { + PluginManager.API.ChangeQuery($"{PluginManagerActionKeyword} uninstall {PluginPair.Metadata.Name}".Trim(), true); + PluginManager.API.ShowMainWindow(); + } + public static bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword); [RelayCommand] diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 3f204c16cc6..5130e7ebafc 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -29,7 +29,7 @@ public ResultViewModel(Result result, Settings settings) if (Result.Glyph is { FontFamily: not null } glyph) { - // Checks if it's a system installed font, which does not require path to be provided. + // Checks if it's a system installed font, which does not require path to be provided. if (glyph.FontFamily.EndsWith(".ttf") || glyph.FontFamily.EndsWith(".otf")) { string fontFamilyPath = glyph.FontFamily; @@ -64,7 +64,7 @@ public ResultViewModel(Result result, Settings settings) } - private Settings Settings { get; } + public Settings Settings { get; } public Visibility ShowOpenResultHotkey => Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Collapsed; diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index d02dc9bd582..107372e0115 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,4 +1,5 @@ -using Flow.Launcher.Infrastructure.UserSettings; +using System; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using System.Collections.Generic; using System.Collections.Specialized; @@ -32,9 +33,15 @@ public ResultsViewModel(Settings settings) : this() _settings = settings; _settings.PropertyChanged += (s, e) => { - if (e.PropertyName == nameof(_settings.MaxResultsToShow)) + switch (e.PropertyName) { - OnPropertyChanged(nameof(MaxHeight)); + case nameof(_settings.MaxResultsToShow): + OnPropertyChanged(nameof(MaxHeight)); + break; + case nameof(_settings.ItemHeightSize): + OnPropertyChanged(nameof(ItemHeightSize)); + OnPropertyChanged(nameof(MaxHeight)); + break; } }; } @@ -43,14 +50,37 @@ public ResultsViewModel(Settings settings) : this() #region Properties - public double MaxHeight => MaxResults * (double)Application.Current.FindResource("ResultItemHeight")!; + public bool IsPreviewOn { get; set; } + + public double MaxHeight + { + get + { + var newResultsCount = MaxResults; + if (IsPreviewOn) + { + newResultsCount = (int)Math.Ceiling(380 / _settings.ItemHeightSize); + if (newResultsCount < MaxResults) + { + newResultsCount = MaxResults; + } + } + return newResultsCount * _settings.ItemHeightSize; + } + } + + public double ItemHeightSize + { + get => _settings.ItemHeightSize; + set => _settings.ItemHeightSize = value; + } public int SelectedIndex { get; set; } public ResultViewModel SelectedItem { get; set; } public Thickness Margin { get; set; } public Visibility Visibility { get; set; } = Visibility.Collapsed; - + public ICommand RightClickResultCommand { get; init; } public ICommand LeftClickResultCommand { get; init; } @@ -117,6 +147,11 @@ public void SelectFirstResult() SelectedIndex = NewIndex(0); } + public void SelectLastResult() + { + SelectedIndex = NewIndex(Results.Count - 1); + } + public void Clear() { lock (_collectionLock) diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index 86a25ffb118..04dd6312b37 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -1,997 +1,66 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using Flow.Launcher.Core; +using Flow.Launcher.Core; using Flow.Launcher.Core.Configuration; -using Flow.Launcher.Core.ExternalPlugins; -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; -using Flow.Launcher.Helper; -using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using Flow.Launcher.Plugin.SharedModels; -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.Input; -using System.Globalization; -namespace Flow.Launcher.ViewModel -{ - public partial class SettingWindowViewModel : BaseModel - { - private readonly Updater _updater; - private readonly IPortable _portable; - private readonly FlowLauncherJsonStorage _storage; - - public SettingWindowViewModel(Updater updater, IPortable portable) - { - _updater = updater; - _portable = portable; - _storage = new FlowLauncherJsonStorage(); - Settings = _storage.Load(); - Settings.PropertyChanged += (s, e) => - { - switch (e.PropertyName) - { - case nameof(Settings.ActivateTimes): - OnPropertyChanged(nameof(ActivatedTimes)); - break; - case nameof(Settings.WindowSize): - OnPropertyChanged(nameof(WindowWidthSize)); - break; - case nameof(Settings.UseDate): - case nameof(Settings.DateFormat): - OnPropertyChanged(nameof(DateText)); - break; - case nameof(Settings.UseClock): - case nameof(Settings.TimeFormat): - OnPropertyChanged(nameof(ClockText)); - break; - case nameof(Settings.Language): - OnPropertyChanged(nameof(ClockText)); - OnPropertyChanged(nameof(DateText)); - OnPropertyChanged(nameof(AlwaysPreviewToolTip)); - break; - case nameof(Settings.PreviewHotkey): - OnPropertyChanged(nameof(AlwaysPreviewToolTip)); - break; - case nameof(Settings.SoundVolume): - OnPropertyChanged(nameof(SoundEffectVolume)); - break; - } - }; - - } - - public Settings Settings { get; set; } - - public async void UpdateApp() - { - await _updater.UpdateAppAsync(App.API, false); - } - - public bool AutoUpdates - { - get => Settings.AutoUpdates; - set - { - Settings.AutoUpdates = value; - - if (value) - { - UpdateApp(); - } - } - } - - public CultureInfo Culture => CultureInfo.DefaultThreadCurrentCulture; - - public bool StartFlowLauncherOnSystemStartup - { - get => Settings.StartFlowLauncherOnSystemStartup; - set - { - Settings.StartFlowLauncherOnSystemStartup = value; - - try - { - if (value) - AutoStartup.Enable(); - else - AutoStartup.Disable(); - } - catch (Exception e) - { - Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message); - } - } - } - - // This is only required to set at startup. When portable mode enabled/disabled a restart is always required - private bool _portableMode = DataLocation.PortableDataLocationInUse(); - public bool PortableMode - { - get => _portableMode; - set - { - if (!_portable.CanUpdatePortability()) - return; - - if (DataLocation.PortableDataLocationInUse()) - { - _portable.DisablePortableMode(); - } - else - { - _portable.EnablePortableMode(); - } - } - } - - /// - /// Save Flow settings. Plugins settings are not included. - /// - public void Save() - { - foreach (var vm in PluginViewModels) - { - var id = vm.PluginPair.Metadata.ID; - - Settings.PluginSettings.Plugins[id].Disabled = vm.PluginPair.Metadata.Disabled; - Settings.PluginSettings.Plugins[id].Priority = vm.Priority; - } - - _storage.Save(); - } - - public string GetFileFromDialog(string title, string filter = "") - { - var dlg = new System.Windows.Forms.OpenFileDialog - { - InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), - Multiselect = false, - CheckFileExists = true, - CheckPathExists = true, - Title = title, - Filter = filter - }; - - var result = dlg.ShowDialog(); - if (result == System.Windows.Forms.DialogResult.OK) - { - return dlg.FileName; - } - else - { - return string.Empty; - } - } - - #region general - - // todo a better name? - public class LastQueryMode : BaseModel - { - public string Display { get; set; } - public Infrastructure.UserSettings.LastQueryMode Value { get; set; } - } - - private List _lastQueryModes = new List(); - public List LastQueryModes - { - get - { - if (_lastQueryModes.Count == 0) - { - _lastQueryModes = InitLastQueryModes(); - } - return _lastQueryModes; - } - } - - private List InitLastQueryModes() - { - var modes = new List(); - var enums = (Infrastructure.UserSettings.LastQueryMode[])Enum.GetValues(typeof(Infrastructure.UserSettings.LastQueryMode)); - foreach (var e in enums) - { - var key = $"LastQuery{e}"; - var display = _translater.GetTranslation(key); - var m = new LastQueryMode - { - Display = display, Value = e, - }; - modes.Add(m); - } - return modes; - } - - private void UpdateLastQueryModeDisplay() - { - foreach (var item in LastQueryModes) - { - item.Display = _translater.GetTranslation($"LastQuery{item.Value}"); - } - } - - public string Language - { - get - { - return Settings.Language; - } - set - { - InternationalizationManager.Instance.ChangeLanguage(value); - - if (InternationalizationManager.Instance.PromptShouldUsePinyin(value)) - ShouldUsePinyin = true; - - UpdateLastQueryModeDisplay(); - } - } - - public bool ShouldUsePinyin - { - get - { - return Settings.ShouldUsePinyin; - } - set - { - Settings.ShouldUsePinyin = value; - } - } - - public List QuerySearchPrecisionStrings - { - get - { - var precisionStrings = new List(); - - var enumList = Enum.GetValues(typeof(SearchPrecisionScore)).Cast().ToList(); - - enumList.ForEach(x => precisionStrings.Add(x.ToString())); - - return precisionStrings; - } - } - - public List OpenResultModifiersList => new List - { - KeyConstant.Alt, - KeyConstant.Ctrl, - $"{KeyConstant.Ctrl}+{KeyConstant.Alt}" - }; - private Internationalization _translater => InternationalizationManager.Instance; - public List Languages => _translater.LoadAvailableLanguages(); - public IEnumerable MaxResultsRange => Enumerable.Range(2, 16); - - public string AlwaysPreviewToolTip => string.Format(_translater.GetTranslation("AlwaysPreviewToolTip"), Settings.PreviewHotkey); - - public string TestProxy() - { - var proxyServer = Settings.Proxy.Server; - var proxyUserName = Settings.Proxy.UserName; - if (string.IsNullOrEmpty(proxyServer)) - { - return InternationalizationManager.Instance.GetTranslation("serverCantBeEmpty"); - } - if (Settings.Proxy.Port <= 0) - { - return InternationalizationManager.Instance.GetTranslation("portCantBeEmpty"); - } - - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository); - - if (string.IsNullOrEmpty(proxyUserName) || string.IsNullOrEmpty(Settings.Proxy.Password)) - { - request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port); - } - else - { - request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port) - { - Credentials = new NetworkCredential(proxyUserName, Settings.Proxy.Password) - }; - } - try - { - var response = (HttpWebResponse)request.GetResponse(); - if (response.StatusCode == HttpStatusCode.OK) - { - return InternationalizationManager.Instance.GetTranslation("proxyIsCorrect"); - } - else - { - return InternationalizationManager.Instance.GetTranslation("proxyConnectFailed"); - } - } - catch - { - return InternationalizationManager.Instance.GetTranslation("proxyConnectFailed"); - } - } - - #endregion - - #region plugin - - public static string Plugin => @"https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest"; - public PluginViewModel SelectedPlugin { get; set; } - - public IList PluginViewModels - { - get => PluginManager.AllPlugins - .OrderBy(x => x.Metadata.Disabled) - .ThenBy(y => y.Metadata.Name) - .Select(p => new PluginViewModel - { - PluginPair = p - }) - .ToList(); - } - - public IList ExternalPlugins - { - get - { - return LabelMaker(PluginsManifest.UserPlugins); - } - } - - private IList LabelMaker(IList list) - { - return list.Select(p => new PluginStoreItemViewModel(p)) - .OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease) - .ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated) - .ThenByDescending(p => p.Category == PluginStoreItemViewModel.None) - .ThenByDescending(p => p.Category == PluginStoreItemViewModel.Installed) - .ToList(); - } - - public Control SettingProvider - { - get - { - var settingProvider = SelectedPlugin.PluginPair.Plugin as ISettingProvider; - if (settingProvider != null) - { - var control = settingProvider.CreateSettingPanel(); - control.HorizontalAlignment = HorizontalAlignment.Stretch; - control.VerticalAlignment = VerticalAlignment.Stretch; - return control; - } - else - { - return new Control(); - } - } - } - - [RelayCommand] - private async Task RefreshExternalPluginsAsync() - { - await PluginsManifest.UpdateManifestAsync(); - OnPropertyChanged(nameof(ExternalPlugins)); - } - - - - internal void DisplayPluginQuery(string queryToDisplay, PluginPair plugin, int actionKeywordPosition = 0) - { - var actionKeyword = plugin.Metadata.ActionKeywords.Count == 0 - ? string.Empty - : plugin.Metadata.ActionKeywords[actionKeywordPosition]; - - App.API.ChangeQuery($"{actionKeyword} {queryToDisplay}"); - App.API.ShowMainWindow(); - } - - #endregion - - #region theme - - public static string Theme => @"https://flowlauncher.com/docs/#/how-to-create-a-theme"; - public static string ThemeGallery => @"https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438"; - - public string SelectedTheme - { - get { return Settings.Theme; } - set - { - ThemeManager.Instance.ChangeTheme(value); - - if (ThemeManager.Instance.BlurEnabled && Settings.UseDropShadowEffect) - DropShadowEffect = false; - } - } - - public List Themes - => ThemeManager.Instance.LoadAvailableThemes().Select(Path.GetFileNameWithoutExtension).ToList(); - - public bool DropShadowEffect - { - get { return Settings.UseDropShadowEffect; } - set - { - if (ThemeManager.Instance.BlurEnabled && value) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed")); - return; - } - - if (value) - { - ThemeManager.Instance.AddDropShadowEffectToCurrentTheme(); - } - else - { - ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme(); - } - - Settings.UseDropShadowEffect = value; - } - } - - public class ColorScheme - { - public string Display { get; set; } - public ColorSchemes Value { get; set; } - } - - public List ColorSchemes - { - get - { - List modes = new List(); - var enums = (ColorSchemes[])Enum.GetValues(typeof(ColorSchemes)); - foreach (var e in enums) - { - var key = $"ColorScheme{e}"; - var display = _translater.GetTranslation(key); - var m = new ColorScheme - { - Display = display, Value = e, - }; - modes.Add(m); - } - return modes; - } - } - - public class SearchWindowScreen - { - public string Display { get; set; } - public SearchWindowScreens Value { get; set; } - } - - public List SearchWindowScreens - { - get - { - List modes = new List(); - var enums = (SearchWindowScreens[])Enum.GetValues(typeof(SearchWindowScreens)); - foreach (var e in enums) - { - var key = $"SearchWindowScreen{e}"; - var display = _translater.GetTranslation(key); - var m = new SearchWindowScreen - { - Display = display, - Value = e, - }; - modes.Add(m); - } - return modes; - } - } +namespace Flow.Launcher.ViewModel; - public class SearchWindowAlign - { - public string Display { get; set; } - public SearchWindowAligns Value { get; set; } - } - - public List SearchWindowAligns - { - get - { - List modes = new List(); - var enums = (SearchWindowAligns[])Enum.GetValues(typeof(SearchWindowAligns)); - foreach (var e in enums) - { - var key = $"SearchWindowAlign{e}"; - var display = _translater.GetTranslation(key); - var m = new SearchWindowAlign - { - Display = display, Value = e, - }; - modes.Add(m); - } - return modes; - } - } - - public List ScreenNumbers - { - get - { - var screens = System.Windows.Forms.Screen.AllScreens; - var screenNumbers = new List(); - for (int i = 1; i <= screens.Length; i++) - { - screenNumbers.Add(i); - } - return screenNumbers; - } - } - - public List TimeFormatList { get; } = new() - { - "h:mm", - "hh:mm", - "H:mm", - "HH:mm", - "tt h:mm", - "tt hh:mm", - "h:mm tt", - "hh:mm tt", - "hh:mm:ss tt", - "HH:mm:ss" - }; - - public List DateFormatList { get; } = new() - { - "MM'/'dd dddd", - "MM'/'dd ddd", - "MM'/'dd", - "MM'-'dd", - "MMMM', 'dd", - "dd'/'MM", - "dd'-'MM", - "ddd MM'/'dd", - "dddd MM'/'dd", - "dddd", - "ddd dd'/'MM", - "dddd dd'/'MM", - "dddd dd', 'MMMM", - "dd', 'MMMM" - }; - - public string TimeFormat - { - get => Settings.TimeFormat; - set => Settings.TimeFormat = value; - } - - public string DateFormat - { - get => Settings.DateFormat; - set => Settings.DateFormat = value; - } - - public string ClockText => DateTime.Now.ToString(TimeFormat, Culture); - - public string DateText => DateTime.Now.ToString(DateFormat, Culture); - - public double WindowWidthSize - { - get => Settings.WindowSize; - set => Settings.WindowSize = value; - } - - public bool UseGlyphIcons - { - get => Settings.UseGlyphIcons; - set => Settings.UseGlyphIcons = value; - } - - public bool UseAnimation - { - get => Settings.UseAnimation; - set => Settings.UseAnimation = value; - } - - public class AnimationSpeed - { - public string Display { get; set; } - public AnimationSpeeds Value { get; set; } - } - - public List AnimationSpeeds - { - get - { - List speeds = new List(); - var enums = (AnimationSpeeds[])Enum.GetValues(typeof(AnimationSpeeds)); - foreach (var e in enums) - { - var key = $"AnimationSpeed{e}"; - var display = _translater.GetTranslation(key); - var m = new AnimationSpeed - { - Display = display, - Value = e, - }; - speeds.Add(m); - } - return speeds; - } - } - - public bool UseSound - { - get => Settings.UseSound; - set => Settings.UseSound = value; - } - - public double SoundEffectVolume - { - get => Settings.SoundVolume; - set => Settings.SoundVolume = value; - } - - public bool UseClock - { - get => Settings.UseClock; - set => Settings.UseClock = value; - } - - public bool UseDate - { - get => Settings.UseDate; - set => Settings.UseDate = value; - } - - public double SettingWindowWidth - { - get => Settings.SettingWindowWidth; - set => Settings.SettingWindowWidth = value; - } - - public double SettingWindowHeight - { - get => Settings.SettingWindowHeight; - set => Settings.SettingWindowHeight = value; - } - - public double SettingWindowTop - { - get => Settings.SettingWindowTop; - set => Settings.SettingWindowTop = value; - } - - public double SettingWindowLeft - { - get => Settings.SettingWindowLeft; - set => Settings.SettingWindowLeft = value; - } - - public Brush PreviewBackground - { - get - { - var wallpaper = WallpaperPathRetrieval.GetWallpaperPath(); - if (wallpaper != null && File.Exists(wallpaper)) - { - var memStream = new MemoryStream(File.ReadAllBytes(wallpaper)); - var bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.StreamSource = memStream; - bitmap.DecodePixelWidth = 800; - bitmap.DecodePixelHeight = 600; - bitmap.EndInit(); - var brush = new ImageBrush(bitmap) - { - Stretch = Stretch.UniformToFill - }; - return brush; - } - else - { - var wallpaperColor = WallpaperPathRetrieval.GetWallpaperColor(); - var brush = new SolidColorBrush(wallpaperColor); - return brush; - } - } - } - - public ResultsViewModel PreviewResults - { - get - { - var results = new List - { - new Result - { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png") - }, - new Result - { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png") - }, - new Result - { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png") - }, - new Result - { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png") - } - }; - var vm = new ResultsViewModel(Settings); - vm.AddResults(results, "PREVIEW"); - return vm; - } - } - - public FontFamily SelectedQueryBoxFont - { - get - { - if (Fonts.SystemFontFamilies.Count(o => - o.FamilyNames.Values != null && - o.FamilyNames.Values.Contains(Settings.QueryBoxFont)) > 0) - { - var font = new FontFamily(Settings.QueryBoxFont); - return font; - } - else - { - var font = new FontFamily("Segoe UI"); - return font; - } - } - set - { - Settings.QueryBoxFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); - } - } - - public FamilyTypeface SelectedQueryBoxFontFaces - { - get - { - var typeface = SyntaxSugars.CallOrRescueDefault( - () => SelectedQueryBoxFont.ConvertFromInvariantStringsOrNormal( - Settings.QueryBoxFontStyle, - Settings.QueryBoxFontWeight, - Settings.QueryBoxFontStretch - )); - return typeface; - } - set - { - Settings.QueryBoxFontStretch = value.Stretch.ToString(); - Settings.QueryBoxFontWeight = value.Weight.ToString(); - Settings.QueryBoxFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); - } - } - - public FontFamily SelectedResultFont - { - get - { - if (Fonts.SystemFontFamilies.Count(o => - o.FamilyNames.Values != null && - o.FamilyNames.Values.Contains(Settings.ResultFont)) > 0) - { - var font = new FontFamily(Settings.ResultFont); - return font; - } - else - { - var font = new FontFamily("Segoe UI"); - return font; - } - } - set - { - Settings.ResultFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); - } - } - - public FamilyTypeface SelectedResultFontFaces - { - get - { - var typeface = SyntaxSugars.CallOrRescueDefault( - () => SelectedResultFont.ConvertFromInvariantStringsOrNormal( - Settings.ResultFontStyle, - Settings.ResultFontWeight, - Settings.ResultFontStretch - )); - return typeface; - } - set - { - Settings.ResultFontStretch = value.Stretch.ToString(); - Settings.ResultFontWeight = value.Weight.ToString(); - Settings.ResultFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); - } - } - - public string ThemeImage => Constant.QueryTextBoxIconImagePath; - - #endregion - - #region hotkey - - public CustomPluginHotkey SelectedCustomPluginHotkey { get; set; } - - #endregion - - #region shortcut - - public ObservableCollection CustomShortcuts => Settings.CustomShortcuts; - - public ObservableCollection BuiltinShortcuts => Settings.BuiltinShortcuts; - - public CustomShortcutModel? SelectedCustomShortcut { get; set; } - - public void DeleteSelectedCustomShortcut() - { - var item = SelectedCustomShortcut; - if (item == null) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); - return; - } - - string deleteWarning = string.Format( - InternationalizationManager.Instance.GetTranslation("deleteCustomShortcutWarning"), - item.Key, item.Value); - if (MessageBox.Show(deleteWarning, InternationalizationManager.Instance.GetTranslation("delete"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) - { - Settings.CustomShortcuts.Remove(item); - } - } - - public bool EditSelectedCustomShortcut() - { - var item = SelectedCustomShortcut; - if (item == null) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); - return false; - } - - var shortcutSettingWindow = new CustomShortcutSetting(item.Key, item.Value, this); - if (shortcutSettingWindow.ShowDialog() == true) - { - // Fix un-selectable shortcut item after the first selection - // https://stackoverflow.com/questions/16789360/wpf-listbox-items-with-changing-hashcode - SelectedCustomShortcut = null; - item.Key = shortcutSettingWindow.Key; - item.Value = shortcutSettingWindow.Value; - SelectedCustomShortcut = item; - return true; - } - return false; - } - - public void AddCustomShortcut() - { - var shortcutSettingWindow = new CustomShortcutSetting(this); - if (shortcutSettingWindow.ShowDialog() == true) - { - var shortcut = new CustomShortcutModel(shortcutSettingWindow.Key, shortcutSettingWindow.Value); - Settings.CustomShortcuts.Add(shortcut); - } - } - - public bool ShortcutExists(string key) - { - return Settings.CustomShortcuts.Any(x => x.Key == key) || Settings.BuiltinShortcuts.Any(x => x.Key == key); - } - - #endregion - - #region about +public class SettingWindowViewModel : BaseModel +{ + private readonly FlowLauncherJsonStorage _storage; - public string Website => Constant.Website; - public string SponsorPage => Constant.SponsorPage; - public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest"; - public string Documentation => Constant.Documentation; - public string Docs => Constant.Docs; - public string Github => Constant.GitHub; - public string Version - { - get - { - if (Constant.Version == "1.0.0") - { - return Constant.Dev; - } - else - { - return Constant.Version; - } - } - } - public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); + public Updater Updater { get; } - public string CheckLogFolder - { - get - { - var logFiles = GetLogFiles(); - long size = logFiles.Sum(file => file.Length); - return string.Format("{0} ({1})", _translater.GetTranslation("clearlogfolder"), BytesToReadableString(size)); - } - } + public IPortable Portable { get; } - private static DirectoryInfo GetLogDir(string version = "") - { - return new DirectoryInfo(Path.Combine(DataLocation.DataDirectory(), Constant.Logs, version)); - } + public Settings Settings { get; } - private static List GetLogFiles(string version = "") - { - return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList(); - } + public SettingWindowViewModel(Updater updater, IPortable portable) + { + _storage = new FlowLauncherJsonStorage(); - internal void ClearLogFolder() - { - var logDirectory = GetLogDir(); - var logFiles = GetLogFiles(); + Updater = updater; + Portable = portable; + Settings = _storage.Load(); + } - logFiles.ForEach(f => f.Delete()); + public async void UpdateApp() + { + await Updater.UpdateAppAsync(App.API, false); + } - logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly) - .Where(dir => !Constant.Version.Equals(dir.Name)) - .ToList() - .ForEach(dir => dir.Delete()); - OnPropertyChanged(nameof(CheckLogFolder)); - } - internal void OpenLogFolder() - { - App.API.OpenDirectory(GetLogDir(Constant.Version).FullName); - } + /// + /// Save Flow settings. Plugins settings are not included. + /// + public void Save() + { + _storage.Save(); + } - internal static string BytesToReadableString(long bytes) - { - const int scale = 1024; - string[] orders = new string[] - { - "GB", "MB", "KB", "B" - }; - long max = (long)Math.Pow(scale, orders.Length - 1); + public double SettingWindowWidth + { + get => Settings.SettingWindowWidth; + set => Settings.SettingWindowWidth = value; + } - foreach (string order in orders) - { - if (bytes > max) - return string.Format("{0:##.##} {1}", decimal.Divide(bytes, max), order); + public double SettingWindowHeight + { + get => Settings.SettingWindowHeight; + set => Settings.SettingWindowHeight = value; + } - max /= scale; - } - return "0 B"; - } + public double? SettingWindowTop + { + get => Settings.SettingWindowTop; + set => Settings.SettingWindowTop = value; + } - #endregion + public double? SettingWindowLeft + { + get => Settings.SettingWindowLeft; + set => Settings.SettingWindowLeft = value; } } diff --git a/Flow.Launcher/WelcomeWindow.xaml b/Flow.Launcher/WelcomeWindow.xaml index c7820d436bd..003dac5bcd8 100644 --- a/Flow.Launcher/WelcomeWindow.xaml +++ b/Flow.Launcher/WelcomeWindow.xaml @@ -10,6 +10,10 @@ Title="{DynamicResource Welcome_Page1_Title}" Width="550" Height="650" + MinWidth="550" + MinHeight="650" + MaxWidth="550" + MaxHeight="650" Activated="OnActivated" Background="{DynamicResource Color00B}" Foreground="{DynamicResource PopupTextColor}" diff --git a/Flow.Launcher/app.manifest b/Flow.Launcher/app.manifest index 52d1c39327b..c45508fcf03 100644 --- a/Flow.Launcher/app.manifest +++ b/Flow.Launcher/app.manifest @@ -49,13 +49,12 @@ DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. --> - diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs index 09755fe0cb8..65757b80253 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs @@ -3,23 +3,22 @@ using System.Collections.Generic; using System.IO; -namespace Flow.Launcher.Plugin.BrowserBookmark +namespace Flow.Launcher.Plugin.BrowserBookmark; + +public class ChromeBookmarkLoader : ChromiumBookmarkLoader { - public class ChromeBookmarkLoader : ChromiumBookmarkLoader + public override List GetBookmarks() { - public override List GetBookmarks() - { - return LoadChromeBookmarks(); - } + return LoadChromeBookmarks(); + } - private List LoadChromeBookmarks() - { - var bookmarks = new List(); - var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome\User Data"), "Google Chrome")); - bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome SxS\User Data"), "Google Chrome Canary")); - bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Chromium\User Data"), "Chromium")); - return bookmarks; - } + private List LoadChromeBookmarks() + { + var bookmarks = new List(); + var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome\User Data"), "Google Chrome")); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome SxS\User Data"), "Google Chrome Canary")); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Chromium\User Data"), "Chromium")); + return bookmarks; } -} \ No newline at end of file +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs index 8ce597b30e1..48acf61090b 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs @@ -4,92 +4,91 @@ using System.Text.Json; using Flow.Launcher.Infrastructure.Logger; -namespace Flow.Launcher.Plugin.BrowserBookmark +namespace Flow.Launcher.Plugin.BrowserBookmark; + +public abstract class ChromiumBookmarkLoader : IBookmarkLoader { - public abstract class ChromiumBookmarkLoader : IBookmarkLoader + public abstract List GetBookmarks(); + + protected List LoadBookmarks(string browserDataPath, string name) { - public abstract List GetBookmarks(); + var bookmarks = new List(); + if (!Directory.Exists(browserDataPath)) return bookmarks; + var paths = Directory.GetDirectories(browserDataPath); - protected List LoadBookmarks(string browserDataPath, string name) + foreach (var profile in paths) { - var bookmarks = new List(); - if (!Directory.Exists(browserDataPath)) return bookmarks; - var paths = Directory.GetDirectories(browserDataPath); - - foreach (var profile in paths) - { - var bookmarkPath = Path.Combine(profile, "Bookmarks"); - if (!File.Exists(bookmarkPath)) - continue; - - Main.RegisterBookmarkFile(bookmarkPath); + var bookmarkPath = Path.Combine(profile, "Bookmarks"); + if (!File.Exists(bookmarkPath)) + continue; - var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})"); - bookmarks.AddRange(LoadBookmarksFromFile(bookmarkPath, source)); - } + Main.RegisterBookmarkFile(bookmarkPath); - return bookmarks; + var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})"); + bookmarks.AddRange(LoadBookmarksFromFile(bookmarkPath, source)); } - protected List LoadBookmarksFromFile(string path, string source) - { - var bookmarks = new List(); + return bookmarks; + } - if (!File.Exists(path)) - return bookmarks; + protected List LoadBookmarksFromFile(string path, string source) + { + var bookmarks = new List(); - using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path)); - if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement)) - return bookmarks; - EnumerateRoot(rootElement, bookmarks, source); + if (!File.Exists(path)) return bookmarks; - } - private void EnumerateRoot(JsonElement rootElement, ICollection bookmarks, string source) + using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path)); + if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement)) + return bookmarks; + EnumerateRoot(rootElement, bookmarks, source); + return bookmarks; + } + + private void EnumerateRoot(JsonElement rootElement, ICollection bookmarks, string source) + { + foreach (var folder in rootElement.EnumerateObject()) { - foreach (var folder in rootElement.EnumerateObject()) - { - if (folder.Value.ValueKind != JsonValueKind.Object) - continue; + if (folder.Value.ValueKind != JsonValueKind.Object) + continue; - // Fix for Opera. It stores bookmarks slightly different than chrome. See PR and bug report for this change for details. - // If various exceptions start to build up here consider splitting this Loader into multiple separate ones. - if (folder.Name == "custom_root") - EnumerateRoot(folder.Value, bookmarks, source); - else - EnumerateFolderBookmark(folder.Value, bookmarks, source); - } + // Fix for Opera. It stores bookmarks slightly different than chrome. See PR and bug report for this change for details. + // If various exceptions start to build up here consider splitting this Loader into multiple separate ones. + if (folder.Name == "custom_root") + EnumerateRoot(folder.Value, bookmarks, source); + else + EnumerateFolderBookmark(folder.Value, bookmarks, source); } + } - private void EnumerateFolderBookmark(JsonElement folderElement, ICollection bookmarks, - string source) + private void EnumerateFolderBookmark(JsonElement folderElement, ICollection bookmarks, + string source) + { + if (!folderElement.TryGetProperty("children", out var childrenElement)) + return; + foreach (var subElement in childrenElement.EnumerateArray()) { - if (!folderElement.TryGetProperty("children", out var childrenElement)) - return; - foreach (var subElement in childrenElement.EnumerateArray()) + if (subElement.TryGetProperty("type", out var type)) { - if (subElement.TryGetProperty("type", out var type)) - { - switch (type.GetString()) - { - case "folder": - case "workspace": // Edge Workspace - EnumerateFolderBookmark(subElement, bookmarks, source); - break; - default: - bookmarks.Add(new Bookmark( - subElement.GetProperty("name").GetString(), - subElement.GetProperty("url").GetString(), - source)); - break; - } - } - else + switch (type.GetString()) { - Log.Error( - $"ChromiumBookmarkLoader: EnumerateFolderBookmark: type property not found for {subElement.GetString()}"); + case "folder": + case "workspace": // Edge Workspace + EnumerateFolderBookmark(subElement, bookmarks, source); + break; + default: + bookmarks.Add(new Bookmark( + subElement.GetProperty("name").GetString(), + subElement.GetProperty("url").GetString(), + source)); + break; } } + else + { + Log.Error( + $"ChromiumBookmarkLoader: EnumerateFolderBookmark: type property not found for {subElement.GetString()}"); + } } } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs index d08c05b6ba1..3468015ebaf 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs @@ -4,56 +4,55 @@ using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.SharedModels; -namespace Flow.Launcher.Plugin.BrowserBookmark.Commands +namespace Flow.Launcher.Plugin.BrowserBookmark.Commands; + +internal static class BookmarkLoader { - internal static class BookmarkLoader + internal static MatchResult MatchProgram(Bookmark bookmark, string queryString) { - internal static MatchResult MatchProgram(Bookmark bookmark, string queryString) - { - var match = StringMatcher.FuzzySearch(queryString, bookmark.Name); - if (match.IsSearchPrecisionScoreMet()) - return match; + var match = StringMatcher.FuzzySearch(queryString, bookmark.Name); + if (match.IsSearchPrecisionScoreMet()) + return match; - return StringMatcher.FuzzySearch(queryString, bookmark.Url); - } + return StringMatcher.FuzzySearch(queryString, bookmark.Url); + } - internal static List LoadAllBookmarks(Settings setting) - { - var allBookmarks = new List(); + internal static List LoadAllBookmarks(Settings setting) + { + var allBookmarks = new List(); - if (setting.LoadChromeBookmark) - { - // Add Chrome bookmarks - var chromeBookmarks = new ChromeBookmarkLoader(); - allBookmarks.AddRange(chromeBookmarks.GetBookmarks()); - } + if (setting.LoadChromeBookmark) + { + // Add Chrome bookmarks + var chromeBookmarks = new ChromeBookmarkLoader(); + allBookmarks.AddRange(chromeBookmarks.GetBookmarks()); + } - if (setting.LoadFirefoxBookmark) - { - // Add Firefox bookmarks - var mozBookmarks = new FirefoxBookmarkLoader(); - allBookmarks.AddRange(mozBookmarks.GetBookmarks()); - } + if (setting.LoadFirefoxBookmark) + { + // Add Firefox bookmarks + var mozBookmarks = new FirefoxBookmarkLoader(); + allBookmarks.AddRange(mozBookmarks.GetBookmarks()); + } - if (setting.LoadEdgeBookmark) - { - // Add Edge (Chromium) bookmarks - var edgeBookmarks = new EdgeBookmarkLoader(); - allBookmarks.AddRange(edgeBookmarks.GetBookmarks()); - } + if (setting.LoadEdgeBookmark) + { + // Add Edge (Chromium) bookmarks + var edgeBookmarks = new EdgeBookmarkLoader(); + allBookmarks.AddRange(edgeBookmarks.GetBookmarks()); + } - foreach (var browser in setting.CustomChromiumBrowsers) + foreach (var browser in setting.CustomChromiumBrowsers) + { + IBookmarkLoader loader = browser.BrowserType switch { - IBookmarkLoader loader = browser.BrowserType switch - { - BrowserType.Chromium => new CustomChromiumBookmarkLoader(browser), - BrowserType.Firefox => new CustomFirefoxBookmarkLoader(browser), - _ => new CustomChromiumBookmarkLoader(browser), - }; - allBookmarks.AddRange(loader.GetBookmarks()); - } - - return allBookmarks.Distinct().ToList(); + BrowserType.Chromium => new CustomChromiumBookmarkLoader(browser), + BrowserType.Firefox => new CustomFirefoxBookmarkLoader(browser), + _ => new CustomChromiumBookmarkLoader(browser), + }; + allBookmarks.AddRange(loader.GetBookmarks()); } + + return allBookmarks.Distinct().ToList(); } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs index fa98f4d7c98..005c83992bf 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs @@ -1,19 +1,18 @@ using Flow.Launcher.Plugin.BrowserBookmark.Models; using System.Collections.Generic; -namespace Flow.Launcher.Plugin.BrowserBookmark +namespace Flow.Launcher.Plugin.BrowserBookmark; + +public class CustomChromiumBookmarkLoader : ChromiumBookmarkLoader { - public class CustomChromiumBookmarkLoader : ChromiumBookmarkLoader + public CustomChromiumBookmarkLoader(CustomBrowser browser) { - public CustomChromiumBookmarkLoader(CustomBrowser browser) - { - BrowserName = browser.Name; - BrowserDataPath = browser.DataDirectoryPath; - } - public string BrowserDataPath { get; init; } - public string BookmarkFilePath { get; init; } - public string BrowserName { get; init; } - - public override List GetBookmarks() => BrowserDataPath != null ? LoadBookmarks(BrowserDataPath, BrowserName) : LoadBookmarksFromFile(BookmarkFilePath, BrowserName); + BrowserName = browser.Name; + BrowserDataPath = browser.DataDirectoryPath; } -} \ No newline at end of file + public string BrowserDataPath { get; init; } + public string BookmarkFilePath { get; init; } + public string BrowserName { get; init; } + + public override List GetBookmarks() => BrowserDataPath != null ? LoadBookmarks(BrowserDataPath, BrowserName) : LoadBookmarksFromFile(BookmarkFilePath, BrowserName); +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomFirefoxBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomFirefoxBookmarkLoader.cs index 82bdc29f54d..d0bb7b0cc71 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomFirefoxBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomFirefoxBookmarkLoader.cs @@ -2,26 +2,25 @@ using System.IO; using Flow.Launcher.Plugin.BrowserBookmark.Models; -namespace Flow.Launcher.Plugin.BrowserBookmark +namespace Flow.Launcher.Plugin.BrowserBookmark; + +public class CustomFirefoxBookmarkLoader : FirefoxBookmarkLoaderBase { - public class CustomFirefoxBookmarkLoader : FirefoxBookmarkLoaderBase + public CustomFirefoxBookmarkLoader(CustomBrowser browser) { - public CustomFirefoxBookmarkLoader(CustomBrowser browser) - { - BrowserName = browser.Name; - BrowserDataPath = browser.DataDirectoryPath; - } - - /// - /// Path to places.sqlite - /// - public string BrowserDataPath { get; init; } - - public string BrowserName { get; init; } + BrowserName = browser.Name; + BrowserDataPath = browser.DataDirectoryPath; + } + + /// + /// Path to places.sqlite + /// + public string BrowserDataPath { get; init; } - public override List GetBookmarks() - { - return GetBookmarksFromPath(Path.Combine(BrowserDataPath, "places.sqlite")); - } + public string BrowserName { get; init; } + + public override List GetBookmarks() + { + return GetBookmarksFromPath(Path.Combine(BrowserDataPath, "places.sqlite")); } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs index 79190f0ef29..40123b022e1 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs @@ -3,21 +3,20 @@ using System.Collections.Generic; using System.IO; -namespace Flow.Launcher.Plugin.BrowserBookmark +namespace Flow.Launcher.Plugin.BrowserBookmark; + +public class EdgeBookmarkLoader : ChromiumBookmarkLoader { - public class EdgeBookmarkLoader : ChromiumBookmarkLoader + private List LoadEdgeBookmarks() { - private List LoadEdgeBookmarks() - { - var bookmarks = new List(); - var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge\User Data"), "Microsoft Edge")); - bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge Dev\User Data"), "Microsoft Edge Dev")); - bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge SxS\User Data"), "Microsoft Edge Canary")); + var bookmarks = new List(); + var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge\User Data"), "Microsoft Edge")); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge Dev\User Data"), "Microsoft Edge Dev")); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge SxS\User Data"), "Microsoft Edge Canary")); - return bookmarks; - } - - public override List GetBookmarks() => LoadEdgeBookmarks(); + return bookmarks; } -} \ No newline at end of file + + public override List GetBookmarks() => LoadEdgeBookmarks(); +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs index 3d061e75846..35ad32fb3d4 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs @@ -5,140 +5,133 @@ using System.IO; using System.Linq; -namespace Flow.Launcher.Plugin.BrowserBookmark +namespace Flow.Launcher.Plugin.BrowserBookmark; + +public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader { - public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader - { - public abstract List GetBookmarks(); + public abstract List GetBookmarks(); - private const string queryAllBookmarks = @"SELECT moz_places.url, moz_bookmarks.title - FROM moz_places - INNER JOIN moz_bookmarks ON ( + private const string QueryAllBookmarks = """ + SELECT moz_places.url, moz_bookmarks.title + FROM moz_places + INNER JOIN moz_bookmarks ON ( moz_bookmarks.fk NOT NULL AND moz_bookmarks.title NOT NULL AND moz_bookmarks.fk = moz_places.id - ) - ORDER BY moz_places.visit_count DESC - "; + ) + ORDER BY moz_places.visit_count DESC + """; - private const string dbPathFormat = "Data Source ={0}"; + private const string DbPathFormat = "Data Source ={0}"; - protected static List GetBookmarksFromPath(string placesPath) - { - // Return empty list if the places.sqlite file cannot be found - if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath)) - return new List(); - - var bookmarkList = new List(); - - Main.RegisterBookmarkFile(placesPath); - - // create the connection string and init the connection - string dbPath = string.Format(dbPathFormat, placesPath); - using var dbConnection = new SqliteConnection(dbPath); - // Open connection to the database file and execute the query - dbConnection.Open(); - var reader = new SqliteCommand(queryAllBookmarks, dbConnection).ExecuteReader(); - - // return results in List format - bookmarkList = reader.Select( - x => new Bookmark(x["title"] is DBNull ? string.Empty : x["title"].ToString(), - x["url"].ToString()) - ).ToList(); - - return bookmarkList; - } + protected static List GetBookmarksFromPath(string placesPath) + { + // Return empty list if the places.sqlite file cannot be found + if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath)) + return new List(); + + Main.RegisterBookmarkFile(placesPath); + + // create the connection string and init the connection + string dbPath = string.Format(DbPathFormat, placesPath); + using var dbConnection = new SqliteConnection(dbPath); + // Open connection to the database file and execute the query + dbConnection.Open(); + var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader(); + + // return results in List format + return reader + .Select( + x => new Bookmark( + x["title"] is DBNull ? string.Empty : x["title"].ToString(), + x["url"].ToString() + ) + ) + .ToList(); } +} + +public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase +{ + /// + /// Searches the places.sqlite db and returns all bookmarks + /// + public override List GetBookmarks() + { + return GetBookmarksFromPath(PlacesPath); + } - public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase + /// + /// Path to places.sqlite + /// + private string PlacesPath { - /// - /// Searches the places.sqlite db and returns all bookmarks - /// - public override List GetBookmarks() + get { - return GetBookmarksFromPath(PlacesPath); - } - - /// - /// Path to places.sqlite - /// - private string PlacesPath - { - get - { - var profileFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Mozilla\Firefox"); - var profileIni = Path.Combine(profileFolderPath, @"profiles.ini"); - - if (!File.Exists(profileIni)) - return string.Empty; - - // get firefox default profile directory from profiles.ini - string ini; - using (var sReader = new StreamReader(profileIni)) - { - ini = sReader.ReadToEnd(); - } - - /* - Current profiles.ini structure example as of Firefox version 69.0.1 - - [Install736426B0AF4A39CB] - Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile - Locked=1 - - [Profile2] - Name=newblahprofile - IsRelative=0 - Path=C:\t6h2yuq8.newblahprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code. - - [Profile1] - Name=default - IsRelative=1 - Path=Profiles/cydum7q4.default - Default=1 - - [Profile0] - Name=default-release - IsRelative=1 - Path=Profiles/7789f565.default-release - - [General] - StartWithLastProfile=1 - Version=2 - */ - - var lines = ini.Split(new string[] - { - "\r\n" - }, StringSplitOptions.None).ToList(); - - var defaultProfileFolderNameRaw = lines.Where(x => x.Contains("Default=") && x != "Default=1").FirstOrDefault() ?? string.Empty; - - if (string.IsNullOrEmpty(defaultProfileFolderNameRaw)) - return string.Empty; - - var defaultProfileFolderName = defaultProfileFolderNameRaw.Split('=').Last(); - - var indexOfDefaultProfileAtttributePath = lines.IndexOf("Path=" + defaultProfileFolderName); - - // Seen in the example above, the IsRelative attribute is always above the Path attribute - var relativeAttribute = lines[indexOfDefaultProfileAtttributePath - 1]; - - return relativeAttribute == "0" // See above, the profile is located in a custom location, path is not relative, so IsRelative=0 - ? defaultProfileFolderName + @"\places.sqlite" - : Path.Combine(profileFolderPath, defaultProfileFolderName) + @"\places.sqlite"; - } + var profileFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Mozilla\Firefox"); + var profileIni = Path.Combine(profileFolderPath, @"profiles.ini"); + + if (!File.Exists(profileIni)) + return string.Empty; + + // get firefox default profile directory from profiles.ini + using var sReader = new StreamReader(profileIni); + var ini = sReader.ReadToEnd(); + + /* + Current profiles.ini structure example as of Firefox version 69.0.1 + + [Install736426B0AF4A39CB] + Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile + Locked=1 + + [Profile2] + Name=newblahprofile + IsRelative=0 + Path=C:\t6h2yuq8.newblahprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code. + + [Profile1] + Name=default + IsRelative=1 + Path=Profiles/cydum7q4.default + Default=1 + + [Profile0] + Name=default-release + IsRelative=1 + Path=Profiles/7789f565.default-release + + [General] + StartWithLastProfile=1 + Version=2 + */ + var lines = ini.Split("\r\n").ToList(); + + var defaultProfileFolderNameRaw = lines.FirstOrDefault(x => x.Contains("Default=") && x != "Default=1") ?? string.Empty; + + if (string.IsNullOrEmpty(defaultProfileFolderNameRaw)) + return string.Empty; + + var defaultProfileFolderName = defaultProfileFolderNameRaw.Split('=').Last(); + + var indexOfDefaultProfileAttributePath = lines.IndexOf("Path=" + defaultProfileFolderName); + + // Seen in the example above, the IsRelative attribute is always above the Path attribute + var relativeAttribute = lines[indexOfDefaultProfileAttributePath - 1]; + + return relativeAttribute == "0" // See above, the profile is located in a custom location, path is not relative, so IsRelative=0 + ? defaultProfileFolderName + @"\places.sqlite" + : Path.Combine(profileFolderPath, defaultProfileFolderName) + @"\places.sqlite"; } } +} - public static class Extensions +public static class Extensions +{ + public static IEnumerable Select(this SqliteDataReader reader, Func projection) { - public static IEnumerable Select(this SqliteDataReader reader, Func projection) + while (reader.Read()) { - while (reader.Read()) - { - yield return projection(reader); - } + yield return projection(reader); } } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index fe118c2c3bf..e49d1d5f5c0 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -12,6 +12,7 @@ false false true + en @@ -35,6 +36,44 @@ false + + + + + + + + PreserveNewest @@ -56,7 +95,7 @@ - + - \ No newline at end of file + diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs index 2c48cfd557e..8a972735275 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs @@ -1,10 +1,9 @@ using Flow.Launcher.Plugin.BrowserBookmark.Models; using System.Collections.Generic; -namespace Flow.Launcher.Plugin.BrowserBookmark +namespace Flow.Launcher.Plugin.BrowserBookmark; + +public interface IBookmarkLoader { - public interface IBookmarkLoader - { - public List GetBookmarks(); - } -} \ No newline at end of file + public List GetBookmarks(); +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ar.xaml index 90f4ea49bd2..bde48c7d4af 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ar.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ar.xaml @@ -2,27 +2,27 @@ - Browser Bookmarks - Search your browser bookmarks + إشارات المتصفح + ابحث في إشارات المتصفح - Bookmark Data - Open bookmarks in: - New window - New tab - Set browser from path: - Choose - Copy url - Copy the bookmark's url to clipboard - Load Browser From: - Browser Name - Data Directory Path - Add - Edit - Delete - Browse - Others - Browser Engine - If you are not using Chrome, Firefox or Edge, or you are using their portable version, you need to add bookmarks data directory and select correct browser engine to make this plugin work. - For example: Brave's engine is Chromium; and its default bookmarks data location is: "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData". For Firefox engine, the bookmarks directory is the userdata folder contains the places.sqlite file. + بيانات الإشارات المرجعية + فتح الإشارات المرجعية في: + نافذة جديدة + علامة تبويب جديدة + تحديد المتصفح من المسار: + اختر + نسخ الرابط + نسخ رابط الإشارة المرجعية إلى الحافظة + تحميل المتصفح من: + اسم المتصفح + مسار دليل البيانات + إضافة إشارة مرجعية + تعديل إشارة مرجعية + حذف إشارة مرجعية + تصفح + أخرى + محرك المتصفح + إذا كنت لا تستخدم Chrome أو Firefox أو Edge، أو كنت تستخدم نسختهم المحمولة، ستحتاج إلى إضافة دليل بيانات الإشارات المرجعية وتحديد محرك المتصفح الصحيح لجعل هذه الإضافة تعمل. + على سبيل المثال: محرك Brave هو Chromium؛ وموقع بيانات الإشارات المرجعية الافتراضي هو: "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData". بالنسبة لمحرك Firefox، دليل الإشارات المرجعية هو مجلد userdata الذي يحتوي على ملف places.sqlite. diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es.xaml index 3bbf5001748..b80e4f926b5 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/es.xaml @@ -6,7 +6,7 @@ Busca en los marcadores del navegador - Datos de marcador + Datos del marcador Abrir marcadores en: Nueva ventana Nueva pestaña diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ko.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ko.xaml index 8010e56f4fa..93916ffaa01 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ko.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/ko.xaml @@ -14,7 +14,7 @@ 선택 Copy url Copy the bookmark's url to clipboard - Load Browser From: + 데이터를 가져올 브라우저: 브라우저 이름 데이터 디렉토리 위치 추가 @@ -22,7 +22,7 @@ 삭제 찾아보기 Others - Browser Engine - If you are not using Chrome, Firefox or Edge, or you are using their portable version, you need to add bookmarks data directory and select correct browser engine to make this plugin work. - For example: Brave's engine is Chromium; and its default bookmarks data location is: "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData". For Firefox engine, the bookmarks directory is the userdata folder contains the places.sqlite file. + 브라우저 엔진 + 크롬, 파이어폭스 또는 엣지를 사용하지 않거나 이들의 포터블 버전을 사용하고 있다면, 이 플러그인을 작동시키기 위해 북마크 데이터 디렉토리를 추가하고 올바른 브라우저 엔진을 선택해야 합니다. + 예를 들어: Brave의 엔진은 Chromium이며, 기본 북마크 데이터 위치는 "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData"입니다. Firefox 엔진의 경우, 북마크 위치는 places.sqlite 파일이 포함된 userdata 폴더입니다. diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/tr.xaml index 05e97f2c050..76ab141111a 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/tr.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/tr.xaml @@ -2,27 +2,27 @@ - Yer İşaretleri - Tarayıcılarınızdaki yer işaretlerini arayın. + Yer İmleri + Tarayıcınızdaki yer işaretlerini arayın Yer İmleri Verisi - Open bookmarks in: + Yer imlerini şurada aç: Yeni Pencere Yeni Sekme Dizin üzerinden tarayıcı seç: Seç Url Kopyala - Copy the bookmark's url to clipboard - Load Browser From: - Browser Name - Data Directory Path + Yer imi bağlantısını kopyala + Taraycılardan İçe Aktar: + Tarayıcı Adı + Veri Dizini Ekle Düzenle Sil Gözat - Others - Browser Engine - If you are not using Chrome, Firefox or Edge, or you are using their portable version, you need to add bookmarks data directory and select correct browser engine to make this plugin work. - For example: Brave's engine is Chromium; and its default bookmarks data location is: "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData". For Firefox engine, the bookmarks directory is the userdata folder contains the places.sqlite file. + Diğerleri + Tarayıcı Motoru + Eğer Chrome, Firefox veya Edge kullanmıyor veya bu tarayıcıların taşınabilir sürümlerini kullanıyorsanız tarayıcı motorunu ve yer imlerinin saklandığı dizini elle girmeniz gerekir. + Örneğin: Brave tarayıcısı Chromium tabanlıdır ve yer imleri varsayılan olarak "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData" klasöründe saklanır. Firefox tabanlı tarayıcılar için bu places.sqlite dosyasının bulunduğu kullanıcı verisi klasörüdür. diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/vi.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/vi.xaml new file mode 100644 index 00000000000..ddc10950e7a --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/vi.xaml @@ -0,0 +1,28 @@ + + + + + Dấu trang trình duyệt + Tìm kiếm dấu trang trình duyệt của bạn + + + Dữ liệu đánh dấu + Mở dấu trang trong: + Cửa sổ mới + Thêm Tab mới + Đặt trình duyệt từ đường dẫn: + Chọn + Sao chép url + Sao chép url của dấu trang vào clipboard + Tải trình duyệt từ: + Tên trình duyệt + Đường dẫn thư mục dữ liệu + Thêm + Sửa + Xóa + Duyệt + Khác + Công cụ trình duyệt + Nếu bạn không sử dụng Chrome, Firefox hoặc Edge hoặc bạn đang sử dụng phiên bản di động của chúng, bạn cần thêm thư mục dữ liệu dấu trang và chọn đúng công cụ trình duyệt để plugin này hoạt động. + Ví dụ: Engine của Brave là Chrome; và vị trí dữ liệu dấu trang mặc định của nó là: "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData". Đối với công cụ Firefox, thư mục dấu trang là thư mục dữ liệu người dùng chứa tệp địa điểm.sqlite. + diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index a13d6c929fe..a48d70f2d46 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Windows; using System.Windows.Controls; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin.BrowserBookmark.Commands; @@ -12,233 +11,234 @@ using System.Threading.Tasks; using System.Threading; -namespace Flow.Launcher.Plugin.BrowserBookmark +namespace Flow.Launcher.Plugin.BrowserBookmark; + +public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable { - public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable - { - private static PluginInitContext context; + private static PluginInitContext _context; - private static List cachedBookmarks = new List(); + private static List _cachedBookmarks = new List(); - private static Settings _settings; + private static Settings _settings; - private static bool initialized = false; + private static bool _initialized = false; - public void Init(PluginInitContext context) - { - Main.context = context; + public void Init(PluginInitContext context) + { + _context = context; - _settings = context.API.LoadSettingJsonStorage(); + _settings = context.API.LoadSettingJsonStorage(); - LoadBookmarksIfEnabled(); - } + LoadBookmarksIfEnabled(); + } - private static void LoadBookmarksIfEnabled() + private static void LoadBookmarksIfEnabled() + { + if (_context.CurrentPluginMetadata.Disabled) { - if (context.CurrentPluginMetadata.Disabled) - { - // Don't load or monitor files if disabled - return; - } - - cachedBookmarks = BookmarkLoader.LoadAllBookmarks(_settings); - _ = MonitorRefreshQueueAsync(); - initialized = true; + // Don't load or monitor files if disabled + return; } - public List Query(Query query) + _cachedBookmarks = BookmarkLoader.LoadAllBookmarks(_settings); + _ = MonitorRefreshQueueAsync(); + _initialized = true; + } + + public List Query(Query query) + { + // For when the plugin being previously disabled and is now re-enabled + if (!_initialized) { - // For when the plugin being previously disabled and is now renabled - if (!initialized) - { - LoadBookmarksIfEnabled(); - } + LoadBookmarksIfEnabled(); + } - string param = query.Search.TrimStart(); + string param = query.Search.TrimStart(); - // Should top results be returned? (true if no search parameters have been passed) - var topResults = string.IsNullOrEmpty(param); + // Should top results be returned? (true if no search parameters have been passed) + var topResults = string.IsNullOrEmpty(param); - if (!topResults) - { - // Since we mixed chrome and firefox bookmarks, we should order them again - var returnList = cachedBookmarks.Select(c => new Result() - { - Title = c.Name, - SubTitle = c.Url, - IcoPath = @"Images\bookmark.png", - Score = BookmarkLoader.MatchProgram(c, param).Score, - Action = _ => + if (!topResults) + { + // Since we mixed chrome and firefox bookmarks, we should order them again + return _cachedBookmarks + .Select( + c => new Result { - context.API.OpenUrl(c.Url); + Title = c.Name, + SubTitle = c.Url, + IcoPath = @"Images\bookmark.png", + Score = BookmarkLoader.MatchProgram(c, param).Score, + Action = _ => + { + _context.API.OpenUrl(c.Url); - return true; - }, - ContextData = new BookmarkAttributes - { - Url = c.Url + return true; + }, + ContextData = new BookmarkAttributes { Url = c.Url } } - }).Where(r => r.Score > 0); - return returnList.ToList(); - } - else - { - return cachedBookmarks.Select(c => new Result() - { - Title = c.Name, - SubTitle = c.Url, - IcoPath = @"Images\bookmark.png", - Score = 5, - Action = _ => - { - context.API.OpenUrl(c.Url); - return true; - }, - ContextData = new BookmarkAttributes + ) + .Where(r => r.Score > 0) + .ToList(); + } + else + { + return _cachedBookmarks + .Select( + c => new Result { - Url = c.Url + Title = c.Name, + SubTitle = c.Url, + IcoPath = @"Images\bookmark.png", + Score = 5, + Action = _ => + { + _context.API.OpenUrl(c.Url); + return true; + }, + ContextData = new BookmarkAttributes { Url = c.Url } } - }).ToList(); - } + ) + .ToList(); } + } - private static Channel refreshQueue = Channel.CreateBounded(1); + private static Channel _refreshQueue = Channel.CreateBounded(1); - private static SemaphoreSlim fileMonitorSemaphore = new(1, 1); + private static SemaphoreSlim _fileMonitorSemaphore = new(1, 1); - private static async Task MonitorRefreshQueueAsync() + private static async Task MonitorRefreshQueueAsync() + { + if (_fileMonitorSemaphore.CurrentCount < 1) { - if (fileMonitorSemaphore.CurrentCount < 1) - { - return; - } - await fileMonitorSemaphore.WaitAsync(); - var reader = refreshQueue.Reader; - while (await reader.WaitToReadAsync()) - { - if (reader.TryRead(out _)) - { - ReloadAllBookmarks(false); - } - } - fileMonitorSemaphore.Release(); + return; } - - private static readonly List Watchers = new(); - - internal static void RegisterBookmarkFile(string path) + await _fileMonitorSemaphore.WaitAsync(); + var reader = _refreshQueue.Reader; + while (await reader.WaitToReadAsync()) { - var directory = Path.GetDirectoryName(path); - if (!Directory.Exists(directory) || !File.Exists(path)) + if (reader.TryRead(out _)) { - return; + ReloadAllBookmarks(false); } - if (Watchers.Any(x => x.Path.Equals(directory, StringComparison.OrdinalIgnoreCase))) - { - return; - } - - var watcher = new FileSystemWatcher(directory!); - watcher.Filter = Path.GetFileName(path); - - watcher.NotifyFilter = NotifyFilters.FileName | - NotifyFilters.LastWrite | - NotifyFilters.Size; - - watcher.Changed += static (_, _) => - { - refreshQueue.Writer.TryWrite(default); - }; - - watcher.Renamed += static (_, _) => - { - refreshQueue.Writer.TryWrite(default); - }; - - watcher.EnableRaisingEvents = true; - - Watchers.Add(watcher); } + _fileMonitorSemaphore.Release(); + } - public void ReloadData() - { - ReloadAllBookmarks(); - } + private static readonly List Watchers = new(); - public static void ReloadAllBookmarks(bool disposeFileWatchers = true) + internal static void RegisterBookmarkFile(string path) + { + var directory = Path.GetDirectoryName(path); + if (!Directory.Exists(directory) || !File.Exists(path)) { - cachedBookmarks.Clear(); - if (disposeFileWatchers) - DisposeFileWatchers(); - LoadBookmarksIfEnabled(); + return; } - - public string GetTranslatedPluginTitle() + if (Watchers.Any(x => x.Path.Equals(directory, StringComparison.OrdinalIgnoreCase))) { - return context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_name"); + return; } - public string GetTranslatedPluginDescription() + var watcher = new FileSystemWatcher(directory!); + watcher.Filter = Path.GetFileName(path); + + watcher.NotifyFilter = NotifyFilters.FileName | + NotifyFilters.LastWrite | + NotifyFilters.Size; + + watcher.Changed += static (_, _) => { - return context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_description"); - } + _refreshQueue.Writer.TryWrite(default); + }; - public Control CreateSettingPanel() + watcher.Renamed += static (_, _) => { - return new SettingsControl(_settings); - } + _refreshQueue.Writer.TryWrite(default); + }; - public List LoadContextMenus(Result selectedResult) + watcher.EnableRaisingEvents = true; + + Watchers.Add(watcher); + } + + public void ReloadData() + { + ReloadAllBookmarks(); + } + + public static void ReloadAllBookmarks(bool disposeFileWatchers = true) + { + _cachedBookmarks.Clear(); + if (disposeFileWatchers) + DisposeFileWatchers(); + LoadBookmarksIfEnabled(); + } + + public string GetTranslatedPluginTitle() + { + return _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_name"); + } + + public string GetTranslatedPluginDescription() + { + return _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_description"); + } + + public Control CreateSettingPanel() + { + return new SettingsControl(_settings); + } + + public List LoadContextMenus(Result selectedResult) + { + return new List() { - return new List() + new Result { - new Result + Title = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"), + SubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"), + Action = _ => { - Title = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"), - SubTitle = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"), - Action = _ => + try { - try - { - context.API.CopyToClipboard(((BookmarkAttributes)selectedResult.ContextData).Url); + _context.API.CopyToClipboard(((BookmarkAttributes)selectedResult.ContextData).Url); - return true; - } - catch (Exception e) - { - var message = "Failed to set url in clipboard"; - Log.Exception("Main", message, e, "LoadContextMenus"); - - context.API.ShowMsg(message); - - return false; - } - }, - IcoPath = "Images\\copylink.png", - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8c8") - } - }; - } + return true; + } + catch (Exception e) + { + var message = "Failed to set url in clipboard"; + Log.Exception("Main", message, e, "LoadContextMenus"); - internal class BookmarkAttributes - { - internal string Url { get; set; } - } + _context.API.ShowMsg(message); - public void Dispose() - { - DisposeFileWatchers(); - } + return false; + } + }, + IcoPath = @"Images\copylink.png", + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8c8") + } + }; + } + + internal class BookmarkAttributes + { + internal string Url { get; set; } + } - private static void DisposeFileWatchers() + public void Dispose() + { + DisposeFileWatchers(); + } + + private static void DisposeFileWatchers() + { + foreach (var watcher in Watchers) { - foreach (var watcher in Watchers) - { - watcher.Dispose(); - } - Watchers.Clear(); + watcher.Dispose(); } + Watchers.Clear(); } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs index c2fa9d9775a..c738da389c5 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs @@ -1,22 +1,21 @@ using System.Collections.Generic; -namespace Flow.Launcher.Plugin.BrowserBookmark.Models +namespace Flow.Launcher.Plugin.BrowserBookmark.Models; + +// Source may be important in the future +public record Bookmark(string Name, string Url, string Source = "") { - // Source may be important in the future - public record Bookmark(string Name, string Url, string Source = "") + public override int GetHashCode() { - public override int GetHashCode() - { - var hashName = Name?.GetHashCode() ?? 0; - var hashUrl = Url?.GetHashCode() ?? 0; - return hashName ^ hashUrl; - } - - public virtual bool Equals(Bookmark other) - { - return other != null && Name == other.Name && Url == other.Url; - } + var hashName = Name?.GetHashCode() ?? 0; + var hashUrl = Url?.GetHashCode() ?? 0; + return hashName ^ hashUrl; + } - public List CustomBrowsers { get; set; }= new(); + public virtual bool Equals(Bookmark other) + { + return other != null && Name == other.Name && Url == other.Url; } -} \ No newline at end of file + + public List CustomBrowsers { get; set; } = new(); +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs index 69bb56e4879..74e0f299aff 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs @@ -1,45 +1,44 @@ -namespace Flow.Launcher.Plugin.BrowserBookmark.Models +namespace Flow.Launcher.Plugin.BrowserBookmark.Models; + +public class CustomBrowser : BaseModel { - public class CustomBrowser : BaseModel - { - private string _name; - private string _dataDirectoryPath; - private BrowserType browserType = BrowserType.Chromium; + private string _name; + private string _dataDirectoryPath; + private BrowserType _browserType = BrowserType.Chromium; - public string Name - { - get => _name; - set - { - _name = value; - OnPropertyChanged(nameof(Name)); - } - } - - public string DataDirectoryPath + public string Name + { + get => _name; + set { - get => _dataDirectoryPath; - set - { - _dataDirectoryPath = value; - OnPropertyChanged(nameof(DataDirectoryPath)); - } + _name = value; + OnPropertyChanged(); } - - public BrowserType BrowserType + } + + public string DataDirectoryPath + { + get => _dataDirectoryPath; + set { - get => browserType; - set - { - browserType = value; - OnPropertyChanged(nameof(BrowserType)); - } + _dataDirectoryPath = value; + OnPropertyChanged(); } } - public enum BrowserType + public BrowserType BrowserType { - Chromium, - Firefox, + get => _browserType; + set + { + _browserType = value; + OnPropertyChanged(); + } } } + +public enum BrowserType +{ + Chromium, + Firefox, +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs index dc1016b4edf..86532d27598 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs @@ -1,17 +1,16 @@ using System.Collections.ObjectModel; -namespace Flow.Launcher.Plugin.BrowserBookmark.Models +namespace Flow.Launcher.Plugin.BrowserBookmark.Models; + +public class Settings : BaseModel { - public class Settings : BaseModel - { - public bool OpenInNewBrowserWindow { get; set; } = true; + public bool OpenInNewBrowserWindow { get; set; } = true; - public string BrowserPath { get; set; } + public string BrowserPath { get; set; } - public bool LoadChromeBookmark { get; set; } = true; - public bool LoadFirefoxBookmark { get; set; } = true; - public bool LoadEdgeBookmark { get; set; } = true; + public bool LoadChromeBookmark { get; set; } = true; + public bool LoadFirefoxBookmark { get; set; } = true; + public bool LoadEdgeBookmark { get; set; } = true; - public ObservableCollection CustomChromiumBrowsers { get; set; } = new(); - } -} \ No newline at end of file + public ObservableCollection CustomChromiumBrowsers { get; set; } = new(); +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml index 392c9e0a78c..f5017ced5d8 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml @@ -63,7 +63,6 @@ +/// Interaction logic for CustomBrowserSetting.xaml +/// +public partial class CustomBrowserSettingWindow : Window { - /// - /// Interaction logic for CustomBrowserSetting.xaml - /// - public partial class CustomBrowserSettingWindow : Window + private CustomBrowser _currentCustomBrowser; + public CustomBrowserSettingWindow(CustomBrowser browser) { - private CustomBrowser currentCustomBrowser; - public CustomBrowserSettingWindow(CustomBrowser browser) - { - InitializeComponent(); - currentCustomBrowser = browser; - DataContext = new CustomBrowser - { - Name = browser.Name, - DataDirectoryPath = browser.DataDirectoryPath, - BrowserType = browser.BrowserType, - }; - } - - private void ConfirmEditCustomBrowser(object sender, RoutedEventArgs e) + InitializeComponent(); + _currentCustomBrowser = browser; + DataContext = new CustomBrowser { - CustomBrowser editBrowser = (CustomBrowser)DataContext; - currentCustomBrowser.Name = editBrowser.Name; - currentCustomBrowser.DataDirectoryPath = editBrowser.DataDirectoryPath; - currentCustomBrowser.BrowserType = editBrowser.BrowserType; - DialogResult = true; - Close(); - } + Name = browser.Name, + DataDirectoryPath = browser.DataDirectoryPath, + BrowserType = browser.BrowserType, + }; + } - private void CancelEditCustomBrowser(object sender, RoutedEventArgs e) - { - Close(); - } + private void ConfirmEditCustomBrowser(object sender, RoutedEventArgs e) + { + CustomBrowser editBrowser = (CustomBrowser)DataContext; + _currentCustomBrowser.Name = editBrowser.Name; + _currentCustomBrowser.DataDirectoryPath = editBrowser.DataDirectoryPath; + _currentCustomBrowser.BrowserType = editBrowser.BrowserType; + DialogResult = true; + Close(); + } - private void WindowKeyDown(object sender, System.Windows.Input.KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - ConfirmEditCustomBrowser(sender, e); - } - } + private void CancelEditCustomBrowser(object sender, RoutedEventArgs e) + { + Close(); + } - private void OnSelectPathClick(object sender, RoutedEventArgs e) + private void WindowKeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == Key.Enter) { - var dialog = new FolderBrowserDialog(); - dialog.ShowDialog(); - CustomBrowser editBrowser = (CustomBrowser)DataContext; - editBrowser.DataDirectoryPath = dialog.SelectedPath; + ConfirmEditCustomBrowser(sender, e); } } + + private void OnSelectPathClick(object sender, RoutedEventArgs e) + { + var dialog = new FolderBrowserDialog(); + dialog.ShowDialog(); + CustomBrowser editBrowser = (CustomBrowser)DataContext; + editBrowser.DataDirectoryPath = dialog.SelectedPath; + } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs index 2947c2cb56f..4bdab89a945 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs @@ -4,119 +4,116 @@ using System.ComponentModel; using System.Threading.Tasks; -namespace Flow.Launcher.Plugin.BrowserBookmark.Views +namespace Flow.Launcher.Plugin.BrowserBookmark.Views; + +public partial class SettingsControl : INotifyPropertyChanged { - public partial class SettingsControl : INotifyPropertyChanged - { - public Settings Settings { get; } + public Settings Settings { get; } - public CustomBrowser SelectedCustomBrowser { get; set; } + public CustomBrowser SelectedCustomBrowser { get; set; } - public bool LoadChromeBookmark + public bool LoadChromeBookmark + { + get => Settings.LoadChromeBookmark; + set { - get => Settings.LoadChromeBookmark; - set - { - Settings.LoadChromeBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } + Settings.LoadChromeBookmark = value; + _ = Task.Run(() => Main.ReloadAllBookmarks()); } + } - public bool LoadFirefoxBookmark + public bool LoadFirefoxBookmark + { + get => Settings.LoadFirefoxBookmark; + set { - get => Settings.LoadFirefoxBookmark; - set - { - Settings.LoadFirefoxBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } + Settings.LoadFirefoxBookmark = value; + _ = Task.Run(() => Main.ReloadAllBookmarks()); } + } - public bool LoadEdgeBookmark + public bool LoadEdgeBookmark + { + get => Settings.LoadEdgeBookmark; + set { - get => Settings.LoadEdgeBookmark; - set - { - Settings.LoadEdgeBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } + Settings.LoadEdgeBookmark = value; + _ = Task.Run(() => Main.ReloadAllBookmarks()); } + } - public bool OpenInNewBrowserWindow + public bool OpenInNewBrowserWindow + { + get => Settings.OpenInNewBrowserWindow; + set { - get => Settings.OpenInNewBrowserWindow; - set - { - Settings.OpenInNewBrowserWindow = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OpenInNewBrowserWindow))); - } + Settings.OpenInNewBrowserWindow = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OpenInNewBrowserWindow))); } + } - public SettingsControl(Settings settings) - { - Settings = settings; - InitializeComponent(); - } + public SettingsControl(Settings settings) + { + Settings = settings; + InitializeComponent(); + } - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler PropertyChanged; - private void NewCustomBrowser(object sender, RoutedEventArgs e) - { - var newBrowser = new CustomBrowser(); - var window = new CustomBrowserSettingWindow(newBrowser); - window.ShowDialog(); - if (newBrowser is not - { - Name: null, - DataDirectoryPath: null - }) + private void NewCustomBrowser(object sender, RoutedEventArgs e) + { + var newBrowser = new CustomBrowser(); + var window = new CustomBrowserSettingWindow(newBrowser); + window.ShowDialog(); + if (newBrowser is not { - Settings.CustomChromiumBrowsers.Add(newBrowser); - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } - } - - private void DeleteCustomBrowser(object sender, RoutedEventArgs e) + Name: null, + DataDirectoryPath: null + }) { - if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser) - { - Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser); - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } + Settings.CustomChromiumBrowsers.Add(newBrowser); + _ = Task.Run(() => Main.ReloadAllBookmarks()); } - - private void MouseDoubleClickOnSelectedCustomBrowser(object sender, MouseButtonEventArgs e) + } + + private void DeleteCustomBrowser(object sender, RoutedEventArgs e) + { + if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser) { - EditSelectedCustomBrowser(); + Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser); + _ = Task.Run(() => Main.ReloadAllBookmarks()); } - - private void Others_Click(object sender, RoutedEventArgs e) - { + } - if (CustomBrowsersList.Visibility == Visibility.Collapsed) - { - CustomBrowsersList.Visibility = Visibility.Visible; - } - else - CustomBrowsersList.Visibility = Visibility.Collapsed; - } + private void MouseDoubleClickOnSelectedCustomBrowser(object sender, MouseButtonEventArgs e) + { + EditSelectedCustomBrowser(); + } - private void EditCustomBrowser(object sender, RoutedEventArgs e) + private void Others_Click(object sender, RoutedEventArgs e) + { + CustomBrowsersList.Visibility = CustomBrowsersList.Visibility switch { - EditSelectedCustomBrowser(); - } + Visibility.Collapsed => Visibility.Visible, + _ => Visibility.Collapsed + }; + } - private void EditSelectedCustomBrowser() - { - if (SelectedCustomBrowser is null) - return; + private void EditCustomBrowser(object sender, RoutedEventArgs e) + { + EditSelectedCustomBrowser(); + } - var window = new CustomBrowserSettingWindow(SelectedCustomBrowser); - var result = window.ShowDialog() ?? false; - if (result) - { - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } + private void EditSelectedCustomBrowser() + { + if (SelectedCustomBrowser is null) + return; + + var window = new CustomBrowserSettingWindow(SelectedCustomBrowser); + var result = window.ShowDialog() ?? false; + if (result) + { + _ = Task.Run(() => Main.ReloadAllBookmarks()); } } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json index c4a24a57b62..bb788fcfc25 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json @@ -4,7 +4,7 @@ "Name": "Browser Bookmarks", "Description": "Search your browser bookmarks", "Author": "qianlifeng, Ioannis G.", - "Version": "3.2.0", + "Version": "3.3.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/DecimalSeparator.cs b/Plugins/Flow.Launcher.Plugin.Calculator/DecimalSeparator.cs index 27bdf94ee10..81a68739b92 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/DecimalSeparator.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/DecimalSeparator.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using Flow.Launcher.Core.Resource; -namespace Flow.Launcher.Plugin.Caculator +namespace Flow.Launcher.Plugin.Calculator { [TypeConverter(typeof(LocalizationConverter))] public enum DecimalSeparator diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj index 415f852f4c8..69c03b877fd 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj @@ -5,8 +5,8 @@ net7.0-windows {59BD9891-3837-438A-958D-ADC7F91F6F7E} Properties - Flow.Launcher.Plugin.Caculator - Flow.Launcher.Plugin.Caculator + Flow.Launcher.Plugin.Calculator + Flow.Launcher.Plugin.Calculator true true false @@ -18,7 +18,7 @@ true portable false - ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Caculator\ + ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Calculator\ DEBUG;TRACE prompt 4 @@ -28,7 +28,7 @@ pdbonly true - ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Caculator\ + ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Calculator\ TRACE prompt 4 diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ar.xaml index 15598118cbc..3219d647e85 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ar.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ar.xaml @@ -1,15 +1,15 @@  - Calculator - Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher) - Not a number (NaN) - Expression wrong or incomplete (Did you forget some parentheses?) - Copy this number to the clipboard - Decimal separator - The decimal separator to be used in the output. - Use system locale - Comma (,) - Dot (.) - Max. decimal places + آلة حاسبة + تمكنك من إجراء العمليات الحسابية. (جرب 5*3-2 في Flow Launcher) + ليست رقمًا (NaN) + التعبير خاطئ أو غير مكتمل (هل نسيت بعض الأقواس؟) + نسخ هذا الرقم إلى الحافظة + فاصل عشري + الفاصل العشري الذي سيتم استخدامه في الناتج. + استخدام إعدادات النظام المحلية + فاصلة (,) + نقطة (.) + أقصى عدد من المنازل العشرية diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/tr.xaml index 07a287d5203..345c814339f 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/tr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/tr.xaml @@ -6,10 +6,10 @@ Sayı değil (NaN) İfade hatalı ya da eksik. (Parantez koymayı mı unuttunuz?) Bu sayıyı panoya kopyala - Decimal separator - The decimal separator to be used in the output. - Use system locale - Comma (,) - Dot (.) - Max. decimal places + Ondalık ayracı + Ondalık kısımları ayırmak için kullanılacak işaret. + Sistem yerelleştirme ayarını kullan + Virgül (,) + Nokta (.) + Maks. ondalık basamak diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/vi.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/vi.xaml new file mode 100644 index 00000000000..82ebbbe9363 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/vi.xaml @@ -0,0 +1,15 @@ + + + + Máy tính + Cho phép thực hiện các phép tính toán học. (Thử 5*3-2 trong Flow Launcher) + Không phải là số (NaN) + Biểu thức sai hoặc không đầy đủ (Bạn có quên một số dấu ngoặc đơn không?) + Sao chép số này vào clipboard + Dấu tách thập phân + Dấu phân cách thập phân được sử dụng ở đầu ra. + Sử dụng ngôn ngữ hệ thống + Dấu phẩy (,) + dấu chấm (.) + Tối đa. chữ số thập phân + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 338b5bcbe43..5077e60614d 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -6,10 +6,10 @@ using System.Windows; using System.Windows.Controls; using Mages.Core; -using Flow.Launcher.Plugin.Caculator.ViewModels; -using Flow.Launcher.Plugin.Caculator.Views; +using Flow.Launcher.Plugin.Calculator.ViewModels; +using Flow.Launcher.Plugin.Calculator.Views; -namespace Flow.Launcher.Plugin.Caculator +namespace Flow.Launcher.Plugin.Calculator { public class Main : IPlugin, IPluginI18n, ISettingProvider { @@ -62,7 +62,7 @@ public List Query(Query query) switch (_settings.DecimalSeparator) { case DecimalSeparator.Comma: - case DecimalSeparator.UseSystemLocale when CultureInfo.DefaultThreadCurrentCulture.NumberFormat.NumberDecimalSeparator == ",": + case DecimalSeparator.UseSystemLocale when CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator == ",": expression = query.Search.Replace(",", "."); break; default: @@ -158,7 +158,7 @@ private string ChangeDecimalSeparator(decimal value, string newDecimalSeparator) private string GetDecimalSeparator() { - string systemDecimalSeperator = CultureInfo.DefaultThreadCurrentCulture.NumberFormat.NumberDecimalSeparator; + string systemDecimalSeperator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; switch (_settings.DecimalSeparator) { case DecimalSeparator.UseSystemLocale: return systemDecimalSeperator; diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs b/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs index ed4aed75bd4..4eacb9d349c 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs @@ -2,7 +2,7 @@ using System.Text; using System.Text.RegularExpressions; -namespace Flow.Launcher.Plugin.Caculator +namespace Flow.Launcher.Plugin.Calculator { /// /// Tries to convert all numbers in a text from one culture format to another. diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs index 61551487381..8354863b852 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs @@ -1,5 +1,5 @@  -namespace Flow.Launcher.Plugin.Caculator +namespace Flow.Launcher.Plugin.Calculator { public class Settings { diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs index afe4d1c0cb0..09f745669fc 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Flow.Launcher.Plugin.Caculator.ViewModels +namespace Flow.Launcher.Plugin.Calculator.ViewModels { public class SettingsViewModel : BaseModel { diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml index 9fd4bb17c90..d6237c6daf6 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml @@ -1,13 +1,13 @@ /// Interaction logic for CalculatorSettings.xaml diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json index a77475460c0..b814fcd1888 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json @@ -4,9 +4,9 @@ "Name": "Calculator", "Description": "Provide mathematical calculations.(Try 5*3-2 in Flow Launcher)", "Author": "cxfksword", - "Version": "3.1.0", + "Version": "3.1.2", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", - "ExecuteFileName": "Flow.Launcher.Plugin.Caculator.dll", + "ExecuteFileName": "Flow.Launcher.Plugin.Calculator.dll", "IcoPath": "Images\\calculator.png" } \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs index 2297e5f9633..f061ca183cf 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs @@ -9,6 +9,7 @@ using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using System.Linq; +using Flow.Launcher.Plugin.Explorer.Helper; using MessageBox = System.Windows.Forms.MessageBox; using MessageBoxIcon = System.Windows.Forms.MessageBoxIcon; using MessageBoxButton = System.Windows.Forms.MessageBoxButtons; @@ -222,34 +223,7 @@ public List LoadContextMenus(Result selectedResult) if (record.Type is ResultType.Volume) return false; - var screenWithMouseCursor = System.Windows.Forms.Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var xOfScreenCenter = screenWithMouseCursor.WorkingArea.Left + screenWithMouseCursor.WorkingArea.Width / 2; - var yOfScreenCenter = screenWithMouseCursor.WorkingArea.Top + screenWithMouseCursor.WorkingArea.Height / 2; - var showPosition = new System.Drawing.Point(xOfScreenCenter, yOfScreenCenter); - - switch (record.Type) - { - case ResultType.File: - { - var fileInfos = new FileInfo[] - { - new(record.FullPath) - }; - - new Peter.ShellContextMenu().ShowContextMenu(fileInfos, showPosition); - break; - } - case ResultType.Folder: - { - var directoryInfos = new DirectoryInfo[] - { - new(record.FullPath) - }; - - new Peter.ShellContextMenu().ShowContextMenu(directoryInfos, showPosition); - break; - } - } + ResultManager.ShowNativeContextMenu(record.FullPath, record.Type); return false; }, @@ -276,8 +250,48 @@ public List LoadContextMenus(Result selectedResult) return true; }, - IcoPath = Constants.DifferentUserIconImagePath + IcoPath = Constants.DifferentUserIconImagePath, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue748"), }); + + if (record.Type is ResultType.File or ResultType.Folder && Settings.ShowInlinedWindowsContextMenu) + { + var includedItems = Settings + .WindowsContextMenuIncludedItems + .Replace("\r", "") + .Split("\n") + .Where(v => !string.IsNullOrWhiteSpace(v)) + .ToArray(); + var excludedItems = Settings + .WindowsContextMenuExcludedItems + .Replace("\r", "") + .Split("\n") + .Where(v => !string.IsNullOrWhiteSpace(v)) + .ToArray(); + var menuItems = ShellContextMenuDisplayHelper + .GetContextMenuWithIcons(record.FullPath) + .Where(contextMenuItem => + (includedItems.Length == 0 || includedItems.Any(filter => + contextMenuItem.Label.Contains(filter, StringComparison.OrdinalIgnoreCase) + )) && + (excludedItems.Length == 0 || !excludedItems.Any(filter => + contextMenuItem.Label.Contains(filter, StringComparison.OrdinalIgnoreCase) + )) + ); + foreach (var menuItem in menuItems) + { + contextMenus.Add(new Result + { + Title = menuItem.Label, + Icon = () => menuItem.Icon, + Action = _ => + { + ShellContextMenuDisplayHelper.ExecuteContextMenuItem(record.FullPath, menuItem.CommandId); + return true; + } + }); + } + } } return contextMenus; @@ -403,7 +417,8 @@ private Result CreateAddToIndexSearchExclusionListResult(SearchResult record) return false; }, - IcoPath = Constants.ExcludeFromIndexImagePath + IcoPath = Constants.ExcludeFromIndexImagePath, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uf140"), }; } @@ -435,7 +450,8 @@ private Result CreateOpenWindowsIndexingOptions() return false; } }, - IcoPath = Constants.IndexingOptionsIconImagePath + IcoPath = Constants.IndexingOptionsIconImagePath, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue773"), }; } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenuDisplayHelper.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenuDisplayHelper.cs new file mode 100644 index 00000000000..304c47cb64e --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenuDisplayHelper.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace Flow.Launcher.Plugin.Explorer.Helper; + +public static class ShellContextMenuDisplayHelper +{ + #region DllImport + + [DllImport("shell32.dll")] + private static extern Int32 SHGetMalloc(out IntPtr hObject); + + [DllImport("shell32.dll")] + private static extern Int32 SHParseDisplayName( + [MarshalAs(UnmanagedType.LPWStr)] string pszName, + IntPtr pbc, + out IntPtr ppidl, + UInt32 sfgaoIn, + out UInt32 psfgaoOut + ); + + [DllImport("shell32.dll")] + private static extern Int32 SHBindToParent( + IntPtr pidl, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid, + out IntPtr ppv, + ref IntPtr ppidlLast + ); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr CreatePopupMenu(); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool DestroyMenu(IntPtr hMenu); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern uint GetMenuItemCount(IntPtr hMenu); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern uint GetMenuString( + IntPtr hMenu, uint uIDItem, StringBuilder lpString, int nMaxCount, uint uFlag + ); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool GetMenuItemInfo(IntPtr hMenu, uint uItem, bool fByPosition, ref MENUITEMINFO lpmii); + + [DllImport("gdi32.dll")] + private static extern bool DeleteObject(IntPtr hObject); + + #endregion + + #region Constants + + private const uint ContextMenuStartId = 0x0001; + private const uint ContextMenuEndId = 0x7FFF; + + private static readonly string[] IgnoredContextMenuCommands = + { + // We haven't managed to make these work, so we don't display them in the context menu. + "Share", + "Windows.ModernShare", + "PinToStartScreen", + "CopyAsPath", + + // Hide functionality provided by the Explorer plugin itself + "Copy", + "Delete" + }; + + #endregion + + #region Enums + + [Flags] + enum ContextMenuFlags : uint + { + Normal = 0x00000000, + DefaultOnly = 0x00000001, + VerbsOnly = 0x00000002, + Explore = 0x00000004, + NoVerbs = 0x00000008, + CanRename = 0x00000010, + NoDefault = 0x00000020, + IncludeStatic = 0x00000040, + ItemMenu = 0x00000080, + ExtendedVerbs = 0x00000100, + DisabledVerbs = 0x00000200, + AsyncVerbState = 0x00000400, + OptimizeForInvoke = 0x00000800, + SyncCascadeMenu = 0x00001000, + DoNotPickDefault = 0x00002000, + Reserved = 0xffff0000 + } + + [Flags] + enum ContextMenuInvokeCommandFlags : uint + { + Icon = 0x00000010, + Hotkey = 0x00000020, + FlagNoUi = 0x00000400, + Unicode = 0x00004000, + NoConsole = 0x00008000, + AsyncOk = 0x00100000, + NoZoneChecks = 0x00800000, + ShiftDown = 0x10000000, + ControlDown = 0x40000000, + FlagLogUsage = 0x04000000, + PointInvoke = 0x20000000 + } + + [Flags] + enum MenuItemInformationMask : uint + { + Bitmap = 0x00000080, + Checkmarks = 0x00000008, + Data = 0x00000020, + Ftype = 0x00000100, + Id = 0x00000002, + State = 0x00000001, + String = 0x00000040, + Submenu = 0x00000004, + Type = 0x00000010 + } + + enum MenuItemFtype : uint + { + Bitmap = 0x00000004, + MenuBarBreak = 0x00000020, + MenuBreak = 0x00000040, + OwnerDraw = 0x00000100, + RadioCheck = 0x00000200, + RightJustify = 0x00004000, + RightOrder = 0x00002000, + Separator = 0x00000800, + String = 0x00000000, + } + + enum GetCommandStringFlags : uint + { + VerbA = 0x00000000, + HelpTextA = 0x00000001, + ValidateA = 0x00000002, + Unicode = VerbW, + Verb = VerbW, + VerbW = 0x00000004, + HelpText = HelpTextW, + HelpTextW = 0x00000005, + Validate = ValidateW, + ValidateW = 0x00000006, + VerbIconW = 0x00000014 + } + #endregion + + private static IMalloc GetMalloc() + { + SHGetMalloc(out var pMalloc); + return (IMalloc)Marshal.GetTypedObjectForIUnknown(pMalloc, typeof(IMalloc)); + } + + public static void ExecuteContextMenuItem(string fileName, uint menuItemId) + { + IMalloc malloc = null; + IntPtr originalPidl = IntPtr.Zero; + IntPtr pShellFolder = IntPtr.Zero; + IntPtr pContextMenu = IntPtr.Zero; + IntPtr hMenu = IntPtr.Zero; + IContextMenu contextMenu = null; + IShellFolder shellFolder = null; + + try + { + malloc = GetMalloc(); + var hr = SHParseDisplayName(fileName, IntPtr.Zero, out var pidl, 0, out _); + if (hr != 0) throw new Exception("SHParseDisplayName failed"); + + originalPidl = pidl; + + var guid = typeof(IShellFolder).GUID; + hr = SHBindToParent(pidl, guid, out pShellFolder, ref pidl); + if (hr != 0) throw new Exception("SHBindToParent failed"); + + shellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pShellFolder, typeof(IShellFolder)); + hr = shellFolder.GetUIObjectOf( + IntPtr.Zero, 1, new[] { pidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out pContextMenu + ); + if (hr != 0) throw new Exception("GetUIObjectOf failed"); + + contextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(pContextMenu, typeof(IContextMenu)); + + hMenu = CreatePopupMenu(); + contextMenu.QueryContextMenu(hMenu, 0, ContextMenuStartId, ContextMenuEndId, (uint)ContextMenuFlags.Explore); + + var directory = Path.GetDirectoryName(fileName); + var invokeCommandInfo = new CMINVOKECOMMANDINFO + { + cbSize = (uint)Marshal.SizeOf(typeof(CMINVOKECOMMANDINFO)), + fMask = (uint)ContextMenuInvokeCommandFlags.Unicode, + hwnd = IntPtr.Zero, + lpVerb = (IntPtr)(menuItemId - ContextMenuStartId), + lpParameters = null, + lpDirectory = null, + nShow = 1, + hIcon = IntPtr.Zero, + }; + + hr = contextMenu.InvokeCommand(ref invokeCommandInfo); + if (hr != 0) + { + throw new Exception($"InvokeCommand failed with code {hr:X}"); + } + } + finally + { + if (hMenu != IntPtr.Zero) + DestroyMenu(hMenu); + + if (contextMenu != null) + Marshal.ReleaseComObject(contextMenu); + + if (pContextMenu != IntPtr.Zero) + Marshal.Release(pContextMenu); + + if (shellFolder != null) + Marshal.ReleaseComObject(shellFolder); + + if (pShellFolder != IntPtr.Zero) + Marshal.Release(pShellFolder); + + if (originalPidl != IntPtr.Zero) + malloc?.Free(originalPidl); + + if (malloc != null) + Marshal.ReleaseComObject(malloc); + } + } + + public static List GetContextMenuWithIcons(string filePath) + { + IMalloc malloc = null; + IntPtr originalPidl = IntPtr.Zero; + IntPtr pShellFolder = IntPtr.Zero; + IntPtr pContextMenu = IntPtr.Zero; + IntPtr hMenu = IntPtr.Zero; + IShellFolder shellFolder = null; + IContextMenu contextMenu = null; + + try + { + malloc = GetMalloc(); + var hr = SHParseDisplayName(filePath, IntPtr.Zero, out var pidl, 0, out _); + if (hr != 0) throw new Exception("SHParseDisplayName failed"); + + originalPidl = pidl; + + var guid = typeof(IShellFolder).GUID; + hr = SHBindToParent(pidl, guid, out pShellFolder, ref pidl); + if (hr != 0) throw new Exception("SHBindToParent failed"); + + shellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pShellFolder, typeof(IShellFolder)); + hr = shellFolder.GetUIObjectOf( + IntPtr.Zero, 1, new[] { pidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out pContextMenu + ); + if (hr != 0) throw new Exception("GetUIObjectOf failed"); + + contextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(pContextMenu, typeof(IContextMenu)); + + // Without waiting, some items, such as "Send to > Documents", don't always appear, which shifts item ids + // even though it shouldn't. Please replace this if you find a better way to fix this bug. + Thread.Sleep(200); + + hMenu = CreatePopupMenu(); + contextMenu.QueryContextMenu(hMenu, 0, ContextMenuStartId, ContextMenuEndId, (uint)ContextMenuFlags.Explore); + + var menuItems = new List(); + ProcessMenuWithIcons(hMenu, contextMenu, menuItems); + + return menuItems; + } + finally + { + if (hMenu != IntPtr.Zero) + DestroyMenu(hMenu); + + if (contextMenu != null) + Marshal.ReleaseComObject(contextMenu); + + if (pContextMenu != IntPtr.Zero) + Marshal.Release(pContextMenu); + + if (shellFolder != null) + Marshal.ReleaseComObject(shellFolder); + + if (pShellFolder != IntPtr.Zero) + Marshal.Release(pShellFolder); + + if (originalPidl != IntPtr.Zero) + malloc?.Free(originalPidl); + + if (malloc != null) + Marshal.ReleaseComObject(malloc); + } + } + + + private static void ProcessMenuWithIcons(IntPtr hMenu, IContextMenu contextMenu, List menuItems, string prefix = "") + { + uint menuCount = GetMenuItemCount(hMenu); + + for (uint i = 0; i < menuCount; i++) + { + var mii = new MENUITEMINFO + { + cbSize = (uint)Marshal.SizeOf(typeof(MENUITEMINFO)), + fMask = (uint)(MenuItemInformationMask.Bitmap | MenuItemInformationMask.Ftype | + MenuItemInformationMask.Submenu | MenuItemInformationMask.Id) + }; + + GetMenuItemInfo(hMenu, i, true, ref mii); + var menuText = new StringBuilder(256); + uint result = GetMenuString(hMenu, mii.wID, menuText, menuText.Capacity, 0); + + if (result == 0 || string.IsNullOrWhiteSpace(menuText.ToString())) + { + continue; + } + + menuText.Replace("&", ""); + + IntPtr hSubMenu = GetSubMenu(hMenu, (int)i); + if (hSubMenu != IntPtr.Zero) + { + ProcessMenuWithIcons(hSubMenu, contextMenu, menuItems, prefix + menuText + " > "); + } + else if (!string.IsNullOrWhiteSpace(menuText.ToString())) + { + var commandBuilder = new StringBuilder(256); + contextMenu.GetCommandString( + mii.wID - ContextMenuStartId, + (uint)GetCommandStringFlags.Verb, + IntPtr.Zero, + commandBuilder, + commandBuilder.Capacity + ); + if (IgnoredContextMenuCommands.Contains(commandBuilder.ToString(), StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + ImageSource icon = null; + if (mii.hbmpItem != IntPtr.Zero) + { + icon = GetBitmapSourceFromHBitmap(mii.hbmpItem); + } + else if (mii.hbmpChecked != IntPtr.Zero) + { + icon = GetBitmapSourceFromHBitmap(mii.hbmpChecked); + } + + menuItems.Add(new ContextMenuItem(prefix + menuText, icon, mii.wID)); + } + } + } + + private static BitmapSource GetBitmapSourceFromHBitmap(IntPtr hBitmap) + { + try + { + var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap( + hBitmap, + IntPtr.Zero, + Int32Rect.Empty, + BitmapSizeOptions.FromWidthAndHeight(16, 16) + ); + + if (!DeleteObject(hBitmap)) + { + throw new Exception("Failed to delete HBitmap."); + } + + return bitmapSource; + } + catch (COMException) + { + // ignore + } + + return null; + } +} + +#region Data Structures + +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("000214E6-0000-0000-C000-000000000046")] +public interface IShellFolder +{ + [PreserveSig] + int ParseDisplayName( + IntPtr hwnd, IntPtr pbc, [In, MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, out uint pchEaten, + out IntPtr ppidl, ref uint pdwAttributes + ); + + [PreserveSig] + int EnumObjects(IntPtr hwnd, uint grfFlags, out IntPtr ppenumIDList); + + [PreserveSig] + int BindToObject(IntPtr pidl, IntPtr pbc, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); + + [PreserveSig] + int BindToStorage(IntPtr pidl, IntPtr pbc, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); + + [PreserveSig] + int CompareIDs(IntPtr lParam, IntPtr pidl1, IntPtr pidl2); + + [PreserveSig] + int CreateViewObject(IntPtr hwndOwner, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); + + [PreserveSig] + int GetAttributesOf( + uint cidl, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] apidl, ref uint rgfInOut + ); + + [PreserveSig] + int GetUIObjectOf( + IntPtr hwndOwner, uint cidl, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] apidl, + [In, MarshalAs(UnmanagedType.LPStruct)] + Guid riid, IntPtr rgfReserved, out IntPtr ppv + ); + + [PreserveSig] + int GetDisplayNameOf(IntPtr pidl, uint uFlags, IntPtr pName); + + [PreserveSig] + int SetNameOf( + IntPtr hwnd, IntPtr pidl, [In, MarshalAs(UnmanagedType.LPWStr)] string pszName, uint uFlags, out IntPtr ppidlOut + ); +} + +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("00000002-0000-0000-C000-000000000046")] +public interface IMalloc +{ + [PreserveSig] + IntPtr Alloc(UInt32 cb); + + [PreserveSig] + IntPtr Realloc(IntPtr pv, UInt32 cb); + + [PreserveSig] + void Free(IntPtr pv); + + [PreserveSig] + UInt32 GetSize(IntPtr pv); + + [PreserveSig] + Int16 DidAlloc(IntPtr pv); + + [PreserveSig] + void HeapMinimize(); +} + +[StructLayout(LayoutKind.Sequential)] +public struct CMINVOKECOMMANDINFO +{ + public uint cbSize; + public uint fMask; + public IntPtr hwnd; + public IntPtr lpVerb; + [MarshalAs(UnmanagedType.LPStr)] public string lpParameters; + [MarshalAs(UnmanagedType.LPStr)] public string lpDirectory; + public int nShow; + public uint dwHotKey; + public IntPtr hIcon; +} + +[StructLayout(LayoutKind.Sequential)] +public struct MENUITEMINFO +{ + public uint cbSize; + public uint fMask; + public uint fType; + public uint fState; + public uint wID; + public IntPtr hSubMenu; + public IntPtr hbmpChecked; + public IntPtr hbmpUnchecked; + public IntPtr dwItemData; + public IntPtr dwTypeData; + public uint cch; + public IntPtr hbmpItem; +} + +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("000214E4-0000-0000-C000-000000000046")] +public interface IContextMenu +{ + [PreserveSig] + int QueryContextMenu(IntPtr hmenu, uint indexMenu, uint idCmdFirst, uint idCmdLast, uint uFlags); + + [PreserveSig] + int InvokeCommand(ref CMINVOKECOMMANDINFO pici); + + [PreserveSig] + int GetCommandString(uint idcmd, uint uflags, IntPtr reserved, StringBuilder commandstring, int cch); +} + +public record ContextMenuItem(string Label, ImageSource Icon, uint CommandId); + +#endregion diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png index 552b2c5bd17..25da8e49c8f 100644 Binary files a/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ar.xaml index ad83e6e054c..003f1ac4ec9 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ar.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ar.xaml @@ -2,144 +2,162 @@ - Please make a selection first - Please select a folder link - Are you sure you want to delete {0}? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} - Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword - Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword - The required service for Windows Index Search does not appear to be running - To fix this, start the Windows Search service. Select here to remove this warning - The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return - Explorer Alternative - Error occurred during search: {0} - Could not open folder - Could not open file + يرجى إجراء تحديد أولاً + يرجى تحديد رابط المجلد + هل أنت متأكد أنك تريد حذف {0}؟ + هل أنت متأكد أنك تريد حذف هذا الملف نهائيًا؟ + هل أنت متأكد أنك تريد حذف هذا الملف/المجلد نهائيًا؟ + تم الحذف بنجاح + تم الحذف بنجاح {0} + قد يؤدي تعيين الكلمة المفتاحية العامة للإجراء إلى ظهور الكثير من النتائج أثناء البحث. يرجى اختيار كلمة مفتاحية محددة للإجراء + لا يمكن تعيين الوصول السريع للكلمة المفتاحية العامة عند التمكين. يرجى اختيار كلمة مفتاحية محددة للإجراء + يبدو أن الخدمة المطلوبة للبحث في فهرس Windows غير قيد التشغيل + لإصلاح ذلك، ابدأ خدمة بحث Windows. اختر هنا لإزالة هذا التحذير + تم إيقاف رسالة التحذير. كبديل للبحث عن الملفات والمجلدات، هل ترغب في تثبيت إضافة Everything؟{0}{0}اختر 'نعم' لتثبيت إضافة Everything، أو 'لا' للعودة + بديل المستكشف + حدث خطأ أثناء البحث: {0} + تعذر فتح المجلد + تعذر فتح الملف - Delete - Edit - Add - General Setting - Customise Action Keywords - Quick Access Links - Everything Setting - Sort Option: - Everything Path: - Launch Hidden - Editor Path - Shell Path - Index Search Excluded Paths - Use search result's location as the working directory of the executable - Hit Enter to open folder in Default File Manager - Use Index Search For Path Search - Indexing Options - Search: - Path Search: - File Content Search: - Index Search: - Quick Access: - Current Action Keyword - Done - Enabled - When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword - Everything - Windows Index - Direct Enumeration - File Editor Path - Folder Editor Path + حذف + تعديل + إضافة + إعدادات عامة + تخصيص الكلمات المفتاحية للإجراءات + روابط الوصول السريع + إعدادات Everything + لوحة المعاينة + الحجم + تاريخ الإنشاء + تاريخ التعديل + عرض معلومات الملف + تنسيق التاريخ والوقت + خيارات الترتيب: + مسار Everything: + إطلاق مخفي + مسار المحرر + مسار الشل + مسارات مستبعدة من البحث المفهرس + استخدام موقع نتيجة البحث كدليل العمل للتنفيذ + اضغط Enter لفتح المجلد في مدير الملفات الافتراضي + استخدام البحث المفهرس للبحث في المسار + خيارات الفهرسة + بحث: + بحث المسار: + بحث في محتوى الملفات: + بحث مفهرس: + وصول سريع: + الكلمة المفتاحية الحالية للإجراء + تم + مفعّل + عند التعطيل، لن يقوم Flow بتنفيذ هذا الخيار في البحث، وسيرجع إلى '*' لتحرير الكلمة المفتاحية للإجراء + كل شيء + فهرس Windows + تعداد مباشر + مسار محرر الملفات + مسار محرر المجلدات - Content Search Engine - Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + محرك البحث في المحتوى + محرك البحث التكراري في المجلدات + محرك البحث المفهرس + فتح خيار فهرس Windows + أنواع الملفات المستبعدة (مفصولة بفاصلة) + على سبيل المثال: exe,jpg,png + الحد الأقصى للنتائج + الحد الأقصى لعدد النتائج المطلوبة من محرك البحث النشط - Explorer - Find and manage files and folders via Windows Search or Everything + المستكشف + ابحث وأدر الملفات والمجلدات عبر بحث Windows أو Everything - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Ctrl + Enter لفتح الدليل + Ctrl + Enter لفتح المجلد الحاوي - Copy path - Copy path of current item to clipboard - Copy - Copy current file to clipboard - Copy current folder to clipboard - Delete - Permanently delete current file - Permanently delete current folder - Path: - Delete the selected - Run as different user - Run the selected using a different user account - Open containing folder - Open the location that contains current item - Open With Editor: - Failed to open file at {0} with Editor {1} at {2} - Open With Shell: - Failed to open folder {0} with Shell {1} at {2} - Exclude current and sub-directories from Index Search - Excluded from Index Search - Open Windows Indexing Options - Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access - Add current item to Quick Access - Successfully Added - Successfully added to Quick Access - Successfully Removed - Successfully removed from Quick Access - Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access - Remove current item from Quick Access - Show Windows Context Menu - Open With - Select a program to open with + نسخ المسار + نسخ مسار العنصر الحالي إلى الحافظة + نسخ + نسخ الملف الحالي إلى الحافظة + نسخ المجلد الحالي إلى الحافظة + حذف + حذف الملف الحالي نهائيًا + حذف المجلد الحالي نهائيًا + المسار: + حذف المحدد + تشغيل كمستخدم مختلف + تشغيل العنصر المحدد باستخدام حساب مستخدم مختلف + فتح المجلد الحاوي + فتح الموقع الذي يحتوي على العنصر الحالي + فتح بالمحرر: + فشل في فتح الملف في {0} بالمحرر {1} في {2} + فتح بالشيل: + فشل في فتح المجلد {0} بالشيل {1} في {2} + استبعاد الحالي والمجلدات الفرعية من البحث المفهرس + تم الاستبعاد من البحث المفهرس + فتح خيارات فهرسة Windows + إدارة الملفات والمجلدات المفهرسة + فشل في فتح خيارات فهرسة Windows + إضافة إلى الوصول السريع + إضافة العنصر الحالي إلى الوصول السريع + تمت الإضافة بنجاح + تمت الإضافة إلى الوصول السريع بنجاح + تمت الإزالة بنجاح + تمت الإزالة من الوصول السريع بنجاح + أضف إلى الوصول السريع بحيث يمكن فتحه باستخدام كلمة مفتاحية لتفعيل البحث في المستكشف + إزالة من الوصول السريع + إزالة من الوصول السريع + إزالة العنصر الحالي من الوصول السريع + عرض قائمة السياق في Windows + فتح بواسطة + اختر برنامج لفتح العنصر - - {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + {0} متبقي من {1} + فتح في مدير الملفات الافتراضي + + استخدم '>' للبحث في هذا الدليل، '*' للبحث عن امتدادات الملفات أو '> *' للجمع بين البحثين. + - Failed to load Everything SDK - Warning: Everything service is not running - Error while querying Everything - Sort By - Name - Path - Size - Extension - Type Name - Date Created - Date Modified - Attributes - File List FileName - Run Count - Date Recently Changed - Date Accessed - Date Run + فشل في تحميل Everything SDK + تحذير: خدمة Everything غير قيد التشغيل + خطأ أثناء استعلام Everything + الترتيب حسب + الاسم + المسار + الحجم + الامتداد + نوع الاسم + تاريخ الإنشاء + تاريخ التعديل + السمات + اسم ملف قائمة الملفات + عدد مرات التشغيل + تاريخ التغيير الأخير + تاريخ الوصول + تاريخ التشغيل - Warning: This is not a Fast Sort option, searches may be slow + تحذير: هذا ليس خيار ترتيب سريع، قد تكون عمليات البحث بطيئة - Search Full Path - - Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you - Do you want to enable content search for Everything? - It can be very slow without index (which is only supported in Everything v1.5+) + بحث في المسار الكامل + تمكين عدد مرات التشغيل للملف/المجلد + انقر لتشغيل أو تثبيت Everything + تثبيت Everything + تثبيت خدمة Everything. يرجى الانتظار... + تم تثبيت خدمة Everything بنجاح + فشل التثبيت التلقائي لخدمة Everything. يرجى تثبيتها يدويًا من https://www.voidtools.com + انقر هنا لتشغيلها + تعذر العثور على تثبيت Everything، هل ترغب في تحديد موقع يدويًا؟{0}{0}انقر لا وسيتم تثبيت Everything تلقائيًا لك + هل ترغب في تمكين البحث في المحتوى لـ Everything؟ + قد يكون بطيئًا جدًا بدون فهرسة (المدعومة فقط في Everything v1.5+) + + + قائمة السياق الأصلية + عرض قائمة السياق الأصلية (تجريبي) + أدناه يمكنك تحديد العناصر التي تريد تضمينها في قائمة السياق، يمكن أن تكون جزئية (على سبيل المثال 'pen wit') أو كاملة ('فتح بواسطة'). + أدناه يمكنك تحديد العناصر التي تريد استبعادها من قائمة السياق، يمكن أن تكون جزئية (على سبيل المثال 'pen wit') أو كاملة ('فتح بواسطة') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/cs.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/cs.xaml index 83334b9b134..3f35583c3f0 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/cs.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/cs.xaml @@ -27,6 +27,12 @@ Upravit aktivační příkaz Odkazy rychlého přístupu Nastavení Everything + Preview Panel + Velikost + Datum vytvoření + Datum změny + Display File Info + Date and time format Možnosti řazení: Umístění Everything: Spustit skryté @@ -56,6 +62,10 @@ Rekurzivní vyhledávač ve složce Indexový vyhledávač Otevření možností vyhledávání v systému Windows + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Průzkumník @@ -103,10 +113,12 @@ Open With Select a program to open with - + Volných {0} z {1} Otevřít ve výchozím správci souborů - Použijte ">" pro vyhledávání v této složce, "*" pro vyhledávání přípon souborů nebo ">*" pro kombinaci obou vyhledávání. + + Použijte ">" pro vyhledávání v této složce, "*" pro vyhledávání přípon souborů nebo ">*" pro kombinaci obou vyhledávání. + Nepodařilo se načíst SDK Everything @@ -131,7 +143,8 @@ Poznámka: Toto není možnost Fast Sort, vyhledávání může být pomalé Hledat celou cestu - + Enable File/Folder Run Count + Kliknutím spustíte nebo nainstalujete aplikaci Everything Instalace Everything Služba Everything se nainstaluje. Počkejte prosím... @@ -142,4 +155,9 @@ Chcete povolit vyhledávání obsahu prostřednictvím služby Everything? Bez indexu (který je podporován pouze ve verzi Everything v1.5+) může být velmi pomalý + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/da.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/da.xaml index 9dfbb94a1f0..676b921a5b4 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/da.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/da.xaml @@ -27,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml index 1bae42c771c..c5aead1a958 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml @@ -27,6 +27,12 @@ Aktions-Schlüsselwörter ändern Schnellzugriff-Links Everything Setting + Preview Panel + Größe + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,10 +113,12 @@ Öffnen mit Programm zum Öffnen auswählen - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Klicken, um Everything zu starten oder zu installieren Everything Installation Installing Everything service. Please wait... @@ -142,4 +155,9 @@ Möchten Sie die Inhaltssuche für alles aktivieren? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index f2491d928ea..0bc9d443725 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -29,6 +29,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -58,6 +64,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -105,10 +115,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -133,7 +145,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -144,4 +157,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es-419.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es-419.xaml index 2d23b315efa..71bcb2e631c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es-419.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es-419.xaml @@ -27,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Fecha de creación + Fecha de modificación + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Advertencia: No es una opción de orden rápido, las búsquedas pueden ser lentas Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Instalación de Everything Instalando el servicio de Everything. Por favor, espere... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es.xaml index ad5d7c31ab9..2959cae4459 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es.xaml @@ -5,7 +5,7 @@ Por favor haga una selección primero Por favor, seleccione un enlace de carpeta ¿Está seguro de que desea eliminar {0}? - ¿Está seguro que desea eliminar permanentemente este archivo? + ¿Está seguro de que desea eliminar permanentemente este archivo? ¿Está seguro de que desea eliminar permanentemente este/esta archivo/carpeta? Eliminación correcta {0} se ha eliminado correctamente @@ -27,6 +27,12 @@ Personalizar palabras clave de acción Enlaces de acceso rápido Configuración Everything + Panel de vista previa + Tamaño + Fecha de creación + Fecha de modificación + Mostrar información del archivo + Formato de fecha y hora Ordenar por: Ruta de Everything: Iniciar oculto @@ -56,6 +62,10 @@ Motor de búsqueda recursiva de directorio Motor de búsqueda del Índice Abrir opciones de indexación de Windows + Tipos de archivo excluidos (separados por comas) + Por ejemplo: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorador @@ -103,10 +113,12 @@ Abrir con Seleccione un programa para abrir con - + {0} libre de {1} Abrir en administrador de archivos predeterminado - Use '>' para buscar en este directorio, '*' para buscar extensiones de archivo o '>*' para combinar ambas búsquedas. + + Use '>' para buscar en este directorio, '*' para buscar extensiones de archivo o '>*' para combinar ambas búsquedas. + No se ha podido cargar Everything SDK @@ -131,7 +143,8 @@ Advertencia: Esta no es una opción de clasificación rápida, las búsquedas pueden ser lentas Buscar ruta completa - + Activar número de ejecuciones de archivos/carpetas + Hacer clic para lanzar o instalar Everything Instalación de Everything Instalando el servicio de Everything. Por favor, espere... @@ -142,4 +155,9 @@ ¿Desea activar la búsqueda de contenidos de Everything? Puede ser muy lento sin índice (solo se admite en Everything v1.5+) + + Menú contextual nativo + Mostrar menú contextual nativo (experimental) + En el siguiente cuadro puede especificar los elementos que desea incluir en el menú contextual, puede describirlos de forma parcial o completa (p. ej., 'ejecutar' o 'Abrir con'). + En el siguiente cuadro puede especificar los elementos que desea excluir del menú contextual, puede describirlos de forma parcial o completa (p. ej., 'ejecutar' o 'Abrir con'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/fr.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/fr.xaml index adad69af4db..754bdf54c6b 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/fr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/fr.xaml @@ -27,6 +27,12 @@ Personnaliser les mots-clés d'action Liens d'accès rapide Paramètres Everything + Panneau d'aperçu + Taille + Date de création + Date de modification + Afficher les informations du fichier + Format de la date et de l'heure Option de tri : Chemin vers Everything : Démarrer minimisé @@ -56,6 +62,10 @@ Moteur de recherche récursif du répertoire Moteur de recherche de l'index Ouvrir l'option d'index de Windows + Types de fichiers exclus (séparés par des virgules) + Par exemple : exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorateur @@ -103,10 +113,12 @@ Ouvrir avec Sélectionnez un programme à ouvrir avec - + {0} libres sur {1} Ouvrir dans le gestionnaire de fichiers par défaut - Utilisez '>' pour effectuer une recherche dans ce répertoire, '*' pour rechercher les extensions de fichiers ou '>*' pour combiner les deux recherches. + + Utilisez '>' pour effectuer une recherche dans ce répertoire, '*' pour rechercher les extensions de fichiers ou '>*' pour combiner les deux recherches. + Échec du chargement du SDK Everything @@ -131,7 +143,8 @@ Avertissement : Il ne s'agit pas d'une option de tri rapide, les recherches peuvent être lentes. Recherche du chemin complet - + Activer le nombre d'exécutions de fichiers/dossiers + Cliquez pour lancer ou installer Everything Installation d'Everything Installation du service Everything. Veuillez patienter... @@ -142,4 +155,9 @@ Voulez-vous activer la recherche de contenu pour Everything ? Il peut être très lent sans index (qui n'est supporté que dans Everything v1.5+). + + Menu contextuel natif + Afficher le menu contextuel natif (expérimental) + Ci-dessous, vous pouvez spécifier les éléments que vous souhaitez inclure dans le menu contextuel. Ils peuvent être partiels ('pen wit') ou complets ('Ouvrir avec'). + Vous pouvez spécifier ci-dessous les éléments que vous souhaitez exclure du menu contextuel. Ces éléments peuvent être partiels ('pen wit') ou complets ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/it.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/it.xaml index f69f15868f2..5e8b370c19e 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/it.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/it.xaml @@ -27,6 +27,12 @@ Personalizza Parola Chiave Collegamenti ad Accesso Rapido Impostazioni di Everything + Pannello Anteprima + Dimensioni + Data di creazione + Data della modifica + Visualizza Informazioni File + Formato data e ora Opzioni di ordinamento: Percorso di Everything: Avvia nascosto @@ -56,6 +62,10 @@ Motore di Ricerca Ricorsivo sulle Catelle Indice del Motore di Ricerca Apri Opzioni di Indicizzazione di Windows + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Esplora Risorse @@ -100,13 +110,15 @@ Rimuovi da Accesso Rapido Rimuovi elemento corrente dall'Accesso Rapido Mostra Menu Contestuale di Windows - Open With - Select a program to open with + Apri Con + Seleziona un programma con cui aprire - + {0} disponibili su {1} Aprire con il Gestore File Predefinito - Usa '>' per cercare in questa directory, '*' per cercare estensioni file o '>*' per combinare entrambe le ricerche. + + Usa '>' per cercare in questa directory, '*' per cercare estensioni file o '>*' per combinare entrambe le ricerche. + Impossibile caricare l'SDK di Everything @@ -131,7 +143,8 @@ Attenzione: Questa non è un'opzione di ordinamento rapido, le ricerche potrebbero essere lente Cerca Percorso Completo - + Enable File/Folder Run Count + Clicca per avviare o installare Everything Installazione di Everything Installazione di everything. Si prega di attendere... @@ -142,4 +155,9 @@ Vuoi abilitare la ricerca di contenuti per Everything? Può essere molto lento senza indice (che è supportato solo in Everything v1.5+) + + Menu Contestuale Nativo + Visualizza il menu contestuale nativo (sperimentale) + Sotto si può specificare elementi che si vuole includere nel menu contestuale, che possono essere parziali (es 'pri co') o completi ('Apri con'). + Sotto si può specificare elementi che si vuole escludere dal menu contestuale, che possono essere parziali (es 'pri co') o completi ('Apri con') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ja.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ja.xaml index d582424ed42..8171d3c10d7 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ja.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ja.xaml @@ -27,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + サイズ + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ko.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ko.xaml index 37a096f09e3..c6f7030b8d3 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ko.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ko.xaml @@ -27,6 +27,12 @@ 사용자 지정 액션 키워드 빠른 접근 항목 Everything 설정 + 미리보기 패널 + 크기 + 만든 날짜 + 수정한 날짜 + 파일 정보 표시 + 시간과 날짜 형식 정렬 옵션: Everything 경로: Launch Hidden @@ -34,7 +40,7 @@ 쉘 경로 색인 제외 경로 검색 결과위치를 실행 가능한 작업 디렉토리(Working Directory)로 사용 - Hit Enter to open folder in Default File Manager + Enter 키를 눌렀을 때 기본 파일 관리자에서 폴더 열기 Use Index Search For Path Search 색인 옵션 검색: @@ -56,6 +62,10 @@ 경로 재귀 검색 엔진 색인 검색 엔진 윈도우 색인 설정 열기 + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine 탐색기 @@ -100,13 +110,15 @@ 빠른 접근에서 제거 이 항목을 빠른 접근에서 제거 우클릭 메뉴 보기 - Open With - Select a program to open with + 함께 열기 + 이 항목을 열 프로그램을 선택 - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Everything을 실행 또는 설치하려면 클릭 Everything 설치 Everything 서비스 설치 중. 잠시 기다려주세요... @@ -142,4 +155,9 @@ Eveyrhing으로 내용 검색을 활성화하시겠습니까? 인덱스가 없으면 매우 느릴 수 있습니다.(Everything v1.5+에서만 지원) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nb.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nb.xaml index a5fd6c0c49a..3dbfb5fa4c5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nb.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nb.xaml @@ -27,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nl.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nl.xaml index a7a993b378b..b411f8d3057 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nl.xaml @@ -27,10 +27,16 @@ Customise Action Keywords Quick Access Links Everything Setting + Voorbeeld Paneel + Grootte + Datum aangemaakt + Datum gewijzigd + Bestandsinformatie weergeven + Formaat voor datum en tijd Sort Option: Everything Path: Launch Hidden - Editor Path + Editor pad Shell Path Index Search Excluded Paths Use search result's location as the working directory of the executable @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,43 +113,51 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK - Warning: Everything service is not running - Error while querying Everything - Sort By + Waarschuwing: Everything service is niet actief + Fout bij het opvragen Everything + Sorteer op Name - Path + Pad Size - Extension - Type Name - Date Created - Date Modified - Attributes - File List FileName - Run Count - Date Recently Changed - Date Accessed - Date Run + Uitbreiding + Type naam + Datum aangemaakt + Datum gewijzigd + Kenmerken + Bestandslijst Bestandsnaam + Aantal keer uitgevoerd + Datum onlangs gewijzigd + Datum laatst geopend + Datum uitvoering - Warning: This is not a Fast Sort option, searches may be slow + Waarschuwing: Dit is geen snelle sorteeroptie, zoekopdrachten kunnen traag zijn Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you + Everything geïnstalleerd + Installeren Everything alles service. Een ogenblik geduld... + Everything service is succesvol geïnstalleerd + Automatisch installeren mislukt. Installeer de Everything-service handmatig, vanaf https://www.voidtools.com + Klik hier om te starten + Kan geen 'Everything' vinden, wilt u handmatig een locatie selecteren?{0}{0}Klik op nee en alles zal automatisch voor u worden geïnstalleerd Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml index 13d9cf731bc..8eee1020137 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml @@ -27,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Rozmiar + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-br.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-br.xaml index 8e5761345d6..480d09de492 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-br.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-br.xaml @@ -27,6 +27,12 @@ Customise Action Keywords Links de Acesso Rápido Everything Setting + Preview Panel + Tamanho + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorador @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-pt.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-pt.xaml index 193ae0dd9db..3a3c0e8e5e7 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-pt.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-pt.xaml @@ -27,6 +27,12 @@ Personalizar palavras-chave Ligações de acesso rápido Definições Everything + Painel de pré-visualização + Tamanho + Data de criação + Data de modificação + Mostrar informações do ficheiro + Formato de data e de hora Ordenação: Caminho para Everything: Iniciar oculto @@ -56,6 +62,10 @@ Mecanismo de pesquisa recursiva de diretórios Mecanismo de pesquisa do índice Abrir opções de índice do Windows + Tipos de ficheiros excluídos (separados por vírgula) + Exemplo: exe,jpg,png + Número máximo de resultados + O número máximo de resultados solicitados ao motor de pesquisa ativo Explorador @@ -103,10 +113,12 @@ Abrir com Selecione o programa a utilizar - + {0} livre de {1} no total Abrir no gestor de ficheiros padrão - Utilize '>' para pesquisar nesta pasta, '*' para pesquisar por extensão de ficheiro e '>*' para combinar ambas as anteriores. + + Utilize '>' para pesquisar nesta pasta, '*' para pesquisar por extensão de ficheiro e '>*' para combinar ambas as anteriores. + Falha ao carregar Everything SDK @@ -131,7 +143,8 @@ Aviso: esta não é uma opção de ordenação rápida e as pesquisas podem ser demoradas Pesquisar caminho completo - + Ativar contagem de execução de ficheiros/pastas + Clique para iniciar ou instalar Everything Instalação Everything A instalar o serviço Everything. Por favor aguarde... @@ -142,4 +155,9 @@ Deseja ativar a pesquisa de conteúdo através de Everything? Pode ser muito lento sem índice (que apenas existe em Everything v1.5+) + + Menu de contexto nativo + Mostrar menu de contexto nativo (experimental) + Aqui pode especificar os itens a incluir no menu de contexto. Podem ser parciais (ex.: 'brir co') ou completos ('Abrir com'). + Aqui pode especificar os itens a excluir do menu de contexto. Podem ser parciais (ex.: 'brir co') ou completos ('Abrir com'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ru.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ru.xaml index cb7f2cda536..0febbe93043 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ru.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ru.xaml @@ -15,9 +15,9 @@ To fix this, start the Windows Search service. Select here to remove this warning The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative - Error occurred during search: {0} - Could not open folder - Could not open file + При поиске произошла ошибка: {0} + Не удалось открыть папку + Не удалось открыть файл Удалить @@ -27,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Размер + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml index e926735b885..b8b7c5f3be5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml @@ -27,6 +27,12 @@ Upraviť aktivačný príkaz Odkazy Rýchleho prístupu Nastavenia Everything + Panel náhľadu + Veľkosť + Dátum vytvorenia + Dátum úpravy + Zobraziť informácie o súbore + Formát dátumu a času Zoradenie: Umiestnenie Everything: Spustiť skryté @@ -56,6 +62,10 @@ Priečinkový rekurzívny vyhľadávač Indexový vyhľadávač Otvoriť možnosti vyhľadávania vo Windowse + Vylúčené typy súborov (oddelené čiarkou) + Napr.: exe,jpg,png + Maximum výsledkov + Maximálny počet výsledkov pre aktívny vyhľadávač Prieskumník @@ -103,10 +113,12 @@ Otvoriť s Vyberte program, ktorým chcete otvoriť súbor - + Voľných {0} z {1} Otvoriť v predvolenom správcovi súborov - Použite ">" na vyhľadávanie v tomto priečinku, "*" na vyhľadávanie prípon súborov alebo ">*" na kombináciu oboch hľadaní. + + Použite ">" na vyhľadávanie v tomto priečinku, "*" na vyhľadávanie prípon súborov alebo ">*" na kombináciu oboch hľadaní. + Nepodarilo sa načítať SDK Everything @@ -131,7 +143,8 @@ Upozornenie: Toto nie je voľba Fast Sort, vyhľadávanie môže byť pomalé Vyhľadávanie celej cesty - + Povoliť počítadlo spustení súboru/priečinka + Kliknutím spustíte alebo nainštalujete Everything Inštalácia Everything Inštaluje sa služba Everything. Čakajte, prosím… @@ -142,4 +155,9 @@ Chcete povoliť vyhľadávanie obsahu cez Everything? Bez indexu (ktorý je podporovaný len v Everything v1.5+) to môže byť veľmi pomalé + + Natívna kontextová ponuka + Zobraziť natívnu kontextovú ponuku (experimentálne) + Nižšie môžete určiť položky, ktoré chcete zahrnúť do kontextovej ponuky, môžu byť čiastočné (napr. "tvoriť v program") alebo úplné ("Otvoriť v programe"). + Nižšie môžete určiť položky, ktoré chcete vylúčiť z kontextovej ponuky, môžu byť čiastočné (napr. "tvoriť v program") alebo úplné ("Otvoriť v programe") diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sr.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sr.xaml index 2b6f087afa3..3942626c7a8 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sr.xaml @@ -27,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml index 2c90a77205b..c27da5d993e 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml @@ -5,141 +5,159 @@ Please make a selection first Lütfen bir klasör bağlantısı seçin {0} bağlantısını silmek istediğinize emin misiniz? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? + Bu dosyayı kalıcı olarak silmek istediğinizden emin misiniz? + Bu öğeyi kalıcı olarak silmek istediğinizden emin misiniz? Deletion successful - Successfully deleted {0} + Başarıyla silindi: {0} Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword The required service for Windows Index Search does not appear to be running To fix this, start the Windows Search service. Select here to remove this warning The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative - Error occurred during search: {0} - Could not open folder - Could not open file + Arama sırasında hata oluştu: {0} + Klasör açılamadı + Dosya açılamadı Sil Düzenle Ekle - General Setting - Customise Action Keywords + Genel Ayarlar + Anahtar Kelimeler Quick Access Links - Everything Setting - Sort Option: - Everything Path: + Everything Ayarları + Önizleme Paneli + Boyut + Oluşturma Tarihi + Değiştirme Tarihi + Dosya Özelliklerini Göster + Tarih ve saat biçimi + Sıralama Seçeneği: + Everything Kurulumu: Launch Hidden - Düzenleyici Konumu - Shell Path - Index Search Excluded Paths + Metin Düzenleyici + Komut İstemi + Hariç Tutulan Dizinler Programın çalışma klasörü olarak sonuç klasörünü kullan Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options - Search: - Path Search: - File Content Search: + Ara: + Yolu Ara: + Dosya İçeriğini Ara: Index Search: - Quick Access: - Current Action Keyword + Hızlı Erişim: + Geçerli Anahtar Kelime Tamam - Enabled + Etkin When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword Everything - Windows Index - Direct Enumeration - Düzenleyici Konumu - Folder Editor Path + Windows Arama + Doğrudan Sayım + Metin Düzenleyici + Klasör Düzenleyici - Content Search Engine + Dosya İçeriği Arama Motoru Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + Arama Motoru + Windows Dizin Oluşturma Ayarları + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine - Explorer + Dosya Gezgini Find and manage files and folders via Windows Search or Everything - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Dizini açmak içi Ctrl + Enter + Dosya konumunu açmak için Ctrl + Enter - Copy path - Copy path of current item to clipboard - Copy - Copy current file to clipboard - Copy current folder to clipboard + Yolu kopyala + Mevcut dosya konumunu panoya kopyala + Kopyala + Mevcut dosyayı panoya kopyala + Mevcut klasörü panoya kopyala Sil - Permanently delete current file - Permanently delete current folder - Path: - Delete the selected - Run as different user + Mevcut dosyayı kalıcı olarak sil + Mevcut klasörü kalıcı olarak sil + Yol: + Seçileni sil + Başka bir kullanıcı olarak çalıştır Run the selected using a different user account - Open containing folder + Dosya konumunu aç Open the location that contains current item - Open With Editor: + Metin Düzenleyici ile Aç: Failed to open file at {0} with Editor {1} at {2} - Open With Shell: + Komut İstemi ile Aç: Failed to open folder {0} with Shell {1} at {2} Exclude current and sub-directories from Index Search Excluded from Index Search - Open Windows Indexing Options + Windows Dizin Oluşturma Ayarları Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access + Windows Dizin Oluşturma ayarlarını açma başarısız oldu + Hızlı Erişime Sabitle Add current item to Quick Access - Successfully Added + Başarıyla Eklendi Successfully added to Quick Access - Successfully Removed + Başarıyla Kaldırıldı Successfully removed from Quick Access Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access + Hızlı Erişimden Kaldır + Hızlı Erişimden Kaldır Remove current item from Quick Access - Show Windows Context Menu - Open With + Windows Bağlam Menüsünü Aç + Birlikte Aç Select a program to open with - + {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Dosya Yöneticisinde Aç + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + - Failed to load Everything SDK + Everything SDK'sini yükleme başarısız oldu Everything Servisi çalışmıyor Sorgu Everything üzerinde çalıştırılırken hata oluştu - Sort By + Şuna Göre Sırala Name - Path + Yol Boyut - Extension - Type Name - Date Created - Date Modified - Attributes + Uzantı + Tür + Oluşturma Tarihi + Değiştirme Tarihi + Özellikler File List FileName - Run Count + Erişim Sayısı Date Recently Changed - Date Accessed + Erişim Tarihi Date Run Warning: This is not a Fast Sort option, searches may be slow - Search Full Path - - Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it + Tam Yolu Ara + Enable File/Folder Run Count + + Everything'i yükle veya başlat + Everything Kurulumu + Everything hizmeti yükleniyor. Lütfen bekleyin... + Everything hizmeti başarıyla yüklendi + Everything hizmetini otomatik olarak yükleme başarısız oldu. Lütfen https://www.voidtools.com adresinden elle yükleyin. + Başlat Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/uk-UA.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/uk-UA.xaml index 9909bf9c56c..19467d15087 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/uk-UA.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/uk-UA.xaml @@ -27,6 +27,12 @@ Налаштувати ключові слова дії Посилання швидкого доступу Налаштування Everything + Preview Panel + Розмір + Дата створення + Дата останньої зміни + Display File Info + Date and time format Варіант сортування: Шлях до Everything: Запустити приховано @@ -56,6 +62,10 @@ Рекурсивна пошукова система за каталогом Пошукова система індексів Відкрити параметри індексування Windows + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Провідник @@ -103,10 +113,12 @@ Відкрити за допомогою Виберіть програму для відкриття - + {0} не містить {1} Відкрити у файловому менеджері за замовчуванням - Використовуйте '>' для пошуку в цьому каталозі, '*' для пошуку за розширеннями файлів або '>*' для поєднання обох варіантів пошуку. + + Використовуйте '>' для пошуку в цьому каталозі, '*' для пошуку за розширеннями файлів або '>*' для поєднання обох варіантів пошуку. + Не вдалося завантажити Everything SDK @@ -131,7 +143,8 @@ Попередження: Це не швидке сортування, пошук може бути повільним Шукати повний шлях - + Enable File/Folder Run Count + Натисніть, щоб запустити або встановити Everything Встановлення програми Everything Встановлення служби Everything. Будь ласка, зачекайте... @@ -142,4 +155,9 @@ Бажаєте увімкнути пошук контенту для Everything? Без індексу (який підтримується лише у версії Everything v1.5+) воно може працювати дуже повільно + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/vi.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/vi.xaml new file mode 100644 index 00000000000..3d735bb052b --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/vi.xaml @@ -0,0 +1,163 @@ + + + + + Vui lòng lựa chọn trước + Hãy chọn một thư mục. + Bạn có chắc chắn muốn xóa {0} không? + Bạn có chắc là muốn xóa vĩnh viễn các ảnh này? + Bạn có chắc là muốn xóa vĩnh viễn các ảnh này? + Xóa thành công + Thành công trong việc xóa + Việc chỉ định từ khóa hành động chung có thể mang lại quá nhiều kết quả trong quá trình tìm kiếm. Vui lòng chọn từ khóa hành động cụ thể + Không thể đặt Truy cập nhanh thành từ khóa hành động chung khi được bật. Vui lòng chọn từ khóa hành động cụ thể + Dịch vụ cần thiết cho Windows Index Search dường như không chạy + Để khắc phục điều này, hãy khởi động dịch vụ Windows Search. Chọn vào đây để xóa cảnh báo này + Thông báo cảnh báo đã bị tắt. Là một giải pháp thay thế cho việc tìm kiếm tệp và thư mục, bạn có muốn cài đặt plugin Mọi thứ không?{0}{0}Chọn 'Có' để cài đặt plugin Mọi thứ hoặc 'Không' để quay lại + Nhà thám hiểm thay thế + Đã xảy ra lỗi trong quá trình tìm kiếm: {0} + Không thể mở thư mục + Không thể mở file + + + Xóa + Sửa + Thêm + Cài đặt chung + Tùy chỉnh từ khóa hành động + Liên kết truy cập nhanh + Cài đặt mọi thứ + Bảng xem trước + Kích thước + Date Created + Date Modified + Hiển thị thông tin tệp + Định dạng ngày và giờ + Tùy Chọn Sắp Xếp + Đường dẫn mọi thứ: + Khởi chạy ẩn + Đường dẫn soạn thảo + Đường dẫn Shell + Đường dẫn bị loại trừ tìm kiếm chỉ mục + Sử dụng vị trí của kết quả tìm kiếm làm thư mục làm việc của tệp thực thi + Chạy với tư cách quản trị viên / mở thư mục trong trình quản lý tệp tiêu chuẩn + Sử dụng Tìm kiếm Chỉ mục để Tìm kiếm Đường dẫn + Tùy chọn lập chỉ mục + Tìm kiếm trên web với sự hỗ trợ của công cụ tìm kiếm khác + Tìm kiếm đường dẫn: + Tìm kiếm nội dung tệp: + Tìm kiếm chỉ mục: + Truy cập nhanh + Từ hành động hiện tại + Xong + Đã bật + Khi bị tắt, Flow sẽ không thực thi tùy chọn tìm kiếm này và sẽ hoàn nguyên về '*' để giải phóng từ khóa hành động + Tất cả mọi thứ + Chỉ mục Windows + Đếm trực tiếp + Đường dẫn trình soạn thảo tệp + Folder Editor Path + + Công cụ tìm kiếm nội dung + Directory Recursive Search Engine + Công cụ tìm kiếm chỉ mục + Mở tùy chọn lập chỉ mục Windows + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine + + + Thư mục + Tìm và quản lý tệp và thư mục thông qua Windows Search hoặc Everything + + + Ctrl + Enter để mở thư mục + Ctrl + Enter để mở thư mục chứa + + + Copy đường dẫn + Sao chép đường dẫn của mục hiện tại vào clipboard + Sao chép + Sao chép tập tin hiện tại vào clipboard + Copy current folder to clipboard + Xóa + Permanently delete current file + Permanently delete current folder + Đường dẫn: + Xóa đã chọn + Xóa lựa chọn đã chọn + Chạy phần đã chọn bằng tài khoản người dùng khác + Mở thư mục chứa + Open the location that contains current item + Mở bằng trình chỉnh sửa: + Failed to open file at {0} with Editor {1} at {2} + Mở bằng Shell: + Failed to open folder {0} with Shell {1} at {2} + Loại trừ các thư mục hiện tại và thư mục con khỏi Tìm kiếm chỉ mục + Bị loại trừ khỏi Tìm kiếm chỉ mục + Mở tùy chọn lập chỉ mục Windows + Quản lý các tập tin và thư mục được lập chỉ mục + Quản lý các tập tin và thư mục được lập chỉ mục + Thêm vào Bảng truy cập nhanh + Thêm {0} hiện tại vào Truy cập nhanh + Đã thêm thành công + Đã thêm thành công vào Truy cập nhanh + Đã được gỡ bỏ thành công + Đã xóa thành công khỏi Truy cập nhanh + Thêm vào Truy cập nhanh để có thể mở nó bằng từ khóa hành động Kích hoạt tìm kiếm của Explorer + Loại bỏ khỏi bảng truy cập nhanh + Loại bỏ khỏi bảng truy cập nhanh + Xóa {0} hiện tại khỏi Truy cập nhanh + Show Windows Context Menu + Mở bằng + Select a program to open with + + + {0} phần {1} + Trình quản lý tệp mặc định + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + + + Failed to load Everything SDK + Warning: Everything service is not running + Error while querying Everything + Sắp xếp theo + Tên + Đường dẫn + Kích thước + Mở rộng + Loại tên + Ngày Tạo + ngày sửa đổi + Thuộc tính + File List FileName + Số lần chạy + Date Recently Changed + Ngày truy cập + Date Run + + + Warning: This is not a Fast Sort option, searches may be slow + + Search Full Path + Enable File/Folder Run Count + + Click to launch or install Everything + Everything Installation + Installing Everything service. Please wait... + Successfully installed Everything service + Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com + Click here to start it + Không thể tìm thấy bản cài đặt Mọi thứ, bạn có muốn chọn vị trí theo cách thủ công không?{0}{0}Nhấp vào không và Mọi thứ sẽ được cài đặt tự động cho bạn + Do you want to enable content search for Everything? + It can be very slow without index (which is only supported in Everything v1.5+) + + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml index 2899c77c646..430fbef6a21 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml @@ -27,6 +27,12 @@ 自定义动作关键字 快速访问链接 Everything 设置 + Preview Panel + 大小 + 创建日期 + 修改日期 + Display File Info + Date and time format 排序选项 Everything 路径 隐藏启动 @@ -56,6 +62,10 @@ 目录递归搜索引擎 索引搜索引擎 打开 Windows 索引选项 + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine 文件管理器 @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} 可用,共 {1} 在默认文件管理器中打开 - 使用 '>' 在这个目录中搜索,'*' 以搜索文件扩展名或 '>*' 以结合这两个搜索。 + + 使用 '>' 在这个目录中搜索,'*' 以搜索文件扩展名或 '>*' 以结合这两个搜索。 + 加载 Everything SDK 失败 @@ -131,7 +143,8 @@ 警告:这不是一个快速排序选项,搜索可能较慢。 搜索完整路径 - + Enable File/Folder Run Count + 单击启动或安装 Everything Everything 安装 正在安装 Everything 服务。请稍后... @@ -142,4 +155,9 @@ 您想要启用 Everything 的内容搜索功能吗? 如果没有索引,它可能会非常慢(索引仅在 Everything v1.5+ 中支持) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml index d60f1f09daa..230904115d6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml @@ -27,6 +27,12 @@ Customise Action Keywords 快速訪問連結 Everything Setting + Preview Panel + 大小 + 創建日期 + 修改日期 + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -56,6 +62,10 @@ Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine 檔案總管 @@ -103,10 +113,12 @@ Open With Select a program to open with - + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +143,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything 安裝程序 正在安裝 Everything 服務,請稍後... @@ -142,4 +155,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with') diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs index 2bb9a73c2f9..6c9155539fe 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs @@ -64,7 +64,11 @@ public async IAsyncEnumerable SearchAsync(string search, [Enumerat if (token.IsCancellationRequested) yield break; - var option = new EverythingSearchOption(search, Settings.SortOption, IsFullPathSearch: Settings.EverythingSearchFullPath); + var option = new EverythingSearchOption(search, + Settings.SortOption, + MaxCount: Settings.MaxResult, + IsFullPathSearch: Settings.EverythingSearchFullPath, + IsRunCounterEnabled: Settings.EverythingEnableRunCount); await foreach (var result in EverythingApi.SearchAsync(option, token)) yield return result; @@ -96,7 +100,9 @@ public async IAsyncEnumerable ContentSearchAsync(string plainSearc Settings.SortOption, IsContentSearch: true, ContentSearchKeyword: contentSearch, - IsFullPathSearch: Settings.EverythingSearchFullPath); + MaxCount: Settings.MaxResult, + IsFullPathSearch: Settings.EverythingSearchFullPath, + IsRunCounterEnabled: Settings.EverythingEnableRunCount); await foreach (var result in EverythingApi.SearchAsync(option, token)) { @@ -115,7 +121,9 @@ public async IAsyncEnumerable EnumerateAsync(string path, string s Settings.SortOption, ParentPath: path, IsRecursive: recursive, - IsFullPathSearch: Settings.EverythingSearchFullPath); + MaxCount: Settings.MaxResult, + IsFullPathSearch: Settings.EverythingSearchFullPath, + IsRunCounterEnabled: Settings.EverythingEnableRunCount); await foreach (var result in EverythingApi.SearchAsync(option, token)) yield return result; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs index 3d930becf50..92b8e96238e 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs @@ -11,6 +11,7 @@ public record struct EverythingSearchOption( bool IsRecursive = true, int Offset = 0, int MaxCount = 100, - bool IsFullPathSearch = true + bool IsFullPathSearch = true, + bool IsRunCounterEnabled = true ); } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index a87f766a1f9..1e7555a8d49 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -8,11 +8,16 @@ using System.Windows; using Flow.Launcher.Plugin.Explorer.Search.Everything; using System.Windows.Input; +using Path = System.IO.Path; +using System.Windows.Controls; +using Flow.Launcher.Plugin.Explorer.Views; +using Peter; namespace Flow.Launcher.Plugin.Explorer.Search { public static class ResultManager { + private static readonly string[] SizeUnits = { "B", "KB", "MB", "GB", "TB" }; private static PluginInitContext Context; private static Settings Settings { get; set; } @@ -66,6 +71,27 @@ public static Result CreateResult(Query query, SearchResult result) }; } + internal static void ShowNativeContextMenu(string path, ResultType type) + { + var screenWithMouseCursor = System.Windows.Forms.Screen.FromPoint(System.Windows.Forms.Cursor.Position); + var xOfScreenCenter = screenWithMouseCursor.WorkingArea.Left + screenWithMouseCursor.WorkingArea.Width / 2; + var yOfScreenCenter = screenWithMouseCursor.WorkingArea.Top + screenWithMouseCursor.WorkingArea.Height / 2; + var showPosition = new System.Drawing.Point(xOfScreenCenter, yOfScreenCenter); + + switch (type) + { + case ResultType.File: + var fileInfo = new FileInfo[] { new(path) }; + new ShellContextMenu().ShowContextMenu(fileInfo, showPosition); + break; + + case ResultType.Folder: + var folderInfo = new System.IO.DirectoryInfo[] { new(path) }; + new ShellContextMenu().ShowContextMenu(folderInfo, showPosition); + break; + } + } + internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool windowsIndexed = false) { return new Result @@ -76,8 +102,17 @@ internal static Result CreateFolderResult(string title, string subtitle, string AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder), TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, CopyText = path, + Preview = new Result.PreviewInfo + { + FilePath = path, + }, Action = c => { + if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) + { + ShowNativeContextMenu(path, ResultType.Folder); + return false; + } // open folder if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift)) { @@ -161,6 +196,10 @@ internal static Result CreateDriveSpaceDisplayResult(string path, string actionK Score = 500, ProgressBar = progressValue, ProgressBarColor = progressBarColor, + Preview = new Result.PreviewInfo + { + FilePath = path, + }, Action = _ => { OpenFolder(path); @@ -172,36 +211,28 @@ internal static Result CreateDriveSpaceDisplayResult(string path, string actionK }; } - private static string ToReadableSize(long pDrvSize, int pi) + internal static string ToReadableSize(long sizeOnDrive, int pi) { - int mok = 0; - double drvSize = pDrvSize; - string uom = "Byte"; // Unit Of Measurement + var unitIndex = 0; + double readableSize = sizeOnDrive; - while (drvSize > 1024.0) + while (readableSize > 1024.0 && unitIndex < SizeUnits.Length - 1) { - drvSize /= 1024.0; - mok++; + readableSize /= 1024.0; + unitIndex++; } - if (mok == 1) - uom = "KB"; - else if (mok == 2) - uom = " MB"; - else if (mok == 3) - uom = " GB"; - else if (mok == 4) - uom = " TB"; - - var returnStr = $"{Convert.ToInt32(drvSize)}{uom}"; - if (mok != 0) + var unit = SizeUnits[unitIndex] ?? ""; + + var returnStr = $"{Convert.ToInt32(readableSize)} {unit}"; + if (unitIndex != 0) { returnStr = pi switch { - 1 => $"{drvSize:F1}{uom}", - 2 => $"{drvSize:F2}{uom}", - 3 => $"{drvSize:F3}{uom}", - _ => $"{Convert.ToInt32(drvSize)}{uom}" + 1 => $"{readableSize:F1} {unit}", + 2 => $"{readableSize:F2} {unit}", + 3 => $"{readableSize:F3} {unit}", + _ => $"{Convert.ToInt32(readableSize)} {unit}" }; } @@ -222,8 +253,13 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK IcoPath = folderPath, Score = 500, CopyText = folderPath, - Action = _ => + Action = c => { + if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) + { + ShowNativeContextMenu(folderPath, ResultType.Folder); + return false; + } OpenFolder(folderPath); return true; }, @@ -233,24 +269,35 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) { - Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) - ? new Result.PreviewInfo { IsMedia = true, PreviewImagePath = filePath, } - : Result.PreviewInfo.Default; - + bool isMedia = IsMedia(Path.GetExtension(filePath)); var title = Path.GetFileName(filePath); + + /* Preview Detail */ + var result = new Result { Title = title, SubTitle = Path.GetDirectoryName(filePath), IcoPath = filePath, - Preview = preview, + Preview = new Result.PreviewInfo + { + IsMedia = isMedia, + PreviewImagePath = isMedia ? filePath : null, + FilePath = filePath, + }, AutoCompleteText = GetAutoCompleteText(title, query, filePath, ResultType.File), TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, Score = score, CopyText = filePath, + PreviewPanel = new Lazy(() => new PreviewPanel(Settings, filePath)), Action = c => { + if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) + { + ShowNativeContextMenu(filePath, ResultType.File); + return false; + } try { if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift)) @@ -301,7 +348,7 @@ private static void OpenFolder(string folderPath, string fileNameOrFilePath = nu private static void IncrementEverythingRunCounterIfNeeded(string fileOrFolder) { - if (Settings.EverythingEnabled) + if (Settings.EverythingEnabled && Settings.EverythingEnableRunCount) _ = Task.Run(() => EverythingApi.IncrementRunCounterAsync(fileOrFolder)); } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 5fe01d3a5c0..8fd1674765f 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Plugin.Explorer.Exceptions; +using Path = System.IO.Path; namespace Flow.Launcher.Plugin.Explorer.Search { @@ -68,7 +69,9 @@ internal async Task> SearchAsync(Query query, CancellationToken tok IAsyncEnumerable searchResults; - bool isPathSearch = query.Search.IsLocationPathString() || IsEnvironmentVariableSearch(query.Search); + bool isPathSearch = query.Search.IsLocationPathString() + || EnvironmentVariables.IsEnvironmentVariableSearch(query.Search) + || EnvironmentVariables.HasEnvironmentVar(query.Search); string engineName; @@ -107,7 +110,11 @@ when ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) try { await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false)) - results.Add(ResultManager.CreateResult(query, search)); + if (search.Type == ResultType.File && IsExcludedFile(search)) { + continue; + } else { + results.Add(ResultManager.CreateResult(query, search)); + } } catch (OperationCanceledException) { @@ -178,16 +185,16 @@ private async Task> PathSearchAsync(Query query, CancellationToken // Query is a location path with a full environment variable, eg. %appdata%\somefolder\, c:\users\%USERNAME%\downloads var needToExpand = EnvironmentVariables.HasEnvironmentVar(querySearch); - var locationPath = needToExpand ? Environment.ExpandEnvironmentVariables(querySearch) : querySearch; + var path = needToExpand ? Environment.ExpandEnvironmentVariables(querySearch) : querySearch; // Check that actual location exists, otherwise directory search will throw directory not found exception - if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath).LocationExists()) + if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path).LocationExists()) return results.ToList(); var useIndexSearch = Settings.IndexSearchEngine is Settings.IndexSearchEngineOption.WindowsIndex - && UseWindowsIndexForDirectorySearch(locationPath); + && UseWindowsIndexForDirectorySearch(path); - var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath); + var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); results.Add(retrievedDirectoryPath.EndsWith(":\\") ? ResultManager.CreateDriveSpaceDisplayResult(retrievedDirectoryPath, query.ActionKeyword, useIndexSearch) @@ -198,21 +205,21 @@ private async Task> PathSearchAsync(Query query, CancellationToken IAsyncEnumerable directoryResult; - var recursiveIndicatorIndex = query.Search.IndexOf('>'); + var recursiveIndicatorIndex = path.IndexOf('>'); if (recursiveIndicatorIndex > 0 && Settings.PathEnumerationEngine != Settings.PathEnumerationEngineOption.DirectEnumeration) { directoryResult = Settings.PathEnumerator.EnumerateAsync( - query.Search[..recursiveIndicatorIndex].Trim(), - query.Search[(recursiveIndicatorIndex + 1)..], + path[..recursiveIndicatorIndex].Trim(), + path[(recursiveIndicatorIndex + 1)..], true, token); } else { - directoryResult = DirectoryInfoSearch.TopLevelDirectorySearch(query, query.Search, token).ToAsyncEnumerable(); + directoryResult = DirectoryInfoSearch.TopLevelDirectorySearch(query, path, token).ToAsyncEnumerable(); } if (token.IsCancellationRequested) @@ -246,11 +253,12 @@ private bool UseWindowsIndexForDirectorySearch(string locationPath) && WindowsIndex.WindowsIndex.PathIsIndexed(pathToDirectory); } - internal static bool IsEnvironmentVariableSearch(string search) + private bool IsExcludedFile(SearchResult result) { - return search.StartsWith("%") - && search != "%%" - && !search.Contains('\\'); + string[] excludedFileTypes = Settings.ExcludedFileTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + string fileExtension = Path.GetExtension(result.FullPath).TrimStart('.'); + + return excludedFileTypes.Contains(fileExtension, StringComparer.OrdinalIgnoreCase); } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs index 137ba2f1982..3d30bcf29e5 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -25,10 +25,16 @@ public class Settings public string ShellPath { get; set; } = "cmd"; + public string ExcludedFileTypes { get; set; } = ""; + public bool UseLocationAsWorkingDir { get; set; } = false; - public bool ShowWindowsContextMenu { get; set; } = true; + public bool ShowInlinedWindowsContextMenu { get; set; } = false; + + public string WindowsContextMenuIncludedItems { get; set; } = string.Empty; + + public string WindowsContextMenuExcludedItems { get; set; } = string.Empty; public bool DefaultOpenFolderInFileManager { get; set; } = false; @@ -55,6 +61,16 @@ public class Settings public bool WarnWindowsSearchServiceOff { get; set; } = true; + public bool ShowFileSizeInPreviewPanel { get; set; } = true; + + public bool ShowCreatedDateInPreviewPanel { get; set; } = true; + + public bool ShowModifiedDateInPreviewPanel { get; set; } = true; + + public string PreviewPanelDateFormat { get; set; } = "yyyy-MM-dd"; + + public string PreviewPanelTimeFormat { get; set; } = "HH:mm"; + private EverythingSearchManager _everythingManagerInstance; private WindowsIndexSearchManager _windowsIndexSearchManager; @@ -137,7 +153,8 @@ public enum ContentIndexSearchEngineOption ContentSearchEngine == ContentIndexSearchEngineOption.Everything; public bool EverythingSearchFullPath { get; set; } = false; - + public bool EverythingEnableRunCount { get; set; } = true; + #endregion internal enum ActionKeyword diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 02199fb5aef..309f7a6f7ac 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Linq; using System.Windows; @@ -101,8 +102,142 @@ private void InitializeEngineSelection() #endregion + #region Native Context Menu + + public bool ShowWindowsContextMenu + { + get => Settings.ShowInlinedWindowsContextMenu; + set + { + Settings.ShowInlinedWindowsContextMenu = value; + OnPropertyChanged(); + } + } + + public string WindowsContextMenuIncludedItems + { + get => Settings.WindowsContextMenuIncludedItems; + set + { + Settings.WindowsContextMenuIncludedItems = value; + OnPropertyChanged(); + } + } + + public string WindowsContextMenuExcludedItems + { + get => Settings.WindowsContextMenuExcludedItems; + set + { + Settings.WindowsContextMenuExcludedItems = value; + OnPropertyChanged(); + } + } + + #endregion + + #region Preview Panel + + public bool ShowFileSizeInPreviewPanel + { + get => Settings.ShowFileSizeInPreviewPanel; + set + { + Settings.ShowFileSizeInPreviewPanel = value; + OnPropertyChanged(); + } + } + + public bool ShowCreatedDateInPreviewPanel + { + get => Settings.ShowCreatedDateInPreviewPanel; + set + { + Settings.ShowCreatedDateInPreviewPanel = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ShowPreviewPanelDateTimeChoices)); + OnPropertyChanged(nameof(PreviewPanelDateTimeChoicesVisibility)); + } + } + + public bool ShowModifiedDateInPreviewPanel + { + get => Settings.ShowModifiedDateInPreviewPanel; + set + { + Settings.ShowModifiedDateInPreviewPanel = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ShowPreviewPanelDateTimeChoices)); + OnPropertyChanged(nameof(PreviewPanelDateTimeChoicesVisibility)); + } + } + + public string PreviewPanelDateFormat + { + get => Settings.PreviewPanelDateFormat; + set + { + Settings.PreviewPanelDateFormat = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(PreviewPanelDateFormatDemo)); + } + } + + public string PreviewPanelTimeFormat + { + get => Settings.PreviewPanelTimeFormat; + set + { + Settings.PreviewPanelTimeFormat = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(PreviewPanelTimeFormatDemo)); + } + } + + public string PreviewPanelDateFormatDemo => DateTime.Now.ToString(PreviewPanelDateFormat, CultureInfo.CurrentCulture); + public string PreviewPanelTimeFormatDemo => DateTime.Now.ToString(PreviewPanelTimeFormat, CultureInfo.CurrentCulture); + + public bool ShowPreviewPanelDateTimeChoices => ShowCreatedDateInPreviewPanel || ShowModifiedDateInPreviewPanel; + + public Visibility PreviewPanelDateTimeChoicesVisibility => ShowCreatedDateInPreviewPanel || ShowModifiedDateInPreviewPanel ? Visibility.Visible : Visibility.Collapsed; + public List TimeFormatList { get; } = new() + { + "h:mm", + "hh:mm", + "H:mm", + "HH:mm", + "tt h:mm", + "tt hh:mm", + "h:mm tt", + "hh:mm tt", + "hh:mm:ss tt", + "HH:mm:ss" + }; + + + public List DateFormatList { get; } = new() + { + "dd/MM/yyyy", + "dd/MM/yyyy ddd", + "dd/MM/yyyy, dddd", + "dd-MM-yyyy", + "dd-MM-yyyy ddd", + "dd-MM-yyyy, dddd", + "dd.MM.yyyy", + "dd.MM.yyyy ddd", + "dd.MM.yyyy, dddd", + "MM/dd/yyyy", + "MM/dd/yyyy ddd", + "MM/dd/yyyy, dddd", + "yyyy-MM-dd", + "yyyy-MM-dd ddd", + "yyyy-MM-dd, dddd", + }; + + #endregion + #region ActionKeyword [MemberNotNull(nameof(ActionKeywordsModels))] @@ -378,6 +513,31 @@ public string ShellPath } } + public string ExcludedFileTypes + { + get => Settings.ExcludedFileTypes; + set + { + // remove spaces and dots from the string before saving + string sanitized = string.IsNullOrEmpty(value) ? "" : value.Replace(" ", "").Replace(".", ""); + Settings.ExcludedFileTypes = sanitized; + OnPropertyChanged(); + } + } + + public int MaxResultLowerLimit => 100; + public int MaxResultUpperLimit => 100000; + + public int MaxResult + { + get => Settings.MaxResult; + set + { + Settings.MaxResult = Math.Clamp(value, MaxResultLowerLimit, MaxResultUpperLimit); + OnPropertyChanged(); + } + } + #region Everything FastSortWarning diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml index b0708b793b6..001859caaf0 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml @@ -22,7 +22,7 @@ @@ -105,13 +105,13 @@ - + @@ -128,7 +128,7 @@ @@ -160,20 +160,20 @@ Width="Auto" Header="{DynamicResource plugin_explorer_generalsetting_header}" Style="{DynamicResource ExplorerTabItem}"> - + - + @@ -188,7 +188,7 @@