Merge pull request #957 from Flow-Launcher/dev
Release 1.10.0 | Plugin 3.0.0
146
.editorconfig
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
|
||||
###############################
|
||||
# Core EditorConfig Options #
|
||||
###############################
|
||||
# All files
|
||||
[*]
|
||||
indent_style = space
|
||||
|
||||
# XML project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# XML config files
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# Code files
|
||||
[*.{cs,csx,vb,vbx}]
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
charset = utf-8-bom
|
||||
###############################
|
||||
# .NET Coding Conventions #
|
||||
###############################
|
||||
[*.{cs,vb}]
|
||||
# Organize usings
|
||||
dotnet_sort_system_directives_first = true
|
||||
# this. preferences
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
# Language keywords vs BCL types preferences
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||
dotnet_style_predefined_type_for_member_access = true:silent
|
||||
# Parentheses preferences
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
|
||||
# Modifier preferences
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
# Expression-level preferences
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
###############################
|
||||
# Naming Conventions #
|
||||
###############################
|
||||
# Style Definitions
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
# Use PascalCase for constant fields
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
|
||||
dotnet_naming_symbols.constant_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.constant_fields.required_modifiers = const
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 2
|
||||
end_of_line = crlf
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_diagnostic.CA1416.severity = silent
|
||||
###############################
|
||||
# C# Coding Conventions #
|
||||
###############################
|
||||
[*.cs]
|
||||
dotnet_diagnostics.VSTHRD200.severity = none # VSTHRD200: Use "Async" suffix for async methods
|
||||
dotnet_analyzer_diagnostic.VSTHRD200.severity = none # VSTHRD200: Use "Async" suffix for async methods
|
||||
# var preferences
|
||||
csharp_style_var_for_built_in_types = true:silent
|
||||
csharp_style_var_when_type_is_apparent = true:silent
|
||||
csharp_style_var_elsewhere = true:silent
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
# Pattern matching preferences
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
# Null-checking preferences
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
# Modifier preferences
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
|
||||
# Expression-level preferences
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
###############################
|
||||
# C# Formatting Rules #
|
||||
###############################
|
||||
# New line preferences
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
# Indentation preferences
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
# Wrapping preferences
|
||||
csharp_preserve_single_line_statements = true
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
###############################
|
||||
# VB Coding Conventions #
|
||||
###############################
|
||||
[*.vb]
|
||||
# Modifier preferences
|
||||
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
|
||||
32
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
name: "\U0001F41E Bug report"
|
||||
about: Create a bug report to help us improve Flow Launcher
|
||||
title: "[Describe Your Bug]"
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug/issue**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Your System**
|
||||
```
|
||||
Windows build number: (run "ver" at a command prompt)
|
||||
Flow Launcher version: (Settings => About)
|
||||
```
|
||||
**Flow Launcher Error Log**
|
||||
<!--
|
||||
The latest log file can be found here: %AppData%\FlowLauncher\Logs\<version>\<date>.txt
|
||||
or for portable mode: %LocalAppData%\FlowLauncher\<App-Version>\UserData\Logs\<version>\<date>.txt
|
||||
Drag and drop the log file below this comment.
|
||||
-->
|
||||
78
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
name: "\U0001F41E Bug Report"
|
||||
description: Create a bug report to help us improve Flow Launcher
|
||||
title: "BUG: "
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checks
|
||||
options:
|
||||
- label: >
|
||||
I have checked that this issue has not already been reported.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: A clear and concise description of what the problem is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
value: >
|
||||
1. ...
|
||||
|
||||
2. ...
|
||||
|
||||
3. ...
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Flow Launcher Version
|
||||
description: Go to "Settings" => "About".
|
||||
value: v1.8.3
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Windows Build Number
|
||||
description: Run "ver" at CMD (command prompt).
|
||||
value: 10.0.19043.1288
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Error Log
|
||||
description: >
|
||||
Log file place:
|
||||
|
||||
- The latest version place: `%AppData%\FlowLauncher\Logs\<version>\<date>.txt`
|
||||
|
||||
- For portable mode: `%LocalAppData%\FlowLauncher\<App-Version>\UserData\Logs\<version>\<date>.txt`
|
||||
value: >
|
||||
<details>
|
||||
|
||||
|
||||
```shell
|
||||
|
||||
|
||||
Replace this line with the important log contents.
|
||||
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<!-- # Or drag and drop the log file and delete the above detail part. -->
|
||||
17
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "nuget" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "squirrel-windows"
|
||||
reviewers:
|
||||
- "jjw24"
|
||||
- "taooceros"
|
||||
- "JohnTheGr8"
|
||||
26
.github/workflows/stale.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
days-before-stale: 45
|
||||
days-before-close: 7
|
||||
days-before-pr-close: -1
|
||||
exempt-all-milestones: true
|
||||
close-issue-message: 'This issue was closed because it has been stale for 7 days with no activity. If you feel this issue still needs attention please feel free to reopen.'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
exempt-issue-labels: 'keep-fresh'
|
||||
exempt-pr-labels: 'keep-fresh,awaiting-approval,work-in-progress'
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.Win32;
|
||||
using Microsoft.Win32;
|
||||
using Squirrel;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
|
@ -127,7 +127,7 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -10,43 +12,43 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
{
|
||||
public static class PluginsManifest
|
||||
{
|
||||
static PluginsManifest()
|
||||
{
|
||||
UpdateTask = UpdateManifestAsync();
|
||||
}
|
||||
|
||||
public static List<UserPlugin> UserPlugins { get; private set; } = new List<UserPlugin>();
|
||||
|
||||
public static Task UpdateTask { get; private set; }
|
||||
private const string manifestFileUrl = "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json";
|
||||
|
||||
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
||||
|
||||
public static Task UpdateManifestAsync()
|
||||
{
|
||||
if (manifestUpdateLock.CurrentCount == 0)
|
||||
{
|
||||
return UpdateTask;
|
||||
}
|
||||
private static string latestEtag = "";
|
||||
|
||||
return UpdateTask = DownloadManifestAsync();
|
||||
}
|
||||
public static List<UserPlugin> UserPlugins { get; private set; } = new List<UserPlugin>();
|
||||
|
||||
private static async Task DownloadManifestAsync()
|
||||
public static async Task UpdateManifestAsync(CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await manifestUpdateLock.WaitAsync().ConfigureAwait(false);
|
||||
await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false);
|
||||
|
||||
await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json")
|
||||
.ConfigureAwait(false);
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl);
|
||||
request.Headers.Add("If-None-Match", latestEtag);
|
||||
|
||||
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(jsonStream).ConfigureAwait(false);
|
||||
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo");
|
||||
|
||||
await using var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
|
||||
|
||||
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(json, cancellationToken: token).ConfigureAwait(false);
|
||||
|
||||
latestEtag = response.Headers.ETag.Tag;
|
||||
}
|
||||
else if (response.StatusCode != HttpStatusCode.NotModified)
|
||||
{
|
||||
Log.Warn($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http response for manifest file was {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception("|PluginManagement.GetManifest|Encountered error trying to download plugins manifest", e);
|
||||
|
||||
UserPlugins = new List<UserPlugin>();
|
||||
Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -54,4 +56,4 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
using System;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
public record UserPlugin
|
||||
{
|
||||
|
|
@ -12,5 +14,8 @@
|
|||
public string UrlDownload { get; set; }
|
||||
public string UrlSourceCode { get; set; }
|
||||
public string IcoPath { get; set; }
|
||||
public DateTime LatestReleaseDate { get; set; }
|
||||
public DateTime DateAdded { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<UseWpf>true</UseWpf>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<OutputType>Library</OutputType>
|
||||
|
|
@ -54,9 +54,9 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Droplex" Version="1.4.1" />
|
||||
<PackageReference Include="FSharp.Core" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
|
||||
<PackageReference Include="squirrel.windows" Version="1.5.2" />
|
||||
<PackageReference Include="FSharp.Core" Version="6.0.6" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
|
||||
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -93,4 +93,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public Dictionary<string, object> SettingsChange { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -26,6 +27,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public string Name { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string urlLabel { get; set; }
|
||||
public Uri url { get; set; }
|
||||
public bool Validation { get; set; }
|
||||
public List<string> Options { get; set; }
|
||||
public string DefaultValue { get; set; }
|
||||
|
|
@ -40,4 +43,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
DefaultValue = this.DefaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,31 @@
|
|||
using Accessibility;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using CheckBox = System.Windows.Controls.CheckBox;
|
||||
using Control = System.Windows.Controls.Control;
|
||||
using Label = System.Windows.Controls.Label;
|
||||
using Orientation = System.Windows.Controls.Orientation;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using static System.Windows.Forms.LinkLabel;
|
||||
using Droplex;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -39,7 +37,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
protected PluginInitContext context;
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
|
||||
/// <summary>
|
||||
/// The language this JsonRPCPlugin support
|
||||
/// </summary>
|
||||
|
|
@ -69,7 +66,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
private static readonly JsonSerializerOptions options = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
#pragma warning disable SYSLIB0020
|
||||
// IgnoreNullValues is obsolete, but the replacement JsonIgnoreCondition.WhenWritingNull still
|
||||
// deserializes null, instead of ignoring it and leaving the default (empty list). We can change the behaviour
|
||||
// to accept null and fallback to a default etc, or just keep IgnoreNullValues for now
|
||||
// see: https://github.com/dotnet/runtime/issues/39152
|
||||
IgnoreNullValues = true,
|
||||
#pragma warning restore SYSLIB0020 // Type or member is obsolete
|
||||
Converters =
|
||||
{
|
||||
new JsonObjectConverter()
|
||||
|
|
@ -82,16 +85,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
};
|
||||
private Dictionary<string, object> Settings { get; set; }
|
||||
|
||||
private Dictionary<string, FrameworkElement> _settingControls = new();
|
||||
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
||||
|
||||
private async Task<List<Result>> DeserializedResultAsync(Stream output)
|
||||
{
|
||||
if (output == Stream.Null) return null;
|
||||
await using (output)
|
||||
{
|
||||
if (output == Stream.Null) return null;
|
||||
|
||||
var queryResponseModel =
|
||||
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
|
||||
var queryResponseModel =
|
||||
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
|
||||
|
||||
return ParseResults(queryResponseModel);
|
||||
return ParseResults(queryResponseModel);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Result> DeserializedResult(string output)
|
||||
|
|
@ -115,7 +121,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
foreach (var result in queryResponseModel.Result)
|
||||
{
|
||||
result.Action = c =>
|
||||
result.AsyncAction = async c =>
|
||||
{
|
||||
UpdateSettings(result.SettingsChange);
|
||||
|
||||
|
|
@ -133,15 +139,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
else
|
||||
{
|
||||
var actionResponse = Request(result.JsonRPCAction);
|
||||
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
|
||||
|
||||
if (string.IsNullOrEmpty(actionResponse))
|
||||
if (actionResponse.Length == 0)
|
||||
{
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
}
|
||||
|
||||
var jsonRpcRequestModel =
|
||||
JsonSerializer.Deserialize<JsonRPCRequestModel>(actionResponse, options);
|
||||
var jsonRpcRequestModel = await
|
||||
JsonSerializer.DeserializeAsync<JsonRPCRequestModel>(actionResponse, options);
|
||||
|
||||
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
|
||||
{
|
||||
|
|
@ -166,19 +172,20 @@ namespace Flow.Launcher.Core.Plugin
|
|||
private void ExecuteFlowLauncherAPI(string method, object[] parameters)
|
||||
{
|
||||
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
|
||||
MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method, parametersTypeArray);
|
||||
if (methodInfo != null)
|
||||
var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray);
|
||||
if (methodInfo == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
methodInfo.Invoke(PluginManager.API, parameters);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
methodInfo.Invoke(PluginManager.API, parameters);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
#if (DEBUG)
|
||||
throw;
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -240,74 +247,55 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default)
|
||||
{
|
||||
Process process = null;
|
||||
using var exitTokenSource = new CancellationTokenSource();
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
var sourceBuffer = BufferManager.GetStream();
|
||||
using var errorBuffer = BufferManager.GetStream();
|
||||
|
||||
var sourceCopyTask = process.StandardOutput.BaseStream.CopyToAsync(sourceBuffer, token);
|
||||
var errorCopyTask = process.StandardError.BaseStream.CopyToAsync(errorBuffer, token);
|
||||
|
||||
await using var registeredEvent = token.Register(() =>
|
||||
{
|
||||
if (!process.HasExited)
|
||||
process.Kill();
|
||||
sourceBuffer.Dispose();
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
|
||||
await using var source = process.StandardOutput.BaseStream;
|
||||
|
||||
var buffer = BufferManager.GetStream();
|
||||
|
||||
token.Register(() =>
|
||||
{
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
// Manually Check whether disposed
|
||||
if (!exitTokenSource.IsCancellationRequested && !process.HasExited)
|
||||
process.Kill();
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// token expire won't instantly trigger the exception,
|
||||
// manually kill process at before
|
||||
await source.CopyToAsync(buffer, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
await buffer.DisposeAsync();
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
buffer.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
var errorMessage = process.StandardError.EndOfStream ?
|
||||
"Empty JSONRPC Response" :
|
||||
await process.StandardError.ReadToEndAsync();
|
||||
throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}");
|
||||
}
|
||||
|
||||
if (!process.StandardError.EndOfStream)
|
||||
{
|
||||
using var standardError = process.StandardError;
|
||||
var error = await standardError.ReadToEndAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}");
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
// token expire won't instantly trigger the exception,
|
||||
// manually kill process at before
|
||||
await process.WaitForExitAsync(token);
|
||||
await Task.WhenAll(sourceCopyTask, errorCopyTask);
|
||||
}
|
||||
finally
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
exitTokenSource.Cancel();
|
||||
process?.Dispose();
|
||||
await sourceBuffer.DisposeAsync();
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
switch (sourceBuffer.Length, errorBuffer.Length)
|
||||
{
|
||||
case (0, 0):
|
||||
const string errorMessage = "Empty JSON-RPC Response.";
|
||||
Log.Warn($"|{nameof(JsonRPCPlugin)}.{nameof(ExecuteAsync)}|{errorMessage}");
|
||||
break;
|
||||
case (_, not 0):
|
||||
throw new InvalidDataException(Encoding.UTF8.GetString(errorBuffer.ToArray())); // The process has exited with an error message
|
||||
}
|
||||
|
||||
sourceBuffer.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
return sourceBuffer;
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
var request = new JsonRPCRequestModel
|
||||
|
|
@ -355,126 +343,240 @@ namespace Flow.Launcher.Core.Plugin
|
|||
this.context = context;
|
||||
await InitSettingAsync();
|
||||
}
|
||||
private static readonly Thickness settingControlMargin = new(10, 4, 10, 4);
|
||||
private static readonly Thickness settingPanelMargin = new(15, 20, 15, 20);
|
||||
private static readonly Thickness settingTextBlockMargin = new(10, 4, 10, 4);
|
||||
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
|
||||
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
|
||||
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
|
||||
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
|
||||
private JsonRpcConfigurationModel _settingsTemplate;
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
{
|
||||
if (Settings == null)
|
||||
return new();
|
||||
var settingWindow = new UserControl();
|
||||
var mainPanel = new StackPanel
|
||||
var mainPanel = new Grid
|
||||
{
|
||||
Margin = settingPanelMargin,
|
||||
Orientation = Orientation.Vertical
|
||||
Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
settingWindow.Content = mainPanel;
|
||||
ColumnDefinition gridCol1 = new ColumnDefinition();
|
||||
ColumnDefinition gridCol2 = new ColumnDefinition();
|
||||
|
||||
gridCol1.Width = new GridLength(70, GridUnitType.Star);
|
||||
gridCol2.Width = new GridLength(30, GridUnitType.Star);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol1);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol2);
|
||||
settingWindow.Content = mainPanel;
|
||||
int rowCount = 0;
|
||||
foreach (var (type, attribute) in _settingsTemplate.Body)
|
||||
{
|
||||
Separator sep = new Separator();
|
||||
sep.VerticalAlignment = VerticalAlignment.Top;
|
||||
sep.Margin = settingSepMargin;
|
||||
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = settingControlMargin
|
||||
Orientation = Orientation.Vertical, VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelPanelMargin
|
||||
};
|
||||
RowDefinition gridRow = new RowDefinition();
|
||||
mainPanel.RowDefinitions.Add(gridRow);
|
||||
var name = new TextBlock()
|
||||
{
|
||||
Text = attribute.Label,
|
||||
Width = 120,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingControlMargin,
|
||||
Margin = settingLabelMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
var desc = new TextBlock()
|
||||
{
|
||||
Text = attribute.Description, FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,Margin = settingDescMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
|
||||
|
||||
if (attribute.Description == null) /* if no description, hide */
|
||||
desc.Visibility = Visibility.Collapsed;
|
||||
|
||||
|
||||
if (type != "textBlock") /* if textBlock, hide desc */
|
||||
{
|
||||
panel.Children.Add(name);
|
||||
panel.Children.Add(desc);
|
||||
}
|
||||
|
||||
|
||||
Grid.SetColumn(panel, 0);
|
||||
Grid.SetRow(panel, rowCount);
|
||||
|
||||
FrameworkElement contentControl;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "textBlock":
|
||||
{
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||
Margin = settingTextBlockMargin,
|
||||
MaxWidth = 500,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||
Margin = settingTextBlockMargin,
|
||||
Padding = new Thickness(0,0,0,0),
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case "input":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "inputWithFileBtn":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Width = 300,
|
||||
Margin = new Thickness(10, 0, 0, 0),
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
var Btn = new System.Windows.Controls.Button()
|
||||
{
|
||||
Margin = new Thickness(10,0,0,0),
|
||||
Content = "Browse"
|
||||
};
|
||||
var dockPanel = new DockPanel()
|
||||
{
|
||||
Margin = settingControlMargin
|
||||
};
|
||||
DockPanel.SetDock(Btn, Dock.Right);
|
||||
dockPanel.Children.Add(Btn);
|
||||
dockPanel.Children.Add(textBox);
|
||||
contentControl = dockPanel;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "textarea":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Width = 300,
|
||||
Height = 120,
|
||||
Margin = settingControlMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
Height = 120,
|
||||
Margin = settingControlMargin,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case "passwordBox":
|
||||
{
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
Width = 300,
|
||||
Margin = settingControlMargin,
|
||||
Password = Settings[attribute.Name] as string ?? string.Empty,
|
||||
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
contentControl = passwordBox;
|
||||
Margin = settingControlMargin,
|
||||
Password = Settings[attribute.Name] as string ?? string.Empty,
|
||||
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
contentControl = passwordBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case "dropdown":
|
||||
{
|
||||
var comboBox = new System.Windows.Controls.ComboBox()
|
||||
{
|
||||
var comboBox = new ComboBox()
|
||||
{
|
||||
ItemsSource = attribute.Options,
|
||||
SelectedItem = Settings[attribute.Name],
|
||||
Margin = settingControlMargin,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem;
|
||||
};
|
||||
contentControl = comboBox;
|
||||
ItemsSource = attribute.Options,
|
||||
SelectedItem = Settings[attribute.Name],
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
|
||||
};
|
||||
contentControl = comboBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case "checkbox":
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue),
|
||||
Margin = settingControlMargin,
|
||||
Margin = settingCheckboxMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
checkBox.Click += (sender, _) =>
|
||||
|
|
@ -482,18 +584,49 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
|
||||
};
|
||||
contentControl = checkBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
case "hyperlink":
|
||||
var hyperlink = new Hyperlink
|
||||
{
|
||||
ToolTip = attribute.Description,
|
||||
NavigateUri = attribute.url
|
||||
};
|
||||
var linkbtn = new System.Windows.Controls.Button
|
||||
{
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
Margin = settingControlMargin
|
||||
};
|
||||
linkbtn.Content = attribute.urlLabel;
|
||||
|
||||
contentControl = linkbtn;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
if (type != "textBlock")
|
||||
_settingControls[attribute.Name] = contentControl;
|
||||
panel.Children.Add(name);
|
||||
panel.Children.Add(contentControl);
|
||||
mainPanel.Children.Add(panel);
|
||||
mainPanel.Children.Add(contentControl);
|
||||
rowCount++;
|
||||
|
||||
}
|
||||
return settingWindow;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
if (Settings != null)
|
||||
|
|
@ -525,7 +658,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
case PasswordBox passwordBox:
|
||||
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string);
|
||||
break;
|
||||
case ComboBox comboBox:
|
||||
case System.Windows.Controls.ComboBox comboBox:
|
||||
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
||||
break;
|
||||
case CheckBox checkBox:
|
||||
|
|
@ -536,4 +669,5 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
public static async Task ReloadData()
|
||||
public static async Task ReloadDataAsync()
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
|
|
@ -110,7 +110,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// Call initialize for all plugins
|
||||
/// </summary>
|
||||
/// <returns>return the list of failed to init plugins or null for none</returns>
|
||||
public static async Task InitializePlugins(IPublicAPI api)
|
||||
public static async Task InitializePluginsAsync(IPublicAPI api)
|
||||
{
|
||||
API = api;
|
||||
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
||||
|
|
@ -329,4 +329,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -11,7 +11,6 @@ using Flow.Launcher.Infrastructure.Logger;
|
|||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System.Diagnostics;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
|
|
@ -41,14 +40,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var milliseconds = Stopwatch.Debug(
|
||||
$"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () =>
|
||||
{
|
||||
#if DEBUG
|
||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||
var assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||
typeof(IAsyncPlugin));
|
||||
|
||||
var plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
#else
|
||||
Assembly assembly = null;
|
||||
IAsyncPlugin plugin = null;
|
||||
|
||||
|
|
@ -62,6 +53,12 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception e)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
catch (Exception e) when (assembly == null)
|
||||
{
|
||||
Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e);
|
||||
|
|
@ -79,6 +76,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
erroredPlugins.Add(metadata.Name);
|
||||
|
|
@ -98,7 +96,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
|
||||
+ "errored and cannot be loaded:";
|
||||
|
||||
Task.Run(() =>
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return null;
|
||||
}
|
||||
|
||||
var rawQuery = string.Join(Query.TermSeparator, terms);
|
||||
var rawQuery = text;
|
||||
string actionKeyword, search;
|
||||
string possibleActionKeyword = terms[0];
|
||||
string[] searchTerms;
|
||||
|
|
@ -24,13 +24,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled)
|
||||
{ // use non global plugin for query
|
||||
actionKeyword = possibleActionKeyword;
|
||||
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..] : string.Empty;
|
||||
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
|
||||
searchTerms = terms[1..];
|
||||
}
|
||||
else
|
||||
{ // non action keyword
|
||||
actionKeyword = string.Empty;
|
||||
search = rawQuery;
|
||||
search = rawQuery.TrimStart();
|
||||
searchTerms = terms;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
|
||||
|
|
|
|||
|
|
@ -96,10 +96,17 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
Settings.Language = language.LanguageCode;
|
||||
CultureInfo.CurrentCulture = new CultureInfo(language.LanguageCode);
|
||||
// Culture of this thread
|
||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows
|
||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||
Task.Run(() =>
|
||||
// App domain
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.DefaultThreadCurrentCulture;
|
||||
|
||||
// Raise event after culture is set
|
||||
Settings.Language = language.LanguageCode;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
UpdatePluginMetadataTranslations();
|
||||
});
|
||||
|
|
@ -115,7 +122,11 @@ namespace Flow.Launcher.Core.Resource
|
|||
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
|
||||
return false;
|
||||
|
||||
if (MessageBox.Show("Do you want to turn on search with Pinyin?", string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
// No other languages should show the following text so just make it hard-coded
|
||||
// "Do you want to search with pinyin?"
|
||||
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
|
||||
|
||||
if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
|
@ -182,6 +193,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.DefaultThreadCurrentCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -220,4 +232,4 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -17,7 +17,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
public class Theme
|
||||
{
|
||||
private const int ShadowExtraMargin = 12;
|
||||
private const int ShadowExtraMargin = 32;
|
||||
|
||||
private readonly List<string> _themeDirectories = new List<string>();
|
||||
private ResourceDictionary _oldResource;
|
||||
|
|
@ -102,7 +102,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
SetBlurForWindow();
|
||||
}
|
||||
catch (DirectoryNotFoundException e)
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
|
||||
if (theme != defaultTheme)
|
||||
|
|
@ -112,7 +112,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
return false;
|
||||
}
|
||||
catch (XamlParseException e)
|
||||
catch (XamlParseException)
|
||||
{
|
||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
|
||||
if (theme != defaultTheme)
|
||||
|
|
@ -238,9 +238,10 @@ namespace Flow.Launcher.Core.Resource
|
|||
Property = Border.EffectProperty,
|
||||
Value = new DropShadowEffect
|
||||
{
|
||||
Opacity = 0.4,
|
||||
ShadowDepth = 2,
|
||||
BlurRadius = 15
|
||||
Opacity = 0.3,
|
||||
ShadowDepth = 12,
|
||||
Direction = 270,
|
||||
BlurRadius = 30
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -250,7 +251,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
marginSetter = new Setter()
|
||||
{
|
||||
Property = Border.MarginProperty,
|
||||
Value = new Thickness(ShadowExtraMargin),
|
||||
Value = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin),
|
||||
};
|
||||
windowBorderStyle.Setters.Add(marginSetter);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
|
@ -40,7 +40,7 @@ namespace Flow.Launcher.Core
|
|||
api.ShowMsg(api.GetTranslation("pleaseWait"),
|
||||
api.GetTranslation("update_flowlauncher_update_check"));
|
||||
|
||||
using var updateManager = await GitHubUpdateManager(GitHubRepository).ConfigureAwait(false);
|
||||
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
||||
|
||||
// UpdateApp CheckForUpdate will return value only if the app is squirrel installed
|
||||
var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false);
|
||||
|
|
@ -79,7 +79,7 @@ namespace Flow.Launcher.Core
|
|||
await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var newVersionTips = NewVersinoTips(newReleaseVersion.ToString());
|
||||
var newVersionTips = NewVersionTips(newReleaseVersion.ToString());
|
||||
|
||||
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ namespace Flow.Launcher.Core
|
|||
}
|
||||
|
||||
/// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||
private async Task<UpdateManager> GitHubUpdateManager(string repository)
|
||||
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||
{
|
||||
var uri = new Uri(repository);
|
||||
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
|
||||
|
|
@ -137,12 +137,13 @@ namespace Flow.Launcher.Core
|
|||
return manager;
|
||||
}
|
||||
|
||||
public string NewVersinoTips(string version)
|
||||
public string NewVersionTips(string version)
|
||||
{
|
||||
var translater = InternationalizationManager.Instance;
|
||||
var tips = string.Format(translater.GetTranslation("newVersionTips"), version);
|
||||
var translator = InternationalizationManager.Instance;
|
||||
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
|
||||
|
||||
return tips;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
|
|
@ -21,13 +21,15 @@ namespace Flow.Launcher.Infrastructure
|
|||
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
|
||||
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
|
||||
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
|
||||
public const string Documentation = "https://flow-launcher.github.io/docs/#/usage-tips";
|
||||
public static readonly string Dev = "Dev";
|
||||
public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips";
|
||||
|
||||
public static readonly int ThumbnailSize = 64;
|
||||
private static readonly string ImagesDirectory = Path.Combine(ProgramDirectory, "Images");
|
||||
public static readonly string DefaultIcon = Path.Combine(ImagesDirectory, "app.png");
|
||||
public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png");
|
||||
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
||||
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
||||
|
||||
public static string PythonPath;
|
||||
|
||||
|
|
@ -43,8 +45,9 @@ namespace Flow.Launcher.Infrastructure
|
|||
public const string Settings = "Settings";
|
||||
public const string Logs = "Logs";
|
||||
|
||||
public const string Website = "https://flow-launcher.github.io";
|
||||
public const string Website = "https://flowlauncher.com";
|
||||
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
|
||||
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
|
||||
public const string Docs = "https://flow-launcher.github.io/docs";
|
||||
public const string Docs = "https://flowlauncher.com/docs";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ namespace Flow.Launcher.Infrastructure.Exception
|
|||
sb.AppendLine();
|
||||
sb.AppendLine("## Assemblies - " + AppDomain.CurrentDomain.FriendlyName);
|
||||
sb.AppendLine();
|
||||
foreach (var ass in AppDomain.CurrentDomain.GetAssemblies().OrderBy(o => o.GlobalAssemblyCache ? 50 : 0))
|
||||
foreach (var ass in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
sb.Append("* ");
|
||||
sb.Append(ass.FullName);
|
||||
|
|
@ -166,7 +166,7 @@ namespace Flow.Launcher.Infrastructure.Exception
|
|||
}
|
||||
return result;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
catch
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<UseWpf>true</UseWpf>
|
||||
|
|
@ -53,27 +53,16 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="16.10.56" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.3.44" />
|
||||
<PackageReference Include="NLog" Version="4.7.10" />
|
||||
<PackageReference Include="NLog.Schema" Version="4.7.10" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.13.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
|
||||
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->
|
||||
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="pinyindb\pinyin_gwoyeu_mapping.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="pinyindb\pinyin_mapping.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="pinyindb\unicode_to_hanyu_pinyin.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -68,7 +68,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
var userName when string.IsNullOrEmpty(userName) =>
|
||||
(new Uri($"http://{Proxy.Server}:{Proxy.Port}"), null),
|
||||
_ => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"),
|
||||
new NetworkCredential(Proxy.UserName, Proxy.Password))
|
||||
new NetworkCredential(Proxy.UserName, Proxy.Password))
|
||||
},
|
||||
_ => (null, null)
|
||||
},
|
||||
|
|
@ -79,7 +79,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
catch(UriFormatException e)
|
||||
catch (UriFormatException e)
|
||||
{
|
||||
API.ShowMsg("Please try again", "Unable to parse Http Proxy");
|
||||
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
|
||||
|
|
@ -94,7 +94,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
await response.Content.CopyToAsync(fileStream, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -117,7 +117,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
public static Task<string> GetAsync([NotNull] string url, CancellationToken token = default)
|
||||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
return GetAsync(new Uri(url.Replace("#", "%23")), token);
|
||||
return GetAsync(new Uri(url), token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -130,28 +130,57 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
using var response = await client.GetAsync(url, token);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
else
|
||||
var content = await response.Content.ReadAsStringAsync(token);
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
$"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>");
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchrously get the result as stream from url.
|
||||
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="url">The Uri the request is sent to.</param>
|
||||
/// <param name="completionOption">An HTTP completion option value that indicates when the operation should be considered completed.</param>
|
||||
/// <param name="token">A cancellation token that can be used by other objects or threads to receive notice of cancellation</param>
|
||||
/// <returns></returns>
|
||||
public static Task<Stream> GetStreamAsync([NotNull] string url,
|
||||
CancellationToken token = default) => GetStreamAsync(new Uri(url), token);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
|
||||
public static async Task<Stream> GetStreamAsync([NotNull] Uri url,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
return await response.Content.ReadAsStreamAsync();
|
||||
return await client.GetStreamAsync(url, token);
|
||||
}
|
||||
|
||||
public static async Task<HttpResponseMessage> GetResponseAsync(string url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken token = default)
|
||||
=> await GetResponseAsync(new Uri(url), completionOption, token);
|
||||
|
||||
public static async Task<HttpResponseMessage> GetResponseAsync([NotNull] Uri url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
return await client.GetAsync(url, completionOption, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchrously send an HTTP request.
|
||||
/// </summary>
|
||||
public static async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken token = default)
|
||||
{
|
||||
return await client.SendAsync(request, completionOption, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
public class ImageCache
|
||||
{
|
||||
private const int MaxCached = 50;
|
||||
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>();
|
||||
public ConcurrentDictionary<(string, bool), ImageUsage> Data { get; } = new();
|
||||
private const int permissibleFactor = 2;
|
||||
private SemaphoreSlim semaphore = new(1, 1);
|
||||
|
||||
public void Initialization(Dictionary<string, int> usage)
|
||||
public void Initialization(Dictionary<(string, bool), int> usage)
|
||||
{
|
||||
foreach (var key in usage.Keys)
|
||||
{
|
||||
|
|
@ -37,29 +37,29 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
}
|
||||
}
|
||||
|
||||
public ImageSource this[string path]
|
||||
public ImageSource this[string path, bool isFullImage = false]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Data.TryGetValue(path, out var value))
|
||||
if (!Data.TryGetValue((path, isFullImage), out var value))
|
||||
{
|
||||
value.usage++;
|
||||
return value.imageSource;
|
||||
return null;
|
||||
}
|
||||
value.usage++;
|
||||
return value.imageSource;
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
Data.AddOrUpdate(
|
||||
path,
|
||||
new ImageUsage(0, value),
|
||||
(k, v) =>
|
||||
{
|
||||
v.imageSource = value;
|
||||
v.usage++;
|
||||
return v;
|
||||
}
|
||||
(path, isFullImage),
|
||||
new ImageUsage(0, value),
|
||||
(k, v) =>
|
||||
{
|
||||
v.imageSource = value;
|
||||
v.usage++;
|
||||
return v;
|
||||
}
|
||||
);
|
||||
|
||||
SliceExtra();
|
||||
|
|
@ -82,9 +82,9 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
}
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
public bool ContainsKey(string key, bool isFullImage)
|
||||
{
|
||||
return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null;
|
||||
return key is not null && Data.ContainsKey((key, isFullImage)) && Data[(key, isFullImage)].imageSource != null;
|
||||
}
|
||||
|
||||
public int CacheSize()
|
||||
|
|
@ -100,4 +100,4 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return Data.Values.Select(x => x.imageSource).Distinct().Count();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,30 +14,23 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
{
|
||||
public string GetHashFromImage(ImageSource imageSource)
|
||||
{
|
||||
if (!(imageSource is BitmapSource image))
|
||||
if (imageSource is not BitmapSource image)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var outStream = new MemoryStream())
|
||||
{
|
||||
// PngBitmapEncoder enc2 = new PngBitmapEncoder();
|
||||
// enc2.Frames.Add(BitmapFrame.Create(tt));
|
||||
|
||||
var enc = new JpegBitmapEncoder();
|
||||
var bitmapFrame = BitmapFrame.Create(image);
|
||||
bitmapFrame.Freeze();
|
||||
enc.Frames.Add(bitmapFrame);
|
||||
enc.Save(outStream);
|
||||
var byteArray = outStream.GetBuffer();
|
||||
using (var sha1 = new SHA1CryptoServiceProvider())
|
||||
{
|
||||
var hash = Convert.ToBase64String(sha1.ComputeHash(byteArray));
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
using var outStream = new MemoryStream();
|
||||
var enc = new JpegBitmapEncoder();
|
||||
var bitmapFrame = BitmapFrame.Create(image);
|
||||
bitmapFrame.Freeze();
|
||||
enc.Frames.Add(bitmapFrame);
|
||||
enc.Save(outStream);
|
||||
var byteArray = outStream.GetBuffer();
|
||||
using var sha1 = SHA1.Create();
|
||||
var hash = Convert.ToBase64String(sha1.ComputeHash(byteArray));
|
||||
return hash;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -46,4 +39,4 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +1,63 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using static Flow.Launcher.Infrastructure.Http.Http;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
public static class ImageLoader
|
||||
{
|
||||
private static readonly ImageCache ImageCache = new ImageCache();
|
||||
private static BinaryStorage<Dictionary<string, int>> _storage;
|
||||
private static readonly ConcurrentDictionary<string, string> GuidToKey = new ConcurrentDictionary<string, string>();
|
||||
private static readonly ImageCache ImageCache = new();
|
||||
private static BinaryStorage<Dictionary<(string, bool), int>> _storage;
|
||||
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
|
||||
private static IImageHashGenerator _hashGenerator;
|
||||
private static bool EnableImageHash = true;
|
||||
public static ImageSource DefaultImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||
private static readonly bool EnableImageHash = true;
|
||||
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
||||
public const int SmallIconSize = 64;
|
||||
public const int FullIconSize = 256;
|
||||
|
||||
|
||||
private static readonly string[] ImageExtensions =
|
||||
{
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".ico"
|
||||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"
|
||||
};
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
_storage = new BinaryStorage<Dictionary<string, int>>("Image");
|
||||
_storage = new BinaryStorage<Dictionary<(string, bool), int>>("Image");
|
||||
_hashGenerator = new ImageHashGenerator();
|
||||
|
||||
var usage = LoadStorageToConcurrentDictionary();
|
||||
|
||||
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
|
||||
foreach (var icon in new[]
|
||||
{
|
||||
Constant.DefaultIcon, Constant.MissingImgIcon
|
||||
})
|
||||
{
|
||||
ImageSource img = new BitmapImage(new Uri(icon));
|
||||
img.Freeze();
|
||||
ImageCache[icon] = img;
|
||||
ImageCache[icon, false] = img;
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
Stopwatch.Normal("|ImageLoader.Initialize|Preload images cost", () =>
|
||||
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () =>
|
||||
{
|
||||
ImageCache.Data.AsParallel().ForAll(x =>
|
||||
foreach (var ((path, isFullImage), _) in ImageCache.Data)
|
||||
{
|
||||
Load(x.Key);
|
||||
});
|
||||
await LoadAsync(path, isFullImage);
|
||||
}
|
||||
});
|
||||
Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
|
||||
});
|
||||
|
|
@ -63,17 +67,20 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
{
|
||||
lock (_storage)
|
||||
{
|
||||
_storage.Save(ImageCache.Data.Select(x => (x.Key, x.Value.usage)).ToDictionary(x => x.Key, x => x.usage));
|
||||
_storage.Save(ImageCache.Data
|
||||
.ToDictionary(
|
||||
x => x.Key,
|
||||
x => x.Value.usage));
|
||||
}
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<string, int> LoadStorageToConcurrentDictionary()
|
||||
private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary()
|
||||
{
|
||||
lock (_storage)
|
||||
{
|
||||
var loaded = _storage.TryLoad(new Dictionary<string, int>());
|
||||
var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>());
|
||||
|
||||
return new ConcurrentDictionary<string, int>(loaded);
|
||||
return new ConcurrentDictionary<(string, bool), int>(loaded);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,11 +102,12 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
Folder,
|
||||
Data,
|
||||
ImageFile,
|
||||
FullImageFile,
|
||||
Error,
|
||||
Cache
|
||||
}
|
||||
|
||||
private static ImageResult LoadInternal(string path, bool loadFullImage = false)
|
||||
private static async ValueTask<ImageResult> LoadInternalAsync(string path, bool loadFullImage = false)
|
||||
{
|
||||
ImageResult imageResult;
|
||||
|
||||
|
|
@ -107,13 +115,21 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return new ImageResult(ImageCache[Constant.MissingImgIcon], ImageType.Error);
|
||||
}
|
||||
if (ImageCache.ContainsKey(path))
|
||||
{
|
||||
return new ImageResult(ImageCache[path], ImageType.Cache);
|
||||
return new ImageResult(MissingImage, ImageType.Error);
|
||||
}
|
||||
|
||||
if (ImageCache.ContainsKey(path, loadFullImage))
|
||||
{
|
||||
return new ImageResult(ImageCache[path, loadFullImage], ImageType.Cache);
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult)
|
||||
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
var image = await LoadRemoteImageAsync(loadFullImage, uriResult);
|
||||
ImageCache[path, loadFullImage] = image;
|
||||
return new ImageResult(image, ImageType.ImageFile);
|
||||
}
|
||||
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var imageSource = new BitmapImage(new Uri(path));
|
||||
|
|
@ -121,12 +137,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return new ImageResult(imageSource, ImageType.Data);
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(path))
|
||||
{
|
||||
path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path));
|
||||
}
|
||||
|
||||
imageResult = GetThumbnailResult(ref path, loadFullImage);
|
||||
imageResult = await Task.Run(() => GetThumbnailResult(ref path, loadFullImage));
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
|
|
@ -140,14 +151,35 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e);
|
||||
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2);
|
||||
|
||||
ImageSource image = ImageCache[Constant.MissingImgIcon];
|
||||
ImageCache[path] = image;
|
||||
ImageSource image = ImageCache[Constant.MissingImgIcon, false];
|
||||
ImageCache[path, false] = image;
|
||||
imageResult = new ImageResult(image, ImageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
return imageResult;
|
||||
}
|
||||
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
||||
{
|
||||
// Download image from url
|
||||
await using var resp = await GetStreamAsync(uriResult);
|
||||
await using var buffer = new MemoryStream();
|
||||
await resp.CopyToAsync(buffer);
|
||||
buffer.Seek(0, SeekOrigin.Begin);
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
if (!loadFullImage)
|
||||
{
|
||||
image.DecodePixelHeight = SmallIconSize;
|
||||
image.DecodePixelWidth = SmallIconSize;
|
||||
}
|
||||
image.StreamSource = buffer;
|
||||
image.EndInit();
|
||||
image.StreamSource = null;
|
||||
image.Freeze();
|
||||
return image;
|
||||
}
|
||||
|
||||
private static ImageResult GetThumbnailResult(ref string path, bool loadFullImage = false)
|
||||
{
|
||||
|
|
@ -173,6 +205,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
if (loadFullImage)
|
||||
{
|
||||
image = LoadFullImage(path);
|
||||
type = ImageType.FullImageFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -187,12 +220,12 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
else
|
||||
{
|
||||
type = ImageType.File;
|
||||
image = GetThumbnail(path, ThumbnailOptions.None);
|
||||
image = GetThumbnail(path, ThumbnailOptions.None, loadFullImage ? FullIconSize : SmallIconSize);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
image = ImageCache[Constant.MissingImgIcon];
|
||||
image = ImageCache[Constant.MissingImgIcon, false];
|
||||
path = Constant.MissingImgIcon;
|
||||
}
|
||||
|
||||
|
|
@ -204,46 +237,50 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return new ImageResult(image, type);
|
||||
}
|
||||
|
||||
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly)
|
||||
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, int size = SmallIconSize)
|
||||
{
|
||||
return WindowsThumbnailProvider.GetThumbnail(
|
||||
path,
|
||||
Constant.ThumbnailSize,
|
||||
Constant.ThumbnailSize,
|
||||
size,
|
||||
size,
|
||||
option);
|
||||
}
|
||||
|
||||
public static bool CacheContainImage(string path)
|
||||
public static bool CacheContainImage(string path, bool loadFullImage = false)
|
||||
{
|
||||
return ImageCache.ContainsKey(path) && ImageCache[path] != null;
|
||||
return ImageCache.ContainsKey(path, false) && ImageCache[path, loadFullImage] != null;
|
||||
}
|
||||
|
||||
public static ImageSource Load(string path, bool loadFullImage = false)
|
||||
public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false)
|
||||
{
|
||||
var imageResult = LoadInternal(path, loadFullImage);
|
||||
var imageResult = await LoadInternalAsync(path, loadFullImage);
|
||||
|
||||
var img = imageResult.ImageSource;
|
||||
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
|
||||
{ // we need to get image hash
|
||||
string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null;
|
||||
if (imageResult.ImageType == ImageType.FullImageFile)
|
||||
{
|
||||
path = $"{path}_{ImageType.FullImageFile}";
|
||||
}
|
||||
if (hash != null)
|
||||
{
|
||||
|
||||
if (GuidToKey.TryGetValue(hash, out string key))
|
||||
{ // image already exists
|
||||
img = ImageCache[key] ?? img;
|
||||
img = ImageCache[key, false] ?? img;
|
||||
}
|
||||
else
|
||||
{ // new guid
|
||||
|
||||
GuidToKey[hash] = path;
|
||||
}
|
||||
}
|
||||
|
||||
// update cache
|
||||
ImageCache[path] = img;
|
||||
ImageCache[path, false] = img;
|
||||
}
|
||||
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
|
|
@ -252,8 +289,33 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
BitmapImage image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.UriSource = new Uri(path);
|
||||
image.UriSource = new Uri(path);
|
||||
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
image.EndInit();
|
||||
|
||||
if (image.PixelWidth > 320)
|
||||
{
|
||||
BitmapImage resizedWidth = new BitmapImage();
|
||||
resizedWidth.BeginInit();
|
||||
resizedWidth.CacheOption = BitmapCacheOption.OnLoad;
|
||||
resizedWidth.UriSource = new Uri(path);
|
||||
resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
resizedWidth.DecodePixelWidth = 320;
|
||||
resizedWidth.EndInit();
|
||||
|
||||
if (resizedWidth.PixelHeight > 320)
|
||||
{
|
||||
BitmapImage resizedHeight = new BitmapImage();
|
||||
resizedHeight.BeginInit();
|
||||
resizedHeight.CacheOption = BitmapCacheOption.OnLoad;
|
||||
resizedHeight.UriSource = new Uri(path);
|
||||
resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
resizedHeight.DecodePixelHeight = 320;
|
||||
resizedHeight.EndInit();
|
||||
return resizedHeight;
|
||||
}
|
||||
return resizedWidth;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using NLog;
|
||||
|
|
@ -211,4 +211,4 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
LogInternal(message, LogLevel.Warn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
private List<int> originalIndexs = new List<int>();
|
||||
private List<int> translatedIndexs = new List<int>();
|
||||
private int translaedLength = 0;
|
||||
private int translatedLength = 0;
|
||||
|
||||
public string key { get; private set; }
|
||||
|
||||
|
|
@ -32,13 +32,13 @@ namespace Flow.Launcher.Infrastructure
|
|||
originalIndexs.Add(originalIndex);
|
||||
translatedIndexs.Add(translatedIndex);
|
||||
translatedIndexs.Add(translatedIndex + length);
|
||||
translaedLength += length - 1;
|
||||
translatedLength += length - 1;
|
||||
}
|
||||
|
||||
public int MapToOriginalIndex(int translatedIndex)
|
||||
{
|
||||
if (translatedIndex > translatedIndexs.Last())
|
||||
return translatedIndex - translaedLength - 1;
|
||||
return translatedIndex - translatedLength - 1;
|
||||
|
||||
int lowerBound = 0;
|
||||
int upperBound = originalIndexs.Count - 1;
|
||||
|
|
@ -83,7 +83,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
translatedIndex < translatedIndexs[upperBound * 2])
|
||||
{
|
||||
int indexDef = 0;
|
||||
|
||||
|
||||
for (int j = 0; j < upperBound; j++)
|
||||
{
|
||||
indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2];
|
||||
|
|
@ -102,9 +102,24 @@ namespace Flow.Launcher.Infrastructure
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translate a language to English letters using a given rule.
|
||||
/// </summary>
|
||||
public interface IAlphabet
|
||||
{
|
||||
/// <summary>
|
||||
/// Translate a string to English letters, using a given rule.
|
||||
/// </summary>
|
||||
/// <param name="stringToTranslate">String to translate.</param>
|
||||
/// <returns></returns>
|
||||
public (string translation, TranslationMapping map) Translate(string stringToTranslate);
|
||||
|
||||
/// <summary>
|
||||
/// Determine if a string can be translated to English letter with this Alphabet.
|
||||
/// </summary>
|
||||
/// <param name="stringToTranslate">String to translate.</param>
|
||||
/// <returns></returns>
|
||||
public bool CanBeTranslated(string stringToTranslate);
|
||||
}
|
||||
|
||||
public class PinyinAlphabet : IAlphabet
|
||||
|
|
@ -119,63 +134,70 @@ namespace Flow.Launcher.Infrastructure
|
|||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
public bool CanBeTranslated(string stringToTranslate)
|
||||
{
|
||||
return WordsHelper.HasChinese(stringToTranslate);
|
||||
}
|
||||
|
||||
public (string translation, TranslationMapping map) Translate(string content)
|
||||
{
|
||||
if (_settings.ShouldUsePinyin)
|
||||
{
|
||||
if (!_pinyinCache.ContainsKey(content))
|
||||
{
|
||||
if (WordsHelper.HasChinese(content))
|
||||
{
|
||||
var resultList = WordsHelper.GetPinyinList(content);
|
||||
|
||||
StringBuilder resultBuilder = new StringBuilder();
|
||||
TranslationMapping map = new TranslationMapping();
|
||||
|
||||
bool pre = false;
|
||||
|
||||
for (int i = 0; i < resultList.Length; i++)
|
||||
{
|
||||
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
|
||||
{
|
||||
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
|
||||
resultBuilder.Append(' ');
|
||||
resultBuilder.Append(resultList[i]);
|
||||
pre = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pre)
|
||||
{
|
||||
pre = false;
|
||||
resultBuilder.Append(' ');
|
||||
}
|
||||
|
||||
resultBuilder.Append(resultList[i]);
|
||||
}
|
||||
}
|
||||
|
||||
map.endConstruct();
|
||||
|
||||
var key = resultBuilder.ToString();
|
||||
map.setKey(key);
|
||||
|
||||
return _pinyinCache[content] = (key, map);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (content, null);
|
||||
}
|
||||
return BuildCacheFromContent(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _pinyinCache[content];
|
||||
}
|
||||
}
|
||||
return (content, null);
|
||||
}
|
||||
|
||||
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
|
||||
{
|
||||
if (WordsHelper.HasChinese(content))
|
||||
{
|
||||
var resultList = WordsHelper.GetPinyinList(content);
|
||||
|
||||
StringBuilder resultBuilder = new StringBuilder();
|
||||
TranslationMapping map = new TranslationMapping();
|
||||
|
||||
bool pre = false;
|
||||
|
||||
for (int i = 0; i < resultList.Length; i++)
|
||||
{
|
||||
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
|
||||
{
|
||||
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
|
||||
resultBuilder.Append(' ');
|
||||
resultBuilder.Append(resultList[i]);
|
||||
pre = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pre)
|
||||
{
|
||||
pre = false;
|
||||
resultBuilder.Append(' ');
|
||||
}
|
||||
|
||||
resultBuilder.Append(resultList[i]);
|
||||
}
|
||||
}
|
||||
|
||||
map.endConstruct();
|
||||
|
||||
var key = resultBuilder.ToString();
|
||||
map.setKey(key);
|
||||
|
||||
return _pinyinCache[content] = (key, map);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (content, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@
|
|||
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher")]
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Core")]
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
|
||||
[assembly: System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.19041.0")]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
|||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
#pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete.
|
||||
/// <summary>
|
||||
/// Stroage object using binary data
|
||||
/// Normally, it has better performance, but not readable
|
||||
|
|
@ -113,4 +114,5 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore SYSLIB0011
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
|
@ -60,8 +60,13 @@ namespace Flow.Launcher.Infrastructure
|
|||
return new MatchResult(false, UserSettingSearchPrecision);
|
||||
|
||||
query = query.Trim();
|
||||
TranslationMapping translationMapping;
|
||||
(stringToCompare, translationMapping) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null);
|
||||
TranslationMapping translationMapping = null;
|
||||
if (_alphabet is not null && !_alphabet.CanBeTranslated(query))
|
||||
{
|
||||
// We assume that if a query can be translated (containing characters of a language, like Chinese)
|
||||
// it actually means user doesn't want it to be translated to English letters.
|
||||
(stringToCompare, translationMapping) = _alphabet.Translate(stringToCompare);
|
||||
}
|
||||
|
||||
var currentAcronymQueryIndex = 0;
|
||||
var acronymMatchData = new List<int>();
|
||||
|
|
@ -202,7 +207,11 @@ namespace Flow.Launcher.Infrastructure
|
|||
if (allQuerySubstringsMatched)
|
||||
{
|
||||
var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex);
|
||||
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1,
|
||||
|
||||
// firstMatchIndex - nearestSpaceIndex - 1 is to set the firstIndex as the index of the first matched char
|
||||
// preceded by a space e.g. 'world' matching 'hello world' firstIndex would be 0 not 6
|
||||
// giving more weight than 'we or donald' by allowing the distance calculation to treat the starting position at after the space.
|
||||
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, spaceIndices,
|
||||
lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
|
||||
|
||||
var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList();
|
||||
|
|
@ -296,7 +305,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
return currentQuerySubstringIndex >= querySubstringsLength;
|
||||
}
|
||||
|
||||
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen,
|
||||
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, List<int> spaceIndices, int matchLen,
|
||||
bool allSubstringsContainedInCompareString)
|
||||
{
|
||||
// A match found near the beginning of a string is scored more than a match found near the end
|
||||
|
|
@ -304,6 +313,14 @@ namespace Flow.Launcher.Infrastructure
|
|||
// while the score is lower if they are more spread out
|
||||
var score = 100 * (query.Length + 1) / ((1 + firstIndex) + (matchLen + 1));
|
||||
|
||||
// Give more weight to a match that is closer to the start of the string.
|
||||
// if the first matched char is immediately before space and all strings are contained in the compare string e.g. 'world' matching 'hello world'
|
||||
// and 'world hello', because both have 'world' immediately preceded by space, their firstIndex will be 0 when distance is calculated,
|
||||
// to prevent them scoring the same, we adjust the score by deducting the number of spaces it has from the start of the string, so 'world hello'
|
||||
// will score slightly higher than 'hello world' because 'hello world' has one additional space.
|
||||
if (firstIndex == 0 && allSubstringsContainedInCompareString)
|
||||
score -= spaceIndices.Count;
|
||||
|
||||
// A match with less characters assigning more weights
|
||||
if (stringToCompare.Length - query.Length < 5)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
public abstract class ShortcutBaseModel
|
||||
{
|
||||
public string Key { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Func<string> Expand { get; set; } = () => { return ""; };
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ShortcutBaseModel other &&
|
||||
Key == other.Key;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Key.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomShortcutModel : ShortcutBaseModel
|
||||
{
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonConstructorAttribute]
|
||||
public CustomShortcutModel(string key, string value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
Expand = () => { return Value; };
|
||||
}
|
||||
|
||||
public void Deconstruct(out string key, out string value)
|
||||
{
|
||||
key = Key;
|
||||
value = Value;
|
||||
}
|
||||
|
||||
public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut)
|
||||
{
|
||||
return (shortcut.Key, shortcut.Value);
|
||||
}
|
||||
|
||||
public static implicit operator CustomShortcutModel((string Key, string Value) shortcut)
|
||||
{
|
||||
return new CustomShortcutModel(shortcut.Key, shortcut.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public class BuiltinShortcutModel : ShortcutBaseModel
|
||||
{
|
||||
public string Description { get; set; }
|
||||
|
||||
public BuiltinShortcutModel(string key, string description, Func<string> expand)
|
||||
{
|
||||
Key = key;
|
||||
Description = description;
|
||||
Expand = expand ?? (() => { return ""; });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
|
|
@ -41,8 +41,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public bool UseGlyphIcons { get; set; } = true;
|
||||
public bool UseAnimation { get; set; } = true;
|
||||
public bool UseSound { get; set; } = true;
|
||||
public bool UseClock { get; set; } = true;
|
||||
public bool UseDate { get; set; } = false;
|
||||
public string TimeFormat { get; set; } = "hh:mm tt";
|
||||
public string DateFormat { get; set; } = "MM'/'dd ddd";
|
||||
public bool FirstLaunch { get; set; } = true;
|
||||
|
||||
public double SettingWindowWidth { get; set; } = 1000;
|
||||
public double SettingWindowHeight { get; set; } = 700;
|
||||
public double SettingWindowTop { get; set; }
|
||||
public double SettingWindowLeft { get; set; }
|
||||
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
|
||||
|
||||
public int CustomExplorerIndex { get; set; } = 0;
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
@ -120,8 +130,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
PrivateArg = "-private",
|
||||
EnablePrivate = false,
|
||||
Editable = false
|
||||
}
|
||||
,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "MS Edge",
|
||||
|
|
@ -137,6 +146,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
/// when false Alphabet static service will always return empty results
|
||||
/// </summary>
|
||||
public bool ShouldUsePinyin { get; set; } = false;
|
||||
public bool AlwaysPreview { get; set; } = false;
|
||||
|
||||
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
|
||||
|
|
@ -177,6 +187,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
|
||||
|
||||
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>();
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new ObservableCollection<BuiltinShortcutModel>() {
|
||||
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText)
|
||||
};
|
||||
|
||||
public bool DontPromptUpdateMsg { get; set; }
|
||||
public bool EnableUpdateLog { get; set; }
|
||||
|
||||
|
|
@ -194,7 +211,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
public bool LeaveCmdOpen { get; set; }
|
||||
public bool HideWhenDeactive { get; set; } = true;
|
||||
public bool RememberLastLaunchLocation { get; set; }
|
||||
public SearchWindowPositions SearchWindowPosition { get; set; } = SearchWindowPositions.MouseScreenCenter;
|
||||
public bool IgnoreHotkeysOnFullscreen { get; set; }
|
||||
|
||||
public HttpProxy Proxy { get; set; } = new HttpProxy();
|
||||
|
|
@ -220,4 +237,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
Light,
|
||||
Dark
|
||||
}
|
||||
}
|
||||
public enum SearchWindowPositions
|
||||
{
|
||||
RememberLastLaunchLocation,
|
||||
MouseScreenCenter,
|
||||
MouseScreenCenterTop,
|
||||
MouseScreenLeftTop,
|
||||
MouseScreenRightTop
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,58 @@
|
|||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Allowed plugin languages
|
||||
/// </summary>
|
||||
public static class AllowedLanguage
|
||||
{
|
||||
/// <summary>
|
||||
/// Python
|
||||
/// </summary>
|
||||
public static string Python
|
||||
{
|
||||
get { return "PYTHON"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C#
|
||||
/// </summary>
|
||||
public static string CSharp
|
||||
{
|
||||
get { return "CSHARP"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F#
|
||||
/// </summary>
|
||||
public static string FSharp
|
||||
{
|
||||
get { return "FSHARP"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standard .exe
|
||||
/// </summary>
|
||||
public static string Executable
|
||||
{
|
||||
get { return "EXECUTABLE"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this language is a .NET language
|
||||
/// </summary>
|
||||
/// <param name="language"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsDotNet(string language)
|
||||
{
|
||||
return language.ToUpper() == CSharp
|
||||
|| language.ToUpper() == FSharp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this language is supported
|
||||
/// </summary>
|
||||
/// <param name="language"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsAllowed(string language)
|
||||
{
|
||||
return IsDotNet(language)
|
||||
|
|
@ -35,4 +60,4 @@
|
|||
|| language.ToUpper() == Executable.ToUpper();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,24 @@ using JetBrains.Annotations;
|
|||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Base model for plugin classes
|
||||
/// </summary>
|
||||
public class BaseModel : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Property changed event handler
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a property changes
|
||||
/// </summary>
|
||||
/// <param name="propertyName"></param>
|
||||
[NotifyPropertyChangedInvocator]
|
||||
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,24 @@ using System.Windows.Input;
|
|||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for key down event
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
public delegate void FlowLauncherKeyDownEventHandler(FlowLauncherKeyDownEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for query event
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
public delegate void AfterFlowLauncherQueryEventHandler(FlowLauncherQueryEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for drop events [unused?]
|
||||
/// </summary>
|
||||
/// <param name="result"></param>
|
||||
/// <param name="dropObject"></param>
|
||||
/// <param name="e"></param>
|
||||
public delegate void ResultItemDropEventHandler(Result result, IDataObject dropObject, DragEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -17,14 +32,30 @@ namespace Flow.Launcher.Plugin
|
|||
/// <returns>return true to continue handling, return false to intercept system handling</returns>
|
||||
public delegate bool FlowLauncherGlobalKeyboardEventHandler(int keyevent, int vkcode, SpecialKeyState state);
|
||||
|
||||
/// <summary>
|
||||
/// Arguments container for the Key Down event
|
||||
/// </summary>
|
||||
public class FlowLauncherKeyDownEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The actual query
|
||||
/// </summary>
|
||||
public string Query { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Relevant key events for this event
|
||||
/// </summary>
|
||||
public KeyEventArgs keyEventArgs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Arguments container for the Query event
|
||||
/// </summary>
|
||||
public class FlowLauncherQueryEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The actual query
|
||||
/// </summary>
|
||||
public Query Query { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<ProjectGuid>{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}</ProjectGuid>
|
||||
<UseWPF>true</UseWPF>
|
||||
<OutputType>Library</OutputType>
|
||||
|
|
@ -14,10 +14,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>2.1.1</Version>
|
||||
<PackageVersion>2.1.1</PackageVersion>
|
||||
<AssemblyVersion>2.1.1</AssemblyVersion>
|
||||
<FileVersion>2.1.1</FileVersion>
|
||||
<Version>3.0.0</Version>
|
||||
<PackageVersion>3.0.0</PackageVersion>
|
||||
<AssemblyVersion>3.0.0</AssemblyVersion>
|
||||
<FileVersion>3.0.0</FileVersion>
|
||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||
<Authors>Flow-Launcher</Authors>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public interface IAsyncReloadable : IFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Reload plugin data
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task ReloadDataAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using JetBrains.Annotations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
|
@ -41,7 +42,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Copy Text to clipboard
|
||||
/// </summary>
|
||||
/// <param name="Text">Text to save on clipboard</param>
|
||||
/// <param name="text">Text to save on clipboard</param>
|
||||
public void CopyToClipboard(string text);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -163,6 +164,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// Download the specific url to a cretain file path
|
||||
/// </summary>
|
||||
/// <param name="url">URL to download file</param>
|
||||
/// <param name="filePath">path to save downloaded file</param>
|
||||
/// <param name="token">place to store file</param>
|
||||
/// <returns>Task showing the progress</returns>
|
||||
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
|
||||
|
|
@ -178,9 +180,16 @@ namespace Flow.Launcher.Plugin
|
|||
/// Remove ActionKeyword for specific plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
|
||||
/// <param name="newActionKeyword">The actionkeyword that is supposed to be removed</param>
|
||||
/// <param name="oldActionKeyword">The actionkeyword that is supposed to be removed</param>
|
||||
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether specific ActionKeyword is assigned to any of the plugin
|
||||
/// </summary>
|
||||
/// <param name="actionKeyword">The actionkeyword for checking</param>
|
||||
/// <returns>True if the actionkeyword is already assigned, False otherwise</returns>
|
||||
bool ActionKeywordAssigned(string actionKeyword);
|
||||
|
||||
/// <summary>
|
||||
/// Log debug message
|
||||
/// Message will only be logged in Debug mode
|
||||
|
|
@ -228,8 +237,27 @@ namespace Flow.Launcher.Plugin
|
|||
public void OpenDirectory(string DirectoryPath, string FileName = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the url. The browser and mode used is based on what's configured in Flow's default browser settings.
|
||||
/// Opens the URL with the given Uri object.
|
||||
/// The browser and mode used is based on what's configured in Flow's default browser settings.
|
||||
/// </summary>
|
||||
public void OpenUrl(Uri url, bool? inPrivate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the URL with the given string.
|
||||
/// The browser and mode used is based on what's configured in Flow's default browser settings.
|
||||
/// Non-C# plugins should use this method.
|
||||
/// </summary>
|
||||
public void OpenUrl(string url, bool? inPrivate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the application URI with the given Uri object, e.g. obsidian://search-query-example
|
||||
/// </summary>
|
||||
public void OpenAppUri(Uri appUri);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the application URI with the given string, e.g. obsidian://search-query-example
|
||||
/// Non-C# plugins should use this method
|
||||
/// </summary>
|
||||
public void OpenAppUri(string appUri);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
/// </summary>
|
||||
public interface IReloadable : IFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronously reload plugin data
|
||||
/// </summary>
|
||||
void ReloadData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher")]
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Core")]
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
|
||||
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ namespace Flow.Launcher.Plugin
|
|||
{
|
||||
Search = search;
|
||||
RawQuery = rawQuery;
|
||||
#pragma warning disable CS0618
|
||||
Terms = terms;
|
||||
#pragma warning restore CS0618
|
||||
SearchTerms = searchTerms;
|
||||
ActionKeyword = actionKeyword;
|
||||
}
|
||||
|
|
@ -98,4 +100,4 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
public override string ToString() => RawQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Describes the result of a plugin
|
||||
/// </summary>
|
||||
public class Result
|
||||
{
|
||||
|
||||
|
|
@ -29,6 +33,17 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public string ActionKeywordAssigned { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This holds the text which can be provided by plugin to be copied to the
|
||||
/// user's clipboard when Ctrl + C is pressed on a result. If the text is a file/directory path
|
||||
/// flow will copy the actual file/folder instead of just the path text.
|
||||
/// </summary>
|
||||
public string CopyText
|
||||
{
|
||||
get => string.IsNullOrEmpty(_copyText) ? SubTitle : _copyText;
|
||||
set => _copyText = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This holds the text which can be provided by plugin to help Flow autocomplete text
|
||||
/// for user on the plugin result. If autocomplete action for example is tab, pressing tab will have
|
||||
|
|
@ -46,9 +61,14 @@ namespace Flow.Launcher.Plugin
|
|||
get { return _icoPath; }
|
||||
set
|
||||
{
|
||||
if (!string.IsNullOrEmpty(PluginDirectory) && !Path.IsPathRooted(value))
|
||||
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
|
||||
if (!string.IsNullOrEmpty(value)
|
||||
&& !string.IsNullOrEmpty(PluginDirectory)
|
||||
&& !Path.IsPathRooted(value)
|
||||
&& !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_icoPath = Path.Combine(value, IcoPath);
|
||||
_icoPath = Path.Combine(PluginDirectory, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -56,13 +76,22 @@ namespace Flow.Launcher.Plugin
|
|||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Determines if Icon has a border radius
|
||||
/// </summary>
|
||||
public bool RoundedIcon { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate function, see <see cref="Icon"/>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public delegate ImageSource IconDelegate();
|
||||
|
||||
/// <summary>
|
||||
/// Delegate to Get Image Source
|
||||
/// </summary>
|
||||
public IconDelegate Icon;
|
||||
private string _copyText = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
|
||||
|
|
@ -78,6 +107,14 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public Func<ActionContext, bool> Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate. An Async action to take in the form of a function call when the result has been selected
|
||||
/// <returns>
|
||||
/// true to hide flowlauncher after select result
|
||||
/// </returns>
|
||||
/// </summary>
|
||||
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Priority of the current result
|
||||
/// <value>default: 0</value>
|
||||
|
|
@ -89,6 +126,9 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public IList<int> TitleHighlightData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deprecated as of Flow Launcher v1.9.1. Subtitle highlighting is no longer offered
|
||||
/// </summary>
|
||||
[Obsolete("Deprecated as of Flow Launcher v1.9.1. Subtitle highlighting is no longer offered")]
|
||||
public IList<int> SubTitleHighlightData { get; set; }
|
||||
|
||||
|
|
@ -106,10 +146,11 @@ namespace Flow.Launcher.Plugin
|
|||
set
|
||||
{
|
||||
_pluginDirectory = value;
|
||||
if (!string.IsNullOrEmpty(IcoPath) && !Path.IsPathRooted(IcoPath))
|
||||
{
|
||||
IcoPath = Path.Combine(value, IcoPath);
|
||||
}
|
||||
|
||||
// When the Result object is returned from the query call, PluginDirectory is not provided until
|
||||
// UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available
|
||||
// we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded.
|
||||
IcoPath = _icoPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,5 +203,56 @@ namespace Flow.Launcher.Plugin
|
|||
/// Show message as ToolTip on result SubTitle hover over
|
||||
/// </summary>
|
||||
public string SubTitleToolTip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Customized Preview Panel
|
||||
/// </summary>
|
||||
public Lazy<UserControl> PreviewPanel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Run this result, asynchronously
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public ValueTask<bool> ExecuteAsync(ActionContext context)
|
||||
{
|
||||
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result
|
||||
/// </summary>
|
||||
public int? ProgressBar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optionally set the color of the progress bar
|
||||
/// </summary>
|
||||
/// <default>#26a0da (blue)</default>
|
||||
public string ProgressBarColor { get; set; } = "#26a0da";
|
||||
|
||||
public PreviewInfo Preview { get; set; } = PreviewInfo.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Info of the preview image.
|
||||
/// </summary>
|
||||
public record PreviewInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Full image used for preview panel
|
||||
/// </summary>
|
||||
public string PreviewImagePath { get; set; }
|
||||
/// <summary>
|
||||
/// Determines if the preview image should occupy the full width of the preveiw panel.
|
||||
/// </summary>
|
||||
public bool IsMedia { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
public static PreviewInfo Default { get; } = new()
|
||||
{
|
||||
PreviewImagePath = null,
|
||||
Description = null,
|
||||
IsMedia = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ using System.Windows;
|
|||
|
||||
namespace Flow.Launcher.Plugin.SharedCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Commands that are useful to run on files... and folders!
|
||||
/// </summary>
|
||||
public static class FilesFolders
|
||||
{
|
||||
private const string FileExplorerProgramName = "explorer";
|
||||
|
|
@ -53,10 +56,10 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
CopyAll(subdir.FullName, temppath);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
#if DEBUG
|
||||
throw e;
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath));
|
||||
RemoveFolderIfExists(targetPath);
|
||||
|
|
@ -65,6 +68,13 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the files and directories are identical between <paramref name="fromPath"/>
|
||||
/// and <paramref name="toPath"/>
|
||||
/// </summary>
|
||||
/// <param name="fromPath"></param>
|
||||
/// <param name="toPath"></param>
|
||||
/// <returns></returns>
|
||||
public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath)
|
||||
{
|
||||
try
|
||||
|
|
@ -80,10 +90,10 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
#if DEBUG
|
||||
throw e;
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath));
|
||||
return false;
|
||||
|
|
@ -92,6 +102,10 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a folder if it exists
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
public static void RemoveFolderIfExists(this string path)
|
||||
{
|
||||
try
|
||||
|
|
@ -99,26 +113,40 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
if (Directory.Exists(path))
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
#if DEBUG
|
||||
throw e;
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a directory exists
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static bool LocationExists(this string path)
|
||||
{
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a file exists
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
public static bool FileExists(this string filePath)
|
||||
{
|
||||
return File.Exists(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open a directory window (using the OS's default handler, usually explorer)
|
||||
/// </summary>
|
||||
/// <param name="fileOrFolderPath"></param>
|
||||
public static void OpenPath(string fileOrFolderPath)
|
||||
{
|
||||
var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' };
|
||||
|
|
@ -127,16 +155,20 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
if (LocationExists(fileOrFolderPath) || FileExists(fileOrFolderPath))
|
||||
Process.Start(psi);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
#if DEBUG
|
||||
throw e;
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the folder that contains <paramref name="path"/>
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
public static void OpenContainingFolder(string path)
|
||||
{
|
||||
Process.Start(FileExplorerProgramEXE, $" /select,\"{path}\"");
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
|
||||
try
|
||||
{
|
||||
Process.Start(psi);
|
||||
Process.Start(psi)?.Dispose();
|
||||
}
|
||||
catch (System.ComponentModel.Win32Exception)
|
||||
{
|
||||
|
|
@ -100,7 +100,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
psi.FileName = url;
|
||||
}
|
||||
|
||||
Process.Start(psi);
|
||||
Process.Start(psi)?.Dispose();
|
||||
}
|
||||
// This error may be thrown if browser path is incorrect
|
||||
catch (System.ComponentModel.Win32Exception)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
|
@ -87,7 +89,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
/// <summary>
|
||||
/// Runs a windows command using the provided ProcessStartInfo using a custom execute command function
|
||||
/// </summary>
|
||||
/// <param name="Func startProcess">allows you to pass in a custom command execution function</param>
|
||||
/// <param name="startProcess">allows you to pass in a custom command execution function</param>
|
||||
/// <param name="info">allows you to pass in the info that will be passed to startProcess</param>
|
||||
/// <exception cref="FileNotFoundException">Thrown when unable to find the file specified in the command </exception>
|
||||
/// <exception cref="Win32Exception">Thrown when error occurs during the execution of the command </exception>
|
||||
public static void Execute(Func<ProcessStartInfo, Process> startProcess, ProcessStartInfo info)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
|
||||
<ProjectGuid>{FF742965-9A80-41A5-B042-D6C7D3A21708}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="nunit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0">
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
|
@ -129,14 +129,20 @@ namespace Flow.Launcher.Test
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// These are standard match scenarios
|
||||
/// The intention of this test is provide a bench mark for how much the score has increased from a change.
|
||||
/// Usually the increase in scoring should not be drastic, increase of less than 10 is acceptable.
|
||||
/// </summary>
|
||||
[TestCase(Chrome, Chrome, 157)]
|
||||
[TestCase(Chrome, LastIsChrome, 147)]
|
||||
[TestCase(Chrome, LastIsChrome, 145)]
|
||||
[TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)]
|
||||
[TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)]
|
||||
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)]
|
||||
[TestCase(Chrome, CandyCrushSagaFromKing, 0)]
|
||||
[TestCase("sql", MicrosoftSqlServerManagementStudio, 110)]
|
||||
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended
|
||||
[TestCase("sql", MicrosoftSqlServerManagementStudio, 109)]
|
||||
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 120)] //double spacing intended
|
||||
public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring(
|
||||
string queryString, string compareString, int expectedScore)
|
||||
{
|
||||
|
|
@ -275,7 +281,40 @@ namespace Flow.Launcher.Test
|
|||
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
||||
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
|
||||
$"Should be greater than{Environment.NewLine}" +
|
||||
$"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}");
|
||||
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
[TestCase("red", "red colour", "metro red")]
|
||||
[TestCase("red", "this red colour", "this colour red")]
|
||||
[TestCase("red", "this red colour", "this colour is very red")]
|
||||
[TestCase("red", "this red colour", "this colour is surprisingly super awesome red and cool")]
|
||||
[TestCase("red", "this colour is surprisingly super red very and cool", "this colour is surprisingly super very red and cool")]
|
||||
public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToIndexZero(
|
||||
string queryString, string compareString1, string compareString2)
|
||||
{
|
||||
// When
|
||||
var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
|
||||
|
||||
// Given
|
||||
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
|
||||
var compareString2Result = matcher.FuzzyMatch(queryString, compareString2);
|
||||
|
||||
Debug.WriteLine("");
|
||||
Debug.WriteLine("###############################################");
|
||||
Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}");
|
||||
Debug.WriteLine(
|
||||
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}");
|
||||
Debug.WriteLine(
|
||||
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
|
||||
Debug.WriteLine("###############################################");
|
||||
Debug.WriteLine("");
|
||||
|
||||
// Should
|
||||
Assert.True(compareString1Result.Score > compareString2Result.Score,
|
||||
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
||||
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
|
||||
$"Should be greater than{Environment.NewLine}" +
|
||||
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
[TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.Explorer;
|
||||
using Flow.Launcher.Plugin.Explorer.Search;
|
||||
using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
|
||||
|
|
@ -7,6 +7,7 @@ using Flow.Launcher.Plugin.SharedCommands;
|
|||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -19,10 +20,12 @@ namespace Flow.Launcher.Test.Plugins
|
|||
[TestFixture]
|
||||
public class ExplorerTest
|
||||
{
|
||||
#pragma warning disable CS1998 // async method with no await (more readable to leave it async to match the tested signature)
|
||||
private async Task<List<Result>> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken)
|
||||
{
|
||||
return new List<Result>();
|
||||
}
|
||||
#pragma warning restore CS1998
|
||||
|
||||
private List<Result> MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token)
|
||||
{
|
||||
|
|
@ -30,12 +33,11 @@ namespace Flow.Launcher.Test.Plugins
|
|||
{
|
||||
new Result
|
||||
{
|
||||
Title="Result 1"
|
||||
Title = "Result 1"
|
||||
},
|
||||
|
||||
new Result
|
||||
{
|
||||
Title="Result 2"
|
||||
Title = "Result 2"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -44,15 +46,13 @@ namespace Flow.Launcher.Test.Plugins
|
|||
|
||||
private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false;
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("C:\\SomeFolder\\", "directory='file:C:\\SomeFolder\\'")]
|
||||
public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestrictionsShouldUseDirectoryString(string path, string expectedString)
|
||||
{
|
||||
// Given
|
||||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
|
||||
// When
|
||||
var folderPath = path;
|
||||
var result = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(folderPath);
|
||||
var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(result == expectedString,
|
||||
|
|
@ -60,6 +60,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
$"Actual: {result}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")]
|
||||
[TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")]
|
||||
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
|
||||
|
|
@ -68,130 +69,68 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
|
||||
//When
|
||||
var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath);
|
||||
var queryString = queryConstructor.Directory(folderPath);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(queryString == expectedString,
|
||||
Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
|
||||
$"Expected string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {queryString}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
[TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
|
||||
"FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
|
||||
"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" +
|
||||
" AND directory='file:C:\\SomeFolder' ORDER BY System.FileName")]
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" +
|
||||
" FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" +
|
||||
" AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
|
||||
" ORDER BY System.FileName")]
|
||||
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
|
||||
string userSearchString, string expectedString)
|
||||
string folderPath, string userSearchString, string expectedString)
|
||||
{
|
||||
// Given
|
||||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
|
||||
//When
|
||||
var queryString = queryConstructor.QueryForTopLevelDirectorySearch(userSearchString);
|
||||
var queryString = queryConstructor.Directory(folderPath, userSearchString);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(queryString == expectedString,
|
||||
$"Expected string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {queryString}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
[TestCase("C:\\SomeFolder\\SomeApp", "(System.FileName LIKE 'SomeApp%' " +
|
||||
"OR CONTAINS(System.FileName,'\"SomeApp*\"',1033))" +
|
||||
" AND directory='file:C:\\SomeFolder'")]
|
||||
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryWhereRestrictionsShouldUseDirectoryString(
|
||||
string userSearchString, string expectedString)
|
||||
{
|
||||
// Given
|
||||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
|
||||
//When
|
||||
var queryString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(userSearchString);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(queryString == expectedString,
|
||||
$"Expected string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {queryString}{Environment.NewLine}");
|
||||
Assert.AreEqual(expectedString, queryString);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("scope='file:'")]
|
||||
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString)
|
||||
{
|
||||
//When
|
||||
var resultString = QueryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch;
|
||||
const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch;
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(resultString == expectedString,
|
||||
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||
Assert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
|
||||
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
|
||||
"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
|
||||
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
|
||||
"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
|
||||
[TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")]
|
||||
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
|
||||
string userSearchString, string expectedString)
|
||||
{
|
||||
// Given
|
||||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
var baseQuery = queryConstructor.CreateBaseQuery();
|
||||
|
||||
var baseQuery = queryConstructor.BaseQueryHelper;
|
||||
|
||||
// system running this test could have different locale than the hard-coded 1033 LCID en-US.
|
||||
var queryKeywordLocale = baseQuery.QueryKeywordLocale;
|
||||
expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString());
|
||||
|
||||
|
||||
|
||||
//When
|
||||
var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
|
||||
var resultString = queryConstructor.FilesAndFolders(userSearchString);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(resultString == expectedString,
|
||||
$"Expected query string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||
Assert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch()
|
||||
{
|
||||
// Given
|
||||
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
|
||||
|
||||
// When
|
||||
var results = await searchManager.TopLevelDirectorySearchBehaviourAsync(
|
||||
MethodWindowsIndexSearchReturnsZeroResultsAsync,
|
||||
MethodDirectoryInfoClassSearchReturnsTwoResults,
|
||||
false,
|
||||
new Query(),
|
||||
"string not used",
|
||||
default);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(results.Count == 2,
|
||||
$"Expected to have 2 results from DirectoryInfoClassSearch {Environment.NewLine} " +
|
||||
$"Actual number of results is {results.Count} {Environment.NewLine}");
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch()
|
||||
{
|
||||
// Given
|
||||
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
|
||||
|
||||
// When
|
||||
var results = await searchManager.TopLevelDirectorySearchBehaviourAsync(
|
||||
MethodWindowsIndexSearchReturnsZeroResultsAsync,
|
||||
MethodDirectoryInfoClassSearchReturnsTwoResults,
|
||||
true,
|
||||
new Query(),
|
||||
"string not used",
|
||||
default);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(results.Count == 0,
|
||||
$"Expected to have 0 results because location is indexed {Environment.NewLine} " +
|
||||
$"Actual number of results is {results.Count} {Environment.NewLine}");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase(@"some words", @"FREETEXT('some words')")]
|
||||
public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString(
|
||||
string querySearchString, string expectedString)
|
||||
|
|
@ -200,7 +139,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
|
||||
//When
|
||||
var resultString = queryConstructor.QueryWhereRestrictionsForFileContentSearch(querySearchString);
|
||||
var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(resultString == expectedString,
|
||||
|
|
@ -208,8 +147,9 @@ namespace Flow.Launcher.Test.Plugins
|
|||
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
|
||||
"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
|
||||
"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
|
||||
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
|
||||
string userSearchString, string expectedString)
|
||||
{
|
||||
|
|
@ -217,7 +157,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
|
||||
//When
|
||||
var resultString = queryConstructor.QueryForFileContentSearch(userSearchString);
|
||||
var resultString = queryConstructor.FileContent(userSearchString);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(resultString == expectedString,
|
||||
|
|
@ -228,12 +168,15 @@ namespace Flow.Launcher.Test.Plugins
|
|||
public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
|
||||
{
|
||||
// Given
|
||||
var query = new Query { ActionKeyword = "doc:", Search = "search term" };
|
||||
var query = new Query
|
||||
{
|
||||
ActionKeyword = "doc:", Search = "search term"
|
||||
};
|
||||
|
||||
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
|
||||
|
||||
// When
|
||||
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
|
||||
var result = SearchManager.IsFileContentSearch(query.ActionKeyword);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(result,
|
||||
|
|
@ -301,24 +244,19 @@ namespace Flow.Launcher.Test.Plugins
|
|||
$"Actual path string is {returnedPath} {Environment.NewLine}");
|
||||
}
|
||||
|
||||
[TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")]
|
||||
[TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' "
|
||||
+ "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND "
|
||||
+ "scope='file:c:\\SomeFolder'")]
|
||||
public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("c:\\SomeFolder", "scope='file:c:\\SomeFolder'")]
|
||||
[TestCase("c:\\OtherFolder", "scope='file:c:\\OtherFolder'")]
|
||||
public void GivenFilePath_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
|
||||
{
|
||||
// Given
|
||||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
|
||||
//When
|
||||
var resultString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path);
|
||||
var resultString = QueryConstructor.RecursiveDirectoryConstraint(path);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(resultString == expectedString,
|
||||
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||
Assert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("c:\\somefolder\\>somefile", "*somefile*")]
|
||||
[TestCase("c:\\somefolder\\somefile", "somefile*")]
|
||||
[TestCase("c:\\somefolder\\", "*")]
|
||||
|
|
@ -329,9 +267,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(resultString == expectedString,
|
||||
$"Expected criteria string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual criteria string was: {resultString}{Environment.NewLine}");
|
||||
Assert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
foreach (var result in results)
|
||||
{
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsNotNull(result.Action);
|
||||
Assert.IsNotNull(result.AsyncAction);
|
||||
Assert.IsNotNull(result.Title);
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
|
||||
public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference)
|
||||
{
|
||||
var camelText = JsonSerializer.Serialize(reference, new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
var camelText = JsonSerializer.Serialize(reference, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
|
||||
var pascalText = JsonSerializer.Serialize(reference);
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
Assert.AreEqual(result1, referenceResult);
|
||||
|
||||
Assert.IsNotNull(result1);
|
||||
Assert.IsNotNull(result1.Action);
|
||||
Assert.IsNotNull(result1.AsyncAction);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
using Flow.Launcher.Plugin.Program.Programs;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Flow.Launcher.Test.Plugins
|
||||
{
|
||||
[TestFixture]
|
||||
public class ProgramTest
|
||||
{
|
||||
[TestCase("Microsoft.WindowsCamera", "ms-resource:LensSDK/Resources/AppTitle", "ms-resource://Microsoft.WindowsCamera/LensSDK/Resources/AppTitle")]
|
||||
[TestCase("microsoft.windowscommunicationsapps", "ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName",
|
||||
"ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName")]
|
||||
[TestCase("windows.immersivecontrolpanel", "ms-resource:DisplayName", "ms-resource://windows.immersivecontrolpanel/Resources/DisplayName")]
|
||||
[TestCase("Microsoft.MSPaint", "ms-resource:AppName", "ms-resource://Microsoft.MSPaint/Resources/AppName")]
|
||||
public void WhenGivenPriReferenceValueShouldReturnCorrectFormat(string packageName, string rawPriReferenceValue, string expectedFormat)
|
||||
{
|
||||
// Arrange
|
||||
var app = new UWP.Application();
|
||||
|
||||
// Act
|
||||
var result = app.FormattedPriReferenceValue(packageName, rawPriReferenceValue);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result == expectedFormat,
|
||||
$"Expected Pri reference format: {expectedFormat}{Environment.NewLine} " +
|
||||
$"Actual: {result}{Environment.NewLine}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ namespace Flow.Launcher.Test
|
|||
|
||||
Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins);
|
||||
|
||||
Assert.AreEqual("file.txt file2 file3", q.Search);
|
||||
Assert.AreEqual("file.txt file2 file3", q.Search);
|
||||
Assert.AreEqual(">", q.ActionKeyword);
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ namespace Flow.Launcher.Test
|
|||
|
||||
Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins);
|
||||
|
||||
Assert.AreEqual("> file.txt file2 file3", q.Search);
|
||||
Assert.AreEqual("> file.txt file2 file3", q.Search);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29806.167
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.3.32901.215
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Test", "Flow.Launcher.Test\Flow.Launcher.Test.csproj", "{FF742965-9A80-41A5-B042-D6C7D3A21708}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
|
|
@ -43,6 +43,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Url",
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FFD651C7-0546-441F-BC8C-D4EE8FD01EA7}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitattributes = .gitattributes
|
||||
.gitignore = .gitignore
|
||||
appveyor.yml = appveyor.yml
|
||||
|
|
@ -79,7 +80,7 @@ Global
|
|||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@
|
|||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0,0,0,0"
|
||||
FontFamily="Segoe UI"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource actionKeywordsTitle}"
|
||||
|
|
|
|||
|
|
@ -8,24 +8,17 @@ using Flow.Launcher.ViewModel;
|
|||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class ActionKeywords : Window
|
||||
public partial class ActionKeywords
|
||||
{
|
||||
private readonly PluginPair plugin;
|
||||
private Settings settings;
|
||||
private readonly Internationalization translater = InternationalizationManager.Instance;
|
||||
private readonly PluginViewModel pluginViewModel;
|
||||
|
||||
public ActionKeywords(string pluginId, Settings settings, PluginViewModel pluginViewModel)
|
||||
public ActionKeywords(PluginViewModel pluginViewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
plugin = PluginManager.GetPluginForId(pluginId);
|
||||
this.settings = settings;
|
||||
plugin = pluginViewModel.PluginPair;
|
||||
this.pluginViewModel = pluginViewModel;
|
||||
if (plugin == null)
|
||||
{
|
||||
MessageBox.Show(translater.GetTranslation("cannotFindSpecifiedPlugin"));
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ namespace Flow.Launcher
|
|||
Http.API = API;
|
||||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
await PluginManager.InitializePlugins(API);
|
||||
await PluginManager.InitializePluginsAsync(API);
|
||||
var window = new MainWindow(_settings, _mainVM);
|
||||
|
||||
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
||||
|
|
@ -161,7 +161,6 @@ namespace Flow.Launcher
|
|||
DispatcherUnhandledException += ErrorReporting.DispatcherUnhandledException;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// let exception throw as normal is better for Debug
|
||||
/// </summary>
|
||||
|
|
@ -184,7 +183,7 @@ namespace Flow.Launcher
|
|||
|
||||
public void OnSecondAppStarted()
|
||||
{
|
||||
Current.MainWindow.Show();
|
||||
_mainVM.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
Flow.Launcher/Converters/BoolToVisibilityConverter.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (parameter != null)
|
||||
{
|
||||
if (value is true)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (value is true)
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
else {
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
}
|
||||
}
|
||||
19
Flow.Launcher/Converters/DateTimeFormatToNowConverter.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
public class DateTimeFormatToNowConverter : IValueConverter
|
||||
{
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is not string format ? null : DateTime.Now.ToString(format);
|
||||
}
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,17 +11,17 @@ namespace Flow.Launcher.Converters
|
|||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
||||
public class OpenResultHotkeyVisibilityConverter : IValueConverter
|
||||
{
|
||||
private const int MaxVisibleHotkeys = 9;
|
||||
private const int MaxVisibleHotkeys = 10;
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var hotkeyNumber = int.MaxValue;
|
||||
var number = int.MaxValue;
|
||||
|
||||
if (value is ListBoxItem listBoxItem
|
||||
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
|
||||
hotkeyNumber = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||
number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||
|
||||
return hotkeyNumber <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
|
||||
return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ namespace Flow.Launcher.Converters
|
|||
{
|
||||
if (value is ListBoxItem listBoxItem
|
||||
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
|
||||
return listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||
{
|
||||
var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||
return res == 10 ? 0 : res; // 10th item => HOTKEY+0
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ namespace Flow.Launcher.Converters
|
|||
|
||||
// Check if Text will be larger then our QueryTextBox
|
||||
System.Windows.Media.Typeface typeface = new Typeface(QueryTextBox.FontFamily, QueryTextBox.FontStyle, QueryTextBox.FontWeight, QueryTextBox.FontStretch);
|
||||
System.Windows.Media.FormattedText ft = new FormattedText(QueryTextBox.Text, System.Globalization.CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, QueryTextBox.FontSize, Brushes.Black);
|
||||
// TODO: Obsolete warning?
|
||||
System.Windows.Media.FormattedText ft = new FormattedText(QueryTextBox.Text, System.Globalization.CultureInfo.DefaultThreadCurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, QueryTextBox.FontSize, Brushes.Black);
|
||||
|
||||
var offset = QueryTextBox.Padding.Right;
|
||||
|
||||
|
|
@ -75,4 +76,4 @@ namespace Flow.Launcher.Converters
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
Flow.Launcher/Converters/TextConverter.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
public class TextConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var ID = value.ToString();
|
||||
switch(ID)
|
||||
{
|
||||
case PluginStoreItemViewModel.NewRelease:
|
||||
return InternationalizationManager.Instance.GetTranslation("pluginStore_NewRelease");
|
||||
case PluginStoreItemViewModel.RecentlyUpdated:
|
||||
return InternationalizationManager.Instance.GetTranslation("pluginStore_RecentlyUpdated");
|
||||
case PluginStoreItemViewModel.None:
|
||||
return InternationalizationManager.Instance.GetTranslation("pluginStore_None");
|
||||
case PluginStoreItemViewModel.Installed:
|
||||
return InternationalizationManager.Instance.GetTranslation("pluginStore_Installed");
|
||||
default:
|
||||
return ID;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
}
|
||||
}
|
||||
20
Flow.Launcher/Converters/TranslationConverter.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
{
|
||||
public class TranlationConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var key = value.ToString();
|
||||
if (String.IsNullOrEmpty(key))
|
||||
return key;
|
||||
return InternationalizationManager.Instance.GetTranslation(key);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
|
@ -64,7 +64,6 @@
|
|||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0,0,0,0"
|
||||
FontFamily="Segoe UI"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource customeQueryHotkeyTitle}"
|
||||
|
|
@ -76,10 +75,14 @@
|
|||
Text="{DynamicResource customeQueryHotkeyTips}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<Image
|
||||
Width="478"
|
||||
Margin="0,20,0,0"
|
||||
Source="/Images/illustration_01.png" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,20,0,0" Orientation="Horizontal">
|
||||
<Grid Width="470">
|
||||
<Grid Width="478">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
|
|
@ -132,6 +135,7 @@
|
|||
LastChildFill="True">
|
||||
<Button
|
||||
x:Name="btnTestActionKeyword"
|
||||
Margin="0,0,10,0"
|
||||
Padding="10,5,10,5"
|
||||
Click="BtnTestActionKeyword_OnClick"
|
||||
Content="{DynamicResource preview}"
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ namespace Flow.Launcher
|
|||
}
|
||||
|
||||
tbAction.Text = updateCustomHotkey.ActionKeyword;
|
||||
ctlHotkey.SetHotkey(updateCustomHotkey.Hotkey, false);
|
||||
_ = ctlHotkey.SetHotkeyAsync(updateCustomHotkey.Hotkey, false);
|
||||
update = true;
|
||||
lblAdd.Text = InternationalizationManager.Instance.GetTranslation("update");
|
||||
}
|
||||
|
|
|
|||
165
Flow.Launcher/CustomShortcutSetting.xaml
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<Window
|
||||
x:Class="Flow.Launcher.CustomShortcutSetting"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
|
||||
Title="{DynamicResource customeQueryShortcutTitle}"
|
||||
Width="530"
|
||||
Background="{DynamicResource PopuBGColor}"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
Foreground="{DynamicResource PopupTextColor}"
|
||||
Icon="Images\app.png"
|
||||
ResizeMode="NoResize"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
|
||||
</WindowChrome.WindowChrome>
|
||||
<Window.InputBindings>
|
||||
<KeyBinding Key="Escape" Command="Close" />
|
||||
</Window.InputBindings>
|
||||
<Window.CommandBindings>
|
||||
<CommandBinding Command="Close" Executed="cmdEsc_OnPress" />
|
||||
</Window.CommandBindings>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="80" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="4"
|
||||
Click="BtnCancel_OnClick"
|
||||
Style="{StaticResource TitleBarCloseButtonStyle}">
|
||||
<Path
|
||||
Width="46"
|
||||
Height="32"
|
||||
Data="M 18,11 27,20 M 18,20 27,11"
|
||||
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
|
||||
StrokeThickness="1">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="26,0,26,0">
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,12">
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0,0,0,0"
|
||||
FontFamily="Segoe UI"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource customQueryShortcut}"
|
||||
TextAlignment="Left" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
FontSize="14"
|
||||
Text="{DynamicResource customeQueryShortcutTips}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<Image
|
||||
Width="478"
|
||||
Margin="0,20,0,0"
|
||||
Source="/Images/illustration_02.png" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,10,0,10" Orientation="Horizontal">
|
||||
<Grid Width="478">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource customShortcut}" />
|
||||
<TextBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Width="180"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding Key}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource customShortcutExpansion}" />
|
||||
|
||||
<DockPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
LastChildFill="True">
|
||||
<Button
|
||||
x:Name="btnTestShortcut"
|
||||
Margin="0,0,10,0"
|
||||
Padding="10,5,10,5"
|
||||
Click="BtnTestShortcut_OnClick"
|
||||
Content="{DynamicResource preview}"
|
||||
DockPanel.Dock="Right" />
|
||||
<TextBox
|
||||
x:Name="tbExpand"
|
||||
Margin="10,0,10,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Value}" />
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="0,10,0,0"
|
||||
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="btnCancel"
|
||||
MinWidth="140"
|
||||
Margin="10,0,5,0"
|
||||
Click="BtnCancel_OnClick"
|
||||
Content="{DynamicResource cancel}" />
|
||||
<Button
|
||||
x:Name="btnAdd"
|
||||
MinWidth="140"
|
||||
Margin="5,0,10,0"
|
||||
Click="BtnAdd_OnClick"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
73
Flow.Launcher/CustomShortcutSetting.xaml.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class CustomShortcutSetting : Window
|
||||
{
|
||||
private SettingWindowViewModel viewModel;
|
||||
public string Key { get; set; } = String.Empty;
|
||||
public string Value { get; set; } = String.Empty;
|
||||
private string originalKey { get; init; } = null;
|
||||
private string originalValue { get; init; } = null;
|
||||
private bool update { get; init; } = false;
|
||||
|
||||
public CustomShortcutSetting(SettingWindowViewModel vm)
|
||||
{
|
||||
viewModel = vm;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
|
||||
{
|
||||
viewModel = vm;
|
||||
Key = key;
|
||||
Value = value;
|
||||
originalKey = key;
|
||||
originalValue = value;
|
||||
update = true;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
|
||||
return;
|
||||
}
|
||||
// Check if key is modified or adding a new one
|
||||
if (((update && originalKey != Key) || !update)
|
||||
&& viewModel.ShortcutExists(Key))
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
|
||||
return;
|
||||
}
|
||||
DialogResult = !update || originalKey != Key || originalValue != Value;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
App.API.ChangeQuery(tbExpand.Text);
|
||||
Application.Current.MainWindow.Show();
|
||||
Application.Current.MainWindow.Opacity = 1;
|
||||
Application.Current.MainWindow.Focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<StartupObject>Flow.Launcher.App</StartupObject>
|
||||
|
|
@ -83,20 +83,22 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
|
||||
<PackageReference Include="Fody" Version="6.5.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="InputSimulator" Version="1.0.4" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
|
||||
<PackageReference Include="NHotkey.Wpf" Version="2.1.0" />
|
||||
<PackageReference Include="NuGet.CommandLine" Version="5.7.2">
|
||||
<PackageReference Include="NuGet.CommandLine" Version="6.3.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="SharpVectors" Version="1.7.6" />
|
||||
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -114,4 +116,14 @@
|
|||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
<Exec Command="taskkill /f /fi "IMAGENAME eq Flow.Launcher.exe"" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
<Target Name="RemoveDuplicateAnalyzers" BeforeTargets="CoreCompile">
|
||||
<!-- Work around https://github.com/dotnet/wpf/issues/6792 -->
|
||||
|
||||
<ItemGroup>
|
||||
<FilteredAnalyzer Include="@(Analyzer->Distinct())" />
|
||||
<Analyzer Remove="@(Analyzer)" />
|
||||
<Analyzer Include="@(FilteredAnalyzer)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ namespace Flow.Launcher.Helper
|
|||
internal static void Initialize(MainViewModel mainVM)
|
||||
{
|
||||
mainViewModel = mainVM;
|
||||
settings = mainViewModel._settings;
|
||||
settings = mainViewModel.Settings;
|
||||
|
||||
SetHotkey(settings.Hotkey, OnToggleHotkey);
|
||||
LoadCustomPluginHotkey();
|
||||
|
|
|
|||
|
|
@ -385,27 +385,5 @@ namespace Flow.Launcher.Helper
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Classes
|
||||
|
||||
/// <summary>
|
||||
/// Remoting service class which is exposed by the server i.e the first instance and called by the second instance
|
||||
/// to pass on the command line arguments to the first instance and cause it to activate itself.
|
||||
/// </summary>
|
||||
private class IPCRemoteService : MarshalByRefObject
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Remoting Object's ease expires after every 5 minutes by default. We need to override the InitializeLifetimeService class
|
||||
/// to ensure that lease never expires.
|
||||
/// </summary>
|
||||
/// <returns>Always null.</returns>
|
||||
public override object InitializeLifetimeService()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ namespace Flow.Launcher.Helper
|
|||
//get current active window
|
||||
IntPtr hWnd = GetForegroundWindow();
|
||||
|
||||
if (hWnd != null && !hWnd.Equals(IntPtr.Zero))
|
||||
if (!hWnd.Equals(IntPtr.Zero))
|
||||
{
|
||||
//if current active window is NOT desktop or shell
|
||||
if (!(hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)))
|
||||
|
|
@ -98,7 +98,7 @@ namespace Flow.Launcher.Helper
|
|||
{
|
||||
IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||||
hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView");
|
||||
if (hWndDesktop != null && !hWndDesktop.Equals(IntPtr.Zero))
|
||||
if (!hWndDesktop.Equals(IntPtr.Zero))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -160,4 +160,4 @@ namespace Flow.Launcher.Helper
|
|||
public int Bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,8 @@
|
|||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Color05B}"
|
||||
Visibility="Visible">
|
||||
Press key
|
||||
</TextBlock>
|
||||
Text="{DynamicResource flowlauncherPressHotkey}"
|
||||
Visibility="Visible" />
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
|
|
@ -49,8 +48,8 @@
|
|||
Margin="0,0,18,0"
|
||||
VerticalContentAlignment="Center"
|
||||
input:InputMethod.IsInputMethodEnabled="False"
|
||||
LostFocus="tbHotkey_LostFocus"
|
||||
PreviewKeyDown="TbHotkey_OnPreviewKeyDown"
|
||||
TabIndex="100"
|
||||
LostFocus="tbHotkey_LostFocus"/>
|
||||
TabIndex="100" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
@ -9,6 +9,7 @@ using Flow.Launcher.Helper;
|
|||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Threading;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
|
|
@ -65,11 +66,11 @@ namespace Flow.Launcher
|
|||
{
|
||||
await Task.Delay(500, token);
|
||||
if (!token.IsCancellationRequested)
|
||||
await SetHotkey(hotkeyModel);
|
||||
await SetHotkeyAsync(hotkeyModel);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SetHotkey(HotkeyModel keyModel, bool triggerValidate = true)
|
||||
public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true)
|
||||
{
|
||||
CurrentHotkey = keyModel;
|
||||
|
||||
|
|
@ -101,9 +102,9 @@ namespace Flow.Launcher
|
|||
}
|
||||
}
|
||||
|
||||
public void SetHotkey(string keyStr, bool triggerValidate = true)
|
||||
public Task SetHotkeyAsync(string keyStr, bool triggerValidate = true)
|
||||
{
|
||||
SetHotkey(new HotkeyModel(keyStr), triggerValidate);
|
||||
return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate);
|
||||
}
|
||||
|
||||
private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey);
|
||||
|
|
@ -113,7 +114,7 @@ namespace Flow.Launcher
|
|||
private void tbHotkey_LostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
tbMsg.Text = tbMsgTextOriginal;
|
||||
tbMsg.Foreground = tbMsgForegroundColorOriginal;
|
||||
tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 796 B After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 597 B After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 530 B After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 501 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 506 B After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
Flow.Launcher/Images/illustration_01.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
Flow.Launcher/Images/illustration_02.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 634 B After Width: | Height: | Size: 687 B |
BIN
Flow.Launcher/Images/loading.png
Normal file
|
After Width: | Height: | Size: 274 B |
|
Before Width: | Height: | Size: 759 B After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 674 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 792 B After Width: | Height: | Size: 752 B |