mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into ProgressBarDispatcher
This commit is contained in:
commit
9c1ff85b53
814 changed files with 59900 additions and 22887 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
|
||||
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.symbols = constant_fields
|
||||
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_using_directive_placement = outside_namespace:silent
|
||||
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_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
|
|
|
|||
22
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
22
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
|
|
@ -14,7 +14,9 @@ body:
|
|||
options:
|
||||
- label: >
|
||||
I have checked that this issue has not already been reported.
|
||||
|
||||
- label: >
|
||||
I am using the latest version of Flow Launcher.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem Description
|
||||
|
|
@ -41,25 +43,25 @@ body:
|
|||
- type: input
|
||||
attributes:
|
||||
label: Flow Launcher Version
|
||||
description: Go to "Settings" => "About".
|
||||
value: v1.8.3
|
||||
description: Go to "Settings" => "About". If you are using a prerelease version please append the build number.
|
||||
placeholder: "Example: 1.11.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Windows Build Number
|
||||
description: Run "ver" at CMD (command prompt).
|
||||
value: 10.0.19043.1288
|
||||
description: Run "ver" at CMD (command prompt) or go to Windows Settings -> Systems -> About.
|
||||
placeholder: "Example: 10.0.19043.1288"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Error Log
|
||||
description: >
|
||||
Log file place:
|
||||
|
||||
- The latest version place: `%AppData%\FlowLauncher\Logs\<version>\<date>.txt`
|
||||
|
||||
- For portable mode: `%LocalAppData%\FlowLauncher\<App-Version>\UserData\Logs\<version>\<date>.txt`
|
||||
From flow type 'open log location' and find log file with the corresponding date containing the error info.
|
||||
value: >
|
||||
<details>
|
||||
|
||||
|
|
|
|||
17
.github/actions/spelling/README.md
vendored
Normal file
17
.github/actions/spelling/README.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# check-spelling/check-spelling configuration
|
||||
|
||||
File | Purpose | Format | Info
|
||||
-|-|-|-
|
||||
[dictionary.txt](dictionary.txt) | Replacement dictionary (creating this file will override the default dictionary) | one word per line | [dictionary](https://github.com/check-spelling/check-spelling/wiki/Configuration#dictionary)
|
||||
[allow.txt](allow.txt) | Add words to the dictionary | one word per line (only letters and `'`s allowed) | [allow](https://github.com/check-spelling/check-spelling/wiki/Configuration#allow)
|
||||
[reject.txt](reject.txt) | Remove words from the dictionary (after allow) | grep pattern matching whole dictionary words | [reject](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-reject)
|
||||
[excludes.txt](excludes.txt) | Files to ignore entirely | perl regular expression | [excludes](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-excludes)
|
||||
[only.txt](only.txt) | Only check matching files (applied after excludes) | perl regular expression | [only](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-only)
|
||||
[patterns.txt](patterns.txt) | Patterns to ignore from checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns)
|
||||
[candidate.patterns](candidate.patterns) | Patterns that might be worth adding to [patterns.txt](patterns.txt) | perl regular expression with optional comment block introductions (all matches will be suggested) | [candidates](https://github.com/check-spelling/check-spelling/wiki/Feature:-Suggest-patterns)
|
||||
[line_forbidden.patterns](line_forbidden.patterns) | Patterns to flag in checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns)
|
||||
[expect.txt](expect.txt) | Expected words that aren't in the dictionary | one word per line (sorted, alphabetically) | [expect](https://github.com/check-spelling/check-spelling/wiki/Configuration#expect)
|
||||
[advice.md](advice.md) | Supplement for GitHub comment when unrecognized words are found | GitHub Markdown | [advice](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice)
|
||||
|
||||
Note: you can replace any of these files with a directory by the same name (minus the suffix)
|
||||
and then include multiple files inside that directory (with that suffix) to merge multiple files together.
|
||||
25
.github/actions/spelling/advice.md
vendored
Normal file
25
.github/actions/spelling/advice.md
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
|
||||
<details><summary>If the flagged items are :exploding_head: false positives</summary>
|
||||
|
||||
If items relate to a ...
|
||||
* binary file (or some other file you wouldn't want to check at all).
|
||||
|
||||
Please add a file path to the `excludes.txt` file matching the containing file.
|
||||
|
||||
File paths are Perl 5 Regular Expressions - you can [test](
|
||||
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.
|
||||
|
||||
`^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
|
||||
../tree/HEAD/README.md) (on whichever branch you're using).
|
||||
|
||||
* well-formed pattern.
|
||||
|
||||
If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
|
||||
try adding it to the `patterns.txt` file.
|
||||
|
||||
Patterns are Perl 5 Regular Expressions - you can [test](
|
||||
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.
|
||||
|
||||
Note that patterns can't match multiline strings.
|
||||
|
||||
</details>
|
||||
8
.github/actions/spelling/allow.txt
vendored
Normal file
8
.github/actions/spelling/allow.txt
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
github
|
||||
https
|
||||
ssh
|
||||
ubuntu
|
||||
runcount
|
||||
Firefox
|
||||
Português
|
||||
Português (Brasil)
|
||||
522
.github/actions/spelling/candidate.patterns
vendored
Normal file
522
.github/actions/spelling/candidate.patterns
vendored
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
# marker to ignore all code on line
|
||||
^.*/\* #no-spell-check-line \*/.*$
|
||||
# marker for ignoring a comment to the end of the line
|
||||
// #no-spell-check.*$
|
||||
|
||||
# patch hunk comments
|
||||
^\@\@ -\d+(?:,\d+|) \+\d+(?:,\d+|) \@\@ .*
|
||||
# git index header
|
||||
index [0-9a-z]{7,40}\.\.[0-9a-z]{7,40}
|
||||
|
||||
# cid urls
|
||||
(['"])cid:.*?\g{-1}
|
||||
|
||||
# data url in parens
|
||||
\(data:[^)]*?(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})[^)]*\)
|
||||
# data url in quotes
|
||||
([`'"])data:.*?(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,}).*\g{-1}
|
||||
# data url
|
||||
data:[-a-zA-Z=;:/0-9+]*,\S*
|
||||
|
||||
# mailto urls
|
||||
mailto:[-a-zA-Z=;:/?%&0-9+@.]{3,}
|
||||
|
||||
# magnet urls
|
||||
magnet:[?=:\w]+
|
||||
|
||||
# magnet urls
|
||||
"magnet:[^"]+"
|
||||
|
||||
# obs:
|
||||
"obs:[^"]*"
|
||||
|
||||
# The `\b` here means a break, it's the fancy way to handle urls, but it makes things harder to read
|
||||
# In this examples content, I'm using a number of different ways to match things to show various approaches
|
||||
# asciinema
|
||||
\basciinema\.org/a/[0-9a-zA-Z]+
|
||||
|
||||
# apple
|
||||
\bdeveloper\.apple\.com/[-\w?=/]+
|
||||
# Apple music
|
||||
\bembed\.music\.apple\.com/fr/playlist/usr-share/[-\w.]+
|
||||
|
||||
# appveyor api
|
||||
\bci\.appveyor\.com/api/projects/status/[0-9a-z]+
|
||||
# appveyor project
|
||||
\bci\.appveyor\.com/project/(?:[^/\s"]*/){2}builds?/\d+/job/[0-9a-z]+
|
||||
|
||||
# Amazon
|
||||
|
||||
# Amazon
|
||||
\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)
|
||||
# AWS S3
|
||||
\b\w*\.s3[^.]*\.amazonaws\.com/[-\w/&#%_?:=]*
|
||||
# AWS execute-api
|
||||
\b[0-9a-z]{10}\.execute-api\.[-0-9a-z]+\.amazonaws\.com\b
|
||||
# AWS ELB
|
||||
\b\w+\.[-0-9a-z]+\.elb\.amazonaws\.com\b
|
||||
# AWS SNS
|
||||
\bsns\.[-0-9a-z]+.amazonaws\.com/[-\w/&#%_?:=]*
|
||||
# AWS VPC
|
||||
vpc-\w+
|
||||
|
||||
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
|
||||
# YouTube url
|
||||
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
|
||||
# YouTube music
|
||||
\bmusic\.youtube\.com/youtubei/v1/browse(?:[?&]\w+=[-a-zA-Z0-9?&=_]*)
|
||||
# YouTube tag
|
||||
<\s*youtube\s+id=['"][-a-zA-Z0-9?_]*['"]
|
||||
# YouTube image
|
||||
\bimg\.youtube\.com/vi/[-a-zA-Z0-9?&=_]*
|
||||
# Google Accounts
|
||||
\baccounts.google.com/[-_/?=.:;+%&0-9a-zA-Z]*
|
||||
# Google Analytics
|
||||
\bgoogle-analytics\.com/collect.[-0-9a-zA-Z?%=&_.~]*
|
||||
# Google APIs
|
||||
\bgoogleapis\.(?:com|dev)/[a-z]+/(?:v\d+/|)[a-z]+/[-@:./?=\w+|&]+
|
||||
# Google Storage
|
||||
\b[-a-zA-Z0-9.]*\bstorage\d*\.googleapis\.com(?:/\S*|)
|
||||
# Google Calendar
|
||||
\bcalendar\.google\.com/calendar(?:/u/\d+|)/embed\?src=[@./?=\w&%]+
|
||||
\w+\@group\.calendar\.google\.com\b
|
||||
# Google DataStudio
|
||||
\bdatastudio\.google\.com/(?:(?:c/|)u/\d+/|)(?:embed/|)(?:open|reporting|datasources|s)/[-0-9a-zA-Z]+(?:/page/[-0-9a-zA-Z]+|)
|
||||
# The leading `/` here is as opposed to the `\b` above
|
||||
# ... a short way to match `https://` or `http://` since most urls have one of those prefixes
|
||||
# Google Docs
|
||||
/docs\.google\.com/[a-z]+/(?:ccc\?key=\w+|(?:u/\d+|d/(?:e/|)[0-9a-zA-Z_-]+/)?(?:edit\?[-\w=#.]*|/\?[\w=&]*|))
|
||||
# Google Drive
|
||||
\bdrive\.google\.com/(?:file/d/|open)[-0-9a-zA-Z_?=]*
|
||||
# Google Groups
|
||||
\bgroups\.google\.com/(?:(?:forum/#!|d/)(?:msg|topics?|searchin)|a)/[^/\s"]+/[-a-zA-Z0-9$]+(?:/[-a-zA-Z0-9]+)*
|
||||
# Google Maps
|
||||
\bmaps\.google\.com/maps\?[\w&;=]*
|
||||
# Google themes
|
||||
themes\.googleusercontent\.com/static/fonts/[^/\s"]+/v\d+/[^.]+.
|
||||
# Google CDN
|
||||
\bclients2\.google(?:usercontent|)\.com[-0-9a-zA-Z/.]*
|
||||
# Goo.gl
|
||||
/goo\.gl/[a-zA-Z0-9]+
|
||||
# Google Chrome Store
|
||||
\bchrome\.google\.com/webstore/detail/[-\w]*(?:/\w*|)
|
||||
# Google Books
|
||||
\bgoogle\.(?:\w{2,4})/books(?:/\w+)*\?[-\w\d=&#.]*
|
||||
# Google Fonts
|
||||
\bfonts\.(?:googleapis|gstatic)\.com/[-/?=:;+&0-9a-zA-Z]*
|
||||
# Google Forms
|
||||
\bforms\.gle/\w+
|
||||
# Google Scholar
|
||||
\bscholar\.google\.com/citations\?user=[A-Za-z0-9_]+
|
||||
# Google Colab Research Drive
|
||||
\bcolab\.research\.google\.com/drive/[-0-9a-zA-Z_?=]*
|
||||
|
||||
# GitHub SHAs (api)
|
||||
\bapi.github\.com/repos(?:/[^/\s"]+){3}/[0-9a-f]+\b
|
||||
# GitHub SHAs (markdown)
|
||||
(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|)
|
||||
# GitHub SHAs
|
||||
\bgithub\.com(?:/[^/\s"]+){2}[@#][0-9a-f]+\b
|
||||
# GitHub wiki
|
||||
\bgithub\.com/(?:[^/]+/){2}wiki/(?:(?:[^/]+/|)_history|[^/]+(?:/_compare|)/[0-9a-f.]{40,})\b
|
||||
# githubusercontent
|
||||
/[-a-z0-9]+\.githubusercontent\.com/[-a-zA-Z0-9?&=_\/.]*
|
||||
# githubassets
|
||||
\bgithubassets.com/[0-9a-f]+(?:[-/\w.]+)
|
||||
# gist github
|
||||
\bgist\.github\.com/[^/\s"]+/[0-9a-f]+
|
||||
# git.io
|
||||
\bgit\.io/[0-9a-zA-Z]+
|
||||
# GitHub JSON
|
||||
"node_id": "[-a-zA-Z=;:/0-9+]*"
|
||||
# Contributor
|
||||
\[[^\]]+\]\(https://github\.com/[^/\s"]+\)
|
||||
# GHSA
|
||||
GHSA(?:-[0-9a-z]{4}){3}
|
||||
|
||||
# GitLab commit
|
||||
\bgitlab\.[^/\s"]*/\S+/\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\b
|
||||
# GitLab merge requests
|
||||
\bgitlab\.[^/\s"]*/\S+/\S+/-/merge_requests/\d+/diffs#[0-9a-f]{40}\b
|
||||
# GitLab uploads
|
||||
\bgitlab\.[^/\s"]*/uploads/[-a-zA-Z=;:/0-9+]*
|
||||
# GitLab commits
|
||||
\bgitlab\.[^/\s"]*/(?:[^/\s"]+/){2}commits?/[0-9a-f]+\b
|
||||
|
||||
# binanace
|
||||
accounts.binance.com/[a-z/]*oauth/authorize\?[-0-9a-zA-Z&%]*
|
||||
|
||||
# bitbucket diff
|
||||
\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}diff(?:stat|)(?:/[^/\s"]+){2}:[0-9a-f]+
|
||||
# bitbucket repositories commits
|
||||
\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}commits?/[0-9a-f]+
|
||||
# bitbucket commits
|
||||
\bbitbucket\.org/(?:[^/\s"]+/){2}commits?/[0-9a-f]+
|
||||
|
||||
# bit.ly
|
||||
\bbit\.ly/\w+
|
||||
|
||||
# bitrise
|
||||
\bapp\.bitrise\.io/app/[0-9a-f]*/[\w.?=&]*
|
||||
|
||||
# bootstrapcdn.com
|
||||
\bbootstrapcdn\.com/[-./\w]+
|
||||
|
||||
# cdn.cloudflare.com
|
||||
\bcdnjs\.cloudflare\.com/[./\w]+
|
||||
|
||||
# circleci
|
||||
\bcircleci\.com/gh(?:/[^/\s"]+){1,5}.[a-z]+\?[-0-9a-zA-Z=&]+
|
||||
|
||||
# gitter
|
||||
\bgitter\.im(?:/[^/\s"]+){2}\?at=[0-9a-f]+
|
||||
|
||||
# gravatar
|
||||
\bgravatar\.com/avatar/[0-9a-f]+
|
||||
|
||||
# ibm
|
||||
[a-z.]*ibm\.com/[-_#=:%!?~.\\/\d\w]*
|
||||
|
||||
# imgur
|
||||
\bimgur\.com/[^.]+
|
||||
|
||||
# Internet Archive
|
||||
\barchive\.org/web/\d+/(?:[-\w.?,'/\\+&%$#_:]*)
|
||||
|
||||
# discord
|
||||
/discord(?:app\.com|\.gg)/(?:invite/)?[a-zA-Z0-9]{7,}
|
||||
|
||||
# Disqus
|
||||
\bdisqus\.com/[-\w/%.()!?&=_]*
|
||||
|
||||
# medium link
|
||||
\blink\.medium\.com/[a-zA-Z0-9]+
|
||||
# medium
|
||||
\bmedium\.com/\@?[^/\s"]+/[-\w]+
|
||||
|
||||
# microsoft
|
||||
\b(?:https?://|)(?:(?:download\.visualstudio|docs|msdn2?|research)\.microsoft|blogs\.msdn)\.com/[-_a-zA-Z0-9()=./%]*
|
||||
# powerbi
|
||||
\bapp\.powerbi\.com/reportEmbed/[^"' ]*
|
||||
# vs devops
|
||||
\bvisualstudio.com(?::443|)/[-\w/?=%&.]*
|
||||
# microsoft store
|
||||
\bmicrosoft\.com/store/apps/\w+
|
||||
|
||||
# mvnrepository.com
|
||||
\bmvnrepository\.com/[-0-9a-z./]+
|
||||
|
||||
# now.sh
|
||||
/[0-9a-z-.]+\.now\.sh\b
|
||||
|
||||
# oracle
|
||||
\bdocs\.oracle\.com/[-0-9a-zA-Z./_?#&=]*
|
||||
|
||||
# chromatic.com
|
||||
/\S+.chromatic.com\S*[")]
|
||||
|
||||
# codacy
|
||||
\bapi\.codacy\.com/project/badge/Grade/[0-9a-f]+
|
||||
|
||||
# compai
|
||||
\bcompai\.pub/v1/png/[0-9a-f]+
|
||||
|
||||
# mailgun api
|
||||
\.api\.mailgun\.net/v3/domains/[0-9a-z]+\.mailgun.org/messages/[0-9a-zA-Z=@]*
|
||||
# mailgun
|
||||
\b[0-9a-z]+.mailgun.org
|
||||
|
||||
# /message-id/
|
||||
/message-id/[-\w@./%]+
|
||||
|
||||
# Reddit
|
||||
\breddit\.com/r/[/\w_]*
|
||||
|
||||
# requestb.in
|
||||
\brequestb\.in/[0-9a-z]+
|
||||
|
||||
# sched
|
||||
\b[a-z0-9]+\.sched\.com\b
|
||||
|
||||
# Slack url
|
||||
slack://[a-zA-Z0-9?&=]+
|
||||
# Slack
|
||||
\bslack\.com/[-0-9a-zA-Z/_~?&=.]*
|
||||
# Slack edge
|
||||
\bslack-edge\.com/[-a-zA-Z0-9?&=%./]+
|
||||
# Slack images
|
||||
\bslack-imgs\.com/[-a-zA-Z0-9?&=%.]+
|
||||
|
||||
# shields.io
|
||||
\bshields\.io/[-\w/%?=&.:+;,]*
|
||||
|
||||
# stackexchange -- https://stackexchange.com/feeds/sites
|
||||
\b(?:askubuntu|serverfault|stack(?:exchange|overflow)|superuser).com/(?:questions/\w+/[-\w]+|a/)
|
||||
|
||||
# Sentry
|
||||
[0-9a-f]{32}\@o\d+\.ingest\.sentry\.io\b
|
||||
|
||||
# Twitter markdown
|
||||
\[\@[^[/\]:]*?\]\(https://twitter.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)\)
|
||||
# Twitter hashtag
|
||||
\btwitter\.com/hashtag/[\w?_=&]*
|
||||
# Twitter status
|
||||
\btwitter\.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)
|
||||
# Twitter profile images
|
||||
\btwimg\.com/profile_images/[_\w./]*
|
||||
# Twitter media
|
||||
\btwimg\.com/media/[-_\w./?=]*
|
||||
# Twitter link shortened
|
||||
\bt\.co/\w+
|
||||
|
||||
# facebook
|
||||
\bfburl\.com/[0-9a-z_]+
|
||||
# facebook CDN
|
||||
\bfbcdn\.net/[\w/.,]*
|
||||
# facebook watch
|
||||
\bfb\.watch/[0-9A-Za-z]+
|
||||
|
||||
# dropbox
|
||||
\bdropbox\.com/sh?/[^/\s"]+/[-0-9A-Za-z_.%?=&;]+
|
||||
|
||||
# ipfs protocol
|
||||
ipfs://[0-9a-z]*
|
||||
# ipfs url
|
||||
/ipfs/[0-9a-z]*
|
||||
|
||||
# w3
|
||||
\bw3\.org/[-0-9a-zA-Z/#.]+
|
||||
|
||||
# loom
|
||||
\bloom\.com/embed/[0-9a-f]+
|
||||
|
||||
# regex101
|
||||
\bregex101\.com/r/[^/\s"]+/\d+
|
||||
|
||||
# figma
|
||||
\bfigma\.com/file(?:/[0-9a-zA-Z]+/)+
|
||||
|
||||
# freecodecamp.org
|
||||
\bfreecodecamp\.org/[-\w/.]+
|
||||
|
||||
# image.tmdb.org
|
||||
\bimage\.tmdb\.org/[/\w.]+
|
||||
|
||||
# mermaid
|
||||
\bmermaid\.ink/img/[-\w]+|\bmermaid-js\.github\.io/mermaid-live-editor/#/edit/[-\w]+
|
||||
|
||||
# Wikipedia
|
||||
\ben\.wikipedia\.org/wiki/[-\w%.#]+
|
||||
|
||||
# gitweb
|
||||
[^"\s]+/gitweb/\S+;h=[0-9a-f]+
|
||||
|
||||
# HyperKitty lists
|
||||
/archives/list/[^@/]+\@[^/\s"]*/message/[^/\s"]*/
|
||||
|
||||
# lists
|
||||
/thread\.html/[^"\s]+
|
||||
|
||||
# list-management
|
||||
\blist-manage\.com/subscribe(?:[?&](?:u|id)=[0-9a-f]+)+
|
||||
|
||||
# kubectl.kubernetes.io/last-applied-configuration
|
||||
"kubectl.kubernetes.io/last-applied-configuration": ".*"
|
||||
|
||||
# pgp
|
||||
\bgnupg\.net/pks/lookup[?&=0-9a-zA-Z]*
|
||||
|
||||
# Spotify
|
||||
\bopen\.spotify\.com/embed/playlist/\w+
|
||||
|
||||
# Mastodon
|
||||
\bmastodon\.[-a-z.]*/(?:media/|\@)[?&=0-9a-zA-Z_]*
|
||||
|
||||
# scastie
|
||||
\bscastie\.scala-lang\.org/[^/]+/\w+
|
||||
|
||||
# images.unsplash.com
|
||||
\bimages\.unsplash\.com/(?:(?:flagged|reserve)/|)[-\w./%?=%&.;]+
|
||||
|
||||
# pastebin
|
||||
\bpastebin\.com/[\w/]+
|
||||
|
||||
# heroku
|
||||
\b\w+\.heroku\.com/source/archive/\w+
|
||||
|
||||
# quip
|
||||
\b\w+\.quip\.com/\w+(?:(?:#|/issues/)\w+)?
|
||||
|
||||
# badgen.net
|
||||
\bbadgen\.net/badge/[^")\]'\s]+
|
||||
|
||||
# statuspage.io
|
||||
\w+\.statuspage\.io\b
|
||||
|
||||
# media.giphy.com
|
||||
\bmedia\.giphy\.com/media/[^/]+/[\w.?&=]+
|
||||
|
||||
# tinyurl
|
||||
\btinyurl\.com/\w+
|
||||
|
||||
# getopts
|
||||
\bgetopts\s+(?:"[^"]+"|'[^']+')
|
||||
|
||||
# ANSI color codes
|
||||
(?:\\(?:u00|x)1b|\x1b)\[\d+(?:;\d+|)m
|
||||
|
||||
# URL escaped characters
|
||||
\%[0-9A-F][A-F]
|
||||
# IPv6
|
||||
\b(?:[0-9a-fA-F]{0,4}:){3,7}[0-9a-fA-F]{0,4}\b
|
||||
# c99 hex digits (not the full format, just one I've seen)
|
||||
0x[0-9a-fA-F](?:\.[0-9a-fA-F]*|)[pP]
|
||||
# Punycode
|
||||
\bxn--[-0-9a-z]+
|
||||
# sha
|
||||
sha\d+:[0-9]*[a-f]{3,}[0-9a-f]*
|
||||
# sha-... -- uses a fancy capture
|
||||
(['"]|")[0-9a-f]{40,}\g{-1}
|
||||
# hex runs
|
||||
\b[0-9a-fA-F]{16,}\b
|
||||
# hex in url queries
|
||||
=[0-9a-fA-F]*?(?:[A-F]{3,}|[a-f]{3,})[0-9a-fA-F]*?&
|
||||
# ssh
|
||||
(?:ssh-\S+|-nistp256) [-a-zA-Z=;:/0-9+]{12,}
|
||||
|
||||
# PGP
|
||||
\b(?:[0-9A-F]{4} ){9}[0-9A-F]{4}\b
|
||||
# GPG keys
|
||||
\b(?:[0-9A-F]{4} ){5}(?: [0-9A-F]{4}){5}\b
|
||||
# Well known gpg keys
|
||||
.well-known/openpgpkey/[\w./]+
|
||||
|
||||
# uuid:
|
||||
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
|
||||
# hex digits including css/html color classes:
|
||||
(?:[\\0][xX]|\\u|[uU]\+|#x?|\%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|u\d+)\b
|
||||
# integrity
|
||||
integrity="sha\d+-[-a-zA-Z=;:/0-9+]{40,}"
|
||||
|
||||
# https://www.gnu.org/software/groff/manual/groff.html
|
||||
# man troff content
|
||||
\\f[BCIPR]
|
||||
# '
|
||||
\\\(aq
|
||||
|
||||
# .desktop mime types
|
||||
^MimeTypes?=.*$
|
||||
# .desktop localized entries
|
||||
^[A-Z][a-z]+\[[a-z]+\]=.*$
|
||||
# Localized .desktop content
|
||||
Name\[[^\]]+\]=.*
|
||||
|
||||
# IServiceProvider
|
||||
\bI(?=(?:[A-Z][a-z]{2,})+\b)
|
||||
|
||||
# crypt
|
||||
"\$2[ayb]\$.{56}"
|
||||
|
||||
# scrypt / argon
|
||||
\$(?:scrypt|argon\d+[di]*)\$\S+
|
||||
|
||||
# Input to GitHub JSON
|
||||
content: "[-a-zA-Z=;:/0-9+]*="
|
||||
|
||||
# Python stringprefix / binaryprefix
|
||||
# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings
|
||||
(?<!')\b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)'(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})
|
||||
|
||||
# Regular expressions for (P|p)assword
|
||||
\([A-Z]\|[a-z]\)[a-z]+
|
||||
|
||||
# JavaScript regular expressions
|
||||
# javascript test regex
|
||||
/.*/[gim]*\.test\(
|
||||
# javascript match regex
|
||||
\.match\(/[^/\s"]*/[gim]*\s*
|
||||
# javascript match regex
|
||||
\.match\(/\\[b].*?/[gim]*\s*\)(?:;|$)
|
||||
# javascript regex
|
||||
^\s*/\\[b].*/[gim]*\s*(?:\)(?:;|$)|,$)
|
||||
# javascript replace regex
|
||||
\.replace\(/[^/\s"]*/[gim]*\s*,
|
||||
|
||||
# Go regular expressions
|
||||
regexp?\.MustCompile\(`[^`]*`\)
|
||||
|
||||
# sed regular expressions
|
||||
sed 's/(?:[^/]*?[a-zA-Z]{3,}[^/]*?/){2}
|
||||
|
||||
# go install
|
||||
go install(?:\s+[a-z]+\.[-@\w/.]+)+
|
||||
|
||||
# kubernetes pod status lists
|
||||
# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
|
||||
\w+(?:-\w+)+\s+\d+/\d+\s+(?:Running|Pending|Succeeded|Failed|Unknown)\s+
|
||||
|
||||
# kubectl - pods in CrashLoopBackOff
|
||||
\w+-[0-9a-f]+-\w+\s+\d+/\d+\s+CrashLoopBackOff\s+
|
||||
|
||||
# kubernetes object suffix
|
||||
-[0-9a-f]{10}-\w{5}\s
|
||||
|
||||
# posthog secrets
|
||||
posthog\.init\((['"])phc_[^"',]+\g{-1},
|
||||
|
||||
# xcode
|
||||
|
||||
# xcodeproject scenes
|
||||
(?:Controller|ID|id)="\w{3}-\w{2}-\w{3}"
|
||||
|
||||
# xcode api botches
|
||||
customObjectInstantitationMethod
|
||||
|
||||
# font awesome classes
|
||||
\.fa-[-a-z0-9]+
|
||||
|
||||
# Update Lorem based on your content (requires `ge` and `w` from https://github.com/jsoref/spelling; and `review` from https://github.com/check-spelling/check-spelling/wiki/Looking-for-items-locally )
|
||||
# grep '^[^#].*lorem' .github/actions/spelling/patterns.txt|perl -pne 's/.*i..\?://;s/\).*//' |tr '|' "\n"|sort -f |xargs -n1 ge|perl -pne 's/^[^:]*://'|sort -u|w|sed -e 's/ .*//'|w|review -
|
||||
# Warning, while `(?i)` is very neat and fancy, if you have some binary files that aren't proper unicode, you might run into:
|
||||
## Operation "substitution (s///)" returns its argument for non-Unicode code point 0x1C19AE (the code point will vary).
|
||||
## You could manually change `(?i)X...` to use `[Xx]...`
|
||||
## or you could add the files to your `excludes` file (a version after 0.0.19 should identify the file path)
|
||||
# Lorem
|
||||
(?:\w|\s|[,.])*\b(?i)(?:amet|consectetur|cursus|dolor|eros|ipsum|lacus|libero|ligula|lorem|magna|neque|nulla|suscipit|tempus)\b(?:\w|\s|[,.])*
|
||||
|
||||
# Non-English
|
||||
[a-zA-Z]*[ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*
|
||||
|
||||
# French
|
||||
# This corpus only had capital letters, but you probably want lowercase ones as well.
|
||||
\b[LN]'+[a-z]{2,}\b
|
||||
|
||||
# latex
|
||||
\\(?:n(?:ew|ormal|osub)|r(?:enew)|t(?:able(?:of|)|he|itle))(?=[a-z]+)
|
||||
|
||||
# the negative lookahead here is to allow catching 'templatesz' as a misspelling
|
||||
# but to otherwise recognize a Windows path with \templates\foo.template or similar:
|
||||
\\(?:necessary|r(?:eport|esolve[dr]?|esult)|t(?:arget|emplates?))(?![a-z])
|
||||
# ignore long runs of a single character:
|
||||
\b([A-Za-z])\g{-1}{3,}\b
|
||||
# Note that the next example is no longer necessary if you are using
|
||||
# to match a string starting with a `#`, use a character-class:
|
||||
[#]backwards
|
||||
# version suffix <word>v#
|
||||
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
|
||||
# Compiler flags (Scala)
|
||||
(?:^|[\t ,>"'`=(])-J-[DPWXY](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
|
||||
# Compiler flags
|
||||
(?:^|[\t ,"'`=(])-[DPWXYLlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
|
||||
# Compiler flags (linker)
|
||||
,-B
|
||||
# curl arguments
|
||||
\b(?:\\n|)curl(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)*
|
||||
# set arguments
|
||||
\bset(?:\s+-[abefimouxE]{1,2})*\s+-[abefimouxE]{3,}(?:\s+-[abefimouxE]+)*
|
||||
# tar arguments
|
||||
\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
|
||||
# tput arguments -- https://man7.org/linux/man-pages/man5/terminfo.5.html -- technically they can be more than 5 chars long...
|
||||
\btput\s+(?:(?:-[SV]|-T\s*\w+)\s+)*\w{3,5}\b
|
||||
# macOS temp folders
|
||||
/var/folders/\w\w/[+\w]+/(?:T|-Caches-)/
|
||||
73
.github/actions/spelling/excludes.txt
vendored
Normal file
73
.github/actions/spelling/excludes.txt
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-excludes
|
||||
(?:^|/)(?i)COPYRIGHT
|
||||
(?:^|/)(?i)LICEN[CS]E
|
||||
(?:^|/)3rdparty/
|
||||
(?:^|/)go\.sum$
|
||||
(?:^|/)package(?:-lock|)\.json$
|
||||
(?:^|/)vendor/
|
||||
\.a$
|
||||
\.ai$
|
||||
\.avi$
|
||||
\.bmp$
|
||||
\.bz2$
|
||||
\.class$
|
||||
\.crt$
|
||||
\.dll$
|
||||
\.docx?$
|
||||
\.drawio$
|
||||
\.DS_Store$
|
||||
\.eot$
|
||||
\.exe$
|
||||
\.gif$
|
||||
\.gitattributes$
|
||||
\.gitignore$
|
||||
\.graffle$
|
||||
\.gz$
|
||||
\.icns$
|
||||
\.ico$
|
||||
\.jar$
|
||||
\.jks$
|
||||
\.jpe?g$
|
||||
\.key$
|
||||
\.lib$
|
||||
\.lock$
|
||||
\.map$
|
||||
\.min\..
|
||||
\.mod$
|
||||
\.mp[34]$
|
||||
\.o$
|
||||
\.ocf$
|
||||
\.otf$
|
||||
\.pdf$
|
||||
\.pem$
|
||||
\.png$
|
||||
\.psd$
|
||||
\.pyc$
|
||||
\.s$
|
||||
\.svgz?$
|
||||
\.tar$
|
||||
\.tiff?$
|
||||
\.ttf$
|
||||
\.wav$
|
||||
\.webm$
|
||||
\.webp$
|
||||
\.woff2?$
|
||||
\.xlsx?$
|
||||
\.zip$
|
||||
^\.github/actions/spelling/
|
||||
^\Q.github/workflows/spelling.yml\E$
|
||||
# Custom
|
||||
(?:^|/)Languages/(?!en\.xaml)
|
||||
Scripts/
|
||||
\.resx$
|
||||
^\QPlugins/Flow.Launcher.Plugin.WindowsSettings/WindowsSettings.json\E$
|
||||
^\QPlugins/Flow.Launcher.Plugin.WebSearch/setting.json\E$
|
||||
(?:^|/)FodyWeavers\.xml
|
||||
.editorconfig
|
||||
ignore$
|
||||
\.ps1$
|
||||
\.yml$
|
||||
\.sln$
|
||||
\.csproj$
|
||||
\.DotSettings$
|
||||
\.targets$
|
||||
112
.github/actions/spelling/expect.txt
vendored
Normal file
112
.github/actions/spelling/expect.txt
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
crowdin
|
||||
DWM
|
||||
workflows
|
||||
wpf
|
||||
actionkeyword
|
||||
stackoverflow
|
||||
Wox
|
||||
flowlauncher
|
||||
Fody
|
||||
stackoverflow
|
||||
IContext
|
||||
IShell
|
||||
IPlugin
|
||||
appveyor
|
||||
netflix
|
||||
youtube
|
||||
appdata
|
||||
Prioritise
|
||||
Segoe
|
||||
Google
|
||||
Customise
|
||||
uwp
|
||||
Bokmal
|
||||
Bokm
|
||||
uninstallation
|
||||
uninstalling
|
||||
voidtools
|
||||
fullscreen
|
||||
hotkeys
|
||||
totalcmd
|
||||
lnk
|
||||
amazonaws
|
||||
mscorlib
|
||||
pythonw
|
||||
dotnet
|
||||
winget
|
||||
jjw24
|
||||
wolframalpha
|
||||
gmail
|
||||
duckduckgo
|
||||
facebook
|
||||
findicon
|
||||
baidu
|
||||
pls
|
||||
websearch
|
||||
qianlifeng
|
||||
userdata
|
||||
srchadmin
|
||||
EWX
|
||||
dlgtext
|
||||
CMD
|
||||
appref-ms
|
||||
appref
|
||||
TSource
|
||||
runas
|
||||
dpi
|
||||
popup
|
||||
ptr
|
||||
pluginindicator
|
||||
TobiasSekan
|
||||
img
|
||||
resx
|
||||
bak
|
||||
tmp
|
||||
directx
|
||||
mvvm
|
||||
dlg
|
||||
ddd
|
||||
dddd
|
||||
clearlogfolder
|
||||
ACCENT_ENABLE_TRANSPARENTGRADIENT
|
||||
ACCENT_ENABLE_BLURBEHIND
|
||||
WCA_ACCENT_POLICY
|
||||
HGlobal
|
||||
dopusrt
|
||||
firefox
|
||||
Firefox
|
||||
msedge
|
||||
svgc
|
||||
ime
|
||||
zindex
|
||||
txb
|
||||
btn
|
||||
otf
|
||||
searchplugin
|
||||
wpftk
|
||||
mkv
|
||||
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
|
||||
62
.github/actions/spelling/line_forbidden.patterns
vendored
Normal file
62
.github/actions/spelling/line_forbidden.patterns
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# reject `m_data` as there's a certain OS which has evil defines that break things if it's used elsewhere
|
||||
# \bm_data\b
|
||||
|
||||
# If you have a framework that uses `it()` for testing and `fit()` for debugging a specific test,
|
||||
# you might not want to check in code where you were debugging w/ `fit()`, in which case, you might want
|
||||
# to use this:
|
||||
#\bfit\(
|
||||
|
||||
# s.b. GitHub
|
||||
#\bGithub\b
|
||||
|
||||
# s.b. GitLab
|
||||
\bGitlab\b
|
||||
|
||||
# s.b. JavaScript
|
||||
\bJavascript\b
|
||||
|
||||
# s.b. Microsoft
|
||||
\bMicroSoft\b
|
||||
|
||||
# s.b. another
|
||||
\ban[- ]other\b
|
||||
|
||||
# s.b. greater than
|
||||
\bgreater then\b
|
||||
|
||||
# s.b. into
|
||||
\sin to\s
|
||||
|
||||
# s.b. opt-in
|
||||
\sopt in\s
|
||||
|
||||
# s.b. less than
|
||||
\bless then\b
|
||||
|
||||
# s.b. otherwise
|
||||
\bother[- ]wise\b
|
||||
|
||||
# s.b. nonexistent
|
||||
\bnon existing\b
|
||||
\b[Nn]o[nt][- ]existent\b
|
||||
|
||||
# s.b. preexisting
|
||||
[Pp]re[- ]existing
|
||||
|
||||
# s.b. preempt
|
||||
[Pp]re[- ]empt\b
|
||||
|
||||
# s.b. preemptively
|
||||
[Pp]re[- ]emptively
|
||||
|
||||
# s.b. reentrancy
|
||||
[Rr]e[- ]entrancy
|
||||
|
||||
# s.b. reentrant
|
||||
[Rr]e[- ]entrant
|
||||
|
||||
# s.b. workaround(s)
|
||||
\bwork[- ]arounds?\b
|
||||
|
||||
# Reject duplicate words
|
||||
\s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\s\g{-1}\s
|
||||
123
.github/actions/spelling/patterns.txt
vendored
Normal file
123
.github/actions/spelling/patterns.txt
vendored
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
|
||||
|
||||
# Questionably acceptable forms of `in to`
|
||||
# Personally, I prefer `log into`, but people object
|
||||
# https://www.tprteaching.com/log-into-log-in-to-login/
|
||||
\b[Ll]og in to\b
|
||||
|
||||
# acceptable duplicates
|
||||
# ls directory listings
|
||||
[-bcdlpsw](?:[-r][-w][-sx]){3}\s+\d+\s+(\S+)\s+\g{-1}\s+\d+\s+
|
||||
# C types and repeated CSS values
|
||||
\s(center|div|inherit|long|LONG|none|normal|solid|thin|transparent|very)(?: \g{-1})+\s
|
||||
# go templates
|
||||
\s(\w+)\s+\g{-1}\s+\`(?:graphql|json|yaml):
|
||||
# javadoc / .net
|
||||
(?:[\\@](?:groupname|param)|(?:public|private)(?:\s+static|\s+readonly)*)\s+(\w+)\s+\g{-1}\s
|
||||
|
||||
# Commit message -- Signed-off-by and friends
|
||||
^\s*(?:(?:Based-on-patch|Co-authored|Helped|Mentored|Reported|Reviewed|Signed-off)-by|Thanks-to): (?:[^<]*<[^>]*>|[^<]*)\s*$
|
||||
|
||||
# Autogenerated revert commit message
|
||||
^This reverts commit [0-9a-f]{40}\.$
|
||||
|
||||
# ignore long runs of a single character:
|
||||
\b([A-Za-z])\g{-1}{3,}\b
|
||||
|
||||
# Automatically suggested patterns
|
||||
# hit-count: 360 file-count: 108
|
||||
# IServiceProvider
|
||||
\bI(?=(?:[A-Z][a-z]{2,})+\b)
|
||||
|
||||
# hit-count: 297 file-count: 18
|
||||
# uuid:
|
||||
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
|
||||
|
||||
# hit-count: 138 file-count: 27
|
||||
# hex digits including css/html color classes:
|
||||
(?:[\\0][xX]|\\u|[uU]\+|#x?|\%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|u\d+)\b
|
||||
|
||||
# hit-count: 93 file-count: 28
|
||||
# hex runs
|
||||
\b[0-9a-fA-F]{16,}\b
|
||||
|
||||
# hit-count: 52 file-count: 3
|
||||
# githubusercontent
|
||||
/[-a-z0-9]+\.githubusercontent\.com/[-a-zA-Z0-9?&=_\/.]*
|
||||
|
||||
/[-a-z0-9]+\.github\.com/[-a-zA-Z0-9?&=_\/.]*
|
||||
|
||||
# hit-count: 24 file-count: 12
|
||||
# GitHub SHAs (markdown)
|
||||
(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|)
|
||||
|
||||
# hit-count: 11 file-count: 10
|
||||
# stackexchange -- https://stackexchange.com/feeds/sites
|
||||
\b(?:askubuntu|serverfault|stack(?:exchange|overflow)|superuser).com/(?:questions/\w+/[-\w]+|a/)
|
||||
|
||||
# hit-count: 11 file-count: 8
|
||||
# microsoft
|
||||
\b(?:https?://|)(?:(?:download\.visualstudio|docs|msdn2?|research)\.microsoft|blogs\.msdn)\.com/[-_a-zA-Z0-9()=./%]*
|
||||
|
||||
# hit-count: 2 file-count: 2
|
||||
# Twitter status
|
||||
\btwitter\.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)
|
||||
|
||||
# hit-count: 2 file-count: 1
|
||||
# discord
|
||||
/discord(?:app\.com|\.gg)/(?:invite/)?[a-zA-Z0-9]{7,}
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# appveyor api
|
||||
\bci\.appveyor\.com/api/projects/status/[0-9a-z]+
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# gist github
|
||||
\bgist\.github\.com/[^/\s"]+/[0-9a-f]+
|
||||
|
||||
# Automatically suggested patterns
|
||||
# hit-count: 21 file-count: 21
|
||||
# w3
|
||||
\bw3\.org/[-0-9a-zA-Z/#.]+
|
||||
|
||||
# hit-count: 6 file-count: 1
|
||||
# shields.io
|
||||
\bshields\.io/[-\w/%?=&.:+;,]*
|
||||
|
||||
# hit-count: 3 file-count: 2
|
||||
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
|
||||
# YouTube url
|
||||
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
|
||||
|
||||
# hit-count: 2 file-count: 2
|
||||
# Google Maps
|
||||
\bmaps\.google\.com/maps\?[\w&;=]*
|
||||
|
||||
# hit-count: 2 file-count: 2
|
||||
# Contributor
|
||||
\[[^\]]+\]\(https://github\.com/[^/\s"]+\)
|
||||
|
||||
# hit-count: 2 file-count: 1
|
||||
# URL escaped characters
|
||||
\%[0-9A-F][A-F]
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# Compiler flags
|
||||
(?:^|[\t ,"'`=(])-[DPWXYLlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
|
||||
|
||||
# Localization keys
|
||||
#x:Key="[^"]+"
|
||||
#{DynamicResource [^"]+}
|
||||
|
||||
# html tag
|
||||
<\w+[^>]*>
|
||||
</\w+[^>]*>
|
||||
|
||||
#http/https
|
||||
(?:\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_]))
|
||||
10
.github/actions/spelling/reject.txt
vendored
Normal file
10
.github/actions/spelling/reject.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
^attache$
|
||||
benefitting
|
||||
occurences?
|
||||
^dependan.*
|
||||
^oer$
|
||||
Sorce
|
||||
^[Ss]pae.*
|
||||
^untill$
|
||||
^untilling$
|
||||
^wether.*
|
||||
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
|
|
@ -8,10 +8,15 @@ updates:
|
|||
- package-ecosystem: "nuget" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 3
|
||||
ignore:
|
||||
- dependency-name: "squirrel-windows"
|
||||
reviewers:
|
||||
- "jjw24"
|
||||
- "taooceros"
|
||||
- "JohnTheGr8"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
|
|
|||
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
|
||||
160
.github/workflows/spelling.yml
vendored
Normal file
160
.github/workflows/spelling.yml
vendored
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
name: Check Spelling
|
||||
|
||||
# Comment management is handled through a secondary job, for details see:
|
||||
# https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions
|
||||
#
|
||||
# `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment
|
||||
# (in odd cases, it might actually run just to collapse a comment, but that's fairly rare)
|
||||
# it needs `contents: write` in order to add a comment.
|
||||
#
|
||||
# `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment
|
||||
# or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment)
|
||||
# it needs `pull-requests: write` in order to manipulate those comments.
|
||||
|
||||
# Updating pull request branches is managed via comment handling.
|
||||
# For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list
|
||||
#
|
||||
# These elements work together to make it happen:
|
||||
#
|
||||
# `on.issue_comment`
|
||||
# This event listens to comments by users asking to update the metadata.
|
||||
#
|
||||
# `jobs.update`
|
||||
# This job runs in response to an issue_comment and will push a new commit
|
||||
# to update the spelling metadata.
|
||||
#
|
||||
# `with.experimental_apply_changes_via_bot`
|
||||
# Tells the action to support and generate messages that enable it
|
||||
# to make a commit to update the spelling metadata.
|
||||
#
|
||||
# `with.ssh_key`
|
||||
# In order to trigger workflows when the commit is made, you can provide a
|
||||
# secret (typically, a write-enabled github deploy key).
|
||||
#
|
||||
# For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - '**'
|
||||
# - '!l10n_dev'
|
||||
# tags-ignore:
|
||||
# - "**"
|
||||
pull_request_target:
|
||||
branches:
|
||||
- '**'
|
||||
# - '!l10n_dev'
|
||||
tags-ignore:
|
||||
- "**"
|
||||
types:
|
||||
- 'opened'
|
||||
- 'reopened'
|
||||
- 'synchronize'
|
||||
# issue_comment:
|
||||
# types:
|
||||
# - 'created'
|
||||
|
||||
jobs:
|
||||
spelling:
|
||||
name: Check Spelling
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
security-events: write
|
||||
outputs:
|
||||
followup: ${{ steps.spelling.outputs.followup }}
|
||||
runs-on: ubuntu-latest
|
||||
if: (contains(github.event_name, 'pull_request') && github.head_ref != 'l10n_dev')
|
||||
concurrency:
|
||||
group: spelling-${{ github.event.pull_request.number || github.ref }}
|
||||
# note: If you use only_check_changed_files, you do not want cancel-in-progress
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: check-spelling
|
||||
id: spelling
|
||||
uses: check-spelling/check-spelling@prerelease
|
||||
with:
|
||||
suppress_push_for_open_pull_request: 1
|
||||
checkout: true
|
||||
check_file_names: 1
|
||||
spell_check_this: check-spelling/spell-check-this@main
|
||||
post_comment: 0
|
||||
use_magic_file: 1
|
||||
experimental_apply_changes_via_bot: 1
|
||||
use_sarif: 0 # to show in pr page
|
||||
extra_dictionary_limit: 10
|
||||
check_commit_messages: commits title description
|
||||
only_check_changed_files: 1
|
||||
check_extra_dictionaries: ''
|
||||
quit_without_error: true
|
||||
extra_dictionaries:
|
||||
cspell:software-terms/dict/softwareTerms.txt
|
||||
cspell:win32/src/win32.txt
|
||||
cspell:filetypes/filetypes.txt
|
||||
cspell:csharp/csharp.txt
|
||||
cspell:dotnet/dict/dotnet.txt
|
||||
cspell:python/src/common/extra.txt
|
||||
cspell:python/src/python/python-lib.txt
|
||||
cspell:aws/aws.txt
|
||||
cspell:companies/src/companies.txt
|
||||
warnings:
|
||||
binary-file,deprecated-feature,large-file,limited-references,noisy-file,non-alpha-in-dictionary,unexpected-line-ending,whitespace-in-dictionary,minified-file,unsupported-configuration,unrecognized-spelling,no-newline-at-eof
|
||||
|
||||
|
||||
|
||||
# comment-push:
|
||||
# name: Report (Push)
|
||||
# # If your workflow isn't running on push, you can remove this job
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: spelling
|
||||
# permissions:
|
||||
# contents: write
|
||||
# if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
|
||||
# steps:
|
||||
# - name: comment
|
||||
# uses: check-spelling/check-spelling@@v0.0.22
|
||||
# with:
|
||||
# checkout: true
|
||||
# spell_check_this: check-spelling/spell-check-this@main
|
||||
# task: ${{ needs.spelling.outputs.followup }}
|
||||
|
||||
comment-pr:
|
||||
name: Report (PR)
|
||||
# If you workflow isn't running on pull_request*, you can remove this job
|
||||
runs-on: ubuntu-latest
|
||||
needs: spelling
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
|
||||
steps:
|
||||
- name: comment
|
||||
uses: check-spelling/check-spelling@prerelease
|
||||
with:
|
||||
checkout: true
|
||||
spell_check_this: check-spelling/spell-check-this@main
|
||||
task: ${{ needs.spelling.outputs.followup }}
|
||||
experimental_apply_changes_via_bot: 1
|
||||
|
||||
# update:
|
||||
# name: Update PR
|
||||
# permissions:
|
||||
# contents: write
|
||||
# pull-requests: write
|
||||
# actions: read
|
||||
# runs-on: ubuntu-latest
|
||||
# if: ${{
|
||||
# github.event_name == 'issue_comment' &&
|
||||
# github.event.issue.pull_request &&
|
||||
# contains(github.event.comment.body, '@check-spelling-bot apply')
|
||||
# }}
|
||||
# concurrency:
|
||||
# group: spelling-update-${{ github.event.issue.number }}
|
||||
# cancel-in-progress: false
|
||||
# steps:
|
||||
# - name: apply spelling updates
|
||||
# uses: check-spelling/check-spelling@v0.0.22
|
||||
# with:
|
||||
# experimental_apply_changes_via_bot: 1
|
||||
# checkout: true
|
||||
# ssh_key: "${{ secrets.CHECK_SPELLING }}"
|
||||
15
.github/workflows/stale.yml
vendored
15
.github/workflows/stale.yml
vendored
|
|
@ -6,6 +6,11 @@ on:
|
|||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
env:
|
||||
days-before-stale: 60
|
||||
days-before-close: 7
|
||||
exempt-issue-labels: 'keep-fresh'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -13,14 +18,14 @@ jobs:
|
|||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
days-before-stale: 45
|
||||
days-before-close: 7
|
||||
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: ${{ env.days-before-stale }}
|
||||
days-before-close: ${{ env.days-before-close }}
|
||||
days-before-pr-close: -1
|
||||
exempt-all-milestones: true
|
||||
close-issue-message: 'This issue was closed because it has been stale for 7 days with no activity. If you feel this issue still needs attention please feel free to reopen.'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
exempt-issue-labels: 'keep-fresh'
|
||||
exempt-issue-labels: ${{ env.exempt-issue-labels }}
|
||||
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
|
||||
13
.github/workflows/winget.yml
vendored
Normal file
13
.github/workflows/winget.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
name: Publish to Winget
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: Flow-Launcher.Flow-Launcher
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
|
@ -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.Plugin.SharedCommands;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Configuration
|
||||
{
|
||||
public class Portable : IPortable
|
||||
{
|
||||
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
||||
/// </summary>
|
||||
|
|
@ -40,14 +44,14 @@ namespace Flow.Launcher.Core.Configuration
|
|||
#endif
|
||||
IndicateDeletion(DataLocation.PortableDataPath);
|
||||
|
||||
MessageBox.Show("Flow Launcher needs to restart to finish disabling portable mode, " +
|
||||
API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " +
|
||||
"after the restart your portable data profile will be deleted and roaming data profile kept");
|
||||
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception("|Portable.DisablePortableMode|Error occured while disabling portable mode", e);
|
||||
Log.Exception("|Portable.DisablePortableMode|Error occurred while disabling portable mode", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,14 +68,14 @@ namespace Flow.Launcher.Core.Configuration
|
|||
#endif
|
||||
IndicateDeletion(DataLocation.RoamingDataPath);
|
||||
|
||||
MessageBox.Show("Flow Launcher needs to restart to finish enabling portable mode, " +
|
||||
API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " +
|
||||
"after the restart your roaming data profile will be deleted and portable data profile kept");
|
||||
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception("|Portable.EnablePortableMode|Error occured while enabling portable mode", e);
|
||||
Log.Exception("|Portable.EnablePortableMode|Error occurred while enabling portable mode", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,13 +99,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
||||
{
|
||||
FilesFolders.CopyAll(fromLocation, toLocation);
|
||||
FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||
VerifyUserDataAfterMove(fromLocation, toLocation);
|
||||
}
|
||||
|
||||
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
|
||||
{
|
||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation);
|
||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||
}
|
||||
|
||||
public void CreateShortcuts()
|
||||
|
|
@ -157,13 +161,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
// delete it and prompt the user to pick the portable data location
|
||||
if (File.Exists(roamingDataDeleteFilePath))
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(roamingDataDir);
|
||||
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
|
||||
|
||||
if (MessageBox.Show("Flow Launcher has detected you enabled portable mode, " +
|
||||
if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " +
|
||||
"would you like to move it to a different location?", string.Empty,
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
FilesFolders.OpenPath(Constant.RootDirectory);
|
||||
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
|
@ -172,9 +176,9 @@ namespace Flow.Launcher.Core.Configuration
|
|||
// delete it and notify the user about it.
|
||||
else if (File.Exists(portableDataDeleteFilePath))
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(portableDataDir);
|
||||
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
|
||||
|
||||
MessageBox.Show("Flow Launcher has detected you disabled portable mode, " +
|
||||
API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " +
|
||||
"the relevant shortcuts and uninstaller entry have been created");
|
||||
}
|
||||
}
|
||||
|
|
@ -186,8 +190,8 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
if (roamingLocationExists && portableLocationExists)
|
||||
{
|
||||
MessageBox.Show(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 occured.",
|
||||
API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " +
|
||||
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
|
||||
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
||||
|
||||
return false;
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
public abstract class AbstractPluginEnvironment
|
||||
{
|
||||
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
internal abstract string Language { get; }
|
||||
|
||||
internal abstract string EnvName { get; }
|
||||
|
||||
internal abstract string EnvPath { get; }
|
||||
|
||||
internal abstract string InstallPath { get; }
|
||||
|
||||
internal abstract string ExecutablePath { get; }
|
||||
|
||||
internal virtual string FileDialogFilter => string.Empty;
|
||||
|
||||
internal abstract string PluginsSettingsFilePath { get; set; }
|
||||
|
||||
internal List<PluginMetadata> PluginMetadataList;
|
||||
|
||||
internal PluginsSettings PluginSettings;
|
||||
|
||||
internal AbstractPluginEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings)
|
||||
{
|
||||
PluginMetadataList = pluginMetadataList;
|
||||
PluginSettings = pluginSettings;
|
||||
}
|
||||
|
||||
internal IEnumerable<PluginPair> Setup()
|
||||
{
|
||||
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
|
||||
return new List<PluginPair>();
|
||||
|
||||
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
|
||||
{
|
||||
// Ensure latest only if user is using Flow's environment setup.
|
||||
if (PluginsSettingsFilePath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase))
|
||||
EnsureLatestInstalled(ExecutablePath, PluginsSettingsFilePath, EnvPath);
|
||||
|
||||
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
||||
}
|
||||
|
||||
var noRuntimeMessage = string.Format(
|
||||
InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
{
|
||||
var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
|
||||
string selectedFile;
|
||||
|
||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedFile))
|
||||
PluginsSettingsFilePath = selectedFile;
|
||||
|
||||
// Nothing selected because user pressed cancel from the file dialog window
|
||||
if (string.IsNullOrEmpty(selectedFile))
|
||||
InstallEnvironment();
|
||||
}
|
||||
else
|
||||
{
|
||||
InstallEnvironment();
|
||||
}
|
||||
|
||||
if (FilesFolders.FileExists(PluginsSettingsFilePath))
|
||||
{
|
||||
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
||||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
|
||||
Log.Error("PluginsLoader",
|
||||
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
||||
$"{Language}Environment");
|
||||
|
||||
return new List<PluginPair>();
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract void InstallEnvironment();
|
||||
|
||||
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
|
||||
{
|
||||
if (expectedPath == currentPath)
|
||||
return;
|
||||
|
||||
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
|
||||
|
||||
InstallEnvironment();
|
||||
|
||||
}
|
||||
|
||||
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
|
||||
|
||||
private IEnumerable<PluginPair> SetPathForPluginPairs(string filePath, string languageToSet)
|
||||
{
|
||||
var pluginPairs = new List<PluginPair>();
|
||||
|
||||
foreach (var metadata in PluginMetadataList)
|
||||
{
|
||||
if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase))
|
||||
pluginPairs.Add(CreatePluginPair(filePath, metadata));
|
||||
}
|
||||
|
||||
return pluginPairs;
|
||||
}
|
||||
|
||||
private string GetFileFromDialog(string title, string filter = "")
|
||||
{
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
|
||||
Multiselect = false,
|
||||
CheckFileExists = true,
|
||||
CheckPathExists = true,
|
||||
Title = title,
|
||||
Filter = filter
|
||||
};
|
||||
|
||||
var result = dlg.ShowDialog();
|
||||
return result == DialogResult.OK ? dlg.FileName : string.Empty;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After app updated while in portable mode or switched between portable/roaming mode,
|
||||
/// need to update each plugin's executable path so user will not be prompted again to reinstall the environments.
|
||||
/// </summary>
|
||||
/// <param name="settings"></param>
|
||||
public static void PreStartPluginExecutablePathUpdate(Settings settings)
|
||||
{
|
||||
if (DataLocation.PortableDataLocationInUse())
|
||||
{
|
||||
// When user is using portable but has moved flow to a different location
|
||||
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName)
|
||||
&& !settings.PluginSettings.PythonExecutablePath.StartsWith(DataLocation.PortableDataPath))
|
||||
{
|
||||
settings.PluginSettings.PythonExecutablePath
|
||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
|
||||
}
|
||||
|
||||
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName)
|
||||
&& !settings.PluginSettings.NodeExecutablePath.StartsWith(DataLocation.PortableDataPath))
|
||||
{
|
||||
settings.PluginSettings.NodeExecutablePath
|
||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
|
||||
}
|
||||
|
||||
// When user has switched from roaming to portable
|
||||
if (IsUsingRoamingPath(settings.PluginSettings.PythonExecutablePath))
|
||||
{
|
||||
settings.PluginSettings.PythonExecutablePath
|
||||
= settings.PluginSettings.PythonExecutablePath.Replace(DataLocation.RoamingDataPath, DataLocation.PortableDataPath);
|
||||
}
|
||||
|
||||
if (IsUsingRoamingPath(settings.PluginSettings.NodeExecutablePath))
|
||||
{
|
||||
settings.PluginSettings.NodeExecutablePath
|
||||
= settings.PluginSettings.NodeExecutablePath.Replace(DataLocation.RoamingDataPath, DataLocation.PortableDataPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
|
||||
settings.PluginSettings.PythonExecutablePath
|
||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
|
||||
|
||||
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
|
||||
settings.PluginSettings.NodeExecutablePath
|
||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return false;
|
||||
|
||||
// DataLocation.PortableDataPath returns the current portable path, this determines if an out
|
||||
// of date path is also a portable path.
|
||||
var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}";
|
||||
|
||||
return filePath.Contains(portableAppEnvLocation);
|
||||
}
|
||||
|
||||
private static bool IsUsingRoamingPath(string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return false;
|
||||
|
||||
return filePath.StartsWith(DataLocation.RoamingDataPath);
|
||||
}
|
||||
|
||||
private static string GetUpdatedEnvironmentPath(string filePath)
|
||||
{
|
||||
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
|
||||
|
||||
// get the substring after "Environments" because we can not determine it dynamically
|
||||
var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count());
|
||||
return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 JavaScriptEnvironment : TypeScriptEnvironment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.JavaScript;
|
||||
|
||||
internal JavaScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using System.Collections.Generic;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
|
||||
internal class JavaScriptV2Environment : TypeScriptV2Environment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.JavaScriptV2;
|
||||
|
||||
internal JavaScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using Droplex;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
internal class PythonEnvironment : AbstractPluginEnvironment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.Python;
|
||||
|
||||
internal override string EnvName => DataLocation.PythonEnvironmentName;
|
||||
|
||||
internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName);
|
||||
|
||||
internal override string InstallPath => Path.Combine(EnvPath, "PythonEmbeddable-v3.11.4");
|
||||
|
||||
internal override string ExecutablePath => Path.Combine(InstallPath, "pythonw.exe");
|
||||
|
||||
internal override string FileDialogFilter => "Python|pythonw.exe";
|
||||
|
||||
internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; }
|
||||
|
||||
internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
|
||||
internal override void InstallEnvironment()
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||
|
||||
// Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and
|
||||
// uses Python plugin they need to custom install and use v3.8.9
|
||||
DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath).Wait();
|
||||
|
||||
PluginsSettingsFilePath = ExecutablePath;
|
||||
}
|
||||
|
||||
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||
{
|
||||
return new PluginPair
|
||||
{
|
||||
Plugin = new PythonPlugin(filePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
internal class PythonV2Environment : PythonEnvironment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.PythonV2;
|
||||
|
||||
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||
{
|
||||
return new PluginPair
|
||||
{
|
||||
Plugin = new PythonPluginV2(filePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
|
||||
internal PythonV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.IO;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
internal class TypeScriptEnvironment : AbstractPluginEnvironment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.TypeScript;
|
||||
|
||||
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 TypeScriptEnvironment(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 NodePlugin(filePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -12,38 +8,37 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
{
|
||||
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 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
|
||||
{
|
||||
await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl);
|
||||
request.Headers.Add("If-None-Match", latestEtag);
|
||||
|
||||
var response = await Http.SendAsync(request, token).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
if (UserPlugins == null || usePrimaryUrlOnly || DateTime.Now.Subtract(lastFetchedAt) >= fetchTimeout)
|
||||
{
|
||||
Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo");
|
||||
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
|
||||
|
||||
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);
|
||||
|
||||
latestEtag = response.Headers.ETag.Tag;
|
||||
}
|
||||
else if (response.StatusCode != HttpStatusCode.NotModified)
|
||||
{
|
||||
Log.Warn($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http response for manifest file was {response.StatusCode}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -54,6 +49,8 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
{
|
||||
manifestUpdateLock.Release();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
|
|
@ -13,9 +13,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
public string Website { get; set; }
|
||||
public string UrlDownload { get; set; }
|
||||
public string UrlSourceCode { get; set; }
|
||||
public string LocalInstallPath { get; set; }
|
||||
public string IcoPath { get; set; }
|
||||
public DateTime LatestReleaseDate { get; set; }
|
||||
public DateTime DateAdded { get; set; }
|
||||
public DateTime? LatestReleaseDate { get; set; }
|
||||
public DateTime? DateAdded { get; set; }
|
||||
|
||||
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<UseWpf>true</UseWpf>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<OutputType>Library</OutputType>
|
||||
|
|
@ -53,10 +53,12 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Droplex" Version="1.4.1" />
|
||||
<PackageReference Include="FSharp.Core" Version="6.0.6" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
|
||||
<PackageReference Include="Droplex" Version="1.7.0" />
|
||||
<PackageReference Include="FSharp.Core" Version="9.0.201" />
|
||||
<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="StreamJsonRpc" Version="2.21.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
internal class ExecutablePlugin : JsonRPCPlugin
|
||||
{
|
||||
private readonly ProcessStartInfo _startInfo;
|
||||
public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;
|
||||
|
||||
public ExecutablePlugin(string filename)
|
||||
{
|
||||
|
|
@ -29,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
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
|
||||
_startInfo.ArgumentList[0] = request.ToString();
|
||||
_startInfo.ArgumentList[0] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||
return ExecuteAsync(_startInfo, token);
|
||||
}
|
||||
|
||||
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
|
||||
_startInfo.ArgumentList[0] = rpcRequest.ToString();
|
||||
_startInfo.ArgumentList[0] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||
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.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
public class JsonRPCErrorModel
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public record JsonRPCBase(int Id, JsonRPCErrorModel Error = default);
|
||||
public record JsonRPCErrorModel(int Code, string Message, string Data);
|
||||
|
||||
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>
|
||||
/// Json RPC Request(in query response) that client sent to Flow Launcher
|
||||
/// </summary>
|
||||
public class JsonRPCClientRequestModel : JsonRPCRequestModel
|
||||
{
|
||||
public bool DontHideAfterAction { get; set; }
|
||||
}
|
||||
|
||||
public record JsonRPCClientRequestModel(
|
||||
int Id,
|
||||
string Method,
|
||||
object[] Parameters,
|
||||
IReadOnlyDictionary<string, object> Settings,
|
||||
bool DontHideAfterAction = false,
|
||||
JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -26,6 +27,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public string Name { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string urlLabel { get; set; }
|
||||
public Uri url { get; set; }
|
||||
public bool Validation { get; set; }
|
||||
public List<string> Options { get; set; }
|
||||
public string DefaultValue { get; set; }
|
||||
|
|
@ -40,4 +43,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
DefaultValue = this.DefaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ using Control = System.Windows.Controls.Control;
|
|||
using Orientation = System.Windows.Controls.Orientation;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
using System.Windows.Documents;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -29,33 +30,25 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// Represent the plugin that using JsonPRC
|
||||
/// every JsonRPC plugin should has its own plugin instance
|
||||
/// </summary>
|
||||
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||
internal abstract class JsonRPCPlugin : JsonRPCPluginBase
|
||||
{
|
||||
protected PluginInitContext context;
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
|
||||
/// <summary>
|
||||
/// The language this JsonRPCPlugin support
|
||||
/// </summary>
|
||||
public abstract string SupportedLanguage { get; set; }
|
||||
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
|
||||
protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
|
||||
|
||||
private static readonly RecyclableMemoryStreamManager BufferManager = new();
|
||||
|
||||
private string SettingConfigurationPath => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name, "Settings.json");
|
||||
private int RequestId { get; set; }
|
||||
|
||||
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
|
||||
{
|
||||
Method = "context_menu",
|
||||
Parameters = new[]
|
||||
{
|
||||
selectedResult.ContextData
|
||||
}
|
||||
};
|
||||
var request = new JsonRPCRequestModel(RequestId++,
|
||||
"context_menu",
|
||||
new[] { selectedResult.ContextData });
|
||||
var output = Request(request);
|
||||
return DeserializedResult(output);
|
||||
}
|
||||
|
|
@ -80,7 +73,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
private Dictionary<string, object> Settings { get; set; }
|
||||
|
||||
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
||||
|
||||
|
|
@ -106,85 +98,42 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return ParseResults(queryResponseModel);
|
||||
}
|
||||
|
||||
|
||||
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
|
||||
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||
{
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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>();
|
||||
|
||||
results.AddRange(queryResponseModel.Result);
|
||||
|
||||
UpdateSettings(queryResponseModel.SettingsChange);
|
||||
|
||||
return results;
|
||||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
}
|
||||
|
||||
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>
|
||||
/// Execute external program and return the output
|
||||
|
|
@ -259,9 +208,16 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
await using var registeredEvent = token.Register(() =>
|
||||
{
|
||||
if (!process.HasExited)
|
||||
process.Kill();
|
||||
sourceBuffer.Dispose();
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
process.Kill();
|
||||
sourceBuffer.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception("|JsonRPCPlugin.ExecuteAsync|Exception when kill process", e);
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
|
|
@ -288,238 +244,23 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
sourceBuffer.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
|
||||
return sourceBuffer;
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
var request = new JsonRPCRequestModel
|
||||
{
|
||||
Method = "query",
|
||||
Parameters = new object[]
|
||||
var request = new JsonRPCRequestModel(RequestId++,
|
||||
"query",
|
||||
new object[]
|
||||
{
|
||||
query.Search
|
||||
},
|
||||
Settings = Settings
|
||||
};
|
||||
Settings?.Inner);
|
||||
|
||||
var output = await RequestAsync(request, token);
|
||||
|
||||
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(10, 4, 10, 4);
|
||||
private static readonly Thickness settingPanelMargin = new(15, 20, 15, 20);
|
||||
private static readonly Thickness settingTextBlockMargin = new(10, 4, 10, 4);
|
||||
private JsonRpcConfigurationModel _settingsTemplate;
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
{
|
||||
if (Settings == null)
|
||||
return new();
|
||||
var settingWindow = new UserControl();
|
||||
var mainPanel = new StackPanel
|
||||
{
|
||||
Margin = settingPanelMargin, Orientation = Orientation.Vertical
|
||||
};
|
||||
settingWindow.Content = mainPanel;
|
||||
|
||||
foreach (var (type, attribute) in _settingsTemplate.Body)
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal, Margin = settingControlMargin
|
||||
};
|
||||
var name = new TextBlock()
|
||||
{
|
||||
Text = attribute.Label,
|
||||
Width = 120,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingControlMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
FrameworkElement contentControl;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "textBlock":
|
||||
{
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||
Margin = settingTextBlockMargin,
|
||||
MaxWidth = 500,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "input":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Width = 300,
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
Margin = settingControlMargin,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
break;
|
||||
}
|
||||
case "textarea":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Width = 300,
|
||||
Height = 120,
|
||||
Margin = settingControlMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
break;
|
||||
}
|
||||
case "passwordBox":
|
||||
{
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
Width = 300,
|
||||
Margin = settingControlMargin,
|
||||
Password = Settings[attribute.Name] as string ?? string.Empty,
|
||||
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
contentControl = passwordBox;
|
||||
break;
|
||||
}
|
||||
case "dropdown":
|
||||
{
|
||||
var comboBox = new ComboBox()
|
||||
{
|
||||
ItemsSource = attribute.Options,
|
||||
SelectedItem = Settings[attribute.Name],
|
||||
Margin = settingControlMargin,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem;
|
||||
};
|
||||
contentControl = comboBox;
|
||||
break;
|
||||
}
|
||||
case "checkbox":
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue),
|
||||
Margin = settingControlMargin,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
checkBox.Click += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
|
||||
};
|
||||
contentControl = checkBox;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
if (type != "textBlock")
|
||||
_settingControls[attribute.Name] = contentControl;
|
||||
panel.Children.Add(name);
|
||||
panel.Children.Add(contentControl);
|
||||
mainPanel.Children.Add(panel);
|
||||
}
|
||||
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 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
|
||||
);
|
||||
}
|
||||
50
Flow.Launcher.Core/Plugin/NodePlugin.cs
Normal file
50
Flow.Launcher.Core/Plugin/NodePlugin.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Execution of JavaScript & TypeScript plugins
|
||||
/// </summary>
|
||||
internal class NodePlugin : JsonRPCPlugin
|
||||
{
|
||||
private readonly ProcessStartInfo _startInfo;
|
||||
|
||||
public NodePlugin(string filename)
|
||||
{
|
||||
_startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = filename,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
}
|
||||
|
||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||
{
|
||||
_startInfo.ArgumentList[1] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||
return ExecuteAsync(_startInfo, token);
|
||||
}
|
||||
|
||||
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
|
||||
_startInfo.ArgumentList[1] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
public override async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
_startInfo.ArgumentList.Add(string.Empty);
|
||||
_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.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
|
@ -37,6 +34,17 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
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)
|
||||
{
|
||||
|
|
@ -44,4 +52,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
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.Plugin;
|
||||
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
|
||||
{
|
||||
|
|
@ -25,11 +29,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public static readonly HashSet<PluginPair> GlobalPlugins = 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
|
||||
public static PluginsSettings Settings;
|
||||
private static PluginsSettings Settings;
|
||||
private static List<PluginMetadata> _metadatas;
|
||||
private static List<string> _modifiedPlugins = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 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()
|
||||
{
|
||||
foreach (var plugin in AllPlugins)
|
||||
|
|
@ -85,6 +94,48 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}).ToArray());
|
||||
}
|
||||
|
||||
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public static async Task CloseExternalPreviewAsync()
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.ClosePreviewAsync(),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public static bool UseExternalPreview()
|
||||
{
|
||||
return GetPluginsForInterface<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()
|
||||
{
|
||||
// validate user directory
|
||||
|
|
@ -110,9 +161,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// Call initialize for all plugins
|
||||
/// </summary>
|
||||
/// <returns>return the list of failed to init plugins or null for none</returns>
|
||||
public static async Task InitializePluginsAsync(IPublicAPI api)
|
||||
public static async Task InitializePluginsAsync()
|
||||
{
|
||||
API = api;
|
||||
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
||||
|
||||
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())
|
||||
{
|
||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||
API.ShowMsg($"Fail to Init Plugins",
|
||||
$"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help",
|
||||
"", false);
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("failedToInitializePluginsTitle"),
|
||||
string.Format(
|
||||
API.GetTranslation("failedToInitializePluginsMessage"),
|
||||
failed
|
||||
),
|
||||
"",
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,11 +227,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
if (query is null)
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
|
||||
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
||||
return GlobalPlugins;
|
||||
|
||||
|
||||
|
||||
|
||||
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
||||
return new List<PluginPair>
|
||||
{
|
||||
|
|
@ -207,12 +266,24 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -220,8 +291,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
r.PluginID = metadata.ID;
|
||||
r.OriginQuery = query;
|
||||
|
||||
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
|
||||
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
|
||||
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
|
||||
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
|
||||
if (metadata.ActionKeywords.Count == 1)
|
||||
r.ActionKeywordAssigned = query.ActionKeyword;
|
||||
}
|
||||
|
|
@ -239,7 +310,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
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)
|
||||
|
|
@ -328,5 +400,215 @@ namespace Flow.Launcher.Core.Plugin
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,52 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
||||
#pragma warning disable IDE0005
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
#pragma warning restore IDE0005
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
public static class PluginsLoader
|
||||
{
|
||||
public const string PythonExecutable = "pythonw.exe";
|
||||
|
||||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||
{
|
||||
var dotnetPlugins = DotNetPlugins(metadatas);
|
||||
var pythonPlugins = PythonPlugins(metadatas, settings);
|
||||
|
||||
var pythonEnv = new PythonEnvironment(metadatas, settings);
|
||||
var pythonV2Env = new PythonV2Environment(metadatas, settings);
|
||||
var tsEnv = new TypeScriptEnvironment(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 pythonV2Plugins = pythonV2Env.Setup();
|
||||
var tsPlugins = tsEnv.Setup();
|
||||
var jsPlugins = jsEnv.Setup();
|
||||
var tsV2Plugins = tsV2Env.Setup();
|
||||
var jsV2Plugins = jsV2Env.Setup();
|
||||
|
||||
var executablePlugins = ExecutablePlugins(metadatas);
|
||||
var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
|
||||
var executableV2Plugins = ExecutableV2Plugins(metadatas);
|
||||
|
||||
var plugins = dotnetPlugins
|
||||
.Concat(pythonPlugins)
|
||||
.Concat(pythonV2Plugins)
|
||||
.Concat(tsPlugins)
|
||||
.Concat(jsPlugins)
|
||||
.Concat(tsV2Plugins)
|
||||
.Concat(jsV2Plugins)
|
||||
.Concat(executablePlugins)
|
||||
.Concat(executableV2Plugins)
|
||||
.ToList();
|
||||
return plugins;
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +105,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return;
|
||||
}
|
||||
|
||||
plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
|
||||
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||
});
|
||||
metadata.InitTime += milliseconds;
|
||||
}
|
||||
|
|
@ -98,111 +120,34 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"Please refer to the logs for more information", "",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
});
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, PluginsSettings settings)
|
||||
{
|
||||
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
|
||||
return new List<PluginPair>();
|
||||
|
||||
if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
|
||||
return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
|
||||
|
||||
var pythonPath = string.Empty;
|
||||
|
||||
if (MessageBox.Show("Flow detected you have installed Python plugins, which " +
|
||||
"will need Python to run. Would you like to download Python? " +
|
||||
Environment.NewLine + Environment.NewLine +
|
||||
"Click no if it's already installed, " +
|
||||
"and you will be prompted to select the folder that contains the Python executable",
|
||||
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
|
||||
&& string.IsNullOrEmpty(settings.PythonDirectory))
|
||||
{
|
||||
var dlg = new FolderBrowserDialog
|
||||
{
|
||||
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
|
||||
};
|
||||
|
||||
var result = dlg.ShowDialog();
|
||||
if (result == DialogResult.OK)
|
||||
{
|
||||
string pythonDirectory = dlg.SelectedPath;
|
||||
if (!string.IsNullOrEmpty(pythonDirectory))
|
||||
{
|
||||
pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
|
||||
if (File.Exists(pythonPath))
|
||||
{
|
||||
settings.PythonDirectory = pythonDirectory;
|
||||
Constant.PythonPath = pythonPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show("Can't find python in given directory");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable");
|
||||
|
||||
// Python 3.8.9 is used for Windows 7 compatibility
|
||||
DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait();
|
||||
|
||||
pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
|
||||
if (FilesFolders.FileExists(pythonPath))
|
||||
{
|
||||
settings.PythonDirectory = installedPythonDirectory;
|
||||
Constant.PythonPath = pythonPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("PluginsLoader",
|
||||
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
|
||||
"PythonPlugins");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath))
|
||||
{
|
||||
MessageBox.Show(
|
||||
"Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
|
||||
Log.Error("PluginsLoader",
|
||||
$"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.",
|
||||
"PythonPlugins");
|
||||
|
||||
return new List<PluginPair>();
|
||||
}
|
||||
|
||||
return SetPythonPathForPluginPairs(source, pythonPath);
|
||||
}
|
||||
|
||||
private static IEnumerable<PluginPair> SetPythonPathForPluginPairs(List<PluginMetadata> source, string pythonPath)
|
||||
=> source
|
||||
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
|
||||
.Select(metadata => new PluginPair
|
||||
{
|
||||
Plugin = new PythonPlugin(pythonPath),
|
||||
Metadata = metadata
|
||||
})
|
||||
.ToList();
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable)
|
||||
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(metadata => new PluginPair
|
||||
{
|
||||
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.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
|
|
@ -11,7 +12,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
internal class PythonPlugin : JsonRPCPlugin
|
||||
{
|
||||
private readonly ProcessStartInfo _startInfo;
|
||||
public override string SupportedLanguage { get; set; } = AllowedLanguage.Python;
|
||||
|
||||
public PythonPlugin(string filename)
|
||||
{
|
||||
|
|
@ -24,37 +24,79 @@ namespace Flow.Launcher.Core.Plugin
|
|||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
// temp fix for issue #667
|
||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
||||
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||
// Prevent Python from writing .py[co] files.
|
||||
// Because .pyc contains location infos which will prevent python portable.
|
||||
_startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
|
||||
|
||||
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
|
||||
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
|
||||
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
|
||||
|
||||
|
||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
||||
_startInfo.ArgumentList.Add("-B");
|
||||
}
|
||||
|
||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||
{
|
||||
_startInfo.ArgumentList[2] = request.ToString();
|
||||
_startInfo.ArgumentList[2] = JsonSerializer.Serialize(request, RequestSerializeOption);
|
||||
|
||||
return ExecuteAsync(_startInfo, token);
|
||||
}
|
||||
|
||||
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
|
||||
{
|
||||
_startInfo.ArgumentList[2] = rpcRequest.ToString();
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
// since this is not static, request strings will build up in ArgumentList if index is not specified
|
||||
_startInfo.ArgumentList[2] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption);
|
||||
_startInfo.WorkingDirectory = Context.CurrentPluginMetadata.PluginDirectory;
|
||||
// TODO: Async Action
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
public override async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
_startInfo.ArgumentList.Add("");
|
||||
// Run .py files via `-c <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");
|
||||
|
||||
// This makes it easier for plugin authors to import their own modules.
|
||||
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
|
||||
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
|
||||
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
|
||||
_startInfo.ArgumentList.Add("-c");
|
||||
_startInfo.ArgumentList.Add(
|
||||
$"""
|
||||
import sys
|
||||
sys.path.append(r'{rootDirectory}')
|
||||
sys.path.append(r'{libDirectory}')
|
||||
sys.path.append(r'{libPyWin32LibDirectory}')
|
||||
sys.path.append(r'{libPyWin32Directory}')
|
||||
sys.path.append(r'{pluginDirectory}')
|
||||
|
||||
import runpy
|
||||
runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__')
|
||||
"""
|
||||
);
|
||||
// Plugins always expect the JSON data to be in the third argument
|
||||
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
|
||||
_startInfo.ArgumentList.Add("");
|
||||
}
|
||||
// Run .pyz files as is
|
||||
else
|
||||
{
|
||||
// No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now,
|
||||
// but the plugins still expect data to be sent as the third argument, so we're keeping
|
||||
// the flag here, even though it's not necessary anymore.
|
||||
_startInfo.ArgumentList.Add("-B");
|
||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
// Plugins always expect the JSON data to be in the third argument
|
||||
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
|
||||
_startInfo.ArgumentList.Add("");
|
||||
}
|
||||
|
||||
await base.InitAsync(context);
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
}
|
||||
|
|
|
|||
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.Linq;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
|
|
@ -34,9 +33,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
searchTerms = terms;
|
||||
}
|
||||
|
||||
var query = new Query(rawQuery, search,terms, searchTerms, actionKeyword);
|
||||
|
||||
return query;
|
||||
return new 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
|
||||
{
|
||||
|
|
@ -23,8 +23,12 @@ namespace Flow.Launcher.Core.Resource
|
|||
public static Language Spanish_LatinAmerica = new Language("es-419", "Spanish (Latin America)");
|
||||
public static Language Italian = new Language("it", "Italiano");
|
||||
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 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()
|
||||
{
|
||||
|
|
@ -50,9 +54,46 @@ namespace Flow.Launcher.Core.Resource
|
|||
Italian,
|
||||
Norwegian_Bokmal,
|
||||
Slovak,
|
||||
Turkish
|
||||
Turkish,
|
||||
Czech,
|
||||
Arabic,
|
||||
Vietnamese,
|
||||
Hebrew
|
||||
};
|
||||
return languages;
|
||||
}
|
||||
|
||||
public static string GetSystemTranslation(string languageCode)
|
||||
{
|
||||
return languageCode switch
|
||||
{
|
||||
"en" => "System",
|
||||
"zh-cn" => "系统",
|
||||
"zh-tw" => "系統",
|
||||
"uk-UA" => "Система",
|
||||
"ru" => "Система",
|
||||
"fr" => "Système",
|
||||
"ja" => "システム",
|
||||
"nl" => "Systeem",
|
||||
"pl" => "System",
|
||||
"da" => "System",
|
||||
"de" => "System",
|
||||
"ko" => "시스템",
|
||||
"sr" => "Систем",
|
||||
"pt-pt" => "Sistema",
|
||||
"pt-br" => "Sistema",
|
||||
"es" => "Sistema",
|
||||
"es-419" => "Sistema",
|
||||
"it" => "Sistema",
|
||||
"nb-NO" => "System",
|
||||
"sk" => "Systém",
|
||||
"tr" => "Sistem",
|
||||
"cs" => "Systém",
|
||||
"ar" => "النظام",
|
||||
"vi-vn" => "Hệ thống",
|
||||
"he" => "מערכת",
|
||||
_ => "System",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,38 +11,65 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
|||
using Flow.Launcher.Plugin;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class Internationalization
|
||||
{
|
||||
public Settings Settings { get; set; }
|
||||
private const string Folder = "Languages";
|
||||
private const string DefaultLanguageCode = "en";
|
||||
private const string DefaultFile = "en.xaml";
|
||||
private const string Extension = ".xaml";
|
||||
private readonly Settings _settings;
|
||||
private readonly List<string> _languageDirectories = new List<string>();
|
||||
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
|
||||
private readonly string SystemLanguageCode;
|
||||
|
||||
public Internationalization()
|
||||
public Internationalization(Settings settings)
|
||||
{
|
||||
AddPluginLanguageDirectories();
|
||||
LoadDefaultLanguage();
|
||||
// we don't want to load /Languages/en.xaml twice
|
||||
// so add flowlauncher language directory after load plugin language files
|
||||
_settings = settings;
|
||||
AddFlowLauncherLanguageDirectory();
|
||||
SystemLanguageCode = GetSystemLanguageCodeAtStartup();
|
||||
}
|
||||
|
||||
|
||||
private void AddFlowLauncherLanguageDirectory()
|
||||
{
|
||||
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
_languageDirectories.Add(directory);
|
||||
}
|
||||
|
||||
|
||||
private void AddPluginLanguageDirectories()
|
||||
private static string GetSystemLanguageCodeAtStartup()
|
||||
{
|
||||
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 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}>");
|
||||
}
|
||||
}
|
||||
|
||||
LoadDefaultLanguage();
|
||||
}
|
||||
|
||||
private void LoadDefaultLanguage()
|
||||
{
|
||||
// Removes language files loaded before any plugins were loaded.
|
||||
// Prevents the language Flow started in from overwriting English if the user switches back to English
|
||||
RemoveOldLanguageFiles();
|
||||
LoadLanguage(AvailableLanguages.English);
|
||||
_oldResources.Clear();
|
||||
}
|
||||
|
|
@ -67,8 +99,18 @@ namespace Flow.Launcher.Core.Resource
|
|||
public void ChangeLanguage(string languageCode)
|
||||
{
|
||||
languageCode = languageCode.NonNull();
|
||||
Language language = GetLanguageByLanguageCode(languageCode);
|
||||
ChangeLanguage(language);
|
||||
|
||||
// Get actual language if language code is system
|
||||
var isSystem = false;
|
||||
if (languageCode == Constant.SystemLanguageCode)
|
||||
{
|
||||
languageCode = SystemLanguageCode;
|
||||
isSystem = true;
|
||||
}
|
||||
|
||||
// Get language by language code and change language
|
||||
var language = GetLanguageByLanguageCode(languageCode);
|
||||
ChangeLanguage(language, isSystem);
|
||||
}
|
||||
|
||||
private Language GetLanguageByLanguageCode(string languageCode)
|
||||
|
|
@ -86,19 +128,22 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
}
|
||||
|
||||
public void ChangeLanguage(Language language)
|
||||
private void ChangeLanguage(Language language, bool isSystem)
|
||||
{
|
||||
language = language.NonNull();
|
||||
|
||||
|
||||
RemoveOldLanguageFiles();
|
||||
if (language != AvailableLanguages.English)
|
||||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
Settings.Language = language.LanguageCode;
|
||||
CultureInfo.CurrentCulture = new CultureInfo(language.LanguageCode);
|
||||
// Culture of main thread
|
||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||
|
||||
// Raise event after culture is set
|
||||
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
UpdatePluginMetadataTranslations();
|
||||
|
|
@ -109,7 +154,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
|
||||
|
||||
if (Settings.ShouldUsePinyin)
|
||||
if (_settings.ShouldUsePinyin)
|
||||
return false;
|
||||
|
||||
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
|
||||
|
|
@ -119,7 +164,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
// "Do you want to search with pinyin?"
|
||||
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
|
||||
|
||||
if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
if (Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
|
@ -136,11 +181,14 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
private void LoadLanguage(Language language)
|
||||
{
|
||||
var flowEnglishFile = Path.Combine(Constant.ProgramDirectory, Folder, DefaultFile);
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
var filename = $"{language.LanguageCode}{Extension}";
|
||||
var files = _languageDirectories
|
||||
.Select(d => LanguageFile(d, filename))
|
||||
.Where(f => !string.IsNullOrEmpty(f))
|
||||
// Exclude Flow's English language file since it's built into the binary, and there's no need to load
|
||||
// it again from the file system.
|
||||
.Where(f => !string.IsNullOrEmpty(f) && f != flowEnglishFile)
|
||||
.ToArray();
|
||||
|
||||
if (files.Length > 0)
|
||||
|
|
@ -159,7 +207,9 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
private static Internationalization instance;
|
||||
private static object syncObject = new object();
|
||||
|
||||
public static Internationalization Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
lock (syncObject)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = new Internationalization();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
=> Ioc.Default.GetRequiredService<Internationalization>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using System.Globalization;
|
|||
using System.Reflection;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Core
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class LocalizationConverter : IValueConverter
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System.ComponentModel;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
|
||||
namespace Flow.Launcher.Core
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class LocalizedDescriptionAttribute : DescriptionAttribute
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,41 +2,52 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Xml;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Shell;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class Theme
|
||||
{
|
||||
private const string ThemeMetadataNamePrefix = "Name:";
|
||||
private const string ThemeMetadataIsDarkPrefix = "IsDark:";
|
||||
private const string ThemeMetadataHasBlurPrefix = "HasBlur:";
|
||||
|
||||
private const int ShadowExtraMargin = 32;
|
||||
|
||||
private readonly List<string> _themeDirectories = new List<string>();
|
||||
private readonly IPublicAPI _api;
|
||||
private readonly Settings _settings;
|
||||
private readonly List<string> _themeDirectories = new();
|
||||
private ResourceDictionary _oldResource;
|
||||
private string _oldTheme;
|
||||
public Settings Settings { get; set; }
|
||||
private const string Folder = Constant.Themes;
|
||||
private const string Extension = ".xaml";
|
||||
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
|
||||
|
||||
public string CurrentTheme => _settings.Theme;
|
||||
|
||||
public bool BlurEnabled { get; set; }
|
||||
|
||||
private double mainWindowWidth;
|
||||
|
||||
public Theme()
|
||||
public Theme(IPublicAPI publicAPI, Settings settings)
|
||||
{
|
||||
_api = publicAPI;
|
||||
_settings = settings;
|
||||
|
||||
_themeDirectories.Add(DirectoryPath);
|
||||
_themeDirectories.Add(UserDirectoryPath);
|
||||
MakesureThemeDirectoriesExist();
|
||||
MakeSureThemeDirectoriesExist();
|
||||
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
_oldResource = dicts.First(d =>
|
||||
|
|
@ -55,20 +66,17 @@ namespace Flow.Launcher.Core.Resource
|
|||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
|
||||
private void MakesureThemeDirectoriesExist()
|
||||
private void MakeSureThemeDirectoriesExist()
|
||||
{
|
||||
foreach (string dir in _themeDirectories)
|
||||
foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir)))
|
||||
{
|
||||
if (!Directory.Exists(dir))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e);
|
||||
}
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -83,11 +91,12 @@ namespace Flow.Launcher.Core.Resource
|
|||
if (string.IsNullOrEmpty(path))
|
||||
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
|
||||
|
||||
Settings.Theme = theme;
|
||||
|
||||
// reload all resources even if the theme itself hasn't changed in order to pickup changes
|
||||
// to things like fonts
|
||||
UpdateResourceDictionary(GetResourceDictionary());
|
||||
UpdateResourceDictionary(GetResourceDictionary(theme));
|
||||
|
||||
_settings.Theme = theme;
|
||||
|
||||
|
||||
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
|
||||
if (_oldTheme != theme || theme == defaultTheme)
|
||||
|
|
@ -95,19 +104,19 @@ namespace Flow.Launcher.Core.Resource
|
|||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
|
||||
BlurEnabled = IsBlurTheme();
|
||||
BlurEnabled = Win32Helper.IsBlurTheme();
|
||||
|
||||
if (Settings.UseDropShadowEffect && !BlurEnabled)
|
||||
if (_settings.UseDropShadowEffect && !BlurEnabled)
|
||||
AddDropShadowEffectToCurrentTheme();
|
||||
|
||||
SetBlurForWindow();
|
||||
Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
|
||||
if (theme != defaultTheme)
|
||||
{
|
||||
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
|
||||
_api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
|
||||
ChangeTheme(defaultTheme);
|
||||
}
|
||||
return false;
|
||||
|
|
@ -117,7 +126,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
|
||||
if (theme != defaultTheme)
|
||||
{
|
||||
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
|
||||
_api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
|
||||
ChangeTheme(defaultTheme);
|
||||
}
|
||||
return false;
|
||||
|
|
@ -134,9 +143,9 @@ namespace Flow.Launcher.Core.Resource
|
|||
_oldResource = dictionaryToUpdate;
|
||||
}
|
||||
|
||||
private ResourceDictionary CurrentThemeResourceDictionary()
|
||||
private ResourceDictionary GetThemeResourceDictionary(string theme)
|
||||
{
|
||||
var uri = GetThemePath(Settings.Theme);
|
||||
var uri = GetThemePath(theme);
|
||||
var dict = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri(uri, UriKind.Absolute)
|
||||
|
|
@ -145,17 +154,19 @@ namespace Flow.Launcher.Core.Resource
|
|||
return dict;
|
||||
}
|
||||
|
||||
public ResourceDictionary GetResourceDictionary()
|
||||
private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(_settings.Theme);
|
||||
|
||||
public ResourceDictionary GetResourceDictionary(string theme)
|
||||
{
|
||||
var dict = CurrentThemeResourceDictionary();
|
||||
|
||||
var dict = GetThemeResourceDictionary(theme);
|
||||
|
||||
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
|
||||
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
|
||||
{
|
||||
var fontFamily = new FontFamily(Settings.QueryBoxFont);
|
||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.QueryBoxFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.QueryBoxFontStretch);
|
||||
var fontFamily = new FontFamily(_settings.QueryBoxFont);
|
||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
||||
|
|
@ -176,41 +187,96 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
|
||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||
dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle &&
|
||||
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
|
||||
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
||||
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
|
||||
{
|
||||
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultFont));
|
||||
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultFontStyle));
|
||||
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultFontWeight));
|
||||
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultFontStretch));
|
||||
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultFont));
|
||||
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle));
|
||||
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight));
|
||||
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch));
|
||||
|
||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||
Array.ForEach(
|
||||
new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o
|
||||
new[] { resultItemStyle, resultItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o
|
||||
=> Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||
}
|
||||
|
||||
if (
|
||||
dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
|
||||
{
|
||||
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultSubFont));
|
||||
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle));
|
||||
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight));
|
||||
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch));
|
||||
|
||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||
Array.ForEach(
|
||||
new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o
|
||||
=> Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||
}
|
||||
|
||||
/* Ignore Theme Window Width and use setting */
|
||||
var windowStyle = dict["WindowStyle"] as Style;
|
||||
var width = Settings.WindowSize;
|
||||
var width = _settings.WindowSize;
|
||||
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
|
||||
mainWindowWidth = (double)width;
|
||||
return dict;
|
||||
}
|
||||
|
||||
public List<string> LoadAvailableThemes()
|
||||
private ResourceDictionary GetCurrentResourceDictionary( )
|
||||
{
|
||||
List<string> themes = new List<string>();
|
||||
return GetResourceDictionary(_settings.Theme);
|
||||
}
|
||||
|
||||
public List<ThemeData> LoadAvailableThemes()
|
||||
{
|
||||
List<ThemeData> themes = new List<ThemeData>();
|
||||
foreach (var themeDirectory in _themeDirectories)
|
||||
{
|
||||
themes.AddRange(
|
||||
Directory.GetFiles(themeDirectory)
|
||||
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
|
||||
.ToList());
|
||||
var filePaths = Directory
|
||||
.GetFiles(themeDirectory)
|
||||
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
|
||||
.Select(GetThemeDataFromPath);
|
||||
themes.AddRange(filePaths);
|
||||
}
|
||||
return themes.OrderBy(o => o).ToList();
|
||||
|
||||
return themes.OrderBy(o => o.Name).ToList();
|
||||
}
|
||||
|
||||
private ThemeData GetThemeDataFromPath(string path)
|
||||
{
|
||||
using var reader = XmlReader.Create(path);
|
||||
reader.Read();
|
||||
|
||||
var extensionlessName = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
if (reader.NodeType is not XmlNodeType.Comment)
|
||||
return new ThemeData(extensionlessName, extensionlessName);
|
||||
|
||||
var commentLines = reader.Value.Trim().Split('\n').Select(v => v.Trim());
|
||||
|
||||
var name = extensionlessName;
|
||||
bool? isDark = null;
|
||||
bool? hasBlur = null;
|
||||
foreach (var line in commentLines)
|
||||
{
|
||||
if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim();
|
||||
}
|
||||
else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim());
|
||||
}
|
||||
else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return new ThemeData(extensionlessName, name, isDark, hasBlur);
|
||||
}
|
||||
|
||||
private string GetThemePath(string themeName)
|
||||
|
|
@ -229,7 +295,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
public void AddDropShadowEffectToCurrentTheme()
|
||||
{
|
||||
var dict = GetResourceDictionary();
|
||||
var dict = GetCurrentResourceDictionary();
|
||||
|
||||
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
|
||||
|
||||
|
|
@ -248,12 +314,15 @@ namespace Flow.Launcher.Core.Resource
|
|||
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
||||
if (marginSetter == null)
|
||||
{
|
||||
var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin);
|
||||
marginSetter = new Setter()
|
||||
{
|
||||
Property = Border.MarginProperty,
|
||||
Value = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin),
|
||||
Value = margin,
|
||||
};
|
||||
windowBorderStyle.Setters.Add(marginSetter);
|
||||
|
||||
SetResizeBoarderThickness(margin);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -264,6 +333,8 @@ namespace Flow.Launcher.Core.Resource
|
|||
baseMargin.Right + ShadowExtraMargin,
|
||||
baseMargin.Bottom + ShadowExtraMargin);
|
||||
marginSetter.Value = newMargin;
|
||||
|
||||
SetResizeBoarderThickness(newMargin);
|
||||
}
|
||||
|
||||
windowBorderStyle.Setters.Add(effectSetter);
|
||||
|
|
@ -273,7 +344,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
public void RemoveDropShadowEffectFromCurrentTheme()
|
||||
{
|
||||
var dict = CurrentThemeResourceDictionary();
|
||||
var dict = GetCurrentResourceDictionary();
|
||||
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
|
||||
|
||||
var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;
|
||||
|
|
@ -294,99 +365,36 @@ namespace Flow.Launcher.Core.Resource
|
|||
marginSetter.Value = newMargin;
|
||||
}
|
||||
|
||||
SetResizeBoarderThickness(null);
|
||||
|
||||
UpdateResourceDictionary(dict);
|
||||
}
|
||||
|
||||
#region Blur Handling
|
||||
/*
|
||||
Found on https://github.com/riverar/sample-win10-aeroglass
|
||||
*/
|
||||
private enum AccentState
|
||||
// because adding drop shadow effect will change the margin of the window,
|
||||
// we need to update the window chrome thickness to correct set the resize border
|
||||
private static void SetResizeBoarderThickness(Thickness? effectMargin)
|
||||
{
|
||||
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>
|
||||
/// Sets the blur for a window via SetWindowCompositionAttribute
|
||||
/// </summary>
|
||||
public void SetBlurForWindow()
|
||||
{
|
||||
if (BlurEnabled)
|
||||
var window = Application.Current.MainWindow;
|
||||
if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome)
|
||||
{
|
||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
|
||||
Thickness thickness;
|
||||
if (effectMargin == null)
|
||||
{
|
||||
thickness = SystemParameters.WindowResizeBorderThickness;
|
||||
}
|
||||
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()
|
||||
{
|
||||
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
|
||||
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
private static Theme instance;
|
||||
private static object syncObject = new object();
|
||||
|
||||
public static Theme Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
lock (syncObject)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = new Theme();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
=> Ioc.Default.GetRequiredService<Theme>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
|
||||
namespace Flow.Launcher.Converters
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class TranlationConverter : IValueConverter
|
||||
public class TranslationConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
|
|
@ -22,23 +22,26 @@ namespace Flow.Launcher.Core
|
|||
{
|
||||
public class Updater
|
||||
{
|
||||
public string GitHubRepository { get; }
|
||||
public string GitHubRepository { get; init; }
|
||||
|
||||
public Updater(string gitHubRepository)
|
||||
private readonly IPublicAPI _api;
|
||||
|
||||
public Updater(IPublicAPI publicAPI, string gitHubRepository)
|
||||
{
|
||||
_api = publicAPI;
|
||||
GitHubRepository = gitHubRepository;
|
||||
}
|
||||
|
||||
private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1);
|
||||
|
||||
public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
|
||||
public async Task UpdateAppAsync(bool silentUpdate = true)
|
||||
{
|
||||
await UpdateLock.WaitAsync();
|
||||
await UpdateLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!silentUpdate)
|
||||
api.ShowMsg(api.GetTranslation("pleaseWait"),
|
||||
api.GetTranslation("update_flowlauncher_update_check"));
|
||||
_api.ShowMsg(_api.GetTranslation("pleaseWait"),
|
||||
_api.GetTranslation("update_flowlauncher_update_check"));
|
||||
|
||||
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -53,13 +56,13 @@ namespace Flow.Launcher.Core
|
|||
if (newReleaseVersion <= currentVersion)
|
||||
{
|
||||
if (!silentUpdate)
|
||||
MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest"));
|
||||
_api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silentUpdate)
|
||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"),
|
||||
api.GetTranslation("update_flowlauncher_updating"));
|
||||
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
|
||||
_api.GetTranslation("update_flowlauncher_updating"));
|
||||
|
||||
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -68,9 +71,9 @@ namespace Flow.Launcher.Core
|
|||
if (DataLocation.PortableDataLocationInUse())
|
||||
{
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
|
||||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination);
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination))
|
||||
MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||
DataLocation.PortableDataPath,
|
||||
targetDestination));
|
||||
}
|
||||
|
|
@ -79,21 +82,25 @@ namespace Flow.Launcher.Core
|
|||
await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var newVersionTips = NewVersinoTips(newReleaseVersion.ToString());
|
||||
var newVersionTips = NewVersionTips(newReleaseVersion.ToString());
|
||||
|
||||
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
|
||||
if ((e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException))
|
||||
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
|
||||
else
|
||||
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
|
||||
|
||||
if (!silentUpdate)
|
||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
|
||||
api.GetTranslation("update_flowlauncher_check_connection"));
|
||||
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
|
||||
_api.GetTranslation("update_flowlauncher_check_connection"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -114,7 +121,7 @@ namespace Flow.Launcher.Core
|
|||
public string HtmlUrl { get; [UsedImplicitly] set; }
|
||||
}
|
||||
|
||||
/// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||
{
|
||||
var uri = new Uri(repository);
|
||||
|
|
@ -137,13 +144,12 @@ namespace Flow.Launcher.Core
|
|||
return manager;
|
||||
}
|
||||
|
||||
public string NewVersinoTips(string version)
|
||||
public string NewVersionTips(string version)
|
||||
{
|
||||
var translater = InternationalizationManager.Instance;
|
||||
var tips = string.Format(translater.GetTranslation("newVersionTips"), version);
|
||||
var translator = InternationalizationManager.Instance;
|
||||
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
|
||||
|
||||
return tips;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,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 const string FlowLauncher = "Flow.Launcher";
|
||||
public const string FlowLauncherFullName = "Flow Launcher";
|
||||
public const string Plugins = "Plugins";
|
||||
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 PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
|
||||
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
|
||||
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 Dev = "Dev";
|
||||
public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips";
|
||||
|
|
@ -29,8 +30,11 @@ namespace Flow.Launcher.Infrastructure
|
|||
public static readonly string DefaultIcon = Path.Combine(ImagesDirectory, "app.png");
|
||||
public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png");
|
||||
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
||||
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
||||
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
|
||||
|
||||
public static string PythonPath;
|
||||
public static string NodePath;
|
||||
|
||||
public static readonly string QueryTextBoxIconImagePath = $"{ProgramDirectory}\\Images\\mainsearch.svg";
|
||||
|
||||
|
|
@ -45,7 +49,10 @@ namespace Flow.Launcher.Infrastructure
|
|||
public const string Logs = "Logs";
|
||||
|
||||
public const string Website = "https://flowlauncher.com";
|
||||
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
|
||||
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
|
||||
public const string Docs = "https://flowlauncher.com/docs";
|
||||
|
||||
public const string SystemLanguageCode = "system";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Exception
|
||||
|
|
@ -63,10 +62,11 @@ namespace Flow.Launcher.Infrastructure.Exception
|
|||
sb.AppendLine($"* Command Line: {Environment.CommandLine}");
|
||||
sb.AppendLine($"* Timestamp: {DateTime.Now.ToString(CultureInfo.InvariantCulture)}");
|
||||
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($"* x64: {Environment.Is64BitOperatingSystem}");
|
||||
sb.AppendLine($"* Python Path: {Constant.PythonPath}");
|
||||
sb.AppendLine($"* Node Path: {Constant.NodePath}");
|
||||
sb.AppendLine($"* CLR Version: {Environment.Version}");
|
||||
sb.AppendLine($"* Installed .NET Framework: ");
|
||||
foreach (var result in GetFrameworkVersionFromRegistry())
|
||||
|
|
@ -172,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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
98
Flow.Launcher.Infrastructure/FileExplorerHelper.cs
Normal file
98
Flow.Launcher.Infrastructure/FileExplorerHelper.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Windows.Win32;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
public static class FileExplorerHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path of the file explorer that is currently in the foreground
|
||||
/// </summary>
|
||||
public static string GetActiveExplorerPath()
|
||||
{
|
||||
var explorerWindow = GetActiveExplorer();
|
||||
string locationUrl = explorerWindow?.LocationURL;
|
||||
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>
|
||||
/// Gets the file explorer that is currently in the foreground
|
||||
/// </summary>
|
||||
private static dynamic GetActiveExplorer()
|
||||
{
|
||||
Type type = Type.GetTypeFromProgID("Shell.Application");
|
||||
if (type == null) return null;
|
||||
dynamic shell = Activator.CreateInstance(type);
|
||||
if (shell == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var explorerWindows = new List<dynamic>();
|
||||
var openWindows = shell.Windows();
|
||||
for (int i = 0; i < openWindows.Count; i++)
|
||||
{
|
||||
var window = openWindows.Item(i);
|
||||
if (window == null) continue;
|
||||
|
||||
// find the desired window and make sure that it is indeed a file explorer
|
||||
// we don't want the Internet Explorer or the classic control panel
|
||||
// ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE"
|
||||
if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe")
|
||||
{
|
||||
explorerWindows.Add(window);
|
||||
}
|
||||
}
|
||||
|
||||
if (explorerWindows.Count == 0) return null;
|
||||
|
||||
var zOrders = GetZOrder(explorerWindows);
|
||||
|
||||
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
||||
}
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
|
||||
/// </summary>
|
||||
private static IEnumerable<int> GetZOrder(List<dynamic> hWnds)
|
||||
{
|
||||
var z = new int[hWnds.Count];
|
||||
for (var i = 0; i < hWnds.Count; i++) z[i] = -1;
|
||||
|
||||
var index = 0;
|
||||
var numRemaining = hWnds.Count;
|
||||
PInvoke.EnumWindows((wnd, _) =>
|
||||
{
|
||||
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
|
||||
if (searchIndex != -1)
|
||||
{
|
||||
z[searchIndex] = index;
|
||||
numRemaining--;
|
||||
if (numRemaining == 0) return false;
|
||||
}
|
||||
index++;
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
|
||||
return z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<UseWpf>true</UseWpf>
|
||||
|
|
@ -35,6 +35,10 @@
|
|||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="NativeMethods.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SolutionAssemblyInfo.cs" Link="Properties\SolutionAssemblyInfo.cs" />
|
||||
<None Include="FodyWeavers.xml" />
|
||||
|
|
@ -49,16 +53,21 @@
|
|||
|
||||
<ItemGroup>
|
||||
<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">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.3.44" />
|
||||
<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.Schema" Version="4.7.10" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.13.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
|
||||
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->
|
||||
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
/// <summary>
|
||||
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
|
||||
/// </summary>
|
||||
public static T NonNull<T>(this T obj)
|
||||
public static T NonNull<T>(this T? obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||
{
|
||||
|
|
@ -11,44 +15,45 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
/// </summary>
|
||||
public unsafe class GlobalHotkey : IDisposable
|
||||
{
|
||||
private static readonly IntPtr hookId;
|
||||
|
||||
|
||||
|
||||
private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback;
|
||||
private static readonly UnhookWindowsHookExSafeHandle hookId;
|
||||
|
||||
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
|
||||
internal static Func<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()
|
||||
{
|
||||
// Set the hook
|
||||
hookId = InterceptKeys.SetHook(& LowLevelKeyboardProc);
|
||||
hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL);
|
||||
}
|
||||
|
||||
private static UnhookWindowsHookExSafeHandle SetHook(HOOKPROC proc, WINDOWS_HOOK_ID hookId)
|
||||
{
|
||||
using var curProcess = Process.GetCurrentProcess();
|
||||
using var curModule = curProcess.MainModule;
|
||||
return PInvoke.SetWindowsHookEx(hookId, proc, PInvoke.GetModuleHandle(curModule.ModuleName), 0);
|
||||
}
|
||||
|
||||
public static SpecialKeyState CheckModifiers()
|
||||
{
|
||||
SpecialKeyState state = new SpecialKeyState();
|
||||
if ((InterceptKeys.GetKeyState(VK_SHIFT) & 0x8000) != 0)
|
||||
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) & 0x8000) != 0)
|
||||
{
|
||||
//SHIFT is pressed
|
||||
state.ShiftPressed = true;
|
||||
}
|
||||
if ((InterceptKeys.GetKeyState(VK_CONTROL) & 0x8000) != 0)
|
||||
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) & 0x8000) != 0)
|
||||
{
|
||||
//CONTROL is pressed
|
||||
state.CtrlPressed = true;
|
||||
}
|
||||
if ((InterceptKeys.GetKeyState(VK_ALT) & 0x8000) != 0)
|
||||
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) & 0x8000) != 0)
|
||||
{
|
||||
//ALT is pressed
|
||||
state.AltPressed = true;
|
||||
}
|
||||
if ((InterceptKeys.GetKeyState(VK_WIN) & 0x8000) != 0)
|
||||
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_LWIN) & 0x8000) != 0 ||
|
||||
(PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_RWIN) & 0x8000) != 0)
|
||||
{
|
||||
//WIN is pressed
|
||||
state.WinPressed = true;
|
||||
|
|
@ -57,33 +62,33 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
return state;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
|
||||
private static LRESULT HookKeyboardCallback(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
bool continues = true;
|
||||
|
||||
if (nCode >= 0)
|
||||
{
|
||||
if (wParam.ToUInt32() == (int)KeyEvent.WM_KEYDOWN ||
|
||||
wParam.ToUInt32() == (int)KeyEvent.WM_KEYUP ||
|
||||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYDOWN ||
|
||||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYUP)
|
||||
if (wParam.Value == (int)KeyEvent.WM_KEYDOWN ||
|
||||
wParam.Value == (int)KeyEvent.WM_KEYUP ||
|
||||
wParam.Value == (int)KeyEvent.WM_SYSKEYDOWN ||
|
||||
wParam.Value == (int)KeyEvent.WM_SYSKEYUP)
|
||||
{
|
||||
if (hookedKeyboardCallback != null)
|
||||
continues = hookedKeyboardCallback((KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), CheckModifiers());
|
||||
continues = hookedKeyboardCallback((KeyEvent)wParam.Value, Marshal.ReadInt32(lParam), CheckModifiers());
|
||||
}
|
||||
}
|
||||
|
||||
if (continues)
|
||||
{
|
||||
return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam);
|
||||
return PInvoke.CallNextHookEx(hookId, nCode, wParam, lParam);
|
||||
}
|
||||
return (IntPtr)(-1);
|
||||
|
||||
return new LRESULT(1);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
InterceptKeys.UnhookWindowsHookEx(hookId);
|
||||
hookId.Dispose();
|
||||
}
|
||||
|
||||
~GlobalHotkey()
|
||||
|
|
@ -91,4 +96,4 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||
{
|
||||
public class HotkeyModel
|
||||
public record struct HotkeyModel
|
||||
{
|
||||
public bool Alt { get; set; }
|
||||
public bool Shift { get; set; }
|
||||
public bool Win { get; set; }
|
||||
public bool Ctrl { get; set; }
|
||||
public Key CharKey { get; set; }
|
||||
|
||||
public Key CharKey { get; set; } = Key.None;
|
||||
|
||||
Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string>
|
||||
private static readonly Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string>
|
||||
{
|
||||
{Key.Space, "Space"},
|
||||
{Key.Oem3, "~"}
|
||||
{ Key.Space, "Space" }, { Key.Oem3, "~" }
|
||||
};
|
||||
|
||||
public ModifierKeys ModifierKeys
|
||||
|
|
@ -27,20 +27,24 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
ModifierKeys modifierKeys = ModifierKeys.None;
|
||||
if (Alt)
|
||||
{
|
||||
modifierKeys = ModifierKeys.Alt;
|
||||
modifierKeys |= ModifierKeys.Alt;
|
||||
}
|
||||
|
||||
if (Shift)
|
||||
{
|
||||
modifierKeys = modifierKeys | ModifierKeys.Shift;
|
||||
modifierKeys |= ModifierKeys.Shift;
|
||||
}
|
||||
|
||||
if (Win)
|
||||
{
|
||||
modifierKeys = modifierKeys | ModifierKeys.Windows;
|
||||
modifierKeys |= ModifierKeys.Windows;
|
||||
}
|
||||
|
||||
if (Ctrl)
|
||||
{
|
||||
modifierKeys = modifierKeys | ModifierKeys.Control;
|
||||
modifierKeys |= ModifierKeys.Control;
|
||||
}
|
||||
|
||||
return modifierKeys;
|
||||
}
|
||||
}
|
||||
|
|
@ -65,31 +69,37 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> keys = hotkeyString.Replace(" ", "").Split('+').ToList();
|
||||
if (keys.Contains("Alt"))
|
||||
{
|
||||
Alt = true;
|
||||
keys.Remove("Alt");
|
||||
}
|
||||
|
||||
if (keys.Contains("Shift"))
|
||||
{
|
||||
Shift = true;
|
||||
keys.Remove("Shift");
|
||||
}
|
||||
|
||||
if (keys.Contains("Win"))
|
||||
{
|
||||
Win = true;
|
||||
keys.Remove("Win");
|
||||
}
|
||||
|
||||
if (keys.Contains("Ctrl"))
|
||||
{
|
||||
Ctrl = true;
|
||||
keys.Remove("Ctrl");
|
||||
}
|
||||
if (keys.Count > 0)
|
||||
|
||||
if (keys.Count == 1)
|
||||
{
|
||||
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)
|
||||
{
|
||||
CharKey = specialSymbolPair.Value.Key;
|
||||
|
|
@ -98,11 +108,10 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
{
|
||||
try
|
||||
{
|
||||
CharKey = (Key) Enum.Parse(typeof (Key), charKey);
|
||||
CharKey = (Key)Enum.Parse(typeof(Key), charKey);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,36 +119,112 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
string text = string.Empty;
|
||||
if (Ctrl)
|
||||
return string.Join(" + ", EnumerateDisplayKeys());
|
||||
}
|
||||
|
||||
public IEnumerable<string> EnumerateDisplayKeys()
|
||||
{
|
||||
if (Ctrl && CharKey is not (Key.LeftCtrl or Key.RightCtrl))
|
||||
{
|
||||
text += "Ctrl + ";
|
||||
yield return "Ctrl";
|
||||
}
|
||||
if (Alt)
|
||||
|
||||
if (Alt && CharKey is not (Key.LeftAlt or Key.RightAlt))
|
||||
{
|
||||
text += "Alt + ";
|
||||
yield return "Alt";
|
||||
}
|
||||
if (Shift)
|
||||
|
||||
if (Shift && CharKey is not (Key.LeftShift or Key.RightShift))
|
||||
{
|
||||
text += "Shift + ";
|
||||
yield return "Shift";
|
||||
}
|
||||
if (Win)
|
||||
|
||||
if (Win && CharKey is not (Key.LWin or Key.RWin))
|
||||
{
|
||||
text += "Win + ";
|
||||
yield return "Win";
|
||||
}
|
||||
|
||||
if (CharKey != Key.None)
|
||||
{
|
||||
text += specialSymbolDictionary.ContainsKey(CharKey)
|
||||
? specialSymbolDictionary[CharKey]
|
||||
yield return specialSymbolDictionary.TryGetValue(CharKey, out var value)
|
||||
? value
|
||||
: CharKey.ToString();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
text = text.Remove(text.Length - 3);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
/// <summary>
|
||||
/// Validate hotkey
|
||||
/// </summary>
|
||||
/// <param name="validateKeyGestrue">Try to validate hotkey as a KeyGesture.</param>
|
||||
/// <returns></returns>
|
||||
public bool Validate(bool validateKeyGestrue = false)
|
||||
{
|
||||
switch (CharKey)
|
||||
{
|
||||
case Key.LeftAlt:
|
||||
case Key.RightAlt:
|
||||
case Key.LeftCtrl:
|
||||
case Key.RightCtrl:
|
||||
case Key.LeftShift:
|
||||
case Key.RightShift:
|
||||
case Key.LWin:
|
||||
case Key.RWin:
|
||||
case Key.None:
|
||||
return false;
|
||||
default:
|
||||
if (validateKeyGestrue)
|
||||
{
|
||||
try
|
||||
{
|
||||
KeyGesture keyGesture = new KeyGesture(CharKey, ModifierKeys);
|
||||
}
|
||||
catch (System.Exception e) when
|
||||
(e is NotSupportedException || e is InvalidEnumArgumentException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ModifierKeys == ModifierKeys.None)
|
||||
{
|
||||
return !IsPrintableCharacter(CharKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPrintableCharacter(Key key)
|
||||
{
|
||||
// https://stackoverflow.com/questions/11881199/identify-if-a-event-key-is-text-not-only-alphanumeric
|
||||
return (key >= Key.A && key <= Key.Z) ||
|
||||
(key >= Key.D0 && key <= Key.D9) ||
|
||||
(key >= Key.NumPad0 && key <= Key.NumPad9) ||
|
||||
key == Key.OemQuestion ||
|
||||
key == Key.OemQuotes ||
|
||||
key == Key.OemPlus ||
|
||||
key == Key.OemOpenBrackets ||
|
||||
key == Key.OemCloseBrackets ||
|
||||
key == Key.OemMinus ||
|
||||
key == Key.DeadCharProcessed ||
|
||||
key == Key.Oem1 ||
|
||||
key == Key.Oem7 ||
|
||||
key == Key.OemPeriod ||
|
||||
key == Key.OemComma ||
|
||||
key == Key.OemMinus ||
|
||||
key == Key.Add ||
|
||||
key == Key.Divide ||
|
||||
key == Key.Multiply ||
|
||||
key == Key.Subtract ||
|
||||
key == Key.Oem102 ||
|
||||
key == Key.Decimal;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
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
|
||||
{
|
||||
public enum KeyEvent
|
||||
|
|
@ -5,21 +7,21 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
/// <summary>
|
||||
/// Key down
|
||||
/// </summary>
|
||||
WM_KEYDOWN = 256,
|
||||
WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN,
|
||||
|
||||
/// <summary>
|
||||
/// Key up
|
||||
/// </summary>
|
||||
WM_KEYUP = 257,
|
||||
WM_KEYUP = (int)PInvoke.WM_KEYUP,
|
||||
|
||||
/// <summary>
|
||||
/// System key up
|
||||
/// </summary>
|
||||
WM_SYSKEYUP = 261,
|
||||
WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP,
|
||||
|
||||
/// <summary>
|
||||
/// System key down
|
||||
/// </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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Interop;
|
||||
using Flow.Launcher.Plugin;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Http
|
||||
{
|
||||
|
|
@ -20,8 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
|
||||
private static HttpClient client = new HttpClient();
|
||||
|
||||
public static IPublicAPI API { get; set; }
|
||||
|
||||
static Http()
|
||||
{
|
||||
// need to be added so it would work on a win10 machine
|
||||
|
|
@ -68,7 +64,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
var userName when string.IsNullOrEmpty(userName) =>
|
||||
(new Uri($"http://{Proxy.Server}:{Proxy.Port}"), null),
|
||||
_ => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"),
|
||||
new NetworkCredential(Proxy.UserName, Proxy.Password))
|
||||
new NetworkCredential(Proxy.UserName, Proxy.Password))
|
||||
},
|
||||
_ => (null, null)
|
||||
},
|
||||
|
|
@ -79,22 +75,57 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
||||
var canReportProgress = totalBytes != -1;
|
||||
|
||||
if (canReportProgress && reportProgress != null)
|
||||
{
|
||||
await using var contentStream = await response.Content.ReadAsStreamAsync(token);
|
||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true);
|
||||
|
||||
var buffer = new byte[8192];
|
||||
long totalRead = 0;
|
||||
int read;
|
||||
double progressValue = 0;
|
||||
|
||||
reportProgress(0);
|
||||
|
||||
while ((read = await contentStream.ReadAsync(buffer, token)) > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer.AsMemory(0, read), token);
|
||||
totalRead += read;
|
||||
|
||||
progressValue = totalRead * 100.0 / totalBytes;
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
else
|
||||
reportProgress(progressValue);
|
||||
}
|
||||
|
||||
if (progressValue < 100)
|
||||
reportProgress(100);
|
||||
}
|
||||
else
|
||||
{
|
||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||
await response.Content.CopyToAsync(fileStream, token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -117,7 +148,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
public static Task<string> GetAsync([NotNull] string url, CancellationToken token = default)
|
||||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
return GetAsync(new Uri(url.Replace("#", "%23")), token);
|
||||
return GetAsync(new Uri(url), token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -130,36 +161,57 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
using var response = await client.GetAsync(url, token);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
else
|
||||
var content = await response.Content.ReadAsStringAsync(token);
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
$"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>");
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchrously get the result as stream from url.
|
||||
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="url">The Uri the request is sent to.</param>
|
||||
/// <param name="completionOption">An HTTP completion option value that indicates when the operation should be considered completed.</param>
|
||||
/// <param name="token">A cancellation token that can be used by other objects or threads to receive notice of cancellation</param>
|
||||
/// <returns></returns>
|
||||
public static Task<Stream> GetStreamAsync([NotNull] string url,
|
||||
CancellationToken token = default) => GetStreamAsync(new Uri(url), token);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
|
||||
public static async Task<Stream> GetStreamAsync([NotNull] Uri url,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
return await response.Content.ReadAsStreamAsync();
|
||||
return await client.GetStreamAsync(url, token);
|
||||
}
|
||||
|
||||
public static async Task<HttpResponseMessage> GetResponseAsync(string url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken token = default)
|
||||
=> await GetResponseAsync(new Uri(url), completionOption, token);
|
||||
|
||||
public static async Task<HttpResponseMessage> GetResponseAsync([NotNull] Uri url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
Log.Debug($"|Http.Get|Url <{url}>");
|
||||
return await client.GetAsync(url, completionOption, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchrously send an HTTP request.
|
||||
/// </summary>
|
||||
public static async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token = default)
|
||||
public static async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken token = default)
|
||||
{
|
||||
return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
return await client.SendAsync(request, completionOption, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,95 +1,68 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using BitFaster.Caching.Lfu;
|
||||
|
||||
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
|
||||
{
|
||||
private const int MaxCached = 50;
|
||||
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>();
|
||||
private const int permissibleFactor = 2;
|
||||
private SemaphoreSlim semaphore = new(1, 1);
|
||||
private const int MaxCached = 150;
|
||||
|
||||
public void Initialization(Dictionary<string, 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);
|
||||
}
|
||||
}
|
||||
|
||||
public ImageSource this[string path]
|
||||
public ImageSource this[string path, bool isFullImage = false]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Data.TryGetValue(path, out var value))
|
||||
{
|
||||
value.usage++;
|
||||
return value.imageSource;
|
||||
}
|
||||
|
||||
return null;
|
||||
return CacheManager.TryGet((path, isFullImage), out var value) ? value : null;
|
||||
}
|
||||
set
|
||||
{
|
||||
Data.AddOrUpdate(
|
||||
path,
|
||||
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();
|
||||
}
|
||||
}
|
||||
CacheManager.AddOrUpdate((path, isFullImage), value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
public async ValueTask<ImageSource> GetOrAddAsync(string key,
|
||||
Func<(string, bool), Task<ImageSource>> valueFactory,
|
||||
bool isFullImage = false)
|
||||
{
|
||||
return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null;
|
||||
return await CacheManager.GetOrAddAsync((key, isFullImage), valueFactory);
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key, bool isFullImage)
|
||||
{
|
||||
return CacheManager.TryGet((key, isFullImage), out _);
|
||||
}
|
||||
|
||||
public bool TryGetValue(string key, bool isFullImage, out ImageSource image)
|
||||
{
|
||||
if (CacheManager.TryGet((key, isFullImage), out var value))
|
||||
{
|
||||
image = value;
|
||||
return image != null;
|
||||
}
|
||||
|
||||
|
||||
image = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public int CacheSize()
|
||||
{
|
||||
return Data.Count;
|
||||
return CacheManager.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -97,7 +70,14 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,77 +3,90 @@ using System.Collections.Concurrent;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using static Flow.Launcher.Infrastructure.Http.Http;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
public static class ImageLoader
|
||||
{
|
||||
private static readonly ImageCache ImageCache = new();
|
||||
private static BinaryStorage<Dictionary<string, 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 IImageHashGenerator _hashGenerator;
|
||||
private static readonly bool EnableImageHash = true;
|
||||
public static ImageSource DefaultImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||
public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon));
|
||||
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
||||
public const int SmallIconSize = 64;
|
||||
public const int FullIconSize = 256;
|
||||
|
||||
|
||||
private static readonly string[] ImageExtensions =
|
||||
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
||||
|
||||
public static async Task InitializeAsync()
|
||||
{
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".ico"
|
||||
};
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
_storage = new BinaryStorage<Dictionary<string, int>>("Image");
|
||||
_storage = new BinaryStorage<List<(string, bool)>>("Image");
|
||||
_hashGenerator = new ImageHashGenerator();
|
||||
|
||||
var usage = LoadStorageToConcurrentDictionary();
|
||||
var usage = await LoadStorageToConcurrentDictionaryAsync();
|
||||
|
||||
ImageCache.Initialize(usage);
|
||||
|
||||
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
|
||||
{
|
||||
ImageSource img = new BitmapImage(new Uri(icon));
|
||||
img.Freeze();
|
||||
ImageCache[icon] = img;
|
||||
ImageCache[icon, false] = img;
|
||||
}
|
||||
|
||||
_ = Task.Run(() =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
Stopwatch.Normal("|ImageLoader.Initialize|Preload images cost", () =>
|
||||
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () =>
|
||||
{
|
||||
ImageCache.Data.AsParallel().ForAll(x =>
|
||||
foreach (var (path, isFullImage) in usage)
|
||||
{
|
||||
Load(x.Key);
|
||||
});
|
||||
await LoadAsync(path, isFullImage);
|
||||
}
|
||||
});
|
||||
Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
|
||||
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.Select(x => (x.Key, x.Value.usage)).ToDictionary(x => x.Key, x => x.usage));
|
||||
await _storage.SaveAsync(ImageCache.EnumerateEntries()
|
||||
.Select(x => x.Key)
|
||||
.ToList());
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<string, int> LoadStorageToConcurrentDictionary()
|
||||
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
|
||||
{
|
||||
lock (_storage)
|
||||
await storageLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var loaded = _storage.TryLoad(new Dictionary<string, int>());
|
||||
|
||||
return new ConcurrentDictionary<string, int>(loaded);
|
||||
return await _storage.TryLoadAsync(new List<(string, bool)>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,11 +108,12 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
Folder,
|
||||
Data,
|
||||
ImageFile,
|
||||
FullImageFile,
|
||||
Error,
|
||||
Cache
|
||||
}
|
||||
|
||||
private static ImageResult LoadInternal(string path, bool loadFullImage = false)
|
||||
private static async ValueTask<ImageResult> LoadInternalAsync(string path, bool loadFullImage = false)
|
||||
{
|
||||
ImageResult imageResult;
|
||||
|
||||
|
|
@ -107,26 +121,33 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return new ImageResult(ImageCache[Constant.MissingImgIcon], ImageType.Error);
|
||||
}
|
||||
if (ImageCache.ContainsKey(path))
|
||||
{
|
||||
return new ImageResult(ImageCache[path], ImageType.Cache);
|
||||
return new ImageResult(MissingImage, ImageType.Error);
|
||||
}
|
||||
|
||||
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
// extra scope for use of same variable name
|
||||
{
|
||||
if (ImageCache.TryGetValue(path, loadFullImage, out var imageSource))
|
||||
{
|
||||
return new ImageResult(imageSource, ImageType.Cache);
|
||||
}
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult)
|
||||
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
var image = await LoadRemoteImageAsync(loadFullImage, uriResult);
|
||||
ImageCache[path, loadFullImage] = image;
|
||||
return new ImageResult(image, ImageType.ImageFile);
|
||||
}
|
||||
|
||||
if (path.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var imageSource = new BitmapImage(new Uri(path));
|
||||
imageSource.Freeze();
|
||||
return new ImageResult(imageSource, ImageType.Data);
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(path))
|
||||
{
|
||||
path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path));
|
||||
}
|
||||
|
||||
imageResult = GetThumbnailResult(ref path, loadFullImage);
|
||||
imageResult = await Task.Run(() => GetThumbnailResult(ref path, loadFullImage));
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
|
|
@ -140,8 +161,8 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e);
|
||||
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2);
|
||||
|
||||
ImageSource image = ImageCache[Constant.MissingImgIcon];
|
||||
ImageCache[path] = image;
|
||||
ImageSource image = ImageCache[Constant.MissingImgIcon, false];
|
||||
ImageCache[path, false] = image;
|
||||
imageResult = new ImageResult(image, ImageType.Error);
|
||||
}
|
||||
}
|
||||
|
|
@ -149,6 +170,29 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return imageResult;
|
||||
}
|
||||
|
||||
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
||||
{
|
||||
// Download image from url
|
||||
await using var resp = await GetStreamAsync(uriResult);
|
||||
await using var buffer = new MemoryStream();
|
||||
await resp.CopyToAsync(buffer);
|
||||
buffer.Seek(0, SeekOrigin.Begin);
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
if (!loadFullImage)
|
||||
{
|
||||
image.DecodePixelHeight = SmallIconSize;
|
||||
image.DecodePixelWidth = SmallIconSize;
|
||||
}
|
||||
|
||||
image.StreamSource = buffer;
|
||||
image.EndInit();
|
||||
image.StreamSource = null;
|
||||
image.Freeze();
|
||||
return image;
|
||||
}
|
||||
|
||||
private static ImageResult GetThumbnailResult(ref string path, bool loadFullImage = false)
|
||||
{
|
||||
ImageSource image;
|
||||
|
|
@ -157,8 +201,8 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
if (Directory.Exists(path))
|
||||
{
|
||||
/* Directories can also have thumbnails instead of shell icons.
|
||||
* Generating thumbnails for a bunch of folder results while scrolling
|
||||
* could have a big impact on performance and Flow.Launcher responsibility.
|
||||
* Generating thumbnails for a bunch of folder results while scrolling
|
||||
* could have a big impact on performance and Flow.Launcher responsibility.
|
||||
* - Solution: just load the icon
|
||||
*/
|
||||
type = ImageType.Folder;
|
||||
|
|
@ -172,13 +216,22 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
type = ImageType.ImageFile;
|
||||
if (loadFullImage)
|
||||
{
|
||||
image = LoadFullImage(path);
|
||||
try
|
||||
{
|
||||
image = LoadFullImage(path);
|
||||
type = ImageType.FullImageFile;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
image = Image;
|
||||
type = ImageType.Error;
|
||||
}
|
||||
}
|
||||
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
|
||||
* be the case in many situations while testing.
|
||||
* be the case in many situations while testing.
|
||||
* - Solution: explicitly pass the ThumbnailOnly flag
|
||||
*/
|
||||
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
|
||||
|
|
@ -187,12 +240,12 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
else
|
||||
{
|
||||
type = ImageType.File;
|
||||
image = GetThumbnail(path, ThumbnailOptions.None);
|
||||
image = GetThumbnail(path, ThumbnailOptions.None, loadFullImage ? FullIconSize : SmallIconSize);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
image = ImageCache[Constant.MissingImgIcon];
|
||||
image = ImageCache[Constant.MissingImgIcon, false];
|
||||
path = Constant.MissingImgIcon;
|
||||
}
|
||||
|
||||
|
|
@ -204,43 +257,52 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return new ImageResult(image, type);
|
||||
}
|
||||
|
||||
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly)
|
||||
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly,
|
||||
int size = SmallIconSize)
|
||||
{
|
||||
return WindowsThumbnailProvider.GetThumbnail(
|
||||
path,
|
||||
Constant.ThumbnailSize,
|
||||
Constant.ThumbnailSize,
|
||||
size,
|
||||
size,
|
||||
option);
|
||||
}
|
||||
|
||||
public static bool CacheContainImage(string path)
|
||||
public static bool CacheContainImage(string path, bool loadFullImage = false)
|
||||
{
|
||||
return ImageCache.ContainsKey(path) && ImageCache[path] != null;
|
||||
return ImageCache.ContainsKey(path, loadFullImage);
|
||||
}
|
||||
|
||||
public static ImageSource Load(string path, bool loadFullImage = false)
|
||||
public static bool TryGetValue(string path, bool loadFullImage, out ImageSource image)
|
||||
{
|
||||
var imageResult = LoadInternal(path, loadFullImage);
|
||||
return ImageCache.TryGetValue(path, loadFullImage, out image);
|
||||
}
|
||||
|
||||
public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false)
|
||||
{
|
||||
var imageResult = await LoadInternalAsync(path, loadFullImage);
|
||||
|
||||
var img = imageResult.ImageSource;
|
||||
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
|
||||
{ // we need to get image hash
|
||||
{
|
||||
// we need to get image hash
|
||||
string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null;
|
||||
if (hash != null)
|
||||
{
|
||||
|
||||
if (GuidToKey.TryGetValue(hash, out string key))
|
||||
{ // image already exists
|
||||
img = ImageCache[key] ?? img;
|
||||
{
|
||||
// image already exists
|
||||
img = ImageCache[key, loadFullImage] ?? img;
|
||||
}
|
||||
else
|
||||
{ // new guid
|
||||
{
|
||||
// new guid
|
||||
|
||||
GuidToKey[hash] = path;
|
||||
}
|
||||
}
|
||||
|
||||
// update cache
|
||||
ImageCache[path] = img;
|
||||
ImageCache[path, loadFullImage] = img;
|
||||
}
|
||||
|
||||
return img;
|
||||
|
|
@ -254,6 +316,32 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
image.UriSource = new Uri(path);
|
||||
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
image.EndInit();
|
||||
|
||||
if (image.PixelWidth > 320)
|
||||
{
|
||||
BitmapImage resizedWidth = new BitmapImage();
|
||||
resizedWidth.BeginInit();
|
||||
resizedWidth.CacheOption = BitmapCacheOption.OnLoad;
|
||||
resizedWidth.UriSource = new Uri(path);
|
||||
resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
resizedWidth.DecodePixelWidth = 320;
|
||||
resizedWidth.EndInit();
|
||||
|
||||
if (resizedWidth.PixelHeight > 320)
|
||||
{
|
||||
BitmapImage resizedHeight = new BitmapImage();
|
||||
resizedHeight.BeginInit();
|
||||
resizedHeight.CacheOption = BitmapCacheOption.OnLoad;
|
||||
resizedHeight.UriSource = new Uri(path);
|
||||
resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
resizedHeight.DecodePixelHeight = 320;
|
||||
resizedHeight.EndInit();
|
||||
return resizedHeight;
|
||||
}
|
||||
|
||||
return resizedWidth;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.Graphics.Gdi;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// Subclass of <see cref="Windows.Win32.UI.Shell.SIIGBF"/>
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ThumbnailOptions
|
||||
{
|
||||
|
|
@ -22,91 +29,13 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
{
|
||||
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
|
||||
|
||||
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern int SHCreateItemFromParsingName(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string path,
|
||||
IntPtr pbc,
|
||||
ref Guid riid,
|
||||
[MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool DeleteObject(IntPtr hObject);
|
||||
|
||||
[ComImport]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
|
||||
internal interface IShellItem
|
||||
{
|
||||
void BindToHandler(IntPtr pbc,
|
||||
[MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
|
||||
[MarshalAs(UnmanagedType.LPStruct)]Guid riid,
|
||||
out IntPtr ppv);
|
||||
|
||||
void GetParent(out IShellItem ppsi);
|
||||
void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
|
||||
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
||||
void Compare(IShellItem psi, uint hint, out int piOrder);
|
||||
};
|
||||
|
||||
internal enum SIGDN : uint
|
||||
{
|
||||
NORMALDISPLAY = 0,
|
||||
PARENTRELATIVEPARSING = 0x80018001,
|
||||
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
|
||||
DESKTOPABSOLUTEPARSING = 0x80028000,
|
||||
PARENTRELATIVEEDITING = 0x80031001,
|
||||
DESKTOPABSOLUTEEDITING = 0x8004c000,
|
||||
FILESYSPATH = 0x80058000,
|
||||
URL = 0x80068000
|
||||
}
|
||||
|
||||
internal enum HResult
|
||||
{
|
||||
Ok = 0x0000,
|
||||
False = 0x0001,
|
||||
InvalidArguments = unchecked((int)0x80070057),
|
||||
OutOfMemory = unchecked((int)0x8007000E),
|
||||
NoInterface = unchecked((int)0x80004002),
|
||||
Fail = unchecked((int)0x80004005),
|
||||
ExtractionFailed = unchecked((int)0x8004B200),
|
||||
ElementNotFound = unchecked((int)0x80070490),
|
||||
TypeElementNotFound = unchecked((int)0x8002802B),
|
||||
NoObject = unchecked((int)0x800401E5),
|
||||
Win32ErrorCanceled = 1223,
|
||||
Canceled = unchecked((int)0x800704C7),
|
||||
ResourceInUse = unchecked((int)0x800700AA),
|
||||
AccessDenied = unchecked((int)0x80030005)
|
||||
}
|
||||
|
||||
[ComImportAttribute()]
|
||||
[GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
|
||||
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IShellItemImageFactory
|
||||
{
|
||||
[PreserveSig]
|
||||
HResult GetImage(
|
||||
[In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
|
||||
[In] ThumbnailOptions flags,
|
||||
[Out] out IntPtr phbm);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct NativeSize
|
||||
{
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
public int Width { set { width = value; } }
|
||||
public int Height { set { height = value; } }
|
||||
};
|
||||
private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID;
|
||||
|
||||
private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200;
|
||||
|
||||
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -115,39 +44,61 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
finally
|
||||
{
|
||||
// delete HBitmap to avoid memory leaks
|
||||
DeleteObject(hBitmap);
|
||||
PInvoke.DeleteObject(hBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
IShellItem nativeShellItem;
|
||||
Guid shellItem2Guid = new Guid(IShellItem2Guid);
|
||||
int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
|
||||
|
||||
if (retCode != 0)
|
||||
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
var retCode = PInvoke.SHCreateItemFromParsingName(
|
||||
fileName,
|
||||
null,
|
||||
GUID_IShellItem,
|
||||
out var nativeShellItem);
|
||||
|
||||
if (retCode != HRESULT.S_OK)
|
||||
throw Marshal.GetExceptionForHR(retCode);
|
||||
|
||||
NativeSize nativeSize = new NativeSize
|
||||
if (nativeShellItem is not IShellItemImageFactory imageFactory)
|
||||
{
|
||||
Width = width,
|
||||
Height = height
|
||||
};
|
||||
|
||||
IntPtr hBitmap;
|
||||
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap);
|
||||
|
||||
// if extracting image thumbnail and failed, extract shell icon
|
||||
if (options == ThumbnailOptions.ThumbnailOnly && hr == HResult.ExtractionFailed)
|
||||
{
|
||||
hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, out hBitmap);
|
||||
Marshal.ReleaseComObject(nativeShellItem);
|
||||
nativeShellItem = null;
|
||||
throw new InvalidOperationException("Failed to get IShellItemImageFactory");
|
||||
}
|
||||
|
||||
Marshal.ReleaseComObject(nativeShellItem);
|
||||
SIZE size = new SIZE
|
||||
{
|
||||
cx = width,
|
||||
cy = height
|
||||
};
|
||||
|
||||
if (hr == HResult.Ok) return hBitmap;
|
||||
HBITMAP hBitmap = default;
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
imageFactory.GetImage(size, (SIIGBF)options, &hBitmap);
|
||||
}
|
||||
catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly)
|
||||
{
|
||||
// Fallback to IconOnly if ThumbnailOnly fails
|
||||
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
|
||||
}
|
||||
catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly)
|
||||
{
|
||||
// Fallback to IconOnly if files cannot be found
|
||||
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (nativeShellItem != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(nativeShellItem);
|
||||
}
|
||||
}
|
||||
|
||||
throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr));
|
||||
return hBitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@ using NLog;
|
|||
using NLog.Config;
|
||||
using NLog.Targets;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using JetBrains.Annotations;
|
||||
using NLog.Fluent;
|
||||
using NLog.Targets.Wrappers;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Logger
|
||||
{
|
||||
|
|
@ -51,17 +48,45 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
configuration.AddTarget("file", fileTargetASyncWrapper);
|
||||
configuration.AddTarget("debug", debugTarget);
|
||||
|
||||
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper)
|
||||
{
|
||||
RuleName = "file"
|
||||
};
|
||||
#if DEBUG
|
||||
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper);
|
||||
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget);
|
||||
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget)
|
||||
{
|
||||
RuleName = "debug"
|
||||
};
|
||||
configuration.LoggingRules.Add(debugRule);
|
||||
#else
|
||||
var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
|
||||
#endif
|
||||
configuration.LoggingRules.Add(fileRule);
|
||||
LogManager.Configuration = configuration;
|
||||
}
|
||||
|
||||
public static void SetLogLevel(LOGLEVEL level)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case LOGLEVEL.DEBUG:
|
||||
UseDebugLogLevel();
|
||||
break;
|
||||
default:
|
||||
UseInfoLogLevel();
|
||||
break;
|
||||
}
|
||||
Info(nameof(Logger), $"Using log level: {level}.");
|
||||
}
|
||||
|
||||
private static void UseDebugLogLevel()
|
||||
{
|
||||
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
|
||||
}
|
||||
|
||||
private static void UseInfoLogLevel()
|
||||
{
|
||||
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
|
||||
}
|
||||
|
||||
private static void LogFaultyFormat(string message)
|
||||
{
|
||||
var logger = LogManager.GetLogger("FaultyLogger");
|
||||
|
|
@ -76,7 +101,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
return valid;
|
||||
}
|
||||
|
||||
|
||||
public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
exception = exception.Demystify();
|
||||
|
|
@ -115,8 +139,6 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
{
|
||||
var logger = LogManager.GetLogger(classAndMethod);
|
||||
|
||||
var messageBuilder = new StringBuilder();
|
||||
|
||||
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>
|
||||
public static void Exception(string message, System.Exception e)
|
||||
{
|
||||
|
|
@ -158,7 +181,7 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
#endif
|
||||
}
|
||||
|
||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
||||
/// Example: "|ClassName.MethodName|Message"
|
||||
public static void Error(string message)
|
||||
{
|
||||
LogInternal(message, LogLevel.Error);
|
||||
|
|
@ -183,7 +206,7 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
LogInternal(LogLevel.Debug, className, message, methodName);
|
||||
}
|
||||
|
||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
||||
/// Example: "|ClassName.MethodName|Message""
|
||||
public static void Debug(string message)
|
||||
{
|
||||
LogInternal(message, LogLevel.Debug);
|
||||
|
|
@ -194,7 +217,7 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
LogInternal(LogLevel.Info, className, message, methodName);
|
||||
}
|
||||
|
||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
||||
/// Example: "|ClassName.MethodName|Message"
|
||||
public static void Info(string message)
|
||||
{
|
||||
LogInternal(message, LogLevel.Info);
|
||||
|
|
@ -205,10 +228,16 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
LogInternal(LogLevel.Warn, className, message, methodName);
|
||||
}
|
||||
|
||||
/// <param name="message">example: "|prefix|unprefixed" </param>
|
||||
/// Example: "|ClassName.MethodName|Message"
|
||||
public static void Warn(string message)
|
||||
{
|
||||
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 Flow.Launcher.Infrastructure.UserSettings;
|
||||
using ToolGood.Words.Pinyin;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
|
|
@ -129,7 +130,12 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,115 +4,78 @@ using System.Reflection;
|
|||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using MemoryPack;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
#pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete.
|
||||
/// <summary>
|
||||
/// Stroage object using binary data
|
||||
/// Normally, it has better performance, but not readable
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable
|
||||
/// https://github.com/Cysharp/MemoryPack
|
||||
/// </remarks>
|
||||
public class BinaryStorage<T>
|
||||
{
|
||||
const string DirectoryName = "Cache";
|
||||
|
||||
const string FileSuffix = ".cache";
|
||||
|
||||
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);
|
||||
|
||||
const string fileSuffix = ".cache";
|
||||
FilePath = Path.Combine(directoryPath, $"{filename}{fileSuffix}");
|
||||
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||
}
|
||||
|
||||
public string FilePath { get; }
|
||||
|
||||
public T TryLoad(T defaultData)
|
||||
public async ValueTask<T> TryLoadAsync(T defaultData)
|
||||
{
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
if (new FileInfo(FilePath).Length == 0)
|
||||
{
|
||||
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>");
|
||||
Save(defaultData);
|
||||
await SaveAsync(defaultData);
|
||||
return defaultData;
|
||||
}
|
||||
|
||||
using (var stream = new FileStream(FilePath, FileMode.Open))
|
||||
{
|
||||
var d = Deserialize(stream, defaultData);
|
||||
return d;
|
||||
}
|
||||
await using var stream = new FileStream(FilePath, FileMode.Open);
|
||||
var d = await DeserializeAsync(stream, defaultData);
|
||||
return d;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data");
|
||||
Save(defaultData);
|
||||
await SaveAsync(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
|
||||
{
|
||||
var t = ((T)binaryFormatter.Deserialize(stream)).NonNull();
|
||||
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
|
||||
return t;
|
||||
}
|
||||
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;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
|
||||
}
|
||||
}
|
||||
|
||||
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
|
||||
public async ValueTask SaveAsync(T data)
|
||||
{
|
||||
Assembly ayResult = null;
|
||||
string sShortAssemblyName = args.Name.Split(',')[0];
|
||||
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);
|
||||
}
|
||||
}
|
||||
await using var stream = new FileStream(FilePath, FileMode.Create);
|
||||
await MemoryPackSerializer.SerializeAsync(stream, data);
|
||||
}
|
||||
}
|
||||
#pragma warning restore SYSLIB0011
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
||||
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 { }
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
|
|
@ -11,62 +13,158 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
/// </summary>
|
||||
public class JsonStorage<T> where T : new()
|
||||
{
|
||||
protected T _data;
|
||||
protected T? Data;
|
||||
|
||||
// need a new directory name
|
||||
public const string DirectoryName = "Settings";
|
||||
public const string FileSuffix = ".json";
|
||||
public string FilePath { get; set; }
|
||||
public string DirectoryPath { get; set; }
|
||||
|
||||
protected string FilePath { get; init; } = null!;
|
||||
|
||||
public T Load()
|
||||
private string TempFilePath => $"{FilePath}.tmp";
|
||||
|
||||
private string BackupFilePath => $"{FilePath}.bak";
|
||||
|
||||
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))
|
||||
{
|
||||
var serialized = File.ReadAllText(FilePath);
|
||||
if (!string.IsNullOrWhiteSpace(serialized))
|
||||
serialized = await File.ReadAllTextAsync(FilePath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(serialized))
|
||||
{
|
||||
try
|
||||
{
|
||||
Deserialize(serialized);
|
||||
Data = JsonSerializer.Deserialize<T>(serialized) ?? await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
else
|
||||
catch (JsonException)
|
||||
{
|
||||
LoadDefault();
|
||||
Data = await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadDefault();
|
||||
Data = await LoadBackupOrDefaultAsync();
|
||||
}
|
||||
return _data.NonNull();
|
||||
|
||||
return Data.NonNull();
|
||||
}
|
||||
|
||||
private void Deserialize(string serialized)
|
||||
private async ValueTask<T> LoadBackupOrDefaultAsync()
|
||||
{
|
||||
var backup = await TryLoadBackupAsync();
|
||||
|
||||
return backup ?? LoadDefault();
|
||||
}
|
||||
|
||||
private async ValueTask<T?> TryLoadBackupAsync()
|
||||
{
|
||||
if (!File.Exists(BackupFilePath))
|
||||
return default;
|
||||
|
||||
try
|
||||
{
|
||||
_data = JsonSerializer.Deserialize<T>(serialized);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
LoadDefault();
|
||||
Log.Exception($"|JsonStorage.Deserialize|Deserialize error for json <{FilePath}>", e);
|
||||
}
|
||||
await using var source = File.OpenRead(BackupFilePath);
|
||||
var data = await JsonSerializer.DeserializeAsync<T>(source) ?? default;
|
||||
|
||||
if (_data == null)
|
||||
if (data != null)
|
||||
RestoreBackup();
|
||||
|
||||
return data;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
LoadDefault();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadDefault()
|
||||
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()
|
||||
{
|
||||
string? serialized = null;
|
||||
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
serialized = File.ReadAllText(FilePath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(serialized))
|
||||
{
|
||||
try
|
||||
{
|
||||
Data = JsonSerializer.Deserialize<T>(serialized) ?? TryLoadBackup() ?? LoadDefault();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
Data = TryLoadBackup() ?? LoadDefault();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Data = TryLoadBackup() ?? LoadDefault();
|
||||
}
|
||||
|
||||
return Data.NonNull();
|
||||
}
|
||||
|
||||
private T LoadDefault()
|
||||
{
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
BackupOriginFile();
|
||||
}
|
||||
|
||||
_data = new T();
|
||||
Save();
|
||||
return new T();
|
||||
}
|
||||
|
||||
private T? TryLoadBackup()
|
||||
{
|
||||
if (!File.Exists(BackupFilePath))
|
||||
return default;
|
||||
|
||||
try
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
|
||||
|
||||
if (data != null)
|
||||
RestoreBackup();
|
||||
|
||||
return data;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private void BackupOriginFile()
|
||||
|
|
@ -82,13 +180,33 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
|
||||
public void Save()
|
||||
{
|
||||
string serialized = JsonSerializer.Serialize(_data, new JsonSerializerOptions() { WriteIndented = true });
|
||||
string serialized = JsonSerializer.Serialize(Data,
|
||||
new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
File.WriteAllText(FilePath, 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))
|
||||
{
|
||||
File.Move(TempFilePath, FilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var finalFilePath = new FileInfo(FilePath).LinkTarget ?? FilePath;
|
||||
File.Replace(TempFilePath, finalFilePath, BackupFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[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 class JsonStrorage<T> : JsonStorage<T> where T : new() { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
|||
|
||||
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()
|
||||
{
|
||||
// C# related, add python related below
|
||||
var dataType = typeof(T);
|
||||
var assemblyName = dataType.Assembly.GetName().Name;
|
||||
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName);
|
||||
AssemblyName = dataType.Assembly.GetName().Name;
|
||||
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName);
|
||||
Helper.ValidateDirectory(DirectoryPath);
|
||||
|
||||
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
||||
|
|
@ -18,8 +21,15 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
|
||||
public PluginJsonStorage(T data) : this()
|
||||
{
|
||||
_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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
public class StringMatcher
|
||||
{
|
||||
private readonly MatchOption _defaultMatchOption = new MatchOption();
|
||||
private readonly MatchOption _defaultMatchOption = new();
|
||||
|
||||
public SearchPrecisionScore UserSettingSearchPrecision { get; set; }
|
||||
|
||||
private readonly IAlphabet _alphabet;
|
||||
|
||||
public StringMatcher(IAlphabet alphabet = null)
|
||||
public StringMatcher(IAlphabet alphabet, Settings settings)
|
||||
{
|
||||
_alphabet = alphabet;
|
||||
UserSettingSearchPrecision = settings.QuerySearchPrecision;
|
||||
}
|
||||
|
||||
// This is a workaround to allow unit tests to set the instance
|
||||
public StringMatcher(IAlphabet alphabet)
|
||||
{
|
||||
_alphabet = alphabet;
|
||||
}
|
||||
|
||||
public static StringMatcher Instance { get; internal set; }
|
||||
|
||||
public static MatchResult FuzzySearch(string query, string stringToCompare)
|
||||
{
|
||||
return Instance.FuzzyMatch(query, stringToCompare);
|
||||
return Ioc.Default.GetRequiredService<StringMatcher>().FuzzyMatch(query, stringToCompare);
|
||||
}
|
||||
|
||||
public MatchResult FuzzyMatch(string query, string stringToCompare)
|
||||
|
|
@ -241,16 +248,16 @@ namespace Flow.Launcher.Infrastructure
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool IsAcronymChar(string stringToCompare, int compareStringIndex)
|
||||
private static bool IsAcronymChar(string stringToCompare, int compareStringIndex)
|
||||
=> char.IsUpper(stringToCompare[compareStringIndex]) ||
|
||||
compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
|
||||
char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
|
||||
|
||||
private bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
|
||||
private static bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
|
||||
=> stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;
|
||||
|
||||
// To get the index of the closest space which preceeds the first matching index
|
||||
private int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
||||
private static int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
||||
{
|
||||
var closestSpaceIndex = -1;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
using Flow.Launcher.Plugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
|
|
@ -31,5 +27,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins);
|
||||
public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins);
|
||||
|
||||
public const string PythonEnvironmentName = "Python";
|
||||
public const string NodeEnvironmentName = "Node.js";
|
||||
public const string PluginEnvironments = "Environments";
|
||||
public static readonly string PluginEnvironmentsPath = Path.Combine(DataDirectory(), PluginEnvironments);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
public enum ProxyProperty
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue