mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into ChannelSelect
This commit is contained in:
commit
06410da91e
692 changed files with 45878 additions and 21849 deletions
80
.cm/gitstream.cm
Normal file
80
.cm/gitstream.cm
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# -*- mode: yaml -*-
|
||||||
|
# This example configuration for provides basic automations to get started with gitStream.
|
||||||
|
# View the gitStream quickstart for more examples: https://docs.gitstream.cm/examples/
|
||||||
|
manifest:
|
||||||
|
version: 1.0
|
||||||
|
|
||||||
|
|
||||||
|
triggers:
|
||||||
|
exclude:
|
||||||
|
branch:
|
||||||
|
- l10n_dev
|
||||||
|
- dev
|
||||||
|
- r/([Dd]ependabot|[Rr]enovate)/
|
||||||
|
|
||||||
|
|
||||||
|
automations:
|
||||||
|
# Add a label that indicates how many minutes it will take to review the PR.
|
||||||
|
estimated_time_to_review:
|
||||||
|
on:
|
||||||
|
- commit
|
||||||
|
if:
|
||||||
|
- true
|
||||||
|
run:
|
||||||
|
- action: add-label@v1
|
||||||
|
args:
|
||||||
|
label: "{{ calc.etr }} min review"
|
||||||
|
color: {{ colors.red if (calc.etr >= 20) else ( colors.yellow if (calc.etr >= 5) else colors.green ) }}
|
||||||
|
# Post a comment that lists the best experts for the files that were modified.
|
||||||
|
explain_code_experts:
|
||||||
|
if:
|
||||||
|
- true
|
||||||
|
run:
|
||||||
|
- action: explain-code-experts@v1
|
||||||
|
args:
|
||||||
|
gt: 10
|
||||||
|
# Post a comment notifying that the PR contains a TODO statement.
|
||||||
|
review_todo_comments:
|
||||||
|
if:
|
||||||
|
- {{ source.diff.files | matchDiffLines(regex=r/^[+].*\b(TODO|todo)\b/) | some }}
|
||||||
|
run:
|
||||||
|
- action: add-comment@v1
|
||||||
|
args:
|
||||||
|
comment: |
|
||||||
|
This PR contains a TODO statement. Please check to see if they should be removed.
|
||||||
|
# Post a comment that request a before and after screenshot
|
||||||
|
request_screenshot:
|
||||||
|
# Triggered for PRs that lack an image file or link to an image in the PR description
|
||||||
|
if:
|
||||||
|
- {{ not (has.screenshot_link or has.image_uploaded) }}
|
||||||
|
run:
|
||||||
|
- action: add-comment@v1
|
||||||
|
args:
|
||||||
|
comment: |
|
||||||
|
Be a legend :trophy: by adding a before and after screenshot of the changes you made, especially if they are around UI/UX.
|
||||||
|
|
||||||
|
|
||||||
|
# +----------------------------------------------------------------------------+
|
||||||
|
# | Custom Expressions |
|
||||||
|
# | https://docs.gitstream.cm/how-it-works/#custom-expressions |
|
||||||
|
# +----------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
calc:
|
||||||
|
etr: {{ branch | estimatedReviewTime }}
|
||||||
|
|
||||||
|
colors:
|
||||||
|
red: 'b60205'
|
||||||
|
yellow: 'fbca04'
|
||||||
|
green: '0e8a16'
|
||||||
|
|
||||||
|
changes:
|
||||||
|
# Sum all the lines added/edited in the PR
|
||||||
|
additions: {{ branch.diff.files_metadata | map(attr='additions') | sum }}
|
||||||
|
# Sum all the line removed in the PR
|
||||||
|
deletions: {{ branch.diff.files_metadata | map(attr='deletions') | sum }}
|
||||||
|
# Calculate the ratio of new code
|
||||||
|
ratio: {{ (changes.additions / (changes.additions + changes.deletions)) * 100 | round(2) }}
|
||||||
|
|
||||||
|
has:
|
||||||
|
screenshot_link: {{ pr.description | includes(regex=r/!\[.*\]\(.*(jpg|svg|png|gif|psd).*\)/) }}
|
||||||
|
image_uploaded: {{ pr.description | includes(regex=r/(<img.*src.*(jpg|svg|png|gif|psd).*>)|!\[image\]\(.*github\.com.*\)/) }}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
28
.github/actions/spelling/expect.txt
vendored
28
.github/actions/spelling/expect.txt
vendored
|
|
@ -19,7 +19,6 @@ Prioritise
|
||||||
Segoe
|
Segoe
|
||||||
Google
|
Google
|
||||||
Customise
|
Customise
|
||||||
UWP
|
|
||||||
uwp
|
uwp
|
||||||
Bokmal
|
Bokmal
|
||||||
Bokm
|
Bokm
|
||||||
|
|
@ -59,7 +58,6 @@ popup
|
||||||
ptr
|
ptr
|
||||||
pluginindicator
|
pluginindicator
|
||||||
TobiasSekan
|
TobiasSekan
|
||||||
Img
|
|
||||||
img
|
img
|
||||||
resx
|
resx
|
||||||
bak
|
bak
|
||||||
|
|
@ -76,6 +74,7 @@ WCA_ACCENT_POLICY
|
||||||
HGlobal
|
HGlobal
|
||||||
dopusrt
|
dopusrt
|
||||||
firefox
|
firefox
|
||||||
|
Firefox
|
||||||
msedge
|
msedge
|
||||||
svgc
|
svgc
|
||||||
ime
|
ime
|
||||||
|
|
@ -84,7 +83,30 @@ txb
|
||||||
btn
|
btn
|
||||||
otf
|
otf
|
||||||
searchplugin
|
searchplugin
|
||||||
Noresult
|
|
||||||
wpftk
|
wpftk
|
||||||
mkv
|
mkv
|
||||||
flac
|
flac
|
||||||
|
IPublic
|
||||||
|
keyevent
|
||||||
|
KListener
|
||||||
|
requery
|
||||||
|
vkcode
|
||||||
|
čeština
|
||||||
|
Polski
|
||||||
|
Srpski
|
||||||
|
Português
|
||||||
|
Português (Brasil)
|
||||||
|
Italiano
|
||||||
|
Slovenský
|
||||||
|
quicklook
|
||||||
|
Tiếng Việt
|
||||||
|
Droplex
|
||||||
|
Preinstalled
|
||||||
|
errormetadatafile
|
||||||
|
noresult
|
||||||
|
pluginsmanager
|
||||||
|
alreadyexists
|
||||||
|
JsonRPC
|
||||||
|
JsonRPCV2
|
||||||
|
Softpedia
|
||||||
|
img
|
||||||
|
|
|
||||||
6
.github/actions/spelling/patterns.txt
vendored
6
.github/actions/spelling/patterns.txt
vendored
|
|
@ -115,3 +115,9 @@
|
||||||
|
|
||||||
#http/https
|
#http/https
|
||||||
(?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]
|
(?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]
|
||||||
|
|
||||||
|
# UWP
|
||||||
|
[Uu][Ww][Pp]
|
||||||
|
|
||||||
|
# version suffix <word>v#
|
||||||
|
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
|
||||||
|
|
|
||||||
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
|
|
@ -8,7 +8,8 @@ updates:
|
||||||
- package-ecosystem: "nuget" # See documentation for possible values
|
- package-ecosystem: "nuget" # See documentation for possible values
|
||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 3
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "squirrel-windows"
|
- dependency-name: "squirrel-windows"
|
||||||
reviewers:
|
reviewers:
|
||||||
|
|
|
||||||
31
.github/pr-labeler.yml
vendored
Normal file
31
.github/pr-labeler.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# The bot always updates the labels, add/remove as necessary [default: false]
|
||||||
|
alwaysReplace: false
|
||||||
|
# Treats the text and labels as case sensitive [default: true]
|
||||||
|
caseSensitive: false
|
||||||
|
# Array of labels to be applied to the PR [default: []]
|
||||||
|
customLabels:
|
||||||
|
# Finds the `text` within the PR title and body and applies the `label`
|
||||||
|
- text: 'bug'
|
||||||
|
label: 'bug'
|
||||||
|
- text: 'fix'
|
||||||
|
label: 'bug'
|
||||||
|
- text: 'dependabot'
|
||||||
|
label: 'bug'
|
||||||
|
- text: 'New Crowdin updates'
|
||||||
|
label: 'bug'
|
||||||
|
- text: 'New Crowdin updates'
|
||||||
|
label: 'kind/i18n'
|
||||||
|
- text: 'feature'
|
||||||
|
label: 'enhancement'
|
||||||
|
- text: 'add new'
|
||||||
|
label: 'enhancement'
|
||||||
|
- text: 'refactor'
|
||||||
|
label: 'enhancement'
|
||||||
|
- text: 'refactor'
|
||||||
|
label: 'Code Refactor'
|
||||||
|
# Search the body of the PR for the `text` [default: true]
|
||||||
|
searchBody: true
|
||||||
|
# Search the title of the PR for the `text` [default: true]
|
||||||
|
searchTitle: true
|
||||||
|
# Search for whole words only [default: false]
|
||||||
|
wholeWords: false
|
||||||
372
.github/workflows/default_plugins.yml
vendored
Normal file
372
.github/workflows/default_plugins.yml
vendored
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
name: Publish Default Plugins
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['master']
|
||||||
|
paths: ['Plugins/**']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 7.0.x
|
||||||
|
|
||||||
|
- name: Determine New Plugin Updates
|
||||||
|
uses: dorny/paths-filter@v3
|
||||||
|
id: changes
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
browserbookmark:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json'
|
||||||
|
calculator:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.Calculator/plugin.json'
|
||||||
|
explorer:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.Explorer/plugin.json'
|
||||||
|
pluginindicator:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json'
|
||||||
|
pluginsmanager:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json'
|
||||||
|
processkiller:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json'
|
||||||
|
program:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.Program/plugin.json'
|
||||||
|
shell:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.Shell/plugin.json'
|
||||||
|
sys:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.Sys/plugin.json'
|
||||||
|
url:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.Url/plugin.json'
|
||||||
|
websearch:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json'
|
||||||
|
windowssettings:
|
||||||
|
- 'Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json'
|
||||||
|
base: 'master'
|
||||||
|
|
||||||
|
- name: Get BrowserBookmark Version
|
||||||
|
if: steps.changes.outputs.browserbookmark == 'true'
|
||||||
|
id: updated-version-browserbookmark
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build BrowserBookmark
|
||||||
|
if: steps.changes.outputs.browserbookmark == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.BrowserBookmark.zip" "./Flow.Launcher.Plugin.BrowserBookmark/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.BrowserBookmark"
|
||||||
|
|
||||||
|
- name: Publish BrowserBookmark
|
||||||
|
if: steps.changes.outputs.browserbookmark == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.BrowserBookmark"
|
||||||
|
files: "Flow.Launcher.Plugin.BrowserBookmark.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-browserbookmark.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get Calculator Version
|
||||||
|
if: steps.changes.outputs.calculator == 'true'
|
||||||
|
id: updated-version-calculator
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.Calculator/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build Calculator
|
||||||
|
if: steps.changes.outputs.calculator == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.Calculator.zip" "./Flow.Launcher.Plugin.Calculator/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.Calculator"
|
||||||
|
|
||||||
|
- name: Publish Calculator
|
||||||
|
if: steps.changes.outputs.calculator == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Calculator"
|
||||||
|
files: "Flow.Launcher.Plugin.Calculator.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-calculator.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get Explorer Version
|
||||||
|
if: steps.changes.outputs.explorer == 'true'
|
||||||
|
id: updated-version-explorer
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.Explorer/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build Explorer
|
||||||
|
if: steps.changes.outputs.explorer == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.Explorer.zip" "./Flow.Launcher.Plugin.Explorer/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.Explorer"
|
||||||
|
|
||||||
|
- name: Publish Explorer
|
||||||
|
if: steps.changes.outputs.explorer == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Explorer"
|
||||||
|
files: "Flow.Launcher.Plugin.Explorer.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-explorer.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get PluginIndicator Version
|
||||||
|
if: steps.changes.outputs.pluginindicator == 'true'
|
||||||
|
id: updated-version-pluginindicator
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build PluginIndicator
|
||||||
|
if: steps.changes.outputs.pluginindicator == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.PluginIndicator.zip" "./Flow.Launcher.Plugin.PluginIndicator/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.PluginIndicator"
|
||||||
|
|
||||||
|
- name: Publish PluginIndicator
|
||||||
|
if: steps.changes.outputs.pluginindicator == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginIndicator"
|
||||||
|
files: "Flow.Launcher.Plugin.PluginIndicator.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-pluginindicator.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get PluginsManager Version
|
||||||
|
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||||
|
id: updated-version-pluginsmanager
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build PluginsManager
|
||||||
|
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.PluginsManager.zip" "./Flow.Launcher.Plugin.PluginsManager/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.PluginsManager"
|
||||||
|
|
||||||
|
- name: Publish PluginsManager
|
||||||
|
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginsManager"
|
||||||
|
files: "Flow.Launcher.Plugin.PluginsManager.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-pluginsmanager.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get ProcessKiller Version
|
||||||
|
if: steps.changes.outputs.processkiller == 'true'
|
||||||
|
id: updated-version-processkiller
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build ProcessKiller
|
||||||
|
if: steps.changes.outputs.processkiller == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.ProcessKiller.zip" "./Flow.Launcher.Plugin.ProcessKiller/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.ProcessKiller"
|
||||||
|
|
||||||
|
- name: Publish ProcessKiller
|
||||||
|
if: steps.changes.outputs.processkiller == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller"
|
||||||
|
files: "Flow.Launcher.Plugin.ProcessKiller.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-processkiller.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get Program Version
|
||||||
|
if: steps.changes.outputs.program == 'true'
|
||||||
|
id: updated-version-program
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.Program/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build Program
|
||||||
|
if: steps.changes.outputs.program == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net7.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.Program.zip" "./Flow.Launcher.Plugin.Program/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.Program"
|
||||||
|
|
||||||
|
- name: Publish Program
|
||||||
|
if: steps.changes.outputs.program == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Program"
|
||||||
|
files: "Flow.Launcher.Plugin.Program.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-program.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get Shell Version
|
||||||
|
if: steps.changes.outputs.shell == 'true'
|
||||||
|
id: updated-version-shell
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.Shell/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build Shell
|
||||||
|
if: steps.changes.outputs.shell == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.Shell.zip" "./Flow.Launcher.Plugin.Shell/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.Shell"
|
||||||
|
|
||||||
|
- name: Publish Shell
|
||||||
|
if: steps.changes.outputs.shell == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Shell"
|
||||||
|
files: "Flow.Launcher.Plugin.Shell.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-shell.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get Sys Version
|
||||||
|
if: steps.changes.outputs.sys == 'true'
|
||||||
|
id: updated-version-sys
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.Sys/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build Sys
|
||||||
|
if: steps.changes.outputs.sys == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.Sys.zip" "./Flow.Launcher.Plugin.Sys/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.Sys"
|
||||||
|
|
||||||
|
- name: Publish Sys
|
||||||
|
if: steps.changes.outputs.sys == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Sys"
|
||||||
|
files: "Flow.Launcher.Plugin.Sys.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-sys.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get Url Version
|
||||||
|
if: steps.changes.outputs.url == 'true'
|
||||||
|
id: updated-version-url
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.Url/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build Url
|
||||||
|
if: steps.changes.outputs.url == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.Url.zip" "./Flow.Launcher.Plugin.Url/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.Url"
|
||||||
|
|
||||||
|
- name: Publish Url
|
||||||
|
if: steps.changes.outputs.url == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.Url"
|
||||||
|
files: "Flow.Launcher.Plugin.Url.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-url.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get WebSearch Version
|
||||||
|
if: steps.changes.outputs.websearch == 'true'
|
||||||
|
id: updated-version-websearch
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build WebSearch
|
||||||
|
if: steps.changes.outputs.websearch == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.WebSearch.zip" "./Flow.Launcher.Plugin.WebSearch/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.WebSearch"
|
||||||
|
|
||||||
|
- name: Publish WebSearch
|
||||||
|
if: steps.changes.outputs.websearch == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.WebSearch"
|
||||||
|
files: "Flow.Launcher.Plugin.WebSearch.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-websearch.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get WindowsSettings Version
|
||||||
|
if: steps.changes.outputs.windowssettings == 'true'
|
||||||
|
id: updated-version-windowssettings
|
||||||
|
uses: notiz-dev/github-action-json-property@release
|
||||||
|
with:
|
||||||
|
path: 'Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json'
|
||||||
|
prop_path: 'Version'
|
||||||
|
|
||||||
|
- name: Build WindowsSettings
|
||||||
|
if: steps.changes.outputs.windowssettings == 'true'
|
||||||
|
run: |
|
||||||
|
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
|
||||||
|
7z a -tzip "Flow.Launcher.Plugin.WindowsSettings.zip" "./Flow.Launcher.Plugin.WindowsSettings/*"
|
||||||
|
rm -r "Flow.Launcher.Plugin.WindowsSettings"
|
||||||
|
|
||||||
|
- name: Publish WindowsSettings
|
||||||
|
if: steps.changes.outputs.windowssettings == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
repository: "Flow-Launcher/Flow.Launcher.Plugin.WindowsSettings"
|
||||||
|
files: "Flow.Launcher.Plugin.WindowsSettings.zip"
|
||||||
|
tag_name: "v${{steps.updated-version-windowssettings.outputs.prop}}"
|
||||||
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
49
.github/workflows/gitstream.yml
vendored
Normal file
49
.github/workflows/gitstream.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Code generated by gitStream GitHub app - DO NOT EDIT
|
||||||
|
|
||||||
|
name: gitStream workflow automation
|
||||||
|
run-name: |
|
||||||
|
/:\ gitStream: PR #${{ fromJSON(fromJSON(github.event.inputs.client_payload)).pullRequestNumber }} from ${{ github.event.inputs.full_repository }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
client_payload:
|
||||||
|
description: The Client payload
|
||||||
|
required: true
|
||||||
|
full_repository:
|
||||||
|
description: the repository name include the owner in `owner/repo_name` format
|
||||||
|
required: true
|
||||||
|
head_ref:
|
||||||
|
description: the head sha
|
||||||
|
required: true
|
||||||
|
base_ref:
|
||||||
|
description: the base ref
|
||||||
|
required: true
|
||||||
|
installation_id:
|
||||||
|
description: the installation id
|
||||||
|
required: false
|
||||||
|
resolver_url:
|
||||||
|
description: the resolver url to pass results to
|
||||||
|
required: true
|
||||||
|
resolver_token:
|
||||||
|
description: Optional resolver token for resolver service
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitStream:
|
||||||
|
timeout-minutes: 5
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: gitStream workflow automation
|
||||||
|
steps:
|
||||||
|
- name: Evaluate Rules
|
||||||
|
uses: linear-b/gitstream-github-action@v2
|
||||||
|
id: rules-engine
|
||||||
|
with:
|
||||||
|
full_repository: ${{ github.event.inputs.full_repository }}
|
||||||
|
head_ref: ${{ github.event.inputs.head_ref }}
|
||||||
|
base_ref: ${{ github.event.inputs.base_ref }}
|
||||||
|
client_payload: ${{ github.event.inputs.client_payload }}
|
||||||
|
installation_id: ${{ github.event.inputs.installation_id }}
|
||||||
|
resolver_url: ${{ github.event.inputs.resolver_url }}
|
||||||
|
resolver_token: ${{ github.event.inputs.resolver_token }}
|
||||||
17
.github/workflows/pr_assignee.yml
vendored
Normal file
17
.github/workflows/pr_assignee.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
name: Assign PR to creator
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
branches-ignore:
|
||||||
|
- l10n_dev
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Assign PR to creator
|
||||||
|
uses: toshimaru/auto-author-assign@v2.1.1
|
||||||
22
.github/workflows/pr_milestone.yml
vendored
Normal file
22
.github/workflows/pr_milestone.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: Set Milestone
|
||||||
|
|
||||||
|
# Assigns the earliest created milestone that matches the below glob pattern.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: set-milestone
|
||||||
|
uses: andrefcdias/add-to-milestone@v1.3.0
|
||||||
|
with:
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
milestone: "+([0-9]).+([0-9]).+([0-9])"
|
||||||
|
use-expression: true
|
||||||
14
.github/workflows/spelling.yml
vendored
14
.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@main
|
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
|
||||||
|
|
@ -89,12 +89,12 @@ jobs:
|
||||||
check_extra_dictionaries: ''
|
check_extra_dictionaries: ''
|
||||||
quit_without_error: true
|
quit_without_error: true
|
||||||
extra_dictionaries:
|
extra_dictionaries:
|
||||||
cspell:software-terms/src/software-terms.txt
|
cspell:software-terms/dict/softwareTerms.txt
|
||||||
cspell:win32/src/win32.txt
|
cspell:win32/src/win32.txt
|
||||||
cspell:php/php.txt
|
|
||||||
cspell:filetypes/filetypes.txt
|
cspell:filetypes/filetypes.txt
|
||||||
cspell:csharp/csharp.txt
|
cspell:csharp/csharp.txt
|
||||||
cspell:dotnet/dotnet.txt
|
cspell:dotnet/dict/dotnet.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
|
||||||
cspell:companies/src/companies.txt
|
cspell:companies/src/companies.txt
|
||||||
|
|
@ -113,7 +113,7 @@ jobs:
|
||||||
# if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
|
# if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
|
||||||
# steps:
|
# steps:
|
||||||
# - name: comment
|
# - name: comment
|
||||||
# uses: check-spelling/check-spelling@main
|
# uses: check-spelling/check-spelling@@v0.0.22
|
||||||
# with:
|
# with:
|
||||||
# checkout: true
|
# checkout: true
|
||||||
# spell_check_this: check-spelling/spell-check-this@main
|
# spell_check_this: check-spelling/spell-check-this@main
|
||||||
|
|
@ -129,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@main
|
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
|
||||||
|
|
@ -153,7 +153,7 @@ jobs:
|
||||||
# cancel-in-progress: false
|
# cancel-in-progress: false
|
||||||
# steps:
|
# steps:
|
||||||
# - name: apply spelling updates
|
# - name: apply spelling updates
|
||||||
# uses: check-spelling/check-spelling@main
|
# uses: check-spelling/check-spelling@v0.0.22
|
||||||
# with:
|
# with:
|
||||||
# experimental_apply_changes_via_bot: 1
|
# experimental_apply_changes_via_bot: 1
|
||||||
# checkout: true
|
# checkout: true
|
||||||
|
|
|
||||||
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'
|
||||||
|
|
|
||||||
25
.github/workflows/top-ranking-issues.yml
vendored
Normal file
25
.github/workflows/top-ranking-issues.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: Top-Ranking Issues
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 */1 * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ShowAndLabelTopIssues:
|
||||||
|
name: Display and label top issues.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Top Issues action
|
||||||
|
uses: rickstaa/top-issues-action@v1.3.101
|
||||||
|
env:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
dashboard: true
|
||||||
|
dashboard_show_total_reactions: true
|
||||||
|
top_list_size: 10
|
||||||
|
top_features: true
|
||||||
|
top_bugs: true
|
||||||
|
dashboard_title: Top-Ranking Issues 📈
|
||||||
|
dashboard_label: ⭐ Dashboard
|
||||||
|
hide_dashboard_footer: true
|
||||||
|
top_issues: false
|
||||||
6
.github/workflows/winget.yml
vendored
6
.github/workflows/winget.yml
vendored
|
|
@ -1,13 +1,11 @@
|
||||||
name: Publish to Winget
|
name: Publish to Winget
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
workflow_dispatch:
|
||||||
types: [released]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
# Action can only be run on windows
|
runs-on: ubuntu-latest
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
New-Alias nuget.exe ".\packages\NuGet.CommandLine.*\tools\NuGet.exe"
|
|
||||||
$env:APPVEYOR_BUILD_FOLDER = Convert-Path .
|
|
||||||
$env:APPVEYOR_BUILD_VERSION = "1.2.0"
|
|
||||||
& .\Deploy\squirrel_installer.ps1
|
|
||||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -9,11 +9,15 @@ using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Configuration
|
namespace Flow.Launcher.Core.Configuration
|
||||||
{
|
{
|
||||||
public class Portable : IPortable
|
public class Portable : IPortable
|
||||||
{
|
{
|
||||||
|
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -40,7 +44,7 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
#endif
|
#endif
|
||||||
IndicateDeletion(DataLocation.PortableDataPath);
|
IndicateDeletion(DataLocation.PortableDataPath);
|
||||||
|
|
||||||
MessageBox.Show("Flow Launcher needs to restart to finish disabling portable mode, " +
|
API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " +
|
||||||
"after the restart your portable data profile will be deleted and roaming data profile kept");
|
"after the restart your portable data profile will be deleted and roaming data profile kept");
|
||||||
|
|
||||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||||
|
|
@ -64,7 +68,7 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
#endif
|
#endif
|
||||||
IndicateDeletion(DataLocation.RoamingDataPath);
|
IndicateDeletion(DataLocation.RoamingDataPath);
|
||||||
|
|
||||||
MessageBox.Show("Flow Launcher needs to restart to finish enabling portable mode, " +
|
API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " +
|
||||||
"after the restart your roaming data profile will be deleted and portable data profile kept");
|
"after the restart your roaming data profile will be deleted and portable data profile kept");
|
||||||
|
|
||||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||||
|
|
@ -95,13 +99,13 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
|
|
||||||
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
||||||
{
|
{
|
||||||
FilesFolders.CopyAll(fromLocation, toLocation);
|
FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||||
VerifyUserDataAfterMove(fromLocation, toLocation);
|
VerifyUserDataAfterMove(fromLocation, toLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
|
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
|
||||||
{
|
{
|
||||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation);
|
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateShortcuts()
|
public void CreateShortcuts()
|
||||||
|
|
@ -157,13 +161,13 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
// delete it and prompt the user to pick the portable data location
|
// delete it and prompt the user to pick the portable data location
|
||||||
if (File.Exists(roamingDataDeleteFilePath))
|
if (File.Exists(roamingDataDeleteFilePath))
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(roamingDataDir);
|
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
if (MessageBox.Show("Flow Launcher has detected you enabled portable mode, " +
|
if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " +
|
||||||
"would you like to move it to a different location?", string.Empty,
|
"would you like to move it to a different location?", string.Empty,
|
||||||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||||
{
|
{
|
||||||
FilesFolders.OpenPath(Constant.RootDirectory);
|
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
|
@ -172,9 +176,9 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
// delete it and notify the user about it.
|
// delete it and notify the user about it.
|
||||||
else if (File.Exists(portableDataDeleteFilePath))
|
else if (File.Exists(portableDataDeleteFilePath))
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(portableDataDir);
|
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
MessageBox.Show("Flow Launcher has detected you disabled portable mode, " +
|
API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " +
|
||||||
"the relevant shortcuts and uninstaller entry have been created");
|
"the relevant shortcuts and uninstaller entry have been created");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +190,7 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
|
|
||||||
if (roamingLocationExists && portableLocationExists)
|
if (roamingLocationExists && portableLocationExists)
|
||||||
{
|
{
|
||||||
MessageBox.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " +
|
API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " +
|
||||||
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
|
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
|
||||||
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
||||||
|
|
||||||
|
|
|
||||||
68
Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs
Normal file
68
Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
using Flow.Launcher.Infrastructure.Http;
|
||||||
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
|
{
|
||||||
|
public record CommunityPluginSource(string ManifestFileUrl)
|
||||||
|
{
|
||||||
|
private string latestEtag = "";
|
||||||
|
|
||||||
|
private List<UserPlugin> plugins = new();
|
||||||
|
|
||||||
|
private static JsonSerializerOptions PluginStoreItemSerializationOption = new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method will only return plugin details when the underlying http request is successful (200 or 304).
|
||||||
|
/// In any other case, an exception is raised
|
||||||
|
/// </remarks>
|
||||||
|
public async Task<List<UserPlugin>> FetchAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
Log.Info(nameof(CommunityPluginSource), $"Loading plugins from {ManifestFileUrl}");
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl);
|
||||||
|
|
||||||
|
request.Headers.Add("If-None-Match", latestEtag);
|
||||||
|
|
||||||
|
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
this.plugins = await response.Content
|
||||||
|
.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}");
|
||||||
|
return this.plugins;
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == HttpStatusCode.NotModified)
|
||||||
|
{
|
||||||
|
Log.Info(nameof(CommunityPluginSource), $"Resource {ManifestFileUrl} has not been modified.");
|
||||||
|
return this.plugins;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs
Normal file
54
Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Describes a store of community-made plugins.
|
||||||
|
/// The provided URLs should point to a json file, whose content
|
||||||
|
/// is deserializable as a <see cref="UserPlugin"/> array.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="primaryUrl">Primary URL to the manifest json file.</param>
|
||||||
|
/// <param name="secondaryUrls">Secondary URLs to access the <paramref name="primaryUrl"/>, for example CDN links</param>
|
||||||
|
public record CommunityPluginStore(string primaryUrl, params string[] secondaryUrls)
|
||||||
|
{
|
||||||
|
private readonly List<CommunityPluginSource> pluginSources =
|
||||||
|
secondaryUrls
|
||||||
|
.Append(primaryUrl)
|
||||||
|
.Select(url => new CommunityPluginSource(url))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
public async Task<List<UserPlugin>> FetchAsync(CancellationToken token, bool onlyFromPrimaryUrl = false)
|
||||||
|
{
|
||||||
|
// we create a new cancellation token source linked to the given token.
|
||||||
|
// Once any of the http requests completes successfully, we call cancel
|
||||||
|
// to stop the rest of the running http requests.
|
||||||
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||||
|
|
||||||
|
var tasks = onlyFromPrimaryUrl
|
||||||
|
? new() { pluginSources.Last().FetchAsync(cts.Token) }
|
||||||
|
: pluginSources.Select(pluginSource => pluginSource.FetchAsync(cts.Token)).ToList();
|
||||||
|
|
||||||
|
var pluginResults = new List<UserPlugin>();
|
||||||
|
|
||||||
|
// keep going until all tasks have completed
|
||||||
|
while (tasks.Any())
|
||||||
|
{
|
||||||
|
var completedTask = await Task.WhenAny(tasks);
|
||||||
|
if (completedTask.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
// one of the requests completed successfully; keep its results
|
||||||
|
// and cancel the remaining http requests.
|
||||||
|
pluginResults = await completedTask;
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
tasks.Remove(completedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
// all tasks have finished
|
||||||
|
return pluginResults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,14 +4,18 @@ using Flow.Launcher.Plugin;
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using Flow.Launcher.Core.Resource;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
{
|
{
|
||||||
public abstract class AbstractPluginEnvironment
|
public abstract class AbstractPluginEnvironment
|
||||||
{
|
{
|
||||||
|
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
|
|
||||||
internal abstract string Language { get; }
|
internal abstract string Language { get; }
|
||||||
|
|
||||||
internal abstract string EnvName { get; }
|
internal abstract string EnvName { get; }
|
||||||
|
|
@ -24,7 +28,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
internal virtual string FileDialogFilter => string.Empty;
|
internal virtual string FileDialogFilter => string.Empty;
|
||||||
|
|
||||||
internal abstract string PluginsSettingsFilePath { get; set; }
|
internal abstract string PluginsSettingsFilePath { get; set; }
|
||||||
|
|
||||||
internal List<PluginMetadata> PluginMetadataList;
|
internal List<PluginMetadata> PluginMetadataList;
|
||||||
|
|
||||||
|
|
@ -41,18 +45,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
|
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
|
||||||
return new List<PluginPair>();
|
return new List<PluginPair>();
|
||||||
|
|
||||||
// TODO: Remove. This is backwards compatibility for 1.10.0 release- changed PythonEmbeded to Environments/Python
|
|
||||||
if (Language.Equals(AllowedLanguage.Python, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
FilesFolders.RemoveFolderIfExists(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable"));
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(PluginSettings.PythonDirectory) && PluginSettings.PythonDirectory.StartsWith(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable")))
|
|
||||||
{
|
|
||||||
InstallEnvironment();
|
|
||||||
PluginSettings.PythonDirectory = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
|
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
|
||||||
{
|
{
|
||||||
// Ensure latest only if user is using Flow's environment setup.
|
// Ensure latest only if user is using Flow's environment setup.
|
||||||
|
|
@ -62,15 +54,16 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " +
|
var noRuntimeMessage = string.Format(
|
||||||
$"will require {EnvName} to run. Would you like to download {EnvName}? " +
|
InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
|
||||||
Environment.NewLine + Environment.NewLine +
|
Language,
|
||||||
"Click no if it's already installed, " +
|
EnvName,
|
||||||
$"and you will be prompted to select the folder that contains the {EnvName} executable",
|
Environment.NewLine
|
||||||
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
|
);
|
||||||
|
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||||
{
|
{
|
||||||
var msg = $"Please select the {EnvName} executable";
|
var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
|
||||||
var selectedFile = string.Empty;
|
string selectedFile;
|
||||||
|
|
||||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||||
|
|
||||||
|
|
@ -92,8 +85,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
API.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
|
||||||
$"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom).");
|
|
||||||
Log.Error("PluginsLoader",
|
Log.Error("PluginsLoader",
|
||||||
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
||||||
$"{Language}Environment");
|
$"{Language}Environment");
|
||||||
|
|
@ -109,7 +101,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
if (expectedPath == currentPath)
|
if (expectedPath == currentPath)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
FilesFolders.RemoveFolderIfExists(installedDirPath);
|
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
InstallEnvironment();
|
InstallEnvironment();
|
||||||
|
|
||||||
|
|
@ -143,14 +135,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) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName);
|
internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName);
|
||||||
|
|
||||||
internal override string InstallPath => Path.Combine(EnvPath, "PythonEmbeddable-v3.8.9");
|
internal override string InstallPath => Path.Combine(EnvPath, "PythonEmbeddable-v3.11.4");
|
||||||
|
|
||||||
internal override string ExecutablePath => Path.Combine(InstallPath, "pythonw.exe");
|
internal override string ExecutablePath => Path.Combine(InstallPath, "pythonw.exe");
|
||||||
|
|
||||||
|
|
@ -28,10 +28,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
internal override void InstallEnvironment()
|
internal override void InstallEnvironment()
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(InstallPath);
|
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
// Python 3.8.9 is used for Windows 7 compatibility
|
// Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and
|
||||||
DroplexPackage.Drop(App.python_3_8_9_embeddable, InstallPath).Wait();
|
// uses Python plugin they need to custom install and use v3.8.9
|
||||||
|
DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath).Wait();
|
||||||
|
|
||||||
PluginsSettingsFilePath = ExecutablePath;
|
PluginsSettingsFilePath = ExecutablePath;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
internal override void InstallEnvironment()
|
internal override void InstallEnvironment()
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(InstallPath);
|
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
|
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,10 +1,6 @@
|
||||||
using Flow.Launcher.Infrastructure.Http;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
@ -12,38 +8,37 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
{
|
{
|
||||||
public static class PluginsManifest
|
public static class PluginsManifest
|
||||||
{
|
{
|
||||||
private const string manifestFileUrl = "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json";
|
private static readonly CommunityPluginStore mainPluginStore =
|
||||||
|
new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json",
|
||||||
|
"https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json",
|
||||||
|
"https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json",
|
||||||
|
"https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json");
|
||||||
|
|
||||||
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
||||||
|
|
||||||
private static string latestEtag = "";
|
private static DateTime lastFetchedAt = DateTime.MinValue;
|
||||||
|
private static TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
|
||||||
|
|
||||||
public static List<UserPlugin> UserPlugins { get; private set; } = new List<UserPlugin>();
|
public static List<UserPlugin> UserPlugins { get; private set; }
|
||||||
|
|
||||||
public static async Task UpdateManifestAsync(CancellationToken token = default)
|
public static async Task<bool> UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false);
|
await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl);
|
if (UserPlugins == null || usePrimaryUrlOnly || DateTime.Now.Subtract(lastFetchedAt) >= fetchTimeout)
|
||||||
request.Headers.Add("If-None-Match", latestEtag);
|
|
||||||
|
|
||||||
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.OK)
|
|
||||||
{
|
{
|
||||||
Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo");
|
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
|
||||||
|
|
||||||
await using var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
|
// If the results are empty, we shouldn't update the manifest because the results are invalid.
|
||||||
|
if (results.Count != 0)
|
||||||
|
{
|
||||||
|
UserPlugins = results;
|
||||||
|
lastFetchedAt = DateTime.Now;
|
||||||
|
|
||||||
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(json, cancellationToken: token).ConfigureAwait(false);
|
return true;
|
||||||
|
}
|
||||||
latestEtag = response.Headers.ETag.Tag;
|
|
||||||
}
|
|
||||||
else if (response.StatusCode != HttpStatusCode.NotModified)
|
|
||||||
{
|
|
||||||
Log.Warn($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http response for manifest file was {response.StatusCode}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -54,6 +49,8 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
{
|
{
|
||||||
manifestUpdateLock.Release();
|
manifestUpdateLock.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net7.0-windows</TargetFramework>
|
||||||
|
|
@ -53,10 +53,12 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Droplex" Version="1.6.0" />
|
<PackageReference Include="Droplex" Version="1.7.0" />
|
||||||
<PackageReference Include="FSharp.Core" Version="7.0.0" />
|
<PackageReference Include="FSharp.Core" Version="9.0.201" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.1" />
|
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.0" />
|
||||||
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||||
<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.21.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
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;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -28,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,76 +12,42 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using Flow.Launcher.Core.Resource;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using System.Text.Json;
|
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
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,6 @@ using Orientation = System.Windows.Controls.Orientation;
|
||||||
using TextBox = System.Windows.Controls.TextBox;
|
using TextBox = System.Windows.Controls.TextBox;
|
||||||
using UserControl = System.Windows.Controls.UserControl;
|
using UserControl = System.Windows.Controls.UserControl;
|
||||||
using System.Windows.Documents;
|
using System.Windows.Documents;
|
||||||
using static System.Windows.Forms.LinkLabel;
|
|
||||||
using Droplex;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -33,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);
|
||||||
|
|
@ -43,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);
|
||||||
}
|
}
|
||||||
|
|
@ -80,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();
|
||||||
|
|
||||||
|
|
@ -106,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
|
||||||
|
|
@ -299,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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
178
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
178
Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
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>
|
||||||
|
public 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 SettingDirectory => Path.Combine(DataLocation.PluginSettingsDirectory,
|
||||||
|
Context.CurrentPluginMetadata.Name);
|
||||||
|
|
||||||
|
private string SettingPath => Path.Combine(SettingDirectory, "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 bool NeedCreateSettingPanel()
|
||||||
|
{
|
||||||
|
return Settings.NeedCreateSettingPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Control CreateSettingPanel()
|
||||||
|
{
|
||||||
|
return Settings.CreateSettingPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeletePluginSettingsDirectory()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(SettingDirectory))
|
||||||
|
{
|
||||||
|
Directory.Delete(SettingDirectory, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
446
Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Normal file
446
Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Normal file
|
|
@ -0,0 +1,446 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Flow.Launcher.Infrastructure.Storage;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using CheckBox = System.Windows.Controls.CheckBox;
|
||||||
|
using ComboBox = System.Windows.Controls.ComboBox;
|
||||||
|
using Control = System.Windows.Controls.Control;
|
||||||
|
using Orientation = System.Windows.Controls.Orientation;
|
||||||
|
using TextBox = System.Windows.Controls.TextBox;
|
||||||
|
using UserControl = System.Windows.Controls.UserControl;
|
||||||
|
|
||||||
|
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 bool NeedCreateSettingPanel()
|
||||||
|
{
|
||||||
|
// If there are no settings or the settings configuration is empty, return null
|
||||||
|
return Settings != null && Configuration != null && Configuration.Body.Count != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Control CreateSettingPanel()
|
||||||
|
{
|
||||||
|
// No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true
|
||||||
|
|
||||||
|
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":
|
||||||
|
case "inputWithFolderBtn":
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
};
|
||||||
|
|
||||||
|
Btn.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
using CommonDialog dialog = type switch
|
||||||
|
{
|
||||||
|
"inputWithFolderBtn" => new FolderBrowserDialog(),
|
||||||
|
_ => new OpenFileDialog(),
|
||||||
|
};
|
||||||
|
if (dialog.ShowDialog() != DialogResult.OK) return;
|
||||||
|
|
||||||
|
var path = dialog switch
|
||||||
|
{
|
||||||
|
FolderBrowserDialog folderDialog => folderDialog.SelectedPath,
|
||||||
|
OpenFileDialog fileDialog => fileDialog.FileName,
|
||||||
|
};
|
||||||
|
textBox.Text = path;
|
||||||
|
Settings[attribute.Name] = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
Normal file
142
Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method,
|
||||||
|
argument: result.JsonRPCAction.Parameters);
|
||||||
|
|
||||||
|
return res.Hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
|
||||||
|
|
||||||
|
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||||
|
{
|
||||||
|
var res = JTF.Run(() => RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("context_menu",
|
||||||
|
new object[] { selectedResult.ContextData }));
|
||||||
|
|
||||||
|
var results = ParseResults(res);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||||
|
{
|
||||||
|
var res = await RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("query",
|
||||||
|
new object[] { query, Settings.Inner },
|
||||||
|
token);
|
||||||
|
|
||||||
|
var results = ParseResults(res);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 async Task ReloadDataAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RPC.InvokeAsync("reload_data", Context);
|
||||||
|
}
|
||||||
|
catch (RemoteMethodNotFoundException e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
189
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
Normal file
189
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
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, Action<double> reportProgress = null,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return _api.HttpDownloadAsync(url, filePath, reportProgress, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BackToQueryResults()
|
||||||
|
{
|
||||||
|
_api.BackToQueryResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartLoadingBar()
|
||||||
|
{
|
||||||
|
_api.StartLoadingBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopLoadingBar()
|
||||||
|
{
|
||||||
|
_api.StopLoadingBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,9 +1,6 @@
|
||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Text.Json;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
@ -31,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
using Flow.Launcher.Infrastructure;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
@ -37,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)
|
||||||
{
|
{
|
||||||
|
|
@ -44,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,10 @@ 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;
|
||||||
|
using Flow.Launcher.Core.Resource;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -25,11 +29,13 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
||||||
|
|
||||||
public static IPublicAPI API { private set; get; }
|
// We should not initialize API in static constructor because it will create another API instance
|
||||||
|
private static IPublicAPI api = null;
|
||||||
|
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -48,6 +54,9 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save json and ISavable
|
||||||
|
/// </summary>
|
||||||
public static void Save()
|
public static void Save()
|
||||||
{
|
{
|
||||||
foreach (var plugin in AllPlugins)
|
foreach (var plugin in AllPlugins)
|
||||||
|
|
@ -85,6 +94,48 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}).ToArray());
|
}).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||||
|
{
|
||||||
|
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||||
|
{
|
||||||
|
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
|
||||||
|
_ => Task.CompletedTask,
|
||||||
|
}).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task CloseExternalPreviewAsync()
|
||||||
|
{
|
||||||
|
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||||
|
{
|
||||||
|
IAsyncExternalPreview p => p.ClosePreviewAsync(),
|
||||||
|
_ => Task.CompletedTask,
|
||||||
|
}).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||||
|
{
|
||||||
|
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||||
|
{
|
||||||
|
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
|
||||||
|
_ => Task.CompletedTask,
|
||||||
|
}).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool UseExternalPreview()
|
||||||
|
{
|
||||||
|
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool AllowAlwaysPreview()
|
||||||
|
{
|
||||||
|
var plugin = GetPluginsForInterface<IAsyncExternalPreview>().FirstOrDefault(x => !x.Metadata.Disabled);
|
||||||
|
|
||||||
|
if (plugin is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
|
||||||
|
}
|
||||||
|
|
||||||
static PluginManager()
|
static PluginManager()
|
||||||
{
|
{
|
||||||
// validate user directory
|
// validate user directory
|
||||||
|
|
@ -110,9 +161,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// Call initialize for all plugins
|
/// Call initialize for all plugins
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>return the list of failed to init plugins or null for none</returns>
|
/// <returns>return the list of failed to init plugins or null for none</returns>
|
||||||
public static async Task InitializePluginsAsync(IPublicAPI api)
|
public static async Task InitializePluginsAsync()
|
||||||
{
|
{
|
||||||
API = api;
|
|
||||||
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
||||||
|
|
||||||
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
|
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
|
||||||
|
|
@ -155,12 +205,21 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
|
||||||
|
InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService<Settings>().Language);
|
||||||
|
|
||||||
if (failedPlugins.Any())
|
if (failedPlugins.Any())
|
||||||
{
|
{
|
||||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||||
API.ShowMsg($"Fail to Init Plugins",
|
API.ShowMsg(
|
||||||
$"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help",
|
API.GetTranslation("failedToInitializePluginsTitle"),
|
||||||
"", false);
|
string.Format(
|
||||||
|
API.GetTranslation("failedToInitializePluginsMessage"),
|
||||||
|
failed
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,11 +227,11 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
if (query is null)
|
if (query is null)
|
||||||
return Array.Empty<PluginPair>();
|
return Array.Empty<PluginPair>();
|
||||||
|
|
||||||
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
||||||
return GlobalPlugins;
|
return GlobalPlugins;
|
||||||
|
|
||||||
|
|
||||||
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
||||||
return new List<PluginPair>
|
return new List<PluginPair>
|
||||||
{
|
{
|
||||||
|
|
@ -207,12 +266,24 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UpdatePluginMetadata(List<Result> results, PluginMetadata metadata, Query query)
|
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
|
||||||
{
|
{
|
||||||
foreach (var r in results)
|
foreach (var r in results)
|
||||||
{
|
{
|
||||||
|
|
@ -220,8 +291,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
r.PluginID = metadata.ID;
|
r.PluginID = metadata.ID;
|
||||||
r.OriginQuery = query;
|
r.OriginQuery = query;
|
||||||
|
|
||||||
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
|
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
|
||||||
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
|
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
|
||||||
if (metadata.ActionKeywords.Count == 1)
|
if (metadata.ActionKeywords.Count == 1)
|
||||||
r.ActionKeywordAssigned = query.ActionKeyword;
|
r.ActionKeywordAssigned = query.ActionKeyword;
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +310,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
|
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
|
||||||
{
|
{
|
||||||
return AllPlugins.Where(p => p.Plugin is T);
|
// Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup
|
||||||
|
return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty<PluginPair>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Result> GetContextMenusForPlugin(Result result)
|
public static List<Result> GetContextMenusForPlugin(Result result)
|
||||||
|
|
@ -328,5 +400,215 @@ 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, removePluginFromSettings:false, removePluginSettings: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 removePluginFromSettings = true, bool removePluginSettings = false)
|
||||||
|
{
|
||||||
|
UninstallPlugin(plugin, removePluginFromSettings, removePluginSettings, 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, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(tempFolderPluginPath))
|
||||||
|
Directory.Delete(tempFolderPluginPath, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Exception($"|PluginManager.InstallPlugin|Failed to delete temp folder {tempFolderPluginPath}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkModified)
|
||||||
|
{
|
||||||
|
_modifiedPlugins.Add(plugin.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
|
||||||
|
{
|
||||||
|
if (checkModified && PluginModified(plugin.ID))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removePluginSettings)
|
||||||
|
{
|
||||||
|
if (AllowedLanguage.IsDotNet(plugin.Language)) // for the plugin in .NET, we can use assembly loader
|
||||||
|
{
|
||||||
|
var assemblyLoader = new PluginAssemblyLoader(plugin.ExecuteFilePath);
|
||||||
|
var assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||||
|
var assemblyName = assembly.GetName().Name;
|
||||||
|
|
||||||
|
// if user want to remove the plugin settings, we cannot call save method for the plugin json storage instance of this plugin
|
||||||
|
// so we need to remove it from the api instance
|
||||||
|
var method = API.GetType().GetMethod("RemovePluginSettings");
|
||||||
|
var pluginJsonStorage = method?.Invoke(API, new object[] { assemblyName });
|
||||||
|
|
||||||
|
// if there exists a json storage for current plugin, we need to delete the directory path
|
||||||
|
if (pluginJsonStorage != null)
|
||||||
|
{
|
||||||
|
var deleteMethod = pluginJsonStorage.GetType().GetMethod("DeleteDirectory");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
deleteMethod?.Invoke(pluginJsonStorage, null);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e);
|
||||||
|
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
|
||||||
|
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // the plugin with json prc interface
|
||||||
|
{
|
||||||
|
var pluginPair = AllPlugins.FirstOrDefault(p => p.Metadata.ID == plugin.ID);
|
||||||
|
if (pluginPair != null && pluginPair.Plugin is JsonRPCPlugin jsonRpcPlugin)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jsonRpcPlugin.DeletePluginSettingsDirectory();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e);
|
||||||
|
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
|
||||||
|
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removePluginFromSettings)
|
||||||
|
{
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
||||||
|
#pragma warning disable IDE0005
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
#pragma warning restore IDE0005
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||||
|
|
@ -17,22 +20,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +105,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;
|
||||||
}
|
}
|
||||||
|
|
@ -106,17 +120,17 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||||
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
||||||
$"Please refer to the logs for more information", "",
|
$"Please refer to the logs for more information", "",
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|
@ -125,5 +139,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()
|
||||||
|
{
|
||||||
|
await base.DisposeAsync();
|
||||||
|
ClientProcess.Kill(true);
|
||||||
|
await ClientProcess.WaitForExitAsync();
|
||||||
|
ClientProcess.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
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;
|
||||||
|
|
@ -23,22 +24,20 @@ 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;
|
||||||
|
// Prevent Python from writing .py[co] files.
|
||||||
|
// Because .pyc contains location infos which will prevent python portable.
|
||||||
|
_startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
|
||||||
|
|
||||||
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
|
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
|
||||||
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
|
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
|
||||||
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
|
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
|
||||||
|
|
||||||
|
|
||||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
|
||||||
_startInfo.ArgumentList.Add("-B");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task<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);
|
||||||
}
|
}
|
||||||
|
|
@ -46,15 +45,58 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task InitAsync(PluginInitContext context)
|
public override async Task InitAsync(PluginInitContext context)
|
||||||
{
|
{
|
||||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
// Run .py files via `-c <code>`
|
||||||
_startInfo.ArgumentList.Add("");
|
if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
var libDirectory = Path.Combine(rootDirectory, "lib");
|
||||||
|
var libPyWin32Directory = Path.Combine(libDirectory, "win32");
|
||||||
|
var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
|
||||||
|
var pluginDirectory = Path.Combine(rootDirectory, "plugin");
|
||||||
|
|
||||||
|
// This makes it easier for plugin authors to import their own modules.
|
||||||
|
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
|
||||||
|
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
|
||||||
|
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
|
||||||
|
_startInfo.ArgumentList.Add("-c");
|
||||||
|
_startInfo.ArgumentList.Add(
|
||||||
|
$"""
|
||||||
|
import sys
|
||||||
|
sys.path.append(r'{rootDirectory}')
|
||||||
|
sys.path.append(r'{libDirectory}')
|
||||||
|
sys.path.append(r'{libPyWin32LibDirectory}')
|
||||||
|
sys.path.append(r'{libPyWin32Directory}')
|
||||||
|
sys.path.append(r'{pluginDirectory}')
|
||||||
|
|
||||||
|
import runpy
|
||||||
|
runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__')
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
// Plugins always expect the JSON data to be in the third argument
|
||||||
|
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
|
||||||
|
_startInfo.ArgumentList.Add("");
|
||||||
|
}
|
||||||
|
// Run .pyz files as is
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now,
|
||||||
|
// but the plugins still expect data to be sent as the third argument, so we're keeping
|
||||||
|
// the flag here, even though it's not necessary anymore.
|
||||||
|
_startInfo.ArgumentList.Add("-B");
|
||||||
|
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||||
|
// Plugins always expect the JSON data to be in the third argument
|
||||||
|
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
|
||||||
|
_startInfo.ArgumentList.Add("");
|
||||||
|
}
|
||||||
|
|
||||||
await base.InitAsync(context);
|
await base.InitAsync(context);
|
||||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
73
Flow.Launcher.Core/Plugin/PythonPluginV2.cs
Normal file
73
Flow.Launcher.Core/Plugin/PythonPluginV2.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
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;
|
||||||
|
StartInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task InitAsync(PluginInitContext context)
|
||||||
|
{
|
||||||
|
// Run .py files via `-c <code>`
|
||||||
|
if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
var libDirectory = Path.Combine(rootDirectory, "lib");
|
||||||
|
var libPyWin32Directory = Path.Combine(libDirectory, "win32");
|
||||||
|
var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
|
||||||
|
var pluginDirectory = Path.Combine(rootDirectory, "plugin");
|
||||||
|
var filePath = context.CurrentPluginMetadata.ExecuteFilePath;
|
||||||
|
|
||||||
|
// This makes it easier for plugin authors to import their own modules.
|
||||||
|
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
|
||||||
|
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
|
||||||
|
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
|
||||||
|
StartInfo.ArgumentList.Add("-c");
|
||||||
|
StartInfo.ArgumentList.Add(
|
||||||
|
$"""
|
||||||
|
import sys
|
||||||
|
sys.path.append(r'{rootDirectory}')
|
||||||
|
sys.path.append(r'{libDirectory}')
|
||||||
|
sys.path.append(r'{libPyWin32LibDirectory}')
|
||||||
|
sys.path.append(r'{libPyWin32Directory}')
|
||||||
|
sys.path.append(r'{pluginDirectory}')
|
||||||
|
|
||||||
|
import runpy
|
||||||
|
runpy.run_path(r'{filePath}', None, '__main__')
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Run .pyz files as is
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||||
|
}
|
||||||
|
await base.InitAsync(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MessageHandlerType MessageHandler { get; } = MessageHandlerType.NewLineDelimited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
@ -34,9 +33,13 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
searchTerms = terms;
|
searchTerms = terms;
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = new Query(rawQuery, search,terms, searchTerms, actionKeyword);
|
return new Query ()
|
||||||
|
{
|
||||||
return query;
|
Search = search,
|
||||||
|
RawQuery = rawQuery,
|
||||||
|
SearchTerms = searchTerms,
|
||||||
|
ActionKeyword = actionKeyword
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Resource
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
|
|
@ -23,8 +23,12 @@ namespace Flow.Launcher.Core.Resource
|
||||||
public static Language Spanish_LatinAmerica = new Language("es-419", "Spanish (Latin America)");
|
public static Language Spanish_LatinAmerica = new Language("es-419", "Spanish (Latin America)");
|
||||||
public static Language Italian = new Language("it", "Italiano");
|
public static Language Italian = new Language("it", "Italiano");
|
||||||
public static Language Norwegian_Bokmal = new Language("nb-NO", "Norsk Bokmål");
|
public static Language Norwegian_Bokmal = new Language("nb-NO", "Norsk Bokmål");
|
||||||
public static Language Slovak = new Language("sk", "Slovenský");
|
public static Language Slovak = new Language("sk", "Slovenčina");
|
||||||
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 Arabic = new Language("ar", "اللغة العربية");
|
||||||
|
public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt");
|
||||||
|
public static Language Hebrew = new Language("he", "עברית");
|
||||||
|
|
||||||
public static List<Language> GetAvailableLanguages()
|
public static List<Language> GetAvailableLanguages()
|
||||||
{
|
{
|
||||||
|
|
@ -50,9 +54,46 @@ namespace Flow.Launcher.Core.Resource
|
||||||
Italian,
|
Italian,
|
||||||
Norwegian_Bokmal,
|
Norwegian_Bokmal,
|
||||||
Slovak,
|
Slovak,
|
||||||
Turkish
|
Turkish,
|
||||||
|
Czech,
|
||||||
|
Arabic,
|
||||||
|
Vietnamese,
|
||||||
|
Hebrew
|
||||||
};
|
};
|
||||||
return languages;
|
return languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetSystemTranslation(string languageCode)
|
||||||
|
{
|
||||||
|
return languageCode switch
|
||||||
|
{
|
||||||
|
"en" => "System",
|
||||||
|
"zh-cn" => "系统",
|
||||||
|
"zh-tw" => "系統",
|
||||||
|
"uk-UA" => "Система",
|
||||||
|
"ru" => "Система",
|
||||||
|
"fr" => "Système",
|
||||||
|
"ja" => "システム",
|
||||||
|
"nl" => "Systeem",
|
||||||
|
"pl" => "System",
|
||||||
|
"da" => "System",
|
||||||
|
"de" => "System",
|
||||||
|
"ko" => "시스템",
|
||||||
|
"sr" => "Систем",
|
||||||
|
"pt-pt" => "Sistema",
|
||||||
|
"pt-br" => "Sistema",
|
||||||
|
"es" => "Sistema",
|
||||||
|
"es-419" => "Sistema",
|
||||||
|
"it" => "Sistema",
|
||||||
|
"nb-NO" => "System",
|
||||||
|
"sk" => "Systém",
|
||||||
|
"tr" => "Sistem",
|
||||||
|
"cs" => "Systém",
|
||||||
|
"ar" => "النظام",
|
||||||
|
"vi-vn" => "Hệ thống",
|
||||||
|
"he" => "מערכת",
|
||||||
|
_ => "System",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,38 +11,65 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Resource
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
public class Internationalization
|
public class Internationalization
|
||||||
{
|
{
|
||||||
public Settings Settings { get; set; }
|
|
||||||
private const string Folder = "Languages";
|
private const string Folder = "Languages";
|
||||||
|
private const string DefaultLanguageCode = "en";
|
||||||
private const string DefaultFile = "en.xaml";
|
private const string DefaultFile = "en.xaml";
|
||||||
private const string Extension = ".xaml";
|
private const string Extension = ".xaml";
|
||||||
|
private readonly Settings _settings;
|
||||||
private readonly List<string> _languageDirectories = new List<string>();
|
private readonly List<string> _languageDirectories = new List<string>();
|
||||||
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
|
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
|
||||||
|
private readonly string SystemLanguageCode;
|
||||||
|
|
||||||
public Internationalization()
|
public Internationalization(Settings settings)
|
||||||
{
|
{
|
||||||
AddPluginLanguageDirectories();
|
_settings = settings;
|
||||||
LoadDefaultLanguage();
|
|
||||||
// we don't want to load /Languages/en.xaml twice
|
|
||||||
// so add flowlauncher language directory after load plugin language files
|
|
||||||
AddFlowLauncherLanguageDirectory();
|
AddFlowLauncherLanguageDirectory();
|
||||||
|
SystemLanguageCode = GetSystemLanguageCodeAtStartup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void AddFlowLauncherLanguageDirectory()
|
private void AddFlowLauncherLanguageDirectory()
|
||||||
{
|
{
|
||||||
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
||||||
_languageDirectories.Add(directory);
|
_languageDirectories.Add(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetSystemLanguageCodeAtStartup()
|
||||||
private void AddPluginLanguageDirectories()
|
|
||||||
{
|
{
|
||||||
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
var availableLanguages = AvailableLanguages.GetAvailableLanguages();
|
||||||
|
|
||||||
|
// Retrieve the language identifiers for the current culture.
|
||||||
|
// ChangeLanguage method overrides the CultureInfo.CurrentCulture, so this needs to
|
||||||
|
// be called at startup in order to get the correct lang code of system.
|
||||||
|
var currentCulture = CultureInfo.CurrentCulture;
|
||||||
|
var twoLetterCode = currentCulture.TwoLetterISOLanguageName;
|
||||||
|
var threeLetterCode = currentCulture.ThreeLetterISOLanguageName;
|
||||||
|
var fullName = currentCulture.Name;
|
||||||
|
|
||||||
|
// Try to find a match in the available languages list
|
||||||
|
foreach (var language in availableLanguages)
|
||||||
|
{
|
||||||
|
var languageCode = language.LanguageCode;
|
||||||
|
|
||||||
|
if (string.Equals(languageCode, twoLetterCode, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return languageCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultLanguageCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AddPluginLanguageDirectories(IEnumerable<PluginPair> plugins)
|
||||||
|
{
|
||||||
|
foreach (var plugin in plugins)
|
||||||
{
|
{
|
||||||
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
||||||
var dir = Path.GetDirectoryName(location);
|
var dir = Path.GetDirectoryName(location);
|
||||||
|
|
@ -56,10 +83,15 @@ namespace Flow.Launcher.Core.Resource
|
||||||
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
|
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadDefaultLanguage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadDefaultLanguage()
|
private void LoadDefaultLanguage()
|
||||||
{
|
{
|
||||||
|
// Removes language files loaded before any plugins were loaded.
|
||||||
|
// Prevents the language Flow started in from overwriting English if the user switches back to English
|
||||||
|
RemoveOldLanguageFiles();
|
||||||
LoadLanguage(AvailableLanguages.English);
|
LoadLanguage(AvailableLanguages.English);
|
||||||
_oldResources.Clear();
|
_oldResources.Clear();
|
||||||
}
|
}
|
||||||
|
|
@ -67,8 +99,18 @@ namespace Flow.Launcher.Core.Resource
|
||||||
public void ChangeLanguage(string languageCode)
|
public void ChangeLanguage(string languageCode)
|
||||||
{
|
{
|
||||||
languageCode = languageCode.NonNull();
|
languageCode = languageCode.NonNull();
|
||||||
Language language = GetLanguageByLanguageCode(languageCode);
|
|
||||||
ChangeLanguage(language);
|
// Get actual language if language code is system
|
||||||
|
var isSystem = false;
|
||||||
|
if (languageCode == Constant.SystemLanguageCode)
|
||||||
|
{
|
||||||
|
languageCode = SystemLanguageCode;
|
||||||
|
isSystem = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get language by language code and change language
|
||||||
|
var language = GetLanguageByLanguageCode(languageCode);
|
||||||
|
ChangeLanguage(language, isSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Language GetLanguageByLanguageCode(string languageCode)
|
private Language GetLanguageByLanguageCode(string languageCode)
|
||||||
|
|
@ -86,26 +128,22 @@ namespace Flow.Launcher.Core.Resource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeLanguage(Language language)
|
private void ChangeLanguage(Language language, bool isSystem)
|
||||||
{
|
{
|
||||||
language = language.NonNull();
|
language = language.NonNull();
|
||||||
|
|
||||||
|
|
||||||
RemoveOldLanguageFiles();
|
RemoveOldLanguageFiles();
|
||||||
if (language != AvailableLanguages.English)
|
if (language != AvailableLanguages.English)
|
||||||
{
|
{
|
||||||
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 = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
UpdatePluginMetadataTranslations();
|
UpdatePluginMetadataTranslations();
|
||||||
|
|
@ -116,7 +154,7 @@ namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
|
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
|
||||||
|
|
||||||
if (Settings.ShouldUsePinyin)
|
if (_settings.ShouldUsePinyin)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
|
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
|
||||||
|
|
@ -126,7 +164,7 @@ namespace Flow.Launcher.Core.Resource
|
||||||
// "Do you want to search with pinyin?"
|
// "Do you want to search with pinyin?"
|
||||||
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
|
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
|
||||||
|
|
||||||
if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
if (Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -143,11 +181,14 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
private void LoadLanguage(Language language)
|
private void LoadLanguage(Language language)
|
||||||
{
|
{
|
||||||
|
var flowEnglishFile = Path.Combine(Constant.ProgramDirectory, Folder, DefaultFile);
|
||||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||||
var filename = $"{language.LanguageCode}{Extension}";
|
var filename = $"{language.LanguageCode}{Extension}";
|
||||||
var files = _languageDirectories
|
var files = _languageDirectories
|
||||||
.Select(d => LanguageFile(d, filename))
|
.Select(d => LanguageFile(d, filename))
|
||||||
.Where(f => !string.IsNullOrEmpty(f))
|
// Exclude Flow's English language file since it's built into the binary, and there's no need to load
|
||||||
|
// it again from the file system.
|
||||||
|
.Where(f => !string.IsNullOrEmpty(f) && f != flowEnglishFile)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (files.Length > 0)
|
if (files.Length > 0)
|
||||||
|
|
@ -166,7 +207,9 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
public List<Language> LoadAvailableLanguages()
|
public List<Language> LoadAvailableLanguages()
|
||||||
{
|
{
|
||||||
return AvailableLanguages.GetAvailableLanguages();
|
var list = AvailableLanguages.GetAvailableLanguages();
|
||||||
|
list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetTranslation(string key)
|
public string GetTranslation(string key)
|
||||||
|
|
@ -193,7 +236,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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,12 @@
|
||||||
namespace Flow.Launcher.Core.Resource
|
using System;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
|
[Obsolete("InternationalizationManager.Instance is obsolete. Use Ioc.Default.GetRequiredService<Internationalization>() instead.")]
|
||||||
public static class InternationalizationManager
|
public static class InternationalizationManager
|
||||||
{
|
{
|
||||||
private static Internationalization instance;
|
|
||||||
private static object syncObject = new object();
|
|
||||||
|
|
||||||
public static Internationalization Instance
|
public static Internationalization Instance
|
||||||
{
|
=> Ioc.Default.GetRequiredService<Internationalization>();
|
||||||
get
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
lock (syncObject)
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new Internationalization();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,34 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Xml;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Interop;
|
|
||||||
using System.Windows.Markup;
|
using System.Windows.Markup;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Effects;
|
using System.Windows.Media.Effects;
|
||||||
|
using System.Windows.Shell;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Resource
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
public class Theme
|
public class Theme
|
||||||
{
|
{
|
||||||
|
private const string ThemeMetadataNamePrefix = "Name:";
|
||||||
|
private const string ThemeMetadataIsDarkPrefix = "IsDark:";
|
||||||
|
private const string ThemeMetadataHasBlurPrefix = "HasBlur:";
|
||||||
|
|
||||||
private const int ShadowExtraMargin = 32;
|
private const int ShadowExtraMargin = 32;
|
||||||
|
|
||||||
private readonly List<string> _themeDirectories = new List<string>();
|
private readonly IPublicAPI _api;
|
||||||
|
private readonly Settings _settings;
|
||||||
|
private readonly List<string> _themeDirectories = new();
|
||||||
private ResourceDictionary _oldResource;
|
private ResourceDictionary _oldResource;
|
||||||
private string _oldTheme;
|
private string _oldTheme;
|
||||||
public Settings Settings { get; set; }
|
|
||||||
private const string Folder = Constant.Themes;
|
private const string Folder = Constant.Themes;
|
||||||
private const string Extension = ".xaml";
|
private const string Extension = ".xaml";
|
||||||
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
||||||
|
|
@ -32,8 +39,11 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
private double mainWindowWidth;
|
private double mainWindowWidth;
|
||||||
|
|
||||||
public Theme()
|
public Theme(IPublicAPI publicAPI, Settings settings)
|
||||||
{
|
{
|
||||||
|
_api = publicAPI;
|
||||||
|
_settings = settings;
|
||||||
|
|
||||||
_themeDirectories.Add(DirectoryPath);
|
_themeDirectories.Add(DirectoryPath);
|
||||||
_themeDirectories.Add(UserDirectoryPath);
|
_themeDirectories.Add(UserDirectoryPath);
|
||||||
MakeSureThemeDirectoriesExist();
|
MakeSureThemeDirectoriesExist();
|
||||||
|
|
@ -79,33 +89,33 @@ namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
|
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
|
||||||
|
|
||||||
// reload all resources even if the theme itself hasn't changed in order to pickup changes
|
// reload all resources even if the theme itself hasn't changed in order to pickup changes
|
||||||
// to things like fonts
|
// to things like fonts
|
||||||
UpdateResourceDictionary(GetResourceDictionary(theme));
|
UpdateResourceDictionary(GetResourceDictionary(theme));
|
||||||
|
|
||||||
Settings.Theme = theme;
|
|
||||||
|
|
||||||
|
_settings.Theme = theme;
|
||||||
|
|
||||||
|
|
||||||
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
|
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
|
||||||
if (_oldTheme != theme || theme == defaultTheme)
|
if (_oldTheme != theme || theme == defaultTheme)
|
||||||
{
|
{
|
||||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlurEnabled = IsBlurTheme();
|
BlurEnabled = Win32Helper.IsBlurTheme();
|
||||||
|
|
||||||
if (Settings.UseDropShadowEffect && !BlurEnabled)
|
if (_settings.UseDropShadowEffect && !BlurEnabled)
|
||||||
AddDropShadowEffectToCurrentTheme();
|
AddDropShadowEffectToCurrentTheme();
|
||||||
|
|
||||||
SetBlurForWindow();
|
Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
|
||||||
}
|
}
|
||||||
catch (DirectoryNotFoundException)
|
catch (DirectoryNotFoundException)
|
||||||
{
|
{
|
||||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
|
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
|
||||||
if (theme != defaultTheme)
|
if (theme != defaultTheme)
|
||||||
{
|
{
|
||||||
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
|
_api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
|
||||||
ChangeTheme(defaultTheme);
|
ChangeTheme(defaultTheme);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -115,7 +125,7 @@ namespace Flow.Launcher.Core.Resource
|
||||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
|
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
|
||||||
if (theme != defaultTheme)
|
if (theme != defaultTheme)
|
||||||
{
|
{
|
||||||
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
|
_api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
|
||||||
ChangeTheme(defaultTheme);
|
ChangeTheme(defaultTheme);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -143,19 +153,19 @@ namespace Flow.Launcher.Core.Resource
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(Settings.Theme);
|
private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(_settings.Theme);
|
||||||
|
|
||||||
public ResourceDictionary GetResourceDictionary(string theme)
|
public ResourceDictionary GetResourceDictionary(string theme)
|
||||||
{
|
{
|
||||||
var dict = GetThemeResourceDictionary(theme);
|
var dict = GetThemeResourceDictionary(theme);
|
||||||
|
|
||||||
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
|
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
|
||||||
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
|
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
|
||||||
{
|
{
|
||||||
var fontFamily = new FontFamily(Settings.QueryBoxFont);
|
var fontFamily = new FontFamily(_settings.QueryBoxFont);
|
||||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle);
|
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle);
|
||||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.QueryBoxFontWeight);
|
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.QueryBoxFontStretch);
|
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||||
|
|
||||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
||||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
||||||
|
|
@ -176,25 +186,39 @@ 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)
|
||||||
{
|
{
|
||||||
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultFont));
|
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultFont));
|
||||||
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultFontStyle));
|
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle));
|
||||||
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultFontWeight));
|
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight));
|
||||||
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultFontStretch));
|
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch));
|
||||||
|
|
||||||
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;
|
||||||
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
|
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
|
||||||
mainWindowWidth = (double)width;
|
mainWindowWidth = (double)width;
|
||||||
return dict;
|
return dict;
|
||||||
|
|
@ -202,20 +226,56 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
private ResourceDictionary GetCurrentResourceDictionary( )
|
private ResourceDictionary GetCurrentResourceDictionary( )
|
||||||
{
|
{
|
||||||
return GetResourceDictionary(Settings.Theme);
|
return GetResourceDictionary(_settings.Theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<string> LoadAvailableThemes()
|
public List<ThemeData> LoadAvailableThemes()
|
||||||
{
|
{
|
||||||
List<string> themes = new List<string>();
|
List<ThemeData> themes = new List<ThemeData>();
|
||||||
foreach (var themeDirectory in _themeDirectories)
|
foreach (var themeDirectory in _themeDirectories)
|
||||||
{
|
{
|
||||||
themes.AddRange(
|
var filePaths = Directory
|
||||||
Directory.GetFiles(themeDirectory)
|
.GetFiles(themeDirectory)
|
||||||
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
|
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
|
||||||
.ToList());
|
.Select(GetThemeDataFromPath);
|
||||||
|
themes.AddRange(filePaths);
|
||||||
}
|
}
|
||||||
return themes.OrderBy(o => o).ToList();
|
|
||||||
|
return themes.OrderBy(o => o.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ThemeData GetThemeDataFromPath(string path)
|
||||||
|
{
|
||||||
|
using var reader = XmlReader.Create(path);
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
var extensionlessName = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
if (reader.NodeType is not XmlNodeType.Comment)
|
||||||
|
return new ThemeData(extensionlessName, extensionlessName);
|
||||||
|
|
||||||
|
var commentLines = reader.Value.Trim().Split('\n').Select(v => v.Trim());
|
||||||
|
|
||||||
|
var name = extensionlessName;
|
||||||
|
bool? isDark = null;
|
||||||
|
bool? hasBlur = null;
|
||||||
|
foreach (var line in commentLines)
|
||||||
|
{
|
||||||
|
if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim();
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim());
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ThemeData(extensionlessName, name, isDark, hasBlur);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetThemePath(string themeName)
|
private string GetThemePath(string themeName)
|
||||||
|
|
@ -253,12 +313,15 @@ namespace Flow.Launcher.Core.Resource
|
||||||
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
||||||
if (marginSetter == null)
|
if (marginSetter == null)
|
||||||
{
|
{
|
||||||
|
var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin);
|
||||||
marginSetter = new Setter()
|
marginSetter = new Setter()
|
||||||
{
|
{
|
||||||
Property = Border.MarginProperty,
|
Property = Border.MarginProperty,
|
||||||
Value = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin),
|
Value = margin,
|
||||||
};
|
};
|
||||||
windowBorderStyle.Setters.Add(marginSetter);
|
windowBorderStyle.Setters.Add(marginSetter);
|
||||||
|
|
||||||
|
SetResizeBoarderThickness(margin);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -269,6 +332,8 @@ namespace Flow.Launcher.Core.Resource
|
||||||
baseMargin.Right + ShadowExtraMargin,
|
baseMargin.Right + ShadowExtraMargin,
|
||||||
baseMargin.Bottom + ShadowExtraMargin);
|
baseMargin.Bottom + ShadowExtraMargin);
|
||||||
marginSetter.Value = newMargin;
|
marginSetter.Value = newMargin;
|
||||||
|
|
||||||
|
SetResizeBoarderThickness(newMargin);
|
||||||
}
|
}
|
||||||
|
|
||||||
windowBorderStyle.Setters.Add(effectSetter);
|
windowBorderStyle.Setters.Add(effectSetter);
|
||||||
|
|
@ -299,99 +364,36 @@ namespace Flow.Launcher.Core.Resource
|
||||||
marginSetter.Value = newMargin;
|
marginSetter.Value = newMargin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetResizeBoarderThickness(null);
|
||||||
|
|
||||||
UpdateResourceDictionary(dict);
|
UpdateResourceDictionary(dict);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Blur Handling
|
// because adding drop shadow effect will change the margin of the window,
|
||||||
/*
|
// we need to update the window chrome thickness to correct set the resize border
|
||||||
Found on https://github.com/riverar/sample-win10-aeroglass
|
private static void SetResizeBoarderThickness(Thickness? effectMargin)
|
||||||
*/
|
|
||||||
private enum AccentState
|
|
||||||
{
|
{
|
||||||
ACCENT_DISABLED = 0,
|
var window = Application.Current.MainWindow;
|
||||||
ACCENT_ENABLE_GRADIENT = 1,
|
if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome)
|
||||||
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
|
|
||||||
ACCENT_ENABLE_BLURBEHIND = 3,
|
|
||||||
ACCENT_INVALID_STATE = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct AccentPolicy
|
|
||||||
{
|
|
||||||
public AccentState AccentState;
|
|
||||||
public int AccentFlags;
|
|
||||||
public int GradientColor;
|
|
||||||
public int AnimationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct WindowCompositionAttributeData
|
|
||||||
{
|
|
||||||
public WindowCompositionAttribute Attribute;
|
|
||||||
public IntPtr Data;
|
|
||||||
public int SizeOfData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum WindowCompositionAttribute
|
|
||||||
{
|
|
||||||
WCA_ACCENT_POLICY = 19
|
|
||||||
}
|
|
||||||
[DllImport("user32.dll")]
|
|
||||||
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the blur for a window via SetWindowCompositionAttribute
|
|
||||||
/// </summary>
|
|
||||||
public void SetBlurForWindow()
|
|
||||||
{
|
|
||||||
if (BlurEnabled)
|
|
||||||
{
|
{
|
||||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
|
Thickness thickness;
|
||||||
}
|
if (effectMargin == null)
|
||||||
else
|
{
|
||||||
{
|
thickness = SystemParameters.WindowResizeBorderThickness;
|
||||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
thickness = new Thickness(
|
||||||
|
effectMargin.Value.Left + SystemParameters.WindowResizeBorderThickness.Left,
|
||||||
|
effectMargin.Value.Top + SystemParameters.WindowResizeBorderThickness.Top,
|
||||||
|
effectMargin.Value.Right + SystemParameters.WindowResizeBorderThickness.Right,
|
||||||
|
effectMargin.Value.Bottom + SystemParameters.WindowResizeBorderThickness.Bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
windowChrome.ResizeBorderThickness = thickness;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsBlurTheme()
|
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
|
||||||
{
|
|
||||||
if (Environment.OSVersion.Version >= new Version(6, 2))
|
|
||||||
{
|
|
||||||
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
|
||||||
|
|
||||||
if (resource is bool)
|
|
||||||
return (bool)resource;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetWindowAccent(Window w, AccentState state)
|
|
||||||
{
|
|
||||||
var windowHelper = new WindowInteropHelper(w);
|
|
||||||
|
|
||||||
windowHelper.EnsureHandle();
|
|
||||||
|
|
||||||
var accent = new AccentPolicy { AccentState = state };
|
|
||||||
var accentStructSize = Marshal.SizeOf(accent);
|
|
||||||
|
|
||||||
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
|
|
||||||
Marshal.StructureToPtr(accent, accentPtr, false);
|
|
||||||
|
|
||||||
var data = new WindowCompositionAttributeData
|
|
||||||
{
|
|
||||||
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
|
|
||||||
SizeOfData = accentStructSize,
|
|
||||||
Data = accentPtr
|
|
||||||
};
|
|
||||||
|
|
||||||
SetWindowCompositionAttribute(windowHelper.Handle, ref data);
|
|
||||||
|
|
||||||
Marshal.FreeHGlobal(accentPtr);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,12 @@
|
||||||
namespace Flow.Launcher.Core.Resource
|
using System;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
|
[Obsolete("ThemeManager.Instance is obsolete. Use Ioc.Default.GetRequiredService<Theme>() instead.")]
|
||||||
public class ThemeManager
|
public class ThemeManager
|
||||||
{
|
{
|
||||||
private static Theme instance;
|
|
||||||
private static object syncObject = new object();
|
|
||||||
|
|
||||||
public static Theme Instance
|
public static Theme Instance
|
||||||
{
|
=> Ioc.Default.GetRequiredService<Theme>();
|
||||||
get
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
lock (syncObject)
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new Theme();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,24 +23,33 @@ namespace Flow.Launcher.Core
|
||||||
public class Updater
|
public class Updater
|
||||||
{
|
{
|
||||||
public bool UpdateToPrerelease { get; set; }
|
public bool UpdateToPrerelease { get; set; }
|
||||||
|
|
||||||
|
// TODO
|
||||||
public const string ReleaseRepository = "https://github.com/Flow-Launcher/Flow.Launcher";
|
public const string ReleaseRepository = "https://github.com/Flow-Launcher/Flow.Launcher";
|
||||||
public const string PrereleaseRepository = "https://github.com/Flow-Launcher/Prereleases";
|
public const string PrereleaseRepository = "https://github.com/Flow-Launcher/Prereleases";
|
||||||
|
|
||||||
|
// TODO
|
||||||
public string GitHubRepository => UpdateToPrerelease ? PrereleaseRepository : ReleaseRepository;
|
public string GitHubRepository => UpdateToPrerelease ? PrereleaseRepository : ReleaseRepository;
|
||||||
|
|
||||||
|
private readonly IPublicAPI _api;
|
||||||
|
|
||||||
|
public Updater(IPublicAPI publicAPI, string gitHubRepository)
|
||||||
|
{
|
||||||
|
_api = publicAPI;
|
||||||
|
GitHubRepository = gitHubRepository;
|
||||||
|
}
|
||||||
|
|
||||||
private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1);
|
private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1);
|
||||||
|
|
||||||
public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
|
public async Task UpdateAppAsync(bool silentUpdate = true)
|
||||||
{
|
{
|
||||||
await UpdateLock.WaitAsync().ConfigureAwait(false);
|
await UpdateLock.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!silentUpdate)
|
if (!silentUpdate)
|
||||||
api.ShowMsg(api.GetTranslation("pleaseWait"),
|
_api.ShowMsg(_api.GetTranslation("pleaseWait"),
|
||||||
api.GetTranslation("update_flowlauncher_update_check"));
|
_api.GetTranslation("update_flowlauncher_update_check"));
|
||||||
|
|
||||||
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
@ -55,14 +64,13 @@ namespace Flow.Launcher.Core
|
||||||
if (newReleaseVersion <= currentVersion)
|
if (newReleaseVersion <= currentVersion)
|
||||||
{
|
{
|
||||||
if (!silentUpdate)
|
if (!silentUpdate)
|
||||||
MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest"));
|
_api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!silentUpdate)
|
if (!silentUpdate)
|
||||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"),
|
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
|
||||||
api.GetTranslation("update_flowlauncher_updating"));
|
_api.GetTranslation("update_flowlauncher_updating"));
|
||||||
|
|
||||||
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
|
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
@ -71,9 +79,9 @@ namespace Flow.Launcher.Core
|
||||||
if (DataLocation.PortableDataLocationInUse())
|
if (DataLocation.PortableDataLocationInUse())
|
||||||
{
|
{
|
||||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
|
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
|
||||||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination);
|
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
|
||||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination))
|
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
|
||||||
MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||||
DataLocation.PortableDataPath,
|
DataLocation.PortableDataPath,
|
||||||
targetDestination));
|
targetDestination));
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +94,7 @@ namespace Flow.Launcher.Core
|
||||||
|
|
||||||
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
|
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
|
||||||
|
|
||||||
if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||||
{
|
{
|
||||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||||
}
|
}
|
||||||
|
|
@ -99,8 +107,8 @@ namespace Flow.Launcher.Core
|
||||||
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
|
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
|
||||||
|
|
||||||
if (!silentUpdate)
|
if (!silentUpdate)
|
||||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
|
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
|
||||||
api.GetTranslation("update_flowlauncher_check_connection"));
|
_api.GetTranslation("update_flowlauncher_check_connection"));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{2FEB2298-7653-4009-B1EA-FFFB1A768BCC}</ProjectGuid>
|
|
||||||
<OutputType>Library</OutputType>
|
|
||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
|
||||||
<RootNamespace>Flow.Launcher.CrashReporter</RootNamespace>
|
|
||||||
<AssemblyName>Flow.Launcher.CrashReporter</AssemblyName>
|
|
||||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
|
|
||||||
<TargetFrameworkProfile />
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>..\Output\Debug\</OutputPath>
|
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>..\Output\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Exceptionless, Version=1.5.2121.0, Culture=neutral, PublicKeyToken=fc181f0a46f65747, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Exceptionless.1.5.2121\lib\net45\Exceptionless.dll</HintPath>
|
|
||||||
<Private>True</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Exceptionless.Models, Version=1.5.2121.0, Culture=neutral, PublicKeyToken=fc181f0a46f65747, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Exceptionless.1.5.2121\lib\net45\Exceptionless.Models.dll</HintPath>
|
|
||||||
<Private>True</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="JetBrains.Annotations, Version=10.1.4.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\JetBrains.Annotations.10.1.4\lib\net20\JetBrains.Annotations.dll</HintPath>
|
|
||||||
<Private>True</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="PresentationCore" />
|
|
||||||
<Reference Include="PresentationFramework" />
|
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Xaml" />
|
|
||||||
<Reference Include="System.Xml.Linq" />
|
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
|
||||||
<Reference Include="System.Data" />
|
|
||||||
<Reference Include="System.Xml" />
|
|
||||||
<Reference Include="WindowsBase" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="..\SolutionAssemblyInfo.cs">
|
|
||||||
<Link>Properties\SolutionAssemblyInfo.cs</Link>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="CrashReporter.cs" />
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
|
||||||
<Compile Include="ReportWindow.xaml.cs">
|
|
||||||
<DependentUpon>ReportWindow.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Page Include="ReportWindow.xaml">
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Flow.Launcher.Core\Flow.Launcher.Core.csproj">
|
|
||||||
<Project>{B749F0DB-8E75-47DB-9E5E-265D16D0C0D2}</Project>
|
|
||||||
<Name>Flow.Launcher.Core</Name>
|
|
||||||
</ProjectReference>
|
|
||||||
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj">
|
|
||||||
<Project>{4fd29318-a8ab-4d8f-aa47-60bc241b8da3}</Project>
|
|
||||||
<Name>Flow.Launcher.Infrastructure</Name>
|
|
||||||
</ProjectReference>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Images\crash_warning.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Resource>
|
|
||||||
<Resource Include="Images\crash_go.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Resource>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Images\crash_stop.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Resource>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="Images\app_error.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Resource>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="packages.config" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
|
||||||
Other similar extension points exist, see Microsoft.Common.targets.
|
|
||||||
<Target Name="BeforeBuild">
|
|
||||||
</Target>
|
|
||||||
<Target Name="AfterBuild">
|
|
||||||
</Target>
|
|
||||||
-->
|
|
||||||
</Project>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 738 B |
|
|
@ -1,5 +0,0 @@
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
[assembly: AssemblyTitle("Flow.Launcher.CrashReporter")]
|
|
||||||
[assembly: Guid("0ea3743c-2c0d-4b13-b9ce-e5e1f85aea23")]
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<packages>
|
|
||||||
<package id="Exceptionless" version="1.5.2121" targetFramework="net452" />
|
|
||||||
<package id="JetBrains.Annotations" version="10.1.4" targetFramework="net452" />
|
|
||||||
<package id="System.Runtime" version="4.0.0" targetFramework="net452" />
|
|
||||||
</packages>
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
@ -19,7 +20,7 @@ namespace Flow.Launcher.Infrastructure
|
||||||
public static readonly string RootDirectory = Directory.GetParent(ApplicationDirectory).ToString();
|
public static readonly string RootDirectory = Directory.GetParent(ApplicationDirectory).ToString();
|
||||||
|
|
||||||
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
|
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
|
||||||
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new/choose";
|
public const string IssuesUrl = "https://github.com/Flow-Launcher/Flow.Launcher/issues";
|
||||||
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
|
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
|
||||||
public static readonly string Dev = "Dev";
|
public static readonly string Dev = "Dev";
|
||||||
public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips";
|
public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips";
|
||||||
|
|
@ -30,6 +31,7 @@ namespace Flow.Launcher.Infrastructure
|
||||||
public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png");
|
public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png");
|
||||||
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
||||||
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
||||||
|
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
|
||||||
|
|
||||||
public static string PythonPath;
|
public static string PythonPath;
|
||||||
public static string NodePath;
|
public static string NodePath;
|
||||||
|
|
@ -50,5 +52,7 @@ namespace Flow.Launcher.Infrastructure
|
||||||
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
|
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
|
||||||
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
|
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
|
||||||
public const string Docs = "https://flowlauncher.com/docs";
|
public const string Docs = "https://flowlauncher.com/docs";
|
||||||
|
|
||||||
|
public const string SystemLanguageCode = "system";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Exception
|
namespace Flow.Launcher.Infrastructure.Exception
|
||||||
|
|
@ -63,7 +62,7 @@ namespace Flow.Launcher.Infrastructure.Exception
|
||||||
sb.AppendLine($"* Command Line: {Environment.CommandLine}");
|
sb.AppendLine($"* Command Line: {Environment.CommandLine}");
|
||||||
sb.AppendLine($"* Timestamp: {DateTime.Now.ToString(CultureInfo.InvariantCulture)}");
|
sb.AppendLine($"* Timestamp: {DateTime.Now.ToString(CultureInfo.InvariantCulture)}");
|
||||||
sb.AppendLine($"* Flow Launcher version: {Constant.Version}");
|
sb.AppendLine($"* Flow Launcher version: {Constant.Version}");
|
||||||
sb.AppendLine($"* OS Version: {Environment.OSVersion.VersionString}");
|
sb.AppendLine($"* OS Version: {GetWindowsFullVersionFromRegistry()}");
|
||||||
sb.AppendLine($"* IntPtr Length: {IntPtr.Size}");
|
sb.AppendLine($"* IntPtr Length: {IntPtr.Size}");
|
||||||
sb.AppendLine($"* x64: {Environment.Is64BitOperatingSystem}");
|
sb.AppendLine($"* x64: {Environment.Is64BitOperatingSystem}");
|
||||||
sb.AppendLine($"* Python Path: {Constant.PythonPath}");
|
sb.AppendLine($"* Python Path: {Constant.PythonPath}");
|
||||||
|
|
@ -173,5 +172,35 @@ namespace Flow.Launcher.Infrastructure.Exception
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetWindowsFullVersionFromRegistry()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buildRevision = GetWindowsRevisionFromRegistry();
|
||||||
|
var currentBuild = Environment.OSVersion.Version.Build;
|
||||||
|
return currentBuild.ToString() + "." + buildRevision;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Environment.OSVersion.VersionString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetWindowsRevisionFromRegistry()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\"))
|
||||||
|
{
|
||||||
|
var buildRevision = registryKey.GetValue("UBR").ToString();
|
||||||
|
return buildRevision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using Windows.Win32;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
|
|
@ -15,7 +15,20 @@ namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
var explorerWindow = GetActiveExplorer();
|
var explorerWindow = GetActiveExplorer();
|
||||||
string locationUrl = explorerWindow?.LocationURL;
|
string locationUrl = explorerWindow?.LocationURL;
|
||||||
return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null;
|
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get directory path from a file path
|
||||||
|
/// </summary>
|
||||||
|
private static string GetDirectoryPath(string path)
|
||||||
|
{
|
||||||
|
if (!path.EndsWith("\\"))
|
||||||
|
{
|
||||||
|
return path + "\\";
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -54,10 +67,6 @@ namespace Flow.Launcher.Infrastructure
|
||||||
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
||||||
|
|
||||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -70,9 +79,9 @@ namespace Flow.Launcher.Infrastructure
|
||||||
|
|
||||||
var index = 0;
|
var index = 0;
|
||||||
var numRemaining = hWnds.Count;
|
var numRemaining = hWnds.Count;
|
||||||
EnumWindows((wnd, _) =>
|
PInvoke.EnumWindows((wnd, _) =>
|
||||||
{
|
{
|
||||||
var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32());
|
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
|
||||||
if (searchIndex != -1)
|
if (searchIndex != -1)
|
||||||
{
|
{
|
||||||
z[searchIndex] = index;
|
z[searchIndex] = index;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net7.0-windows</TargetFramework>
|
||||||
|
|
@ -35,6 +35,10 @@
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Include="NativeMethods.txt" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SolutionAssemblyInfo.cs" Link="Properties\SolutionAssemblyInfo.cs" />
|
<Compile Include="..\SolutionAssemblyInfo.cs" Link="Properties\SolutionAssemblyInfo.cs" />
|
||||||
<None Include="FodyWeavers.xml" />
|
<None Include="FodyWeavers.xml" />
|
||||||
|
|
@ -49,14 +53,19 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||||
|
<PackageReference Include="BitFaster.Caching" Version="2.5.3" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<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="Microsoft.VisualStudio.Threading" Version="17.4.27" />
|
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.12.19" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<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-->
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||||
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
{
|
{
|
||||||
|
|
@ -11,44 +15,45 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe class GlobalHotkey : IDisposable
|
public unsafe class GlobalHotkey : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly IntPtr hookId;
|
private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback;
|
||||||
|
private static readonly UnhookWindowsHookExSafeHandle hookId;
|
||||||
|
|
||||||
|
|
||||||
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
|
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
|
||||||
internal static Func<KeyEvent, int, SpecialKeyState, bool> hookedKeyboardCallback;
|
internal static Func<KeyEvent, int, SpecialKeyState, bool> hookedKeyboardCallback;
|
||||||
|
|
||||||
//Modifier key constants
|
|
||||||
private const int VK_SHIFT = 0x10;
|
|
||||||
private const int VK_CONTROL = 0x11;
|
|
||||||
private const int VK_ALT = 0x12;
|
|
||||||
private const int VK_WIN = 91;
|
|
||||||
|
|
||||||
static GlobalHotkey()
|
static GlobalHotkey()
|
||||||
{
|
{
|
||||||
// Set the hook
|
// Set the hook
|
||||||
hookId = InterceptKeys.SetHook(& LowLevelKeyboardProc);
|
hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UnhookWindowsHookExSafeHandle SetHook(HOOKPROC proc, WINDOWS_HOOK_ID hookId)
|
||||||
|
{
|
||||||
|
using var curProcess = Process.GetCurrentProcess();
|
||||||
|
using var curModule = curProcess.MainModule;
|
||||||
|
return PInvoke.SetWindowsHookEx(hookId, proc, PInvoke.GetModuleHandle(curModule.ModuleName), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SpecialKeyState CheckModifiers()
|
public static SpecialKeyState CheckModifiers()
|
||||||
{
|
{
|
||||||
SpecialKeyState state = new SpecialKeyState();
|
SpecialKeyState state = new SpecialKeyState();
|
||||||
if ((InterceptKeys.GetKeyState(VK_SHIFT) & 0x8000) != 0)
|
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) & 0x8000) != 0)
|
||||||
{
|
{
|
||||||
//SHIFT is pressed
|
//SHIFT is pressed
|
||||||
state.ShiftPressed = true;
|
state.ShiftPressed = true;
|
||||||
}
|
}
|
||||||
if ((InterceptKeys.GetKeyState(VK_CONTROL) & 0x8000) != 0)
|
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) & 0x8000) != 0)
|
||||||
{
|
{
|
||||||
//CONTROL is pressed
|
//CONTROL is pressed
|
||||||
state.CtrlPressed = true;
|
state.CtrlPressed = true;
|
||||||
}
|
}
|
||||||
if ((InterceptKeys.GetKeyState(VK_ALT) & 0x8000) != 0)
|
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) & 0x8000) != 0)
|
||||||
{
|
{
|
||||||
//ALT is pressed
|
//ALT is pressed
|
||||||
state.AltPressed = true;
|
state.AltPressed = true;
|
||||||
}
|
}
|
||||||
if ((InterceptKeys.GetKeyState(VK_WIN) & 0x8000) != 0)
|
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_LWIN) & 0x8000) != 0 ||
|
||||||
|
(PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_RWIN) & 0x8000) != 0)
|
||||||
{
|
{
|
||||||
//WIN is pressed
|
//WIN is pressed
|
||||||
state.WinPressed = true;
|
state.WinPressed = true;
|
||||||
|
|
@ -57,33 +62,33 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly]
|
private static LRESULT HookKeyboardCallback(int nCode, WPARAM wParam, LPARAM lParam)
|
||||||
private static IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
|
|
||||||
{
|
{
|
||||||
bool continues = true;
|
bool continues = true;
|
||||||
|
|
||||||
if (nCode >= 0)
|
if (nCode >= 0)
|
||||||
{
|
{
|
||||||
if (wParam.ToUInt32() == (int)KeyEvent.WM_KEYDOWN ||
|
if (wParam.Value == (int)KeyEvent.WM_KEYDOWN ||
|
||||||
wParam.ToUInt32() == (int)KeyEvent.WM_KEYUP ||
|
wParam.Value == (int)KeyEvent.WM_KEYUP ||
|
||||||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYDOWN ||
|
wParam.Value == (int)KeyEvent.WM_SYSKEYDOWN ||
|
||||||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYUP)
|
wParam.Value == (int)KeyEvent.WM_SYSKEYUP)
|
||||||
{
|
{
|
||||||
if (hookedKeyboardCallback != null)
|
if (hookedKeyboardCallback != null)
|
||||||
continues = hookedKeyboardCallback((KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), CheckModifiers());
|
continues = hookedKeyboardCallback((KeyEvent)wParam.Value, Marshal.ReadInt32(lParam), CheckModifiers());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (continues)
|
if (continues)
|
||||||
{
|
{
|
||||||
return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam);
|
return PInvoke.CallNextHookEx(hookId, nCode, wParam, lParam);
|
||||||
}
|
}
|
||||||
return (IntPtr)(-1);
|
|
||||||
|
return new LRESULT(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
InterceptKeys.UnhookWindowsHookEx(hookId);
|
hookId.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
~GlobalHotkey()
|
~GlobalHotkey()
|
||||||
|
|
@ -91,4 +96,4 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
|
||||||
{
|
|
||||||
internal static unsafe class InterceptKeys
|
|
||||||
{
|
|
||||||
public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam);
|
|
||||||
|
|
||||||
private const int WH_KEYBOARD_LL = 13;
|
|
||||||
|
|
||||||
public static IntPtr SetHook(delegate* unmanaged<int, UIntPtr, IntPtr, IntPtr> proc)
|
|
||||||
{
|
|
||||||
using (Process curProcess = Process.GetCurrentProcess())
|
|
||||||
using (ProcessModule curModule = curProcess.MainModule)
|
|
||||||
{
|
|
||||||
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
public static extern IntPtr SetWindowsHookEx(int idHook, delegate* unmanaged<int, UIntPtr, IntPtr, IntPtr> lpfn, IntPtr hMod, uint dwThreadId);
|
|
||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
|
||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
public static extern IntPtr GetModuleHandle(string lpModuleName);
|
|
||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
|
|
||||||
public static extern short GetKeyState(int keyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Windows.Win32;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
{
|
{
|
||||||
public enum KeyEvent
|
public enum KeyEvent
|
||||||
|
|
@ -5,21 +7,21 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key down
|
/// Key down
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WM_KEYDOWN = 256,
|
WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key up
|
/// Key up
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WM_KEYUP = 257,
|
WM_KEYUP = (int)PInvoke.WM_KEYUP,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System key up
|
/// System key up
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WM_SYSKEYUP = 261,
|
WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System key down
|
/// System key down
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WM_SYSKEYDOWN = 260
|
WM_SYSKEYDOWN = (int)PInvoke.WM_SYSKEYDOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows.Interop;
|
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Http
|
namespace Flow.Launcher.Infrastructure.Http
|
||||||
{
|
{
|
||||||
|
|
@ -20,8 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http
|
||||||
|
|
||||||
private static HttpClient client = new HttpClient();
|
private static HttpClient client = new HttpClient();
|
||||||
|
|
||||||
public static IPublicAPI API { get; set; }
|
|
||||||
|
|
||||||
static Http()
|
static Http()
|
||||||
{
|
{
|
||||||
// need to be added so it would work on a win10 machine
|
// need to be added so it would work on a win10 machine
|
||||||
|
|
@ -81,20 +77,55 @@ namespace Flow.Launcher.Infrastructure.Http
|
||||||
}
|
}
|
||||||
catch (UriFormatException e)
|
catch (UriFormatException e)
|
||||||
{
|
{
|
||||||
API.ShowMsg("Please try again", "Unable to parse Http Proxy");
|
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsg("Please try again", "Unable to parse Http Proxy");
|
||||||
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
|
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default)
|
public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.OK)
|
if (response.StatusCode == HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
||||||
await response.Content.CopyToAsync(fileStream, token);
|
var canReportProgress = totalBytes != -1;
|
||||||
|
|
||||||
|
if (canReportProgress && reportProgress != null)
|
||||||
|
{
|
||||||
|
await using var contentStream = await response.Content.ReadAsStreamAsync(token);
|
||||||
|
await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true);
|
||||||
|
|
||||||
|
var buffer = new byte[8192];
|
||||||
|
long totalRead = 0;
|
||||||
|
int read;
|
||||||
|
double progressValue = 0;
|
||||||
|
|
||||||
|
reportProgress(0);
|
||||||
|
|
||||||
|
while ((read = await contentStream.ReadAsync(buffer, token)) > 0)
|
||||||
|
{
|
||||||
|
await fileStream.WriteAsync(buffer.AsMemory(0, read), token);
|
||||||
|
totalRead += read;
|
||||||
|
|
||||||
|
progressValue = totalRead * 100.0 / totalBytes;
|
||||||
|
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
else
|
||||||
|
reportProgress(progressValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressValue < 100)
|
||||||
|
reportProgress(100);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||||
|
await response.Content.CopyToAsync(fileStream, token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,36 +3,23 @@ using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using BitFaster.Caching.Lfu;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Image
|
namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
[Serializable]
|
|
||||||
public class ImageUsage
|
|
||||||
{
|
|
||||||
|
|
||||||
public int usage;
|
|
||||||
public ImageSource imageSource;
|
|
||||||
|
|
||||||
public ImageUsage(int usage, ImageSource image)
|
|
||||||
{
|
|
||||||
this.usage = usage;
|
|
||||||
imageSource = 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 Initialization(Dictionary<(string, bool), int> usage)
|
private ConcurrentLfu<(string, bool), ImageSource> CacheManager { get; set; } = new(MaxCached);
|
||||||
|
|
||||||
|
public void Initialize(IEnumerable<(string, bool)> usage)
|
||||||
{
|
{
|
||||||
foreach (var key in usage.Keys)
|
foreach (var key in usage)
|
||||||
{
|
{
|
||||||
Data[key] = new ImageUsage(usage[key], null);
|
CacheManager.AddOrUpdate(key, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,70 +27,42 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (!Data.TryGetValue((path, isFullImage), out var value))
|
return CacheManager.TryGet((path, isFullImage), out var value) ? value : null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
value.usage++;
|
|
||||||
return value.imageSource;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
Data.AddOrUpdate(
|
CacheManager.AddOrUpdate((path, isFullImage), value);
|
||||||
(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
|
|
||||||
// This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<ImageSource> GetOrAddAsync(string key,
|
||||||
|
Func<(string, bool), Task<ImageSource>> valueFactory,
|
||||||
|
bool isFullImage = false)
|
||||||
|
{
|
||||||
|
return await CacheManager.GetOrAddAsync((key, isFullImage), valueFactory);
|
||||||
|
}
|
||||||
|
|
||||||
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 CacheManager.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 (CacheManager.TryGet((key, isFullImage), out var value))
|
||||||
{
|
{
|
||||||
bool hasKey = Data.TryGetValue((key, isFullImage), out var imageUsage);
|
image = value;
|
||||||
image = hasKey ? imageUsage.imageSource : null;
|
return image != null;
|
||||||
return hasKey;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
image = null;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
image = null;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CacheSize()
|
public int CacheSize()
|
||||||
{
|
{
|
||||||
return Data.Count;
|
return CacheManager.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -111,7 +70,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.Select(x => x.Value)
|
||||||
|
.Distinct()
|
||||||
|
.Count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<KeyValuePair<(string, bool), ImageSource>> EnumerateEntries()
|
||||||
|
{
|
||||||
|
return CacheManager;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Threading;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Documents;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
|
@ -18,32 +17,30 @@ 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 BinaryStorage<Dictionary<(string, bool), int>> _storage;
|
private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
|
||||||
|
private static BinaryStorage<List<(string, bool)>> _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;
|
||||||
private static readonly bool EnableImageHash = true;
|
private static readonly bool EnableImageHash = true;
|
||||||
|
public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon));
|
||||||
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||||
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
||||||
public const int SmallIconSize = 64;
|
public const int SmallIconSize = 64;
|
||||||
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<List<(string, bool)>>("Image");
|
||||||
_hashGenerator = new ImageHashGenerator();
|
_hashGenerator = new ImageHashGenerator();
|
||||||
|
|
||||||
var usage = LoadStorageToConcurrentDictionary();
|
var usage = await LoadStorageToConcurrentDictionaryAsync();
|
||||||
|
|
||||||
foreach (var icon in new[]
|
ImageCache.Initialize(usage);
|
||||||
{
|
|
||||||
Constant.DefaultIcon, Constant.MissingImgIcon
|
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
|
||||||
})
|
|
||||||
{
|
{
|
||||||
ImageSource img = new BitmapImage(new Uri(icon));
|
ImageSource img = new BitmapImage(new Uri(icon));
|
||||||
img.Freeze();
|
img.Freeze();
|
||||||
|
|
@ -54,33 +51,42 @@ 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(
|
.Select(x => x.Key)
|
||||||
x => x.Key,
|
.ToList());
|
||||||
x => x.Value.usage));
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
storageLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary()
|
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
|
||||||
{
|
{
|
||||||
lock (_storage)
|
await storageLock.WaitAsync();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>());
|
return await _storage.TryLoadAsync(new List<(string, bool)>());
|
||||||
|
}
|
||||||
return new ConcurrentDictionary<(string, bool), int>(loaded);
|
finally
|
||||||
|
{
|
||||||
|
storageLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,9 +124,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)
|
||||||
|
|
@ -130,7 +139,8 @@ 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:image", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var imageSource = new BitmapImage(new Uri(path));
|
var imageSource = new BitmapImage(new Uri(path));
|
||||||
imageSource.Freeze();
|
imageSource.Freeze();
|
||||||
|
|
@ -159,6 +169,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
|
||||||
|
|
@ -174,6 +185,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;
|
||||||
|
|
@ -189,8 +201,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;
|
||||||
|
|
@ -204,14 +216,22 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
type = ImageType.ImageFile;
|
type = ImageType.ImageFile;
|
||||||
if (loadFullImage)
|
if (loadFullImage)
|
||||||
{
|
{
|
||||||
image = LoadFullImage(path);
|
try
|
||||||
type = ImageType.FullImageFile;
|
{
|
||||||
|
image = LoadFullImage(path);
|
||||||
|
type = ImageType.FullImageFile;
|
||||||
|
}
|
||||||
|
catch (NotSupportedException)
|
||||||
|
{
|
||||||
|
image = Image;
|
||||||
|
type = ImageType.Error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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);
|
||||||
|
|
@ -237,7 +257,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,
|
||||||
|
|
@ -262,17 +283,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
|
{
|
||||||
img = ImageCache[key, false] ?? img;
|
// image already exists
|
||||||
|
img = ImageCache[key, loadFullImage] ?? img;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{ // new guid
|
{
|
||||||
|
// new guid
|
||||||
|
|
||||||
GuidToKey[hash] = path;
|
GuidToKey[hash] = path;
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +313,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();
|
||||||
|
|
||||||
|
|
@ -315,8 +338,10 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
resizedHeight.EndInit();
|
resizedHeight.EndInit();
|
||||||
return resizedHeight;
|
return resizedHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resizedWidth;
|
return resizedWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows;
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.UI.Shell;
|
||||||
|
using Windows.Win32.Graphics.Gdi;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Image
|
namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Subclass of <see cref="Windows.Win32.UI.Shell.SIIGBF"/>
|
||||||
|
/// </summary>
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum ThumbnailOptions
|
public enum ThumbnailOptions
|
||||||
{
|
{
|
||||||
|
|
@ -22,91 +29,13 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
|
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
|
||||||
|
|
||||||
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
|
private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID;
|
||||||
|
|
||||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
||||||
internal static extern int SHCreateItemFromParsingName(
|
|
||||||
[MarshalAs(UnmanagedType.LPWStr)] string path,
|
|
||||||
IntPtr pbc,
|
|
||||||
ref Guid riid,
|
|
||||||
[MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
|
|
||||||
|
|
||||||
[DllImport("gdi32.dll")]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
internal static extern bool DeleteObject(IntPtr hObject);
|
|
||||||
|
|
||||||
[ComImport]
|
|
||||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
||||||
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
|
|
||||||
internal interface IShellItem
|
|
||||||
{
|
|
||||||
void BindToHandler(IntPtr pbc,
|
|
||||||
[MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
|
|
||||||
[MarshalAs(UnmanagedType.LPStruct)]Guid riid,
|
|
||||||
out IntPtr ppv);
|
|
||||||
|
|
||||||
void GetParent(out IShellItem ppsi);
|
|
||||||
void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
|
|
||||||
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
||||||
void Compare(IShellItem psi, uint hint, out int piOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
internal enum SIGDN : uint
|
|
||||||
{
|
|
||||||
NORMALDISPLAY = 0,
|
|
||||||
PARENTRELATIVEPARSING = 0x80018001,
|
|
||||||
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
|
|
||||||
DESKTOPABSOLUTEPARSING = 0x80028000,
|
|
||||||
PARENTRELATIVEEDITING = 0x80031001,
|
|
||||||
DESKTOPABSOLUTEEDITING = 0x8004c000,
|
|
||||||
FILESYSPATH = 0x80058000,
|
|
||||||
URL = 0x80068000
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum HResult
|
|
||||||
{
|
|
||||||
Ok = 0x0000,
|
|
||||||
False = 0x0001,
|
|
||||||
InvalidArguments = unchecked((int)0x80070057),
|
|
||||||
OutOfMemory = unchecked((int)0x8007000E),
|
|
||||||
NoInterface = unchecked((int)0x80004002),
|
|
||||||
Fail = unchecked((int)0x80004005),
|
|
||||||
ExtractionFailed = unchecked((int)0x8004B200),
|
|
||||||
ElementNotFound = unchecked((int)0x80070490),
|
|
||||||
TypeElementNotFound = unchecked((int)0x8002802B),
|
|
||||||
NoObject = unchecked((int)0x800401E5),
|
|
||||||
Win32ErrorCanceled = 1223,
|
|
||||||
Canceled = unchecked((int)0x800704C7),
|
|
||||||
ResourceInUse = unchecked((int)0x800700AA),
|
|
||||||
AccessDenied = unchecked((int)0x80030005)
|
|
||||||
}
|
|
||||||
|
|
||||||
[ComImportAttribute()]
|
|
||||||
[GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
|
|
||||||
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
|
|
||||||
internal interface IShellItemImageFactory
|
|
||||||
{
|
|
||||||
[PreserveSig]
|
|
||||||
HResult GetImage(
|
|
||||||
[In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
|
|
||||||
[In] ThumbnailOptions flags,
|
|
||||||
[Out] out IntPtr phbm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct NativeSize
|
|
||||||
{
|
|
||||||
private int width;
|
|
||||||
private int height;
|
|
||||||
|
|
||||||
public int Width { set { width = value; } }
|
|
||||||
public int Height { set { height = value; } }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200;
|
||||||
|
|
||||||
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
|
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
|
||||||
{
|
{
|
||||||
IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -115,39 +44,61 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// delete HBitmap to avoid memory leaks
|
// delete HBitmap to avoid memory leaks
|
||||||
DeleteObject(hBitmap);
|
PInvoke.DeleteObject(hBitmap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
|
||||||
{
|
|
||||||
IShellItem nativeShellItem;
|
|
||||||
Guid shellItem2Guid = new Guid(IShellItem2Guid);
|
|
||||||
int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
|
|
||||||
|
|
||||||
if (retCode != 0)
|
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
||||||
|
{
|
||||||
|
var retCode = PInvoke.SHCreateItemFromParsingName(
|
||||||
|
fileName,
|
||||||
|
null,
|
||||||
|
GUID_IShellItem,
|
||||||
|
out var nativeShellItem);
|
||||||
|
|
||||||
|
if (retCode != HRESULT.S_OK)
|
||||||
throw Marshal.GetExceptionForHR(retCode);
|
throw Marshal.GetExceptionForHR(retCode);
|
||||||
|
|
||||||
NativeSize nativeSize = new NativeSize
|
if (nativeShellItem is not IShellItemImageFactory imageFactory)
|
||||||
{
|
{
|
||||||
Width = width,
|
Marshal.ReleaseComObject(nativeShellItem);
|
||||||
Height = height
|
nativeShellItem = null;
|
||||||
};
|
throw new InvalidOperationException("Failed to get IShellItemImageFactory");
|
||||||
|
|
||||||
IntPtr hBitmap;
|
|
||||||
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap);
|
|
||||||
|
|
||||||
// if extracting image thumbnail and failed, extract shell icon
|
|
||||||
if (options == ThumbnailOptions.ThumbnailOnly && hr == HResult.ExtractionFailed)
|
|
||||||
{
|
|
||||||
hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, out hBitmap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Marshal.ReleaseComObject(nativeShellItem);
|
SIZE size = new SIZE
|
||||||
|
{
|
||||||
|
cx = width,
|
||||||
|
cy = height
|
||||||
|
};
|
||||||
|
|
||||||
if (hr == HResult.Ok) return hBitmap;
|
HBITMAP hBitmap = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
imageFactory.GetImage(size, (SIIGBF)options, &hBitmap);
|
||||||
|
}
|
||||||
|
catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly)
|
||||||
|
{
|
||||||
|
// Fallback to IconOnly if ThumbnailOnly fails
|
||||||
|
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly)
|
||||||
|
{
|
||||||
|
// Fallback to IconOnly if files cannot be found
|
||||||
|
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (nativeShellItem != null)
|
||||||
|
{
|
||||||
|
Marshal.ReleaseComObject(nativeShellItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr));
|
return hBitmap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,8 @@ using NLog;
|
||||||
using NLog.Config;
|
using NLog.Config;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using NLog.Fluent;
|
|
||||||
using NLog.Targets.Wrappers;
|
using NLog.Targets.Wrappers;
|
||||||
using System.Runtime.ExceptionServices;
|
using System.Runtime.ExceptionServices;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Logger
|
namespace Flow.Launcher.Infrastructure.Logger
|
||||||
{
|
{
|
||||||
|
|
@ -51,17 +48,45 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
configuration.AddTarget("file", fileTargetASyncWrapper);
|
configuration.AddTarget("file", fileTargetASyncWrapper);
|
||||||
configuration.AddTarget("debug", debugTarget);
|
configuration.AddTarget("debug", debugTarget);
|
||||||
|
|
||||||
|
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper)
|
||||||
|
{
|
||||||
|
RuleName = "file"
|
||||||
|
};
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper);
|
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget)
|
||||||
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget);
|
{
|
||||||
|
RuleName = "debug"
|
||||||
|
};
|
||||||
configuration.LoggingRules.Add(debugRule);
|
configuration.LoggingRules.Add(debugRule);
|
||||||
#else
|
|
||||||
var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
|
|
||||||
#endif
|
#endif
|
||||||
configuration.LoggingRules.Add(fileRule);
|
configuration.LoggingRules.Add(fileRule);
|
||||||
LogManager.Configuration = configuration;
|
LogManager.Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SetLogLevel(LOGLEVEL level)
|
||||||
|
{
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case LOGLEVEL.DEBUG:
|
||||||
|
UseDebugLogLevel();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
UseInfoLogLevel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Info(nameof(Logger), $"Using log level: {level}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UseDebugLogLevel()
|
||||||
|
{
|
||||||
|
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UseInfoLogLevel()
|
||||||
|
{
|
||||||
|
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
|
||||||
|
}
|
||||||
|
|
||||||
private static void LogFaultyFormat(string message)
|
private static void LogFaultyFormat(string message)
|
||||||
{
|
{
|
||||||
var logger = LogManager.GetLogger("FaultyLogger");
|
var logger = LogManager.GetLogger("FaultyLogger");
|
||||||
|
|
@ -76,7 +101,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
|
public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
|
||||||
{
|
{
|
||||||
exception = exception.Demystify();
|
exception = exception.Demystify();
|
||||||
|
|
@ -115,8 +139,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
{
|
{
|
||||||
var logger = LogManager.GetLogger(classAndMethod);
|
var logger = LogManager.GetLogger(classAndMethod);
|
||||||
|
|
||||||
var messageBuilder = new StringBuilder();
|
|
||||||
|
|
||||||
logger.Error(e, message);
|
logger.Error(e, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +158,8 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
/// Example: "|ClassName.MethodName|Message"
|
||||||
|
/// <param name="message">Example: "|ClassName.MethodName|Message" </param>
|
||||||
/// <param name="e">Exception</param>
|
/// <param name="e">Exception</param>
|
||||||
public static void Exception(string message, System.Exception e)
|
public static void Exception(string message, System.Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -158,7 +181,7 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
/// Example: "|ClassName.MethodName|Message"
|
||||||
public static void Error(string message)
|
public static void Error(string message)
|
||||||
{
|
{
|
||||||
LogInternal(message, LogLevel.Error);
|
LogInternal(message, LogLevel.Error);
|
||||||
|
|
@ -183,7 +206,7 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
LogInternal(LogLevel.Debug, className, message, methodName);
|
LogInternal(LogLevel.Debug, className, message, methodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
/// Example: "|ClassName.MethodName|Message""
|
||||||
public static void Debug(string message)
|
public static void Debug(string message)
|
||||||
{
|
{
|
||||||
LogInternal(message, LogLevel.Debug);
|
LogInternal(message, LogLevel.Debug);
|
||||||
|
|
@ -194,7 +217,7 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
LogInternal(LogLevel.Info, className, message, methodName);
|
LogInternal(LogLevel.Info, className, message, methodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
/// Example: "|ClassName.MethodName|Message"
|
||||||
public static void Info(string message)
|
public static void Info(string message)
|
||||||
{
|
{
|
||||||
LogInternal(message, LogLevel.Info);
|
LogInternal(message, LogLevel.Info);
|
||||||
|
|
@ -205,10 +228,16 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
LogInternal(LogLevel.Warn, className, message, methodName);
|
LogInternal(LogLevel.Warn, className, message, methodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
/// Example: "|ClassName.MethodName|Message"
|
||||||
public static void Warn(string message)
|
public static void Warn(string message)
|
||||||
{
|
{
|
||||||
LogInternal(message, LogLevel.Warn);
|
LogInternal(message, LogLevel.Warn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum LOGLEVEL
|
||||||
|
{
|
||||||
|
DEBUG,
|
||||||
|
INFO
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
Flow.Launcher.Infrastructure/NativeMethods.txt
Normal file
19
Flow.Launcher.Infrastructure/NativeMethods.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
SHCreateItemFromParsingName
|
||||||
|
DeleteObject
|
||||||
|
IShellItem
|
||||||
|
IShellItemImageFactory
|
||||||
|
S_OK
|
||||||
|
|
||||||
|
SetWindowsHookEx
|
||||||
|
UnhookWindowsHookEx
|
||||||
|
CallNextHookEx
|
||||||
|
GetModuleHandle
|
||||||
|
GetKeyState
|
||||||
|
VIRTUAL_KEY
|
||||||
|
|
||||||
|
WM_KEYDOWN
|
||||||
|
WM_KEYUP
|
||||||
|
WM_SYSKEYDOWN
|
||||||
|
WM_SYSKEYUP
|
||||||
|
|
||||||
|
EnumWindows
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Text;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using ToolGood.Words.Pinyin;
|
using ToolGood.Words.Pinyin;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
|
|
@ -129,7 +130,12 @@ namespace Flow.Launcher.Infrastructure
|
||||||
|
|
||||||
private Settings _settings;
|
private Settings _settings;
|
||||||
|
|
||||||
public void Initialize([NotNull] Settings settings)
|
public PinyinAlphabet()
|
||||||
|
{
|
||||||
|
Initialize(Ioc.Default.GetRequiredService<Settings>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Initialize([NotNull] Settings settings)
|
||||||
{
|
{
|
||||||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,115 +4,78 @@ 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)
|
||||||
{
|
{
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
using System;
|
using System.IO;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
|
||||||
{
|
|
||||||
[Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
|
|
||||||
"This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
|
|
||||||
public interface ISavable : Plugin.ISavable { }
|
|
||||||
}
|
|
||||||
|
|
@ -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,87 @@ 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 +157,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)
|
||||||
{
|
{
|
||||||
|
|
@ -108,20 +181,31 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
string serialized = JsonSerializer.Serialize(Data,
|
string serialized = JsonSerializer.Serialize(Data,
|
||||||
new JsonSerializerOptions
|
new JsonSerializerOptions { WriteIndented = true });
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
});
|
|
||||||
|
|
||||||
File.WriteAllText(TempFilePath, serialized);
|
File.WriteAllText(TempFilePath, serialized);
|
||||||
|
|
||||||
|
AtomicWriteSetting();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync()
|
||||||
|
{
|
||||||
|
await using 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);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
File.Replace(TempFilePath, FilePath, BackupFilePath);
|
var finalFilePath = new FileInfo(FilePath).LinkTarget ?? FilePath;
|
||||||
|
File.Replace(TempFilePath, finalFilePath, BackupFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,17 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
public class PluginJsonStorage<T> :JsonStorage<T> where T : new()
|
public class PluginJsonStorage<T> : JsonStorage<T> where T : new()
|
||||||
{
|
{
|
||||||
|
// Use assembly name to check which plugin is using this storage
|
||||||
|
public readonly string AssemblyName;
|
||||||
|
|
||||||
public PluginJsonStorage()
|
public PluginJsonStorage()
|
||||||
{
|
{
|
||||||
// C# related, add python related below
|
// C# related, add python related below
|
||||||
var dataType = typeof(T);
|
var dataType = typeof(T);
|
||||||
var assemblyName = dataType.Assembly.GetName().Name;
|
AssemblyName = dataType.Assembly.GetName().Name;
|
||||||
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName);
|
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName);
|
||||||
Helper.ValidateDirectory(DirectoryPath);
|
Helper.ValidateDirectory(DirectoryPath);
|
||||||
|
|
||||||
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
||||||
|
|
@ -20,6 +23,13 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DeleteDirectory()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(DirectoryPath))
|
||||||
|
{
|
||||||
|
Directory.Delete(DirectoryPath, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,35 @@
|
||||||
using Flow.Launcher.Plugin.SharedModels;
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Plugin.SharedModels;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
public class StringMatcher
|
public class StringMatcher
|
||||||
{
|
{
|
||||||
private readonly MatchOption _defaultMatchOption = new MatchOption();
|
private readonly MatchOption _defaultMatchOption = new();
|
||||||
|
|
||||||
public SearchPrecisionScore UserSettingSearchPrecision { get; set; }
|
public SearchPrecisionScore UserSettingSearchPrecision { get; set; }
|
||||||
|
|
||||||
private readonly IAlphabet _alphabet;
|
private readonly IAlphabet _alphabet;
|
||||||
|
|
||||||
public StringMatcher(IAlphabet alphabet = null)
|
public StringMatcher(IAlphabet alphabet, Settings settings)
|
||||||
|
{
|
||||||
|
_alphabet = alphabet;
|
||||||
|
UserSettingSearchPrecision = settings.QuerySearchPrecision;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a workaround to allow unit tests to set the instance
|
||||||
|
public StringMatcher(IAlphabet alphabet)
|
||||||
{
|
{
|
||||||
_alphabet = alphabet;
|
_alphabet = alphabet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StringMatcher Instance { get; internal set; }
|
|
||||||
|
|
||||||
public static MatchResult FuzzySearch(string query, string stringToCompare)
|
public static MatchResult FuzzySearch(string query, string stringToCompare)
|
||||||
{
|
{
|
||||||
return Instance.FuzzyMatch(query, stringToCompare);
|
return Ioc.Default.GetRequiredService<StringMatcher>().FuzzyMatch(query, stringToCompare);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MatchResult FuzzyMatch(string query, string stringToCompare)
|
public MatchResult FuzzyMatch(string query, string stringToCompare)
|
||||||
|
|
@ -241,16 +248,16 @@ namespace Flow.Launcher.Infrastructure
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAcronymChar(string stringToCompare, int compareStringIndex)
|
private static bool IsAcronymChar(string stringToCompare, int compareStringIndex)
|
||||||
=> char.IsUpper(stringToCompare[compareStringIndex]) ||
|
=> char.IsUpper(stringToCompare[compareStringIndex]) ||
|
||||||
compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
|
compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
|
||||||
char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
|
char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
|
||||||
|
|
||||||
private bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
|
private static bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
|
||||||
=> stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;
|
=> stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;
|
||||||
|
|
||||||
// To get the index of the closest space which preceeds the first matching index
|
// To get the index of the closest space which preceeds the first matching index
|
||||||
private int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
private static int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
||||||
{
|
{
|
||||||
var closestSpaceIndex = -1;
|
var closestSpaceIndex = -1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.ViewModel
|
namespace Flow.Launcher.ViewModel
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using System.ComponentModel;
|
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
|
||||||
{
|
{
|
||||||
public enum ProxyProperty
|
public enum ProxyProperty
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove. This is backwards compatibility for 1.10.0 release.
|
|
||||||
public string PythonDirectory { get; set; }
|
|
||||||
|
|
||||||
public Dictionary<string, Plugin> Plugins { get; set; } = new Dictionary<string, Plugin>();
|
public Dictionary<string, Plugin> Plugins { get; set; } = new Dictionary<string, Plugin>();
|
||||||
|
|
||||||
public void UpdatePluginSettings(List<PluginMetadata> metadatas)
|
public void UpdatePluginSettings(List<PluginMetadata> metadatas)
|
||||||
|
|
@ -38,25 +35,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
if (Plugins.ContainsKey(metadata.ID))
|
if (Plugins.ContainsKey(metadata.ID))
|
||||||
{
|
{
|
||||||
var settings = Plugins[metadata.ID];
|
var settings = Plugins[metadata.ID];
|
||||||
|
|
||||||
if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count > settings.ActionKeywords.Count)
|
|
||||||
{
|
|
||||||
// TODO: Remove. This is backwards compatibility for Explorer 1.8.0 release.
|
|
||||||
// Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder.
|
|
||||||
if (settings.Version.CompareTo("1.8.0") < 0)
|
|
||||||
{
|
|
||||||
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search
|
|
||||||
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search
|
|
||||||
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for quick access action keyword
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove. This is backwards compatibility for Explorer 1.9.0 release.
|
|
||||||
// Introduced a new action keywords in Explorer since 1.8.0, so need to update plugin setting in the UserData folder.
|
|
||||||
if (settings.Version.CompareTo("1.8.0") > 0)
|
|
||||||
{
|
|
||||||
settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for quick access action keyword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(settings.Version))
|
if (string.IsNullOrEmpty(settings.Version))
|
||||||
settings.Version = metadata.Version;
|
settings.Version = metadata.Version;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,39 @@
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
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 CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Infrastructure.Hotkey;
|
||||||
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using Flow.Launcher.Infrastructure.Storage;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
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 FlowLauncherJsonStorage<Settings> _storage;
|
||||||
|
private StringMatcher _stringMatcher = null;
|
||||||
|
|
||||||
|
public void SetStorage(FlowLauncherJsonStorage<Settings> storage)
|
||||||
|
{
|
||||||
|
_storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_stringMatcher = Ioc.Default.GetRequiredService<StringMatcher>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
_storage.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string language = Constant.SystemLanguageCode;
|
||||||
private string _theme = Constant.DefaultTheme;
|
private string _theme = Constant.DefaultTheme;
|
||||||
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
|
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
|
||||||
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
|
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
|
||||||
|
|
@ -20,6 +41,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
|
||||||
{
|
{
|
||||||
|
|
@ -42,7 +75,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
OnPropertyChanged(nameof(MaxResultsToShow));
|
OnPropertyChanged(nameof(MaxResultsToShow));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool UseDropShadowEffect { get; set; } = false;
|
public bool UseDropShadowEffect { get; set; } = true;
|
||||||
|
|
||||||
|
/* Appearance Settings. It should be separated from the setting later.*/
|
||||||
|
public double WindowHeightSize { get; set; } = 42;
|
||||||
|
public double ItemHeightSize { get; set; } = 58;
|
||||||
|
public double QueryBoxFontSize { get; set; } = 20;
|
||||||
|
public double ResultItemFontSize { get; set; } = 16;
|
||||||
|
public double ResultSubItemFontSize { get; set; } = 13;
|
||||||
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
public string 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 +91,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";
|
||||||
|
|
@ -62,9 +108,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
|
|
||||||
public double SettingWindowWidth { get; set; } = 1000;
|
public double SettingWindowWidth { get; set; } = 1000;
|
||||||
public double SettingWindowHeight { get; set; } = 700;
|
public double SettingWindowHeight { get; set; } = 700;
|
||||||
public double SettingWindowTop { get; set; }
|
public double? SettingWindowTop { get; set; } = null;
|
||||||
public double SettingWindowLeft { get; set; }
|
public double? SettingWindowLeft { get; set; } = null;
|
||||||
public WindowState SettingWindowState { get; set; } = WindowState.Normal;
|
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
|
||||||
|
|
||||||
public bool PrereleaseUpdateSource { get; set; }
|
public bool PrereleaseUpdateSource { get; set; }
|
||||||
|
|
||||||
|
|
@ -156,40 +202,28 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// when false Alphabet static service will always return empty results
|
/// when false Alphabet static service will always return empty results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ShouldUsePinyin { get; set; } = false;
|
public bool ShouldUsePinyin { get; set; } = false;
|
||||||
|
|
||||||
public bool AlwaysPreview { get; set; } = false;
|
public bool AlwaysPreview { get; set; } = false;
|
||||||
|
|
||||||
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 != null)
|
||||||
var precisionScore = (SearchPrecisionScore)Enum
|
_stringMatcher.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,17 +231,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; }
|
||||||
|
|
||||||
|
|
@ -219,7 +254,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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -227,6 +262,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public bool EnableUpdateLog { get; set; }
|
public bool EnableUpdateLog { get; set; }
|
||||||
|
|
||||||
public bool StartFlowLauncherOnSystemStartup { get; set; } = false;
|
public bool StartFlowLauncherOnSystemStartup { get; set; } = false;
|
||||||
|
public bool UseLogonTaskForStartup { get; set; } = false;
|
||||||
public bool HideOnStartup { get; set; } = true;
|
public bool HideOnStartup { get; set; } = true;
|
||||||
bool _hideNotifyIcon { get; set; }
|
bool _hideNotifyIcon { get; set; }
|
||||||
public bool HideNotifyIcon
|
public bool HideNotifyIcon
|
||||||
|
|
@ -242,8 +278,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public bool HideWhenDeactivated { get; set; } = true;
|
public bool HideWhenDeactivated { get; set; } = true;
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.RememberLastLaunchLocation;
|
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;
|
||||||
|
|
||||||
|
|
@ -255,17 +291,112 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected;
|
public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
|
||||||
|
public int CustomAnimationLength { get; set; } = 360;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool WMPInstalled { get; set; } = true;
|
||||||
|
|
||||||
// This needs to be loaded last by staying at the bottom
|
// 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
|
||||||
{
|
{
|
||||||
Selected,
|
Selected,
|
||||||
Empty,
|
Empty,
|
||||||
Preserved
|
Preserved,
|
||||||
|
ActionKeywordPreserved,
|
||||||
|
ActionKeywordSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ColorSchemes
|
public enum ColorSchemes
|
||||||
|
|
@ -274,7 +405,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
Light,
|
Light,
|
||||||
Dark
|
Dark
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SearchWindowScreens
|
public enum SearchWindowScreens
|
||||||
{
|
{
|
||||||
RememberLastLaunchLocation,
|
RememberLastLaunchLocation,
|
||||||
|
|
@ -283,7 +414,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
Primary,
|
Primary,
|
||||||
Custom
|
Custom
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SearchWindowAligns
|
public enum SearchWindowAligns
|
||||||
{
|
{
|
||||||
Center,
|
Center,
|
||||||
|
|
@ -292,4 +423,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
RightTop,
|
RightTop,
|
||||||
Custom
|
Custom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum AnimationSpeeds
|
||||||
|
{
|
||||||
|
Slow,
|
||||||
|
Medium,
|
||||||
|
Fast,
|
||||||
|
Custom
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
101
Flow.Launcher.Infrastructure/Win32Helper.cs
Normal file
101
Flow.Launcher.Infrastructure/Win32Helper.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Infrastructure
|
||||||
|
{
|
||||||
|
public static class Win32Helper
|
||||||
|
{
|
||||||
|
#region Blur Handling
|
||||||
|
|
||||||
|
/*
|
||||||
|
Found on https://github.com/riverar/sample-win10-aeroglass
|
||||||
|
*/
|
||||||
|
|
||||||
|
private enum AccentState
|
||||||
|
{
|
||||||
|
ACCENT_DISABLED = 0,
|
||||||
|
ACCENT_ENABLE_GRADIENT = 1,
|
||||||
|
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
|
||||||
|
ACCENT_ENABLE_BLURBEHIND = 3,
|
||||||
|
ACCENT_INVALID_STATE = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct AccentPolicy
|
||||||
|
{
|
||||||
|
public AccentState AccentState;
|
||||||
|
public int AccentFlags;
|
||||||
|
public int GradientColor;
|
||||||
|
public int AnimationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct WindowCompositionAttributeData
|
||||||
|
{
|
||||||
|
public WindowCompositionAttribute Attribute;
|
||||||
|
public IntPtr Data;
|
||||||
|
public int SizeOfData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum WindowCompositionAttribute
|
||||||
|
{
|
||||||
|
WCA_ACCENT_POLICY = 19
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the blur theme is enabled
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsBlurTheme()
|
||||||
|
{
|
||||||
|
if (Environment.OSVersion.Version >= new Version(6, 2))
|
||||||
|
{
|
||||||
|
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
||||||
|
|
||||||
|
if (resource is bool b)
|
||||||
|
return b;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the blur for a window via SetWindowCompositionAttribute
|
||||||
|
/// </summary>
|
||||||
|
public static void SetBlurForWindow(Window w, bool blur)
|
||||||
|
{
|
||||||
|
SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetWindowAccent(Window w, AccentState state)
|
||||||
|
{
|
||||||
|
var windowHelper = new WindowInteropHelper(w);
|
||||||
|
|
||||||
|
windowHelper.EnsureHandle();
|
||||||
|
|
||||||
|
var accent = new AccentPolicy { AccentState = state };
|
||||||
|
var accentStructSize = Marshal.SizeOf(accent);
|
||||||
|
|
||||||
|
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
|
||||||
|
Marshal.StructureToPtr(accent, accentPtr, false);
|
||||||
|
|
||||||
|
var data = new WindowCompositionAttributeData
|
||||||
|
{
|
||||||
|
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
|
||||||
|
SizeOfData = accentStructSize,
|
||||||
|
Data = accentPtr
|
||||||
|
};
|
||||||
|
|
||||||
|
SetWindowCompositionAttribute(windowHelper.Handle, ref data);
|
||||||
|
|
||||||
|
Marshal.FreeHGlobal(accentPtr);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,18 +2,47 @@
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Context provided as a parameter when invoking a
|
||||||
|
/// <see cref="Result.Action"/> or <see cref="Result.AsyncAction"/>
|
||||||
|
/// </summary>
|
||||||
public class ActionContext
|
public class ActionContext
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the press state of certain special keys.
|
||||||
|
/// </summary>
|
||||||
public SpecialKeyState SpecialKeyState { get; set; }
|
public SpecialKeyState SpecialKeyState { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the press state of certain special keys.
|
||||||
|
/// </summary>
|
||||||
public class SpecialKeyState
|
public class SpecialKeyState
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True if the Ctrl key is pressed.
|
||||||
|
/// </summary>
|
||||||
public bool CtrlPressed { get; set; }
|
public bool CtrlPressed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the Shift key is pressed.
|
||||||
|
/// </summary>
|
||||||
public bool ShiftPressed { get; set; }
|
public bool ShiftPressed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the Alt key is pressed.
|
||||||
|
/// </summary>
|
||||||
public bool AltPressed { get; set; }
|
public bool AltPressed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the Windows key is pressed.
|
||||||
|
/// </summary>
|
||||||
public bool WinPressed { get; set; }
|
public bool WinPressed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get this object represented as a <see cref="ModifierKeys"/> flag combination.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public ModifierKeys ToModifierKeys()
|
public ModifierKeys ToModifierKeys()
|
||||||
{
|
{
|
||||||
return (CtrlPressed ? ModifierKeys.Control : ModifierKeys.None) |
|
return (CtrlPressed ? ModifierKeys.Control : ModifierKeys.None) |
|
||||||
|
|
@ -21,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);
|
||||||
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Windows;
|
using System;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
|
|
@ -32,6 +33,24 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <returns>return true to continue handling, return false to intercept system handling</returns>
|
/// <returns>return true to continue handling, return false to intercept system handling</returns>
|
||||||
public delegate bool FlowLauncherGlobalKeyboardEventHandler(int keyevent, int vkcode, SpecialKeyState state);
|
public delegate bool FlowLauncherGlobalKeyboardEventHandler(int keyevent, int vkcode, SpecialKeyState state);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate for when the visibility is changed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
/// <param name="args"></param>
|
||||||
|
public delegate void VisibilityChangedEventHandler(object sender, VisibilityChangedEventArgs args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The event args for <see cref="VisibilityChangedEventHandler"/>
|
||||||
|
/// </summary>
|
||||||
|
public class VisibilityChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <see langword="true"/> if the main window has become visible
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVisible { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Arguments container for the Key Down event
|
/// Arguments container for the Key Down event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
using System;
|
namespace Flow.Launcher.Plugin
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base Interface for Flow's special plugin feature interface
|
/// Base Interface for Flow's special plugin feature interface
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>4.0.0</Version>
|
<Version>4.4.0</Version>
|
||||||
<PackageVersion>4.0.0</PackageVersion>
|
<PackageVersion>4.4.0</PackageVersion>
|
||||||
<AssemblyVersion>4.0.0</AssemblyVersion>
|
<AssemblyVersion>4.4.0</AssemblyVersion>
|
||||||
<FileVersion>4.0.0</FileVersion>
|
<FileVersion>4.4.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>
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
<PackageTags>flowlauncher</PackageTags>
|
<PackageTags>flowlauncher</PackageTags>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
<PackageReadmeFile>Readme.md</PackageReadmeFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(APPVEYOR)' == 'True'">
|
<PropertyGroup Condition="'$(APPVEYOR)' == 'True'">
|
||||||
|
|
@ -56,7 +57,11 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="README.md" />
|
<AdditionalFiles Include="NativeMethods.txt" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Readme.md" Pack="true" PackagePath="\" />
|
||||||
<None Include="FodyWeavers.xml" />
|
<None Include="FodyWeavers.xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
@ -66,7 +71,11 @@
|
||||||
<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="2022.3.1" />
|
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,4 @@
|
||||||
using System;
|
namespace Flow.Launcher.Plugin
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Media;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Text with FontFamily specified
|
/// Text with FontFamily specified
|
||||||
|
|
|
||||||
40
Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs
Normal file
40
Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This interface is for plugins that wish to provide file preview (external preview)
|
||||||
|
/// via a third party app instead of the default preview.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAsyncExternalPreview : IFeatures
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Method for opening/showing the preview.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The file path to open the preview for</param>
|
||||||
|
/// <param name="sendFailToast">Whether to send a toast message notification on failure for the user</param>
|
||||||
|
public Task OpenPreviewAsync(string path, bool sendFailToast = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method for closing/hiding the preview.
|
||||||
|
/// </summary>
|
||||||
|
public Task ClosePreviewAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method for switching the preview to the next file result.
|
||||||
|
/// This requires the external preview be already open/showing
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The file path to switch the preview for</param>
|
||||||
|
/// <param name="sendFailToast">Whether to send a toast message notification on failure for the user</param>
|
||||||
|
public Task SwitchPreviewAsync(string path, bool sendFailToast = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows the preview plugin to override the AlwaysPreview setting. Typically useful if plugin's preview does not
|
||||||
|
/// fully work well with being shown together when the query window appears with results.
|
||||||
|
/// When AlwaysPreview setting is on and this is set to false, the preview will not be shown when query
|
||||||
|
/// window appears with results, instead the internal preview will be shown.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool AllowAlwaysPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds support for presenting additional options for a given <see cref="Result"/> from a context menu.
|
||||||
|
/// </summary>
|
||||||
public interface IContextMenu : IFeatures
|
public interface IContextMenu : IFeatures
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Load context menu items for the given result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="selectedResult">
|
||||||
|
/// The <see cref="Result"/> for which the user has activated the context menu.
|
||||||
|
/// </param>
|
||||||
List<Result> LoadContextMenus(Result selectedResult);
|
List<Result> LoadContextMenus(Result selectedResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -7,8 +7,14 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPluginI18n : IFeatures
|
public interface IPluginI18n : IFeatures
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get a localised version of the plugin's title
|
||||||
|
/// </summary>
|
||||||
string GetTranslatedPluginTitle();
|
string GetTranslatedPluginTitle();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a localised version of the plugin's description
|
||||||
|
/// </summary>
|
||||||
string GetTranslatedPluginDescription();
|
string GetTranslatedPluginDescription();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -16,12 +17,13 @@ namespace Flow.Launcher.Plugin
|
||||||
public interface IPublicAPI
|
public interface IPublicAPI
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Change Flow.Launcher query
|
/// Change Flow.Launcher query.
|
||||||
|
/// When current results are from context menu or history, it will go back to query results before changing query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">query text</param>
|
/// <param name="query">query text</param>
|
||||||
/// <param name="requery">
|
/// <param name="requery">
|
||||||
/// force requery By default, Flow Launcher will not fire query if your query is same with existing one.
|
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
|
||||||
/// Set this to true to force Flow Launcher requerying
|
/// Set this to <see langword="true"/> to force Flow Launcher requerying
|
||||||
/// </param>
|
/// </param>
|
||||||
void ChangeQuery(string query, bool requery = false);
|
void ChangeQuery(string query, bool requery = false);
|
||||||
|
|
||||||
|
|
@ -38,12 +40,18 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <exception cref="FileNotFoundException">Thrown when unable to find the file specified in the command </exception>
|
/// <exception cref="FileNotFoundException">Thrown when unable to find the file specified in the command </exception>
|
||||||
/// <exception cref="Win32Exception">Thrown when error occurs during the execution of the command </exception>
|
/// <exception cref="Win32Exception">Thrown when error occurs during the execution of the command </exception>
|
||||||
void ShellRun(string cmd, string filename = "cmd.exe");
|
void ShellRun(string cmd, string filename = "cmd.exe");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy Text to clipboard
|
/// Copies the passed in text and shows a message indicating whether the operation was completed successfully.
|
||||||
|
/// When directCopy is set to true and passed in text is the path to a file or directory,
|
||||||
|
/// the actual file/directory will be copied to clipboard. Otherwise the text itself will still be copied to clipboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text">Text to save on clipboard</param>
|
/// <param name="text">Text to save on clipboard</param>
|
||||||
public void CopyToClipboard(string text);
|
/// <param name="directCopy">When true it will directly copy the file/folder from the path specified in text</param>
|
||||||
|
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
|
||||||
|
/// It will show file/folder/text is copied successfully.
|
||||||
|
/// Turn this off to show your own notification after copy is done.</param>>
|
||||||
|
public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save everything, all of Flow Launcher and plugins' data and settings
|
/// Save everything, all of Flow Launcher and plugins' data and settings
|
||||||
|
|
@ -80,6 +88,22 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ShowMainWindow();
|
void ShowMainWindow();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hide MainWindow
|
||||||
|
/// </summary>
|
||||||
|
void HideMainWindow();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Representing whether the main window is visible
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool IsMainWindowVisible();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
|
||||||
|
/// </summary>
|
||||||
|
event VisibilityChangedEventHandler VisibilityChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show message box
|
/// Show message box
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -116,13 +140,6 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
List<PluginPair> GetAllPlugins();
|
List<PluginPair> GetAllPlugins();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fired after global keyboard events
|
|
||||||
/// if you want to hook something like Ctrl+R, you should use this event
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Unable to Retrieve correct return value")]
|
|
||||||
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a callback for Global Keyboard Event
|
/// Register a callback for Global Keyboard Event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -165,9 +182,13 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url">URL to download file</param>
|
/// <param name="url">URL to download file</param>
|
||||||
/// <param name="filePath">path to save downloaded file</param>
|
/// <param name="filePath">path to save downloaded file</param>
|
||||||
|
/// <param name="reportProgress">
|
||||||
|
/// Action to report progress. The input of the action is the progress value which is a double value between 0 and 100.
|
||||||
|
/// It will be called if url support range request and the reportProgress is not null.
|
||||||
|
/// </param>
|
||||||
/// <param name="token">place to store file</param>
|
/// <param name="token">place to store file</param>
|
||||||
/// <returns>Task showing the progress</returns>
|
/// <returns>Task showing the progress</returns>
|
||||||
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
|
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add ActionKeyword for specific plugin
|
/// Add ActionKeyword for specific plugin
|
||||||
|
|
@ -259,5 +280,69 @@ 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.
|
||||||
|
/// When current results are from context menu or history, it will go back to query results before requerying.
|
||||||
|
/// </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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Back to the query results.
|
||||||
|
/// This method should run when selected item is from context menu or history.
|
||||||
|
/// </summary>
|
||||||
|
public void BackToQueryResults();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays a standardised Flow message box.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageBoxText">The message of the message box.</param>
|
||||||
|
/// <param name="caption">The caption of the message box.</param>
|
||||||
|
/// <param name="button">Specifies which button or buttons to display.</param>
|
||||||
|
/// <param name="icon">Specifies the icon to display.</param>
|
||||||
|
/// <param name="defaultResult">Specifies the default result of the message box.</param>
|
||||||
|
/// <returns>Specifies which message box button is clicked by the user.</returns>
|
||||||
|
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays a standardised Flow progress box.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="caption">The caption of the progress box.</param>
|
||||||
|
/// <param name="reportProgressAsync">
|
||||||
|
/// Time-consuming task function, whose input is the action to report progress.
|
||||||
|
/// The input of the action is the progress value which is a double value between 0 and 100.
|
||||||
|
/// If there are any exceptions, this action will be null.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="cancelProgress">When user cancel the progress, this action will be called.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action cancelProgress = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start the loading bar in main window
|
||||||
|
/// </summary>
|
||||||
|
public void StartLoadingBar();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop the loading bar in main window
|
||||||
|
/// </summary>
|
||||||
|
public void StopLoadingBar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue