mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge remote-tracking branch 'origin/dev' into quicklook
This commit is contained in:
commit
f902eaed0d
430 changed files with 22935 additions and 14245 deletions
|
|
@ -58,7 +58,7 @@ dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||||
###############################
|
###############################
|
||||||
# Style Definitions
|
# Style Definitions
|
||||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||||
# Use PascalCase for constant fields
|
# 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.severity = suggestion
|
||||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
|
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_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
|
||||||
|
|
@ -134,7 +134,7 @@ csharp_preserve_single_line_statements = true
|
||||||
csharp_preserve_single_line_blocks = true
|
csharp_preserve_single_line_blocks = true
|
||||||
csharp_using_directive_placement = outside_namespace:silent
|
csharp_using_directive_placement = outside_namespace:silent
|
||||||
csharp_prefer_simple_using_statement = true:suggestion
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
csharp_style_namespace_declarations = block_scoped:silent
|
csharp_style_namespace_declarations = file_scoped:silent
|
||||||
csharp_style_prefer_method_group_conversion = true:silent
|
csharp_style_prefer_method_group_conversion = true:silent
|
||||||
csharp_style_expression_bodied_lambdas = true:silent
|
csharp_style_expression_bodied_lambdas = true:silent
|
||||||
csharp_style_expression_bodied_local_functions = false:silent
|
csharp_style_expression_bodied_local_functions = false:silent
|
||||||
|
|
|
||||||
3
.github/actions/spelling/allow.txt
vendored
3
.github/actions/spelling/allow.txt
vendored
|
|
@ -3,3 +3,6 @@ https
|
||||||
ssh
|
ssh
|
||||||
ubuntu
|
ubuntu
|
||||||
runcount
|
runcount
|
||||||
|
Firefox
|
||||||
|
Português
|
||||||
|
Português (Brasil)
|
||||||
|
|
|
||||||
10
.github/actions/spelling/expect.txt
vendored
10
.github/actions/spelling/expect.txt
vendored
|
|
@ -1,7 +1,6 @@
|
||||||
crowdin
|
crowdin
|
||||||
DWM
|
DWM
|
||||||
workflows
|
workflows
|
||||||
Wpf
|
|
||||||
wpf
|
wpf
|
||||||
actionkeyword
|
actionkeyword
|
||||||
stackoverflow
|
stackoverflow
|
||||||
|
|
@ -20,9 +19,7 @@ Prioritise
|
||||||
Segoe
|
Segoe
|
||||||
Google
|
Google
|
||||||
Customise
|
Customise
|
||||||
UWP
|
|
||||||
uwp
|
uwp
|
||||||
Uwp
|
|
||||||
Bokmal
|
Bokmal
|
||||||
Bokm
|
Bokm
|
||||||
uninstallation
|
uninstallation
|
||||||
|
|
@ -61,7 +58,6 @@ popup
|
||||||
ptr
|
ptr
|
||||||
pluginindicator
|
pluginindicator
|
||||||
TobiasSekan
|
TobiasSekan
|
||||||
Img
|
|
||||||
img
|
img
|
||||||
resx
|
resx
|
||||||
bak
|
bak
|
||||||
|
|
@ -87,7 +83,6 @@ txb
|
||||||
btn
|
btn
|
||||||
otf
|
otf
|
||||||
searchplugin
|
searchplugin
|
||||||
Noresult
|
|
||||||
wpftk
|
wpftk
|
||||||
mkv
|
mkv
|
||||||
flac
|
flac
|
||||||
|
|
@ -104,9 +99,14 @@ Português (Brasil)
|
||||||
Italiano
|
Italiano
|
||||||
Slovenský
|
Slovenský
|
||||||
quicklook
|
quicklook
|
||||||
|
Tiếng Việt
|
||||||
Droplex
|
Droplex
|
||||||
Preinstalled
|
Preinstalled
|
||||||
errormetadatafile
|
errormetadatafile
|
||||||
noresult
|
noresult
|
||||||
pluginsmanager
|
pluginsmanager
|
||||||
alreadyexists
|
alreadyexists
|
||||||
|
JsonRPC
|
||||||
|
JsonRPCV2
|
||||||
|
Softpedia
|
||||||
|
img
|
||||||
|
|
|
||||||
3
.github/actions/spelling/patterns.txt
vendored
3
.github/actions/spelling/patterns.txt
vendored
|
|
@ -118,3 +118,6 @@
|
||||||
|
|
||||||
# UWP
|
# UWP
|
||||||
[Uu][Ww][Pp]
|
[Uu][Ww][Pp]
|
||||||
|
|
||||||
|
# version suffix <word>v#
|
||||||
|
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
|
||||||
|
|
|
||||||
28
.github/workflows/default_plugins.yml
vendored
28
.github/workflows/default_plugins.yml
vendored
|
|
@ -13,12 +13,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 7.0.x
|
dotnet-version: 7.0.x
|
||||||
|
|
||||||
- name: Determine New Plugin Updates
|
- name: Determine New Plugin Updates
|
||||||
uses: dorny/paths-filter@v2
|
uses: dorny/paths-filter@v3
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
|
|
@ -65,7 +65,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish BrowserBookmark
|
- name: Publish BrowserBookmark
|
||||||
if: steps.changes.outputs.browserbookmark == 'true'
|
if: steps.changes.outputs.browserbookmark == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.BrowserBookmark"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.BrowserBookmark"
|
||||||
files: "Flow.Launcher.Plugin.BrowserBookmark.zip"
|
files: "Flow.Launcher.Plugin.BrowserBookmark.zip"
|
||||||
|
|
@ -92,7 +92,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish Calculator
|
- name: Publish Calculator
|
||||||
if: steps.changes.outputs.calculator == 'true'
|
if: steps.changes.outputs.calculator == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Calculator"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Calculator"
|
||||||
files: "Flow.Launcher.Plugin.Calculator.zip"
|
files: "Flow.Launcher.Plugin.Calculator.zip"
|
||||||
|
|
@ -119,7 +119,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish Explorer
|
- name: Publish Explorer
|
||||||
if: steps.changes.outputs.explorer == 'true'
|
if: steps.changes.outputs.explorer == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Explorer"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Explorer"
|
||||||
files: "Flow.Launcher.Plugin.Explorer.zip"
|
files: "Flow.Launcher.Plugin.Explorer.zip"
|
||||||
|
|
@ -146,7 +146,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish PluginIndicator
|
- name: Publish PluginIndicator
|
||||||
if: steps.changes.outputs.pluginindicator == 'true'
|
if: steps.changes.outputs.pluginindicator == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginIndicator"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginIndicator"
|
||||||
files: "Flow.Launcher.Plugin.PluginIndicator.zip"
|
files: "Flow.Launcher.Plugin.PluginIndicator.zip"
|
||||||
|
|
@ -173,7 +173,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish PluginsManager
|
- name: Publish PluginsManager
|
||||||
if: steps.changes.outputs.pluginsmanager == 'true'
|
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginsManager"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginsManager"
|
||||||
files: "Flow.Launcher.Plugin.PluginsManager.zip"
|
files: "Flow.Launcher.Plugin.PluginsManager.zip"
|
||||||
|
|
@ -200,7 +200,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish ProcessKiller
|
- name: Publish ProcessKiller
|
||||||
if: steps.changes.outputs.processkiller == 'true'
|
if: steps.changes.outputs.processkiller == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller"
|
||||||
files: "Flow.Launcher.Plugin.ProcessKiller.zip"
|
files: "Flow.Launcher.Plugin.ProcessKiller.zip"
|
||||||
|
|
@ -227,7 +227,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish Program
|
- name: Publish Program
|
||||||
if: steps.changes.outputs.program == 'true'
|
if: steps.changes.outputs.program == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Program"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Program"
|
||||||
files: "Flow.Launcher.Plugin.Program.zip"
|
files: "Flow.Launcher.Plugin.Program.zip"
|
||||||
|
|
@ -254,7 +254,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish Shell
|
- name: Publish Shell
|
||||||
if: steps.changes.outputs.shell == 'true'
|
if: steps.changes.outputs.shell == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Shell"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Shell"
|
||||||
files: "Flow.Launcher.Plugin.Shell.zip"
|
files: "Flow.Launcher.Plugin.Shell.zip"
|
||||||
|
|
@ -281,7 +281,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish Sys
|
- name: Publish Sys
|
||||||
if: steps.changes.outputs.sys == 'true'
|
if: steps.changes.outputs.sys == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Sys"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Sys"
|
||||||
files: "Flow.Launcher.Plugin.Sys.zip"
|
files: "Flow.Launcher.Plugin.Sys.zip"
|
||||||
|
|
@ -308,7 +308,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish Url
|
- name: Publish Url
|
||||||
if: steps.changes.outputs.url == 'true'
|
if: steps.changes.outputs.url == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Url"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Url"
|
||||||
files: "Flow.Launcher.Plugin.Url.zip"
|
files: "Flow.Launcher.Plugin.Url.zip"
|
||||||
|
|
@ -335,7 +335,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish WebSearch
|
- name: Publish WebSearch
|
||||||
if: steps.changes.outputs.websearch == 'true'
|
if: steps.changes.outputs.websearch == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.WebSearch"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.WebSearch"
|
||||||
files: "Flow.Launcher.Plugin.WebSearch.zip"
|
files: "Flow.Launcher.Plugin.WebSearch.zip"
|
||||||
|
|
@ -362,7 +362,7 @@ jobs:
|
||||||
|
|
||||||
- name: Publish WindowsSettings
|
- name: Publish WindowsSettings
|
||||||
if: steps.changes.outputs.windowssettings == 'true'
|
if: steps.changes.outputs.windowssettings == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.WindowsSettings"
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.WindowsSettings"
|
||||||
files: "Flow.Launcher.Plugin.WindowsSettings.zip"
|
files: "Flow.Launcher.Plugin.WindowsSettings.zip"
|
||||||
|
|
|
||||||
7
.github/workflows/spelling.yml
vendored
7
.github/workflows/spelling.yml
vendored
|
|
@ -73,7 +73,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: check-spelling
|
- name: check-spelling
|
||||||
id: spelling
|
id: spelling
|
||||||
uses: check-spelling/check-spelling@v0.0.22
|
uses: check-spelling/check-spelling@prerelease
|
||||||
with:
|
with:
|
||||||
suppress_push_for_open_pull_request: 1
|
suppress_push_for_open_pull_request: 1
|
||||||
checkout: true
|
checkout: true
|
||||||
|
|
@ -91,10 +91,9 @@ jobs:
|
||||||
extra_dictionaries:
|
extra_dictionaries:
|
||||||
cspell:software-terms/dict/softwareTerms.txt
|
cspell:software-terms/dict/softwareTerms.txt
|
||||||
cspell:win32/src/win32.txt
|
cspell:win32/src/win32.txt
|
||||||
cspell:php/src/php.txt
|
|
||||||
cspell:filetypes/filetypes.txt
|
cspell:filetypes/filetypes.txt
|
||||||
cspell:csharp/csharp.txt
|
cspell:csharp/csharp.txt
|
||||||
cspell:dotnet/src/dotnet.txt
|
cspell:dotnet/dict/dotnet.txt
|
||||||
cspell:python/src/common/extra.txt
|
cspell:python/src/common/extra.txt
|
||||||
cspell:python/src/python/python-lib.txt
|
cspell:python/src/python/python-lib.txt
|
||||||
cspell:aws/aws.txt
|
cspell:aws/aws.txt
|
||||||
|
|
@ -130,7 +129,7 @@ jobs:
|
||||||
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
|
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
|
||||||
steps:
|
steps:
|
||||||
- name: comment
|
- name: comment
|
||||||
uses: check-spelling/check-spelling@v0.0.22
|
uses: check-spelling/check-spelling@prerelease
|
||||||
with:
|
with:
|
||||||
checkout: true
|
checkout: true
|
||||||
spell_check_this: check-spelling/spell-check-this@main
|
spell_check_this: check-spelling/spell-check-this@main
|
||||||
|
|
|
||||||
15
.github/workflows/stale.yml
vendored
15
.github/workflows/stale.yml
vendored
|
|
@ -6,6 +6,11 @@ on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '30 1 * * *'
|
- cron: '30 1 * * *'
|
||||||
|
|
||||||
|
env:
|
||||||
|
days-before-stale: 60
|
||||||
|
days-before-close: 7
|
||||||
|
exempt-issue-labels: 'keep-fresh'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -13,14 +18,14 @@ jobs:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v9
|
||||||
with:
|
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.'
|
stale-issue-message: 'This issue is stale because it has been open ${{ env.days-before-stale }} days with no activity. Remove stale label or comment or this will be closed in ${{ env.days-before-stale }} days.\n\nAlternatively this issue can be kept open by adding one of the following labels:\n${{ env.exempt-issue-labels }}'
|
||||||
days-before-stale: 45
|
days-before-stale: ${{ env.days-before-stale }}
|
||||||
days-before-close: 7
|
days-before-close: ${{ env.days-before-close }}
|
||||||
days-before-pr-close: -1
|
days-before-pr-close: -1
|
||||||
exempt-all-milestones: true
|
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.'
|
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'
|
stale-pr-label: 'no-pr-activity'
|
||||||
exempt-issue-labels: 'keep-fresh'
|
exempt-issue-labels: ${{ env.exempt-issue-labels }}
|
||||||
exempt-pr-labels: 'keep-fresh,awaiting-approval,work-in-progress'
|
exempt-pr-labels: 'keep-fresh,awaiting-approval,work-in-progress'
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
@ -16,6 +18,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
|
|
||||||
private List<UserPlugin> plugins = new();
|
private List<UserPlugin> plugins = new();
|
||||||
|
|
||||||
|
private static JsonSerializerOptions PluginStoreItemSerializationOption = new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetch and deserialize the contents of a plugins.json file found at <see cref="ManifestFileUrl"/>.
|
/// Fetch and deserialize the contents of a plugins.json file found at <see cref="ManifestFileUrl"/>.
|
||||||
/// We use conditional http requests to keep repeat requests fast.
|
/// We use conditional http requests to keep repeat requests fast.
|
||||||
|
|
@ -32,12 +39,15 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
|
|
||||||
request.Headers.Add("If-None-Match", latestEtag);
|
request.Headers.Add("If-None-Match", latestEtag);
|
||||||
|
|
||||||
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.OK)
|
if (response.StatusCode == HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
this.plugins = await response.Content.ReadFromJsonAsync<List<UserPlugin>>(cancellationToken: token).ConfigureAwait(false);
|
this.plugins = await response.Content
|
||||||
this.latestEtag = response.Headers.ETag.Tag;
|
.ReadFromJsonAsync<List<UserPlugin>>(PluginStoreItemSerializationOption, cancellationToken: token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
this.latestEtag = response.Headers.ETag?.Tag;
|
||||||
|
|
||||||
Log.Info(nameof(CommunityPluginSource), $"Loaded {this.plugins.Count} plugins from {ManifestFileUrl}");
|
Log.Info(nameof(CommunityPluginSource), $"Loaded {this.plugins.Count} plugins from {ManifestFileUrl}");
|
||||||
return this.plugins;
|
return this.plugins;
|
||||||
|
|
@ -49,7 +59,8 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Warn(nameof(CommunityPluginSource), $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
Log.Warn(nameof(CommunityPluginSource),
|
||||||
|
$"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||||
throw new Exception($"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
throw new Exception($"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
|
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
|
||||||
{
|
{
|
||||||
var msg = $"Please select the {EnvName} executable";
|
var msg = $"Please select the {EnvName} executable";
|
||||||
var selectedFile = string.Empty;
|
string selectedFile;
|
||||||
|
|
||||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||||
|
|
||||||
|
|
@ -131,14 +131,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = dlg.ShowDialog();
|
var result = dlg.ShowDialog();
|
||||||
if (result == DialogResult.OK)
|
return result == DialogResult.OK ? dlg.FileName : string.Empty;
|
||||||
{
|
|
||||||
return dlg.FileName;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
{
|
||||||
|
|
||||||
|
internal class JavaScriptV2Environment : TypeScriptV2Environment
|
||||||
|
{
|
||||||
|
internal override string Language => AllowedLanguage.JavaScriptV2;
|
||||||
|
|
||||||
|
internal JavaScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Flow.Launcher.Core.Plugin;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
{
|
||||||
|
internal class PythonV2Environment : PythonEnvironment
|
||||||
|
{
|
||||||
|
internal override string Language => AllowedLanguage.PythonV2;
|
||||||
|
|
||||||
|
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||||
|
{
|
||||||
|
return new PluginPair
|
||||||
|
{
|
||||||
|
Plugin = new PythonPluginV2(filePath),
|
||||||
|
Metadata = metadata
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal PythonV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Droplex;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using System.IO;
|
||||||
|
using Flow.Launcher.Core.Plugin;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
{
|
||||||
|
internal class TypeScriptV2Environment : AbstractPluginEnvironment
|
||||||
|
{
|
||||||
|
internal override string Language => AllowedLanguage.TypeScriptV2;
|
||||||
|
|
||||||
|
internal override string EnvName => DataLocation.NodeEnvironmentName;
|
||||||
|
|
||||||
|
internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName);
|
||||||
|
|
||||||
|
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
|
||||||
|
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
|
||||||
|
|
||||||
|
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; }
|
||||||
|
|
||||||
|
internal TypeScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||||
|
|
||||||
|
internal override void InstallEnvironment()
|
||||||
|
{
|
||||||
|
FilesFolders.RemoveFolderIfExists(InstallPath);
|
||||||
|
|
||||||
|
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
||||||
|
|
||||||
|
PluginsSettingsFilePath = ExecutablePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||||
|
{
|
||||||
|
return new PluginPair
|
||||||
|
{
|
||||||
|
Plugin = new NodePluginV2(filePath),
|
||||||
|
Metadata = metadata
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.ExternalPlugins
|
namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
{
|
{
|
||||||
|
|
@ -13,9 +13,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
public string Website { get; set; }
|
public string Website { get; set; }
|
||||||
public string UrlDownload { get; set; }
|
public string UrlDownload { get; set; }
|
||||||
public string UrlSourceCode { get; set; }
|
public string UrlSourceCode { get; set; }
|
||||||
|
public string LocalInstallPath { get; set; }
|
||||||
public string IcoPath { get; set; }
|
public string IcoPath { get; set; }
|
||||||
public DateTime LatestReleaseDate { get; set; }
|
public DateTime? LatestReleaseDate { get; set; }
|
||||||
public DateTime DateAdded { get; set; }
|
public DateTime? DateAdded { get; set; }
|
||||||
|
|
||||||
|
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,11 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Droplex" Version="1.7.0" />
|
<PackageReference Include="Droplex" Version="1.7.0" />
|
||||||
<PackageReference Include="FSharp.Core" Version="7.0.400" />
|
<PackageReference Include="FSharp.Core" Version="7.0.401" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.2.1" />
|
||||||
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||||
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
||||||
|
<PackageReference Include="StreamJsonRpc" Version="2.17.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
@ -27,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||||
_startInfo.ArgumentList[0] = request.ToString();
|
_startInfo.ArgumentList[0] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||||
return ExecuteAsync(_startInfo, token);
|
return ExecuteAsync(_startInfo, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||||
_startInfo.ArgumentList[0] = rpcRequest.ToString();
|
_startInfo.ArgumentList[0] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||||
return Execute(_startInfo);
|
return Execute(_startInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs
Normal file
20
Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
{
|
||||||
|
internal sealed class ExecutablePluginV2 : ProcessStreamPluginV2
|
||||||
|
{
|
||||||
|
protected override ProcessStartInfo StartInfo { get; set; }
|
||||||
|
|
||||||
|
public ExecutablePluginV2(string filename)
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo { FileName = filename };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MessageHandlerType MessageHandler { get; } = MessageHandlerType.NewLineDelimited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,67 +19,35 @@ using System.Text.Json;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
public class JsonRPCErrorModel
|
public record JsonRPCBase(int Id, JsonRPCErrorModel Error = default);
|
||||||
{
|
public record JsonRPCErrorModel(int Code, string Message, string Data);
|
||||||
public int Code { get; set; }
|
|
||||||
|
|
||||||
public string Message { get; set; }
|
public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
|
||||||
|
public record JsonRPCQueryResponseModel(int Id,
|
||||||
|
[property: JsonPropertyName("result")] List<JsonRPCResult> Result,
|
||||||
|
IReadOnlyDictionary<string, object> SettingsChange = null,
|
||||||
|
string DebugMessage = "",
|
||||||
|
JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error);
|
||||||
|
|
||||||
public string Data { get; set; }
|
public record JsonRPCRequestModel(int Id,
|
||||||
}
|
string Method,
|
||||||
|
object[] Parameters,
|
||||||
|
IReadOnlyDictionary<string, object> Settings = default,
|
||||||
|
JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
|
||||||
|
|
||||||
|
|
||||||
public class JsonRPCResponseModel
|
|
||||||
{
|
|
||||||
public string Result { get; set; }
|
|
||||||
|
|
||||||
public JsonRPCErrorModel Error { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class JsonRPCQueryResponseModel : JsonRPCResponseModel
|
|
||||||
{
|
|
||||||
[JsonPropertyName("result")]
|
|
||||||
public new List<JsonRPCResult> Result { get; set; }
|
|
||||||
|
|
||||||
public Dictionary<string, object> SettingsChange { get; set; }
|
|
||||||
|
|
||||||
public string DebugMessage { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class JsonRPCRequestModel
|
|
||||||
{
|
|
||||||
public string Method { get; set; }
|
|
||||||
|
|
||||||
public object[] Parameters { get; set; }
|
|
||||||
|
|
||||||
public Dictionary<string, object> Settings { get; set; }
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions options = new()
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
||||||
};
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return JsonSerializer.Serialize(this, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Json RPC Request that Flow Launcher sent to client
|
|
||||||
/// </summary>
|
|
||||||
public class JsonRPCServerRequestModel : JsonRPCRequestModel
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Json RPC Request(in query response) that client sent to Flow Launcher
|
/// Json RPC Request(in query response) that client sent to Flow Launcher
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class JsonRPCClientRequestModel : JsonRPCRequestModel
|
public record JsonRPCClientRequestModel(
|
||||||
{
|
int Id,
|
||||||
public bool DontHideAfterAction { get; set; }
|
string Method,
|
||||||
}
|
object[] Parameters,
|
||||||
|
IReadOnlyDictionary<string, object> Settings,
|
||||||
|
bool DontHideAfterAction = false,
|
||||||
|
JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error);
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represent the json-rpc result item that client send to Flow Launcher
|
/// Represent the json-rpc result item that client send to Flow Launcher
|
||||||
/// Typically, we will send back this request model to client after user select the result item
|
/// Typically, we will send back this request model to client after user select the result item
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// Represent the plugin that using JsonPRC
|
/// Represent the plugin that using JsonPRC
|
||||||
/// every JsonRPC plugin should has its own plugin instance
|
/// every JsonRPC plugin should has its own plugin instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
internal abstract class JsonRPCPlugin : JsonRPCPluginBase
|
||||||
{
|
{
|
||||||
protected PluginInitContext context;
|
|
||||||
public const string JsonRPC = "JsonRPC";
|
public const string JsonRPC = "JsonRPC";
|
||||||
|
|
||||||
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
|
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
|
||||||
|
|
@ -40,19 +39,16 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
private static readonly RecyclableMemoryStreamManager BufferManager = new();
|
private static readonly RecyclableMemoryStreamManager BufferManager = new();
|
||||||
|
|
||||||
private string SettingConfigurationPath => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
private int RequestId { get; set; }
|
||||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name, "Settings.json");
|
|
||||||
|
|
||||||
public List<Result> LoadContextMenus(Result selectedResult)
|
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||||
|
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
|
||||||
|
|
||||||
|
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||||
{
|
{
|
||||||
var request = new JsonRPCRequestModel
|
var request = new JsonRPCRequestModel(RequestId++,
|
||||||
{
|
"context_menu",
|
||||||
Method = "context_menu",
|
new[] { selectedResult.ContextData });
|
||||||
Parameters = new[]
|
|
||||||
{
|
|
||||||
selectedResult.ContextData
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var output = Request(request);
|
var output = Request(request);
|
||||||
return DeserializedResult(output);
|
return DeserializedResult(output);
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +73,6 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
WriteIndented = true
|
||||||
};
|
};
|
||||||
private Dictionary<string, object> Settings { get; set; }
|
|
||||||
|
|
||||||
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
||||||
|
|
||||||
|
|
@ -103,85 +98,42 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return ParseResults(queryResponseModel);
|
return ParseResults(queryResponseModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||||
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
|
|
||||||
{
|
{
|
||||||
if (queryResponseModel.Result == null) return null;
|
if (result.JsonRPCAction == null) return false;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
|
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
|
||||||
{
|
{
|
||||||
context.API.ShowMsg(queryResponseModel.DebugMessage);
|
return !result.JsonRPCAction.DontHideAfterAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var result in queryResponseModel.Result)
|
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
|
||||||
{
|
{
|
||||||
result.AsyncAction = async c =>
|
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
|
||||||
|
result.JsonRPCAction.Parameters);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
|
||||||
|
|
||||||
|
if (actionResponse.Length == 0)
|
||||||
{
|
{
|
||||||
UpdateSettings(result.SettingsChange);
|
|
||||||
|
|
||||||
if (result.JsonRPCAction == null) return false;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
|
|
||||||
{
|
|
||||||
return !result.JsonRPCAction.DontHideAfterAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
|
|
||||||
{
|
|
||||||
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
|
|
||||||
result.JsonRPCAction.Parameters);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
|
|
||||||
|
|
||||||
if (actionResponse.Length == 0)
|
|
||||||
{
|
|
||||||
return !result.JsonRPCAction.DontHideAfterAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonRpcRequestModel = await
|
|
||||||
JsonSerializer.DeserializeAsync<JsonRPCRequestModel>(actionResponse, options);
|
|
||||||
|
|
||||||
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
|
|
||||||
{
|
|
||||||
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
|
|
||||||
jsonRpcRequestModel.Parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !result.JsonRPCAction.DontHideAfterAction;
|
return !result.JsonRPCAction.DontHideAfterAction;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
var jsonRpcRequestModel = await
|
||||||
|
JsonSerializer.DeserializeAsync<JsonRPCRequestModel>(actionResponse, options);
|
||||||
|
|
||||||
|
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
|
||||||
|
{
|
||||||
|
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
|
||||||
|
jsonRpcRequestModel.Parameters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var results = new List<Result>();
|
return !result.JsonRPCAction.DontHideAfterAction;
|
||||||
|
|
||||||
results.AddRange(queryResponseModel.Result);
|
|
||||||
|
|
||||||
UpdateSettings(queryResponseModel.SettingsChange);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteFlowLauncherAPI(string method, object[] parameters)
|
|
||||||
{
|
|
||||||
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
|
|
||||||
var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray);
|
|
||||||
if (methodInfo == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
methodInfo.Invoke(PluginManager.API, parameters);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
#if (DEBUG)
|
|
||||||
throw;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Execute external program and return the output
|
/// Execute external program and return the output
|
||||||
|
|
@ -296,379 +248,19 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return sourceBuffer;
|
return sourceBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||||
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
|
||||||
{
|
{
|
||||||
var request = new JsonRPCRequestModel
|
var request = new JsonRPCRequestModel(RequestId++,
|
||||||
{
|
"query",
|
||||||
Method = "query",
|
new object[]
|
||||||
Parameters = new object[]
|
|
||||||
{
|
{
|
||||||
query.Search
|
query.Search
|
||||||
},
|
},
|
||||||
Settings = Settings
|
Settings?.Inner);
|
||||||
};
|
|
||||||
var output = await RequestAsync(request, token);
|
var output = await RequestAsync(request, token);
|
||||||
|
|
||||||
return await DeserializedResultAsync(output);
|
return await DeserializedResultAsync(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InitSettingAsync()
|
|
||||||
{
|
|
||||||
if (!File.Exists(SettingConfigurationPath))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (File.Exists(SettingPath))
|
|
||||||
{
|
|
||||||
await using var fileStream = File.OpenRead(SettingPath);
|
|
||||||
Settings = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(fileStream, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
|
|
||||||
_settingsTemplate = deserializer.Deserialize<JsonRpcConfigurationModel>(await File.ReadAllTextAsync(SettingConfigurationPath));
|
|
||||||
|
|
||||||
Settings ??= new Dictionary<string, object>();
|
|
||||||
|
|
||||||
foreach (var (type, attribute) in _settingsTemplate.Body)
|
|
||||||
{
|
|
||||||
if (type == "textBlock")
|
|
||||||
continue;
|
|
||||||
if (!Settings.ContainsKey(attribute.Name))
|
|
||||||
{
|
|
||||||
Settings[attribute.Name] = attribute.DefaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual async Task InitAsync(PluginInitContext context)
|
|
||||||
{
|
|
||||||
this.context = context;
|
|
||||||
await InitSettingAsync();
|
|
||||||
}
|
|
||||||
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 Grid
|
|
||||||
{
|
|
||||||
Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center
|
|
||||||
};
|
|
||||||
ColumnDefinition gridCol1 = new ColumnDefinition();
|
|
||||||
ColumnDefinition gridCol2 = new ColumnDefinition();
|
|
||||||
|
|
||||||
gridCol1.Width = new GridLength(70, GridUnitType.Star);
|
|
||||||
gridCol2.Width = new GridLength(30, GridUnitType.Star);
|
|
||||||
mainPanel.ColumnDefinitions.Add(gridCol1);
|
|
||||||
mainPanel.ColumnDefinitions.Add(gridCol2);
|
|
||||||
settingWindow.Content = mainPanel;
|
|
||||||
int rowCount = 0;
|
|
||||||
foreach (var (type, attribute) in _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.Vertical,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Margin = settingLabelPanelMargin
|
|
||||||
};
|
|
||||||
RowDefinition gridRow = new RowDefinition();
|
|
||||||
mainPanel.RowDefinitions.Add(gridRow);
|
|
||||||
var name = new TextBlock()
|
|
||||||
{
|
|
||||||
Text = attribute.Label,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Margin = settingLabelMargin,
|
|
||||||
TextWrapping = TextWrapping.WrapWithOverflow
|
|
||||||
};
|
|
||||||
var desc = new TextBlock()
|
|
||||||
{
|
|
||||||
Text = attribute.Description,
|
|
||||||
FontSize = 12,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Margin = settingDescMargin,
|
|
||||||
TextWrapping = TextWrapping.WrapWithOverflow
|
|
||||||
};
|
|
||||||
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
|
|
||||||
|
|
||||||
if (attribute.Description == null) /* if no description, hide */
|
|
||||||
desc.Visibility = Visibility.Collapsed;
|
|
||||||
|
|
||||||
|
|
||||||
if (type != "textBlock") /* if textBlock, hide desc */
|
|
||||||
{
|
|
||||||
panel.Children.Add(name);
|
|
||||||
panel.Children.Add(desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Grid.SetColumn(panel, 0);
|
|
||||||
Grid.SetRow(panel, rowCount);
|
|
||||||
|
|
||||||
FrameworkElement contentControl;
|
|
||||||
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case "textBlock":
|
|
||||||
{
|
|
||||||
contentControl = new TextBlock
|
|
||||||
{
|
|
||||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
|
||||||
Margin = settingTextBlockMargin,
|
|
||||||
Padding = new Thickness(0, 0, 0, 0),
|
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
|
||||||
TextAlignment = TextAlignment.Left,
|
|
||||||
TextWrapping = TextWrapping.Wrap
|
|
||||||
};
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
Margin = new Thickness(10, 0, 0, 0),
|
|
||||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
|
||||||
ToolTip = attribute.Description
|
|
||||||
};
|
|
||||||
textBox.TextChanged += (_, _) =>
|
|
||||||
{
|
|
||||||
Settings[attribute.Name] = textBox.Text;
|
|
||||||
};
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
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 = settingCheckboxMargin,
|
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
|
||||||
ToolTip = attribute.Description
|
|
||||||
};
|
|
||||||
checkBox.Click += (sender, _) =>
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
mainPanel.Children.Add(panel);
|
|
||||||
mainPanel.Children.Add(contentControl);
|
|
||||||
rowCount++;
|
|
||||||
|
|
||||||
}
|
|
||||||
return settingWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save()
|
|
||||||
{
|
|
||||||
if (Settings != null)
|
|
||||||
{
|
|
||||||
Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name));
|
|
||||||
File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateSettings(Dictionary<string, object> settings)
|
|
||||||
{
|
|
||||||
if (settings == null || settings.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var (key, value) in settings)
|
|
||||||
{
|
|
||||||
if (Settings.ContainsKey(key))
|
|
||||||
{
|
|
||||||
Settings[key] = value;
|
|
||||||
}
|
|
||||||
if (_settingControls.ContainsKey(key))
|
|
||||||
{
|
|
||||||
|
|
||||||
switch (_settingControls[key])
|
|
||||||
{
|
|
||||||
case TextBox textBox:
|
|
||||||
textBox.Dispatcher.Invoke(() => textBox.Text = value as string);
|
|
||||||
break;
|
|
||||||
case PasswordBox passwordBox:
|
|
||||||
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string);
|
|
||||||
break;
|
|
||||||
case System.Windows.Controls.ComboBox comboBox:
|
|
||||||
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
|
||||||
break;
|
|
||||||
case CheckBox checkBox:
|
|
||||||
checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
163
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
163
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
using Flow.Launcher.Core.Resource;
|
||||||
|
using Flow.Launcher.Infrastructure;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Microsoft.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
using CheckBox = System.Windows.Controls.CheckBox;
|
||||||
|
using Control = System.Windows.Controls.Control;
|
||||||
|
using Orientation = System.Windows.Controls.Orientation;
|
||||||
|
using TextBox = System.Windows.Controls.TextBox;
|
||||||
|
using UserControl = System.Windows.Controls.UserControl;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using static System.Windows.Forms.LinkLabel;
|
||||||
|
using Droplex;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Microsoft.VisualStudio.Threading;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represent the plugin that using JsonPRC
|
||||||
|
/// every JsonRPC plugin should has its own plugin instance
|
||||||
|
/// </summary>
|
||||||
|
internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||||
|
{
|
||||||
|
protected PluginInitContext Context;
|
||||||
|
public const string JsonRPC = "JsonRPC";
|
||||||
|
|
||||||
|
private int RequestId { get; set; }
|
||||||
|
|
||||||
|
private string SettingConfigurationPath =>
|
||||||
|
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||||
|
|
||||||
|
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory,
|
||||||
|
Context.CurrentPluginMetadata.Name, "Settings.json");
|
||||||
|
|
||||||
|
public abstract List<Result> LoadContextMenus(Result selectedResult);
|
||||||
|
|
||||||
|
protected static readonly JsonSerializerOptions DeserializeOption = 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() }
|
||||||
|
};
|
||||||
|
|
||||||
|
protected static readonly JsonSerializerOptions RequestSerializeOption = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected abstract Task<bool> ExecuteResultAsync(JsonRPCResult result);
|
||||||
|
|
||||||
|
protected JsonRPCPluginSettings Settings { get; set; }
|
||||||
|
|
||||||
|
protected List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
|
||||||
|
{
|
||||||
|
if (queryResponseModel.Result == null) return null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
|
||||||
|
{
|
||||||
|
Context.API.ShowMsg(queryResponseModel.DebugMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var result in queryResponseModel.Result)
|
||||||
|
{
|
||||||
|
result.AsyncAction = async _ =>
|
||||||
|
{
|
||||||
|
Settings?.UpdateSettings(result.SettingsChange);
|
||||||
|
|
||||||
|
return await ExecuteResultAsync(result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = new List<Result>();
|
||||||
|
|
||||||
|
results.AddRange(queryResponseModel.Result);
|
||||||
|
|
||||||
|
Settings?.UpdateSettings(queryResponseModel.SettingsChange);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ExecuteFlowLauncherAPI(string method, object[] parameters)
|
||||||
|
{
|
||||||
|
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
|
||||||
|
var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray);
|
||||||
|
|
||||||
|
if (methodInfo == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
methodInfo.Invoke(Context.API, parameters);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
#if (DEBUG)
|
||||||
|
throw;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task<List<Result>> QueryAsync(Query query, CancellationToken token);
|
||||||
|
|
||||||
|
|
||||||
|
private async Task InitSettingAsync()
|
||||||
|
{
|
||||||
|
JsonRpcConfigurationModel configuration = null;
|
||||||
|
if (File.Exists(SettingConfigurationPath))
|
||||||
|
{
|
||||||
|
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
|
||||||
|
configuration =
|
||||||
|
deserializer.Deserialize<JsonRpcConfigurationModel>(
|
||||||
|
await File.ReadAllTextAsync(SettingConfigurationPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Settings ??= new JsonRPCPluginSettings
|
||||||
|
{
|
||||||
|
Configuration = configuration, SettingPath = SettingPath, API = Context.API
|
||||||
|
};
|
||||||
|
|
||||||
|
await Settings.InitializeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task InitAsync(PluginInitContext context)
|
||||||
|
{
|
||||||
|
this.Context = context;
|
||||||
|
await InitSettingAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
Settings?.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Control CreateSettingPanel()
|
||||||
|
{
|
||||||
|
return Settings.CreateSettingPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
415
Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Normal file
415
Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Normal file
|
|
@ -0,0 +1,415 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using Flow.Launcher.Infrastructure.Storage;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
{
|
||||||
|
public class JsonRPCPluginSettings
|
||||||
|
{
|
||||||
|
public required JsonRpcConfigurationModel? Configuration { get; init; }
|
||||||
|
|
||||||
|
public required string SettingPath { get; init; }
|
||||||
|
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, object> Inner => Settings;
|
||||||
|
protected ConcurrentDictionary<string, object> Settings { get; set; }
|
||||||
|
public required IPublicAPI API { get; init; }
|
||||||
|
|
||||||
|
private JsonStorage<ConcurrentDictionary<string, object>> _storage;
|
||||||
|
|
||||||
|
// maybe move to resource?
|
||||||
|
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
|
||||||
|
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
|
||||||
|
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
|
||||||
|
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
|
||||||
|
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
|
||||||
|
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
|
||||||
|
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
|
||||||
|
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_storage = new JsonStorage<ConcurrentDictionary<string, object>>(SettingPath);
|
||||||
|
Settings = await _storage.LoadAsync();
|
||||||
|
|
||||||
|
if (Configuration == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (type, attributes) in Configuration.Body)
|
||||||
|
{
|
||||||
|
if (attributes.Name == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Settings.ContainsKey(attributes.Name))
|
||||||
|
{
|
||||||
|
Settings[attributes.Name] = attributes.DefaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void UpdateSettings(IReadOnlyDictionary<string, object> settings)
|
||||||
|
{
|
||||||
|
if (settings == null || settings.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var (key, value) in settings)
|
||||||
|
{
|
||||||
|
Settings[key] = value;
|
||||||
|
|
||||||
|
if (SettingControls.TryGetValue(key, out var control))
|
||||||
|
{
|
||||||
|
switch (control)
|
||||||
|
{
|
||||||
|
case TextBox textBox:
|
||||||
|
textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty);
|
||||||
|
break;
|
||||||
|
case PasswordBox passwordBox:
|
||||||
|
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty);
|
||||||
|
break;
|
||||||
|
case ComboBox comboBox:
|
||||||
|
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
||||||
|
break;
|
||||||
|
case CheckBox checkBox:
|
||||||
|
checkBox.Dispatcher.Invoke(() =>
|
||||||
|
checkBox.IsChecked = value is bool isChecked
|
||||||
|
? isChecked
|
||||||
|
: bool.Parse(value as string ?? string.Empty));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync()
|
||||||
|
{
|
||||||
|
await _storage.SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
_storage.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Control CreateSettingPanel()
|
||||||
|
{
|
||||||
|
if (Settings == null || Settings.Count == 0)
|
||||||
|
return new();
|
||||||
|
|
||||||
|
var settingWindow = new UserControl();
|
||||||
|
var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
||||||
|
|
||||||
|
ColumnDefinition gridCol1 = new ColumnDefinition();
|
||||||
|
ColumnDefinition gridCol2 = new ColumnDefinition();
|
||||||
|
|
||||||
|
gridCol1.Width = new GridLength(70, GridUnitType.Star);
|
||||||
|
gridCol2.Width = new GridLength(30, GridUnitType.Star);
|
||||||
|
mainPanel.ColumnDefinitions.Add(gridCol1);
|
||||||
|
mainPanel.ColumnDefinitions.Add(gridCol2);
|
||||||
|
settingWindow.Content = mainPanel;
|
||||||
|
int rowCount = 0;
|
||||||
|
|
||||||
|
foreach (var (type, attribute) in Configuration.Body)
|
||||||
|
{
|
||||||
|
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.Vertical,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = settingLabelPanelMargin
|
||||||
|
};
|
||||||
|
|
||||||
|
RowDefinition gridRow = new RowDefinition();
|
||||||
|
mainPanel.RowDefinitions.Add(gridRow);
|
||||||
|
var name = new TextBlock()
|
||||||
|
{
|
||||||
|
Text = attribute.Label,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = settingLabelMargin,
|
||||||
|
TextWrapping = TextWrapping.WrapWithOverflow
|
||||||
|
};
|
||||||
|
|
||||||
|
var desc = new TextBlock()
|
||||||
|
{
|
||||||
|
Text = attribute.Description,
|
||||||
|
FontSize = 12,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = settingDescMargin,
|
||||||
|
TextWrapping = TextWrapping.WrapWithOverflow
|
||||||
|
};
|
||||||
|
|
||||||
|
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
|
||||||
|
|
||||||
|
if (attribute.Description == null) /* if no description, hide */
|
||||||
|
desc.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
|
||||||
|
if (type != "textBlock") /* if textBlock, hide desc */
|
||||||
|
{
|
||||||
|
panel.Children.Add(name);
|
||||||
|
panel.Children.Add(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Grid.SetColumn(panel, 0);
|
||||||
|
Grid.SetRow(panel, rowCount);
|
||||||
|
|
||||||
|
FrameworkElement contentControl;
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "textBlock":
|
||||||
|
{
|
||||||
|
contentControl = new TextBlock
|
||||||
|
{
|
||||||
|
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||||
|
Margin = settingTextBlockMargin,
|
||||||
|
Padding = new Thickness(0, 0, 0, 0),
|
||||||
|
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
||||||
|
TextAlignment = TextAlignment.Left,
|
||||||
|
TextWrapping = TextWrapping.Wrap
|
||||||
|
};
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
Margin = new Thickness(10, 0, 0, 0),
|
||||||
|
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||||
|
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||||
|
ToolTip = attribute.Description
|
||||||
|
};
|
||||||
|
|
||||||
|
textBox.TextChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
Settings[attribute.Name] = textBox.Text;
|
||||||
|
};
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
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 = settingCheckboxMargin,
|
||||||
|
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||||
|
ToolTip = attribute.Description
|
||||||
|
};
|
||||||
|
|
||||||
|
checkBox.Click += (sender, _) =>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
mainPanel.Children.Add(panel);
|
||||||
|
mainPanel.Children.Add(contentControl);
|
||||||
|
rowCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
158
Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
Normal file
158
Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Flow.Launcher.Core.Plugin.JsonRPCV2Models;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Microsoft.VisualStudio.Threading;
|
||||||
|
using StreamJsonRpc;
|
||||||
|
using IAsyncDisposable = System.IAsyncDisposable;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
{
|
||||||
|
internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated
|
||||||
|
{
|
||||||
|
public const string JsonRpc = "JsonRPC";
|
||||||
|
|
||||||
|
protected abstract IDuplexPipe ClientPipe { get; set; }
|
||||||
|
|
||||||
|
protected StreamReader ErrorStream { get; set; }
|
||||||
|
|
||||||
|
private JsonRpc RPC { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method,
|
||||||
|
argument: result.JsonRPCAction.Parameters);
|
||||||
|
|
||||||
|
return res.Hide;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
|
||||||
|
|
||||||
|
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = JTF.Run(() => RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("context_menu",
|
||||||
|
new object[] { selectedResult.ContextData }));
|
||||||
|
|
||||||
|
var results = ParseResults(res);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new List<Result>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("query",
|
||||||
|
new object[] { query, Settings.Inner },
|
||||||
|
token);
|
||||||
|
|
||||||
|
var results = ParseResults(res);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new List<Result>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override async Task InitAsync(PluginInitContext context)
|
||||||
|
{
|
||||||
|
await base.InitAsync(context);
|
||||||
|
|
||||||
|
SetupJsonRPC();
|
||||||
|
|
||||||
|
_ = ReadErrorAsync();
|
||||||
|
|
||||||
|
await RPC.InvokeAsync("initialize", context);
|
||||||
|
|
||||||
|
async Task ReadErrorAsync()
|
||||||
|
{
|
||||||
|
var error = await ErrorStream.ReadToEndAsync();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(error))
|
||||||
|
{
|
||||||
|
throw new Exception(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event ResultUpdatedEventHandler ResultsUpdated;
|
||||||
|
|
||||||
|
protected enum MessageHandlerType
|
||||||
|
{
|
||||||
|
HeaderDelimited,
|
||||||
|
LengthHeaderDelimited,
|
||||||
|
NewLineDelimited
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract MessageHandlerType MessageHandler { get; }
|
||||||
|
|
||||||
|
|
||||||
|
private void SetupJsonRPC()
|
||||||
|
{
|
||||||
|
var formatter = new SystemTextJsonFormatter { JsonSerializerOptions = RequestSerializeOption };
|
||||||
|
IJsonRpcMessageHandler handler = MessageHandler switch
|
||||||
|
{
|
||||||
|
MessageHandlerType.HeaderDelimited => new HeaderDelimitedMessageHandler(ClientPipe, formatter),
|
||||||
|
MessageHandlerType.LengthHeaderDelimited => new LengthHeaderMessageHandler(ClientPipe, formatter),
|
||||||
|
MessageHandlerType.NewLineDelimited => new NewLineDelimitedMessageHandler(ClientPipe, formatter),
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
};
|
||||||
|
|
||||||
|
RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API));
|
||||||
|
|
||||||
|
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((rawQuery, response) =>
|
||||||
|
{
|
||||||
|
var results = ParseResults(response);
|
||||||
|
ResultsUpdated?.Invoke(this,
|
||||||
|
new ResultUpdatedEventArgs { Query = new Query() { RawQuery = rawQuery }, Results = results });
|
||||||
|
}));
|
||||||
|
RPC.SynchronizationContext = null;
|
||||||
|
RPC.StartListening();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task ReloadDataAsync()
|
||||||
|
{
|
||||||
|
SetupJsonRPC();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RPC.InvokeAsync("close");
|
||||||
|
}
|
||||||
|
catch (RemoteMethodNotFoundException e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RPC?.Dispose();
|
||||||
|
ErrorStream?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||||
|
{
|
||||||
|
public record JsonRPCExecuteResponse(bool Hide = true);
|
||||||
|
}
|
||||||
177
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
Normal file
177
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Flow.Launcher.Plugin.SharedModels;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||||
|
{
|
||||||
|
public class JsonRPCPublicAPI
|
||||||
|
{
|
||||||
|
private IPublicAPI _api;
|
||||||
|
|
||||||
|
public JsonRPCPublicAPI(IPublicAPI api)
|
||||||
|
{
|
||||||
|
_api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeQuery(string query, bool requery = false)
|
||||||
|
{
|
||||||
|
_api.ChangeQuery(query, requery);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RestartApp()
|
||||||
|
{
|
||||||
|
_api.RestartApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShellRun(string cmd, string filename = "cmd.exe")
|
||||||
|
{
|
||||||
|
_api.ShellRun(cmd, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true)
|
||||||
|
{
|
||||||
|
_api.CopyToClipboard(text, directCopy, showDefaultNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveAppAllSettings()
|
||||||
|
{
|
||||||
|
_api.SaveAppAllSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SavePluginSettings()
|
||||||
|
{
|
||||||
|
_api.SavePluginSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ReloadAllPluginDataAsync()
|
||||||
|
{
|
||||||
|
return _api.ReloadAllPluginData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckForNewUpdate()
|
||||||
|
{
|
||||||
|
_api.CheckForNewUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowMsgError(string title, string subTitle = "")
|
||||||
|
{
|
||||||
|
_api.ShowMsgError(title, subTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowMainWindow()
|
||||||
|
{
|
||||||
|
_api.ShowMainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HideMainWindow()
|
||||||
|
{
|
||||||
|
_api.HideMainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMainWindowVisible()
|
||||||
|
{
|
||||||
|
return _api.IsMainWindowVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowMsg(string title, string subTitle = "", string iconPath = "")
|
||||||
|
{
|
||||||
|
_api.ShowMsg(title, subTitle, iconPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
|
||||||
|
{
|
||||||
|
_api.ShowMsg(title, subTitle, iconPath, useMainWindowAsOwner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenSettingDialog()
|
||||||
|
{
|
||||||
|
_api.OpenSettingDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetTranslation(string key)
|
||||||
|
{
|
||||||
|
return _api.GetTranslation(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PluginPair> GetAllPlugins()
|
||||||
|
{
|
||||||
|
return _api.GetAllPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public MatchResult FuzzySearch(string query, string stringToCompare)
|
||||||
|
{
|
||||||
|
return _api.FuzzySearch(query, stringToCompare);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return _api.HttpGetStringAsync(url, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return _api.HttpGetStreamAsync(url, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return _api.HttpDownloadAsync(url, filePath, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddActionKeyword(string pluginId, string newActionKeyword)
|
||||||
|
{
|
||||||
|
_api.AddActionKeyword(pluginId, newActionKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveActionKeyword(string pluginId, string oldActionKeyword)
|
||||||
|
{
|
||||||
|
_api.RemoveActionKeyword(pluginId, oldActionKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ActionKeywordAssigned(string actionKeyword)
|
||||||
|
{
|
||||||
|
return _api.ActionKeywordAssigned(actionKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogDebug(string className, string message, [CallerMemberName] string methodName = "")
|
||||||
|
{
|
||||||
|
_api.LogDebug(className, message, methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogInfo(string className, string message, [CallerMemberName] string methodName = "")
|
||||||
|
{
|
||||||
|
_api.LogInfo(className, message, methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogWarn(string className, string message, [CallerMemberName] string methodName = "")
|
||||||
|
{
|
||||||
|
_api.LogWarn(className, message, methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null)
|
||||||
|
{
|
||||||
|
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void OpenUrl(string url, bool? inPrivate = null)
|
||||||
|
{
|
||||||
|
_api.OpenUrl(url, inPrivate);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void OpenAppUri(string appUri)
|
||||||
|
{
|
||||||
|
_api.OpenAppUri(appUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||||
|
{
|
||||||
|
public record JsonRPCQueryRequest(
|
||||||
|
List<JsonRPCResult> Results
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
@ -27,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
_startInfo.ArgumentList[1] = request.ToString();
|
_startInfo.ArgumentList[1] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||||
return ExecuteAsync(_startInfo, token);
|
return ExecuteAsync(_startInfo, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||||
_startInfo.ArgumentList[1] = rpcRequest.ToString();
|
_startInfo.ArgumentList[1] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||||
return Execute(_startInfo);
|
return Execute(_startInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,8 +43,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||||
_startInfo.ArgumentList.Add(string.Empty);
|
_startInfo.ArgumentList.Add(string.Empty);
|
||||||
await base.InitAsync(context);
|
|
||||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
await base.InitAsync(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
Flow.Launcher.Core/Plugin/NodePluginV2.cs
Normal file
30
Flow.Launcher.Core/Plugin/NodePluginV2.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Execution of JavaScript & TypeScript plugins
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class NodePluginV2 : ProcessStreamPluginV2
|
||||||
|
{
|
||||||
|
public NodePluginV2(string filename)
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo { FileName = filename, };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ProcessStartInfo StartInfo { get; set; }
|
||||||
|
|
||||||
|
public override async Task InitAsync(PluginInitContext context)
|
||||||
|
{
|
||||||
|
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||||
|
await base.InitAsync(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MessageHandlerType MessageHandler { get; } = MessageHandlerType.HeaderDelimited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,17 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
return existAssembly ?? (assemblyPath == null ? null : LoadFromAssemblyPath(assemblyPath));
|
return existAssembly ?? (assemblyPath == null ? null : LoadFromAssemblyPath(assemblyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||||
|
{
|
||||||
|
var path = dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||||
|
if (!string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return LoadUnmanagedDllFromPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type)
|
internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type)
|
||||||
{
|
{
|
||||||
|
|
@ -41,4 +52,4 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type));
|
return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -27,9 +29,9 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
public static IPublicAPI API { private set; get; }
|
public static IPublicAPI API { private set; get; }
|
||||||
|
|
||||||
// todo happlebao, this should not be public, the indicator function should be embeded
|
private static PluginsSettings Settings;
|
||||||
public static PluginsSettings Settings;
|
|
||||||
private static List<PluginMetadata> _metadatas;
|
private static List<PluginMetadata> _metadatas;
|
||||||
|
private static List<string> _modifiedPlugins = new List<string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Directories that will hold Flow Launcher plugin directory
|
/// Directories that will hold Flow Launcher plugin directory
|
||||||
|
|
@ -242,7 +244,19 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
throw new FlowPluginException(metadata, e);
|
Result r = new()
|
||||||
|
{
|
||||||
|
Title = $"{metadata.Name}: Failed to respond!",
|
||||||
|
SubTitle = "Select this result for more info",
|
||||||
|
IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon,
|
||||||
|
PluginDirectory = metadata.PluginDirectory,
|
||||||
|
ActionKeywordAssigned = query.ActionKeyword,
|
||||||
|
PluginID = metadata.ID,
|
||||||
|
OriginQuery = query,
|
||||||
|
Action = _ => { throw new FlowPluginException(metadata, e);},
|
||||||
|
Score = -100
|
||||||
|
};
|
||||||
|
results.Add(r);
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
@ -363,5 +377,159 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
RemoveActionKeyword(id, oldActionKeyword);
|
RemoveActionKeyword(id, oldActionKeyword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
|
||||||
|
{
|
||||||
|
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
|
||||||
|
var unzippedFilesCount = Directory.GetFiles(unzippedParentFolderPath).Length;
|
||||||
|
|
||||||
|
// adjust path depending on how the plugin is zipped up
|
||||||
|
// the recommended should be to zip up the folder not the contents
|
||||||
|
if (unzippedFolderCount == 1 && unzippedFilesCount == 0)
|
||||||
|
// folder is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/pluginFolderName/
|
||||||
|
return Directory.GetDirectories(unzippedParentFolderPath)[0];
|
||||||
|
|
||||||
|
if (unzippedFilesCount > 1)
|
||||||
|
// content is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/
|
||||||
|
return unzippedParentFolderPath;
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SameOrLesserPluginVersionExists(string metadataPath)
|
||||||
|
{
|
||||||
|
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
|
||||||
|
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
|
||||||
|
&& newMetadata.Version.CompareTo(x.Metadata.Version) <= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Public functions
|
||||||
|
|
||||||
|
public static bool PluginModified(string uuid)
|
||||||
|
{
|
||||||
|
return _modifiedPlugins.Contains(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
|
||||||
|
/// unless it's a local path installation
|
||||||
|
/// </summary>
|
||||||
|
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||||
|
{
|
||||||
|
InstallPlugin(newVersion, zipFilePath, checkModified:false);
|
||||||
|
UninstallPlugin(existingVersion, removeSettings:false, checkModified:false);
|
||||||
|
_modifiedPlugins.Add(existingVersion.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
|
||||||
|
/// </summary>
|
||||||
|
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
|
||||||
|
{
|
||||||
|
InstallPlugin(plugin, zipFilePath, checkModified: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uninstall a plugin.
|
||||||
|
/// </summary>
|
||||||
|
public static void UninstallPlugin(PluginMetadata plugin, bool removeSettings = true)
|
||||||
|
{
|
||||||
|
UninstallPlugin(plugin, removeSettings, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Internal functions
|
||||||
|
|
||||||
|
internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
|
||||||
|
{
|
||||||
|
if (checkModified && PluginModified(plugin.ID))
|
||||||
|
{
|
||||||
|
// Distinguish exception from installing same or less version
|
||||||
|
throw new ArgumentException($"Plugin {plugin.Name} {plugin.ID} has been modified.", nameof(plugin));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unzip plugin files to temp folder
|
||||||
|
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
|
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
|
||||||
|
|
||||||
|
if(!plugin.IsFromLocalInstallPath)
|
||||||
|
File.Delete(zipFilePath);
|
||||||
|
|
||||||
|
var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
|
||||||
|
|
||||||
|
var metadataJsonFilePath = string.Empty;
|
||||||
|
if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName)))
|
||||||
|
metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
|
||||||
|
|
||||||
|
var defaultPluginIDs = new List<string>
|
||||||
|
{
|
||||||
|
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
|
||||||
|
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
|
||||||
|
"572be03c74c642baae319fc283e561a8", // Explorer
|
||||||
|
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
|
||||||
|
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
|
||||||
|
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
|
||||||
|
"791FC278BA414111B8D1886DFE447410", // Program
|
||||||
|
"D409510CD0D2481F853690A07E6DC426", // Shell
|
||||||
|
"CEA08895D2544B019B2E9C5009600DF4", // Sys
|
||||||
|
"0308FD86DE0A4DEE8D62B9B535370992", // URL
|
||||||
|
"565B73353DBF4806919830B9202EE3BF", // WebSearch
|
||||||
|
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
// Treat default plugin differently, it needs to be removable along with each flow release
|
||||||
|
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
|
||||||
|
? DataLocation.PluginsDirectory
|
||||||
|
: Constant.PreinstalledDirectory;
|
||||||
|
|
||||||
|
var newPluginPath = Path.Combine(installDirectory, folderName);
|
||||||
|
|
||||||
|
FilesFolders.CopyAll(pluginFolderPath, newPluginPath);
|
||||||
|
|
||||||
|
Directory.Delete(tempFolderPluginPath, true);
|
||||||
|
|
||||||
|
if (checkModified)
|
||||||
|
{
|
||||||
|
_modifiedPlugins.Add(plugin.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void UninstallPlugin(PluginMetadata plugin, bool removeSettings, bool checkModified)
|
||||||
|
{
|
||||||
|
if (checkModified && PluginModified(plugin.ID))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeSettings)
|
||||||
|
{
|
||||||
|
Settings.Plugins.Remove(plugin.ID);
|
||||||
|
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marked for deletion. Will be deleted on next start up
|
||||||
|
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
|
||||||
|
|
||||||
|
if (checkModified)
|
||||||
|
{
|
||||||
|
_modifiedPlugins.Add(plugin.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,22 +19,33 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||||
{
|
{
|
||||||
var dotnetPlugins = DotNetPlugins(metadatas);
|
var dotnetPlugins = DotNetPlugins(metadatas);
|
||||||
|
|
||||||
var pythonEnv = new PythonEnvironment(metadatas, settings);
|
var pythonEnv = new PythonEnvironment(metadatas, settings);
|
||||||
|
var pythonV2Env = new PythonV2Environment(metadatas, settings);
|
||||||
var tsEnv = new TypeScriptEnvironment(metadatas, settings);
|
var tsEnv = new TypeScriptEnvironment(metadatas, settings);
|
||||||
var jsEnv = new JavaScriptEnvironment(metadatas, settings);
|
var jsEnv = new JavaScriptEnvironment(metadatas, settings);
|
||||||
|
var tsV2Env = new TypeScriptV2Environment(metadatas, settings);
|
||||||
|
var jsV2Env = new JavaScriptV2Environment(metadatas, settings);
|
||||||
var pythonPlugins = pythonEnv.Setup();
|
var pythonPlugins = pythonEnv.Setup();
|
||||||
|
var pythonV2Plugins = pythonV2Env.Setup();
|
||||||
var tsPlugins = tsEnv.Setup();
|
var tsPlugins = tsEnv.Setup();
|
||||||
var jsPlugins = jsEnv.Setup();
|
var jsPlugins = jsEnv.Setup();
|
||||||
|
var tsV2Plugins = tsV2Env.Setup();
|
||||||
|
var jsV2Plugins = jsV2Env.Setup();
|
||||||
|
|
||||||
var executablePlugins = ExecutablePlugins(metadatas);
|
var executablePlugins = ExecutablePlugins(metadatas);
|
||||||
|
var executableV2Plugins = ExecutableV2Plugins(metadatas);
|
||||||
|
|
||||||
var plugins = dotnetPlugins
|
var plugins = dotnetPlugins
|
||||||
.Concat(pythonPlugins)
|
.Concat(pythonPlugins)
|
||||||
.Concat(tsPlugins)
|
.Concat(pythonV2Plugins)
|
||||||
.Concat(jsPlugins)
|
.Concat(tsPlugins)
|
||||||
.Concat(executablePlugins)
|
.Concat(jsPlugins)
|
||||||
.ToList();
|
.Concat(tsV2Plugins)
|
||||||
|
.Concat(jsV2Plugins)
|
||||||
|
.Concat(executablePlugins)
|
||||||
|
.Concat(executableV2Plugins)
|
||||||
|
.ToList();
|
||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +104,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
|
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||||
});
|
});
|
||||||
metadata.InitTime += milliseconds;
|
metadata.InitTime += milliseconds;
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +129,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||||
{
|
{
|
||||||
return source
|
return source
|
||||||
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
|
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
@ -127,5 +138,15 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
|
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source)
|
||||||
|
{
|
||||||
|
return source
|
||||||
|
.Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(metadata => new PluginPair
|
||||||
|
{
|
||||||
|
Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
Normal file
91
Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Flow.Launcher.Infrastructure;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Meziantou.Framework.Win32;
|
||||||
|
using Microsoft.VisualBasic.ApplicationServices;
|
||||||
|
using Nerdbank.Streams;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
{
|
||||||
|
internal abstract class ProcessStreamPluginV2 : JsonRPCPluginV2
|
||||||
|
{
|
||||||
|
private static JobObject _jobObject = new JobObject();
|
||||||
|
|
||||||
|
static ProcessStreamPluginV2()
|
||||||
|
{
|
||||||
|
_jobObject.SetLimits(new JobObjectLimits()
|
||||||
|
{
|
||||||
|
Flags = JobObjectLimitFlags.KillOnJobClose | JobObjectLimitFlags.DieOnUnhandledException |
|
||||||
|
JobObjectLimitFlags.SilentBreakawayOk
|
||||||
|
});
|
||||||
|
|
||||||
|
_jobObject.AssignProcess(Process.GetCurrentProcess());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed override IDuplexPipe ClientPipe { get; set; } = null!;
|
||||||
|
|
||||||
|
protected abstract ProcessStartInfo StartInfo { get; set; }
|
||||||
|
|
||||||
|
protected Process ClientProcess { get; set; } = null!;
|
||||||
|
|
||||||
|
public override async Task InitAsync(PluginInitContext context)
|
||||||
|
{
|
||||||
|
StartInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
|
||||||
|
StartInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
|
||||||
|
StartInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
|
||||||
|
|
||||||
|
StartInfo.RedirectStandardError = true;
|
||||||
|
StartInfo.RedirectStandardInput = true;
|
||||||
|
StartInfo.RedirectStandardOutput = true;
|
||||||
|
StartInfo.CreateNoWindow = true;
|
||||||
|
StartInfo.UseShellExecute = false;
|
||||||
|
StartInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
|
||||||
|
var process = Process.Start(StartInfo);
|
||||||
|
ArgumentNullException.ThrowIfNull(process);
|
||||||
|
ClientProcess = process;
|
||||||
|
_jobObject.AssignProcess(ClientProcess);
|
||||||
|
|
||||||
|
SetupPipe(ClientProcess);
|
||||||
|
|
||||||
|
ErrorStream = ClientProcess.StandardError;
|
||||||
|
|
||||||
|
await base.InitAsync(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupPipe(Process process)
|
||||||
|
{
|
||||||
|
var (reader, writer) = (PipeReader.Create(process.StandardOutput.BaseStream),
|
||||||
|
PipeWriter.Create(process.StandardInput.BaseStream));
|
||||||
|
ClientPipe = new DuplexPipe(reader, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override async Task ReloadDataAsync()
|
||||||
|
{
|
||||||
|
var oldProcess = ClientProcess;
|
||||||
|
ClientProcess = Process.Start(StartInfo);
|
||||||
|
ArgumentNullException.ThrowIfNull(ClientProcess);
|
||||||
|
SetupPipe(ClientProcess);
|
||||||
|
await base.ReloadDataAsync();
|
||||||
|
oldProcess.Kill(true);
|
||||||
|
await oldProcess.WaitForExitAsync();
|
||||||
|
oldProcess.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
ClientProcess.Kill(true);
|
||||||
|
await ClientProcess.WaitForExitAsync();
|
||||||
|
ClientProcess.Dispose();
|
||||||
|
await base.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
|
|
@ -22,7 +23,6 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// temp fix for issue #667
|
|
||||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
||||||
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
_startInfo.ArgumentList[2] = request.ToString();
|
_startInfo.ArgumentList[2] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||||
|
|
||||||
return ExecuteAsync(_startInfo, token);
|
return ExecuteAsync(_startInfo, token);
|
||||||
}
|
}
|
||||||
|
|
@ -45,8 +45,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||||
_startInfo.ArgumentList[2] = rpcRequest.ToString();
|
_startInfo.ArgumentList[2] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
_startInfo.WorkingDirectory = Context.CurrentPluginMetadata.PluginDirectory;
|
||||||
// TODO: Async Action
|
// TODO: Async Action
|
||||||
return Execute(_startInfo);
|
return Execute(_startInfo);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
Flow.Launcher.Core/Plugin/PythonPluginV2.cs
Normal file
42
Flow.Launcher.Core/Plugin/PythonPluginV2.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Flow.Launcher.Core.Plugin.JsonRPCV2Models;
|
||||||
|
using Flow.Launcher.Infrastructure;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Microsoft.VisualStudio.Threading;
|
||||||
|
using Nerdbank.Streams;
|
||||||
|
using StreamJsonRpc;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
{
|
||||||
|
internal sealed class PythonPluginV2 : ProcessStreamPluginV2
|
||||||
|
{
|
||||||
|
protected override ProcessStartInfo StartInfo { get; set; }
|
||||||
|
|
||||||
|
public PythonPluginV2(string filename)
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo { FileName = filename, };
|
||||||
|
|
||||||
|
var path = Path.Combine(Constant.ProgramDirectory, JsonRpc);
|
||||||
|
StartInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||||
|
|
||||||
|
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
||||||
|
StartInfo.ArgumentList.Add("-B");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task InitAsync(PluginInitContext context)
|
||||||
|
{
|
||||||
|
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||||
|
await base.InitAsync(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MessageHandlerType MessageHandler { get; } = MessageHandlerType.NewLineDelimited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Resource
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
|
|
@ -27,6 +27,8 @@ namespace Flow.Launcher.Core.Resource
|
||||||
public static Language Turkish = new Language("tr", "Türkçe");
|
public static Language Turkish = new Language("tr", "Türkçe");
|
||||||
public static Language Czech = new Language("cs", "čeština");
|
public static Language Czech = new Language("cs", "čeština");
|
||||||
public static Language Arabic = new Language("ar", "اللغة العربية");
|
public static Language Arabic = new Language("ar", "اللغة العربية");
|
||||||
|
public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt");
|
||||||
|
|
||||||
|
|
||||||
public static List<Language> GetAvailableLanguages()
|
public static List<Language> GetAvailableLanguages()
|
||||||
{
|
{
|
||||||
|
|
@ -54,7 +56,8 @@ namespace Flow.Launcher.Core.Resource
|
||||||
Slovak,
|
Slovak,
|
||||||
Turkish,
|
Turkish,
|
||||||
Czech,
|
Czech,
|
||||||
Arabic
|
Arabic,
|
||||||
|
Vietnamese
|
||||||
};
|
};
|
||||||
return languages;
|
return languages;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,13 +96,10 @@ namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
LoadLanguage(language);
|
LoadLanguage(language);
|
||||||
}
|
}
|
||||||
// Culture of this thread
|
// Culture of main thread
|
||||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows
|
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||||
// App domain
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.DefaultThreadCurrentCulture;
|
|
||||||
|
|
||||||
// Raise event after culture is set
|
// Raise event after culture is set
|
||||||
Settings.Language = language.LanguageCode;
|
Settings.Language = language.LanguageCode;
|
||||||
|
|
@ -193,7 +190,7 @@ namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||||
pluginI18N.OnCultureInfoChanged(CultureInfo.DefaultThreadCurrentCulture);
|
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -176,8 +176,6 @@ namespace Flow.Launcher.Core.Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||||
dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
|
||||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle &&
|
|
||||||
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
|
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
|
||||||
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
||||||
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
|
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
|
||||||
|
|
@ -189,9 +187,25 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||||
Array.ForEach(
|
Array.ForEach(
|
||||||
new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o
|
new[] { resultItemStyle, resultItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o
|
||||||
=> Array.ForEach(setters, p => o.Setters.Add(p)));
|
=> Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||||
|
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
|
||||||
|
{
|
||||||
|
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultSubFont));
|
||||||
|
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultSubFontStyle));
|
||||||
|
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultSubFontWeight));
|
||||||
|
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultSubFontStretch));
|
||||||
|
|
||||||
|
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||||
|
Array.ForEach(
|
||||||
|
new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o
|
||||||
|
=> Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||||
|
}
|
||||||
|
|
||||||
/* Ignore Theme Window Width and use setting */
|
/* Ignore Theme Window Width and use setting */
|
||||||
var windowStyle = dict["WindowStyle"] as Style;
|
var windowStyle = dict["WindowStyle"] as Style;
|
||||||
var width = Settings.WindowSize;
|
var width = Settings.WindowSize;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ namespace Flow.Launcher.Infrastructure
|
||||||
public static class Constant
|
public static class Constant
|
||||||
{
|
{
|
||||||
public const string FlowLauncher = "Flow.Launcher";
|
public const string FlowLauncher = "Flow.Launcher";
|
||||||
|
public const string FlowLauncherFullName = "Flow Launcher";
|
||||||
public const string Plugins = "Plugins";
|
public const string Plugins = "Plugins";
|
||||||
public const string PluginMetadataFileName = "plugin.json";
|
public const string PluginMetadataFileName = "plugin.json";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,14 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||||
|
<PackageReference Include="FastCache.Cached" Version="1.8.2" />
|
||||||
<PackageReference Include="Fody" Version="6.5.5">
|
<PackageReference Include="Fody" Version="6.5.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="MemoryPack" Version="1.21.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.7.30" />
|
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.7.30" />
|
||||||
<PackageReference Include="NLog" Version="4.7.10" />
|
<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="PropertyChanged.Fody" Version="3.4.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
<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-->
|
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ using System.Windows.Input;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
{
|
{
|
||||||
public class HotkeyModel
|
public record struct HotkeyModel
|
||||||
{
|
{
|
||||||
public bool Alt { get; set; }
|
public bool Alt { get; set; }
|
||||||
public bool Shift { get; set; }
|
public bool Shift { get; set; }
|
||||||
|
|
@ -17,8 +17,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
|
|
||||||
private static readonly Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string>
|
private static readonly Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string>
|
||||||
{
|
{
|
||||||
{Key.Space, "Space"},
|
{ Key.Space, "Space" }, { Key.Oem3, "~" }
|
||||||
{Key.Oem3, "~"}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public ModifierKeys ModifierKeys
|
public ModifierKeys ModifierKeys
|
||||||
|
|
@ -30,18 +29,22 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
{
|
{
|
||||||
modifierKeys |= ModifierKeys.Alt;
|
modifierKeys |= ModifierKeys.Alt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Shift)
|
if (Shift)
|
||||||
{
|
{
|
||||||
modifierKeys |= ModifierKeys.Shift;
|
modifierKeys |= ModifierKeys.Shift;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Win)
|
if (Win)
|
||||||
{
|
{
|
||||||
modifierKeys |= ModifierKeys.Windows;
|
modifierKeys |= ModifierKeys.Windows;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Ctrl)
|
if (Ctrl)
|
||||||
{
|
{
|
||||||
modifierKeys |= ModifierKeys.Control;
|
modifierKeys |= ModifierKeys.Control;
|
||||||
}
|
}
|
||||||
|
|
||||||
return modifierKeys;
|
return modifierKeys;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,31 +69,37 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<string> keys = hotkeyString.Replace(" ", "").Split('+').ToList();
|
List<string> keys = hotkeyString.Replace(" ", "").Split('+').ToList();
|
||||||
if (keys.Contains("Alt"))
|
if (keys.Contains("Alt"))
|
||||||
{
|
{
|
||||||
Alt = true;
|
Alt = true;
|
||||||
keys.Remove("Alt");
|
keys.Remove("Alt");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.Contains("Shift"))
|
if (keys.Contains("Shift"))
|
||||||
{
|
{
|
||||||
Shift = true;
|
Shift = true;
|
||||||
keys.Remove("Shift");
|
keys.Remove("Shift");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.Contains("Win"))
|
if (keys.Contains("Win"))
|
||||||
{
|
{
|
||||||
Win = true;
|
Win = true;
|
||||||
keys.Remove("Win");
|
keys.Remove("Win");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.Contains("Ctrl"))
|
if (keys.Contains("Ctrl"))
|
||||||
{
|
{
|
||||||
Ctrl = true;
|
Ctrl = true;
|
||||||
keys.Remove("Ctrl");
|
keys.Remove("Ctrl");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.Count == 1)
|
if (keys.Count == 1)
|
||||||
{
|
{
|
||||||
string charKey = keys[0];
|
string charKey = keys[0];
|
||||||
KeyValuePair<Key, string>? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey);
|
KeyValuePair<Key, string>? specialSymbolPair =
|
||||||
|
specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey);
|
||||||
if (specialSymbolPair.Value.Value != null)
|
if (specialSymbolPair.Value.Value != null)
|
||||||
{
|
{
|
||||||
CharKey = specialSymbolPair.Value.Key;
|
CharKey = specialSymbolPair.Value.Key;
|
||||||
|
|
@ -103,7 +112,6 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,33 +119,39 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
List<string> keys = new List<string>();
|
return string.Join(" + ", EnumerateDisplayKeys());
|
||||||
if (Ctrl)
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> EnumerateDisplayKeys()
|
||||||
|
{
|
||||||
|
if (Ctrl && CharKey is not (Key.LeftCtrl or Key.RightCtrl))
|
||||||
{
|
{
|
||||||
keys.Add("Ctrl");
|
yield return "Ctrl";
|
||||||
}
|
}
|
||||||
if (Alt)
|
|
||||||
|
if (Alt && CharKey is not (Key.LeftAlt or Key.RightAlt))
|
||||||
{
|
{
|
||||||
keys.Add("Alt");
|
yield return "Alt";
|
||||||
}
|
}
|
||||||
if (Shift)
|
|
||||||
|
if (Shift && CharKey is not (Key.LeftShift or Key.RightShift))
|
||||||
{
|
{
|
||||||
keys.Add("Shift");
|
yield return "Shift";
|
||||||
}
|
}
|
||||||
if (Win)
|
|
||||||
|
if (Win && CharKey is not (Key.LWin or Key.RWin))
|
||||||
{
|
{
|
||||||
keys.Add("Win");
|
yield return "Win";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CharKey != Key.None)
|
if (CharKey != Key.None)
|
||||||
{
|
{
|
||||||
keys.Add(specialSymbolDictionary.ContainsKey(CharKey)
|
yield return specialSymbolDictionary.TryGetValue(CharKey, out var value)
|
||||||
? specialSymbolDictionary[CharKey]
|
? value
|
||||||
: CharKey.ToString());
|
: CharKey.ToString();
|
||||||
}
|
}
|
||||||
return string.Join(" + ", keys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validate hotkey
|
/// Validate hotkey
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -164,11 +178,13 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
{
|
{
|
||||||
KeyGesture keyGesture = new KeyGesture(CharKey, ModifierKeys);
|
KeyGesture keyGesture = new KeyGesture(CharKey, ModifierKeys);
|
||||||
}
|
}
|
||||||
catch (System.Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException)
|
catch (System.Exception e) when
|
||||||
|
(e is NotSupportedException || e is InvalidEnumArgumentException)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ModifierKeys == ModifierKeys.None)
|
if (ModifierKeys == ModifierKeys.None)
|
||||||
{
|
{
|
||||||
return !IsPrintableCharacter(CharKey);
|
return !IsPrintableCharacter(CharKey);
|
||||||
|
|
@ -206,18 +222,6 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
key == Key.Decimal;
|
key == Key.Decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (obj is HotkeyModel other)
|
|
||||||
{
|
|
||||||
return ModifierKeys == other.ModifierKeys && CharKey == other.CharKey;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
return HashCode.Combine(ModifierKeys, CharKey);
|
return HashCode.Combine(ModifierKeys, CharKey);
|
||||||
|
|
|
||||||
17
Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs
Normal file
17
Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Infrastructure.Hotkey;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface that you should implement in your settings class to be able to pass it to
|
||||||
|
/// <c>Flow.Launcher.HotkeyControlDialog</c>. It allows the dialog to display the hotkeys that have already been
|
||||||
|
/// registered, and optionally provide a way to unregister them.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHotkeySettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A list of hotkeys that have already been registered. The dialog will display these hotkeys and provide a way to
|
||||||
|
/// unregister them.
|
||||||
|
/// </summary>
|
||||||
|
public List<RegisteredHotkeyData> RegisteredHotkeys { get; }
|
||||||
|
}
|
||||||
119
Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs
Normal file
119
Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Infrastructure.Hotkey;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a hotkey that has been registered. Used in <c>Flow.Launcher.HotkeyControlDialog</c> via
|
||||||
|
/// <see cref="UserSettings"/> and <see cref="IHotkeySettings"/> to display errors if user tries to register a hotkey
|
||||||
|
/// that has already been registered, and optionally provides a way to unregister the hotkey.
|
||||||
|
/// </summary>
|
||||||
|
public record RegisteredHotkeyData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="HotkeyModel"/> representation of this hotkey.
|
||||||
|
/// </summary>
|
||||||
|
public HotkeyModel Hotkey { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// String key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
|
||||||
|
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
|
||||||
|
/// </summary>
|
||||||
|
public string DescriptionResourceKey { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Array of values that will replace <c>{0}</c>, <c>{1}</c>, <c>{2}</c>, etc. in the localized string found via
|
||||||
|
/// <see cref="DescriptionResourceKey"/>.
|
||||||
|
/// </summary>
|
||||||
|
public object?[] DescriptionFormatVariables { get; } = Array.Empty<object?>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that
|
||||||
|
/// this hotkey can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
|
||||||
|
/// </summary>
|
||||||
|
public Action? RemoveHotkey { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
|
||||||
|
/// <c>descriptionResourceKey</c> doesn't need any arguments for <c>string.Format</c>. If it does,
|
||||||
|
/// use one of the other constructors.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hotkey">
|
||||||
|
/// The hotkey this class will represent.
|
||||||
|
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
|
||||||
|
/// </param>
|
||||||
|
/// <param name="descriptionResourceKey">
|
||||||
|
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
|
||||||
|
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
|
||||||
|
/// </param>
|
||||||
|
/// <param name="removeHotkey">
|
||||||
|
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
|
||||||
|
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
|
||||||
|
/// </param>
|
||||||
|
public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action? removeHotkey = null)
|
||||||
|
{
|
||||||
|
Hotkey = new HotkeyModel(hotkey);
|
||||||
|
DescriptionResourceKey = descriptionResourceKey;
|
||||||
|
RemoveHotkey = removeHotkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
|
||||||
|
/// <c>descriptionResourceKey</c> needs exactly one argument for <c>string.Format</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hotkey">
|
||||||
|
/// The hotkey this class will represent.
|
||||||
|
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
|
||||||
|
/// </param>
|
||||||
|
/// <param name="descriptionResourceKey">
|
||||||
|
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
|
||||||
|
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
|
||||||
|
/// </param>
|
||||||
|
/// <param name="descriptionFormatVariable">
|
||||||
|
/// The value that will replace <c>{0}</c> in the localized string found via <c>description</c>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="removeHotkey">
|
||||||
|
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
|
||||||
|
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
|
||||||
|
/// </param>
|
||||||
|
public RegisteredHotkeyData(
|
||||||
|
string hotkey, string descriptionResourceKey, object? descriptionFormatVariable, Action? removeHotkey = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Hotkey = new HotkeyModel(hotkey);
|
||||||
|
DescriptionResourceKey = descriptionResourceKey;
|
||||||
|
DescriptionFormatVariables = new[] { descriptionFormatVariable };
|
||||||
|
RemoveHotkey = removeHotkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of <c>RegisteredHotkeyData</c>. Assumes that the key specified in
|
||||||
|
/// <paramref name="descriptionResourceKey"/> needs multiple arguments for <c>string.Format</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hotkey">
|
||||||
|
/// The hotkey this class will represent.
|
||||||
|
/// Example values: <c>F1</c>, <c>Ctrl+Shift+Enter</c>
|
||||||
|
/// </param>
|
||||||
|
/// <param name="descriptionResourceKey">
|
||||||
|
/// The key in the localization dictionary that represents this hotkey. For example, <c>ReloadPluginHotkey</c>,
|
||||||
|
/// which represents the string "Reload Plugins Data" in <c>en.xaml</c>
|
||||||
|
/// </param>
|
||||||
|
/// <param name="descriptionFormatVariables">
|
||||||
|
/// Array of values that will replace <c>{0}</c>, <c>{1}</c>, <c>{2}</c>, etc.
|
||||||
|
/// in the localized string found via <c>description</c>.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="removeHotkey">
|
||||||
|
/// An action that, when called, will unregister this hotkey. If it's <c>null</c>, it's assumed that this hotkey
|
||||||
|
/// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog.
|
||||||
|
/// </param>
|
||||||
|
public RegisteredHotkeyData(
|
||||||
|
string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables, Action? removeHotkey = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Hotkey = new HotkeyModel(hotkey);
|
||||||
|
DescriptionResourceKey = descriptionResourceKey;
|
||||||
|
DescriptionFormatVariables = descriptionFormatVariables;
|
||||||
|
RemoveHotkey = removeHotkey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,13 +4,13 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using FastCache;
|
||||||
|
using FastCache.Services;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Image
|
namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
[Serializable]
|
|
||||||
public class ImageUsage
|
public class ImageUsage
|
||||||
{
|
{
|
||||||
|
|
||||||
public int usage;
|
public int usage;
|
||||||
public ImageSource imageSource;
|
public ImageSource imageSource;
|
||||||
|
|
||||||
|
|
@ -23,16 +23,13 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
|
|
||||||
public class ImageCache
|
public class ImageCache
|
||||||
{
|
{
|
||||||
private const int MaxCached = 50;
|
private const int MaxCached = 150;
|
||||||
public ConcurrentDictionary<(string, bool), ImageUsage> Data { get; } = new();
|
|
||||||
private const int permissibleFactor = 2;
|
|
||||||
private SemaphoreSlim semaphore = new(1, 1);
|
|
||||||
|
|
||||||
public void Initialize(Dictionary<(string, bool), int> usage)
|
public void Initialize(Dictionary<(string, bool), int> usage)
|
||||||
{
|
{
|
||||||
foreach (var key in usage.Keys)
|
foreach (var key in usage.Keys)
|
||||||
{
|
{
|
||||||
Data[key] = new ImageUsage(usage[key], null);
|
Cached<ImageUsage>.Save(key, new ImageUsage(usage[key], null), TimeSpan.MaxValue, MaxCached);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,70 +37,48 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!Data.TryGetValue((path, isFullImage), out var value))
|
if (!Cached<ImageUsage>.TryGet((path, isFullImage), out var value))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
value.usage++;
|
|
||||||
return value.imageSource;
|
|
||||||
|
|
||||||
|
value.Value.usage++;
|
||||||
|
return value.Value.imageSource;
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
Data.AddOrUpdate(
|
if (Cached<ImageUsage>.TryGet((path, isFullImage), out var cached))
|
||||||
(path, isFullImage),
|
|
||||||
new ImageUsage(0, value),
|
|
||||||
(k, v) =>
|
|
||||||
{
|
|
||||||
v.imageSource = value;
|
|
||||||
v.usage++;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
SliceExtra();
|
|
||||||
|
|
||||||
async void SliceExtra()
|
|
||||||
{
|
{
|
||||||
// To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
|
cached.Value.imageSource = value;
|
||||||
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
|
cached.Value.usage++;
|
||||||
if (Data.Count > permissibleFactor * MaxCached)
|
|
||||||
{
|
|
||||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
|
||||||
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary
|
|
||||||
// Double Check to avoid concurrent remove
|
|
||||||
if (Data.Count > permissibleFactor * MaxCached)
|
|
||||||
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key))
|
|
||||||
Data.TryRemove(key, out _);
|
|
||||||
semaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cached<ImageUsage>.Save((path, isFullImage), new ImageUsage(0, value), TimeSpan.MaxValue,
|
||||||
|
MaxCached);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ContainsKey(string key, bool isFullImage)
|
public bool ContainsKey(string key, bool isFullImage)
|
||||||
{
|
{
|
||||||
return key is not null && Data.ContainsKey((key, isFullImage)) && Data[(key, isFullImage)].imageSource != null;
|
return Cached<ImageUsage>.TryGet((key, isFullImage), out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetValue(string key, bool isFullImage, out ImageSource image)
|
public bool TryGetValue(string key, bool isFullImage, out ImageSource image)
|
||||||
{
|
{
|
||||||
if (key is not null)
|
if (Cached<ImageUsage>.TryGet((key, isFullImage), out var value))
|
||||||
{
|
{
|
||||||
bool hasKey = Data.TryGetValue((key, isFullImage), out var imageUsage);
|
image = value.Value.imageSource;
|
||||||
image = hasKey ? imageUsage.imageSource : null;
|
value.Value.usage++;
|
||||||
return hasKey;
|
return image != null;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
image = null;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
image = null;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CacheSize()
|
public int CacheSize()
|
||||||
{
|
{
|
||||||
return Data.Count;
|
return CacheManager.TotalCount<(string, bool), ImageUsage>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -111,7 +86,14 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int UniqueImagesInCache()
|
public int UniqueImagesInCache()
|
||||||
{
|
{
|
||||||
return Data.Values.Select(x => x.imageSource).Distinct().Count();
|
return CacheManager.EnumerateEntries<(string, bool), ImageUsage>().Select(x => x.Value.imageSource)
|
||||||
|
.Distinct()
|
||||||
|
.Count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Cached<(string, bool), ImageUsage>> EnumerateEntries()
|
||||||
|
{
|
||||||
|
return CacheManager.EnumerateEntries<(string, bool), ImageUsage>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
|
@ -15,6 +16,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
public static class ImageLoader
|
public static class ImageLoader
|
||||||
{
|
{
|
||||||
private static readonly ImageCache ImageCache = new();
|
private static readonly ImageCache ImageCache = new();
|
||||||
|
private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
|
||||||
private static BinaryStorage<Dictionary<(string, bool), int>> _storage;
|
private static BinaryStorage<Dictionary<(string, bool), int>> _storage;
|
||||||
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
|
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
|
||||||
private static IImageHashGenerator _hashGenerator;
|
private static IImageHashGenerator _hashGenerator;
|
||||||
|
|
@ -25,24 +27,18 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
public const int FullIconSize = 256;
|
public const int FullIconSize = 256;
|
||||||
|
|
||||||
|
|
||||||
private static readonly string[] ImageExtensions =
|
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
||||||
{
|
|
||||||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"
|
|
||||||
};
|
|
||||||
|
|
||||||
public static void Initialize()
|
public static async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
_storage = new BinaryStorage<Dictionary<(string, bool), int>>("Image");
|
_storage = new BinaryStorage<Dictionary<(string, bool), int>>("Image");
|
||||||
_hashGenerator = new ImageHashGenerator();
|
_hashGenerator = new ImageHashGenerator();
|
||||||
|
|
||||||
var usage = LoadStorageToConcurrentDictionary();
|
var usage = await LoadStorageToConcurrentDictionaryAsync();
|
||||||
|
|
||||||
ImageCache.Initialize(usage.ToDictionary(x => x.Key, x => x.Value));
|
ImageCache.Initialize(usage.ToDictionary(x => x.Key, x => x.Value));
|
||||||
|
|
||||||
foreach (var icon in new[]
|
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
|
||||||
{
|
|
||||||
Constant.DefaultIcon, Constant.MissingImgIcon
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
ImageSource img = new BitmapImage(new Uri(icon));
|
ImageSource img = new BitmapImage(new Uri(icon));
|
||||||
img.Freeze();
|
img.Freeze();
|
||||||
|
|
@ -53,34 +49,46 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () =>
|
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () =>
|
||||||
{
|
{
|
||||||
foreach (var ((path, isFullImage), _) in ImageCache.Data)
|
foreach (var ((path, isFullImage), _) in usage)
|
||||||
{
|
{
|
||||||
await LoadAsync(path, isFullImage);
|
await LoadAsync(path, isFullImage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
|
Log.Info(
|
||||||
|
$"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Save()
|
public static async Task Save()
|
||||||
{
|
{
|
||||||
lock (_storage)
|
await storageLock.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_storage.Save(ImageCache.Data
|
await _storage.SaveAsync(ImageCache.EnumerateEntries()
|
||||||
.ToDictionary(
|
.ToDictionary(
|
||||||
x => x.Key,
|
x => x.Key,
|
||||||
x => x.Value.usage));
|
x => x.Value.usage));
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
storageLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary()
|
private static async Task<ConcurrentDictionary<(string, bool), int>> LoadStorageToConcurrentDictionaryAsync()
|
||||||
{
|
{
|
||||||
lock (_storage)
|
await storageLock.WaitAsync();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>());
|
var loaded = await _storage.TryLoadAsync(new Dictionary<(string, bool), int>());
|
||||||
|
|
||||||
return new ConcurrentDictionary<(string, bool), int>(loaded);
|
return new ConcurrentDictionary<(string, bool), int>(loaded);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
storageLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ImageResult
|
private class ImageResult
|
||||||
|
|
@ -117,9 +125,12 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
return new ImageResult(MissingImage, ImageType.Error);
|
return new ImageResult(MissingImage, ImageType.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImageCache.ContainsKey(path, loadFullImage))
|
// extra scope for use of same variable name
|
||||||
{
|
{
|
||||||
return new ImageResult(ImageCache[path, loadFullImage], ImageType.Cache);
|
if (ImageCache.TryGetValue(path, loadFullImage, out var imageSource))
|
||||||
|
{
|
||||||
|
return new ImageResult(imageSource, ImageType.Cache);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult)
|
if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult)
|
||||||
|
|
@ -129,6 +140,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
ImageCache[path, loadFullImage] = image;
|
ImageCache[path, loadFullImage] = image;
|
||||||
return new ImageResult(image, ImageType.ImageFile);
|
return new ImageResult(image, ImageType.ImageFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var imageSource = new BitmapImage(new Uri(path));
|
var imageSource = new BitmapImage(new Uri(path));
|
||||||
|
|
@ -158,6 +170,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
|
|
||||||
return imageResult;
|
return imageResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
||||||
{
|
{
|
||||||
// Download image from url
|
// Download image from url
|
||||||
|
|
@ -173,6 +186,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
image.DecodePixelHeight = SmallIconSize;
|
image.DecodePixelHeight = SmallIconSize;
|
||||||
image.DecodePixelWidth = SmallIconSize;
|
image.DecodePixelWidth = SmallIconSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
image.StreamSource = buffer;
|
image.StreamSource = buffer;
|
||||||
image.EndInit();
|
image.EndInit();
|
||||||
image.StreamSource = null;
|
image.StreamSource = null;
|
||||||
|
|
@ -188,8 +202,8 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
if (Directory.Exists(path))
|
if (Directory.Exists(path))
|
||||||
{
|
{
|
||||||
/* Directories can also have thumbnails instead of shell icons.
|
/* Directories can also have thumbnails instead of shell icons.
|
||||||
* Generating thumbnails for a bunch of folder results while scrolling
|
* Generating thumbnails for a bunch of folder results while scrolling
|
||||||
* could have a big impact on performance and Flow.Launcher responsibility.
|
* could have a big impact on performance and Flow.Launcher responsibility.
|
||||||
* - Solution: just load the icon
|
* - Solution: just load the icon
|
||||||
*/
|
*/
|
||||||
type = ImageType.Folder;
|
type = ImageType.Folder;
|
||||||
|
|
@ -208,9 +222,9 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
/* Although the documentation for GetImage on MSDN indicates that
|
/* Although the documentation for GetImage on MSDN indicates that
|
||||||
* if a thumbnail is available it will return one, this has proved to not
|
* if a thumbnail is available it will return one, this has proved to not
|
||||||
* be the case in many situations while testing.
|
* be the case in many situations while testing.
|
||||||
* - Solution: explicitly pass the ThumbnailOnly flag
|
* - Solution: explicitly pass the ThumbnailOnly flag
|
||||||
*/
|
*/
|
||||||
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
|
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
|
||||||
|
|
@ -236,7 +250,8 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
return new ImageResult(image, type);
|
return new ImageResult(image, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, int size = SmallIconSize)
|
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly,
|
||||||
|
int size = SmallIconSize)
|
||||||
{
|
{
|
||||||
return WindowsThumbnailProvider.GetThumbnail(
|
return WindowsThumbnailProvider.GetThumbnail(
|
||||||
path,
|
path,
|
||||||
|
|
@ -261,17 +276,19 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
|
|
||||||
var img = imageResult.ImageSource;
|
var img = imageResult.ImageSource;
|
||||||
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
|
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
|
||||||
{ // we need to get image hash
|
{
|
||||||
|
// we need to get image hash
|
||||||
string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null;
|
string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null;
|
||||||
if (hash != null)
|
if (hash != null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (GuidToKey.TryGetValue(hash, out string key))
|
if (GuidToKey.TryGetValue(hash, out string key))
|
||||||
{ // image already exists
|
{
|
||||||
|
// image already exists
|
||||||
img = ImageCache[key, loadFullImage] ?? img;
|
img = ImageCache[key, loadFullImage] ?? img;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{ // new guid
|
{
|
||||||
|
// new guid
|
||||||
|
|
||||||
GuidToKey[hash] = path;
|
GuidToKey[hash] = path;
|
||||||
}
|
}
|
||||||
|
|
@ -289,7 +306,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
BitmapImage image = new BitmapImage();
|
BitmapImage image = new BitmapImage();
|
||||||
image.BeginInit();
|
image.BeginInit();
|
||||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
image.UriSource = new Uri(path);
|
image.UriSource = new Uri(path);
|
||||||
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||||
image.EndInit();
|
image.EndInit();
|
||||||
|
|
||||||
|
|
@ -314,8 +331,10 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
resizedHeight.EndInit();
|
resizedHeight.EndInit();
|
||||||
return resizedHeight;
|
return resizedHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resizedWidth;
|
return resizedWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,67 +4,65 @@ using System.Reflection;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Runtime.Serialization.Formatters;
|
using System.Runtime.Serialization.Formatters;
|
||||||
using System.Runtime.Serialization.Formatters.Binary;
|
using System.Runtime.Serialization.Formatters.Binary;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using MemoryPack;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
#pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete.
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stroage object using binary data
|
/// Stroage object using binary data
|
||||||
/// Normally, it has better performance, but not readable
|
/// Normally, it has better performance, but not readable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable
|
||||||
|
/// https://github.com/Cysharp/MemoryPack
|
||||||
|
/// </remarks>
|
||||||
public class BinaryStorage<T>
|
public class BinaryStorage<T>
|
||||||
{
|
{
|
||||||
|
const string DirectoryName = "Cache";
|
||||||
|
|
||||||
|
const string FileSuffix = ".cache";
|
||||||
|
|
||||||
public BinaryStorage(string filename)
|
public BinaryStorage(string filename)
|
||||||
{
|
{
|
||||||
const string directoryName = "Cache";
|
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), directoryName);
|
|
||||||
Helper.ValidateDirectory(directoryPath);
|
Helper.ValidateDirectory(directoryPath);
|
||||||
|
|
||||||
const string fileSuffix = ".cache";
|
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||||
FilePath = Path.Combine(directoryPath, $"{filename}{fileSuffix}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FilePath { get; }
|
public string FilePath { get; }
|
||||||
|
|
||||||
public T TryLoad(T defaultData)
|
public async ValueTask<T> TryLoadAsync(T defaultData)
|
||||||
{
|
{
|
||||||
if (File.Exists(FilePath))
|
if (File.Exists(FilePath))
|
||||||
{
|
{
|
||||||
if (new FileInfo(FilePath).Length == 0)
|
if (new FileInfo(FilePath).Length == 0)
|
||||||
{
|
{
|
||||||
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>");
|
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>");
|
||||||
Save(defaultData);
|
await SaveAsync(defaultData);
|
||||||
return defaultData;
|
return defaultData;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var stream = new FileStream(FilePath, FileMode.Open))
|
await using var stream = new FileStream(FilePath, FileMode.Open);
|
||||||
{
|
var d = await DeserializeAsync(stream, defaultData);
|
||||||
var d = Deserialize(stream, defaultData);
|
return d;
|
||||||
return d;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data");
|
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data");
|
||||||
Save(defaultData);
|
await SaveAsync(defaultData);
|
||||||
return defaultData;
|
return defaultData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private T Deserialize(FileStream stream, T defaultData)
|
private async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
|
||||||
{
|
{
|
||||||
//http://stackoverflow.com/questions/2120055/binaryformatter-deserialize-gives-serializationexception
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
|
|
||||||
BinaryFormatter binaryFormatter = new BinaryFormatter
|
|
||||||
{
|
|
||||||
AssemblyFormat = FormatterAssemblyStyle.Simple
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var t = ((T)binaryFormatter.Deserialize(stream)).NonNull();
|
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (System.Exception e)
|
||||||
|
|
@ -72,47 +70,12 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
|
Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
|
||||||
return defaultData;
|
return defaultData;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
|
public async ValueTask SaveAsync(T data)
|
||||||
{
|
{
|
||||||
Assembly ayResult = null;
|
await using var stream = new FileStream(FilePath, FileMode.Create);
|
||||||
string sShortAssemblyName = args.Name.Split(',')[0];
|
await MemoryPackSerializer.SerializeAsync(stream, data);
|
||||||
Assembly[] ayAssemblies = AppDomain.CurrentDomain.GetAssemblies();
|
|
||||||
foreach (Assembly ayAssembly in ayAssemblies)
|
|
||||||
{
|
|
||||||
if (sShortAssemblyName == ayAssembly.FullName.Split(',')[0])
|
|
||||||
{
|
|
||||||
ayResult = ayAssembly;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ayResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save(T data)
|
|
||||||
{
|
|
||||||
using (var stream = new FileStream(FilePath, FileMode.Create))
|
|
||||||
{
|
|
||||||
BinaryFormatter binaryFormatter = new BinaryFormatter
|
|
||||||
{
|
|
||||||
AssemblyFormat = FormatterAssemblyStyle.Simple
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
binaryFormatter.Serialize(stream, data);
|
|
||||||
}
|
|
||||||
catch (SerializationException e)
|
|
||||||
{
|
|
||||||
Log.Exception($"|BinaryStorage.Save|serialize error for file <{FilePath}>", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore SYSLIB0011
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
|
|
@ -26,6 +27,85 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
|
|
||||||
protected string DirectoryPath { get; init; } = null!;
|
protected string DirectoryPath { get; init; } = null!;
|
||||||
|
|
||||||
|
// Let the derived class to set the file path
|
||||||
|
protected JsonStorage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public JsonStorage(string filePath)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
|
||||||
|
|
||||||
|
Helper.ValidateDirectory(DirectoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> LoadAsync()
|
||||||
|
{
|
||||||
|
if (Data != null)
|
||||||
|
return Data;
|
||||||
|
|
||||||
|
string? serialized = null;
|
||||||
|
|
||||||
|
if (File.Exists(FilePath))
|
||||||
|
{
|
||||||
|
serialized = await File.ReadAllTextAsync(FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(serialized))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Data = JsonSerializer.Deserialize<T>(serialized) ?? await LoadBackupOrDefaultAsync();
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
Data = await LoadBackupOrDefaultAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Data = await LoadBackupOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Data.NonNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<T> LoadBackupOrDefaultAsync()
|
||||||
|
{
|
||||||
|
var backup = await TryLoadBackupAsync();
|
||||||
|
|
||||||
|
return backup ?? LoadDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<T?> TryLoadBackupAsync()
|
||||||
|
{
|
||||||
|
if (!File.Exists(BackupFilePath))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var source = File.OpenRead(BackupFilePath);
|
||||||
|
var data = await JsonSerializer.DeserializeAsync<T>(source) ?? default;
|
||||||
|
|
||||||
|
if (data != null)
|
||||||
|
RestoreBackup();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void RestoreBackup()
|
||||||
|
{
|
||||||
|
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
|
||||||
|
|
||||||
|
if (File.Exists(FilePath))
|
||||||
|
File.Replace(BackupFilePath, FilePath, null);
|
||||||
|
else
|
||||||
|
File.Move(BackupFilePath, FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
public T Load()
|
public T Load()
|
||||||
{
|
{
|
||||||
|
|
@ -75,18 +155,9 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
|
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
|
||||||
|
|
||||||
if (data != null)
|
if (data != null)
|
||||||
{
|
RestoreBackup();
|
||||||
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
|
|
||||||
|
|
||||||
if(File.Exists(FilePath))
|
|
||||||
File.Replace(BackupFilePath, FilePath, null);
|
|
||||||
else
|
|
||||||
File.Move(BackupFilePath, FilePath);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
|
|
@ -115,6 +186,20 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
|
|
||||||
File.WriteAllText(TempFilePath, serialized);
|
File.WriteAllText(TempFilePath, serialized);
|
||||||
|
|
||||||
|
AtomicWriteSetting();
|
||||||
|
}
|
||||||
|
public async Task SaveAsync()
|
||||||
|
{
|
||||||
|
var tempOutput = File.OpenWrite(TempFilePath);
|
||||||
|
await JsonSerializer.SerializeAsync(tempOutput, Data,
|
||||||
|
new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
AtomicWriteSetting();
|
||||||
|
}
|
||||||
|
private void AtomicWriteSetting()
|
||||||
|
{
|
||||||
if (!File.Exists(FilePath))
|
if (!File.Exists(FilePath))
|
||||||
{
|
{
|
||||||
File.Move(TempFilePath, FilePath);
|
File.Move(TempFilePath, FilePath);
|
||||||
|
|
@ -124,5 +209,6 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
File.Replace(TempFilePath, FilePath, BackupFilePath);
|
File.Replace(TempFilePath, FilePath, BackupFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ using System.Collections.ObjectModel;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using Flow.Launcher.Infrastructure.Hotkey;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Flow.Launcher.Plugin.SharedModels;
|
using Flow.Launcher.Plugin.SharedModels;
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
{
|
{
|
||||||
public class Settings : BaseModel
|
public class Settings : BaseModel, IHotkeySettings
|
||||||
{
|
{
|
||||||
private string language = "en";
|
private string language = "en";
|
||||||
private string _theme = Constant.DefaultTheme;
|
private string _theme = Constant.DefaultTheme;
|
||||||
|
|
@ -20,6 +21,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public bool ShowOpenResultHotkey { get; set; } = true;
|
public bool ShowOpenResultHotkey { get; set; } = true;
|
||||||
public double WindowSize { get; set; } = 580;
|
public double WindowSize { get; set; } = 580;
|
||||||
public string PreviewHotkey { get; set; } = $"F1";
|
public string PreviewHotkey { get; set; } = $"F1";
|
||||||
|
public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab";
|
||||||
|
public string AutoCompleteHotkey2 { get; set; } = $"";
|
||||||
|
public string SelectNextItemHotkey { get; set; } = $"Tab";
|
||||||
|
public string SelectNextItemHotkey2 { get; set; } = $"";
|
||||||
|
public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab";
|
||||||
|
public string SelectPrevItemHotkey2 { get; set; } = $"";
|
||||||
|
public string SelectNextPageHotkey { get; set; } = $"PageUp";
|
||||||
|
public string SelectPrevPageHotkey { get; set; } = $"PageDown";
|
||||||
|
public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O";
|
||||||
|
public string SettingWindowHotkey { get; set; } = $"Ctrl+I";
|
||||||
|
public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up";
|
||||||
|
public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down";
|
||||||
|
|
||||||
public string Language
|
public string Language
|
||||||
{
|
{
|
||||||
|
|
@ -43,6 +56,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool UseDropShadowEffect { get; set; } = false;
|
public bool UseDropShadowEffect { get; set; } = false;
|
||||||
|
|
||||||
|
/* Appearance Settings. It should be separated from the setting later.*/
|
||||||
|
public double WindowHeightSize { get; set; } = 42;
|
||||||
|
public double ItemHeightSize { get; set; } = 58;
|
||||||
|
public double QueryBoxFontSize { get; set; } = 20;
|
||||||
|
public double ResultItemFontSize { get; set; } = 16;
|
||||||
|
public double ResultSubItemFontSize { get; set; } = 13;
|
||||||
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
||||||
public string QueryBoxFontStyle { get; set; }
|
public string QueryBoxFontStyle { get; set; }
|
||||||
public string QueryBoxFontWeight { get; set; }
|
public string QueryBoxFontWeight { get; set; }
|
||||||
|
|
@ -51,9 +71,15 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public string ResultFontStyle { get; set; }
|
public string ResultFontStyle { get; set; }
|
||||||
public string ResultFontWeight { get; set; }
|
public string ResultFontWeight { get; set; }
|
||||||
public string ResultFontStretch { get; set; }
|
public string ResultFontStretch { get; set; }
|
||||||
|
public string ResultSubFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
||||||
|
public string ResultSubFontStyle { get; set; }
|
||||||
|
public string ResultSubFontWeight { get; set; }
|
||||||
|
public string ResultSubFontStretch { get; set; }
|
||||||
public bool UseGlyphIcons { get; set; } = true;
|
public bool UseGlyphIcons { get; set; } = true;
|
||||||
public bool UseAnimation { get; set; } = true;
|
public bool UseAnimation { get; set; } = true;
|
||||||
public bool UseSound { get; set; } = true;
|
public bool UseSound { get; set; } = true;
|
||||||
|
public double SoundVolume { get; set; } = 50;
|
||||||
|
|
||||||
public bool UseClock { get; set; } = true;
|
public bool UseClock { get; set; } = true;
|
||||||
public bool UseDate { get; set; } = false;
|
public bool UseDate { get; set; } = false;
|
||||||
public string TimeFormat { get; set; } = "hh:mm tt";
|
public string TimeFormat { get; set; } = "hh:mm tt";
|
||||||
|
|
@ -166,32 +192,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
|
|
||||||
public bool AlwaysStartEn { get; set; } = false;
|
public bool AlwaysStartEn { get; set; } = false;
|
||||||
|
|
||||||
|
private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular;
|
||||||
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
|
public SearchPrecisionScore QuerySearchPrecision
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public string QuerySearchPrecisionString
|
|
||||||
{
|
{
|
||||||
get { return QuerySearchPrecision.ToString(); }
|
get => _querySearchPrecision;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
try
|
_querySearchPrecision = value;
|
||||||
{
|
if (StringMatcher.Instance != null)
|
||||||
var precisionScore = (SearchPrecisionScore)Enum
|
StringMatcher.Instance.UserSettingSearchPrecision = value;
|
||||||
.Parse(typeof(SearchPrecisionScore), value);
|
|
||||||
|
|
||||||
QuerySearchPrecision = precisionScore;
|
|
||||||
StringMatcher.Instance.UserSettingSearchPrecision = precisionScore;
|
|
||||||
}
|
|
||||||
catch (ArgumentException e)
|
|
||||||
{
|
|
||||||
Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e);
|
|
||||||
|
|
||||||
QuerySearchPrecision = SearchPrecisionScore.Regular;
|
|
||||||
StringMatcher.Instance.UserSettingSearchPrecision = SearchPrecisionScore.Regular;
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,17 +209,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
|
|
||||||
public double WindowLeft { get; set; }
|
public double WindowLeft { get; set; }
|
||||||
public double WindowTop { get; set; }
|
public double WindowTop { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom left position on selected monitor
|
/// Custom left position on selected monitor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double CustomWindowLeft { get; set; } = 0;
|
public double CustomWindowLeft { get; set; } = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom top position on selected monitor
|
/// Custom top position on selected monitor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double CustomWindowTop { get; set; } = 0;
|
public double CustomWindowTop { get; set; } = 0;
|
||||||
|
|
||||||
|
public bool KeepMaxResults { get; set; } = false;
|
||||||
public int MaxResultsToShow { get; set; } = 5;
|
public int MaxResultsToShow { get; set; } = 5;
|
||||||
public int ActivateTimes { get; set; }
|
public int ActivateTimes { get; set; }
|
||||||
|
|
||||||
|
|
@ -221,7 +232,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
|
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
|
||||||
{
|
{
|
||||||
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
|
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
|
||||||
new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
|
new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -245,7 +256,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
|
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public SearchWindowAligns SearchWindowAlign { get; set; } = SearchWindowAligns.Center;
|
public SearchWindowAligns SearchWindowAlign { get; set; } = SearchWindowAligns.Center;
|
||||||
|
|
||||||
|
|
@ -262,9 +273,99 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
|
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
|
||||||
public int CustomAnimationLength { get; set; } = 360;
|
public int CustomAnimationLength { get; set; } = 360;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool WMPInstalled { get; set; } = true;
|
||||||
|
|
||||||
|
|
||||||
// This needs to be loaded last by staying at the bottom
|
// This needs to be loaded last by staying at the bottom
|
||||||
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
|
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<RegisteredHotkeyData> RegisteredHotkeys
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var list = FixedHotkeys();
|
||||||
|
|
||||||
|
// Customizeable hotkeys
|
||||||
|
if(!string.IsNullOrEmpty(Hotkey))
|
||||||
|
list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = ""));
|
||||||
|
if(!string.IsNullOrEmpty(PreviewHotkey))
|
||||||
|
list.Add(new(PreviewHotkey, "previewHotkey", () => PreviewHotkey = ""));
|
||||||
|
if(!string.IsNullOrEmpty(AutoCompleteHotkey))
|
||||||
|
list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = ""));
|
||||||
|
if(!string.IsNullOrEmpty(AutoCompleteHotkey2))
|
||||||
|
list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = ""));
|
||||||
|
if(!string.IsNullOrEmpty(SelectNextItemHotkey))
|
||||||
|
list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = ""));
|
||||||
|
if(!string.IsNullOrEmpty(SelectNextItemHotkey2))
|
||||||
|
list.Add(new(SelectNextItemHotkey2, "SelectNextItemHotkey", () => SelectNextItemHotkey2 = ""));
|
||||||
|
if(!string.IsNullOrEmpty(SelectPrevItemHotkey))
|
||||||
|
list.Add(new(SelectPrevItemHotkey, "SelectPrevItemHotkey", () => SelectPrevItemHotkey = ""));
|
||||||
|
if(!string.IsNullOrEmpty(SelectPrevItemHotkey2))
|
||||||
|
list.Add(new(SelectPrevItemHotkey2, "SelectPrevItemHotkey", () => SelectPrevItemHotkey2 = ""));
|
||||||
|
if(!string.IsNullOrEmpty(SettingWindowHotkey))
|
||||||
|
list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = ""));
|
||||||
|
if(!string.IsNullOrEmpty(OpenContextMenuHotkey))
|
||||||
|
list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = ""));
|
||||||
|
if(!string.IsNullOrEmpty(SelectNextPageHotkey))
|
||||||
|
list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = ""));
|
||||||
|
if(!string.IsNullOrEmpty(SelectPrevPageHotkey))
|
||||||
|
list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = ""));
|
||||||
|
if (!string.IsNullOrEmpty(CycleHistoryUpHotkey))
|
||||||
|
list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = ""));
|
||||||
|
if (!string.IsNullOrEmpty(CycleHistoryDownHotkey))
|
||||||
|
list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = ""));
|
||||||
|
|
||||||
|
// Custom Query Hotkeys
|
||||||
|
foreach (var customPluginHotkey in CustomPluginHotkeys)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey))
|
||||||
|
list.Add(new(customPluginHotkey.Hotkey, "customQueryHotkey", () => customPluginHotkey.Hotkey = ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RegisteredHotkeyData> FixedHotkeys()
|
||||||
|
{
|
||||||
|
return new List<RegisteredHotkeyData>
|
||||||
|
{
|
||||||
|
new("Up", "HotkeyLeftRightDesc"),
|
||||||
|
new("Down", "HotkeyLeftRightDesc"),
|
||||||
|
new("Left", "HotkeyUpDownDesc"),
|
||||||
|
new("Right", "HotkeyUpDownDesc"),
|
||||||
|
new("Escape", "HotkeyESCDesc"),
|
||||||
|
new("F5", "ReloadPluginHotkey"),
|
||||||
|
new("Alt+Home", "HotkeySelectFirstResult"),
|
||||||
|
new("Alt+End", "HotkeySelectLastResult"),
|
||||||
|
new("Ctrl+R", "HotkeyRequery"),
|
||||||
|
new("Ctrl+H", "ToggleHistoryHotkey"),
|
||||||
|
new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"),
|
||||||
|
new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"),
|
||||||
|
new("Ctrl+OemPlus", "QuickHeightHotkey"),
|
||||||
|
new("Ctrl+OemMinus", "QuickHeightHotkey"),
|
||||||
|
new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"),
|
||||||
|
new("Shift+Enter", "OpenContextMenuHotkey"),
|
||||||
|
new("Enter", "HotkeyRunDesc"),
|
||||||
|
new("Ctrl+Enter", "OpenContainFolderHotkey"),
|
||||||
|
new("Alt+Enter", "HotkeyOpenResult"),
|
||||||
|
new("Ctrl+F12", "ToggleGameModeHotkey"),
|
||||||
|
new("Ctrl+Shift+C", "CopyFilePathHotkey"),
|
||||||
|
|
||||||
|
new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1),
|
||||||
|
new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2),
|
||||||
|
new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3),
|
||||||
|
new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4),
|
||||||
|
new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5),
|
||||||
|
new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6),
|
||||||
|
new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7),
|
||||||
|
new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8),
|
||||||
|
new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9),
|
||||||
|
new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum LastQueryMode
|
public enum LastQueryMode
|
||||||
|
|
@ -280,7 +381,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
Light,
|
Light,
|
||||||
Dark
|
Dark
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SearchWindowScreens
|
public enum SearchWindowScreens
|
||||||
{
|
{
|
||||||
RememberLastLaunchLocation,
|
RememberLastLaunchLocation,
|
||||||
|
|
@ -289,7 +390,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
Primary,
|
Primary,
|
||||||
Custom
|
Custom
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SearchWindowAligns
|
public enum SearchWindowAligns
|
||||||
{
|
{
|
||||||
Center,
|
Center,
|
||||||
|
|
|
||||||
|
|
@ -50,5 +50,12 @@ namespace Flow.Launcher.Plugin
|
||||||
(AltPressed ? ModifierKeys.Alt : ModifierKeys.None) |
|
(AltPressed ? ModifierKeys.Alt : ModifierKeys.None) |
|
||||||
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
|
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static readonly SpecialKeyState Default = new () {
|
||||||
|
CtrlPressed = false,
|
||||||
|
ShiftPressed = false,
|
||||||
|
AltPressed = false,
|
||||||
|
WinPressed = false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,11 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Python = "Python";
|
public const string Python = "Python";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python V2
|
||||||
|
/// </summary>
|
||||||
|
public const string PythonV2 = "Python_v2";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// C#
|
/// C#
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -27,16 +32,31 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Executable = "Executable";
|
public const string Executable = "Executable";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standard .exe
|
||||||
|
/// </summary>
|
||||||
|
public const string ExecutableV2 = "Executable_V2";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TypeScript
|
/// TypeScript
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string TypeScript = "TypeScript";
|
public const string TypeScript = "TypeScript";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TypeScript
|
||||||
|
/// </summary>
|
||||||
|
public const string TypeScriptV2 = "TypeScript_V2";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// JavaScript
|
/// JavaScript
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string JavaScript = "JavaScript";
|
public const string JavaScript = "JavaScript";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JavaScript
|
||||||
|
/// </summary>
|
||||||
|
public const string JavaScriptV2 = "JavaScript_V2";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if this language is a .NET language
|
/// Determines if this language is a .NET language
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -45,7 +65,7 @@ namespace Flow.Launcher.Plugin
|
||||||
public static bool IsDotNet(string language)
|
public static bool IsDotNet(string language)
|
||||||
{
|
{
|
||||||
return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase)
|
return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase)
|
||||||
|| language.Equals(FSharp, StringComparison.OrdinalIgnoreCase);
|
|| language.Equals(FSharp, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -56,10 +76,15 @@ namespace Flow.Launcher.Plugin
|
||||||
public static bool IsAllowed(string language)
|
public static bool IsAllowed(string language)
|
||||||
{
|
{
|
||||||
return IsDotNet(language)
|
return IsDotNet(language)
|
||||||
|| language.Equals(Python, StringComparison.OrdinalIgnoreCase)
|
|| language.Equals(Python, StringComparison.OrdinalIgnoreCase)
|
||||||
|| language.Equals(Executable, StringComparison.OrdinalIgnoreCase)
|
|| language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase)
|
||||||
|| language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase)
|
|| language.Equals(Executable, StringComparison.OrdinalIgnoreCase)
|
||||||
|| language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase);
|
|| language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| language.Equals(TypeScriptV2, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| language.Equals(JavaScriptV2, StringComparison.OrdinalIgnoreCase);
|
||||||
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>4.1.1</Version>
|
<Version>4.3.0</Version>
|
||||||
<PackageVersion>4.1.1</PackageVersion>
|
<PackageVersion>4.3.0</PackageVersion>
|
||||||
<AssemblyVersion>4.1.1</AssemblyVersion>
|
<AssemblyVersion>4.3.0</AssemblyVersion>
|
||||||
<FileVersion>4.1.1</FileVersion>
|
<FileVersion>4.3.0</FileVersion>
|
||||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||||
<Authors>Flow-Launcher</Authors>
|
<Authors>Flow-Launcher</Authors>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0" />
|
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -274,5 +274,29 @@ namespace Flow.Launcher.Plugin
|
||||||
/// Non-C# plugins should use this method
|
/// Non-C# plugins should use this method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void OpenAppUri(string appUri);
|
public void OpenAppUri(string appUri);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggles Game Mode. off -> on and backwards
|
||||||
|
/// </summary>
|
||||||
|
public void ToggleGameMode();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switches Game Mode to given value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">New Game Mode status</param>
|
||||||
|
public void SetGameMode(bool value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representing Game Mode status
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool IsGameModeOn();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reloads the query.
|
||||||
|
/// This method should run
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reselect">Choose the first result after reload if true; keep the last selected result if false. Default is true.</param>
|
||||||
|
public void ReQuery(bool reselect = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -64,6 +67,7 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ActionKeyword { get; init; }
|
public string ActionKeyword { get; init; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
|
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -71,12 +75,14 @@ namespace Flow.Launcher.Plugin
|
||||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string FirstSearch => SplitSearch(0);
|
public string FirstSearch => SplitSearch(0);
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
private string _secondToEndSearch;
|
private string _secondToEndSearch;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// strings from second search (including) to last search
|
/// strings from second search (including) to last search
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public string SecondToEndSearch => SearchTerms.Length > 1 ? (_secondToEndSearch ??= string.Join(' ', SearchTerms[1..])) : "";
|
public string SecondToEndSearch => SearchTerms.Length > 1 ? (_secondToEndSearch ??= string.Join(' ', SearchTerms[1..])) : "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -85,6 +91,7 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
public string SecondSearch => SplitSearch(1);
|
public string SecondSearch => SplitSearch(1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -93,6 +100,7 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
public string ThirdSearch => SplitSearch(2);
|
public string ThirdSearch => SplitSearch(2);
|
||||||
|
|
||||||
private string SplitSearch(int index)
|
private string SplitSearch(int index)
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,31 @@ namespace Flow.Launcher.Plugin
|
||||||
return Title + SubTitle + Score;
|
return Title + SubTitle + Score;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clones the current result
|
||||||
|
/// </summary>
|
||||||
|
public Result Clone()
|
||||||
|
{
|
||||||
|
return new Result
|
||||||
|
{
|
||||||
|
Title = Title,
|
||||||
|
SubTitle = SubTitle,
|
||||||
|
ActionKeywordAssigned = ActionKeywordAssigned,
|
||||||
|
CopyText = CopyText,
|
||||||
|
AutoCompleteText = AutoCompleteText,
|
||||||
|
IcoPath = IcoPath,
|
||||||
|
RoundedIcon = RoundedIcon,
|
||||||
|
Icon = Icon,
|
||||||
|
Glyph = Glyph,
|
||||||
|
Action = Action,
|
||||||
|
AsyncAction = AsyncAction,
|
||||||
|
Score = Score,
|
||||||
|
TitleHighlightData = TitleHighlightData,
|
||||||
|
OriginQuery = OriginQuery,
|
||||||
|
PluginDirectory = PluginDirectory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Additional data associated with this result
|
/// Additional data associated with this result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -233,7 +258,7 @@ namespace Flow.Launcher.Plugin
|
||||||
public string ProgressBarColor { get; set; } = "#26a0da";
|
public string ProgressBarColor { get; set; } = "#26a0da";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains data used to populate the the preview section of this result.
|
/// Contains data used to populate the preview section of this result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PreviewInfo Preview { get; set; } = PreviewInfo.Default;
|
public PreviewInfo Preview { get; set; } = PreviewInfo.Default;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
#pragma warning disable IDE0005
|
#pragma warning disable IDE0005
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
#pragma warning restore IDE0005
|
#pragma warning restore IDE0005
|
||||||
|
|
@ -200,6 +201,24 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///<summary>
|
||||||
|
/// This checks whether a given string is a zip file path.
|
||||||
|
/// By default does not check if the zip file actually exist on disk, can do so by
|
||||||
|
/// setting checkFileExists = true.
|
||||||
|
///</summary>
|
||||||
|
public static bool IsZipFilePath(string querySearchString, bool checkFileExists = false)
|
||||||
|
{
|
||||||
|
if (IsLocationPathString(querySearchString) && querySearchString.Split('.').Last() == "zip")
|
||||||
|
{
|
||||||
|
if (checkFileExists)
|
||||||
|
return FileExists(querySearchString);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
///<summary>
|
///<summary>
|
||||||
/// This checks whether a given string is a directory path or network location string.
|
/// This checks whether a given string is a directory path or network location string.
|
||||||
/// It does not check if location actually exists.
|
/// It does not check if location actually exists.
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,12 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="nunit" Version="3.13.3" />
|
<PackageReference Include="nunit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -191,11 +191,15 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
[TestCase(@"\c:\", false)]
|
[TestCase(@"\c:\", false)]
|
||||||
[TestCase(@"cc:\", false)]
|
[TestCase(@"cc:\", false)]
|
||||||
[TestCase(@"\\\SomeNetworkLocation\", false)]
|
[TestCase(@"\\\SomeNetworkLocation\", false)]
|
||||||
|
[TestCase(@"\\SomeNetworkLocation\", true)]
|
||||||
[TestCase("RandomFile", false)]
|
[TestCase("RandomFile", false)]
|
||||||
[TestCase(@"c:\>*", true)]
|
[TestCase(@"c:\>*", true)]
|
||||||
[TestCase(@"c:\>", true)]
|
[TestCase(@"c:\>", true)]
|
||||||
[TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)]
|
[TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)]
|
||||||
[TestCase(@"c:\SomeLocation\SomeOtherLocation", true)]
|
[TestCase(@"c:\SomeLocation\SomeOtherLocation", true)]
|
||||||
|
[TestCase(@"c:\SomeLocation\SomeOtherLocation\SomeFile.exe", true)]
|
||||||
|
[TestCase(@"\\SomeNetworkLocation\SomeFile.exe", true)]
|
||||||
|
|
||||||
public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
|
public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
|
||||||
{
|
{
|
||||||
// When, Given
|
// When, Given
|
||||||
|
|
|
||||||
|
|
@ -53,21 +53,14 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
|
|
||||||
public static List<JsonRPCQueryResponseModel> ResponseModelsSource = new()
|
public static List<JsonRPCQueryResponseModel> ResponseModelsSource = new()
|
||||||
{
|
{
|
||||||
new()
|
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>()),
|
||||||
|
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>
|
||||||
{
|
{
|
||||||
Result = new()
|
new JsonRPCResult
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Result = new()
|
|
||||||
{
|
{
|
||||||
new JsonRPCResult
|
Title = "Test1", SubTitle = "Test2"
|
||||||
{
|
|
||||||
Title = "Test1",
|
|
||||||
SubTitle = "Test2"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
|
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
|
||||||
|
|
@ -94,4 +87,4 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||||
Scripts\post_build.ps1 = Scripts\post_build.ps1
|
Scripts\post_build.ps1 = Scripts\post_build.ps1
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs
|
SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs
|
||||||
|
Settings.XamlStyler = Settings.XamlStyler
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Shell", "Plugins\Flow.Launcher.Plugin.Shell\Flow.Launcher.Plugin.Shell.csproj", "{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Shell", "Plugins\Flow.Launcher.Plugin.Shell\Flow.Launcher.Plugin.Shell.csproj", "{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}"
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
</ui:ThemeResources>
|
</ui:ThemeResources>
|
||||||
<ui:XamlControlsResources />
|
<ui:XamlControlsResources />
|
||||||
<ResourceDictionary Source="pack://application:,,,/Resources/CustomControlTemplate.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/Resources/CustomControlTemplate.xaml" />
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/Resources/SettingWindowStyle.xaml" />
|
||||||
<ResourceDictionary Source="pack://application:,,,/Themes/Win11System.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/Themes/Win11System.xaml" />
|
||||||
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,17 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
_portable.PreStartCleanUpAfterPortabilityUpdate();
|
_portable.PreStartCleanUpAfterPortabilityUpdate();
|
||||||
|
|
||||||
Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
Log.Info(
|
||||||
|
"|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
||||||
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
|
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
|
||||||
RegisterAppDomainExceptions();
|
RegisterAppDomainExceptions();
|
||||||
RegisterDispatcherUnhandledException();
|
RegisterDispatcherUnhandledException();
|
||||||
|
|
||||||
ImageLoader.Initialize();
|
var imageLoadertask = ImageLoader.InitializeAsync();
|
||||||
|
|
||||||
_settingsVM = new SettingWindowViewModel(_updater, _portable);
|
_settingsVM = new SettingWindowViewModel(_updater, _portable);
|
||||||
_settings = _settingsVM.Settings;
|
_settings = _settingsVM.Settings;
|
||||||
|
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
|
||||||
|
|
||||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||||
|
|
||||||
|
|
@ -78,6 +80,8 @@ namespace Flow.Launcher
|
||||||
Http.Proxy = _settings.Proxy;
|
Http.Proxy = _settings.Proxy;
|
||||||
|
|
||||||
await PluginManager.InitializePluginsAsync(API);
|
await PluginManager.InitializePluginsAsync(API);
|
||||||
|
await imageLoadertask;
|
||||||
|
|
||||||
var window = new MainWindow(_settings, _mainVM);
|
var window = new MainWindow(_settings, _mainVM);
|
||||||
|
|
||||||
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
||||||
|
|
@ -85,12 +89,13 @@ namespace Flow.Launcher
|
||||||
Current.MainWindow = window;
|
Current.MainWindow = window;
|
||||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||||
|
|
||||||
HotKeyMapper.Initialize(_mainVM);
|
|
||||||
|
|
||||||
// todo temp fix for instance code logic
|
// todo temp fix for instance code logic
|
||||||
// load plugin before change language, because plugin language also needs be changed
|
// load plugin before change language, because plugin language also needs be changed
|
||||||
InternationalizationManager.Instance.Settings = _settings;
|
InternationalizationManager.Instance.Settings = _settings;
|
||||||
InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
|
InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
|
||||||
|
|
||||||
|
HotKeyMapper.Initialize(_mainVM);
|
||||||
|
|
||||||
// main windows needs initialized before theme change because of blur settings
|
// main windows needs initialized before theme change because of blur settings
|
||||||
ThemeManager.Instance.Settings = _settings;
|
ThemeManager.Instance.Settings = _settings;
|
||||||
ThemeManager.Instance.ChangeTheme(_settings.Theme);
|
ThemeManager.Instance.ChangeTheme(_settings.Theme);
|
||||||
|
|
@ -103,7 +108,8 @@ namespace Flow.Launcher
|
||||||
AutoUpdates();
|
AutoUpdates();
|
||||||
|
|
||||||
API.SaveAppAllSettings();
|
API.SaveAppAllSettings();
|
||||||
Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
Log.Info(
|
||||||
|
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,7 +128,8 @@ namespace Flow.Launcher
|
||||||
// but if it fails (permissions, etc) then don't keep retrying
|
// but if it fails (permissions, etc) then don't keep retrying
|
||||||
// this also gives the user a visual indication in the Settings widget
|
// this also gives the user a visual indication in the Settings widget
|
||||||
_settings.StartFlowLauncherOnSystemStartup = false;
|
_settings.StartFlowLauncherOnSystemStartup = false;
|
||||||
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message);
|
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
|
||||||
|
e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,53 +3,38 @@ using System.Globalization;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
{
|
|
||||||
internal class BoolToIMEConversionModeConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
if (value is bool v)
|
|
||||||
{
|
|
||||||
if (v)
|
|
||||||
{
|
|
||||||
return ImeConversionModeValues.Alphanumeric;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ImeConversionModeValues.DoNotCare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ImeConversionModeValues.DoNotCare;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
internal class BoolToIMEConversionModeConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
true => ImeConversionModeValues.Alphanumeric,
|
||||||
}
|
_ => ImeConversionModeValues.DoNotCare
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class BoolToIMEStateConverter : IValueConverter
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
throw new NotImplementedException();
|
||||||
{
|
}
|
||||||
if (value is bool v)
|
}
|
||||||
{
|
|
||||||
if (v)
|
internal class BoolToIMEStateConverter : IValueConverter
|
||||||
{
|
{
|
||||||
return InputMethodState.Off;
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
}
|
{
|
||||||
else
|
return value switch
|
||||||
{
|
{
|
||||||
return InputMethodState.DoNotCare;
|
true => InputMethodState.Off,
|
||||||
}
|
_ => InputMethodState.DoNotCare
|
||||||
}
|
};
|
||||||
return InputMethodState.DoNotCare;
|
}
|
||||||
}
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
{
|
||||||
{
|
throw new NotImplementedException();
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,70 +2,39 @@
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
|
|
||||||
|
public class BoolToVisibilityConverter : IValueConverter
|
||||||
{
|
{
|
||||||
public class BoolToVisibilityConverter : IValueConverter
|
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
return (value, parameter) switch
|
||||||
{
|
{
|
||||||
if (parameter != null)
|
(true, not null) => Visibility.Collapsed,
|
||||||
{
|
(_, not null) => Visibility.Visible,
|
||||||
if (value is true)
|
|
||||||
{
|
|
||||||
return Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
(true, null) => Visibility.Visible,
|
||||||
{
|
(_, null) => Visibility.Collapsed
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SplitterConverter : IValueConverter
|
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||||
/* Prevents the dragging part of the preview area from working when preview is turned off. */
|
}
|
||||||
{
|
|
||||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
public class SplitterConverter : IValueConverter
|
||||||
{
|
/* Prevents the dragging part of the preview area from working when preview is turned off. */
|
||||||
if (parameter != null)
|
{
|
||||||
{
|
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
||||||
if (value is true)
|
{
|
||||||
{
|
return (value, parameter) switch
|
||||||
return 0;
|
{
|
||||||
}
|
(true, not null) => 0,
|
||||||
|
(_, not null) => 5,
|
||||||
else
|
|
||||||
{
|
(true, null) => 5,
|
||||||
return 5;
|
(_, null) => 0
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||||
if (value is true)
|
|
||||||
{
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,46 +7,41 @@ using System.Windows.Shapes;
|
||||||
|
|
||||||
// For Clipping inside listbox item
|
// For Clipping inside listbox item
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
|
|
||||||
|
public class BorderClipConverter : IMultiValueConverter
|
||||||
{
|
{
|
||||||
public class BorderClipConverter : IMultiValueConverter
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
if (values is not [double width, double height, CornerRadius radius])
|
||||||
{
|
{
|
||||||
if (values.Length == 3 && values[0] is double && values[1] is double && values[2] is CornerRadius)
|
|
||||||
{
|
|
||||||
var width = (double)values[0];
|
|
||||||
var height = (double)values[1];
|
|
||||||
Path myPath = new Path();
|
|
||||||
if (width < Double.Epsilon || height < Double.Epsilon)
|
|
||||||
{
|
|
||||||
return Geometry.Empty;
|
|
||||||
}
|
|
||||||
var radius = (CornerRadius)values[2];
|
|
||||||
var radiusHeight = radius.TopLeft;
|
|
||||||
|
|
||||||
// Drawing Round box for bottom round, and rect for top area of listbox.
|
|
||||||
var corner = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft);
|
|
||||||
var box = new RectangleGeometry(new Rect(0, 0, width, radiusHeight), 0, 0);
|
|
||||||
|
|
||||||
GeometryGroup myGeometryGroup = new GeometryGroup();
|
|
||||||
myGeometryGroup.Children.Add(corner);
|
|
||||||
myGeometryGroup.Children.Add(box);
|
|
||||||
|
|
||||||
CombinedGeometry c1 = new CombinedGeometry(GeometryCombineMode.Union, corner, box);
|
|
||||||
myPath.Data = c1;
|
|
||||||
|
|
||||||
myPath.Data.Freeze();
|
|
||||||
return myPath.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DependencyProperty.UnsetValue;
|
return DependencyProperty.UnsetValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
Path myPath = new Path();
|
||||||
|
if (width < Double.Epsilon || height < Double.Epsilon)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException();
|
return Geometry.Empty;
|
||||||
}
|
}
|
||||||
|
var radiusHeight = radius.TopLeft;
|
||||||
|
|
||||||
|
// Drawing Round box for bottom round, and rect for top area of listbox.
|
||||||
|
var corner = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft);
|
||||||
|
var box = new RectangleGeometry(new Rect(0, 0, width, radiusHeight), 0, 0);
|
||||||
|
|
||||||
|
GeometryGroup myGeometryGroup = new GeometryGroup();
|
||||||
|
myGeometryGroup.Children.Add(corner);
|
||||||
|
myGeometryGroup.Children.Add(box);
|
||||||
|
|
||||||
|
CombinedGeometry c1 = new CombinedGeometry(GeometryCombineMode.Union, corner, box);
|
||||||
|
myPath.Data = c1;
|
||||||
|
|
||||||
|
myPath.Data.Freeze();
|
||||||
|
return myPath.Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,17 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
{
|
|
||||||
public class DateTimeFormatToNowConverter : IValueConverter
|
|
||||||
{
|
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
public class DateTimeFormatToNowConverter : IValueConverter
|
||||||
{
|
{
|
||||||
return value is not string format ? null : DateTime.Now.ToString(format);
|
|
||||||
}
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
{
|
||||||
{
|
return value is not string format ? null : DateTime.Now.ToString(format);
|
||||||
throw new NotImplementedException();
|
}
|
||||||
}
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,22 @@ using System.Globalization;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
|
|
||||||
|
public class DiameterToCenterPointConverter : IValueConverter
|
||||||
{
|
{
|
||||||
public class DiameterToCenterPointConverter : IValueConverter
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
if (value is double d)
|
||||||
{
|
{
|
||||||
if (value is double d)
|
return new Point(d / 2, d / 2);
|
||||||
{
|
|
||||||
return new Point(d / 2, d / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Point(0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
return new Point(0, 0);
|
||||||
{
|
}
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,48 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
using System.Windows.Documents;
|
using System.Windows.Documents;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
|
|
||||||
|
public class HighlightTextConverter : IMultiValueConverter
|
||||||
{
|
{
|
||||||
public class HighlightTextConverter : IMultiValueConverter
|
public object Convert(object[] value, Type targetType, object parameter, CultureInfo cultureInfo)
|
||||||
{
|
{
|
||||||
public object Convert(object[] value, Type targetType, object parameter, CultureInfo cultureInfo)
|
if (value.Length < 2)
|
||||||
|
return new Run(string.Empty);
|
||||||
|
|
||||||
|
if (value[0] is not string text)
|
||||||
|
return new Run(string.Empty);
|
||||||
|
|
||||||
|
if (value[1] is not List<int> { Count: > 0 } highlightData)
|
||||||
|
// No highlight data, just return the text
|
||||||
|
return new Run(text);
|
||||||
|
|
||||||
|
var highlightStyle = (Style)Application.Current.FindResource("HighlightStyle");
|
||||||
|
var textBlock = new Span();
|
||||||
|
|
||||||
|
for (var i = 0; i < text.Length; i++)
|
||||||
{
|
{
|
||||||
var text = value[0] as string;
|
var currentCharacter = text.Substring(i, 1);
|
||||||
var highlightData = value[1] as List<int>;
|
var run = new Run(currentCharacter)
|
||||||
|
|
||||||
var textBlock = new Span();
|
|
||||||
|
|
||||||
if (highlightData == null || !highlightData.Any())
|
|
||||||
{
|
{
|
||||||
// No highlight data, just return the text
|
Style = ShouldHighlight(highlightData, i) ? highlightStyle : null
|
||||||
return new Run(text);
|
};
|
||||||
}
|
textBlock.Inlines.Add(run);
|
||||||
|
|
||||||
for (var i = 0; i < text.Length; i++)
|
|
||||||
{
|
|
||||||
var currentCharacter = text.Substring(i, 1);
|
|
||||||
if (this.ShouldHighlight(highlightData, i))
|
|
||||||
{
|
|
||||||
|
|
||||||
textBlock.Inlines.Add(new Run(currentCharacter) { Style = (Style)Application.Current.FindResource("HighlightStyle") });
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
textBlock.Inlines.Add(new Run(currentCharacter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return textBlock;
|
|
||||||
}
|
}
|
||||||
|
return textBlock;
|
||||||
|
}
|
||||||
|
|
||||||
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
|
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
return new[] { DependencyProperty.UnsetValue, DependencyProperty.UnsetValue };
|
return new[] { DependencyProperty.UnsetValue, DependencyProperty.UnsetValue };
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldHighlight(List<int> highlightData, int index)
|
private bool ShouldHighlight(List<int> highlightData, int index)
|
||||||
{
|
{
|
||||||
return highlightData.Contains(index);
|
return highlightData.Contains(index);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,19 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
{
|
|
||||||
public class IconRadiusConverter : IMultiValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
if (values.Length != 2)
|
|
||||||
throw new ArgumentException("IconRadiusConverter must have 2 parameters");
|
|
||||||
|
|
||||||
return values[1] switch
|
public class IconRadiusConverter : IMultiValueConverter
|
||||||
{
|
{
|
||||||
true => (double)values[0] / 2,
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||||
false => (double)values[0],
|
{
|
||||||
_ => throw new ArgumentException("The second argument should be boolean", nameof(values))
|
if (values is not [double size, bool isIconCircular])
|
||||||
};
|
throw new ArgumentException("IconRadiusConverter must have 2 parameters: [double, bool]");
|
||||||
}
|
|
||||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
return isIconCircular ? size / 2 : size;
|
||||||
{
|
}
|
||||||
throw new NotSupportedException();
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||||
}
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,23 @@ using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
|
|
||||||
|
[ValueConversion(typeof(bool), typeof(Visibility))]
|
||||||
|
public class OpenResultHotkeyVisibilityConverter : IValueConverter
|
||||||
{
|
{
|
||||||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
private const int MaxVisibleHotkeys = 10;
|
||||||
public class OpenResultHotkeyVisibilityConverter : IValueConverter
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
private const int MaxVisibleHotkeys = 10;
|
var number = int.MaxValue;
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
if (value is ListBoxItem listBoxItem
|
||||||
{
|
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
|
||||||
var number = int.MaxValue;
|
number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||||
|
|
||||||
if (value is ListBoxItem listBoxItem
|
return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
|
||||||
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
|
|
||||||
number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
|
||||||
|
|
||||||
return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
{
|
|
||||||
public class OrdinalConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
if (value is ListBoxItem listBoxItem
|
|
||||||
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
|
|
||||||
{
|
|
||||||
var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
|
||||||
return res == 10 ? 0 : res; // 10th item => HOTKEY+0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public class OrdinalConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is not ListBoxItem listBoxItem
|
||||||
|
|| ItemsControl.ItemsControlFromItemContainer(listBoxItem) is not ListBox listBox)
|
||||||
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
|
||||||
|
return res == 10 ? 0 : res; // 10th item => HOTKEY+0
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,74 +6,62 @@ using System.Windows.Media;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
|
|
||||||
|
public class QuerySuggestionBoxConverter : IMultiValueConverter
|
||||||
{
|
{
|
||||||
public class QuerySuggestionBoxConverter : IMultiValueConverter
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
// values[0] is TextBox: The textbox displaying the autocomplete suggestion
|
||||||
|
// values[1] is ResultViewModel: Currently selected item in the list
|
||||||
|
// values[2] is string: Query text
|
||||||
|
if (
|
||||||
|
values.Length != 3 ||
|
||||||
|
values[0] is not TextBox queryTextBox ||
|
||||||
|
values[1] is null ||
|
||||||
|
values[2] is not string queryText ||
|
||||||
|
string.IsNullOrEmpty(queryText)
|
||||||
|
)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (values[1] is not ResultViewModel selectedItem)
|
||||||
|
return Binding.DoNothing;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (values.Length != 3)
|
var selectedResult = selectedItem.Result;
|
||||||
{
|
var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " ";
|
||||||
return string.Empty;
|
var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title;
|
||||||
}
|
|
||||||
var QueryTextBox = values[0] as TextBox;
|
|
||||||
|
|
||||||
var queryText = (string)values[2];
|
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(queryText))
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
// second prop is the current selected item result
|
|
||||||
var val = values[1];
|
// For AutocompleteQueryCommand.
|
||||||
if (val == null)
|
// When user typed lower case and result title is uppercase, we still want to display suggestion
|
||||||
{
|
selectedItem.QuerySuggestionText = queryText + selectedResultPossibleSuggestion.Substring(queryText.Length);
|
||||||
|
|
||||||
|
// Check if Text will be larger than our QueryTextBox
|
||||||
|
Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch);
|
||||||
|
// TODO: Obsolete warning?
|
||||||
|
var ft = new FormattedText(queryTextBox.Text, CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black);
|
||||||
|
|
||||||
|
var offset = queryTextBox.Padding.Right;
|
||||||
|
|
||||||
|
if (ft.Width + offset > queryTextBox.ActualWidth || queryTextBox.HorizontalOffset != 0)
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
|
||||||
if (!(val is ResultViewModel))
|
|
||||||
{
|
|
||||||
return System.Windows.Data.Binding.DoNothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
return selectedItem.QuerySuggestionText;
|
||||||
{
|
|
||||||
var selectedItem = (ResultViewModel)val;
|
|
||||||
|
|
||||||
var selectedResult = selectedItem.Result;
|
|
||||||
var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " ";
|
|
||||||
var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title;
|
|
||||||
|
|
||||||
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
|
|
||||||
// For AutocompleteQueryCommand.
|
|
||||||
// When user typed lower case and result title is uppercase, we still want to display suggestion
|
|
||||||
selectedItem.QuerySuggestionText = queryText + selectedResultPossibleSuggestion.Substring(queryText.Length);
|
|
||||||
|
|
||||||
// Check if Text will be larger then our QueryTextBox
|
|
||||||
System.Windows.Media.Typeface typeface = new Typeface(QueryTextBox.FontFamily, QueryTextBox.FontStyle, QueryTextBox.FontWeight, QueryTextBox.FontStretch);
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
if ((ft.Width + offset) > QueryTextBox.ActualWidth || QueryTextBox.HorizontalOffset != 0)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
};
|
|
||||||
|
|
||||||
return selectedItem.QuerySuggestionText;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e);
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e);
|
||||||
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,30 +3,27 @@ using System.Globalization;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
namespace Flow.Launcher.Converters;
|
||||||
{
|
|
||||||
class StringToKeyBindingConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
var mode = parameter as string;
|
|
||||||
var hotkeyStr = value as string;
|
|
||||||
var converter = new KeyGestureConverter();
|
|
||||||
var key = (KeyGesture)converter.ConvertFromString(hotkeyStr);
|
|
||||||
if (mode == "key")
|
|
||||||
{
|
|
||||||
return key.Key;
|
|
||||||
}
|
|
||||||
else if (mode == "modifiers")
|
|
||||||
{
|
|
||||||
return key.Modifiers;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
class StringToKeyBindingConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (parameter is not string mode || value is not string hotkeyStr)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var converter = new KeyGestureConverter();
|
||||||
|
var key = (KeyGesture)converter.ConvertFromString(hotkeyStr);
|
||||||
|
return mode switch
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
"key" => key?.Key,
|
||||||
}
|
"modifiers" => key?.Modifiers,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,27 @@ using System.Windows.Data;
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
|
||||||
namespace Flow.Launcher.Converters
|
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();
|
public class TextConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
var id = value?.ToString();
|
||||||
|
var translationKey = id switch
|
||||||
|
{
|
||||||
|
PluginStoreItemViewModel.NewRelease => "pluginStore_NewRelease",
|
||||||
|
PluginStoreItemViewModel.RecentlyUpdated => "pluginStore_RecentlyUpdated",
|
||||||
|
PluginStoreItemViewModel.None => "pluginStore_None",
|
||||||
|
PluginStoreItemViewModel.Installed => "pluginStore_Installed",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (translationKey is null)
|
||||||
|
return id;
|
||||||
|
|
||||||
|
return InternationalizationManager.Instance.GetTranslation(translationKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
x:Class="Flow.Launcher.CustomQueryHotkeySetting"
|
x:Class="Flow.Launcher.CustomQueryHotkeySetting"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
|
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
|
||||||
Title="{DynamicResource customeQueryHotkeyTitle}"
|
Title="{DynamicResource customeQueryHotkeyTitle}"
|
||||||
Width="530"
|
Width="530"
|
||||||
Background="{DynamicResource PopuBGColor}"
|
Background="{DynamicResource PopuBGColor}"
|
||||||
|
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||||
Foreground="{DynamicResource PopupTextColor}"
|
Foreground="{DynamicResource PopupTextColor}"
|
||||||
Icon="Images\app.png"
|
Icon="Images\app.png"
|
||||||
MouseDown="window_MouseDown"
|
MouseDown="window_MouseDown"
|
||||||
|
|
@ -60,94 +62,76 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Margin="26,0,26,0">
|
<StackPanel Margin="26,0,26,0">
|
||||||
<StackPanel Grid.Row="0" Margin="0,0,0,12">
|
<TextBlock
|
||||||
<TextBlock
|
Margin="0,0,0,12"
|
||||||
Grid.Column="0"
|
FontSize="20"
|
||||||
Margin="0,0,0,0"
|
FontWeight="SemiBold"
|
||||||
FontSize="20"
|
Text="{DynamicResource customeQueryHotkeyTitle}"
|
||||||
FontWeight="SemiBold"
|
TextAlignment="Left" />
|
||||||
Text="{DynamicResource customeQueryHotkeyTitle}"
|
<TextBlock
|
||||||
TextAlignment="Left" />
|
FontSize="14"
|
||||||
</StackPanel>
|
Text="{DynamicResource customeQueryHotkeyTips}"
|
||||||
<StackPanel>
|
TextAlignment="Left"
|
||||||
<TextBlock
|
TextWrapping="WrapWithOverflow" />
|
||||||
FontSize="14"
|
<Image
|
||||||
Text="{DynamicResource customeQueryHotkeyTips}"
|
Width="478"
|
||||||
TextAlignment="Left"
|
Margin="0,20,0,0"
|
||||||
TextWrapping="WrapWithOverflow" />
|
Source="/Images/illustration_01.png" />
|
||||||
<Image
|
|
||||||
Width="478"
|
|
||||||
Margin="0,20,0,0"
|
|
||||||
Source="/Images/illustration_01.png" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Margin="0,20,0,0" Orientation="Horizontal">
|
<Grid Width="478" Margin="0,20,0,0">
|
||||||
<Grid Width="478">
|
<Grid.RowDefinitions>
|
||||||
<Grid.RowDefinitions>
|
<RowDefinition />
|
||||||
<RowDefinition />
|
<RowDefinition />
|
||||||
<RowDefinition />
|
</Grid.RowDefinitions>
|
||||||
</Grid.RowDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock
|
|
||||||
Grid.Row="0"
|
<TextBlock
|
||||||
Grid.Column="0"
|
Grid.Row="0"
|
||||||
Margin="10"
|
Grid.Column="0"
|
||||||
HorizontalAlignment="Left"
|
Margin="10"
|
||||||
VerticalAlignment="Center"
|
HorizontalAlignment="Left"
|
||||||
FontSize="14"
|
VerticalAlignment="Center"
|
||||||
Text="{DynamicResource hotkey}" />
|
FontSize="14"
|
||||||
<StackPanel
|
Text="{DynamicResource hotkey}" />
|
||||||
Grid.Row="0"
|
<flowlauncher:HotkeyControl
|
||||||
Grid.Column="1"
|
x:Name="HotkeyControl"
|
||||||
Orientation="Horizontal">
|
Grid.Row="0"
|
||||||
<flowlauncher:HotkeyControl
|
Grid.Column="1"
|
||||||
x:Name="ctlHotkey"
|
Grid.ColumnSpan="2"
|
||||||
Grid.Column="1"
|
Margin="10,0,10,0"
|
||||||
Width="200"
|
HorizontalAlignment="Left"
|
||||||
Height="36"
|
VerticalAlignment="Center"
|
||||||
Margin="10,0,10,0"
|
HorizontalContentAlignment="Left"
|
||||||
HorizontalAlignment="Left"
|
HotkeySettings="{Binding Settings}"
|
||||||
VerticalAlignment="Center"
|
DefaultHotkey="" />
|
||||||
HorizontalContentAlignment="Left" />
|
<TextBlock
|
||||||
<TextBlock
|
Grid.Row="1"
|
||||||
Grid.Row="1"
|
Grid.Column="0"
|
||||||
Grid.Column="0"
|
Margin="10"
|
||||||
Margin="10"
|
HorizontalAlignment="Left"
|
||||||
HorizontalAlignment="Left"
|
VerticalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
FontSize="14"
|
||||||
FontSize="14"
|
Text="{DynamicResource customQuery}" />
|
||||||
Text="{DynamicResource actionKeyword}" />
|
<TextBox
|
||||||
</StackPanel>
|
x:Name="tbAction"
|
||||||
<TextBlock
|
Grid.Row="1"
|
||||||
Grid.Row="1"
|
Grid.Column="1"
|
||||||
Grid.Column="0"
|
Margin="10"
|
||||||
Margin="10"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalAlignment="Left"
|
VerticalAlignment="Center" />
|
||||||
VerticalAlignment="Center"
|
<Button
|
||||||
FontSize="14"
|
x:Name="btnTestActionKeyword"
|
||||||
Text="{DynamicResource customQuery}" />
|
Grid.Row="1"
|
||||||
<DockPanel
|
Grid.Column="2"
|
||||||
Grid.Row="1"
|
Margin="0,0,10,0"
|
||||||
Grid.Column="1"
|
Padding="10,5,10,5"
|
||||||
LastChildFill="True">
|
Click="BtnTestActionKeyword_OnClick"
|
||||||
<Button
|
Content="{DynamicResource preview}" />
|
||||||
x:Name="btnTestActionKeyword"
|
</Grid>
|
||||||
Margin="0,0,10,0"
|
|
||||||
Padding="10,5,10,5"
|
|
||||||
Click="BtnTestActionKeyword_OnClick"
|
|
||||||
Content="{DynamicResource preview}"
|
|
||||||
DockPanel.Dock="Right" />
|
|
||||||
<TextBox
|
|
||||||
x:Name="tbAction"
|
|
||||||
Margin="10"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</DockPanel>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Border
|
<Border
|
||||||
|
|
@ -174,4 +158,4 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using Flow.Launcher.ViewModel;
|
||||||
|
|
||||||
namespace Flow.Launcher
|
namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
|
|
@ -14,13 +15,13 @@ namespace Flow.Launcher
|
||||||
private SettingWindow _settingWidow;
|
private SettingWindow _settingWidow;
|
||||||
private bool update;
|
private bool update;
|
||||||
private CustomPluginHotkey updateCustomHotkey;
|
private CustomPluginHotkey updateCustomHotkey;
|
||||||
private Settings _settings;
|
public Settings Settings { get; }
|
||||||
|
|
||||||
public CustomQueryHotkeySetting(SettingWindow settingWidow, Settings settings)
|
public CustomQueryHotkeySetting(SettingWindow settingWidow, Settings settings)
|
||||||
{
|
{
|
||||||
_settingWidow = settingWidow;
|
_settingWidow = settingWidow;
|
||||||
|
Settings = settings;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_settings = settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
|
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
|
@ -32,36 +33,21 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
if (!update)
|
if (!update)
|
||||||
{
|
{
|
||||||
if (!ctlHotkey.CurrentHotkeyAvailable)
|
Settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
|
||||||
{
|
|
||||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("hotkeyIsNotUnavailable"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_settings.CustomPluginHotkeys == null)
|
|
||||||
{
|
|
||||||
_settings.CustomPluginHotkeys = new ObservableCollection<CustomPluginHotkey>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var pluginHotkey = new CustomPluginHotkey
|
var pluginHotkey = new CustomPluginHotkey
|
||||||
{
|
{
|
||||||
Hotkey = ctlHotkey.CurrentHotkey.ToString(),
|
Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text
|
||||||
ActionKeyword = tbAction.Text
|
|
||||||
};
|
};
|
||||||
_settings.CustomPluginHotkeys.Add(pluginHotkey);
|
Settings.CustomPluginHotkeys.Add(pluginHotkey);
|
||||||
|
|
||||||
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
|
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (updateCustomHotkey.Hotkey != ctlHotkey.CurrentHotkey.ToString() && !ctlHotkey.CurrentHotkeyAvailable)
|
|
||||||
{
|
|
||||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("hotkeyIsNotUnavailable"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var oldHotkey = updateCustomHotkey.Hotkey;
|
var oldHotkey = updateCustomHotkey.Hotkey;
|
||||||
updateCustomHotkey.ActionKeyword = tbAction.Text;
|
updateCustomHotkey.ActionKeyword = tbAction.Text;
|
||||||
updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString();
|
updateCustomHotkey.Hotkey = HotkeyControl.CurrentHotkey.ToString();
|
||||||
//remove origin hotkey
|
//remove origin hotkey
|
||||||
HotKeyMapper.RemoveHotkey(oldHotkey);
|
HotKeyMapper.RemoveHotkey(oldHotkey);
|
||||||
HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey);
|
HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey);
|
||||||
|
|
@ -70,9 +56,11 @@ namespace Flow.Launcher
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void UpdateItem(CustomPluginHotkey item)
|
public void UpdateItem(CustomPluginHotkey item)
|
||||||
{
|
{
|
||||||
updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o => o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
|
updateCustomHotkey = Settings.CustomPluginHotkeys.FirstOrDefault(o =>
|
||||||
|
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
|
||||||
if (updateCustomHotkey == null)
|
if (updateCustomHotkey == null)
|
||||||
{
|
{
|
||||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
|
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
|
||||||
|
|
@ -81,7 +69,7 @@ namespace Flow.Launcher
|
||||||
}
|
}
|
||||||
|
|
||||||
tbAction.Text = updateCustomHotkey.ActionKeyword;
|
tbAction.Text = updateCustomHotkey.ActionKeyword;
|
||||||
_ = ctlHotkey.SetHotkeyAsync(updateCustomHotkey.Hotkey, false);
|
HotkeyControl.SetHotkey(updateCustomHotkey.Hotkey, false);
|
||||||
update = true;
|
update = true;
|
||||||
lblAdd.Text = InternationalizationManager.Instance.GetTranslation("update");
|
lblAdd.Text = InternationalizationManager.Instance.GetTranslation("update");
|
||||||
}
|
}
|
||||||
|
|
@ -101,12 +89,10 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */
|
private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */
|
||||||
{
|
{
|
||||||
TextBox textBox = Keyboard.FocusedElement as TextBox;
|
if (Keyboard.FocusedElement is not TextBox textBox) return;
|
||||||
if (textBox != null)
|
|
||||||
{
|
TraversalRequest tRequest = new TraversalRequest(FocusNavigationDirection.Next);
|
||||||
TraversalRequest tRequest = new TraversalRequest(FocusNavigationDirection.Next);
|
textBox.MoveFocus(tRequest);
|
||||||
textBox.MoveFocus(tRequest);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
public partial class CustomShortcutSetting : Window
|
public partial class CustomShortcutSetting : Window
|
||||||
{
|
{
|
||||||
private SettingWindowViewModel viewModel;
|
|
||||||
public string Key { get; set; } = String.Empty;
|
public string Key { get; set; } = String.Empty;
|
||||||
public string Value { get; set; } = String.Empty;
|
public string Value { get; set; } = String.Empty;
|
||||||
private string originalKey { get; init; } = null;
|
private string originalKey { get; init; } = null;
|
||||||
|
|
@ -17,13 +16,11 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
public CustomShortcutSetting(SettingWindowViewModel vm)
|
public CustomShortcutSetting(SettingWindowViewModel vm)
|
||||||
{
|
{
|
||||||
viewModel = vm;
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
|
public CustomShortcutSetting(string key, string value)
|
||||||
{
|
{
|
||||||
viewModel = vm;
|
|
||||||
Key = key;
|
Key = key;
|
||||||
Value = value;
|
Value = value;
|
||||||
originalKey = key;
|
originalKey = key;
|
||||||
|
|
@ -46,8 +43,7 @@ namespace Flow.Launcher
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check if key is modified or adding a new one
|
// Check if key is modified or adding a new one
|
||||||
if (((update && originalKey != Key) || !update)
|
if ((update && originalKey != Key) || !update)
|
||||||
&& viewModel.ShortcutExists(Key))
|
|
||||||
{
|
{
|
||||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
|
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||||
<PackageReference Include="Fody" Version="6.5.4">
|
<PackageReference Include="Fody" Version="6.5.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
<PackageReference Include="NHotkey.Wpf" Version="2.1.1" />
|
<PackageReference Include="NHotkey.Wpf" Version="2.1.1" />
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||||
<PackageReference Include="SharpVectors" Version="1.8.2" />
|
<PackageReference Include="SharpVectors" Version="1.8.2" />
|
||||||
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.7" />
|
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -3,57 +3,56 @@ using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
|
public class AutoStartup
|
||||||
{
|
{
|
||||||
public class AutoStartup
|
private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
|
||||||
|
|
||||||
|
public static bool IsEnabled
|
||||||
{
|
{
|
||||||
private const string StartupPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run";
|
get
|
||||||
|
|
||||||
public static bool IsEnabled
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
|
||||||
var path = key?.GetValue(Constant.FlowLauncher) as string;
|
|
||||||
return path == Constant.ExecutablePath;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.Error("AutoStartup", $"Ignoring non-critical registry error (querying if enabled): {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Disable()
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||||
key?.DeleteValue(Constant.FlowLauncher, false);
|
var path = key?.GetValue(Constant.FlowLauncher) as string;
|
||||||
|
return path == Constant.ExecutablePath;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.Error("AutoStartup", $"Failed to disable auto-startup: {e}");
|
Log.Error("AutoStartup", $"Ignoring non-critical registry error (querying if enabled): {e}");
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal static void Enable()
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Disable()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||||
{
|
key?.DeleteValue(Constant.FlowLauncher, false);
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
}
|
||||||
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
catch (Exception e)
|
||||||
}
|
{
|
||||||
catch (Exception e)
|
Log.Error("AutoStartup", $"Failed to disable auto-startup: {e}");
|
||||||
{
|
throw;
|
||||||
Log.Error("AutoStartup", $"Failed to enable auto-startup: {e}");
|
}
|
||||||
throw;
|
}
|
||||||
}
|
|
||||||
|
internal static void Enable()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||||
|
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error("AutoStartup", $"Failed to enable auto-startup: {e}");
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,70 +4,69 @@ using System.Runtime.InteropServices;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
|
public class DwmDropShadow
|
||||||
{
|
{
|
||||||
public class DwmDropShadow
|
|
||||||
|
[DllImport("dwmapi.dll", PreserveSig = true)]
|
||||||
|
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
||||||
|
|
||||||
|
[DllImport("dwmapi.dll")]
|
||||||
|
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven).
|
||||||
|
/// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect,
|
||||||
|
/// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="window">Window to which the shadow will be applied</param>
|
||||||
|
public static void DropShadowToWindow(Window window)
|
||||||
{
|
{
|
||||||
|
if (!DropShadow(window))
|
||||||
[DllImport("dwmapi.dll", PreserveSig = true)]
|
|
||||||
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
|
||||||
|
|
||||||
[DllImport("dwmapi.dll")]
|
|
||||||
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven).
|
|
||||||
/// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect,
|
|
||||||
/// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">Window to which the shadow will be applied</param>
|
|
||||||
public static void DropShadowToWindow(Window window)
|
|
||||||
{
|
{
|
||||||
if (!DropShadow(window))
|
window.SourceInitialized += window_SourceInitialized;
|
||||||
{
|
|
||||||
window.SourceInitialized += window_SourceInitialized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo
|
private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo
|
||||||
|
{
|
||||||
|
Window window = (Window)sender;
|
||||||
|
|
||||||
|
DropShadow(window);
|
||||||
|
|
||||||
|
window.SourceInitialized -= window_SourceInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The actual method that makes API calls to drop the shadow to the window
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="window">Window to which the shadow will be applied</param>
|
||||||
|
/// <returns>True if the method succeeded, false if not</returns>
|
||||||
|
private static bool DropShadow(Window window)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Window window = (Window)sender;
|
WindowInteropHelper helper = new WindowInteropHelper(window);
|
||||||
|
int val = 2;
|
||||||
|
int ret1 = DwmSetWindowAttribute(helper.Handle, 2, ref val, 4);
|
||||||
|
|
||||||
DropShadow(window);
|
if (ret1 == 0)
|
||||||
|
|
||||||
window.SourceInitialized -= window_SourceInitialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The actual method that makes API calls to drop the shadow to the window
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">Window to which the shadow will be applied</param>
|
|
||||||
/// <returns>True if the method succeeded, false if not</returns>
|
|
||||||
private static bool DropShadow(Window window)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
WindowInteropHelper helper = new WindowInteropHelper(window);
|
Margins m = new Margins { Bottom = 0, Left = 0, Right = 0, Top = 0 };
|
||||||
int val = 2;
|
int ret2 = DwmExtendFrameIntoClientArea(helper.Handle, ref m);
|
||||||
int ret1 = DwmSetWindowAttribute(helper.Handle, 2, ref val, 4);
|
return ret2 == 0;
|
||||||
|
|
||||||
if (ret1 == 0)
|
|
||||||
{
|
|
||||||
Margins m = new Margins { Bottom = 0, Left = 0, Right = 0, Top = 0 };
|
|
||||||
int ret2 = DwmExtendFrameIntoClientArea(helper.Handle, ref m);
|
|
||||||
return ret2 == 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
else
|
||||||
{
|
{
|
||||||
// Probably dwmapi.dll not found (incompatible OS)
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Probably dwmapi.dll not found (incompatible OS)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,68 +2,67 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
|
public class DataWebRequestFactory : IWebRequestCreate
|
||||||
{
|
{
|
||||||
public class DataWebRequestFactory : IWebRequestCreate
|
class DataWebRequest : WebRequest
|
||||||
{
|
{
|
||||||
class DataWebRequest : WebRequest
|
private readonly Uri _uri;
|
||||||
|
|
||||||
|
public DataWebRequest(Uri uri)
|
||||||
{
|
{
|
||||||
private readonly Uri m_uri;
|
_uri = uri;
|
||||||
|
|
||||||
public DataWebRequest(Uri uri)
|
|
||||||
{
|
|
||||||
m_uri = uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override WebResponse GetResponse()
|
|
||||||
{
|
|
||||||
return new DataWebResponse(m_uri);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DataWebResponse : WebResponse
|
public override WebResponse GetResponse()
|
||||||
{
|
{
|
||||||
private readonly string m_contentType;
|
return new DataWebResponse(_uri);
|
||||||
private readonly byte[] m_data;
|
|
||||||
|
|
||||||
public DataWebResponse(Uri uri)
|
|
||||||
{
|
|
||||||
string uriString = uri.AbsoluteUri;
|
|
||||||
|
|
||||||
int commaIndex = uriString.IndexOf(',');
|
|
||||||
var headers = uriString.Substring(0, commaIndex).Split(';');
|
|
||||||
m_contentType = headers[0];
|
|
||||||
string dataString = uriString.Substring(commaIndex + 1);
|
|
||||||
m_data = Convert.FromBase64String(dataString);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ContentType
|
|
||||||
{
|
|
||||||
get { return m_contentType; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override long ContentLength
|
|
||||||
{
|
|
||||||
get { return m_data.Length; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Stream GetResponseStream()
|
|
||||||
{
|
|
||||||
return new MemoryStream(m_data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public WebRequest Create(Uri uri)
|
|
||||||
{
|
|
||||||
return new DataWebRequest(uri);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DataWebResponse : WebResponse
|
||||||
|
{
|
||||||
|
private readonly string _contentType;
|
||||||
|
private readonly byte[] _data;
|
||||||
|
|
||||||
|
public DataWebResponse(Uri uri)
|
||||||
|
{
|
||||||
|
string uriString = uri.AbsoluteUri;
|
||||||
|
|
||||||
|
int commaIndex = uriString.IndexOf(',');
|
||||||
|
var headers = uriString.Substring(0, commaIndex).Split(';');
|
||||||
|
_contentType = headers[0];
|
||||||
|
string dataString = uriString.Substring(commaIndex + 1);
|
||||||
|
_data = Convert.FromBase64String(dataString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ContentType
|
||||||
|
{
|
||||||
|
get { return _contentType; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long ContentLength
|
||||||
|
{
|
||||||
|
get { return _data.Length; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Stream GetResponseStream()
|
||||||
|
{
|
||||||
|
return new MemoryStream(_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebRequest Create(Uri uri)
|
||||||
|
{
|
||||||
|
return new DataWebRequest(uri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,45 +4,52 @@ using NLog;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Exception;
|
using Flow.Launcher.Infrastructure.Exception;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
|
public static class ErrorReporting
|
||||||
{
|
{
|
||||||
public static class ErrorReporting
|
private static void Report(Exception e)
|
||||||
{
|
{
|
||||||
private static void Report(Exception e)
|
var logger = LogManager.GetLogger("UnHandledException");
|
||||||
{
|
logger.Fatal(ExceptionFormatter.FormatExcpetion(e));
|
||||||
var logger = LogManager.GetLogger("UnHandledException");
|
var reportWindow = new ReportWindow(e);
|
||||||
logger.Fatal(ExceptionFormatter.FormatExcpetion(e));
|
reportWindow.Show();
|
||||||
var reportWindow = new ReportWindow(e);
|
}
|
||||||
reportWindow.Show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e)
|
public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e)
|
||||||
{
|
{
|
||||||
//handle non-ui thread exceptions
|
//handle non-ui thread exceptions
|
||||||
Report((Exception)e.ExceptionObject);
|
Report((Exception)e.ExceptionObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||||
{
|
{
|
||||||
//handle ui thread exceptions
|
//handle ui thread exceptions
|
||||||
Report(e.Exception);
|
Report(e.Exception);
|
||||||
//prevent application exist, so the user can copy prompted error info
|
//prevent application exist, so the user can copy prompted error info
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string RuntimeInfo()
|
|
||||||
{
|
|
||||||
var info = $"\nFlow Launcher version: {Constant.Version}" +
|
|
||||||
$"\nOS Version: {ExceptionFormatter.GetWindowsFullVersionFromRegistry()}" +
|
|
||||||
$"\nIntPtr Length: {IntPtr.Size}" +
|
|
||||||
$"\nx64: {Environment.Is64BitOperatingSystem}";
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string DependenciesInfo()
|
public static string RuntimeInfo()
|
||||||
{
|
{
|
||||||
var info = $"\nPython Path: {Constant.PythonPath}\nNode Path: {Constant.NodePath}";
|
var info =
|
||||||
return info;
|
$"""
|
||||||
}
|
|
||||||
|
Flow Launcher version: {Constant.Version}
|
||||||
|
OS Version: {ExceptionFormatter.GetWindowsFullVersionFromRegistry()}
|
||||||
|
IntPtr Length: {IntPtr.Size}
|
||||||
|
x64: {Environment.Is64BitOperatingSystem}
|
||||||
|
""";
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DependenciesInfo()
|
||||||
|
{
|
||||||
|
var info = $"""
|
||||||
|
|
||||||
|
Python Path: {Constant.PythonPath}
|
||||||
|
Node Path: {Constant.NodePath}
|
||||||
|
""";
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,97 +7,96 @@ using Flow.Launcher.Core.Resource;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
|
internal static class HotKeyMapper
|
||||||
{
|
{
|
||||||
internal static class HotKeyMapper
|
private static Settings _settings;
|
||||||
|
private static MainViewModel _mainViewModel;
|
||||||
|
|
||||||
|
internal static void Initialize(MainViewModel mainVM)
|
||||||
{
|
{
|
||||||
private static Settings settings;
|
_mainViewModel = mainVM;
|
||||||
private static MainViewModel mainViewModel;
|
_settings = _mainViewModel.Settings;
|
||||||
|
|
||||||
internal static void Initialize(MainViewModel mainVM)
|
SetHotkey(_settings.Hotkey, OnToggleHotkey);
|
||||||
|
LoadCustomPluginHotkey();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void OnToggleHotkey(object sender, HotkeyEventArgs args)
|
||||||
|
{
|
||||||
|
if (!_mainViewModel.ShouldIgnoreHotkeys())
|
||||||
|
_mainViewModel.ToggleFlowLauncher();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
||||||
|
{
|
||||||
|
var hotkey = new HotkeyModel(hotkeyStr);
|
||||||
|
SetHotkey(hotkey, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
|
||||||
|
{
|
||||||
|
string hotkeyStr = hotkey.ToString();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
mainViewModel = mainVM;
|
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
|
||||||
settings = mainViewModel.Settings;
|
|
||||||
|
|
||||||
SetHotkey(settings.Hotkey, OnToggleHotkey);
|
|
||||||
LoadCustomPluginHotkey();
|
|
||||||
}
|
}
|
||||||
|
catch (Exception)
|
||||||
internal static void OnToggleHotkey(object sender, HotkeyEventArgs args)
|
|
||||||
{
|
{
|
||||||
if (!mainViewModel.ShouldIgnoreHotkeys())
|
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
||||||
mainViewModel.ToggleFlowLauncher();
|
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
||||||
}
|
MessageBox.Show(errorMsg,errorMsgTitle);
|
||||||
|
|
||||||
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
|
||||||
{
|
|
||||||
var hotkey = new HotkeyModel(hotkeyStr);
|
|
||||||
SetHotkey(hotkey, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
|
|
||||||
{
|
|
||||||
string hotkeyStr = hotkey.ToString();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
|
||||||
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
|
||||||
MessageBox.Show(errorMsg,errorMsgTitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void RemoveHotkey(string hotkeyStr)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(hotkeyStr))
|
|
||||||
{
|
|
||||||
HotkeyManager.Current.Remove(hotkeyStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void LoadCustomPluginHotkey()
|
|
||||||
{
|
|
||||||
if (settings.CustomPluginHotkeys == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (CustomPluginHotkey hotkey in settings.CustomPluginHotkeys)
|
|
||||||
{
|
|
||||||
SetCustomQueryHotkey(hotkey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey)
|
|
||||||
{
|
|
||||||
SetHotkey(hotkey.Hotkey, (s, e) =>
|
|
||||||
{
|
|
||||||
if (mainViewModel.ShouldIgnoreHotkeys())
|
|
||||||
return;
|
|
||||||
|
|
||||||
mainViewModel.Show();
|
|
||||||
mainViewModel.ChangeQueryText(hotkey.ActionKeyword, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool CheckAvailability(HotkeyModel currentHotkey)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { });
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void RemoveHotkey(string hotkeyStr)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(hotkeyStr))
|
||||||
|
{
|
||||||
|
HotkeyManager.Current.Remove(hotkeyStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void LoadCustomPluginHotkey()
|
||||||
|
{
|
||||||
|
if (_settings.CustomPluginHotkeys == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys)
|
||||||
|
{
|
||||||
|
SetCustomQueryHotkey(hotkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey)
|
||||||
|
{
|
||||||
|
SetHotkey(hotkey.Hotkey, (s, e) =>
|
||||||
|
{
|
||||||
|
if (_mainViewModel.ShouldIgnoreHotkeys())
|
||||||
|
return;
|
||||||
|
|
||||||
|
_mainViewModel.Show();
|
||||||
|
_mainViewModel.ChangeQueryText(hotkey.ActionKeyword, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool CheckAvailability(HotkeyModel currentHotkey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,26 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
{
|
|
||||||
public static class SingletonWindowOpener
|
|
||||||
{
|
|
||||||
public static T Open<T>(params object[] args) where T : Window
|
|
||||||
{
|
|
||||||
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
|
||||||
?? (T)Activator.CreateInstance(typeof(T), args);
|
|
||||||
|
|
||||||
// Fix UI bug
|
|
||||||
// Add `window.WindowState = WindowState.Normal`
|
|
||||||
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
|
|
||||||
// Not sure why this works tho
|
|
||||||
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
|
|
||||||
// https://stackoverflow.com/a/59719760/4230390
|
|
||||||
window.WindowState = WindowState.Normal;
|
|
||||||
window.Show();
|
|
||||||
|
|
||||||
window.Focus();
|
|
||||||
|
|
||||||
return (T)window;
|
public static class SingletonWindowOpener
|
||||||
}
|
{
|
||||||
|
public static T Open<T>(params object[] args) where T : Window
|
||||||
|
{
|
||||||
|
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
||||||
|
?? (T)Activator.CreateInstance(typeof(T), args);
|
||||||
|
|
||||||
|
// Fix UI bug
|
||||||
|
// Add `window.WindowState = WindowState.Normal`
|
||||||
|
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
|
||||||
|
// Not sure why this works tho
|
||||||
|
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
|
||||||
|
// https://stackoverflow.com/a/59719760/4230390
|
||||||
|
window.WindowState = WindowState.Normal;
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
window.Focus();
|
||||||
|
|
||||||
|
return (T)window;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,23 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
{
|
|
||||||
public static class SyntaxSugars
|
|
||||||
{
|
|
||||||
public static TResult CallOrRescueDefault<TResult>(Func<TResult> callback)
|
|
||||||
{
|
|
||||||
return CallOrRescueDefault(callback, default(TResult));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TResult CallOrRescueDefault<TResult>(Func<TResult> callback, TResult def)
|
public static class SyntaxSugars
|
||||||
|
{
|
||||||
|
public static TResult CallOrRescueDefault<TResult>(Func<TResult> callback)
|
||||||
|
{
|
||||||
|
return CallOrRescueDefault(callback, default(TResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TResult CallOrRescueDefault<TResult>(Func<TResult> callback, TResult def)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
return callback();
|
||||||
{
|
}
|
||||||
return callback();
|
catch
|
||||||
}
|
{
|
||||||
catch
|
return def;
|
||||||
{
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,44 +5,43 @@ using System.Text;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
|
public static class WallpaperPathRetrieval
|
||||||
{
|
{
|
||||||
public static class WallpaperPathRetrieval
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern Int32 SystemParametersInfo(UInt32 action,
|
||||||
|
Int32 uParam, StringBuilder vParam, UInt32 winIni);
|
||||||
|
private static readonly UInt32 SPI_GETDESKWALLPAPER = 0x73;
|
||||||
|
private static int MAX_PATH = 260;
|
||||||
|
|
||||||
|
public static string GetWallpaperPath()
|
||||||
{
|
{
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
var wallpaper = new StringBuilder(MAX_PATH);
|
||||||
private static extern Int32 SystemParametersInfo(UInt32 action,
|
SystemParametersInfo(SPI_GETDESKWALLPAPER, MAX_PATH, wallpaper, 0);
|
||||||
Int32 uParam, StringBuilder vParam, UInt32 winIni);
|
|
||||||
private static readonly UInt32 SPI_GETDESKWALLPAPER = 0x73;
|
|
||||||
private static int MAX_PATH = 260;
|
|
||||||
|
|
||||||
public static string GetWallpaperPath()
|
var str = wallpaper.ToString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color GetWallpaperColor()
|
||||||
|
{
|
||||||
|
RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", true);
|
||||||
|
var result = key?.GetValue("Background", null);
|
||||||
|
if (result is string strResult)
|
||||||
{
|
{
|
||||||
var wallpaper = new StringBuilder(MAX_PATH);
|
try
|
||||||
SystemParametersInfo(SPI_GETDESKWALLPAPER, MAX_PATH, wallpaper, 0);
|
{
|
||||||
|
var parts = strResult.Trim().Split(new[] {' '}, 3).Select(byte.Parse).ToList();
|
||||||
var str = wallpaper.ToString();
|
return Color.FromRgb(parts[0], parts[1], parts[2]);
|
||||||
if (string.IsNullOrEmpty(str))
|
}
|
||||||
return null;
|
catch
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Color GetWallpaperColor()
|
|
||||||
{
|
|
||||||
RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Colors", true);
|
|
||||||
var result = key.GetValue(@"Background", null);
|
|
||||||
if (result != null && result is string)
|
|
||||||
{
|
{
|
||||||
try
|
|
||||||
{
|
|
||||||
var parts = result.ToString().Trim().Split(new[] {' '}, 3).Select(byte.Parse).ToList();
|
|
||||||
return Color.FromRgb(parts[0], parts[1], parts[2]);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Colors.Transparent;
|
|
||||||
}
|
}
|
||||||
|
return Colors.Transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,156 +8,152 @@ using System.Windows.Interop;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using Point = System.Windows.Point;
|
using Point = System.Windows.Point;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
|
public class WindowsInteropHelper
|
||||||
{
|
{
|
||||||
public class WindowsInteropHelper
|
private const int GWL_STYLE = -16; //WPF's Message code for Title Bar's Style
|
||||||
|
private const int WS_SYSMENU = 0x80000; //WPF's Message code for System Menu
|
||||||
|
private static IntPtr _hwnd_shell;
|
||||||
|
private static IntPtr _hwnd_desktop;
|
||||||
|
|
||||||
|
//Accessors for shell and desktop handlers
|
||||||
|
//Will set the variables once and then will return them
|
||||||
|
private static IntPtr HWND_SHELL
|
||||||
{
|
{
|
||||||
private const int GWL_STYLE = -16; //WPF's Message code for Title Bar's Style
|
get
|
||||||
private const int WS_SYSMENU = 0x80000; //WPF's Message code for System Menu
|
|
||||||
private static IntPtr _hwnd_shell;
|
|
||||||
private static IntPtr _hwnd_desktop;
|
|
||||||
|
|
||||||
//Accessors for shell and desktop handlers
|
|
||||||
//Will set the variables once and then will return them
|
|
||||||
private static IntPtr HWND_SHELL
|
|
||||||
{
|
{
|
||||||
get
|
return _hwnd_shell != IntPtr.Zero ? _hwnd_shell : _hwnd_shell = GetShellWindow();
|
||||||
{
|
|
||||||
return _hwnd_shell != IntPtr.Zero ? _hwnd_shell : _hwnd_shell = GetShellWindow();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
private static IntPtr HWND_DESKTOP
|
}
|
||||||
|
private static IntPtr HWND_DESKTOP
|
||||||
|
{
|
||||||
|
get
|
||||||
{
|
{
|
||||||
get
|
return _hwnd_desktop != IntPtr.Zero ? _hwnd_desktop : _hwnd_desktop = GetDesktopWindow();
|
||||||
{
|
|
||||||
return _hwnd_desktop != IntPtr.Zero ? _hwnd_desktop : _hwnd_desktop = GetDesktopWindow();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
internal static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
internal static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
|
||||||
internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
internal static extern IntPtr GetForegroundWindow();
|
internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
internal static extern IntPtr GetDesktopWindow();
|
internal static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
internal static extern IntPtr GetShellWindow();
|
internal static extern IntPtr GetDesktopWindow();
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
[DllImport("user32.dll")]
|
||||||
internal static extern int GetWindowRect(IntPtr hwnd, out RECT rc);
|
internal static extern IntPtr GetShellWindow();
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
|
internal static extern int GetWindowRect(IntPtr hwnd, out RECT rc);
|
||||||
|
|
||||||
[DllImport("user32.DLL")]
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
|
internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
|
||||||
|
|
||||||
|
[DllImport("user32.DLL")]
|
||||||
|
public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
|
||||||
|
|
||||||
|
|
||||||
const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
|
const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
|
||||||
const string WINDOW_CLASS_WINTAB = "Flip3D";
|
const string WINDOW_CLASS_WINTAB = "Flip3D";
|
||||||
const string WINDOW_CLASS_PROGMAN = "Progman";
|
const string WINDOW_CLASS_PROGMAN = "Progman";
|
||||||
const string WINDOW_CLASS_WORKERW = "WorkerW";
|
const string WINDOW_CLASS_WORKERW = "WorkerW";
|
||||||
|
|
||||||
public static bool IsWindowFullscreen()
|
public static bool IsWindowFullscreen()
|
||||||
|
{
|
||||||
|
//get current active window
|
||||||
|
IntPtr hWnd = GetForegroundWindow();
|
||||||
|
|
||||||
|
if (hWnd.Equals(IntPtr.Zero))
|
||||||
{
|
{
|
||||||
//get current active window
|
|
||||||
IntPtr hWnd = GetForegroundWindow();
|
|
||||||
|
|
||||||
if (!hWnd.Equals(IntPtr.Zero))
|
|
||||||
{
|
|
||||||
//if current active window is NOT desktop or shell
|
|
||||||
if (!(hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)))
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder(256);
|
|
||||||
GetClassName(hWnd, sb, sb.Capacity);
|
|
||||||
string windowClass = sb.ToString();
|
|
||||||
|
|
||||||
//for Win+Tab (Flip3D)
|
|
||||||
if (windowClass == WINDOW_CLASS_WINTAB)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
RECT appBounds;
|
|
||||||
GetWindowRect(hWnd, out appBounds);
|
|
||||||
|
|
||||||
//for console (ConsoleWindowClass), we have to check for negative dimensions
|
|
||||||
if (windowClass == WINDOW_CLASS_CONSOLE)
|
|
||||||
{
|
|
||||||
return appBounds.Top < 0 && appBounds.Bottom < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//for desktop (Progman or WorkerW, depends on the system), we have to check
|
|
||||||
if (windowClass == WINDOW_CLASS_PROGMAN || windowClass == WINDOW_CLASS_WORKERW)
|
|
||||||
{
|
|
||||||
IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null);
|
|
||||||
hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView");
|
|
||||||
if (!hWndDesktop.Equals(IntPtr.Zero))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds;
|
|
||||||
if ((appBounds.Bottom - appBounds.Top) == screenBounds.Height && (appBounds.Right - appBounds.Left) == screenBounds.Width)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
//if current active window is desktop or shell, exit early
|
||||||
/// disable windows toolbar's control box
|
if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL))
|
||||||
/// this will also disable system menu with Alt+Space hotkey
|
|
||||||
/// </summary>
|
|
||||||
public static void DisableControlBox(Window win)
|
|
||||||
{
|
{
|
||||||
var hwnd = new WindowInteropHelper(win).Handle;
|
return false;
|
||||||
SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
StringBuilder sb = new StringBuilder(256);
|
||||||
/// Transforms pixels to Device Independent Pixels used by WPF
|
GetClassName(hWnd, sb, sb.Capacity);
|
||||||
/// </summary>
|
string windowClass = sb.ToString();
|
||||||
/// <param name="visual">current window, required to get presentation source</param>
|
|
||||||
/// <param name="unitX">horizontal position in pixels</param>
|
//for Win+Tab (Flip3D)
|
||||||
/// <param name="unitY">vertical position in pixels</param>
|
if (windowClass == WINDOW_CLASS_WINTAB)
|
||||||
/// <returns>point containing device independent pixels</returns>
|
|
||||||
public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
|
|
||||||
{
|
{
|
||||||
Matrix matrix;
|
return false;
|
||||||
var source = PresentationSource.FromVisual(visual);
|
}
|
||||||
if (source != null)
|
|
||||||
|
RECT appBounds;
|
||||||
|
GetWindowRect(hWnd, out appBounds);
|
||||||
|
|
||||||
|
//for console (ConsoleWindowClass), we have to check for negative dimensions
|
||||||
|
if (windowClass == WINDOW_CLASS_CONSOLE)
|
||||||
|
{
|
||||||
|
return appBounds.Top < 0 && appBounds.Bottom < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//for desktop (Progman or WorkerW, depends on the system), we have to check
|
||||||
|
if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW)
|
||||||
|
{
|
||||||
|
IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||||||
|
hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView");
|
||||||
|
if (!hWndDesktop.Equals(IntPtr.Zero))
|
||||||
{
|
{
|
||||||
matrix = source.CompositionTarget.TransformFromDevice;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
using (var src = new HwndSource(new HwndSourceParameters()))
|
|
||||||
{
|
|
||||||
matrix = src.CompositionTarget.TransformFromDevice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds;
|
||||||
|
return (appBounds.Bottom - appBounds.Top) == screenBounds.Height && (appBounds.Right - appBounds.Left) == screenBounds.Width;
|
||||||
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
/// <summary>
|
||||||
public struct RECT
|
/// disable windows toolbar's control box
|
||||||
|
/// this will also disable system menu with Alt+Space hotkey
|
||||||
|
/// </summary>
|
||||||
|
public static void DisableControlBox(Window win)
|
||||||
|
{
|
||||||
|
var hwnd = new WindowInteropHelper(win).Handle;
|
||||||
|
SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms pixels to Device Independent Pixels used by WPF
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="visual">current window, required to get presentation source</param>
|
||||||
|
/// <param name="unitX">horizontal position in pixels</param>
|
||||||
|
/// <param name="unitY">vertical position in pixels</param>
|
||||||
|
/// <returns>point containing device independent pixels</returns>
|
||||||
|
public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
|
||||||
|
{
|
||||||
|
Matrix matrix;
|
||||||
|
var source = PresentationSource.FromVisual(visual);
|
||||||
|
if (source is not null)
|
||||||
{
|
{
|
||||||
public int Left;
|
matrix = source.CompositionTarget.TransformFromDevice;
|
||||||
public int Top;
|
|
||||||
public int Right;
|
|
||||||
public int Bottom;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var src = new HwndSource(new HwndSourceParameters());
|
||||||
|
matrix = src.CompositionTarget.TransformFromDevice;
|
||||||
|
}
|
||||||
|
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct RECT
|
||||||
|
{
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs
Normal file
11
Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Helper;
|
||||||
|
internal static class WindowsMediaPlayerHelper
|
||||||
|
{
|
||||||
|
internal static bool IsWindowsMediaPlayerInstalled()
|
||||||
|
{
|
||||||
|
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MediaPlayer");
|
||||||
|
return key.GetValue("Installation Directory") != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,54 +3,75 @@
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:input="clr-namespace:System.Windows.Input;assembly=PresentationCore"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
Height="24"
|
|
||||||
d:DesignHeight="300"
|
|
||||||
d:DesignWidth="300"
|
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<Grid>
|
<Button
|
||||||
<Grid.ColumnDefinitions>
|
Width="Auto"
|
||||||
<ColumnDefinition Width="200" />
|
FontSize="13"
|
||||||
</Grid.ColumnDefinitions>
|
FontWeight="Bold"
|
||||||
<Popup
|
Foreground="{DynamicResource Color01B}"
|
||||||
x:Name="popup"
|
Click="GetNewHotkey">
|
||||||
AllowDrop="True"
|
<Button.Template>
|
||||||
AllowsTransparency="True"
|
<ControlTemplate TargetType="Button">
|
||||||
IsOpen="{Binding IsKeyboardFocused, ElementName=tbHotkey, Mode=OneWay}"
|
<Border
|
||||||
Placement="Top"
|
x:Name="ButtonBorder"
|
||||||
PlacementTarget="{Binding ElementName=tbHotkey}"
|
Padding="5,0,5,0"
|
||||||
PopupAnimation="Fade"
|
Background="{DynamicResource ButtonBackgroundColor}"
|
||||||
StaysOpen="True"
|
BorderBrush="{DynamicResource ButtonInsideBorder}"
|
||||||
VerticalOffset="-5">
|
BorderThickness="1"
|
||||||
<Border
|
CornerRadius="5">
|
||||||
Width="140"
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
Height="30"
|
</Border>
|
||||||
Background="{DynamicResource Color01B}"
|
<ControlTemplate.Triggers>
|
||||||
BorderBrush="{DynamicResource Color21B}"
|
<MultiTrigger>
|
||||||
BorderThickness="1"
|
<MultiTrigger.Conditions>
|
||||||
CornerRadius="4">
|
<Condition Property="IsMouseOver" Value="True" />
|
||||||
<TextBlock
|
<Condition Property="IsPressed" Value="True" />
|
||||||
x:Name="tbMsg"
|
</MultiTrigger.Conditions>
|
||||||
Margin="0,0,0,0"
|
<Setter TargetName="ButtonBorder" Property="Background"
|
||||||
HorizontalAlignment="Center"
|
Value="{DynamicResource ButtonMousePressed}" />
|
||||||
VerticalAlignment="Center"
|
<Setter TargetName="ButtonBorder" Property="BorderBrush"
|
||||||
FontSize="13"
|
Value="{DynamicResource ButtonMousePressedInsideBorder}" />
|
||||||
FontWeight="SemiBold"
|
</MultiTrigger>
|
||||||
Foreground="{DynamicResource Color05B}"
|
<MultiTrigger>
|
||||||
Text="{DynamicResource flowlauncherPressHotkey}"
|
<MultiTrigger.Conditions>
|
||||||
Visibility="Visible" />
|
<Condition Property="IsMouseOver" Value="True" />
|
||||||
</Border>
|
</MultiTrigger.Conditions>
|
||||||
</Popup>
|
<Setter TargetName="ButtonBorder" Property="Background"
|
||||||
|
Value="{DynamicResource ButtonMouseOver}" />
|
||||||
<TextBox
|
</MultiTrigger>
|
||||||
x:Name="tbHotkey"
|
<MultiTrigger>
|
||||||
Margin="0,0,18,0"
|
<MultiTrigger.Conditions>
|
||||||
VerticalContentAlignment="Center"
|
<Condition Property="IsPressed" Value="True" />
|
||||||
input:InputMethod.IsInputMethodEnabled="False"
|
</MultiTrigger.Conditions>
|
||||||
GotFocus="tbHotkey_GotFocus"
|
<Setter TargetName="ButtonBorder" Property="Background"
|
||||||
LostFocus="tbHotkey_LostFocus"
|
Value="{DynamicResource ButtonMousePressed}" />
|
||||||
PreviewKeyDown="TbHotkey_OnPreviewKeyDown"
|
<Setter TargetName="ButtonBorder" Property="BorderBrush"
|
||||||
TabIndex="100" />
|
Value="{DynamicResource CustomContextHover}" />
|
||||||
</Grid>
|
</MultiTrigger>
|
||||||
</UserControl>
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Button.Template>
|
||||||
|
<Button.Content>
|
||||||
|
<ItemsControl x:Name="HotkeyList">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border
|
||||||
|
Margin="2,5,2,5"
|
||||||
|
Padding="10,5,10,5"
|
||||||
|
Background="{DynamicResource AccentButtonBackground}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="5">
|
||||||
|
<TextBlock Text="{Binding}" />
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Button.Content>
|
||||||
|
</Button>
|
||||||
|
</UserControl>
|
||||||
|
|
|
||||||
|
|
@ -1,141 +1,204 @@
|
||||||
using System;
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
using Flow.Launcher.Helper;
|
using Flow.Launcher.Helper;
|
||||||
using Flow.Launcher.Infrastructure.Hotkey;
|
using Flow.Launcher.Infrastructure.Hotkey;
|
||||||
using Flow.Launcher.Plugin;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Flow.Launcher
|
namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
public partial class HotkeyControl : UserControl
|
public partial class HotkeyControl
|
||||||
{
|
{
|
||||||
public HotkeyModel CurrentHotkey { get; private set; }
|
public IHotkeySettings HotkeySettings {
|
||||||
public bool CurrentHotkeyAvailable { get; private set; }
|
get { return (IHotkeySettings)GetValue(HotkeySettingsProperty); }
|
||||||
|
set { SetValue(HotkeySettingsProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
public event EventHandler HotkeyChanged;
|
public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register(
|
||||||
|
nameof(HotkeySettings),
|
||||||
|
typeof(IHotkeySettings),
|
||||||
|
typeof(HotkeyControl),
|
||||||
|
new PropertyMetadata()
|
||||||
|
);
|
||||||
|
public string WindowTitle {
|
||||||
|
get { return (string)GetValue(WindowTitleProperty); }
|
||||||
|
set { SetValue(WindowTitleProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty WindowTitleProperty = DependencyProperty.Register(
|
||||||
|
nameof(WindowTitle),
|
||||||
|
typeof(string),
|
||||||
|
typeof(HotkeyControl),
|
||||||
|
new PropertyMetadata(string.Empty)
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Designed for Preview Hotkey and KeyGesture.
|
/// Designed for Preview Hotkey and KeyGesture.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ValidateKeyGesture { get; set; } = false;
|
public static readonly DependencyProperty ValidateKeyGestureProperty = DependencyProperty.Register(
|
||||||
|
nameof(ValidateKeyGesture),
|
||||||
|
typeof(bool),
|
||||||
|
typeof(HotkeyControl),
|
||||||
|
new PropertyMetadata(default(bool))
|
||||||
|
);
|
||||||
|
|
||||||
protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty);
|
public bool ValidateKeyGesture
|
||||||
|
|
||||||
public HotkeyControl()
|
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
get { return (bool)GetValue(ValidateKeyGestureProperty); }
|
||||||
|
set { SetValue(ValidateKeyGestureProperty, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource hotkeyUpdateSource;
|
public static readonly DependencyProperty DefaultHotkeyProperty = DependencyProperty.Register(
|
||||||
|
nameof(DefaultHotkey),
|
||||||
|
typeof(string),
|
||||||
|
typeof(HotkeyControl),
|
||||||
|
new PropertyMetadata(default(string))
|
||||||
|
);
|
||||||
|
|
||||||
private void TbHotkey_OnPreviewKeyDown(object sender, KeyEventArgs e)
|
public string DefaultHotkey
|
||||||
{
|
{
|
||||||
hotkeyUpdateSource?.Cancel();
|
get { return (string)GetValue(DefaultHotkeyProperty); }
|
||||||
hotkeyUpdateSource?.Dispose();
|
set { SetValue(DefaultHotkeyProperty, value); }
|
||||||
hotkeyUpdateSource = new();
|
}
|
||||||
var token = hotkeyUpdateSource.Token;
|
|
||||||
e.Handled = true;
|
|
||||||
|
|
||||||
//when alt is pressed, the real key should be e.SystemKey
|
private static void OnHotkeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
Key key = e.Key == Key.System ? e.SystemKey : e.Key;
|
{
|
||||||
|
if (d is not HotkeyControl hotkeyControl)
|
||||||
SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers();
|
|
||||||
|
|
||||||
var hotkeyModel = new HotkeyModel(
|
|
||||||
specialKeyState.AltPressed,
|
|
||||||
specialKeyState.ShiftPressed,
|
|
||||||
specialKeyState.WinPressed,
|
|
||||||
specialKeyState.CtrlPressed,
|
|
||||||
key);
|
|
||||||
|
|
||||||
if (hotkeyModel.Equals(CurrentHotkey))
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = Dispatcher.InvokeAsync(async () =>
|
hotkeyControl.SetKeysToDisplay(new HotkeyModel(hotkeyControl.Hotkey));
|
||||||
{
|
hotkeyControl.CurrentHotkey = new HotkeyModel(hotkeyControl.Hotkey);
|
||||||
await Task.Delay(500, token);
|
|
||||||
if (!token.IsCancellationRequested)
|
|
||||||
await SetHotkeyAsync(hotkeyModel);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true)
|
|
||||||
{
|
|
||||||
tbHotkey.Text = keyModel.ToString();
|
|
||||||
tbHotkey.Select(tbHotkey.Text.Length, 0);
|
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ChangeHotkeyProperty = DependencyProperty.Register(
|
||||||
|
nameof(ChangeHotkey),
|
||||||
|
typeof(ICommand),
|
||||||
|
typeof(HotkeyControl),
|
||||||
|
new PropertyMetadata(default(ICommand))
|
||||||
|
);
|
||||||
|
|
||||||
|
public ICommand? ChangeHotkey
|
||||||
|
{
|
||||||
|
get { return (ICommand)GetValue(ChangeHotkeyProperty); }
|
||||||
|
set { SetValue(ChangeHotkeyProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static readonly DependencyProperty HotkeyProperty = DependencyProperty.Register(
|
||||||
|
nameof(Hotkey),
|
||||||
|
typeof(string),
|
||||||
|
typeof(HotkeyControl),
|
||||||
|
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged)
|
||||||
|
);
|
||||||
|
|
||||||
|
public string Hotkey
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(HotkeyProperty); }
|
||||||
|
set { SetValue(HotkeyProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public HotkeyControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
HotkeyList.ItemsSource = KeysToDisplay;
|
||||||
|
SetKeysToDisplay(CurrentHotkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
|
||||||
|
hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
|
||||||
|
|
||||||
|
public string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none");
|
||||||
|
|
||||||
|
public ObservableCollection<string> KeysToDisplay { get; set; } = new();
|
||||||
|
|
||||||
|
public HotkeyModel CurrentHotkey { get; private set; } = new(false, false, false, false, Key.None);
|
||||||
|
|
||||||
|
|
||||||
|
public void GetNewHotkey(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
OpenHotkeyDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenHotkeyDialog()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(Hotkey))
|
||||||
|
{
|
||||||
|
HotKeyMapper.RemoveHotkey(Hotkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, HotkeySettings, WindowTitle);
|
||||||
|
await dialog.ShowAsync();
|
||||||
|
switch (dialog.ResultType)
|
||||||
|
{
|
||||||
|
case HotkeyControlDialog.EResultType.Cancel:
|
||||||
|
SetHotkey(Hotkey);
|
||||||
|
return;
|
||||||
|
case HotkeyControlDialog.EResultType.Save:
|
||||||
|
SetHotkey(dialog.ResultValue);
|
||||||
|
break;
|
||||||
|
case HotkeyControlDialog.EResultType.Delete:
|
||||||
|
Delete();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true)
|
||||||
|
{
|
||||||
if (triggerValidate)
|
if (triggerValidate)
|
||||||
{
|
{
|
||||||
bool hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture);
|
bool hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture);
|
||||||
CurrentHotkeyAvailable = hotkeyAvailable;
|
|
||||||
SetMessage(hotkeyAvailable);
|
|
||||||
OnHotkeyChanged();
|
|
||||||
|
|
||||||
var token = hotkeyUpdateSource.Token;
|
if (!hotkeyAvailable)
|
||||||
await Task.Delay(500, token);
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (CurrentHotkeyAvailable)
|
|
||||||
{
|
{
|
||||||
CurrentHotkey = keyModel;
|
return;
|
||||||
// To trigger LostFocus
|
|
||||||
FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null);
|
|
||||||
Keyboard.ClearFocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Hotkey = keyModel.ToString();
|
||||||
|
SetKeysToDisplay(CurrentHotkey);
|
||||||
|
ChangeHotkey?.Execute(keyModel);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CurrentHotkey = keyModel;
|
Hotkey = keyModel.ToString();
|
||||||
|
ChangeHotkey?.Execute(keyModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SetHotkeyAsync(string keyStr, bool triggerValidate = true)
|
public void Delete()
|
||||||
{
|
{
|
||||||
return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate);
|
if (!string.IsNullOrEmpty(Hotkey))
|
||||||
|
HotKeyMapper.RemoveHotkey(Hotkey);
|
||||||
|
Hotkey = "";
|
||||||
|
SetKeysToDisplay(new HotkeyModel(false, false, false, false, Key.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
|
private void SetKeysToDisplay(HotkeyModel? hotkey)
|
||||||
|
|
||||||
public new bool IsFocused => tbHotkey.IsFocused;
|
|
||||||
|
|
||||||
private void tbHotkey_LostFocus(object sender, RoutedEventArgs e)
|
|
||||||
{
|
{
|
||||||
tbHotkey.Text = CurrentHotkey?.ToString() ?? "";
|
KeysToDisplay.Clear();
|
||||||
tbHotkey.Select(tbHotkey.Text.Length, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void tbHotkey_GotFocus(object sender, RoutedEventArgs e)
|
if (hotkey == null || hotkey == default(HotkeyModel))
|
||||||
{
|
|
||||||
ResetMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetMessage()
|
|
||||||
{
|
|
||||||
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("flowlauncherPressHotkey");
|
|
||||||
tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetMessage(bool hotkeyAvailable)
|
|
||||||
{
|
|
||||||
if (!hotkeyAvailable)
|
|
||||||
{
|
{
|
||||||
tbMsg.Foreground = new SolidColorBrush(Colors.Red);
|
KeysToDisplay.Add(EmptyHotkey);
|
||||||
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable");
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
foreach (var key in hotkey.Value.EnumerateDisplayKeys()!)
|
||||||
{
|
{
|
||||||
tbMsg.Foreground = new SolidColorBrush(Colors.Green);
|
KeysToDisplay.Add(key);
|
||||||
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success");
|
|
||||||
}
|
}
|
||||||
tbMsg.Visibility = Visibility.Visible;
|
}
|
||||||
|
|
||||||
|
public void SetHotkey(string? keyStr, bool triggerValidate = true)
|
||||||
|
{
|
||||||
|
SetHotkey(new HotkeyModel(keyStr), triggerValidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
169
Flow.Launcher/HotkeyControlDialog.xaml
Normal file
169
Flow.Launcher/HotkeyControlDialog.xaml
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
<ui:ContentDialog
|
||||||
|
x:Class="Flow.Launcher.HotkeyControlDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
Background="{DynamicResource PopuBGColor}"
|
||||||
|
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||||
|
BorderThickness="0,1,0,0"
|
||||||
|
CornerRadius="8"
|
||||||
|
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||||
|
Foreground="{DynamicResource PopupTextColor}"
|
||||||
|
PreviewKeyDown="OnPreviewKeyDown"
|
||||||
|
Style="{DynamicResource ContentDialog}">
|
||||||
|
<ui:ContentDialog.Resources>
|
||||||
|
<Thickness x:Key="ContentDialogPadding">0</Thickness>
|
||||||
|
<Thickness x:Key="ContentDialogTitleMargin">0</Thickness>
|
||||||
|
</ui:ContentDialog.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="100" />
|
||||||
|
<RowDefinition Height="80" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Window title and the keys in the hotkey -->
|
||||||
|
<Grid Grid.Row="0" Margin="26,12,26,0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,0,0,0"
|
||||||
|
FontSize="20"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{Binding WindowTitle}"
|
||||||
|
TextAlignment="Left" />
|
||||||
|
<TextBlock FontSize="14" Text="{DynamicResource hotkeyRegGuide}" />
|
||||||
|
</StackPanel>
|
||||||
|
<Border
|
||||||
|
Grid.Row="1"
|
||||||
|
Width="450"
|
||||||
|
Height="100"
|
||||||
|
Margin="0,100,0,0"
|
||||||
|
Padding="26,12,26,0">
|
||||||
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<ItemsControl ItemsSource="{Binding KeysToDisplay}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border
|
||||||
|
MinWidth="50"
|
||||||
|
MinHeight="50"
|
||||||
|
Margin="5,0,5,0"
|
||||||
|
Padding="8"
|
||||||
|
Background="{DynamicResource AccentButtonBackground}"
|
||||||
|
CornerRadius="6">
|
||||||
|
<TextBlock
|
||||||
|
Margin="5,0,5,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{DynamicResource Color01B}"
|
||||||
|
Text="{Binding}" />
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Warning message for when something went wrong with the new hotkey. -->
|
||||||
|
<Border Grid.Row="1">
|
||||||
|
|
||||||
|
<Border
|
||||||
|
x:Name="Alert"
|
||||||
|
Width="420"
|
||||||
|
Padding="0, 10"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Background="{DynamicResource InfoBarWarningBG}"
|
||||||
|
BorderBrush="{DynamicResource InfoBarBD}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="5"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<Grid VerticalAlignment="Center">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ui:FontIcon
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="20,0,14,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="15"
|
||||||
|
Foreground="{DynamicResource InfoBarWarningIcon}"
|
||||||
|
Glyph="" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
x:Name="tbMsg"
|
||||||
|
Margin="0,0,0,2"
|
||||||
|
Padding="0,0,8,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
FontSize="13"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource Color05B}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Action buttons at the bottom of the dialog -->
|
||||||
|
<Border
|
||||||
|
Grid.Row="2"
|
||||||
|
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||||
|
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||||
|
BorderThickness="0,1,0,0"
|
||||||
|
CornerRadius="0 0 8 8">
|
||||||
|
<StackPanel
|
||||||
|
Margin="10"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Button
|
||||||
|
x:Name="OverwriteBtn"
|
||||||
|
Height="30"
|
||||||
|
MinWidth="100"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
Click="Overwrite"
|
||||||
|
Content="{DynamicResource commonOverwrite}"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
<Button
|
||||||
|
x:Name="SaveBtn"
|
||||||
|
Height="30"
|
||||||
|
MinWidth="100"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
Click="Save"
|
||||||
|
Content="{DynamicResource commonSave}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
<Button
|
||||||
|
Height="30"
|
||||||
|
MinWidth="100"
|
||||||
|
Margin="4,0,4,0"
|
||||||
|
Click="Reset"
|
||||||
|
Content="{DynamicResource commonReset}" />
|
||||||
|
<Button
|
||||||
|
Height="30"
|
||||||
|
MinWidth="100"
|
||||||
|
Margin="4,0,4,0"
|
||||||
|
Click="Delete"
|
||||||
|
Content="{DynamicResource commonDelete}" />
|
||||||
|
<Button
|
||||||
|
Height="30"
|
||||||
|
MinWidth="100"
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
Click="Cancel"
|
||||||
|
Content="{DynamicResource commonCancel}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</ui:ContentDialog>
|
||||||
179
Flow.Launcher/HotkeyControlDialog.xaml.cs
Normal file
179
Flow.Launcher/HotkeyControlDialog.xaml.cs
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Flow.Launcher.Core.Resource;
|
||||||
|
using Flow.Launcher.Helper;
|
||||||
|
using Flow.Launcher.Infrastructure.Hotkey;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using ModernWpf.Controls;
|
||||||
|
|
||||||
|
namespace Flow.Launcher;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public partial class HotkeyControlDialog : ContentDialog
|
||||||
|
{
|
||||||
|
private IHotkeySettings _hotkeySettings;
|
||||||
|
private Action? _overwriteOtherHotkey;
|
||||||
|
private string DefaultHotkey { get; }
|
||||||
|
public string WindowTitle { get; }
|
||||||
|
public HotkeyModel CurrentHotkey { get; private set; }
|
||||||
|
public ObservableCollection<string> KeysToDisplay { get; } = new();
|
||||||
|
|
||||||
|
public enum EResultType
|
||||||
|
{
|
||||||
|
Cancel,
|
||||||
|
Save,
|
||||||
|
Delete
|
||||||
|
}
|
||||||
|
|
||||||
|
public EResultType ResultType { get; private set; } = EResultType.Cancel;
|
||||||
|
public string ResultValue { get; private set; } = string.Empty;
|
||||||
|
public static string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none");
|
||||||
|
|
||||||
|
public HotkeyControlDialog(string hotkey, string defaultHotkey, IHotkeySettings hotkeySettings, string windowTitle = "")
|
||||||
|
{
|
||||||
|
WindowTitle = windowTitle switch
|
||||||
|
{
|
||||||
|
"" or null => InternationalizationManager.Instance.GetTranslation("hotkeyRegTitle"),
|
||||||
|
_ => windowTitle
|
||||||
|
};
|
||||||
|
DefaultHotkey = defaultHotkey;
|
||||||
|
CurrentHotkey = new HotkeyModel(hotkey);
|
||||||
|
_hotkeySettings = hotkeySettings;
|
||||||
|
SetKeysToDisplay(CurrentHotkey);
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reset(object sender, RoutedEventArgs routedEventArgs)
|
||||||
|
{
|
||||||
|
SetKeysToDisplay(new HotkeyModel(DefaultHotkey));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Delete(object sender, RoutedEventArgs routedEventArgs)
|
||||||
|
{
|
||||||
|
KeysToDisplay.Clear();
|
||||||
|
KeysToDisplay.Add(EmptyHotkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel(object sender, RoutedEventArgs routedEventArgs)
|
||||||
|
{
|
||||||
|
ResultType = EResultType.Cancel;
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save(object sender, RoutedEventArgs routedEventArgs)
|
||||||
|
{
|
||||||
|
if (KeysToDisplay.Count == 1 && KeysToDisplay[0] == EmptyHotkey)
|
||||||
|
{
|
||||||
|
ResultType = EResultType.Delete;
|
||||||
|
Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ResultType = EResultType.Save;
|
||||||
|
ResultValue = string.Join("+", KeysToDisplay);
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
|
||||||
|
//when alt is pressed, the real key should be e.SystemKey
|
||||||
|
Key key = e.Key == Key.System ? e.SystemKey : e.Key;
|
||||||
|
|
||||||
|
SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers();
|
||||||
|
|
||||||
|
var hotkeyModel = new HotkeyModel(
|
||||||
|
specialKeyState.AltPressed,
|
||||||
|
specialKeyState.ShiftPressed,
|
||||||
|
specialKeyState.WinPressed,
|
||||||
|
specialKeyState.CtrlPressed,
|
||||||
|
key);
|
||||||
|
|
||||||
|
CurrentHotkey = hotkeyModel;
|
||||||
|
SetKeysToDisplay(CurrentHotkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetKeysToDisplay(HotkeyModel? hotkey)
|
||||||
|
{
|
||||||
|
_overwriteOtherHotkey = null;
|
||||||
|
KeysToDisplay.Clear();
|
||||||
|
|
||||||
|
if (hotkey == null || hotkey == default(HotkeyModel))
|
||||||
|
{
|
||||||
|
KeysToDisplay.Add(EmptyHotkey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in hotkey.Value.EnumerateDisplayKeys()!)
|
||||||
|
{
|
||||||
|
KeysToDisplay.Add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tbMsg == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_hotkeySettings.RegisteredHotkeys.FirstOrDefault(v => v.Hotkey == hotkey) is { } registeredHotkeyData)
|
||||||
|
{
|
||||||
|
var description = string.Format(
|
||||||
|
InternationalizationManager.Instance.GetTranslation(registeredHotkeyData.DescriptionResourceKey),
|
||||||
|
registeredHotkeyData.DescriptionFormatVariables
|
||||||
|
);
|
||||||
|
Alert.Visibility = Visibility.Visible;
|
||||||
|
if (registeredHotkeyData.RemoveHotkey is not null)
|
||||||
|
{
|
||||||
|
tbMsg.Text = string.Format(
|
||||||
|
InternationalizationManager.Instance.GetTranslation("hotkeyUnavailableEditable"),
|
||||||
|
description
|
||||||
|
);
|
||||||
|
SaveBtn.IsEnabled = false;
|
||||||
|
SaveBtn.Visibility = Visibility.Collapsed;
|
||||||
|
OverwriteBtn.IsEnabled = true;
|
||||||
|
OverwriteBtn.Visibility = Visibility.Visible;
|
||||||
|
_overwriteOtherHotkey = registeredHotkeyData.RemoveHotkey;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tbMsg.Text = string.Format(
|
||||||
|
InternationalizationManager.Instance.GetTranslation("hotkeyUnavailableUneditable"),
|
||||||
|
description
|
||||||
|
);
|
||||||
|
SaveBtn.IsEnabled = false;
|
||||||
|
SaveBtn.Visibility = Visibility.Visible;
|
||||||
|
OverwriteBtn.IsEnabled = false;
|
||||||
|
OverwriteBtn.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OverwriteBtn.IsEnabled = false;
|
||||||
|
OverwriteBtn.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
if (!CheckHotkeyAvailability(hotkey.Value, true))
|
||||||
|
{
|
||||||
|
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable");
|
||||||
|
Alert.Visibility = Visibility.Visible;
|
||||||
|
SaveBtn.IsEnabled = false;
|
||||||
|
SaveBtn.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Alert.Visibility = Visibility.Collapsed;
|
||||||
|
SaveBtn.IsEnabled = true;
|
||||||
|
SaveBtn.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
|
||||||
|
hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
|
||||||
|
|
||||||
|
private void Overwrite(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_overwriteOtherHotkey?.Invoke();
|
||||||
|
Save(sender, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Flow.Launcher/Images/info.png
Normal file
BIN
Flow.Launcher/Images/info.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Flow.Launcher/Images/keyboard.png
Normal file
BIN
Flow.Launcher/Images/keyboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 873 B |
BIN
Flow.Launcher/Images/plugins.png
Normal file
BIN
Flow.Launcher/Images/plugins.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 804 B |
BIN
Flow.Launcher/Images/proxy.png
Normal file
BIN
Flow.Launcher/Images/proxy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Flow.Launcher/Images/store.png
Normal file
BIN
Flow.Launcher/Images/store.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 763 B |
BIN
Flow.Launcher/Images/theme.png
Normal file
BIN
Flow.Launcher/Images/theme.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1 KiB |
|
|
@ -154,6 +154,9 @@
|
||||||
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
||||||
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
||||||
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||||
|
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||||
<system:String x:Key="Animation">Animation</system:String>
|
<system:String x:Key="Animation">Animation</system:String>
|
||||||
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
||||||
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
||||||
|
|
@ -168,14 +171,37 @@
|
||||||
<!-- Setting Hotkey -->
|
<!-- Setting Hotkey -->
|
||||||
<system:String x:Key="hotkey">Hotkey</system:String>
|
<system:String x:Key="hotkey">Hotkey</system:String>
|
||||||
<system:String x:Key="hotkeys">Hotkeys</system:String>
|
<system:String x:Key="hotkeys">Hotkeys</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkey">Flow Launcher Hotkey</system:String>
|
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
||||||
<system:String x:Key="previewHotkey">Preview Hotkey</system:String>
|
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||||
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||||
<system:String x:Key="openResultModifiers">Open Result Modifier Key</system:String>
|
<system:String x:Key="openResultModifiers">Open Result Modifier Key</system:String>
|
||||||
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
|
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||||
|
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||||
|
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||||
|
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||||
|
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||||
|
<system:String x:Key="OpenContextMenuHotkey">Open Context Menu</system:String>
|
||||||
|
<system:String x:Key="SettingWindowHotkey">Open Setting Window</system:String>
|
||||||
|
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||||
|
<system:String x:Key="ToggleGameModeHotkey">Toggle Game Mode</system:String>
|
||||||
|
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||||
|
<system:String x:Key="OpenContainFolderHotkey">Open Containing Folder</system:String>
|
||||||
|
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||||
|
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||||
|
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||||
|
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||||
|
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||||
<system:String x:Key="customQueryHotkey">Custom Query Hotkeys</system:String>
|
<system:String x:Key="customQueryHotkey">Custom Query Hotkeys</system:String>
|
||||||
<system:String x:Key="customQueryShortcut">Custom Query Shortcuts</system:String>
|
<system:String x:Key="customQueryShortcut">Custom Query Shortcuts</system:String>
|
||||||
<system:String x:Key="builtinShortcuts">Built-in Shortcuts</system:String>
|
<system:String x:Key="builtinShortcuts">Built-in Shortcuts</system:String>
|
||||||
|
|
@ -186,6 +212,7 @@
|
||||||
<system:String x:Key="delete">Delete</system:String>
|
<system:String x:Key="delete">Delete</system:String>
|
||||||
<system:String x:Key="edit">Edit</system:String>
|
<system:String x:Key="edit">Edit</system:String>
|
||||||
<system:String x:Key="add">Add</system:String>
|
<system:String x:Key="add">Add</system:String>
|
||||||
|
<system:String x:Key="none">None</system:String>
|
||||||
<system:String x:Key="pleaseSelectAnItem">Please select an item</system:String>
|
<system:String x:Key="pleaseSelectAnItem">Please select an item</system:String>
|
||||||
<system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String>
|
<system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String>
|
||||||
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
||||||
|
|
@ -284,6 +311,11 @@
|
||||||
<system:String x:Key="hotkeyIsNotUnavailable">Hotkey is unavailable, please select a new hotkey</system:String>
|
<system:String x:Key="hotkeyIsNotUnavailable">Hotkey is unavailable, please select a new hotkey</system:String>
|
||||||
<system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String>
|
<system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String>
|
||||||
<system:String x:Key="update">Update</system:String>
|
<system:String x:Key="update">Update</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||||
|
|
||||||
<!-- Custom Query Shortcut Dialog -->
|
<!-- Custom Query Shortcut Dialog -->
|
||||||
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
||||||
|
|
@ -295,8 +327,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
||||||
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
||||||
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
||||||
|
|
||||||
<!-- Hotkey Control -->
|
<!-- Common Action -->
|
||||||
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>
|
<system:String x:Key="commonSave">Save</system:String>
|
||||||
|
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||||
|
<system:String x:Key="commonCancel">Cancel</system:String>
|
||||||
|
<system:String x:Key="commonReset">Reset</system:String>
|
||||||
|
<system:String x:Key="commonDelete">Delete</system:String>
|
||||||
|
|
||||||
<!-- Crash Reporter -->
|
<!-- Crash Reporter -->
|
||||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||||
|
|
@ -366,6 +402,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
||||||
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
||||||
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
||||||
|
|
||||||
|
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||||
|
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||||
|
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||||
|
|
||||||
<system:String x:Key="RecommendWeather">Weather</system:String>
|
<system:String x:Key="RecommendWeather">Weather</system:String>
|
||||||
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
||||||
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,9 @@
|
||||||
<system:String x:Key="ColorSchemeDark">Tmavý</system:String>
|
<system:String x:Key="ColorSchemeDark">Tmavý</system:String>
|
||||||
<system:String x:Key="SoundEffect">Zvukový efekt</system:String>
|
<system:String x:Key="SoundEffect">Zvukový efekt</system:String>
|
||||||
<system:String x:Key="SoundEffectTip">Přehrát krátký zvuk při otevření okna vyhledávání</system:String>
|
<system:String x:Key="SoundEffectTip">Přehrát krátký zvuk při otevření okna vyhledávání</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||||
|
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||||
<system:String x:Key="Animation">Animace</system:String>
|
<system:String x:Key="Animation">Animace</system:String>
|
||||||
<system:String x:Key="AnimationTip">Použít animaci v UI</system:String>
|
<system:String x:Key="AnimationTip">Použít animaci v UI</system:String>
|
||||||
<system:String x:Key="AnimationSpeed">Rychlost animace</system:String>
|
<system:String x:Key="AnimationSpeed">Rychlost animace</system:String>
|
||||||
|
|
@ -168,14 +171,37 @@
|
||||||
<!-- Setting Hotkey -->
|
<!-- Setting Hotkey -->
|
||||||
<system:String x:Key="hotkey">Klávesová zkratka</system:String>
|
<system:String x:Key="hotkey">Klávesová zkratka</system:String>
|
||||||
<system:String x:Key="hotkeys">Klávesové zkratky</system:String>
|
<system:String x:Key="hotkeys">Klávesové zkratky</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkey">Klávesová zkratka pro Flow Launcher</system:String>
|
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkeyToolTip">Zadejte zkratku pro zobrazení/skrytí nástroje Flow Launcher.</system:String>
|
<system:String x:Key="flowlauncherHotkeyToolTip">Zadejte zkratku pro zobrazení/skrytí nástroje Flow Launcher.</system:String>
|
||||||
<system:String x:Key="previewHotkey">Klávesová zkratka pro náhled</system:String>
|
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||||
<system:String x:Key="previewHotkeyToolTip">Zadejte klávesovou zkratku pro zobrazení/skrytí náhledu v okně vyhledávání.</system:String>
|
<system:String x:Key="previewHotkeyToolTip">Zadejte klávesovou zkratku pro zobrazení/skrytí náhledu v okně vyhledávání.</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||||
<system:String x:Key="openResultModifiers">Modifikační klávesa pro otevření výsledků</system:String>
|
<system:String x:Key="openResultModifiers">Modifikační klávesa pro otevření výsledků</system:String>
|
||||||
<system:String x:Key="openResultModifiersToolTip">Výběrem modifikační klávesy otevřete vybraný výsledek pomocí klávesnice.</system:String>
|
<system:String x:Key="openResultModifiersToolTip">Výběrem modifikační klávesy otevřete vybraný výsledek pomocí klávesnice.</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkey">Zobrazit klávesovou zkratku</system:String>
|
<system:String x:Key="showOpenResultHotkey">Zobrazit klávesovou zkratku</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkeyToolTip">Zobrazí klávesovou zkratku spolu s výsledky.</system:String>
|
<system:String x:Key="showOpenResultHotkeyToolTip">Zobrazí klávesovou zkratku spolu s výsledky.</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||||
|
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||||
|
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||||
|
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||||
|
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||||
|
<system:String x:Key="OpenContextMenuHotkey">Otevřít kontextovou nabídku</system:String>
|
||||||
|
<system:String x:Key="SettingWindowHotkey">Otevřít okno s nastavením</system:String>
|
||||||
|
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||||
|
<system:String x:Key="ToggleGameModeHotkey">Toggle Game Mode</system:String>
|
||||||
|
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||||
|
<system:String x:Key="OpenContainFolderHotkey">Otevřít umístění složky</system:String>
|
||||||
|
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||||
|
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||||
|
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||||
|
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||||
|
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||||
<system:String x:Key="customQueryHotkey">Vlastní klávesové zkratky pro vyhledávání</system:String>
|
<system:String x:Key="customQueryHotkey">Vlastní klávesové zkratky pro vyhledávání</system:String>
|
||||||
<system:String x:Key="customQueryShortcut">Vlastní zkratky dotazů</system:String>
|
<system:String x:Key="customQueryShortcut">Vlastní zkratky dotazů</system:String>
|
||||||
<system:String x:Key="builtinShortcuts">Vestavěné zkratky</system:String>
|
<system:String x:Key="builtinShortcuts">Vestavěné zkratky</system:String>
|
||||||
|
|
@ -186,6 +212,7 @@
|
||||||
<system:String x:Key="delete">Smazat</system:String>
|
<system:String x:Key="delete">Smazat</system:String>
|
||||||
<system:String x:Key="edit">Editovat</system:String>
|
<system:String x:Key="edit">Editovat</system:String>
|
||||||
<system:String x:Key="add">Přidat</system:String>
|
<system:String x:Key="add">Přidat</system:String>
|
||||||
|
<system:String x:Key="none">None</system:String>
|
||||||
<system:String x:Key="pleaseSelectAnItem">Vyberte prosím položku</system:String>
|
<system:String x:Key="pleaseSelectAnItem">Vyberte prosím položku</system:String>
|
||||||
<system:String x:Key="deleteCustomHotkeyWarning">Jste si jisti, že chcete odstranit klávesovou zkratku {0} pro plugin?</system:String>
|
<system:String x:Key="deleteCustomHotkeyWarning">Jste si jisti, že chcete odstranit klávesovou zkratku {0} pro plugin?</system:String>
|
||||||
<system:String x:Key="deleteCustomShortcutWarning">Opravdu chcete odstranit zástupce: {0} pro dotaz {1}?</system:String>
|
<system:String x:Key="deleteCustomShortcutWarning">Opravdu chcete odstranit zástupce: {0} pro dotaz {1}?</system:String>
|
||||||
|
|
@ -284,6 +311,11 @@
|
||||||
<system:String x:Key="hotkeyIsNotUnavailable">Klávesová zkratka je nedostupná, zadejte prosím novou zkratku</system:String>
|
<system:String x:Key="hotkeyIsNotUnavailable">Klávesová zkratka je nedostupná, zadejte prosím novou zkratku</system:String>
|
||||||
<system:String x:Key="invalidPluginHotkey">Neplatná klávesová zkratka pluginu</system:String>
|
<system:String x:Key="invalidPluginHotkey">Neplatná klávesová zkratka pluginu</system:String>
|
||||||
<system:String x:Key="update">Aktualizovat</system:String>
|
<system:String x:Key="update">Aktualizovat</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||||
|
|
||||||
<!-- Custom Query Shortcut Dialog -->
|
<!-- Custom Query Shortcut Dialog -->
|
||||||
<system:String x:Key="customeQueryShortcutTitle">Vlastní klávesová zkratka pro zadávání dotazů</system:String>
|
<system:String x:Key="customeQueryShortcutTitle">Vlastní klávesová zkratka pro zadávání dotazů</system:String>
|
||||||
|
|
@ -295,8 +327,12 @@ Pokud před zkratku při zadávání přidáte znak "@", bude odpovíd
|
||||||
<system:String x:Key="duplicateShortcut">Zkratka již existuje, zadejte novou zkratku nebo upravte stávající.</system:String>
|
<system:String x:Key="duplicateShortcut">Zkratka již existuje, zadejte novou zkratku nebo upravte stávající.</system:String>
|
||||||
<system:String x:Key="emptyShortcut">Zkratka a/nebo její plné znění je prázdné.</system:String>
|
<system:String x:Key="emptyShortcut">Zkratka a/nebo její plné znění je prázdné.</system:String>
|
||||||
|
|
||||||
<!-- Hotkey Control -->
|
<!-- Common Action -->
|
||||||
<system:String x:Key="hotkeyUnavailable">Klávesová zkratka je nedostupná</system:String>
|
<system:String x:Key="commonSave">Uložit</system:String>
|
||||||
|
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||||
|
<system:String x:Key="commonCancel">Zrušit</system:String>
|
||||||
|
<system:String x:Key="commonReset">Reset</system:String>
|
||||||
|
<system:String x:Key="commonDelete">Smazat</system:String>
|
||||||
|
|
||||||
<!-- Crash Reporter -->
|
<!-- Crash Reporter -->
|
||||||
<system:String x:Key="reportWindow_version">Verze</system:String>
|
<system:String x:Key="reportWindow_version">Verze</system:String>
|
||||||
|
|
@ -366,6 +402,12 @@ Pokud před zkratku při zadávání přidáte znak "@", bude odpovíd
|
||||||
<system:String x:Key="HotkeyCtrlIDesc">Otevřít okno s nastavením</system:String>
|
<system:String x:Key="HotkeyCtrlIDesc">Otevřít okno s nastavením</system:String>
|
||||||
<system:String x:Key="HotkeyF5Desc">Znovu načíst data pluginů</system:String>
|
<system:String x:Key="HotkeyF5Desc">Znovu načíst data pluginů</system:String>
|
||||||
|
|
||||||
|
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||||
|
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||||
|
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||||
|
|
||||||
<system:String x:Key="RecommendWeather">Počasí</system:String>
|
<system:String x:Key="RecommendWeather">Počasí</system:String>
|
||||||
<system:String x:Key="RecommendWeatherDesc">Výsledky počasí Google</system:String>
|
<system:String x:Key="RecommendWeatherDesc">Výsledky počasí Google</system:String>
|
||||||
<system:String x:Key="RecommendShell">> ping 8.8.8</system:String>
|
<system:String x:Key="RecommendShell">> ping 8.8.8</system:String>
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,9 @@
|
||||||
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
||||||
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
||||||
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||||
|
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||||
<system:String x:Key="Animation">Animation</system:String>
|
<system:String x:Key="Animation">Animation</system:String>
|
||||||
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
||||||
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
||||||
|
|
@ -168,14 +171,37 @@
|
||||||
<!-- Setting Hotkey -->
|
<!-- Setting Hotkey -->
|
||||||
<system:String x:Key="hotkey">Genvejstast</system:String>
|
<system:String x:Key="hotkey">Genvejstast</system:String>
|
||||||
<system:String x:Key="hotkeys">Genvejstast</system:String>
|
<system:String x:Key="hotkeys">Genvejstast</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkey">Flow Launcher genvejstast</system:String>
|
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
||||||
<system:String x:Key="previewHotkey">Preview Hotkey</system:String>
|
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||||
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||||
<system:String x:Key="openResultModifiers">Åbn resultatmodifikatorer</system:String>
|
<system:String x:Key="openResultModifiers">Åbn resultatmodifikatorer</system:String>
|
||||||
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkey">Vis hotkey</system:String>
|
<system:String x:Key="showOpenResultHotkey">Vis hotkey</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||||
|
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||||
|
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||||
|
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||||
|
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||||
|
<system:String x:Key="OpenContextMenuHotkey">Open Context Menu</system:String>
|
||||||
|
<system:String x:Key="SettingWindowHotkey">Open Setting Window</system:String>
|
||||||
|
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||||
|
<system:String x:Key="ToggleGameModeHotkey">Toggle Game Mode</system:String>
|
||||||
|
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||||
|
<system:String x:Key="OpenContainFolderHotkey">Open Containing Folder</system:String>
|
||||||
|
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||||
|
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||||
|
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||||
|
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||||
|
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||||
<system:String x:Key="customQueryHotkey">Tilpasset søgegenvejstast</system:String>
|
<system:String x:Key="customQueryHotkey">Tilpasset søgegenvejstast</system:String>
|
||||||
<system:String x:Key="customQueryShortcut">Custom Query Shortcut</system:String>
|
<system:String x:Key="customQueryShortcut">Custom Query Shortcut</system:String>
|
||||||
<system:String x:Key="builtinShortcuts">Built-in Shortcut</system:String>
|
<system:String x:Key="builtinShortcuts">Built-in Shortcut</system:String>
|
||||||
|
|
@ -186,6 +212,7 @@
|
||||||
<system:String x:Key="delete">Slet</system:String>
|
<system:String x:Key="delete">Slet</system:String>
|
||||||
<system:String x:Key="edit">Rediger</system:String>
|
<system:String x:Key="edit">Rediger</system:String>
|
||||||
<system:String x:Key="add">Tilføj</system:String>
|
<system:String x:Key="add">Tilføj</system:String>
|
||||||
|
<system:String x:Key="none">None</system:String>
|
||||||
<system:String x:Key="pleaseSelectAnItem">Vælg venligst</system:String>
|
<system:String x:Key="pleaseSelectAnItem">Vælg venligst</system:String>
|
||||||
<system:String x:Key="deleteCustomHotkeyWarning">Er du sikker på du vil slette {0} plugin genvejstast?</system:String>
|
<system:String x:Key="deleteCustomHotkeyWarning">Er du sikker på du vil slette {0} plugin genvejstast?</system:String>
|
||||||
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
||||||
|
|
@ -284,6 +311,11 @@
|
||||||
<system:String x:Key="hotkeyIsNotUnavailable">Genvejstast er utilgængelig, vælg venligst en ny genvejstast</system:String>
|
<system:String x:Key="hotkeyIsNotUnavailable">Genvejstast er utilgængelig, vælg venligst en ny genvejstast</system:String>
|
||||||
<system:String x:Key="invalidPluginHotkey">Ugyldig plugin genvejstast</system:String>
|
<system:String x:Key="invalidPluginHotkey">Ugyldig plugin genvejstast</system:String>
|
||||||
<system:String x:Key="update">Opdater</system:String>
|
<system:String x:Key="update">Opdater</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||||
|
|
||||||
<!-- Custom Query Shortcut Dialog -->
|
<!-- Custom Query Shortcut Dialog -->
|
||||||
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
||||||
|
|
@ -295,8 +327,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
||||||
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
||||||
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
||||||
|
|
||||||
<!-- Hotkey Control -->
|
<!-- Common Action -->
|
||||||
<system:String x:Key="hotkeyUnavailable">Genvejstast utilgængelig</system:String>
|
<system:String x:Key="commonSave">Gem</system:String>
|
||||||
|
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||||
|
<system:String x:Key="commonCancel">Annuller</system:String>
|
||||||
|
<system:String x:Key="commonReset">Reset</system:String>
|
||||||
|
<system:String x:Key="commonDelete">Slet</system:String>
|
||||||
|
|
||||||
<!-- Crash Reporter -->
|
<!-- Crash Reporter -->
|
||||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||||
|
|
@ -366,6 +402,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
||||||
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
||||||
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
||||||
|
|
||||||
|
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||||
|
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||||
|
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||||
|
|
||||||
<system:String x:Key="RecommendWeather">Weather</system:String>
|
<system:String x:Key="RecommendWeather">Weather</system:String>
|
||||||
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
||||||
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
<system:String x:Key="pluginStore">Erweiterungen laden</system:String>
|
<system:String x:Key="pluginStore">Erweiterungen laden</system:String>
|
||||||
<system:String x:Key="pluginStore_NewRelease">New Release</system:String>
|
<system:String x:Key="pluginStore_NewRelease">New Release</system:String>
|
||||||
<system:String x:Key="pluginStore_RecentlyUpdated">Recently Updated</system:String>
|
<system:String x:Key="pluginStore_RecentlyUpdated">Recently Updated</system:String>
|
||||||
<system:String x:Key="pluginStore_None">Plugins</system:String>
|
<system:String x:Key="pluginStore_None">Erweiterungen</system:String>
|
||||||
<system:String x:Key="pluginStore_Installed">Installed</system:String>
|
<system:String x:Key="pluginStore_Installed">Installed</system:String>
|
||||||
<system:String x:Key="refresh">Aktualisieren</system:String>
|
<system:String x:Key="refresh">Aktualisieren</system:String>
|
||||||
<system:String x:Key="installbtn">Installieren</system:String>
|
<system:String x:Key="installbtn">Installieren</system:String>
|
||||||
|
|
@ -154,6 +154,9 @@
|
||||||
<system:String x:Key="ColorSchemeDark">Dunkel</system:String>
|
<system:String x:Key="ColorSchemeDark">Dunkel</system:String>
|
||||||
<system:String x:Key="SoundEffect">Soundeffekt</system:String>
|
<system:String x:Key="SoundEffect">Soundeffekt</system:String>
|
||||||
<system:String x:Key="SoundEffectTip">Ton abspielen, wenn das Suchfenster geöffnet wird</system:String>
|
<system:String x:Key="SoundEffectTip">Ton abspielen, wenn das Suchfenster geöffnet wird</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||||
|
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||||
<system:String x:Key="Animation">Animation</system:String>
|
<system:String x:Key="Animation">Animation</system:String>
|
||||||
<system:String x:Key="AnimationTip">Animationen in der Oberfläche verwenden</system:String>
|
<system:String x:Key="AnimationTip">Animationen in der Oberfläche verwenden</system:String>
|
||||||
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
||||||
|
|
@ -168,35 +171,59 @@
|
||||||
<!-- Setting Hotkey -->
|
<!-- Setting Hotkey -->
|
||||||
<system:String x:Key="hotkey">Tastenkombination</system:String>
|
<system:String x:Key="hotkey">Tastenkombination</system:String>
|
||||||
<system:String x:Key="hotkeys">Tastenkombination</system:String>
|
<system:String x:Key="hotkeys">Tastenkombination</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkey">Flow Launcher Tastenkombination</system:String>
|
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkeyToolTip">Verknüpfung eingeben, um Flow Launcher anzuzeigen/auszublenden.</system:String>
|
<system:String x:Key="flowlauncherHotkeyToolTip">Verknüpfung eingeben, um Flow Launcher anzuzeigen/auszublenden.</system:String>
|
||||||
<system:String x:Key="previewHotkey">Preview Hotkey</system:String>
|
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||||
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||||
<system:String x:Key="openResultModifiers">Öffnen Sie die Ergebnismodifikatoren</system:String>
|
<system:String x:Key="openResultModifiers">Öffnen Sie die Ergebnismodifikatoren</system:String>
|
||||||
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
<system:String x:Key="openResultModifiersToolTip">Wählen Sie eine Modifikatortaste, um das ausgewählte Ergebnis über die Tastatur zu öffnen.</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkey">Hotkey anzeigen</system:String>
|
<system:String x:Key="showOpenResultHotkey">Hotkey anzeigen</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
<system:String x:Key="showOpenResultHotkeyToolTip">Hotkey für die Ergebnisauswahl mit Ergebnissen anzeigen.</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||||
|
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||||
|
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||||
|
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||||
|
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||||
|
<system:String x:Key="OpenContextMenuHotkey">Kontextmenü öffnen</system:String>
|
||||||
|
<system:String x:Key="SettingWindowHotkey">Einstellungsfenster öffnen</system:String>
|
||||||
|
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||||
|
<system:String x:Key="ToggleGameModeHotkey">Gott Modus</system:String>
|
||||||
|
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||||
|
<system:String x:Key="OpenContainFolderHotkey">Öffne beinhaltenden Ordner</system:String>
|
||||||
|
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||||
|
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||||
|
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||||
|
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||||
|
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||||
<system:String x:Key="customQueryHotkey">Benutzerdefinierte Abfrage Tastenkombination</system:String>
|
<system:String x:Key="customQueryHotkey">Benutzerdefinierte Abfrage Tastenkombination</system:String>
|
||||||
<system:String x:Key="customQueryShortcut">Custom Query Shortcut</system:String>
|
<system:String x:Key="customQueryShortcut">Custom Query Shortcut</system:String>
|
||||||
<system:String x:Key="builtinShortcuts">Built-in Shortcut</system:String>
|
<system:String x:Key="builtinShortcuts">Built-in Shortcut</system:String>
|
||||||
<system:String x:Key="customQuery">Query</system:String>
|
<system:String x:Key="customQuery">Abfrage</system:String>
|
||||||
<system:String x:Key="customShortcut">Shortcut</system:String>
|
<system:String x:Key="customShortcut">Shortcut</system:String>
|
||||||
<system:String x:Key="customShortcutExpansion">Expansion</system:String>
|
<system:String x:Key="customShortcutExpansion">Expansion</system:String>
|
||||||
<system:String x:Key="builtinShortcutDescription">Beschreibung</system:String>
|
<system:String x:Key="builtinShortcutDescription">Beschreibung</system:String>
|
||||||
<system:String x:Key="delete">Löschen</system:String>
|
<system:String x:Key="delete">Löschen</system:String>
|
||||||
<system:String x:Key="edit">Bearbeiten</system:String>
|
<system:String x:Key="edit">Bearbeiten</system:String>
|
||||||
<system:String x:Key="add">Hinzufügen</system:String>
|
<system:String x:Key="add">Hinzufügen</system:String>
|
||||||
|
<system:String x:Key="none">None</system:String>
|
||||||
<system:String x:Key="pleaseSelectAnItem">Bitte einen Eintrag auswählen</system:String>
|
<system:String x:Key="pleaseSelectAnItem">Bitte einen Eintrag auswählen</system:String>
|
||||||
<system:String x:Key="deleteCustomHotkeyWarning">Wollen Sie die {0} Plugin Tastenkombination wirklich löschen?</system:String>
|
<system:String x:Key="deleteCustomHotkeyWarning">Wollen Sie die {0} Plugin Tastenkombination wirklich löschen?</system:String>
|
||||||
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
||||||
<system:String x:Key="shortcut_clipboard_description">Get text from clipboard.</system:String>
|
<system:String x:Key="shortcut_clipboard_description">Get text from clipboard.</system:String>
|
||||||
<system:String x:Key="shortcut_active_explorer_path">Get path from active explorer.</system:String>
|
<system:String x:Key="shortcut_active_explorer_path">Get path from active explorer.</system:String>
|
||||||
<system:String x:Key="queryWindowShadowEffect">Query window shadow effect</system:String>
|
<system:String x:Key="queryWindowShadowEffect">Schatteneffekt im Abfragefenster</system:String>
|
||||||
<system:String x:Key="shadowEffectCPUUsage">Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited.</system:String>
|
<system:String x:Key="shadowEffectCPUUsage">Der Schatteneffekt beansprucht die GPU stark. Nicht empfohlen, wenn die Leistung deines Computers begrenzt ist.</system:String>
|
||||||
<system:String x:Key="windowWidthSize">Window Width Size</system:String>
|
<system:String x:Key="windowWidthSize">Fenstergröße Breite</system:String>
|
||||||
<system:String x:Key="windowWidthSizeToolTip">You can also quickly adjust this by using Ctrl+[ and Ctrl+].</system:String>
|
<system:String x:Key="windowWidthSizeToolTip">You can also quickly adjust this by using Ctrl+[ and Ctrl+].</system:String>
|
||||||
<system:String x:Key="useGlyphUI">Use Segoe Fluent Icons</system:String>
|
<system:String x:Key="useGlyphUI">Segoe Fluent Icons verwenden</system:String>
|
||||||
<system:String x:Key="useGlyphUIEffect">Use Segoe Fluent Icons for query results where supported</system:String>
|
<system:String x:Key="useGlyphUIEffect">Segoe Fluent Icons für Abfrageergebnisse verwenden, sofern unterstützt</system:String>
|
||||||
<system:String x:Key="flowlauncherPressHotkey">Press Key</system:String>
|
<system:String x:Key="flowlauncherPressHotkey">Press Key</system:String>
|
||||||
|
|
||||||
<!-- Setting Proxy -->
|
<!-- Setting Proxy -->
|
||||||
|
|
@ -226,15 +253,15 @@
|
||||||
<system:String x:Key="checkUpdates">Nach Aktuallisierungen Suchen</system:String>
|
<system:String x:Key="checkUpdates">Nach Aktuallisierungen Suchen</system:String>
|
||||||
<system:String x:Key="BecomeASponsor">Become A Sponsor</system:String>
|
<system:String x:Key="BecomeASponsor">Become A Sponsor</system:String>
|
||||||
<system:String x:Key="newVersionTips">Eine neue Version ({0}) ist vorhanden. Bitte starten Sie Flow Launcher neu.</system:String>
|
<system:String x:Key="newVersionTips">Eine neue Version ({0}) ist vorhanden. Bitte starten Sie Flow Launcher neu.</system:String>
|
||||||
<system:String x:Key="checkUpdatesFailed">Check updates failed, please check your connection and proxy settings to api.github.com.</system:String>
|
<system:String x:Key="checkUpdatesFailed">Überprüfung der Updates fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindungs- und Proxy-Einstellungen zu api.github.com.</system:String>
|
||||||
<system:String x:Key="downloadUpdatesFailed">
|
<system:String x:Key="downloadUpdatesFailed">
|
||||||
Download updates failed, please check your connection and proxy settings to github-cloud.s3.amazonaws.com,
|
Das Herunterladen von Updates ist fehlgeschlagen. Bitte überprüfen Sie Ihre Verbindungs- und Proxy-Einstellungen zu github-cloud.s3.amazonaws.com,
|
||||||
or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually.
|
oder gehen Sie zu https://github.com/Flow-Launcher/Flow.Launcher/releases, um Updates manuell herunterzuladen.
|
||||||
</system:String>
|
</system:String>
|
||||||
<system:String x:Key="releaseNotes">Versionshinweise</system:String>
|
<system:String x:Key="releaseNotes">Versionshinweise</system:String>
|
||||||
<system:String x:Key="documentation">Usage Tips</system:String>
|
<system:String x:Key="documentation">Verwendungshinweise</system:String>
|
||||||
<system:String x:Key="devtool">DevTools</system:String>
|
<system:String x:Key="devtool">Entwicklerwerkzeuge</system:String>
|
||||||
<system:String x:Key="settingfolder">Setting Folder</system:String>
|
<system:String x:Key="settingfolder">Einstellungsordner</system:String>
|
||||||
<system:String x:Key="logfolder">Protokoll-Verzeichnis</system:String>
|
<system:String x:Key="logfolder">Protokoll-Verzeichnis</system:String>
|
||||||
<system:String x:Key="clearlogfolder">Protokolldateien leeren</system:String>
|
<system:String x:Key="clearlogfolder">Protokolldateien leeren</system:String>
|
||||||
<system:String x:Key="clearlogfolderMessage">Sind Sie sicher, dass Sie alle Protokolle löschen möchten?</system:String>
|
<system:String x:Key="clearlogfolderMessage">Sind Sie sicher, dass Sie alle Protokolle löschen möchten?</system:String>
|
||||||
|
|
@ -246,13 +273,13 @@
|
||||||
<system:String x:Key="fileManager_tips2">"%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d".</system:String>
|
<system:String x:Key="fileManager_tips2">"%f" is an argument that represent the file path. It is used to emphasize the file/folder name when opening a specific file location in 3rd party file manager. This argument is only available in the "Arg for File" item. If the file manager does not have that function, you can use "%d".</system:String>
|
||||||
<system:String x:Key="fileManager_name">Datei-Manager</system:String>
|
<system:String x:Key="fileManager_name">Datei-Manager</system:String>
|
||||||
<system:String x:Key="fileManager_profile_name">Profilname</system:String>
|
<system:String x:Key="fileManager_profile_name">Profilname</system:String>
|
||||||
<system:String x:Key="fileManager_path">File Manager Path</system:String>
|
<system:String x:Key="fileManager_path">Dateimanager-Pfad</system:String>
|
||||||
<system:String x:Key="fileManager_directory_arg">Argumente für Ordner</system:String>
|
<system:String x:Key="fileManager_directory_arg">Argumente für Ordner</system:String>
|
||||||
<system:String x:Key="fileManager_file_arg">Argumente für Datei</system:String>
|
<system:String x:Key="fileManager_file_arg">Argumente für Datei</system:String>
|
||||||
|
|
||||||
<!-- DefaultBrowser Setting Dialog -->
|
<!-- DefaultBrowser Setting Dialog -->
|
||||||
<system:String x:Key="defaultBrowserTitle">Standard-Webbrowser</system:String>
|
<system:String x:Key="defaultBrowserTitle">Standard-Webbrowser</system:String>
|
||||||
<system:String x:Key="defaultBrowser_tips">The default setting follows the OS default browser setting. If specified separately, flow uses that browser.</system:String>
|
<system:String x:Key="defaultBrowser_tips">Die Standardeinstellung verwendet die Browser-Standardeinstellung des Betriebssystems. Falls angegeben, verwendet Flow diesen Browser.</system:String>
|
||||||
<system:String x:Key="defaultBrowser_name">Browser</system:String>
|
<system:String x:Key="defaultBrowser_name">Browser</system:String>
|
||||||
<system:String x:Key="defaultBrowser_profile_name">Browser-Name</system:String>
|
<system:String x:Key="defaultBrowser_profile_name">Browser-Name</system:String>
|
||||||
<system:String x:Key="defaultBrowser_path">Browserpfad</system:String>
|
<system:String x:Key="defaultBrowser_path">Browserpfad</system:String>
|
||||||
|
|
@ -262,8 +289,8 @@
|
||||||
|
|
||||||
<!-- Priority Setting Dialog -->
|
<!-- Priority Setting Dialog -->
|
||||||
<system:String x:Key="changePriorityWindow">Priorität ändern</system:String>
|
<system:String x:Key="changePriorityWindow">Priorität ändern</system:String>
|
||||||
<system:String x:Key="priority_tips">Greater the number, the higher the result will be ranked. Try setting it as 5. If you want the results to be lower than any other plugin's, provide a negative number</system:String>
|
<system:String x:Key="priority_tips">Je höher die Zahl, desto höher wird das Ergebnis gewertet. Versuchen Sie es mit einer 5. Sollen die Ergebnisse tiefer sein als diejenigen anderer Plugins, verwenden Sie eine negative Zahl</system:String>
|
||||||
<system:String x:Key="invalidPriority">Please provide an valid integer for Priority!</system:String>
|
<system:String x:Key="invalidPriority">Bitte eine gültige Ganzzahl angeben für die Priorität!</system:String>
|
||||||
|
|
||||||
<!-- Action Keyword Setting Dialog -->
|
<!-- Action Keyword Setting Dialog -->
|
||||||
<system:String x:Key="oldActionKeywords">Altes Aktionsschlüsselwort</system:String>
|
<system:String x:Key="oldActionKeywords">Altes Aktionsschlüsselwort</system:String>
|
||||||
|
|
@ -274,7 +301,7 @@
|
||||||
<system:String x:Key="newActionKeywordsCannotBeEmpty">Neues Aktionsschlüsselwort darf nicht leer sein</system:String>
|
<system:String x:Key="newActionKeywordsCannotBeEmpty">Neues Aktionsschlüsselwort darf nicht leer sein</system:String>
|
||||||
<system:String x:Key="newActionKeywordsHasBeenAssigned">Aktionsschlüsselwort ist schon bei einem anderen Plugin in verwendung. Bitte stellen Sie ein anderes Aktionsschlüsselwort ein.</system:String>
|
<system:String x:Key="newActionKeywordsHasBeenAssigned">Aktionsschlüsselwort ist schon bei einem anderen Plugin in verwendung. Bitte stellen Sie ein anderes Aktionsschlüsselwort ein.</system:String>
|
||||||
<system:String x:Key="success">Erfolgreich</system:String>
|
<system:String x:Key="success">Erfolgreich</system:String>
|
||||||
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
|
<system:String x:Key="completedSuccessfully">Erfolgreich abgeschlossen</system:String>
|
||||||
<system:String x:Key="actionkeyword_tips">Benutzen Sie * wenn Sie ein Aktionsschlüsselwort definieren wollen.</system:String>
|
<system:String x:Key="actionkeyword_tips">Benutzen Sie * wenn Sie ein Aktionsschlüsselwort definieren wollen.</system:String>
|
||||||
|
|
||||||
<!-- Custom Query Hotkey Dialog -->
|
<!-- Custom Query Hotkey Dialog -->
|
||||||
|
|
@ -284,6 +311,11 @@
|
||||||
<system:String x:Key="hotkeyIsNotUnavailable">Tastenkombination ist nicht verfügbar, bitte wähle eine andere Tastenkombination</system:String>
|
<system:String x:Key="hotkeyIsNotUnavailable">Tastenkombination ist nicht verfügbar, bitte wähle eine andere Tastenkombination</system:String>
|
||||||
<system:String x:Key="invalidPluginHotkey">Ungültige Plugin Tastenkombination</system:String>
|
<system:String x:Key="invalidPluginHotkey">Ungültige Plugin Tastenkombination</system:String>
|
||||||
<system:String x:Key="update">Aktualisieren</system:String>
|
<system:String x:Key="update">Aktualisieren</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||||
|
|
||||||
<!-- Custom Query Shortcut Dialog -->
|
<!-- Custom Query Shortcut Dialog -->
|
||||||
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
||||||
|
|
@ -295,8 +327,12 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
||||||
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
||||||
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
||||||
|
|
||||||
<!-- Hotkey Control -->
|
<!-- Common Action -->
|
||||||
<system:String x:Key="hotkeyUnavailable">Tastenkombination nicht verfügbar</system:String>
|
<system:String x:Key="commonSave">Speichern</system:String>
|
||||||
|
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||||
|
<system:String x:Key="commonCancel">Abbrechen</system:String>
|
||||||
|
<system:String x:Key="commonReset">Reset</system:String>
|
||||||
|
<system:String x:Key="commonDelete">Löschen</system:String>
|
||||||
|
|
||||||
<!-- Crash Reporter -->
|
<!-- Crash Reporter -->
|
||||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||||
|
|
@ -347,10 +383,10 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
||||||
<system:String x:Key="Welcome_Page2_Text01">Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.</system:String>
|
<system:String x:Key="Welcome_Page2_Text01">Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.</system:String>
|
||||||
<system:String x:Key="Welcome_Page2_Text02">Flow Launcher starts with the hotkey below, go ahead and try it out now. To change it, click on the input and press the desired hotkey on the keyboard.</system:String>
|
<system:String x:Key="Welcome_Page2_Text02">Flow Launcher starts with the hotkey below, go ahead and try it out now. To change it, click on the input and press the desired hotkey on the keyboard.</system:String>
|
||||||
<system:String x:Key="Welcome_Page3_Title">Tastenkombination</system:String>
|
<system:String x:Key="Welcome_Page3_Title">Tastenkombination</system:String>
|
||||||
<system:String x:Key="Welcome_Page4_Title">Action Keyword and Commands</system:String>
|
<system:String x:Key="Welcome_Page4_Title">Aktions-Schlüsselwort und Befehle</system:String>
|
||||||
<system:String x:Key="Welcome_Page4_Text01">Search the web, launch applications or run various functions through Flow Launcher plugins. Certain functions start with an action keyword, and if necessary, they can be used without action keywords. Try the queries below in Flow Launcher.</system:String>
|
<system:String x:Key="Welcome_Page4_Text01">Search the web, launch applications or run various functions through Flow Launcher plugins. Certain functions start with an action keyword, and if necessary, they can be used without action keywords. Try the queries below in Flow Launcher.</system:String>
|
||||||
<system:String x:Key="Welcome_Page5_Title">Let's Start Flow Launcher</system:String>
|
<system:String x:Key="Welcome_Page5_Title">Starte Flow-Launcher</system:String>
|
||||||
<system:String x:Key="Welcome_Page5_Text01">Finished. Enjoy Flow Launcher. Don't forget the hotkey to start :)</system:String>
|
<system:String x:Key="Welcome_Page5_Text01">Fertig. Genießen Sie Flow Launcher. Die Tastenkombination nicht vergessen, um Flow Launcher zu starten :)</system:String>
|
||||||
|
|
||||||
<!-- General Guide & Hotkey -->
|
<!-- General Guide & Hotkey -->
|
||||||
|
|
||||||
|
|
@ -366,13 +402,19 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
|
||||||
<system:String x:Key="HotkeyCtrlIDesc">Einstellungsfenster öffnen</system:String>
|
<system:String x:Key="HotkeyCtrlIDesc">Einstellungsfenster öffnen</system:String>
|
||||||
<system:String x:Key="HotkeyF5Desc">Plugin-Daten neu laden</system:String>
|
<system:String x:Key="HotkeyF5Desc">Plugin-Daten neu laden</system:String>
|
||||||
|
|
||||||
|
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||||
|
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||||
|
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||||
|
|
||||||
<system:String x:Key="RecommendWeather">Wetter</system:String>
|
<system:String x:Key="RecommendWeather">Wetter</system:String>
|
||||||
<system:String x:Key="RecommendWeatherDesc">Wetter in Google-Ergebnis</system:String>
|
<system:String x:Key="RecommendWeatherDesc">Wetter in Google-Ergebnis</system:String>
|
||||||
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
||||||
<system:String x:Key="RecommendShellDesc">Shell-Befehl</system:String>
|
<system:String x:Key="RecommendShellDesc">Shell-Befehl</system:String>
|
||||||
<system:String x:Key="RecommendBluetooth">s Bluetooth</system:String>
|
<system:String x:Key="RecommendBluetooth">s Bluetooth</system:String>
|
||||||
<system:String x:Key="RecommendBluetoothDesc">Bluetooth in Windows-Einstellungen</system:String>
|
<system:String x:Key="RecommendBluetoothDesc">Bluetooth in Windows-Einstellungen</system:String>
|
||||||
<system:String x:Key="RecommendAcronyms">sn</system:String>
|
<system:String x:Key="RecommendAcronyms">nt</system:String>
|
||||||
<system:String x:Key="RecommendAcronymsDesc">Sticky Notes</system:String>
|
<system:String x:Key="RecommendAcronymsDesc">Notizen</system:String>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@
|
||||||
<system:String x:Key="LastQueryPreserved">Preserve Last Query</system:String>
|
<system:String x:Key="LastQueryPreserved">Preserve Last Query</system:String>
|
||||||
<system:String x:Key="LastQuerySelected">Select last Query</system:String>
|
<system:String x:Key="LastQuerySelected">Select last Query</system:String>
|
||||||
<system:String x:Key="LastQueryEmpty">Empty last Query</system:String>
|
<system:String x:Key="LastQueryEmpty">Empty last Query</system:String>
|
||||||
|
<system:String x:Key="KeepMaxResults">Fixed Window Height</system:String>
|
||||||
|
<system:String x:Key="KeepMaxResultsToolTip">The window height will not be resizeable by dragging</system:String>
|
||||||
<system:String x:Key="maxShowResults">Maximum results shown</system:String>
|
<system:String x:Key="maxShowResults">Maximum results shown</system:String>
|
||||||
<system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String>
|
<system:String x:Key="maxShowResultsToolTip">You can also quickly adjust this by using CTRL+Plus and CTRL+Minus.</system:String>
|
||||||
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
|
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
|
||||||
|
|
@ -77,6 +79,9 @@
|
||||||
<system:String x:Key="hideNotifyIconToolTip">When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window.</system:String>
|
<system:String x:Key="hideNotifyIconToolTip">When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window.</system:String>
|
||||||
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String>
|
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String>
|
||||||
<system:String x:Key="querySearchPrecisionToolTip">Changes minimum match score required for results.</system:String>
|
<system:String x:Key="querySearchPrecisionToolTip">Changes minimum match score required for results.</system:String>
|
||||||
|
<system:String x:Key="SearchPrecisionNone">None</system:String>
|
||||||
|
<system:String x:Key="SearchPrecisionLow">Low</system:String>
|
||||||
|
<system:String x:Key="SearchPrecisionRegular">Regular</system:String>
|
||||||
<system:String x:Key="ShouldUsePinyin">Search with Pinyin</system:String>
|
<system:String x:Key="ShouldUsePinyin">Search with Pinyin</system:String>
|
||||||
<system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese.</system:String>
|
<system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese.</system:String>
|
||||||
<system:String x:Key="AlwaysPreview">Always Preview</system:String>
|
<system:String x:Key="AlwaysPreview">Always Preview</system:String>
|
||||||
|
|
@ -142,8 +147,13 @@
|
||||||
<system:String x:Key="SampleSubTitleProgram">Launch programs as admin or a different user</system:String>
|
<system:String x:Key="SampleSubTitleProgram">Launch programs as admin or a different user</system:String>
|
||||||
<system:String x:Key="SampleTitleProcessKiller">ProcessKiller</system:String>
|
<system:String x:Key="SampleTitleProcessKiller">ProcessKiller</system:String>
|
||||||
<system:String x:Key="SampleSubTitleProcessKiller">Terminate unwanted processes</system:String>
|
<system:String x:Key="SampleSubTitleProcessKiller">Terminate unwanted processes</system:String>
|
||||||
|
<system:String x:Key="SearchBarHeight">Search Bar Height</system:String>
|
||||||
|
<system:String x:Key="ItemHeight">Item Height</system:String>
|
||||||
<system:String x:Key="queryBoxFont">Query Box Font</system:String>
|
<system:String x:Key="queryBoxFont">Query Box Font</system:String>
|
||||||
<system:String x:Key="resultItemFont">Result Item Font</system:String>
|
<system:String x:Key="resultItemFont">Result Title Font</system:String>
|
||||||
|
<system:String x:Key="resultSubItemFont">Result Subtitle Font</system:String>
|
||||||
|
<system:String x:Key="resetCustomize">Reset</system:String>
|
||||||
|
<system:String x:Key="CustomizeToolTip">Customize</system:String>
|
||||||
<system:String x:Key="windowMode">Window Mode</system:String>
|
<system:String x:Key="windowMode">Window Mode</system:String>
|
||||||
<system:String x:Key="opacity">Opacity</system:String>
|
<system:String x:Key="opacity">Opacity</system:String>
|
||||||
<system:String x:Key="theme_load_failure_path_not_exists">Theme {0} not exists, fallback to default theme</system:String>
|
<system:String x:Key="theme_load_failure_path_not_exists">Theme {0} not exists, fallback to default theme</system:String>
|
||||||
|
|
@ -156,6 +166,9 @@
|
||||||
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
<system:String x:Key="ColorSchemeDark">Dark</system:String>
|
||||||
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
<system:String x:Key="SoundEffect">Sound Effect</system:String>
|
||||||
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
<system:String x:Key="SoundEffectTip">Play a small sound when the search window opens</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolume">Sound Effect Volume</system:String>
|
||||||
|
<system:String x:Key="SoundEffectVolumeTip">Adjust the volume of the sound effect</system:String>
|
||||||
|
<system:String x:Key="SoundEffectWarning">Windows Media Player is unavailable and is required for Flow's volume adjustment. Please check your installation if you need to adjust volume.</system:String>
|
||||||
<system:String x:Key="Animation">Animation</system:String>
|
<system:String x:Key="Animation">Animation</system:String>
|
||||||
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
<system:String x:Key="AnimationTip">Use Animation in UI</system:String>
|
||||||
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
<system:String x:Key="AnimationSpeed">Animation Speed</system:String>
|
||||||
|
|
@ -167,17 +180,41 @@
|
||||||
<system:String x:Key="Clock">Clock</system:String>
|
<system:String x:Key="Clock">Clock</system:String>
|
||||||
<system:String x:Key="Date">Date</system:String>
|
<system:String x:Key="Date">Date</system:String>
|
||||||
|
|
||||||
|
|
||||||
<!-- Setting Hotkey -->
|
<!-- Setting Hotkey -->
|
||||||
<system:String x:Key="hotkey">Hotkey</system:String>
|
<system:String x:Key="hotkey">Hotkey</system:String>
|
||||||
<system:String x:Key="hotkeys">Hotkeys</system:String>
|
<system:String x:Key="hotkeys">Hotkeys</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkey">Flow Launcher Hotkey</system:String>
|
<system:String x:Key="flowlauncherHotkey">Open Flow Launcher</system:String>
|
||||||
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
<system:String x:Key="flowlauncherHotkeyToolTip">Enter shortcut to show/hide Flow Launcher.</system:String>
|
||||||
<system:String x:Key="previewHotkey">Preview Hotkey</system:String>
|
<system:String x:Key="previewHotkey">Toggle Preview</system:String>
|
||||||
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
<system:String x:Key="previewHotkeyToolTip">Enter shortcut to show/hide preview in search window.</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresets">Hotkey Presets</system:String>
|
||||||
|
<system:String x:Key="hotkeyPresetsToolTip">List of currently registered hotkeys</system:String>
|
||||||
<system:String x:Key="openResultModifiers">Open Result Modifier Key</system:String>
|
<system:String x:Key="openResultModifiers">Open Result Modifier Key</system:String>
|
||||||
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
<system:String x:Key="openResultModifiersToolTip">Select a modifier key to open selected result via keyboard.</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
|
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
|
||||||
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
|
||||||
|
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
|
||||||
|
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
|
||||||
|
<system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
|
||||||
|
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
|
||||||
|
<system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryUpHotkey">Cycle Previous Query</system:String>
|
||||||
|
<system:String x:Key="CycleHistoryDownHotkey">Cycle Next Query</system:String>
|
||||||
|
<system:String x:Key="OpenContextMenuHotkey">Open Context Menu</system:String>
|
||||||
|
<system:String x:Key="SettingWindowHotkey">Open Setting Window</system:String>
|
||||||
|
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
|
||||||
|
<system:String x:Key="ToggleGameModeHotkey">Toggle Game Mode</system:String>
|
||||||
|
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
|
||||||
|
<system:String x:Key="OpenContainFolderHotkey">Open Containing Folder</system:String>
|
||||||
|
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
|
||||||
|
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
|
||||||
|
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
|
||||||
|
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>
|
||||||
|
<system:String x:Key="ReloadPluginHotkeyToolTip">Use when require plugins to reload and update their existing data.</system:String>
|
||||||
|
<system:String x:Key="AdditionalHotkeyToolTip">You can add one more hotkey for this function.</system:String>
|
||||||
<system:String x:Key="customQueryHotkey">Custom Query Hotkeys</system:String>
|
<system:String x:Key="customQueryHotkey">Custom Query Hotkeys</system:String>
|
||||||
<system:String x:Key="customQueryShortcut">Custom Query Shortcuts</system:String>
|
<system:String x:Key="customQueryShortcut">Custom Query Shortcuts</system:String>
|
||||||
<system:String x:Key="builtinShortcuts">Built-in Shortcuts</system:String>
|
<system:String x:Key="builtinShortcuts">Built-in Shortcuts</system:String>
|
||||||
|
|
@ -188,6 +225,7 @@
|
||||||
<system:String x:Key="delete">Delete</system:String>
|
<system:String x:Key="delete">Delete</system:String>
|
||||||
<system:String x:Key="edit">Edit</system:String>
|
<system:String x:Key="edit">Edit</system:String>
|
||||||
<system:String x:Key="add">Add</system:String>
|
<system:String x:Key="add">Add</system:String>
|
||||||
|
<system:String x:Key="none">None</system:String>
|
||||||
<system:String x:Key="pleaseSelectAnItem">Please select an item</system:String>
|
<system:String x:Key="pleaseSelectAnItem">Please select an item</system:String>
|
||||||
<system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String>
|
<system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String>
|
||||||
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
|
||||||
|
|
@ -286,6 +324,11 @@
|
||||||
<system:String x:Key="hotkeyIsNotUnavailable">Hotkey is unavailable, please select a new hotkey</system:String>
|
<system:String x:Key="hotkeyIsNotUnavailable">Hotkey is unavailable, please select a new hotkey</system:String>
|
||||||
<system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String>
|
<system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String>
|
||||||
<system:String x:Key="update">Update</system:String>
|
<system:String x:Key="update">Update</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey.</system:String>
|
||||||
|
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}".</system:String>
|
||||||
|
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
|
||||||
|
|
||||||
<!-- Custom Query Shortcut Dialog -->
|
<!-- Custom Query Shortcut Dialog -->
|
||||||
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
|
||||||
|
|
@ -295,8 +338,12 @@
|
||||||
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
|
||||||
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
|
||||||
|
|
||||||
<!-- Hotkey Control -->
|
<!-- Common Action -->
|
||||||
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>
|
<system:String x:Key="commonSave">Save</system:String>
|
||||||
|
<system:String x:Key="commonOverwrite">Overwrite</system:String>
|
||||||
|
<system:String x:Key="commonCancel">Cancel</system:String>
|
||||||
|
<system:String x:Key="commonReset">Reset</system:String>
|
||||||
|
<system:String x:Key="commonDelete">Delete</system:String>
|
||||||
|
|
||||||
<!-- Crash Reporter -->
|
<!-- Crash Reporter -->
|
||||||
<system:String x:Key="reportWindow_version">Version</system:String>
|
<system:String x:Key="reportWindow_version">Version</system:String>
|
||||||
|
|
@ -366,6 +413,12 @@
|
||||||
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
<system:String x:Key="HotkeyCtrlIDesc">Open Setting Window</system:String>
|
||||||
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
<system:String x:Key="HotkeyF5Desc">Reload Plugin Data</system:String>
|
||||||
|
|
||||||
|
<system:String x:Key="HotkeySelectFirstResult">Select first result</system:String>
|
||||||
|
<system:String x:Key="HotkeySelectLastResult">Select last result</system:String>
|
||||||
|
<system:String x:Key="HotkeyRequery">Run current query again</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResult">Open result</system:String>
|
||||||
|
<system:String x:Key="HotkeyOpenResultN">Open result #{0}</system:String>
|
||||||
|
|
||||||
<system:String x:Key="RecommendWeather">Weather</system:String>
|
<system:String x:Key="RecommendWeather">Weather</system:String>
|
||||||
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
<system:String x:Key="RecommendWeatherDesc">Weather in Google Result</system:String>
|
||||||
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
<system:String x:Key="RecommendShell">> ping 8.8.8.8</system:String>
|
||||||
|
|
@ -375,4 +428,8 @@
|
||||||
<system:String x:Key="RecommendAcronyms">sn</system:String>
|
<system:String x:Key="RecommendAcronyms">sn</system:String>
|
||||||
<system:String x:Key="RecommendAcronymsDesc">Sticky Notes</system:String>
|
<system:String x:Key="RecommendAcronymsDesc">Sticky Notes</system:String>
|
||||||
|
|
||||||
|
<!-- Preview Area -->
|
||||||
|
<system:String x:Key="FileSize">File Size</system:String>
|
||||||
|
<system:String x:Key="Created">Created</system:String>
|
||||||
|
<system:String x:Key="LastModified">Last Modified</system:String>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue