diff --git a/.cm/gitstream.cm b/.cm/gitstream.cm new file mode 100644 index 000000000..767982e3b --- /dev/null +++ b/.cm/gitstream.cm @@ -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/()|!\[image\]\(.*github\.com.*\)/) }} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..e1b8ed79e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,146 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### +# All files +[*] +indent_style = space + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 2 +end_of_line = crlf +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_diagnostic.CA1416.severity = silent +############################### +# C# Coding Conventions # +############################### +[*.cs] +dotnet_diagnostics.VSTHRD200.severity = none # VSTHRD200: Use "Async" suffix for async methods +dotnet_analyzer_diagnostic.VSTHRD200.severity = none # VSTHRD200: Use "Async" suffix for async methods +# var preferences +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = 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 +############################### +# VB Coding Conventions # +############################### +[*.vb] +# Modifier preferences +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 23ced593e..000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: "\U0001F41E Bug report" -about: Create a bug report to help us improve Flow Launcher -title: "[Describe Your Bug]" -labels: 'bug' -assignees: '' - ---- - -**Describe the bug/issue** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. ... -2. ... -3. ... - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Your System** -``` -Windows build number: (run "ver" at a command prompt) -Flow Launcher version: (Settings => About) -``` -**Flow Launcher Error Log** - diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml new file mode 100644 index 000000000..294c06fc1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -0,0 +1,80 @@ +name: "\U0001F41E Bug Report" +description: Create a bug report to help us improve Flow Launcher +title: "BUG: " +labels: ["bug"] + +body: + - type: markdown + attributes: + value: Thanks for taking the time to fill out this bug report! + + - type: checkboxes + attributes: + label: Checks + options: + - label: > + I have checked that this issue has not already been reported. + - label: > + I am using the latest version of Flow Launcher. + + - type: textarea + attributes: + label: Problem Description + description: A clear and concise description of what the problem is. + validations: + required: true + + - type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce the behavior. + value: > + 1. ... + + 2. ... + + 3. ... + + - type: textarea + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + + - type: input + attributes: + label: Flow Launcher Version + description: Go to "Settings" => "About". 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) 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: > + From flow type 'open log location' and find log file with the corresponding date containing the error info. + value: > +
+ + + ```shell + + + Replace this line with the important log contents. + + + ``` + +
+ + + diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md.not_used similarity index 100% rename from .github/ISSUE_TEMPLATE/discussion.md rename to .github/ISSUE_TEMPLATE/discussion.md.not_used diff --git a/.github/actions/spelling/README.md b/.github/actions/spelling/README.md new file mode 100644 index 000000000..1f699f3de --- /dev/null +++ b/.github/actions/spelling/README.md @@ -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. diff --git a/.github/actions/spelling/advice.md b/.github/actions/spelling/advice.md new file mode 100644 index 000000000..1004eeaa6 --- /dev/null +++ b/.github/actions/spelling/advice.md @@ -0,0 +1,25 @@ + +
If the flagged items are :exploding_head: false positives + +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. + +
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt new file mode 100644 index 000000000..a36a6af3e --- /dev/null +++ b/.github/actions/spelling/allow.txt @@ -0,0 +1,8 @@ +github +https +ssh +ubuntu +runcount +Firefox +Português +Português (Brasil) diff --git a/.github/actions/spelling/candidate.patterns b/.github/actions/spelling/candidate.patterns new file mode 100644 index 000000000..d244bb891 --- /dev/null +++ b/.github/actions/spelling/candidate.patterns @@ -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 +(?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-)/ diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt new file mode 100644 index 000000000..224014eba --- /dev/null +++ b/.github/actions/spelling/excludes.txt @@ -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$ diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt new file mode 100644 index 000000000..6ba41e410 --- /dev/null +++ b/.github/actions/spelling/expect.txt @@ -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 diff --git a/.github/actions/spelling/line_forbidden.patterns b/.github/actions/spelling/line_forbidden.patterns new file mode 100644 index 000000000..7341d9b73 --- /dev/null +++ b/.github/actions/spelling/line_forbidden.patterns @@ -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 diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt new file mode 100644 index 000000000..f29f57ad5 --- /dev/null +++ b/.github/actions/spelling/patterns.txt @@ -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+[^>]*> +]*> + +#http/https +(?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|] + +# UWP +[Uu][Ww][Pp] + +# version suffix v# +(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_])) diff --git a/.github/actions/spelling/reject.txt b/.github/actions/spelling/reject.txt new file mode 100644 index 000000000..b5a6d3680 --- /dev/null +++ b/.github/actions/spelling/reject.txt @@ -0,0 +1,10 @@ +^attache$ +benefitting +occurences? +^dependan.* +^oer$ +Sorce +^[Ss]pae.* +^untill$ +^untilling$ +^wether.* diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..da4231f74 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "nuget" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" + open-pull-requests-limit: 3 + ignore: + - dependency-name: "squirrel-windows" + reviewers: + - "jjw24" + - "taooceros" + - "JohnTheGr8" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml new file mode 100644 index 000000000..5556c5ea8 --- /dev/null +++ b/.github/pr-labeler.yml @@ -0,0 +1,31 @@ +# The bot always updates the labels, add/remove as necessary [default: false] +alwaysReplace: false +# Treats the text and labels as case sensitive [default: true] +caseSensitive: false +# Array of labels to be applied to the PR [default: []] +customLabels: + # Finds the `text` within the PR title and body and applies the `label` + - text: 'bug' + label: 'bug' + - text: 'fix' + label: 'bug' + - text: 'dependabot' + label: 'bug' + - text: 'New Crowdin updates' + label: 'bug' + - text: 'New Crowdin updates' + label: 'kind/i18n' + - text: 'feature' + label: 'enhancement' + - text: 'add new' + label: 'enhancement' + - text: 'refactor' + label: 'enhancement' + - text: 'refactor' + label: 'Code Refactor' +# Search the body of the PR for the `text` [default: true] +searchBody: true +# Search the title of the PR for the `text` [default: true] +searchTitle: true +# Search for whole words only [default: false] +wholeWords: false diff --git a/.github/workflows/default_plugins.yml b/.github/workflows/default_plugins.yml new file mode 100644 index 000000000..85acafae1 --- /dev/null +++ b/.github/workflows/default_plugins.yml @@ -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 }} diff --git a/.github/workflows/gitstream.yml b/.github/workflows/gitstream.yml new file mode 100644 index 000000000..0916572df --- /dev/null +++ b/.github/workflows/gitstream.yml @@ -0,0 +1,49 @@ +# Code generated by gitStream GitHub app - DO NOT EDIT + +name: gitStream workflow automation +run-name: | + /:\ gitStream: PR #${{ fromJSON(fromJSON(github.event.inputs.client_payload)).pullRequestNumber }} from ${{ github.event.inputs.full_repository }} + +on: + workflow_dispatch: + inputs: + client_payload: + description: The Client payload + required: true + full_repository: + description: the repository name include the owner in `owner/repo_name` format + required: true + head_ref: + description: the head sha + required: true + base_ref: + description: the base ref + required: true + installation_id: + description: the installation id + required: false + resolver_url: + description: the resolver url to pass results to + required: true + resolver_token: + description: Optional resolver token for resolver service + required: false + default: '' + +jobs: + gitStream: + timeout-minutes: 5 + runs-on: ubuntu-latest + name: gitStream workflow automation + steps: + - name: Evaluate Rules + uses: linear-b/gitstream-github-action@v2 + id: rules-engine + with: + full_repository: ${{ github.event.inputs.full_repository }} + head_ref: ${{ github.event.inputs.head_ref }} + base_ref: ${{ github.event.inputs.base_ref }} + client_payload: ${{ github.event.inputs.client_payload }} + installation_id: ${{ github.event.inputs.installation_id }} + resolver_url: ${{ github.event.inputs.resolver_url }} + resolver_token: ${{ github.event.inputs.resolver_token }} diff --git a/.github/workflows/pr_assignee.yml b/.github/workflows/pr_assignee.yml new file mode 100644 index 000000000..5be603df6 --- /dev/null +++ b/.github/workflows/pr_assignee.yml @@ -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 diff --git a/.github/workflows/pr_milestone.yml b/.github/workflows/pr_milestone.yml new file mode 100644 index 000000000..e2365f554 --- /dev/null +++ b/.github/workflows/pr_milestone.yml @@ -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 diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml new file mode 100644 index 000000000..7aaa9296a --- /dev/null +++ b/.github/workflows/spelling.yml @@ -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 }}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..719f2556e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,31 @@ +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +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 + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + 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: ${{ env.exempt-issue-labels }} + exempt-pr-labels: 'keep-fresh,awaiting-approval,work-in-progress' diff --git a/.github/workflows/top-ranking-issues.yml b/.github/workflows/top-ranking-issues.yml new file mode 100644 index 000000000..bddca4741 --- /dev/null +++ b/.github/workflows/top-ranking-issues.yml @@ -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 diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 000000000..7040ee606 --- /dev/null +++ b/.github/workflows/winget.yml @@ -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 }} diff --git a/Deploy/local_build.ps1 b/Deploy/local_build.ps1 deleted file mode 100644 index 9dd7582b1..000000000 --- a/Deploy/local_build.ps1 +++ /dev/null @@ -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 \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..fa499273c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + true + + \ No newline at end of file diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index 5bca087b8..2b570d2c0 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -1,4 +1,4 @@ -using Microsoft.Win32; +using Microsoft.Win32; using Squirrel; using System; using System.IO; @@ -9,16 +9,20 @@ 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(); + /// /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish /// /// - private UpdateManager NewUpdateManager() + private static UpdateManager NewUpdateManager() { var applicationFolderName = Constant.ApplicationDirectory .Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None) @@ -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,54 +68,48 @@ 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); } } public void RemoveShortcuts() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); + portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup); } public void RemoveUninstallerEntry() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.RemoveUninstallerRegistryEntry(); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.RemoveUninstallerRegistryEntry(); } 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() { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false); - } + using var portabilityUpdater = NewUpdateManager(); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); + portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false); } public void CreateUninstallerEntry() @@ -125,18 +123,14 @@ namespace Flow.Launcher.Core.Configuration subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String); } - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.CreateUninstallerRegistryEntry(); - } + using var portabilityUpdater = NewUpdateManager(); + _ = portabilityUpdater.CreateUninstallerRegistryEntry(); } - internal void IndicateDeletion(string filePathTodelete) + private static void IndicateDeletion(string filePathTodelete) { var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile); - using (var _ = File.CreateText(deleteFilePath)) - { - } + using var _ = File.CreateText(deleteFilePath); } /// @@ -157,13 +151,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 +166,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 +180,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; diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs new file mode 100644 index 000000000..e9713564e --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs @@ -0,0 +1,69 @@ +using Flow.Launcher.Infrastructure.Http; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Plugin; +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 plugins = new(); + + private static JsonSerializerOptions PluginStoreItemSerializationOption = new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }; + + /// + /// Fetch and deserialize the contents of a plugins.json file found at . + /// We use conditional http requests to keep repeat requests fast. + /// + /// + /// 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 + /// + public async Task> 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>(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}"); + } + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs new file mode 100644 index 000000000..1f23c2f66 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins +{ + /// + /// Describes a store of community-made plugins. + /// The provided URLs should point to a json file, whose content + /// is deserializable as a array. + /// + /// Primary URL to the manifest json file. + /// Secondary URLs to access the , for example CDN links + public record CommunityPluginStore(string primaryUrl, params string[] secondaryUrls) + { + private readonly List pluginSources = + secondaryUrls + .Append(primaryUrl) + .Select(url => new CommunityPluginSource(url)) + .ToList(); + + public async Task> 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(); + + // 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; + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs new file mode 100644 index 000000000..bbb6cf638 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Forms; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + public abstract class AbstractPluginEnvironment + { + protected readonly IPublicAPI API = Ioc.Default.GetRequiredService(); + + 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 PluginMetadataList; + + internal PluginsSettings PluginSettings; + + internal AbstractPluginEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) + { + PluginMetadataList = pluginMetadataList; + PluginSettings = pluginSettings; + } + + internal IEnumerable Setup() + { + // If no plugin is using the language, return empty list + if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase))) + { + return new List(); + } + + 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( + API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"), + Language, + EnvName, + Environment.NewLine + ); + if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) + { + var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName); + + var selectedFile = GetFileFromDialog(msg, FileDialogFilter); + + if (!string.IsNullOrEmpty(selectedFile)) + { + PluginsSettingsFilePath = selectedFile; + } + // Nothing selected because user pressed cancel from the file dialog window + else + { + var forceDownloadMessage = string.Format( + API.GetTranslation("runtimeExecutableInvalidChooseDownload"), + Language, + EnvName, + Environment.NewLine + ); + + // Let users select valid path or choose to download + while (string.IsNullOrEmpty(selectedFile)) + { + if (API.ShowMsgBox(forceDownloadMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + // Continue select file + selectedFile = GetFileFromDialog(msg, FileDialogFilter); + } + else + { + // User selected no, break the loop + break; + } + } + + if (!string.IsNullOrEmpty(selectedFile)) + { + PluginsSettingsFilePath = selectedFile; + } + else + { + InstallEnvironment(); + } + } + } + else + { + InstallEnvironment(); + } + + if (FilesFolders.FileExists(PluginsSettingsFilePath)) + { + return SetPathForPluginPairs(PluginsSettingsFilePath, Language); + } + else + { + API.ShowMsgBox(string.Format(API.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(); + } + } + + 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 SetPathForPluginPairs(string filePath, string languageToSet) + { + var pluginPairs = new List(); + + foreach (var metadata in PluginMetadataList) + { + if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase)) + { + metadata.AssemblyName = string.Empty; + pluginPairs.Add(CreatePluginPair(filePath, metadata)); + } + } + + return pluginPairs; + } + + private static 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; + } + + /// + /// 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. + /// + /// + 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 = Path.Combine("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[(index + DataLocation.PluginEnvironments.Length)..]; + return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}"; + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs new file mode 100644 index 000000000..b67059b1b --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs @@ -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 pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs new file mode 100644 index 000000000..6c8c5aa57 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs @@ -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 pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs new file mode 100644 index 000000000..fab5738de --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.IO; +using Droplex; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; + +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 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 + }; + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs new file mode 100644 index 000000000..4d75e1b8f --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs @@ -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 pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs new file mode 100644 index 000000000..8a4f527ba --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; +using Droplex; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; + +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 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 + }; + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs new file mode 100644 index 000000000..61fd28376 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; +using Droplex; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; + +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 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 + }; + } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/FlowPluginException.cs b/Flow.Launcher.Core/ExternalPlugins/FlowPluginException.cs new file mode 100644 index 000000000..47bc285c9 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/FlowPluginException.cs @@ -0,0 +1,24 @@ +using Flow.Launcher.Plugin; +using System; + +namespace Flow.Launcher.Core.ExternalPlugins +{ + public class FlowPluginException : Exception + { + public PluginMetadata Metadata { get; set; } + + public FlowPluginException(PluginMetadata metadata, Exception e) : base(e.Message, e) + { + Metadata = metadata; + } + + public override string ToString() + { + return $@"{Metadata.Name} Exception: +Websites: {Metadata.Website} +Author: {Metadata.Author} +Version: {Metadata.Version} +{base.ToString()}"; + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs new file mode 100644 index 000000000..44d3ef0ff --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins +{ + public static class PluginsManifest + { + 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 DateTime lastFetchedAt = DateTime.MinValue; + private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2); + + public static List UserPlugins { get; private set; } + + public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) + { + try + { + await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false); + + if (UserPlugins == null || usePrimaryUrlOnly || DateTime.Now.Subtract(lastFetchedAt) >= fetchTimeout) + { + var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).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; + + return true; + } + } + } + catch (Exception e) + { + Ioc.Default.GetRequiredService().LogException(nameof(PluginsManifest), "Http request failed", e); + } + finally + { + manifestUpdateLock.Release(); + } + + return false; + } + } +} diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index d7df11f83..e9f199d00 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -1,7 +1,7 @@ - + - net5.0-windows + net7.0-windows true true Library @@ -53,9 +53,12 @@ - - - + + + + + + diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index 13b6ac968..857122aa6 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -1,16 +1,14 @@ -using System; -using System.Diagnostics; +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) { @@ -22,38 +20,23 @@ namespace Flow.Launcher.Core.Plugin RedirectStandardOutput = true, RedirectStandardError = true }; + + // required initialisation for below request calls + _startInfo.ArgumentList.Add(string.Empty); } - protected override Task ExecuteQueryAsync(Query query, CancellationToken token) + protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - JsonRPCServerRequestModel request = new JsonRPCServerRequestModel - { - Method = "query", - Parameters = new object[] {query.Search}, - }; - - _startInfo.Arguments = $"\"{request}\""; - + // since this is not static, request strings will build up in ArgumentList if index is not specified + _startInfo.ArgumentList[0] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } - protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) + protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { - _startInfo.Arguments = $"\"{rpcRequest}\""; - return Execute(_startInfo); - } - - protected override string ExecuteContextMenu(Result selectedResult) - { - JsonRPCServerRequestModel request = new JsonRPCServerRequestModel - { - Method = "contextmenu", - Parameters = new object[] {selectedResult.ContextData}, - }; - - _startInfo.Arguments = $"\"{request}\""; - + // since this is not static, request strings will build up in ArgumentList if index is not specified + _startInfo.ArgumentList[0] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); return Execute(_startInfo); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs b/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs new file mode 100644 index 000000000..852f57b9f --- /dev/null +++ b/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs @@ -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; + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index 5232e46da..5b2a9f6cb 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -12,72 +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 Result, + IReadOnlyDictionary 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 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 Result { get; set; } - - public string DebugMessage { get; set; } - } - - public class JsonRPCRequestModel - { - public string Method { get; set; } - - public object[] Parameters { get; set; } - - private static readonly JsonSerializerOptions options = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - public override string ToString() - { - return JsonSerializer.Serialize(this, options); - } - } - - /// - /// Json RPC Request that Flow Launcher sent to client - /// - public class JsonRPCServerRequestModel : JsonRPCRequestModel - { - - } - /// /// Json RPC Request(in query response) that client sent to Flow Launcher /// - public class JsonRPCClientRequestModel : JsonRPCRequestModel - { - public bool DontHideAfterAction { get; set; } - } - + public record JsonRPCClientRequestModel( + int Id, + string Method, + object[] Parameters, + IReadOnlyDictionary Settings, + bool DontHideAfterAction = false, + JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error); + + /// /// 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 @@ -86,5 +56,7 @@ namespace Flow.Launcher.Core.Plugin public class JsonRPCResult : Result { public JsonRPCClientRequestModel JsonRPCAction { get; set; } + + public Dictionary SettingsChange { get; set; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCConfigurationModel.cs b/Flow.Launcher.Core/Plugin/JsonRPCConfigurationModel.cs new file mode 100644 index 000000000..6dc4be881 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCConfigurationModel.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace Flow.Launcher.Core.Plugin +{ + public class JsonRpcConfigurationModel + { + public List Body { get; set; } + public void Deconstruct(out List Body) + { + Body = this.Body; + } + } + + public class SettingField + { + public string Type { get; set; } + public FieldAttributes Attributes { get; set; } + public void Deconstruct(out string Type, out FieldAttributes attributes) + { + Type = this.Type; + attributes = this.Attributes; + } + } + public class FieldAttributes + { + 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 Options { get; set; } + public string DefaultValue { get; set; } + public char passwordChar { get; set; } + public void Deconstruct(out string Name, out string Label, out string Description, out bool Validation, out List Options, out string DefaultValue) + { + Name = this.Name; + Label = this.Label; + Description = this.Description; + Validation = this.Validation; + Options = this.Options; + DefaultValue = this.DefaultValue; + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 431458881..b19bb6c79 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -1,17 +1,14 @@ -using Flow.Launcher.Core.Resource; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Reflection; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms; -using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Core.Resource; using Flow.Launcher.Plugin; -using JetBrains.Annotations; +using Microsoft.IO; namespace Flow.Launcher.Core.Plugin { @@ -19,38 +16,38 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu + internal abstract class JsonRPCPlugin : JsonRPCPluginBase { - protected PluginInitContext context; - public const string JsonRPC = "JsonRPC"; + public new const string JsonRPC = "JsonRPC"; - /// - /// The language this JsonRPCPlugin support - /// - public abstract string SupportedLanguage { get; set; } + private static readonly string ClassName = nameof(JsonRPCPlugin); - protected abstract Task ExecuteQueryAsync(Query query, CancellationToken token); - protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest); - protected abstract string ExecuteContextMenu(Result selectedResult); + protected abstract Task RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default); + protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default); - public List LoadContextMenus(Result selectedResult) + private static readonly RecyclableMemoryStreamManager BufferManager = new(); + + private int RequestId { get; set; } + + public override List LoadContextMenus(Result selectedResult) { - string output = ExecuteContextMenu(selectedResult); - try - { - return DeserializedResult(output); - } - catch (Exception e) - { - Log.Exception($"|JsonRPCPlugin.LoadContextMenus|Exception on result <{selectedResult}>", e); - return null; - } + var request = new JsonRPCRequestModel(RequestId++, + "context_menu", + new[] { selectedResult.ContextData }); + var output = Request(request); + return DeserializedResult(output); } private static readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true, +#pragma warning disable SYSLIB0020 + // IgnoreNullValues is obsolete, but the replacement JsonIgnoreCondition.WhenWritingNull still + // deserializes null, instead of ignoring it and leaving the default (empty list). We can change the behaviour + // to accept null and fallback to a default etc, or just keep IgnoreNullValues for now + // see: https://github.com/dotnet/runtime/issues/39152 IgnoreNullValues = true, +#pragma warning restore SYSLIB0020 // Type or member is obsolete Converters = { new JsonObjectConverter() @@ -59,14 +56,15 @@ namespace Flow.Launcher.Core.Plugin private async Task> DeserializedResultAsync(Stream output) { - if (output == Stream.Null) return null; + await using (output) + { + if (output == Stream.Null) return null; - var queryResponseModel = await - JsonSerializer.DeserializeAsync(output, options); + var queryResponseModel = + await JsonSerializer.DeserializeAsync(output, options); - await output.DisposeAsync(); - - return ParseResults(queryResponseModel); + return ParseResults(queryResponseModel); + } } private List DeserializedResult(string output) @@ -78,76 +76,40 @@ namespace Flow.Launcher.Core.Plugin return ParseResults(queryResponseModel); } - - private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) + protected override async Task ExecuteResultAsync(JsonRPCResult result) { - var results = new List(); - 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 (JsonRPCResult result in queryResponseModel.Result) + if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) { - result.Action = c => + ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], + result.JsonRPCAction.Parameters); + } + else + { + await using var actionResponse = await RequestAsync(result.JsonRPCAction); + + if (actionResponse.Length == 0) { - 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 - { - var actionResponse = ExecuteCallback(result.JsonRPCAction); - - if (string.IsNullOrEmpty(actionResponse)) - { - return !result.JsonRPCAction.DontHideAfterAction; - } - - var jsonRpcRequestModel = JsonSerializer.Deserialize(actionResponse, options); - - if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) - { - ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], - jsonRpcRequestModel.Parameters); - } - } - return !result.JsonRPCAction.DontHideAfterAction; - }; - results.Add(result); - } - - return results; - } - - private void ExecuteFlowLauncherAPI(string method, object[] parameters) - { - var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray(); - MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method, parametersTypeArray); - if (methodInfo != null) - { - try - { - methodInfo.Invoke(PluginManager.API, parameters); } - catch (Exception) + + var jsonRpcRequestModel = await + JsonSerializer.DeserializeAsync(actionResponse, options); + + if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) { -#if (DEBUG) - throw; -#endif + ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], + jsonRpcRequestModel.Parameters); } } + + return !result.JsonRPCAction.DontHideAfterAction; } /// @@ -187,20 +149,11 @@ namespace Flow.Launcher.Core.Plugin var error = standardError.ReadToEnd(); if (!string.IsNullOrEmpty(error)) { - Log.Error($"|JsonRPCPlugin.Execute|{error}"); + Context.API.LogError(ClassName, error); return string.Empty; } - Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error."); - return string.Empty; - } - - if (result.StartsWith("DEBUG:")) - { - MessageBox.Show(new Form - { - TopMost = true - }, result.Substring(6)); + Context.API.LogError(ClassName, "Empty standard output and standard error."); return string.Empty; } @@ -208,8 +161,8 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception( - $"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", + Context.API.LogException(ClassName, + $"Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", e); return string.Empty; } @@ -217,63 +170,74 @@ namespace Flow.Launcher.Core.Plugin protected async Task ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default) { - try + using var process = Process.Start(startInfo); + if (process == null) { - using var process = Process.Start(startInfo); - if (process == null) - { - Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process"); - return Stream.Null; - } - - var result = process.StandardOutput.BaseStream; - - token.ThrowIfCancellationRequested(); - - if (!process.StandardError.EndOfStream) - { - using var standardError = process.StandardError; - var error = await standardError.ReadToEndAsync(); - - if (!string.IsNullOrEmpty(error)) - { - Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}"); - return Stream.Null; - } - - Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error."); - return Stream.Null; - } - - return result; - } - catch (Exception e) - { - Log.Exception( - $"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", - e); + Context.API.LogError(ClassName, "Can't start new process"); return Stream.Null; } - } - public async Task> QueryAsync(Query query, CancellationToken token) - { - var output = await ExecuteQueryAsync(query, token); + var sourceBuffer = BufferManager.GetStream(); + using var errorBuffer = BufferManager.GetStream(); + + var sourceCopyTask = process.StandardOutput.BaseStream.CopyToAsync(sourceBuffer, token); + var errorCopyTask = process.StandardError.BaseStream.CopyToAsync(errorBuffer, token); + + await using var registeredEvent = token.Register(() => + { + try + { + if (!process.HasExited) + process.Kill(); + sourceBuffer.Dispose(); + } + catch (Exception e) + { + Context.API.LogException(ClassName, "Exception when kill process", e); + } + }); + try { - return await DeserializedResultAsync(output); + // token expire won't instantly trigger the exception, + // manually kill process at before + await process.WaitForExitAsync(token); + await Task.WhenAll(sourceCopyTask, errorCopyTask); } - catch (Exception e) + catch (OperationCanceledException) { - Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e); - return null; + await sourceBuffer.DisposeAsync(); + return Stream.Null; } + + switch (sourceBuffer.Length, errorBuffer.Length) + { + case (0, 0): + const string errorMessage = "Empty JSON-RPC Response."; + Context.API.LogWarn(ClassName, errorMessage); + break; + case (_, not 0): + throw new InvalidDataException(Encoding.UTF8.GetString(errorBuffer.ToArray())); // The process has exited with an error message + } + + sourceBuffer.Seek(0, SeekOrigin.Begin); + + return sourceBuffer; } - public virtual Task InitAsync(PluginInitContext context) + public override async Task> QueryAsync(Query query, CancellationToken token) { - this.context = context; - return Task.CompletedTask; + var request = new JsonRPCRequestModel(RequestId++, + "query", + new object[] + { + query.Search + }, + Settings?.Inner); + + var output = await RequestAsync(request, token); + + return await DeserializedResultAsync(output); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs new file mode 100644 index 000000000..df0438409 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Core.Resource; +using Flow.Launcher.Plugin; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using Control = System.Windows.Controls.Control; + +namespace Flow.Launcher.Core.Plugin +{ + /// + /// Represent the plugin that using JsonPRC + /// every JsonRPC plugin should has its own plugin instance + /// + public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + { + public const string JsonRPC = "JsonRPC"; + + protected PluginInitContext Context; + + private string SettingConfigurationPath => + Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + + private string SettingDirectory => Context.CurrentPluginMetadata.PluginSettingsDirectoryPath; + + private string SettingPath => Path.Combine(SettingDirectory, "Settings.json"); + + public abstract List 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 ExecuteResultAsync(JsonRPCResult result); + + protected JsonRPCPluginSettings Settings { get; set; } + + protected List 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(); + + 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> 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( + await File.ReadAllTextAsync(SettingConfigurationPath)); + } + + Settings ??= new JsonRPCPluginSettings + { + Configuration = configuration, SettingPath = SettingPath, API = Context.API + }; + + await Settings.InitializeAsync(); + } + + public virtual async Task InitAsync(PluginInitContext context) + { + Context = context; + await InitSettingAsync(); + } + + public void Save() + { + Settings?.Save(); + } + + public bool NeedCreateSettingPanel() + { + return Settings.NeedCreateSettingPanel(); + } + + public Control CreateSettingPanel() + { + return Settings.CreateSettingPanel(); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs new file mode 100644 index 000000000..e0a217251 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -0,0 +1,504 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Plugin; + +#nullable enable + +namespace Flow.Launcher.Core.Plugin +{ + public class JsonRPCPluginSettings + { + public required JsonRpcConfigurationModel? Configuration { get; init; } + + public required string SettingPath { get; init; } + public Dictionary SettingControls { get; } = new(); + + public IReadOnlyDictionary Inner => Settings; + protected ConcurrentDictionary Settings { get; set; } = null!; + public required IPublicAPI API { get; init; } + + private static readonly string ClassName = nameof(JsonRPCPluginSettings); + + private JsonStorage> _storage = null!; + + private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin"); + private static readonly Thickness SettingPanelItemLeftMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftMargin"); + private static readonly Thickness SettingPanelItemTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemTopBottomMargin"); + private static readonly Thickness SettingPanelItemLeftTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftTopBottomMargin"); + private static readonly double SettingPanelTextBoxMinWidth = (double)Application.Current.FindResource("SettingPanelTextBoxMinWidth"); + private static readonly double SettingPanelPathTextBoxWidth = (double)Application.Current.FindResource("SettingPanelPathTextBoxWidth"); + private static readonly double SettingPanelAreaTextBoxMinHeight = (double)Application.Current.FindResource("SettingPanelAreaTextBoxMinHeight"); + + public async Task InitializeAsync() + { + if (Settings == null) + { + _storage = new JsonStorage>(SettingPath); + Settings = await _storage.LoadAsync(); + + // Because value type of settings dictionary is object which causes them to be JsonElement when loading from json files, + // we need to convert it to the correct type + foreach (var (key, value) in Settings) + { + if (value is not JsonElement jsonElement) continue; + + Settings[key] = jsonElement.ValueKind switch + { + JsonValueKind.String => jsonElement.GetString() ?? value, + JsonValueKind.True => jsonElement.GetBoolean(), + JsonValueKind.False => jsonElement.GetBoolean(), + JsonValueKind.Null => null, + _ => value + }; + } + } + + if (Configuration == null) return; + + foreach (var (type, attributes) in Configuration.Body) + { + // Skip if the setting does not have attributes or name + if (attributes?.Name == null) continue; + + // Skip if the setting does not have attributes or name + if (!NeedSaveInSettings(type)) continue; + + // If need save in settings, we need to make sure the setting exists in the settings file + if (Settings.ContainsKey(attributes.Name)) continue; + + if (type == "checkbox") + { + // If can parse the default value to bool, use it, otherwise use false + Settings[attributes.Name] = bool.TryParse(attributes.DefaultValue, out var value) && value; + } + else + { + Settings[attributes.Name] = attributes.DefaultValue; + } + } + } + + public void UpdateSettings(IReadOnlyDictionary 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: + var text = value as string ?? string.Empty; + textBox.Dispatcher.Invoke(() => textBox.Text = text); + break; + case PasswordBox passwordBox: + var password = value as string ?? string.Empty; + passwordBox.Dispatcher.Invoke(() => passwordBox.Password = password); + break; + case ComboBox comboBox: + comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); + break; + case CheckBox checkBox: + var isChecked = value is bool boolValue + ? boolValue + // If can parse the default value to bool, use it, otherwise use false + : value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString) + && boolValueFromString; + checkBox.Dispatcher.Invoke(() =>checkBox.IsChecked = isChecked); + break; + } + } + } + + Save(); + } + + public async Task SaveAsync() + { + try + { + await _storage.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e); + } + } + + public void Save() + { + try + { + _storage.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e); + } + } + + 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 + // if (!NeedCreateSettingPanel()) return null; + + // Create main grid with two columns (Column 1: Auto, Column 2: *) + var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; + mainPanel.ColumnDefinitions.Add(new ColumnDefinition() + { + Width = new GridLength(0, GridUnitType.Auto) + }); + mainPanel.ColumnDefinitions.Add(new ColumnDefinition() + { + Width = new GridLength(1, GridUnitType.Star) + }); + + // Iterate over each setting and create one row for it + var rowCount = 0; + foreach (var (type, attributes) in Configuration!.Body) + { + // Skip if the setting does not have attributes or name + if (attributes?.Name == null) continue; + + // Add a new row to the main grid + mainPanel.RowDefinitions.Add(new RowDefinition() + { + Height = new GridLength(0, GridUnitType.Auto) + }); + + // State controls for column 0 and 1 + StackPanel? panel = null; + FrameworkElement contentControl; + + // If the type is textBlock, separator, or checkbox, we do not need to create a panel + if (type != "textBlock" && type != "separator" && type != "checkbox") + { + // Create a panel to hold the label and description + panel = new StackPanel + { + Margin = SettingPanelItemTopBottomMargin, + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Center + }; + + // Create a text block for name + var name = new TextBlock() + { + Text = attributes.Label, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + // Create a text block for description + TextBlock? desc = null; + if (attributes.Description != null) + { + desc = new TextBlock() + { + Text = attributes.Description, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + desc.SetResourceReference(TextBlock.StyleProperty, "SettingPanelTextBlockDescriptionStyle"); // for theme change + } + + // Add the name and description to the panel + panel.Children.Add(name); + if (desc != null) panel.Children.Add(desc); + } + + switch (type) + { + case "textBlock": + { + contentControl = new TextBlock + { + Text = attributes.Description?.Replace("\\r\\n", "\r\n") ?? string.Empty, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; + + break; + } + case "input": + { + var textBox = new TextBox() + { + MinWidth = SettingPanelTextBoxMinWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description + }; + + textBox.TextChanged += (_, _) => + { + Settings[attributes.Name] = textBox.Text; + }; + + contentControl = textBox; + + break; + } + case "inputWithFileBtn": + case "inputWithFolderBtn": + { + var textBox = new TextBox() + { + Width = SettingPanelPathTextBoxWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftMargin, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description + }; + + textBox.TextChanged += (_, _) => + { + Settings[attributes.Name] = textBox.Text; + }; + + var Btn = new Button() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftMargin, + Content = API.GetTranslation("select") + }; + + Btn.Click += (_, _) => + { + using System.Windows.Forms.CommonDialog dialog = type switch + { + "inputWithFolderBtn" => new System.Windows.Forms.FolderBrowserDialog(), + _ => new System.Windows.Forms.OpenFileDialog(), + }; + + if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK) + { + return; + } + + var path = dialog switch + { + System.Windows.Forms.FolderBrowserDialog folderDialog => folderDialog.SelectedPath, + System.Windows.Forms.OpenFileDialog fileDialog => fileDialog.FileName, + _ => throw new System.NotImplementedException() + }; + + textBox.Text = path; + Settings[attributes.Name] = path; + }; + + var stackPanel = new StackPanel() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + Orientation = Orientation.Horizontal + }; + + // Create a stack panel to wrap the button and text box + stackPanel.Children.Add(textBox); + stackPanel.Children.Add(Btn); + + contentControl = stackPanel; + + break; + } + case "textarea": + { + var textBox = new TextBox() + { + MinHeight = SettingPanelAreaTextBoxMinHeight, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + TextWrapping = TextWrapping.WrapWithOverflow, + AcceptsReturn = true, + Text = Settings[attributes.Name] as string ?? string.Empty, + ToolTip = attributes.Description + }; + + textBox.TextChanged += (sender, _) => + { + Settings[attributes.Name] = ((TextBox)sender).Text; + }; + + contentControl = textBox; + + break; + } + case "passwordBox": + { + var passwordBox = new PasswordBox() + { + MinWidth = SettingPanelTextBoxMinWidth, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + Password = Settings[attributes.Name] as string ?? string.Empty, + PasswordChar = attributes.passwordChar == default ? '*' : attributes.passwordChar, + ToolTip = attributes.Description, + }; + + passwordBox.PasswordChanged += (sender, _) => + { + Settings[attributes.Name] = ((PasswordBox)sender).Password; + }; + + contentControl = passwordBox; + + break; + } + case "dropdown": + { + var comboBox = new ComboBox() + { + ItemsSource = attributes.Options, + SelectedItem = Settings[attributes.Name], + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + ToolTip = attributes.Description + }; + + comboBox.SelectionChanged += (sender, _) => + { + Settings[attributes.Name] = (string)((ComboBox)sender).SelectedItem; + }; + + contentControl = comboBox; + + break; + } + case "checkbox": + { + // If can parse the default value to bool, use it, otherwise use false + var defaultValue = bool.TryParse(attributes.DefaultValue, out var value) && value; + var checkBox = new CheckBox + { + IsChecked = + Settings[attributes.Name] is bool isChecked + ? isChecked + : defaultValue, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemTopBottomMargin, + Content = attributes.Label, + ToolTip = attributes.Description + }; + + checkBox.Click += (sender, _) => + { + Settings[attributes.Name] = ((CheckBox)sender).IsChecked ?? defaultValue; + }; + + contentControl = checkBox; + + break; + } + case "hyperlink": + { + var hyperlink = new Hyperlink + { + ToolTip = attributes.Description, + NavigateUri = attributes.url + }; + + hyperlink.Inlines.Add(attributes.urlLabel); + hyperlink.RequestNavigate += (sender, e) => + { + API.OpenUrl(e.Uri); + e.Handled = true; + }; + + var textBlock = new TextBlock() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = SettingPanelItemLeftTopBottomMargin, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; + textBlock.Inlines.Add(hyperlink); + + contentControl = textBlock; + + break; + } + case "separator": + { + var sep = new Separator(); + + sep.SetResourceReference(Separator.StyleProperty, "SettingPanelSeparatorStyle"); + + contentControl = sep; + + break; + } + default: + continue; + } + + // If type is textBlock or separator, we just add the content control to the main grid + if (panel == null) + { + // Add the content control to the column 0, row rowCount and columnSpan 2 of the main grid + mainPanel.Children.Add(contentControl); + Grid.SetColumn(contentControl, 0); + Grid.SetColumnSpan(contentControl, 2); + Grid.SetRow(contentControl, rowCount); + } + else + { + // Add the panel to the column 0 and row rowCount of the main grid + mainPanel.Children.Add(panel); + Grid.SetColumn(panel, 0); + Grid.SetRow(panel, rowCount); + + // Add the content control to the column 1 and row rowCount of the main grid + mainPanel.Children.Add(contentControl); + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + } + + // Add into SettingControls for settings storage if need + if (NeedSaveInSettings(type)) SettingControls[attributes.Name] = contentControl; + + rowCount++; + } + + // Wrap the main grid in a user control + return new UserControl() + { + Content = mainPanel + }; + } + + private static bool NeedSaveInSettings(string type) + { + return type != "textBlock" && type != "separator" && type != "hyperlink"; + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs new file mode 100644 index 000000000..148fd969e --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Core.Plugin.JsonRPCV2Models; +using Flow.Launcher.Plugin; +using Microsoft.VisualStudio.Threading; +using StreamJsonRpc; +using IAsyncDisposable = System.IAsyncDisposable; + +namespace Flow.Launcher.Core.Plugin +{ + internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated + { + public const string JsonRpc = "JsonRPC"; + + private static readonly string ClassName = nameof(JsonRPCPluginV2); + + protected abstract IDuplexPipe ClientPipe { get; set; } + + protected StreamReader ErrorStream { get; set; } + + private JsonRpc RPC { get; set; } + + protected override async Task ExecuteResultAsync(JsonRPCResult result) + { + var res = await RPC.InvokeAsync(result.JsonRPCAction.Method, + argument: result.JsonRPCAction.Parameters); + + return res.Hide; + } + + private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); + + public override List LoadContextMenus(Result selectedResult) + { + var res = JTF.Run(() => RPC.InvokeWithCancellationAsync("context_menu", + new object[] { selectedResult.ContextData })); + + var results = ParseResults(res); + + return results; + } + + public override async Task> QueryAsync(Query query, CancellationToken token) + { + var res = await RPC.InvokeWithCancellationAsync("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((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) + { + // Ignored + } + catch (ConnectionLostException) + { + // Ignored + } + catch (Exception e) + { + Context.API.LogException(ClassName, $"Failed to call reload_data for plugin {Context.CurrentPluginMetadata.Name}", e); + } + } + + public virtual async ValueTask DisposeAsync() + { + try + { + await RPC.InvokeAsync("close"); + } + catch (RemoteMethodNotFoundException) + { + // Ignored + } + catch (ConnectionLostException) + { + // Ignored + } + catch (Exception e) + { + Context.API.LogException(ClassName, $"Failed to call close for plugin {Context.CurrentPluginMetadata.Name}", e); + } + finally + { + RPC?.Dispose(); + ErrorStream?.Dispose(); + } + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs new file mode 100644 index 000000000..6a130f70f --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs @@ -0,0 +1,4 @@ +namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models +{ + public record JsonRPCExecuteResponse(bool Hide = true); +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs new file mode 100644 index 000000000..4d988b837 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -0,0 +1,198 @@ +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 readonly 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 GetAllPlugins() + { + return _api.GetAllPlugins(); + } + + public MatchResult FuzzySearch(string query, string stringToCompare) + { + return _api.FuzzySearch(query, stringToCompare); + } + + public Task HttpGetStringAsync(string url, CancellationToken token = default) + { + return _api.HttpGetStringAsync(url, token); + } + + public Task HttpGetStreamAsync(string url, CancellationToken token = default) + { + return _api.HttpGetStreamAsync(url, token); + } + + public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action 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 LogError(string className, string message, [CallerMemberName] string methodName = "") + { + _api.LogError(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(); + } + + public void SavePluginCaches() + { + _api.SavePluginCaches(); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs new file mode 100644 index 000000000..003724a23 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models +{ + public record JsonRPCQueryRequest( + List Results + ); +} diff --git a/Flow.Launcher.Core/Plugin/NodePlugin.cs b/Flow.Launcher.Core/Plugin/NodePlugin.cs new file mode 100644 index 000000000..aeac18ca9 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/NodePlugin.cs @@ -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 +{ + /// + /// Execution of JavaScript & TypeScript plugins + /// + 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 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); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/NodePluginV2.cs b/Flow.Launcher.Core/Plugin/NodePluginV2.cs new file mode 100644 index 000000000..c8cc37c57 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/NodePluginV2.cs @@ -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 +{ + /// + /// Execution of JavaScript & TypeScript plugins + /// + 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; + } +} diff --git a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs index 515b0bedc..87bf8e6e8 100644 --- a/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginAssemblyLoader.cs @@ -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; @@ -15,20 +12,6 @@ namespace Flow.Launcher.Core.Plugin private readonly AssemblyName assemblyName; - private static readonly ConcurrentDictionary loadedAssembly; - - static PluginAssemblyLoader() - { - var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - loadedAssembly = new ConcurrentDictionary( - currentAssemblies.Select(x => new KeyValuePair(x.FullName, default))); - - AppDomain.CurrentDomain.AssemblyLoad += (sender, args) => - { - loadedAssembly[args.LoadedAssembly.FullName] = default; - }; - } - internal PluginAssemblyLoader(string assemblyFilePath) { dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath); @@ -47,10 +30,20 @@ namespace Flow.Launcher.Core.Plugin // When resolving dependencies, ignore assembly depenedencies that already exits with Flow.Launcher // Otherwise duplicate assembly will be loaded and some weird behavior will occur, such as WinRT.Runtime.dll // will fail due to loading multiple versions in process, each with their own static instance of registration state - if (assemblyPath == null || ExistsInReferencedPackage(assemblyName)) - return null; + var existAssembly = Default.Assemblies.FirstOrDefault(x => x.FullName == assemblyName.FullName); - return LoadFromAssemblyPath(assemblyPath); + return existAssembly ?? (assemblyPath == null ? null : LoadFromAssemblyPath(assemblyPath)); + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var path = dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (!string.IsNullOrEmpty(path)) + { + return LoadUnmanagedDllFromPath(path); + } + + return IntPtr.Zero; } internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type) @@ -58,10 +51,5 @@ namespace Flow.Launcher.Core.Plugin var allTypes = assembly.ExportedTypes; return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type)); } - - internal bool ExistsInReferencedPackage(AssemblyName assemblyName) - { - return loadedAssembly.ContainsKey(assemblyName.FullName); - } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs index ea2119e60..163f97046 100644 --- a/Flow.Launcher.Core/Plugin/PluginConfig.cs +++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs @@ -9,7 +9,6 @@ using System.Text.Json; namespace Flow.Launcher.Core.Plugin { - internal abstract class PluginConfig { /// @@ -45,8 +44,56 @@ namespace Flow.Launcher.Core.Plugin } } } - - return allPluginMetadata; + + (List uniqueList, List duplicateList) = GetUniqueLatestPluginMetadata(allPluginMetadata); + + duplicateList + .ForEach( + x => Log.Warn("PluginConfig", + string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " + + "not loaded due to version not the highest of the duplicates", + x.Name, x.ID, x.Version), + "GetUniqueLatestPluginMetadata")); + + return uniqueList; + } + + internal static (List, List) GetUniqueLatestPluginMetadata(List allPluginMetadata) + { + var duplicate_list = new List(); + var unique_list = new List(); + + var duplicateGroups = allPluginMetadata.GroupBy(x => x.ID).Where(g => g.Count() > 1).Select(y => y).ToList(); + + foreach (var metadata in allPluginMetadata) + { + var duplicatesExist = false; + foreach (var group in duplicateGroups) + { + if (metadata.ID == group.Key) + { + duplicatesExist = true; + + // If metadata's version greater than each duplicate's version, CompareTo > 0 + var count = group.Where(x => metadata.Version.CompareTo(x.Version) > 0).Count(); + + // Only add if the meatadata's version is the highest of all duplicates in the group + if (count == group.Count() - 1) + { + unique_list.Add(metadata); + } + else + { + duplicate_list.Add(metadata); + } + } + } + + if (!duplicatesExist) + unique_list.Add(metadata); + } + + return (unique_list, duplicate_list); } private static PluginMetadata GetPluginMetadata(string pluginDirectory) @@ -64,7 +111,7 @@ namespace Flow.Launcher.Core.Plugin metadata = JsonSerializer.Deserialize(File.ReadAllText(configPath)); metadata.PluginDirectory = pluginDirectory; // for plugins which doesn't has ActionKeywords key - metadata.ActionKeywords = metadata.ActionKeywords ?? new List { metadata.ActionKeyword }; + metadata.ActionKeywords ??= new List { metadata.ActionKeyword }; // for plugin still use old ActionKeyword metadata.ActionKeyword = metadata.ActionKeywords?[0]; } @@ -89,4 +136,4 @@ namespace Flow.Launcher.Core.Plugin return metadata; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 134c3c002..52d6fd736 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -3,12 +3,17 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; +using IRemovable = Flow.Launcher.Core.Storage.IRemovable; using ISavable = Flow.Launcher.Plugin.ISavable; namespace Flow.Launcher.Core.Plugin @@ -18,17 +23,21 @@ namespace Flow.Launcher.Core.Plugin /// public static class PluginManager { + private static readonly string ClassName = nameof(PluginManager); + private static IEnumerable _contextMenuPlugins; public static List AllPlugins { get; private set; } public static readonly HashSet GlobalPlugins = new(); public static readonly Dictionary 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(); - // todo happlebao, this should not be public, the indicator function should be embeded - public static PluginsSettings Settings; + private static PluginsSettings Settings; private static List _metadatas; + private static readonly List _modifiedPlugins = new(); /// /// Directories that will hold Flow Launcher plugin directory @@ -47,20 +56,39 @@ namespace Flow.Launcher.Core.Plugin } } + /// + /// Save json and ISavable + /// public static void Save() { - foreach (var plugin in AllPlugins) + foreach (var pluginPair in AllPlugins) { - var savable = plugin.Plugin as ISavable; - savable?.Save(); + var savable = pluginPair.Plugin as ISavable; + try + { + savable?.Save(); + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e); + } } API.SavePluginSettings(); + API.SavePluginCaches(); } public static async ValueTask DisposePluginsAsync() { foreach (var pluginPair in AllPlugins) + { + await DisposePluginAsync(pluginPair); + } + } + + private static async Task DisposePluginAsync(PluginPair pluginPair) + { + try { switch (pluginPair.Plugin) { @@ -72,9 +100,13 @@ namespace Flow.Launcher.Core.Plugin break; } } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e); + } } - public static async Task ReloadData() + public static async Task ReloadDataAsync() { await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch { @@ -84,6 +116,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().Any(x => !x.Metadata.Disabled); + } + + public static bool AllowAlwaysPreview() + { + var plugin = GetPluginsForInterface().FirstOrDefault(x => !x.Metadata.Disabled); + + if (plugin is null) + return false; + + return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview(); + } + static PluginManager() { // validate user directory @@ -103,22 +177,40 @@ namespace Flow.Launcher.Core.Plugin Settings = settings; Settings.UpdatePluginSettings(_metadatas); AllPlugins = PluginsLoader.Plugins(_metadatas, Settings); + // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins + UpdatePluginDirectory(_metadatas); + } + + private static void UpdatePluginDirectory(List metadatas) + { + foreach (var metadata in metadatas) + { + if (AllowedLanguage.IsDotNet(metadata.Language)) + { + metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName); + metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.AssemblyName); + } + else + { + metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name); + metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.Name); + } + } } /// /// Call initialize for all plugins /// /// return the list of failed to init plugins or null for none - public static async Task InitializePlugins(IPublicAPI api) + public static async Task InitializePluginsAsync() { - API = api; var failedPlugins = new ConcurrentQueue(); var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate { try { - var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", + var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>", () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API))); pair.Metadata.InitTime += milliseconds; @@ -127,7 +219,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e); + Log.Exception(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e); pair.Metadata.Disabled = true; failedPlugins.Enqueue(pair); } @@ -157,38 +249,40 @@ namespace Flow.Launcher.Core.Plugin 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 + ); } } public static ICollection ValidPluginsForQuery(Query query) { - if (NonGlobalPlugins.ContainsKey(query.ActionKeyword)) - { - var plugin = NonGlobalPlugins[query.ActionKeyword]; - return new List - { - plugin - }; - } - else - { + if (query is null) + return Array.Empty(); + + if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) return GlobalPlugins; - } + + return new List + { + plugin + }; } public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) { var results = new List(); + var metadata = pair.Metadata; + try { - var metadata = pair.Metadata; - - long milliseconds = -1L; - - milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", + var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false)); token.ThrowIfCancellationRequested(); @@ -206,11 +300,26 @@ namespace Flow.Launcher.Core.Plugin // null will be fine since the results will only be added into queue if the token hasn't been cancelled return null; } - + catch (Exception e) + { + Result r = new() + { + Title = $"{metadata.Name}: Failed to respond!", + SubTitle = "Select this result for more info", + IcoPath = 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 results, PluginMetadata metadata, Query query) + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) { @@ -218,8 +327,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; } @@ -237,7 +346,8 @@ namespace Flow.Launcher.Core.Plugin public static IEnumerable GetPluginsForInterface() where T : IFeatures { - return AllPlugins.Where(p => p.Plugin is T); + // Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup + return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty(); } public static List GetContextMenusForPlugin(Result result) @@ -273,8 +383,8 @@ namespace Flow.Launcher.Core.Plugin { // this method is only checking for action keywords (defined as not '*') registration // hence the actionKeyword != Query.GlobalPluginWildcardSign logic - return actionKeyword != Query.GlobalPluginWildcardSign - && NonGlobalPlugins.ContainsKey(actionKeyword); + return actionKeyword != Query.GlobalPluginWildcardSign + && NonGlobalPlugins.ContainsKey(actionKeyword); } /// @@ -293,7 +403,16 @@ namespace Flow.Launcher.Core.Plugin NonGlobalPlugins[newActionKeyword] = plugin; } + // Update action keywords and action keyword in plugin metadata plugin.Metadata.ActionKeywords.Add(newActionKeyword); + if (plugin.Metadata.ActionKeywords.Count > 0) + { + plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0]; + } + else + { + plugin.Metadata.ActionKeyword = string.Empty; + } } /// @@ -314,17 +433,214 @@ namespace Flow.Launcher.Core.Plugin if (oldActionkeyword != Query.GlobalPluginWildcardSign) NonGlobalPlugins.Remove(oldActionkeyword); - + // Update action keywords and action keyword in plugin metadata plugin.Metadata.ActionKeywords.Remove(oldActionkeyword); - } - - public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword) - { - if (oldActionKeyword != newActionKeyword) + if (plugin.Metadata.ActionKeywords.Count > 0) { - AddActionKeyword(id, newActionKeyword); - RemoveActionKeyword(id, oldActionKeyword); + plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0]; + } + else + { + plugin.Metadata.ActionKeyword = string.Empty; } } + + 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(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 id) + { + return _modifiedPlugins.Contains(id); + } + + public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) + { + InstallPlugin(newVersion, zipFilePath, checkModified:false); + await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false); + _modifiedPlugins.Add(existingVersion.ID); + } + + public static void InstallPlugin(UserPlugin plugin, string zipFilePath) + { + InstallPlugin(plugin, zipFilePath, checkModified: true); + } + + public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false) + { + await UninstallPluginAsync(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 + { + "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 async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) + { + if (checkModified && PluginModified(plugin.ID)) + { + throw new ArgumentException($"Plugin {plugin.Name} has been modified"); + } + + if (removePluginSettings || removePluginFromSettings) + { + // If we want to remove plugin from AllPlugins, + // we need to dispose them so that they can release file handles + // which can help FL to delete the plugin settings & cache folders successfully + var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID); + foreach (var pluginPair in pluginPairs) + { + await DisposePluginAsync(pluginPair); + } + } + + if (removePluginSettings) + { + // For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances + if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable) + { + removable.RemovePluginSettings(plugin.AssemblyName); + removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath); + } + + try + { + var pluginSettingsDirectory = plugin.PluginSettingsDirectoryPath; + if (Directory.Exists(pluginSettingsDirectory)) + Directory.Delete(pluginSettingsDirectory, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin settings folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), + string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); + } + } + + if (removePluginFromSettings) + { + try + { + var pluginCacheDirectory = plugin.PluginCacheDirectoryPath; + if (Directory.Exists(pluginCacheDirectory)) + Directory.Delete(pluginCacheDirectory, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin cache folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"), + string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); + } + Settings.RemovePluginSettings(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 } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 1b78c68ae..1010d9f08 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -1,35 +1,61 @@ -using System; +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 System.Diagnostics; -using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Core.Plugin { public static class PluginsLoader { - public const string PythonExecutable = "pythonw.exe"; + private static readonly string ClassName = nameof(PluginsLoader); + + // 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(); public static List Plugins(List 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; } - public static IEnumerable DotNetPlugins(List source) + private static IEnumerable DotNetPlugins(List source) { var erroredPlugins = new List(); @@ -38,17 +64,8 @@ namespace Flow.Launcher.Core.Plugin foreach (var metadata in metadatas) { - var milliseconds = Stopwatch.Debug( - $"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () => + var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () => { -#if DEBUG - var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath); - var assembly = assemblyLoader.LoadAssemblyAndDependencies(); - var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, - typeof(IAsyncPlugin)); - - var plugin = Activator.CreateInstance(type) as IAsyncPlugin; -#else Assembly assembly = null; IAsyncPlugin plugin = null; @@ -61,7 +78,15 @@ namespace Flow.Launcher.Core.Plugin typeof(IAsyncPlugin)); plugin = Activator.CreateInstance(type) as IAsyncPlugin; + + metadata.AssemblyName = assembly.GetName().Name; } +#if DEBUG + catch (Exception) + { + throw; + } +#else catch (Exception e) when (assembly == null) { Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e); @@ -79,131 +104,63 @@ namespace Flow.Launcher.Core.Plugin Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e); } #endif + if (plugin == null) { erroredPlugins.Add(metadata.Name); return; } - plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata}); + plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); }); metadata.InitTime += milliseconds; } if (erroredPlugins.Count > 0) { - var errorPluginString = String.Join(Environment.NewLine, erroredPlugins); + var errorPluginString = string.Join(Environment.NewLine, erroredPlugins); var errorMessage = "The following " + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") + "errored and cannot be loaded:"; - Task.Run(() => + _ = Task.Run(() => { - MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + + Ioc.Default.GetRequiredService().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 PythonPlugins(List source, PluginsSettings settings) - { - if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python)) - return new List(); - - 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, " + - "would you like to install Python to run them? " + - 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(); - } - - return SetPythonPathForPluginPairs(source, pythonPath); - } - - private static IEnumerable SetPythonPathForPluginPairs(List 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 ExecutablePlugins(IEnumerable source) + private static IEnumerable ExecutablePlugins(IEnumerable source) { return source - .Where(o => o.Language.ToUpper() == AllowedLanguage.Executable) - .Select(metadata => new PluginPair + .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) + .Select(metadata => { - Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata + return new PluginPair + { + Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), + Metadata = metadata + }; + }); + } + + private static IEnumerable ExecutableV2Plugins(IEnumerable source) + { + return source + .Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase)) + .Select(metadata => + { + return new PluginPair + { + Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), + Metadata = metadata + }; }); } } diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs new file mode 100644 index 000000000..bae263157 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -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(); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 5d2d8d51d..e40b0330e 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -1,6 +1,7 @@ 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,56 +24,81 @@ 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"; - //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"); + _startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; + _startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory; + _startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory; } - protected override Task ExecuteQueryAsync(Query query, CancellationToken token) + protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - JsonRPCServerRequestModel request = new JsonRPCServerRequestModel - { - Method = "query", Parameters = new object[] {query.Search}, - }; - - _startInfo.ArgumentList[2] = request.ToString(); - - // todo happlebao why context can't be used in constructor - _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } - protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) + 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); } - protected override string ExecuteContextMenu(Result selectedResult) + public override async Task InitAsync(PluginInitContext context) { - JsonRPCServerRequestModel request = new JsonRPCServerRequestModel + // Run .py files via `-c ` + if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) { - Method = "context_menu", Parameters = new object[] {selectedResult.ContextData}, - }; - _startInfo.ArgumentList[2] = request.ToString(); + 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; - - // TODO: Async Action - return Execute(_startInfo); - } - - public override Task InitAsync(PluginInitContext context) - { - this.context = context; - _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); - _startInfo.ArgumentList.Add(""); - return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs new file mode 100644 index 000000000..8a9e1ff44 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -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 ` + 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; + } +} diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index df336e14b..3dc7877ac 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin @@ -10,37 +9,37 @@ namespace Flow.Launcher.Core.Plugin public static Query Build(string text, Dictionary nonGlobalPlugins) { // replace multiple white spaces with one white space - var terms = text.Split(new[] { Query.TermSeperater }, StringSplitOptions.RemoveEmptyEntries); + var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries); if (terms.Length == 0) { // nothing was typed return null; } - var rawQuery = string.Join(Query.TermSeperater, terms); + var rawQuery = text; string actionKeyword, search; string possibleActionKeyword = terms[0]; - List actionParameters; + string[] searchTerms; + if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled) { // use non global plugin for query actionKeyword = possibleActionKeyword; - actionParameters = terms.Skip(1).ToList(); - search = actionParameters.Count > 0 ? rawQuery.Substring(actionKeyword.Length + 1) : string.Empty; + search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty; + searchTerms = terms[1..]; } else { // non action keyword actionKeyword = string.Empty; - search = rawQuery; + search = rawQuery.TrimStart(); + searchTerms = terms; } - var query = new Query + return new Query () { - Terms = terms, + Search = search, RawQuery = rawQuery, - ActionKeyword = actionKeyword, - Search = search + SearchTerms = searchTerms, + ActionKeyword = actionKeyword }; - - return query; } } } \ No newline at end of file diff --git a/Flow.Launcher.Core/Properties/AssemblyInfo.cs b/Flow.Launcher.Core/Properties/AssemblyInfo.cs index 40017c46c..ad60e2c9f 100644 --- a/Flow.Launcher.Core/Properties/AssemblyInfo.cs +++ b/Flow.Launcher.Core/Properties/AssemblyInfo.cs @@ -1,3 +1,3 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Flow.Launcher.Test")] \ No newline at end of file +[assembly: InternalsVisibleTo("Flow.Launcher.Test")] diff --git a/Flow.Launcher.Core/Resource/AvailableLanguages.cs b/Flow.Launcher.Core/Resource/AvailableLanguages.cs index 3c3dbc76f..ecaecf646 100644 --- a/Flow.Launcher.Core/Resource/AvailableLanguages.cs +++ b/Flow.Launcher.Core/Resource/AvailableLanguages.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Flow.Launcher.Core.Resource { @@ -17,11 +17,18 @@ namespace Flow.Launcher.Core.Resource public static Language German = new Language("de", "Deutsch"); public static Language Korean = new Language("ko", "한국어"); public static Language Serbian = new Language("sr", "Srpski"); - public static Language Portuguese_BR = new Language("pt-br", "Português (Brasil)"); + public static Language Portuguese_Portugal = new Language("pt-pt", "Português"); + public static Language Portuguese_Brazil = new Language("pt-br", "Português (Brasil)"); + public static Language Spanish = new Language("es", "Spanish"); + 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 GetAvailableLanguages() { @@ -40,13 +47,53 @@ namespace Flow.Launcher.Core.Resource German, Korean, Serbian, - Portuguese_BR, + Portuguese_Portugal, + Portuguese_Brazil, + Spanish, + Spanish_LatinAmerica, 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", + }; + } } } diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index 64b949cbb..df841dbbe 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -9,34 +9,63 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; 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 List _languageDirectories = new List(); - private readonly List _oldResources = new List(); + private readonly Settings _settings; + private readonly List _languageDirectories = new(); + private readonly List _oldResources = new(); + 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 static string GetSystemLanguageCodeAtStartup() + { + 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; + } private void AddPluginLanguageDirectories() { @@ -54,22 +83,68 @@ 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(); } + /// + /// Initialize language. Will change app language and plugin language based on settings. + /// + public async Task InitializeLanguageAsync() + { + // Get actual language + var languageCode = _settings.Language; + if (languageCode == Constant.SystemLanguageCode) + { + languageCode = SystemLanguageCode; + } + + // Get language by language code and change language + var language = GetLanguageByLanguageCode(languageCode); + + // Add plugin language directories first so that we can load language files from plugins + AddPluginLanguageDirectories(); + + // Change language + await ChangeLanguageAsync(language); + } + + /// + /// Change language during runtime. Will change app language and plugin language & save settings. + /// + /// 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); + + // Change language + _ = ChangeLanguageAsync(language); + + // Save settings + _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } - private Language GetLanguageByLanguageCode(string languageCode) + private static Language GetLanguageByLanguageCode(string languageCode) { var lowercase = languageCode.ToLower(); var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); @@ -84,32 +159,39 @@ namespace Flow.Launcher.Core.Resource } } - public void ChangeLanguage(Language language) + private async Task ChangeLanguageAsync(Language language) { - language = language.NonNull(); - - + // Remove old language files and load language RemoveOldLanguageFiles(); if (language != AvailableLanguages.English) { LoadLanguage(language); } - UpdatePluginMetadataTranslations(); - Settings.Language = 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 for plugins after culture is set + await Task.Run(UpdatePluginMetadataTranslations); } public bool PromptShouldUsePinyin(string languageCodeToSet) { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); - if (Settings.ShouldUsePinyin) + if (_settings.ShouldUsePinyin) return false; if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW) return false; - if (MessageBox.Show("Do you want to turn on search with Pinyin?", string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) + // No other languages should show the following text so just make it hard-coded + // "Do you want to search with pinyin?" + string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ; + + if (Ioc.Default.GetRequiredService().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) return false; return true; @@ -126,11 +208,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) @@ -149,10 +234,12 @@ namespace Flow.Launcher.Core.Resource public List LoadAvailableLanguages() { - return AvailableLanguages.GetAvailableLanguages(); + var list = AvailableLanguages.GetAvailableLanguages(); + list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode))); + return list; } - public string GetTranslation(string key) + public static string GetTranslation(string key) { var translation = Application.Current.TryFindResource(key); if (translation is string) @@ -170,12 +257,12 @@ namespace Flow.Launcher.Core.Resource { foreach (var p in PluginManager.GetPluginsForInterface()) { - var pluginI18N = p.Plugin as IPluginI18n; - if (pluginI18N == null) return; + if (p.Plugin is not IPluginI18n pluginI18N) return; try { p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); + pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); } catch (Exception e) { @@ -184,11 +271,11 @@ namespace Flow.Launcher.Core.Resource } } - public string LanguageFile(string folder, string language) + private static string LanguageFile(string folder, string language) { if (Directory.Exists(folder)) { - string path = Path.Combine(folder, language); + var path = Path.Combine(folder, language); if (File.Exists(path)) { return path; @@ -196,7 +283,7 @@ namespace Flow.Launcher.Core.Resource else { Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>"); - string english = Path.Combine(folder, DefaultFile); + var english = Path.Combine(folder, DefaultFile); if (File.Exists(english)) { return english; @@ -214,4 +301,4 @@ namespace Flow.Launcher.Core.Resource } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Resource/InternationalizationManager.cs b/Flow.Launcher.Core/Resource/InternationalizationManager.cs deleted file mode 100644 index 3d87626e6..000000000 --- a/Flow.Launcher.Core/Resource/InternationalizationManager.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Flow.Launcher.Core.Resource -{ - 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; - } - } - } -} \ No newline at end of file diff --git a/Flow.Launcher.Core/Resource/LocalizationConverter.cs b/Flow.Launcher.Core/Resource/LocalizationConverter.cs index 1d835a831..fdda33926 100644 --- a/Flow.Launcher.Core/Resource/LocalizationConverter.cs +++ b/Flow.Launcher.Core/Resource/LocalizationConverter.cs @@ -4,8 +4,9 @@ using System.Globalization; using System.Reflection; using System.Windows.Data; -namespace Flow.Launcher.Core +namespace Flow.Launcher.Core.Resource { + [Obsolete("LocalizationConverter is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")] public class LocalizationConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs b/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs index af8b23136..3e1a19a76 100644 --- a/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs +++ b/Flow.Launcher.Core/Resource/LocalizedDescriptionAttribute.cs @@ -1,16 +1,19 @@ using System.ComponentModel; -using Flow.Launcher.Core.Resource; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; -namespace Flow.Launcher.Core +namespace Flow.Launcher.Core.Resource { public class LocalizedDescriptionAttribute : DescriptionAttribute { - private readonly Internationalization _translator; + // 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(); + private readonly string _resourceKey; public LocalizedDescriptionAttribute(string resourceKey) { - _translator = InternationalizationManager.Instance; _resourceKey = resourceKey; } @@ -18,7 +21,7 @@ namespace Flow.Launcher.Core { get { - string description = _translator.GetTranslation(_resourceKey); + string description = API.GetTranslation(_resourceKey); return string.IsNullOrWhiteSpace(description) ? string.Format("[[{0}]]", _resourceKey) : description; } diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index e70de673e..59e76e2d2 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -1,139 +1,253 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; +using System.Xml; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Interop; +using System.Windows.Controls.Primitives; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Media.Effects; +using System.Windows.Shell; +using System.Windows.Threading; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; +using Microsoft.Win32; namespace Flow.Launcher.Core.Resource { public class Theme { - private const int ShadowExtraMargin = 12; + #region Properties & Fields - private readonly List _themeDirectories = new List(); + public bool BlurEnabled { get; private set; } + + private const string ThemeMetadataNamePrefix = "Name:"; + private const string ThemeMetadataIsDarkPrefix = "IsDark:"; + private const string ThemeMetadataHasBlurPrefix = "HasBlur:"; + + private const int ShadowExtraMargin = 32; + + private readonly IPublicAPI _api; + private readonly Settings _settings; + private readonly List _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); + private static string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder); + private static string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder); - public bool BlurEnabled { get; set; } + private Thickness _themeResizeBorderThickness; - private double mainWindowWidth; + #endregion - public Theme() + #region Constructor + + 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 => + _oldResource = dicts.FirstOrDefault(d => { - if (d.Source == null) - return false; + if (d.Source == null) return false; var p = d.Source.AbsolutePath; - var dir = Path.GetDirectoryName(p).NonNull(); - var info = new DirectoryInfo(dir); - var f = info.Name; - var e = Path.GetExtension(p); - var found = f == Folder && e == Extension; - return found; + return p.Contains(Folder) && Path.GetExtension(p) == Extension; }); - _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + + if (_oldResource != null) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + else + { + Log.Error("Current theme resource not found. Initializing with default theme."); + _oldTheme = Constant.DefaultTheme; + }; } - private void MakesureThemeDirectoriesExist() + #endregion + + #region Theme Resources + + 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); } } } - public bool ChangeTheme(string theme) - { - const string defaultTheme = Constant.DefaultTheme; - - string path = GetThemePath(theme); - try - { - if (string.IsNullOrEmpty(path)) - throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); - - 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) - { - UpdateResourceDictionary(GetResourceDictionary()); - _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); - } - - BlurEnabled = IsBlurTheme(); - - if (Settings.UseDropShadowEffect && !BlurEnabled) - AddDropShadowEffectToCurrentTheme(); - - SetBlurForWindow(); - } - catch (DirectoryNotFoundException e) - { - 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)); - ChangeTheme(defaultTheme); - } - return false; - } - catch (XamlParseException e) - { - 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)); - ChangeTheme(defaultTheme); - } - return false; - } - return true; - } - private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) { - var dicts = Application.Current.Resources.MergedDictionaries; + // Add new resources + if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) + { + Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); + } + + // Remove old resources + if (_oldResource != null && _oldResource != dictionaryToUpdate && + Application.Current.Resources.MergedDictionaries.Contains(_oldResource)) + { + Application.Current.Resources.MergedDictionaries.Remove(_oldResource); + } - dicts.Remove(_oldResource); - dicts.Add(dictionaryToUpdate); _oldResource = dictionaryToUpdate; } - private ResourceDictionary CurrentThemeResourceDictionary() + /// + /// Updates only the font settings and refreshes the UI. + /// + public void UpdateFonts() { - var uri = GetThemePath(Settings.Theme); + try + { + // Load a ResourceDictionary for the specified theme. + var themeName = _settings.Theme; + var dict = GetThemeResourceDictionary(themeName); + + // Apply font settings to the theme resource. + ApplyFontSettings(dict); + UpdateResourceDictionary(dict); + + // Must apply blur and drop shadow effects + _ = RefreshFrameAsync(); + } + catch (Exception e) + { + Log.Exception("Error occurred while updating theme fonts", e); + } + } + + /// + /// Loads and applies font settings to the theme resource. + /// + private void ApplyFontSettings(ResourceDictionary dict) + { + 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); + + SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true); + SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemTitleStyle"] is Style resultItemStyle && + dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && + dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && + dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch); + + SetFontProperties(resultItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle && + dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultSubFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch); + + SetFontProperties(resultSubItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultSubItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + } + + /// + /// Applies font properties to a Style. + /// + private static void SetFontProperties(Style style, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, bool isTextBox) + { + // Remove existing font-related setters + if (isTextBox) + { + // First, find the setters to remove and store them in a list + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == Control.FontFamilyProperty || + setter.Property == Control.FontStyleProperty || + setter.Property == Control.FontWeightProperty || + setter.Property == Control.FontStretchProperty) + .ToList(); + + // Remove each found setter one by one + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + // Add New font setter + style.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); + + // Set caret brush (retain existing logic) + var caretBrushPropertyValue = style.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); + var foregroundPropertyValue = style.Setters.OfType().Where(x => x.Property.Name == "Foreground") + .Select(x => x.Value).FirstOrDefault(); + if (!caretBrushPropertyValue && foregroundPropertyValue != null) + style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue)); + } + else + { + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == TextBlock.FontFamilyProperty || + setter.Property == TextBlock.FontStyleProperty || + setter.Property == TextBlock.FontWeightProperty || + setter.Property == TextBlock.FontStretchProperty) + .ToList(); + + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(TextBlock.FontStretchProperty, fontStretch)); + } + } + + private ResourceDictionary GetThemeResourceDictionary(string theme) + { + var uri = GetThemePath(theme); var dict = new ResourceDictionary { Source = new Uri(uri, UriKind.Absolute) @@ -142,82 +256,111 @@ namespace Flow.Launcher.Core.Resource return dict; } - public ResourceDictionary GetResourceDictionary() + private ResourceDictionary GetResourceDictionary(string theme) { - var dict = CurrentThemeResourceDictionary(); + var dict = GetThemeResourceDictionary(theme); - Style queryBoxStyle = dict["QueryBoxStyle"] as Style; - Style querySuggestionBoxStyle = dict["QuerySuggestionBoxStyle"] as Style; - - if (queryBoxStyle != null && querySuggestionBoxStyle != null) + 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)); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); - queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); + queryBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + queryBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + queryBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); var caretBrushPropertyValue = queryBoxStyle.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); var foregroundPropertyValue = queryBoxStyle.Setters.OfType().Where(x => x.Property.Name == "Foreground") .Select(x => x.Value).FirstOrDefault(); if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling - queryBoxStyle.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue)); + queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue)); // Query suggestion box's font style is aligned with query box - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); - querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight)); + querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch)); } - Style resultItemStyle = dict["ItemTitleStyle"] as Style; - Style resultSubItemStyle = dict["ItemSubTitleStyle"] as Style; - Style resultItemSelectedStyle = dict["ItemTitleSelectedStyle"] as Style; - Style resultSubItemSelectedStyle = dict["ItemSubTitleSelectedStyle"] as Style; - if (resultItemStyle != null && resultSubItemStyle != null && resultSubItemSelectedStyle != null && resultItemSelectedStyle != null) + if (dict["ItemTitleStyle"] is Style resultItemStyle && + 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 }, o => Array.ForEach(setters, p => o.Setters.Add(p))); + Array.ForEach( + new[] { resultItemStyle, resultItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o + => Array.ForEach(setters, p => o.Setters.Add(p))); } - var windowStyle = dict["WindowStyle"] as Style; - - var width = windowStyle?.Setters.OfType().Where(x => x.Property.Name == "Width") - .Select(x => x.Value).FirstOrDefault(); - - if (width == null) + if ( + dict["ItemSubTitleStyle"] is Style resultSubItemStyle && + dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) { - windowStyle = dict["BaseWindowStyle"] as Style; + 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)); - width = windowStyle?.Setters.OfType().Where(x => x.Property.Name == "Width") - .Select(x => x.Value).FirstOrDefault(); + Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; + Array.ForEach( + new[] { resultSubItemStyle, resultSubItemSelectedStyle }, o + => Array.ForEach(setters, p => o.Setters.Add(p))); } - mainWindowWidth = (double)width; - + /* Ignore Theme Window Width and use setting */ + var windowStyle = dict["WindowStyle"] as Style; + var width = _settings.WindowSize; + windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, width)); return dict; } - public List LoadAvailableThemes() + private ResourceDictionary GetCurrentResourceDictionary() { - List themes = new List(); - foreach (var themeDirectory in _themeDirectories) + return GetResourceDictionary(_settings.Theme); + } + + 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) { - themes.AddRange( - Directory.GetFiles(themeDirectory) - .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) - .ToList()); + if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + name = line[ThemeMetadataNamePrefix.Length..].Trim(); + } + else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase)) + { + isDark = bool.Parse(line[ThemeMetadataIsDarkPrefix.Length..].Trim()); + } + else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase)) + { + hasBlur = bool.Parse(line[ThemeMetadataHasBlurPrefix.Length..].Trim()); + } } - return themes.OrderBy(o => o).ToList(); + + return new ThemeData(extensionlessName, name, isDark, hasBlur); } private string GetThemePath(string themeName) @@ -234,40 +377,134 @@ namespace Flow.Launcher.Core.Resource return string.Empty; } + #endregion + + #region Get & Change Theme + + public ThemeData GetCurrentTheme() + { + var themes = GetAvailableThemes(); + var matchingTheme = themes.FirstOrDefault(t => t.FileNameWithoutExtension == _settings.Theme); + if (matchingTheme == null) + { + Log.Warn($"No matching theme found for '{_settings.Theme}'. Falling back to the first available theme."); + } + return matchingTheme ?? themes.FirstOrDefault(); + } + + public List GetAvailableThemes() + { + List themes = new List(); + foreach (var themeDirectory in _themeDirectories) + { + var filePaths = Directory + .GetFiles(themeDirectory) + .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) + .Select(GetThemeDataFromPath); + themes.AddRange(filePaths); + } + + return themes.OrderBy(o => o.Name).ToList(); + } + + public bool ChangeTheme(string theme = null) + { + if (string.IsNullOrEmpty(theme)) + theme = _settings.Theme; + + string path = GetThemePath(theme); + try + { + if (string.IsNullOrEmpty(path)) + throw new DirectoryNotFoundException($"Theme path can't be found <{path}>"); + + // Retrieve theme resource – always use the resource with font settings applied. + var resourceDict = GetResourceDictionary(theme); + + UpdateResourceDictionary(resourceDict); + + _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 == Constant.DefaultTheme) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + + BlurEnabled = IsBlurTheme(); + + // Apply blur and drop shadow effect so that we do not need to call it again + _ = RefreshFrameAsync(); + + return true; + } + catch (DirectoryNotFoundException) + { + Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); + if (theme != Constant.DefaultTheme) + { + _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme)); + ChangeTheme(Constant.DefaultTheme); + } + return false; + } + catch (XamlParseException) + { + Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); + if (theme != Constant.DefaultTheme) + { + _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme)); + ChangeTheme(Constant.DefaultTheme); + } + return false; + } + } + + #endregion + + #region Shadow Effect + public void AddDropShadowEffectToCurrentTheme() { - var dict = CurrentThemeResourceDictionary(); + var dict = GetCurrentResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; - var effectSetter = new Setter(); - effectSetter.Property = Border.EffectProperty; - effectSetter.Value = new DropShadowEffect + var effectSetter = new Setter { - Opacity = 0.9, - ShadowDepth = 2, - BlurRadius = 15 + Property = UIElement.EffectProperty, + Value = new DropShadowEffect + { + Opacity = 0.3, + ShadowDepth = 12, + Direction = 270, + BlurRadius = 30 + } }; - var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; - if (marginSetter == null) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is not Setter marginSetter) { + var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin); marginSetter = new Setter() { - Property = Border.MarginProperty, - Value = new Thickness(ShadowExtraMargin), + Property = FrameworkElement.MarginProperty, + Value = margin, }; windowBorderStyle.Setters.Add(marginSetter); + + SetResizeBoarderThickness(margin); } else { - var baseMargin = (Thickness) marginSetter.Value; + var baseMargin = (Thickness)marginSetter.Value; var newMargin = new Thickness( baseMargin.Left + ShadowExtraMargin, baseMargin.Top + ShadowExtraMargin, baseMargin.Right + ShadowExtraMargin, baseMargin.Bottom + ShadowExtraMargin); marginSetter.Value = newMargin; + + SetResizeBoarderThickness(newMargin); } windowBorderStyle.Setters.Add(effectSetter); @@ -277,17 +514,15 @@ 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; - var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; - - if(effectSetter != null) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == UIElement.EffectProperty) is Setter effectSetter) { windowBorderStyle.Setters.Remove(effectSetter); } - if (marginSetter != null) + + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is Setter marginSetter) { var currentMargin = (Thickness)marginSetter.Value; var newMargin = new Thickness( @@ -298,101 +533,377 @@ namespace Flow.Launcher.Core.Resource marginSetter.Value = newMargin; } + SetResizeBoarderThickness(null); + UpdateResourceDictionary(dict); } + public void SetResizeBorderThickness(WindowChrome windowChrome, bool fixedWindowSize) + { + if (fixedWindowSize) + { + windowChrome.ResizeBorderThickness = new Thickness(0); + } + else + { + windowChrome.ResizeBorderThickness = _themeResizeBorderThickness; + } + } + + // 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 void SetResizeBoarderThickness(Thickness? effectMargin) + { + var window = Application.Current.MainWindow; + if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome) + { + // Save the theme resize border thickness so that we can restore it if we change ResizeWindow setting + if (effectMargin == null) + { + _themeResizeBorderThickness = SystemParameters.WindowResizeBorderThickness; + } + else + { + _themeResizeBorderThickness = 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); + } + + // Apply the resize border thickness to the window chrome + SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults); + } + } + + #endregion + #region Blur Handling - /* - Found on https://github.com/riverar/sample-win10-aeroglass - */ - private enum AccentState - { - ACCENT_DISABLED = 0, - ACCENT_ENABLE_GRADIENT = 1, - ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, - ACCENT_ENABLE_BLURBEHIND = 3, - ACCENT_INVALID_STATE = 4 - } - [StructLayout(LayoutKind.Sequential)] - private struct AccentPolicy + /// + /// Refreshes the frame to apply the current theme settings. + /// + public async Task RefreshFrameAsync() { - public AccentState AccentState; - public int AccentFlags; - public int GradientColor; - public int AnimationId; - } + await Application.Current.Dispatcher.InvokeAsync(() => + { + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, useDropShadowEffect) = GetActualValue(); - [StructLayout(LayoutKind.Sequential)] - private struct WindowCompositionAttributeData - { - public WindowCompositionAttribute Attribute; - public IntPtr Data; - public int SizeOfData; - } + // Remove OS minimizing/maximizing animation + // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); - private enum WindowCompositionAttribute - { - WCA_ACCENT_POLICY = 19 + // The timing of adding the shadow effect should vary depending on whether the theme is transparent. + if (BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + SetBlurForWindow(_settings.Theme, backdropType); + + if (!BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + }, DispatcherPriority.Render); } - [DllImport("user32.dll")] - private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); /// /// Sets the blur for a window via SetWindowCompositionAttribute /// - public void SetBlurForWindow() + public async Task SetBlurForWindowAsync() { + await Application.Current.Dispatcher.InvokeAsync(() => + { + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, _) = GetActualValue(); + + SetBlurForWindow(_settings.Theme, backdropType); + }, DispatcherPriority.Render); + } + + /// + /// Gets the actual backdrop type and drop shadow effect settings based on the current theme status. + /// + public (BackdropTypes BackdropType, bool UseDropShadowEffect) GetActualValue() + { + var backdropType = _settings.BackdropType; + var useDropShadowEffect = _settings.UseDropShadowEffect; + + // When changed non-blur theme, change to backdrop to none + if (!BlurEnabled) + { + backdropType = BackdropTypes.None; + } + + // Dropshadow on and control disabled.(user can't change dropshadow with blur theme) if (BlurEnabled) { - SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND); + useDropShadowEffect = true; + } + + return (backdropType, useDropShadowEffect); + } + + private void SetBlurForWindow(string theme, BackdropTypes backdropType) + { + var dict = GetResourceDictionary(theme); + if (dict == null) return; + + var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null; + if (windowBorderStyle == null) return; + + var mainWindow = Application.Current.MainWindow; + if (mainWindow == null) return; + + // Check if the theme supports blur + bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; + if (BlurEnabled && hasBlur && Win32Helper.IsBackdropSupported()) + { + // If the BackdropType is Mica or MicaAlt, set the windowborderstyle's background to transparent + if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt) + { + windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); + windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)))); + } + else if (backdropType == BackdropTypes.Acrylic) + { + windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); + windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Transparent))); + } + + // Apply the blur effect + Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType); + ColorizeWindow(theme, backdropType); } else { - SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED); + // Apply default style when Blur is disabled + Win32Helper.DWMSetBackdropForWindow(mainWindow, BackdropTypes.None); + ColorizeWindow(theme, backdropType); + } + + UpdateResourceDictionary(dict); + } + + private void AutoDropShadow(bool useDropShadowEffect) + { + SetWindowCornerPreference("Default"); + RemoveDropShadowEffectFromCurrentTheme(); + if (useDropShadowEffect) + { + if (BlurEnabled && Win32Helper.IsBackdropSupported()) + { + SetWindowCornerPreference("Round"); + } + else + { + SetWindowCornerPreference("Default"); + AddDropShadowEffectToCurrentTheme(); + } + } + else + { + if (BlurEnabled && Win32Helper.IsBackdropSupported()) + { + SetWindowCornerPreference("Default"); + } + else + { + RemoveDropShadowEffectFromCurrentTheme(); + } } } - private bool IsBlurTheme() + private static void SetWindowCornerPreference(string cornerType) { - if (Environment.OSVersion.Version >= new Version(6, 2)) + Window mainWindow = Application.Current.MainWindow; + if (mainWindow == null) + return; + + Win32Helper.DWMSetCornerPreferenceForWindow(mainWindow, cornerType); + } + + // Get Background Color from WindowBorderStyle when there not color for BG. + // for theme has not "LightBG" or "DarkBG" case. + private Color GetWindowBorderStyleBackground(string theme) + { + var Resources = GetThemeResourceDictionary(theme); + var windowBorderStyle = (Style)Resources["WindowBorderStyle"]; + + var backgroundSetter = windowBorderStyle.Setters + .OfType() + .FirstOrDefault(s => s.Property == Border.BackgroundProperty); + + if (backgroundSetter != null) { - var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); + // Background's Value is DynamicColor Case + var backgroundValue = backgroundSetter.Value; - if (resource is bool) - return (bool)resource; + if (backgroundValue is SolidColorBrush solidColorBrush) + { + return solidColorBrush.Color; // Return SolidColorBrush's Color + } + else if (backgroundValue is DynamicResourceExtension dynamicResource) + { + // When DynamicResource Extension it is, Key is resource's name. + var resourceKey = backgroundSetter.Value.ToString(); + // find key in resource and return color. + if (Resources.Contains(resourceKey)) + { + var colorResource = Resources[resourceKey]; + if (colorResource is SolidColorBrush colorBrush) + { + return colorBrush.Color; + } + else if (colorResource is Color color) + { + return color; + } + } + } + } + + return Colors.Transparent; // Default is transparent + } + + private void ApplyPreviewBackground(Color? bgColor = null) + { + if (bgColor == null) return; + + // Copy the existing WindowBorderStyle + var previewStyle = new Style(typeof(Border)); + if (Application.Current.Resources.Contains("WindowBorderStyle")) + { + if (Application.Current.Resources["WindowBorderStyle"] is Style originalStyle) + { + foreach (var setter in originalStyle.Setters.OfType()) + { + previewStyle.Setters.Add(new Setter(setter.Property, setter.Value)); + } + } + } + + // Apply background color (remove transparency in color) + // WPF does not allow the use of an acrylic brush within the window's internal area, + // so transparency effects are not applied to the preview. + Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B); + previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor))); + + // The blur theme keeps the corner round fixed (applying DWM code to modify it causes rendering issues). + // The non-blur theme retains the previously set WindowBorderStyle. + if (BlurEnabled) + { + previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5))); + previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1))); + } + Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle; + } + + private void ColorizeWindow(string theme, BackdropTypes backdropType) + { + var dict = GetThemeResourceDictionary(theme); + if (dict == null) return; + + var mainWindow = Application.Current.MainWindow; + if (mainWindow == null) return; + + // Check if the theme supports blur + bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; + + // SystemBG value check (Auto, Light, Dark) + string systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto + + // Check the user's ColorScheme setting + string colorScheme = _settings.ColorScheme; + + // Check system dark mode setting (read AppsUseLightTheme value) + int themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1); + bool isSystemDark = themeValue == 0; + + // Final decision on whether to use dark mode + bool useDarkMode = false; + + // If systemBG is not "Auto", prioritize it over ColorScheme and set the mode based on systemBG value + if (systemBG == "Dark") + { + useDarkMode = true; // Dark + } + else if (systemBG == "Light") + { + useDarkMode = false; // Light + } + else if (systemBG == "Auto") + { + // If systemBG is "Auto", decide based on ColorScheme + if (colorScheme == "Dark") + useDarkMode = true; + else if (colorScheme == "Light") + useDarkMode = false; + else + useDarkMode = isSystemDark; // Auto (based on system setting) + } + + // Apply DWM Dark Mode + Win32Helper.DWMSetDarkModeForWindow(mainWindow, useDarkMode); + + Color LightBG; + Color DarkBG; + + // Retrieve LightBG value (fallback to WindowBorderStyle background color if not found) + try + { + LightBG = dict.Contains("LightBG") ? (Color)dict["LightBG"] : GetWindowBorderStyleBackground(theme); + } + catch (Exception) + { + LightBG = GetWindowBorderStyleBackground(theme); + } + + // Retrieve DarkBG value (fallback to LightBG if not found) + try + { + DarkBG = dict.Contains("DarkBG") ? (Color)dict["DarkBG"] : LightBG; + } + catch (Exception) + { + DarkBG = LightBG; + } + + // Select background color based on ColorScheme and SystemBG + Color selectedBG = useDarkMode ? DarkBG : LightBG; + ApplyPreviewBackground(selectedBG); + + bool isBlurAvailable = hasBlur && Win32Helper.IsBackdropSupported(); // Windows 11 미만이면 hasBlur를 강제 false + + if (!isBlurAvailable) + { + mainWindow.Background = Brushes.Transparent; + } + else + { + // Only set the background to transparent if the theme supports blur + if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt) + { + mainWindow.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); + } + else + { + mainWindow.Background = new SolidColorBrush(selectedBG); + } + } + } + + private static bool IsBlurTheme() + { + if (!Win32Helper.IsBackdropSupported()) // Windows 11 미만이면 무조건 false return false; - } - return false; + var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); + + return resource is bool b && b; } - private void SetWindowAccent(Window w, AccentState state) - { - var windowHelper = new WindowInteropHelper(w); - - // this determines the width of the main query window - w.Width = mainWindowWidth; - 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 } } diff --git a/Flow.Launcher.Core/Resource/ThemeManager.cs b/Flow.Launcher.Core/Resource/ThemeManager.cs deleted file mode 100644 index 71f9acaa5..000000000 --- a/Flow.Launcher.Core/Resource/ThemeManager.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Flow.Launcher.Core.Resource -{ - 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; - } - } - } -} diff --git a/Flow.Launcher.Core/Resource/TranslationConverter.cs b/Flow.Launcher.Core/Resource/TranslationConverter.cs new file mode 100644 index 000000000..eb0032758 --- /dev/null +++ b/Flow.Launcher.Core/Resource/TranslationConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Resource +{ + public class TranslationConverter : IValueConverter + { + // 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(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var key = value.ToString(); + if (string.IsNullOrEmpty(key)) return key; + return API.GetTranslation(key); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => + throw new InvalidOperationException(); + } +} diff --git a/Flow.Launcher.Core/Storage/IRemovable.cs b/Flow.Launcher.Core/Storage/IRemovable.cs new file mode 100644 index 000000000..bcf1cdd5e --- /dev/null +++ b/Flow.Launcher.Core/Storage/IRemovable.cs @@ -0,0 +1,19 @@ +namespace Flow.Launcher.Core.Storage; + +/// +/// Remove storage instances from instance +/// +public interface IRemovable +{ + /// + /// Remove all instances of one plugin + /// + /// + public void RemovePluginSettings(string assemblyName); + + /// + /// Remove all instances of one plugin + /// + /// + public void RemovePluginCaches(string cacheDirectory); +} diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 76713ce2a..83d4fd9e0 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -1,64 +1,68 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using System.Windows; -using JetBrains.Annotations; -using Squirrel; -using Flow.Launcher.Core.Resource; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using System.Text.Json.Serialization; +using JetBrains.Annotations; +using Squirrel; 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; } - public async Task UpdateApp(IPublicAPI api, bool silentUpdate = true) + private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1); + + public async Task UpdateAppAsync(bool silentUpdate = true) { + await UpdateLock.WaitAsync().ConfigureAwait(false); try { - UpdateInfo newUpdateInfo; - if (!silentUpdate) - api.ShowMsg(api.GetTranslation("pleaseWait"), - api.GetTranslation("update_flowlauncher_update_check")); - - using var updateManager = await GitHubUpdateManager(GitHubRepository).ConfigureAwait(false); + _api.ShowMsg(_api.GetTranslation("pleaseWait"), + _api.GetTranslation("update_flowlauncher_update_check")); + using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false); // UpdateApp CheckForUpdate will return value only if the app is squirrel installed - newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false); + var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false); var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); var currentVersion = Version.Parse(Constant.Version); - Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>"); + Log.Info($"|Updater.UpdateApp|Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>"); 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); @@ -66,33 +70,41 @@ 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"), - DataLocation.PortableDataPath, - targetDestination)); + var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}"; + 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)); } else { 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 || e is WebException || e is 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); - api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), - api.GetTranslation("update_flowlauncher_check_connection")); - return; + 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")); + } + finally + { + UpdateLock.Release(); } } @@ -109,19 +121,22 @@ namespace Flow.Launcher.Core public string HtmlUrl { get; [UsedImplicitly] set; } } - /// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs - private async Task GitHubUpdateManager(string repository) + // https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs + private static async Task GitHubUpdateManagerAsync(string repository) { var uri = new Uri(repository); var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; - var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false); + await using var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false); - var releases = await System.Text.Json.JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); + var releases = await JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First(); var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/"); - var client = new WebClient { Proxy = Http.WebProxy }; + var client = new WebClient + { + Proxy = Http.WebProxy + }; var downloader = new FileDownloader(client); var manager = new UpdateManager(latestUrl, urlDownloader: downloader); @@ -129,12 +144,21 @@ namespace Flow.Launcher.Core return manager; } - public string NewVersinoTips(string version) + private string NewVersionTips(string version) { - var translater = InternationalizationManager.Instance; - var tips = string.Format(translater.GetTranslation("newVersionTips"), version); + var tips = string.Format(_api.GetTranslation("newVersionTips"), version); + return tips; } + private static string Formatted(T t) + { + var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions + { + WriteIndented = true + }); + + return formatted; + } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.CrashReporter/Flow.Launcher.CrashReporter.csproj b/Flow.Launcher.CrashReporter/Flow.Launcher.CrashReporter.csproj deleted file mode 100644 index 1e0a3fe52..000000000 --- a/Flow.Launcher.CrashReporter/Flow.Launcher.CrashReporter.csproj +++ /dev/null @@ -1,115 +0,0 @@ - - - - - Debug - AnyCPU - {2FEB2298-7653-4009-B1EA-FFFB1A768BCC} - Library - Properties - Flow.Launcher.CrashReporter - Flow.Launcher.CrashReporter - v4.5.2 - 512 - ..\ - - - - true - full - false - ..\Output\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - ..\Output\Release\ - TRACE - prompt - 4 - false - - - - ..\packages\Exceptionless.1.5.2121\lib\net45\Exceptionless.dll - True - - - ..\packages\Exceptionless.1.5.2121\lib\net45\Exceptionless.Models.dll - True - - - ..\packages\JetBrains.Annotations.10.1.4\lib\net20\JetBrains.Annotations.dll - True - - - - - - - - - - - - - - - Properties\SolutionAssemblyInfo.cs - - - - - ReportWindow.xaml - - - - - Designer - MSBuild:Compile - - - - - {B749F0DB-8E75-47DB-9E5E-265D16D0C0D2} - Flow.Launcher.Core - - - {4fd29318-a8ab-4d8f-aa47-60bc241b8da3} - Flow.Launcher.Infrastructure - - - - - PreserveNewest - - - PreserveNewest - - - - - PreserveNewest - - - - - PreserveNewest - - - - - - - - \ No newline at end of file diff --git a/Flow.Launcher.CrashReporter/Images/app_error.png b/Flow.Launcher.CrashReporter/Images/app_error.png deleted file mode 100644 index 70599f504..000000000 Binary files a/Flow.Launcher.CrashReporter/Images/app_error.png and /dev/null differ diff --git a/Flow.Launcher.CrashReporter/Images/crash_go.png b/Flow.Launcher.CrashReporter/Images/crash_go.png deleted file mode 100644 index 2dd2c45f2..000000000 Binary files a/Flow.Launcher.CrashReporter/Images/crash_go.png and /dev/null differ diff --git a/Flow.Launcher.CrashReporter/Images/crash_stop.png b/Flow.Launcher.CrashReporter/Images/crash_stop.png deleted file mode 100644 index 022fbc197..000000000 Binary files a/Flow.Launcher.CrashReporter/Images/crash_stop.png and /dev/null differ diff --git a/Flow.Launcher.CrashReporter/Images/crash_warning.png b/Flow.Launcher.CrashReporter/Images/crash_warning.png deleted file mode 100644 index 8d29625ee..000000000 Binary files a/Flow.Launcher.CrashReporter/Images/crash_warning.png and /dev/null differ diff --git a/Flow.Launcher.CrashReporter/Properties/AssemblyInfo.cs b/Flow.Launcher.CrashReporter/Properties/AssemblyInfo.cs deleted file mode 100644 index acf2adca1..000000000 --- a/Flow.Launcher.CrashReporter/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("Flow.Launcher.CrashReporter")] -[assembly: Guid("0ea3743c-2c0d-4b13-b9ce-e5e1f85aea23")] \ No newline at end of file diff --git a/Flow.Launcher.CrashReporter/packages.config b/Flow.Launcher.CrashReporter/packages.config deleted file mode 100644 index ddad0d3b3..000000000 --- a/Flow.Launcher.CrashReporter/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index a5b89c300..13da9f79f 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.Reflection; @@ -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,24 +20,41 @@ 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 const string Documentation = "https://flow-launcher.github.io/docs/#/usage-tips"; + public static readonly string Dev = "Dev"; + public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips"; public static readonly int ThumbnailSize = 64; private static readonly string ImagesDirectory = Path.Combine(ProgramDirectory, "Images"); public static readonly string DefaultIcon = Path.Combine(ImagesDirectory, "app.png"); public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png"); public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png"); + public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png"); + public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png"); + public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png"); public static string PythonPath; + public static string NodePath; public static readonly string QueryTextBoxIconImagePath = $"{ProgramDirectory}\\Images\\mainsearch.svg"; - public const string DefaultTheme = "Darker"; + public const string DefaultTheme = "Win11Light"; + + public const string Light = "Light"; + public const string Dark = "Dark"; + public const string System = "System"; public const string Themes = "Themes"; + public const string Settings = "Settings"; + public const string Logs = "Logs"; + public const string Cache = "Cache"; - public const string Website = "https://flow-launcher.github.io"; + public const string Website = "https://flowlauncher.com"; + public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher"; + public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher"; + public const string Docs = "https://flowlauncher.com/docs"; + + public const string SystemLanguageCode = "system"; } } diff --git a/Flow.Launcher.Infrastructure/Exception/ExceptionFormatter.cs b/Flow.Launcher.Infrastructure/Exception/ExceptionFormatter.cs index 3849f6e30..025109e58 100644 --- a/Flow.Launcher.Infrastructure/Exception/ExceptionFormatter.cs +++ b/Flow.Launcher.Infrastructure/Exception/ExceptionFormatter.cs @@ -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()) @@ -78,7 +78,7 @@ namespace Flow.Launcher.Infrastructure.Exception sb.AppendLine(); sb.AppendLine("## Assemblies - " + AppDomain.CurrentDomain.FriendlyName); sb.AppendLine(); - foreach (var ass in AppDomain.CurrentDomain.GetAssemblies().OrderBy(o => o.GlobalAssemblyCache ? 50 : 0)) + foreach (var ass in AppDomain.CurrentDomain.GetAssemblies()) { sb.Append("* "); sb.Append(ass.FullName); @@ -166,11 +166,41 @@ namespace Flow.Launcher.Infrastructure.Exception } return result; } - catch (System.Exception e) + catch { return new List(); } } + + 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"; + } + } } } diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs new file mode 100644 index 000000000..1085cc833 --- /dev/null +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Windows.Win32; + +namespace Flow.Launcher.Infrastructure +{ + public static class FileExplorerHelper + { + /// + /// Gets the path of the file explorer that is currently in the foreground + /// + public static string GetActiveExplorerPath() + { + var explorerWindow = GetActiveExplorer(); + string locationUrl = explorerWindow?.LocationURL; + return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null; + } + + /// + /// Get directory path from a file path + /// + private static string GetDirectoryPath(string path) + { + if (!path.EndsWith("\\")) + { + return path + "\\"; + } + + return path; + } + + /// + /// Gets the file explorer that is currently in the foreground + /// + 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(); + 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; + } + + /// + /// 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. + /// + private static IEnumerable GetZOrder(List 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; + } + } +} diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index aea43506e..f02b2297f 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -1,7 +1,7 @@ - + - net5.0-windows + net7.0-windows {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3} Library true @@ -11,6 +11,7 @@ false false false + true @@ -21,7 +22,6 @@ DEBUG;TRACE prompt 4 - true false @@ -32,10 +32,13 @@ TRACE prompt 4 - false false + + + + @@ -50,24 +53,26 @@ - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - + + + + + - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Helper.cs b/Flow.Launcher.Infrastructure/Helper.cs index faa4c93b5..b02d84ca7 100644 --- a/Flow.Launcher.Infrastructure/Helper.cs +++ b/Flow.Launcher.Infrastructure/Helper.cs @@ -1,22 +1,15 @@ -using System; -using System.IO; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization; +#nullable enable + +using System; namespace Flow.Launcher.Infrastructure { public static class Helper { - static Helper() - { - jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - } - /// /// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy /// - public static T NonNull(this T obj) + public static T NonNull(this T? obj) { if (obj == null) { @@ -35,55 +28,5 @@ namespace Flow.Launcher.Infrastructure throw new NullReferenceException(); } } - - public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory) - { - if (!Directory.Exists(dataDirectory)) - { - Directory.CreateDirectory(dataDirectory); - } - - foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory)) - { - var data = Path.GetFileName(bundledDataPath); - var dataPath = Path.Combine(dataDirectory, data.NonNull()); - if (!File.Exists(dataPath)) - { - File.Copy(bundledDataPath, dataPath); - } - else - { - var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc; - var time2 = new FileInfo(dataPath).LastWriteTimeUtc; - if (time1 != time2) - { - File.Copy(bundledDataPath, dataPath, true); - } - } - } - } - - public static void ValidateDirectory(string path) - { - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - } - - private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions - { - WriteIndented = true - }; - - public static string Formatted(this T t) - { - var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions - { - WriteIndented = true - }); - - return formatted; - } } } diff --git a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs index e92a93c12..b2a140755 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs @@ -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 { @@ -9,59 +13,47 @@ namespace Flow.Launcher.Infrastructure.Hotkey /// Listens keyboard globally. /// Uses WH_KEYBOARD_LL. /// - public class GlobalHotkey : IDisposable + public unsafe class GlobalHotkey : IDisposable { - private static GlobalHotkey instance; - private InterceptKeys.LowLevelKeyboardProc hookedLowLevelKeyboardProc; - private IntPtr hookId = IntPtr.Zero; + private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback; + private static readonly UnhookWindowsHookExSafeHandle hookId; + public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state); - public event KeyboardCallback hookedKeyboardCallback; + internal static Func 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; - - public static GlobalHotkey Instance + static GlobalHotkey() { - get - { - if (instance == null) - { - instance = new GlobalHotkey(); - } - return instance; - } - } - - private GlobalHotkey() - { - // We have to store the LowLevelKeyboardProc, so that it is not garbage collected runtime - hookedLowLevelKeyboardProc = LowLevelKeyboardProc; // Set the hook - hookId = InterceptKeys.SetHook(hookedLowLevelKeyboardProc); + hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL); } - public SpecialKeyState CheckModifiers() + 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; @@ -70,38 +62,38 @@ namespace Flow.Launcher.Infrastructure.Hotkey return state; } - [MethodImpl(MethodImplOptions.NoInlining)] - private 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() + { + hookId.Dispose(); } ~GlobalHotkey() { Dispose(); } - - public void Dispose() - { - InterceptKeys.UnhookWindowsHookEx(hookId); - } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs index 5bd97714c..25bc75a56 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs @@ -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 specialSymbolDictionary = new Dictionary + private static readonly Dictionary specialSymbolDictionary = new Dictionary { - {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 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? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey); + KeyValuePair? 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 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; + /// + /// Validate hotkey + /// + /// Try to validate hotkey as a KeyGesture. + /// + 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); } } } diff --git a/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs b/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs new file mode 100644 index 000000000..448a70d19 --- /dev/null +++ b/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Flow.Launcher.Infrastructure.Hotkey; + +/// +/// Interface that you should implement in your settings class to be able to pass it to +/// Flow.Launcher.HotkeyControlDialog. It allows the dialog to display the hotkeys that have already been +/// registered, and optionally provide a way to unregister them. +/// +public interface IHotkeySettings +{ + /// + /// A list of hotkeys that have already been registered. The dialog will display these hotkeys and provide a way to + /// unregister them. + /// + public List RegisteredHotkeys { get; } +} diff --git a/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs b/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs deleted file mode 100644 index c45e685f3..000000000 --- a/Flow.Launcher.Infrastructure/Hotkey/InterceptKeys.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace Flow.Launcher.Infrastructure.Hotkey -{ - internal static class InterceptKeys - { - public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam); - - private const int WH_KEYBOARD_LL = 13; - - public static IntPtr SetHook(LowLevelKeyboardProc 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, LowLevelKeyboardProc 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); - } -} \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs b/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs deleted file mode 100644 index 15e306883..000000000 --- a/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Flow.Launcher.Infrastructure.Hotkey -{ - public enum KeyEvent - { - /// - /// Key down - /// - WM_KEYDOWN = 256, - - /// - /// Key up - /// - WM_KEYUP = 257, - - /// - /// System key up - /// - WM_SYSKEYUP = 261, - - /// - /// System key down - /// - WM_SYSKEYDOWN = 260 - } -} \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs b/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs new file mode 100644 index 000000000..b6a10fc30 --- /dev/null +++ b/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs @@ -0,0 +1,119 @@ +using System; + +namespace Flow.Launcher.Infrastructure.Hotkey; + +#nullable enable + +/// +/// Represents a hotkey that has been registered. Used in Flow.Launcher.HotkeyControlDialog via +/// and to display errors if user tries to register a hotkey +/// that has already been registered, and optionally provides a way to unregister the hotkey. +/// +public record RegisteredHotkeyData +{ + /// + /// representation of this hotkey. + /// + public HotkeyModel Hotkey { get; } + + /// + /// String key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + public string DescriptionResourceKey { get; } + + /// + /// Array of values that will replace {0}, {1}, {2}, etc. in the localized string found via + /// . + /// + public object?[] DescriptionFormatVariables { get; } = Array.Empty(); + + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that + /// this hotkey can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public Action? RemoveHotkey { get; } + + /// + /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in + /// descriptionResourceKey doesn't need any arguments for string.Format. If it does, + /// use one of the other constructors. + /// + /// + /// The hotkey this class will represent. + /// Example values: F1, Ctrl+Shift+Enter + /// + /// + /// The key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey + /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action? removeHotkey = null) + { + Hotkey = new HotkeyModel(hotkey); + DescriptionResourceKey = descriptionResourceKey; + RemoveHotkey = removeHotkey; + } + + /// + /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in + /// descriptionResourceKey needs exactly one argument for string.Format. + /// + /// + /// The hotkey this class will represent. + /// Example values: F1, Ctrl+Shift+Enter + /// + /// + /// The key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + /// + /// The value that will replace {0} in the localized string found via description. + /// + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey + /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public RegisteredHotkeyData( + string hotkey, string descriptionResourceKey, object? descriptionFormatVariable, Action? removeHotkey = null + ) + { + Hotkey = new HotkeyModel(hotkey); + DescriptionResourceKey = descriptionResourceKey; + DescriptionFormatVariables = new[] { descriptionFormatVariable }; + RemoveHotkey = removeHotkey; + } + + /// + /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in + /// needs multiple arguments for string.Format. + /// + /// + /// The hotkey this class will represent. + /// Example values: F1, Ctrl+Shift+Enter + /// + /// + /// The key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + /// + /// Array of values that will replace {0}, {1}, {2}, etc. + /// in the localized string found via description. + /// + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey + /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public RegisteredHotkeyData( + string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables, Action? removeHotkey = null + ) + { + Hotkey = new HotkeyModel(hotkey); + DescriptionResourceKey = descriptionResourceKey; + DescriptionFormatVariables = descriptionFormatVariables; + RemoveHotkey = removeHotkey; + } +} diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index b45b6adcd..030aff7cf 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -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().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 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 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); } /// @@ -130,28 +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; } /// - /// 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. + /// + /// The Uri the request is sent to. + /// An HTTP completion option value that indicates when the operation should be considered completed. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation + /// + public static Task GetStreamAsync([NotNull] string url, + CancellationToken token = default) => GetStreamAsync(new Uri(url), token); + + + /// + /// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation. /// /// + /// /// - public static async Task GetStreamAsync([NotNull] string url, CancellationToken token = default) + public static async Task 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 GetResponseAsync(string url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, + CancellationToken token = default) + => await GetResponseAsync(new Uri(url), completionOption, token); + + public static async Task GetResponseAsync([NotNull] Uri url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, + CancellationToken token = default) + { + Log.Debug($"|Http.Get|Url <{url}>"); + return await client.GetAsync(url, completionOption, token); + } + + /// + /// Asynchrously send an HTTP request. + /// + public static async Task SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken token = default) + { + return await client.SendAsync(request, completionOption, token); } } } diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index bb7ec6817..b8c12868b 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -1,84 +1,65 @@ -using System; -using System.Collections.Concurrent; +using System; using System.Collections.Generic; using System.Linq; 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 Data { get; private set; } = new ConcurrentDictionary(); - private const int permissibleFactor = 2; - - public void Initialization(Dictionary usage) + private const int MaxCached = 150; + + 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; - } - ); - - // 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) - { - // To delete the images from the data dictionary based on the resizing of the Usage Dictionary. - foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key)) - Data.TryRemove(key, out _); - } + CacheManager.AddOrUpdate((path, isFullImage), value); } } - public bool ContainsKey(string key) + public async ValueTask GetOrAddAsync(string key, + Func<(string, bool), Task> valueFactory, + bool isFullImage = false) { - return 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; } /// @@ -86,7 +67,14 @@ namespace Flow.Launcher.Infrastructure.Image /// public int UniqueImagesInCache() { - return Data.Values.Select(x => x.imageSource).Distinct().Count(); + return CacheManager.Select(x => x.Value) + .Distinct() + .Count(); + } + + public IEnumerable> EnumerateEntries() + { + return CacheManager; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Image/ImageHashGenerator.cs b/Flow.Launcher.Infrastructure/Image/ImageHashGenerator.cs index 736133052..2611e99e8 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageHashGenerator.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageHashGenerator.cs @@ -14,30 +14,23 @@ namespace Flow.Launcher.Infrastructure.Image { public string GetHashFromImage(ImageSource imageSource) { - if (!(imageSource is BitmapSource image)) + if (imageSource is not BitmapSource image) { return null; } try { - using (var outStream = new MemoryStream()) - { - // PngBitmapEncoder enc2 = new PngBitmapEncoder(); - // enc2.Frames.Add(BitmapFrame.Create(tt)); - - var enc = new JpegBitmapEncoder(); - var bitmapFrame = BitmapFrame.Create(image); - bitmapFrame.Freeze(); - enc.Frames.Add(bitmapFrame); - enc.Save(outStream); - var byteArray = outStream.GetBuffer(); - using (var sha1 = new SHA1CryptoServiceProvider()) - { - var hash = Convert.ToBase64String(sha1.ComputeHash(byteArray)); - return hash; - } - } + using var outStream = new MemoryStream(); + var enc = new JpegBitmapEncoder(); + var bitmapFrame = BitmapFrame.Create(image); + bitmapFrame.Freeze(); + enc.Frames.Add(bitmapFrame); + enc.Save(outStream); + var byteArray = outStream.GetBuffer(); + using var sha1 = SHA1.Create(); + var hash = Convert.ToBase64String(sha1.ComputeHash(byteArray)); + return hash; } catch { @@ -46,4 +39,4 @@ namespace Flow.Launcher.Infrastructure.Image } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index ac333d567..9e31d2b4e 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -1,79 +1,110 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; -using Flow.Launcher.Infrastructure.Logger; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Plugin; +using SharpVectors.Converters; +using SharpVectors.Renderers.Wpf; namespace Flow.Launcher.Infrastructure.Image { public static class ImageLoader { - private static readonly ImageCache ImageCache = new ImageCache(); - private static BinaryStorage> _storage; - private static readonly ConcurrentDictionary GuidToKey = new ConcurrentDictionary(); + // 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(); + + private static readonly string ClassName = nameof(ImageLoader); + + private static readonly ImageCache ImageCache = new(); + private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1); + private static BinaryStorage> _storage; + private static readonly ConcurrentDictionary GuidToKey = new(); private static IImageHashGenerator _hashGenerator; - private static bool EnableImageHash = true; - public static ImageSource DefaultImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon)); + private static readonly bool EnableImageHash = true; + public static ImageSource 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; + public const int FullImageSize = 320; + private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" }; + private static readonly string SvgExtension = ".svg"; - private static readonly string[] ImageExtensions = + public static async Task InitializeAsync() { - ".png", - ".jpg", - ".jpeg", - ".gif", - ".bmp", - ".tiff", - ".ico" - }; - - public static void Initialize() - { - _storage = new BinaryStorage>("Image"); + _storage = new BinaryStorage>("Image"); _hashGenerator = new ImageHashGenerator(); - var usage = LoadStorageToConcurrentDictionary(); + var usage = await LoadStorageToConcurrentDictionaryAsync(); + _storage.ClearData(); + + 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 API.StopwatchLogInfoAsync(ClassName, "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()}"); + API.LogInfo(ClassName, $"Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); }); } - public static void Save() + public static async Task SaveAsync() { - 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()); + } + catch (System.Exception e) + { + API.LogException(ClassName, "Failed to save image cache to file", e); + } + finally + { + storageLock.Release(); } } - private static ConcurrentDictionary LoadStorageToConcurrentDictionary() + public static async Task WaitSaveAsync() { - lock (_storage) - { - var loaded = _storage.TryLoad(new Dictionary()); + await storageLock.WaitAsync(); + storageLock.Release(); + } - return new ConcurrentDictionary(loaded); + private static async Task> LoadStorageToConcurrentDictionaryAsync() + { + await storageLock.WaitAsync(); + try + { + return await _storage.TryLoadAsync(new List<(string, bool)>()); + } + finally + { + storageLock.Release(); } } @@ -95,11 +126,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 LoadInternalAsync(string path, bool loadFullImage = false) { ImageResult imageResult; @@ -107,26 +139,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) { @@ -137,11 +176,11 @@ namespace Flow.Launcher.Infrastructure.Image } catch (System.Exception e2) { - 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); + API.LogException(ClassName, $"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e); + API.LogException(ClassName, $"|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 +188,29 @@ namespace Flow.Launcher.Infrastructure.Image return imageResult; } + private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult) + { + // Download image from url + await using var resp = await Http.Http.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 +219,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,27 +234,51 @@ namespace Flow.Launcher.Infrastructure.Image type = ImageType.ImageFile; if (loadFullImage) { - image = LoadFullImage(path); + try + { + image = LoadFullImage(path); + type = ImageType.FullImageFile; + } + catch (NotSupportedException ex) + { + image = Image; + type = ImageType.Error; + API.LogException(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex); + } } 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); } } + else if (extension == SvgExtension) + { + try + { + image = LoadSvgImage(path, loadFullImage); + type = ImageType.FullImageFile; + } + catch (System.Exception ex) + { + image = Image; + type = ImageType.Error; + API.LogException(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex); + } + } 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,57 +290,139 @@ 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 LoadAsync(string path, bool loadFullImage = false, bool cacheImage = true) + { + 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 + else if (cacheImage) + { + // save guid key GuidToKey[hash] = path; } } - // update cache - ImageCache[path] = img; + if (cacheImage) + { + // update cache + ImageCache[path, loadFullImage] = img; + } } - return img; } - private static BitmapImage LoadFullImage(string path) + private static ImageSource LoadFullImage(string path) { BitmapImage image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; image.UriSource = new Uri(path); + image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; image.EndInit(); + + if (image.PixelWidth > FullImageSize) + { + BitmapImage resizedWidth = new BitmapImage(); + resizedWidth.BeginInit(); + resizedWidth.CacheOption = BitmapCacheOption.OnLoad; + resizedWidth.UriSource = new Uri(path); + resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; + resizedWidth.DecodePixelWidth = FullImageSize; + resizedWidth.EndInit(); + + if (resizedWidth.PixelHeight > FullImageSize) + { + BitmapImage resizedHeight = new BitmapImage(); + resizedHeight.BeginInit(); + resizedHeight.CacheOption = BitmapCacheOption.OnLoad; + resizedHeight.UriSource = new Uri(path); + resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; + resizedHeight.DecodePixelHeight = FullImageSize; + resizedHeight.EndInit(); + return resizedHeight; + } + + return resizedWidth; + } + return image; } + + private static ImageSource LoadSvgImage(string path, bool loadFullImage = false) + { + // Set up drawing settings + var desiredHeight = loadFullImage ? FullImageSize : SmallIconSize; + var drawingSettings = new WpfDrawingSettings + { + IncludeRuntime = true, + // Set IgnoreRootViewbox to false to respect the SVG's viewBox + IgnoreRootViewbox = false + }; + + // Load and render the SVG + var converter = new FileSvgReader(drawingSettings); + var drawing = converter.Read(new Uri(path)); + + // Calculate scale to achieve desired height + var drawingBounds = drawing.Bounds; + if (drawingBounds.Height <= 0) + { + throw new InvalidOperationException($"Invalid SVG dimensions: Height must be greater than zero in {path}"); + } + var scale = desiredHeight / drawingBounds.Height; + var scaledWidth = drawingBounds.Width * scale; + var scaledHeight = drawingBounds.Height * scale; + + // Convert the Drawing to a Bitmap + var drawingVisual = new DrawingVisual(); + using (DrawingContext drawingContext = drawingVisual.RenderOpen()) + { + drawingContext.PushTransform(new ScaleTransform(scale, scale)); + drawingContext.DrawDrawing(drawing); + } + + // Create a RenderTargetBitmap to hold the rendered image + var bitmap = new RenderTargetBitmap( + (int)Math.Ceiling(scaledWidth), + (int)Math.Ceiling(scaledHeight), + 96, // DpiX + 96, // DpiY + PixelFormats.Pbgra32); + bitmap.Render(drawingVisual); + + return bitmap; + } } } diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 247238bb6..b98ea50fe 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -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 { + /// + /// Subclass of + /// [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; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 26e305ace..807d631c7 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -1,27 +1,24 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using Flow.Launcher.Infrastructure.UserSettings; 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 { public static class Log { - public const string DirectoryName = "Logs"; + public const string DirectoryName = Constant.Logs; public static string CurrentLogDirectory { get; } static Log() { - CurrentLogDirectory = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version); + CurrentLogDirectory = DataLocation.VersionLogDirectory; if (!Directory.Exists(CurrentLogDirectory)) { Directory.CreateDirectory(CurrentLogDirectory); @@ -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 } } - /// example: "|prefix|unprefixed" + /// Example: "|ClassName.MethodName|Message" + /// Example: "|ClassName.MethodName|Message" /// Exception public static void Exception(string message, System.Exception e) { @@ -158,7 +181,7 @@ namespace Flow.Launcher.Infrastructure.Logger #endif } - /// example: "|prefix|unprefixed" + /// 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); } - /// example: "|prefix|unprefixed" + /// 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); } - /// example: "|prefix|unprefixed" + /// 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); } - /// example: "|prefix|unprefixed" + /// Example: "|ClassName.MethodName|Message" public static void Warn(string message) { LogInternal(message, LogLevel.Warn); } } -} \ No newline at end of file + + public enum LOGLEVEL + { + DEBUG, + INFO + } +} diff --git a/Flow.Launcher.Infrastructure/MonitorInfo.cs b/Flow.Launcher.Infrastructure/MonitorInfo.cs new file mode 100644 index 000000000..3221708c1 --- /dev/null +++ b/Flow.Launcher.Infrastructure/MonitorInfo.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System; +using System.Runtime.InteropServices; +using System.Windows; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Flow.Launcher.Infrastructure; + +/// +/// Contains full information about a display monitor. +/// Codes are edited from: . +/// +internal class MonitorInfo +{ + /// + /// Gets the display monitors (including invisible pseudo-monitors associated with the mirroring drivers). + /// + /// A list of display monitors + public static unsafe IList GetDisplayMonitors() + { + var monitorCount = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CMONITORS); + var list = new List(monitorCount); + var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) => + { + list.Add(new MonitorInfo(monitor, rect)); + return true; + }); + var dwData = new LPARAM(); + var hdc = new HDC(); + bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData); + if (!ok) + { + Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error()); + } + return list; + } + + /// + /// Gets the display monitor that is nearest to a given window. + /// + /// Window handle + /// The display monitor that is nearest to a given window, or null if no monitor is found. + public static unsafe MonitorInfo GetNearestDisplayMonitor(HWND hwnd) + { + var nearestMonitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); + MonitorInfo nearestMonitorInfo = null; + var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) => + { + if (monitor == nearestMonitor) + { + nearestMonitorInfo = new MonitorInfo(monitor, rect); + return false; + } + return true; + }); + var dwData = new LPARAM(); + var hdc = new HDC(); + bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData); + if (!ok) + { + Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error()); + } + return nearestMonitorInfo; + } + + private readonly HMONITOR _monitor; + + internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect) + { + RectMonitor = + new Rect(new Point(rect->left, rect->top), + new Point(rect->right, rect->bottom)); + _monitor = monitor; + var info = new MONITORINFOEXW() { monitorInfo = new MONITORINFO() { cbSize = (uint)sizeof(MONITORINFOEXW) } }; + GetMonitorInfo(monitor, ref info); + RectWork = + new Rect(new Point(info.monitorInfo.rcWork.left, info.monitorInfo.rcWork.top), + new Point(info.monitorInfo.rcWork.right, info.monitorInfo.rcWork.bottom)); + Name = new string(info.szDevice.AsSpan()).Replace("\0", "").Trim(); + } + + /// + /// Gets the name of the display. + /// + public string Name { get; } + + /// + /// Gets the display monitor rectangle, expressed in virtual-screen coordinates. + /// + /// + /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. + /// + public Rect RectMonitor { get; } + + /// + /// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates. + /// + /// + /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. + /// + public Rect RectWork { get; } + + /// + /// Gets if the monitor is the the primary display monitor. + /// + public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(IntPtr.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY); + + /// + public override string ToString() => $"{Name} {RectMonitor.Width}x{RectMonitor.Height}"; + + private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi) + { + fixed (MONITORINFOEXW* lpmiLocal = &lpmi) + { + var lpmiBase = (MONITORINFO*)lpmiLocal; + var __result = PInvoke.GetMonitorInfo(hMonitor, lpmiBase); + return __result; + } + } +} diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt new file mode 100644 index 000000000..18b206022 --- /dev/null +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -0,0 +1,56 @@ +SHCreateItemFromParsingName +DeleteObject +IShellItem +IShellItemImageFactory +S_OK + +SetWindowsHookEx +UnhookWindowsHookEx +CallNextHookEx +GetModuleHandle +GetKeyState +VIRTUAL_KEY + +EnumWindows + +DwmSetWindowAttribute +DWM_SYSTEMBACKDROP_TYPE +DWM_WINDOW_CORNER_PREFERENCE + +MAX_PATH +SystemParametersInfo + +SetForegroundWindow + +GetWindowLong +GetForegroundWindow +GetDesktopWindow +GetShellWindow +GetWindowRect +GetClassName +FindWindowEx +WINDOW_STYLE + +SetLastError +WINDOW_EX_STYLE + +GetSystemMetrics +EnumDisplayMonitors +MonitorFromWindow +GetMonitorInfo +MONITORINFOEXW + +WM_ENTERSIZEMOVE +WM_EXITSIZEMOVE + +GetKeyboardLayout +GetWindowThreadProcessId +ActivateKeyboardLayout +GetKeyboardLayoutList +PostMessage +WM_INPUTLANGCHANGEREQUEST +INPUTLANGCHANGE_FORWARD +LOCALE_TRANSIENT_KEYBOARD1 +LOCALE_TRANSIENT_KEYBOARD2 +LOCALE_TRANSIENT_KEYBOARD3 +LOCALE_TRANSIENT_KEYBOARD4 \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs new file mode 100644 index 000000000..1a72ab7a6 --- /dev/null +++ b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Windows.Win32; + +// Edited from: https://github.com/files-community/Files +internal static partial class PInvoke +{ + [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] + static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); + + [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] + static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); + + // NOTE: + // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa. + // For more info, visit https://github.com/microsoft/CsWin32/issues/882 + public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong) + { + return sizeof(nint) is 4 + ? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong) + : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); + } +} diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 6c2a94e82..8eaa757be 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -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 { @@ -15,7 +16,7 @@ namespace Flow.Launcher.Infrastructure private List originalIndexs = new List(); private List translatedIndexs = new List(); - private int translaedLength = 0; + private int translatedLength = 0; public string key { get; private set; } @@ -32,13 +33,13 @@ namespace Flow.Launcher.Infrastructure originalIndexs.Add(originalIndex); translatedIndexs.Add(translatedIndex); translatedIndexs.Add(translatedIndex + length); - translaedLength += length - 1; + translatedLength += length - 1; } public int MapToOriginalIndex(int translatedIndex) { if (translatedIndex > translatedIndexs.Last()) - return translatedIndex - translaedLength - 1; + return translatedIndex - translatedLength - 1; int lowerBound = 0; int upperBound = originalIndexs.Count - 1; @@ -83,7 +84,7 @@ namespace Flow.Launcher.Infrastructure translatedIndex < translatedIndexs[upperBound * 2]) { int indexDef = 0; - + for (int j = 0; j < upperBound; j++) { indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2]; @@ -102,9 +103,24 @@ namespace Flow.Launcher.Infrastructure } } + /// + /// Translate a language to English letters using a given rule. + /// public interface IAlphabet { + /// + /// Translate a string to English letters, using a given rule. + /// + /// String to translate. + /// public (string translation, TranslationMapping map) Translate(string stringToTranslate); + + /// + /// Determine if a string can be translated to English letter with this Alphabet. + /// + /// String to translate. + /// + public bool CanBeTranslated(string stringToTranslate); } public class PinyinAlphabet : IAlphabet @@ -114,68 +130,80 @@ namespace Flow.Launcher.Infrastructure private Settings _settings; - public void Initialize([NotNull] Settings settings) + public PinyinAlphabet() + { + Initialize(Ioc.Default.GetRequiredService()); + } + + private void Initialize([NotNull] Settings settings) { _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } + public bool CanBeTranslated(string stringToTranslate) + { + return WordsHelper.HasChinese(stringToTranslate); + } + public (string translation, TranslationMapping map) Translate(string content) { if (_settings.ShouldUsePinyin) { if (!_pinyinCache.ContainsKey(content)) { - if (WordsHelper.HasChinese(content)) - { - var resultList = WordsHelper.GetPinyinList(content); - - StringBuilder resultBuilder = new StringBuilder(); - TranslationMapping map = new TranslationMapping(); - - bool pre = false; - - for (int i = 0; i < resultList.Length; i++) - { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) - { - map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1); - resultBuilder.Append(' '); - resultBuilder.Append(resultList[i]); - pre = true; - } - else - { - if (pre) - { - pre = false; - resultBuilder.Append(' '); - } - - resultBuilder.Append(resultList[i]); - } - } - - map.endConstruct(); - - var key = resultBuilder.ToString(); - map.setKey(key); - - return _pinyinCache[content] = (key, map); - } - else - { - return (content, null); - } + return BuildCacheFromContent(content); } else { return _pinyinCache[content]; } } + return (content, null); + } + + private (string translation, TranslationMapping map) BuildCacheFromContent(string content) + { + if (WordsHelper.HasChinese(content)) + { + var resultList = WordsHelper.GetPinyinList(content); + + StringBuilder resultBuilder = new StringBuilder(); + TranslationMapping map = new TranslationMapping(); + + bool pre = false; + + for (int i = 0; i < resultList.Length; i++) + { + if (content[i] >= 0x3400 && content[i] <= 0x9FD5) + { + map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1); + resultBuilder.Append(' '); + resultBuilder.Append(resultList[i]); + pre = true; + } + else + { + if (pre) + { + pre = false; + resultBuilder.Append(' '); + } + + resultBuilder.Append(resultList[i]); + } + } + + map.endConstruct(); + + var key = resultBuilder.ToString(); + map.setKey(key); + + return _pinyinCache[content] = (key, map); + } else { return (content, null); } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Properties/AssemblyInfo.cs b/Flow.Launcher.Infrastructure/Properties/AssemblyInfo.cs index 4cdadffc9..3c29d0dcb 100644 --- a/Flow.Launcher.Infrastructure/Properties/AssemblyInfo.cs +++ b/Flow.Launcher.Infrastructure/Properties/AssemblyInfo.cs @@ -2,4 +2,5 @@ [assembly: InternalsVisibleTo("Flow.Launcher")] [assembly: InternalsVisibleTo("Flow.Launcher.Core")] -[assembly: InternalsVisibleTo("Flow.Launcher.Test")] \ No newline at end of file +[assembly: InternalsVisibleTo("Flow.Launcher.Test")] +[assembly: System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.19041.0")] diff --git a/Flow.Launcher.Infrastructure/Stopwatch.cs b/Flow.Launcher.Infrastructure/Stopwatch.cs index dd6edaff9..784d323fe 100644 --- a/Flow.Launcher.Infrastructure/Stopwatch.cs +++ b/Flow.Launcher.Infrastructure/Stopwatch.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; @@ -7,8 +6,6 @@ namespace Flow.Launcher.Infrastructure { public static class Stopwatch { - private static readonly Dictionary Count = new Dictionary(); - private static readonly object Locker = new object(); /// /// This stopwatch will appear only in Debug mode /// @@ -62,36 +59,5 @@ namespace Flow.Launcher.Infrastructure Log.Info(info); return milliseconds; } - - - - public static void StartCount(string name, Action action) - { - var stopWatch = new System.Diagnostics.Stopwatch(); - stopWatch.Start(); - action(); - stopWatch.Stop(); - var milliseconds = stopWatch.ElapsedMilliseconds; - lock (Locker) - { - if (Count.ContainsKey(name)) - { - Count[name] += milliseconds; - } - else - { - Count[name] = 0; - } - } - } - - public static void EndCount() - { - foreach (var key in Count.Keys) - { - string info = $"{key} already cost {Count[key]}ms"; - Log.Debug(info); - } - } } } diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index 5205543b1..8ff10816c 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -1,11 +1,13 @@ using System; using System.IO; -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 Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; +using MemoryPack; + +#nullable enable namespace Flow.Launcher.Infrastructure.Storage { @@ -13,104 +15,112 @@ namespace Flow.Launcher.Infrastructure.Storage /// Stroage object using binary data /// Normally, it has better performance, but not readable /// - public class BinaryStorage + /// + /// It utilizes MemoryPack, which means the object must be MemoryPackSerializable + /// + public class BinaryStorage : ISavable { - public BinaryStorage(string filename) - { - const string directoryName = "Cache"; - var directoryPath = Path.Combine(DataLocation.DataDirectory(), directoryName); - Helper.ValidateDirectory(directoryPath); + protected T? Data; - const string fileSuffix = ".cache"; - FilePath = Path.Combine(directoryPath, $"{filename}{fileSuffix}"); + public const string FileSuffix = ".cache"; + + protected string FilePath { get; init; } = null!; + + protected string DirectoryPath { get; init; } = null!; + + // Let the derived class to set the file path + protected BinaryStorage() + { } - public string FilePath { get; } - - public T TryLoad(T defaultData) + public BinaryStorage(string filename) { + DirectoryPath = DataLocation.CacheDirectory; + FilesFolders.ValidateDirectory(DirectoryPath); + + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); + } + + // Let the old Program plugin get this constructor + [Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")] + public BinaryStorage(string filename, string directoryPath = null!) + { + DirectoryPath = directoryPath ?? DataLocation.CacheDirectory; + FilesFolders.ValidateDirectory(DirectoryPath); + + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); + } + + public async ValueTask TryLoadAsync(T defaultData) + { + if (Data != null) return Data; + if (File.Exists(FilePath)) { if (new FileInfo(FilePath).Length == 0) { Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>"); - Save(defaultData); - return defaultData; + Data = defaultData; + await SaveAsync(); } - using (var stream = new FileStream(FilePath, FileMode.Open)) - { - var d = Deserialize(stream, defaultData); - return d; - } + await using var stream = new FileStream(FilePath, FileMode.Open); + Data = await DeserializeAsync(stream, defaultData); } else { Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data"); - Save(defaultData); - return defaultData; + Data = defaultData; + await SaveAsync(); } + + return Data; } - private T Deserialize(FileStream stream, T defaultData) + private static async ValueTask 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(); - return t; + var t = await MemoryPackSerializer.DeserializeAsync(stream); + return t ?? defaultData; } - catch (System.Exception e) + catch (System.Exception) { - 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 void Save() { - 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; + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + + var serialized = MemoryPackSerializer.Serialize(Data); + File.WriteAllBytes(FilePath, serialized); } - public void Save(T data) + public async ValueTask SaveAsync() { - using (var stream = new FileStream(FilePath, FileMode.Create)) - { - BinaryFormatter binaryFormatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple - }; + await SaveAsync(Data.NonNull()); + } - try - { - binaryFormatter.Serialize(stream, data); - } - catch (SerializationException e) - { - Log.Exception($"|BinaryStorage.Save|serialize error for file <{FilePath}>", e); - } - } + // ImageCache need to convert data into concurrent dictionary for usage, + // so we would better to clear the data + public void ClearData() + { + Data = default; + } + + // ImageCache storages data in its class, + // so we need to pass it to SaveAsync + public async ValueTask SaveAsync(T data) + { + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + + await using var stream = new FileStream(FilePath, FileMode.Create); + await MemoryPackSerializer.SerializeAsync(stream, data); } } } diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs index ec7a38804..ca78b2f20 100644 --- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs @@ -1,22 +1,51 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; +using System.IO; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { public class FlowLauncherJsonStorage : JsonStorage where T : new() { + private static readonly string ClassName = "FlowLauncherJsonStorage"; + + // 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(); + public FlowLauncherJsonStorage() { - var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); - Helper.ValidateDirectory(directoryPath); + DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); + FilesFolders.ValidateDirectory(DirectoryPath); var filename = typeof(T).Name; - FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); + } + + public new void Save() + { + try + { + base.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e); + } + } + + public new async Task SaveAsync() + { + try + { + await base.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e); + } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Storage/ISavable.cs b/Flow.Launcher.Infrastructure/Storage/ISavable.cs deleted file mode 100644 index ba2b58c6a..000000000 --- a/Flow.Launcher.Infrastructure/Storage/ISavable.cs +++ /dev/null @@ -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 { } -} \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 0083ccb87..f283be59e 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -2,71 +2,172 @@ using System.Globalization; using System.IO; using System.Text.Json; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; + +#nullable enable namespace Flow.Launcher.Infrastructure.Storage { /// /// Serialize object using json format. /// - public class JsonStorage where T : new() + public class JsonStorage : ISavable where T : new() { - protected T _data; + protected T? Data; + // need a new directory name - public const string DirectoryName = "Settings"; + public const string DirectoryName = Constant.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"); + + FilesFolders.ValidateDirectory(DirectoryPath); + } + + public async Task 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(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 LoadBackupOrDefaultAsync() { + var backup = await TryLoadBackupAsync(); + + return backup ?? LoadDefault(); + } + + private async ValueTask TryLoadBackupAsync() + { + if (!File.Exists(BackupFilePath)) + return default; + try { - _data = JsonSerializer.Deserialize(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(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(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(File.ReadAllText(BackupFilePath)); + + if (data != null) + RestoreBackup(); + + return data; + } + catch (JsonException) + { + return default; + } } private void BackupOriginFile() @@ -82,13 +183,39 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { - string serialized = JsonSerializer.Serialize(_data, new JsonSerializerOptions() { WriteIndented = true }); + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); - File.WriteAllText(FilePath, serialized); + var serialized = JsonSerializer.Serialize(Data, + new JsonSerializerOptions { WriteIndented = true }); + + File.WriteAllText(TempFilePath, serialized); + + AtomicWriteSetting(); + } + + public async Task SaveAsync() + { + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + + 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 : JsonStorage where T : new() { } } diff --git a/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs new file mode 100644 index 000000000..d18060e3d --- /dev/null +++ b/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; + +namespace Flow.Launcher.Infrastructure.Storage +{ + public class PluginBinaryStorage : BinaryStorage where T : new() + { + private static readonly string ClassName = "PluginBinaryStorage"; + + // 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(); + + public PluginBinaryStorage(string cacheName, string cacheDirectory) + { + DirectoryPath = cacheDirectory; + FilesFolders.ValidateDirectory(DirectoryPath); + + FilePath = Path.Combine(DirectoryPath, $"{cacheName}{FileSuffix}"); + } + + public new void Save() + { + try + { + base.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e); + } + } + + public new async Task SaveAsync() + { + try + { + await base.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e); + } + } + } +} diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index 923a1a6b5..e8cbd70fb 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -1,25 +1,61 @@ using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { - public class PluginJsonStorage :JsonStorage where T : new() + public class PluginJsonStorage : JsonStorage where T : new() { + // Use assembly name to check which plugin is using this storage + public readonly string AssemblyName; + + private static readonly string ClassName = "PluginJsonStorage"; + + // 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(); + 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); - Helper.ValidateDirectory(DirectoryPath); + AssemblyName = dataType.Assembly.GetName().Name; + DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName); + FilesFolders.ValidateDirectory(DirectoryPath); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); } public PluginJsonStorage(T data) : this() { - _data = data; + Data = data; + } + + public new void Save() + { + try + { + base.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); + } + } + + public new async Task SaveAsync() + { + try + { + await base.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); + } } } } - diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index 3ffa9f7b1..e85c5d6f4 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -1,28 +1,35 @@ +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().FuzzyMatch(query, stringToCompare); } public MatchResult FuzzyMatch(string query, string stringToCompare) @@ -60,8 +67,13 @@ namespace Flow.Launcher.Infrastructure return new MatchResult(false, UserSettingSearchPrecision); query = query.Trim(); - TranslationMapping translationMapping; - (stringToCompare, translationMapping) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null); + TranslationMapping translationMapping = null; + if (_alphabet is not null && !_alphabet.CanBeTranslated(query)) + { + // We assume that if a query can be translated (containing characters of a language, like Chinese) + // it actually means user doesn't want it to be translated to English letters. + (stringToCompare, translationMapping) = _alphabet.Translate(stringToCompare); + } var currentAcronymQueryIndex = 0; var acronymMatchData = new List(); @@ -202,7 +214,11 @@ namespace Flow.Launcher.Infrastructure if (allQuerySubstringsMatched) { var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex); - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, + + // firstMatchIndex - nearestSpaceIndex - 1 is to set the firstIndex as the index of the first matched char + // preceded by a space e.g. 'world' matching 'hello world' firstIndex would be 0 not 6 + // giving more weight than 'we or donald' by allowing the distance calculation to treat the starting position at after the space. + var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, spaceIndices, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); @@ -232,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 spaceIndices, int firstMatchIndex) + private static int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) { var closestSpaceIndex = -1; @@ -296,7 +312,7 @@ namespace Flow.Launcher.Infrastructure return currentQuerySubstringIndex >= querySubstringsLength; } - private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, + private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, List spaceIndices, int matchLen, bool allSubstringsContainedInCompareString) { // A match found near the beginning of a string is scored more than a match found near the end @@ -304,6 +320,14 @@ namespace Flow.Launcher.Infrastructure // while the score is lower if they are more spread out var score = 100 * (query.Length + 1) / ((1 + firstIndex) + (matchLen + 1)); + // Give more weight to a match that is closer to the start of the string. + // if the first matched char is immediately before space and all strings are contained in the compare string e.g. 'world' matching 'hello world' + // and 'world hello', because both have 'world' immediately preceded by space, their firstIndex will be 0 when distance is calculated, + // to prevent them scoring the same, we adjust the score by deducting the number of spaces it has from the start of the string, so 'world hello' + // will score slightly higher than 'hello world' because 'hello world' has one additional space. + if (firstIndex == 0 && allSubstringsContainedInCompareString) + score -= spaceIndices.Count; + // A match with less characters assigning more weights if (stringToCompare.Length - query.Length < 5) { diff --git a/Flow.Launcher.Infrastructure/UI/EnumBindingSource.cs b/Flow.Launcher.Infrastructure/UI/EnumBindingSource.cs index 350c892cf..f9504e6d9 100644 --- a/Flow.Launcher.Infrastructure/UI/EnumBindingSource.cs +++ b/Flow.Launcher.Infrastructure/UI/EnumBindingSource.cs @@ -3,6 +3,7 @@ using System.Windows.Markup; namespace Flow.Launcher.Infrastructure.UI { + [Obsolete("EnumBindingSourceExtension is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")] public class EnumBindingSourceExtension : MarkupExtension { private Type _enumType; diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs new file mode 100644 index 000000000..24584115d --- /dev/null +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomBrowserViewModel.cs @@ -0,0 +1,33 @@ +using Flow.Launcher.Plugin; +using System.Text.Json.Serialization; + +namespace Flow.Launcher.Infrastructure.UserSettings +{ + public class CustomBrowserViewModel : BaseModel + { + public string Name { get; set; } + public string Path { get; set; } + public string PrivateArg { get; set; } + public bool EnablePrivate { get; set; } + public bool OpenInTab { get; set; } = true; + [JsonIgnore] + public bool OpenInNewWindow => !OpenInTab; + public bool Editable { get; set; } = true; + + public CustomBrowserViewModel Copy() + { + return new CustomBrowserViewModel + { + Name = Name, + Path = Path, + OpenInTab = OpenInTab, + PrivateArg = PrivateArg, + EnablePrivate = EnablePrivate, + Editable = Editable + }; + } + } +} + + + diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs new file mode 100644 index 000000000..c54c30478 --- /dev/null +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs @@ -0,0 +1,25 @@ +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.ViewModel +{ + public class CustomExplorerViewModel : BaseModel + { + public string Name { get; set; } + public string Path { get; set; } + public string FileArgument { get; set; } = "\"%d\""; + public string DirectoryArgument { get; set; } = "\"%d\""; + public bool Editable { get; init; } = true; + + public CustomExplorerViewModel Copy() + { + return new CustomExplorerViewModel + { + Name = Name, + Path = Path, + FileArgument = FileArgument, + DirectoryArgument = DirectoryArgument, + Editable = Editable + }; + } + } +} diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs new file mode 100644 index 000000000..71020369a --- /dev/null +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Text.Json.Serialization; + +namespace Flow.Launcher.Infrastructure.UserSettings +{ + public abstract class ShortcutBaseModel + { + public string Key { get; set; } + + [JsonIgnore] + public Func Expand { get; set; } = () => { return ""; }; + + public override bool Equals(object obj) + { + return obj is ShortcutBaseModel other && + Key == other.Key; + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + } + + public class CustomShortcutModel : ShortcutBaseModel + { + public string Value { get; set; } + + [JsonConstructorAttribute] + public CustomShortcutModel(string key, string value) + { + Key = key; + Value = value; + Expand = () => { return Value; }; + } + + public void Deconstruct(out string key, out string value) + { + key = Key; + value = Value; + } + + public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut) + { + return (shortcut.Key, shortcut.Value); + } + + public static implicit operator CustomShortcutModel((string Key, string Value) shortcut) + { + return new CustomShortcutModel(shortcut.Key, shortcut.Value); + } + } + + public class BuiltinShortcutModel : ShortcutBaseModel + { + public string Description { get; set; } + + public BuiltinShortcutModel(string key, string description, Func expand) + { + Key = key; + Description = description; + Expand = expand ?? (() => { return ""; }); + } + } +} diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index e93c341dc..5b948e450 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -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 { @@ -29,7 +25,20 @@ namespace Flow.Launcher.Infrastructure.UserSettings return false; } + public static string VersionLogDirectory => Path.Combine(LogDirectory, Constant.Version); + public static string LogDirectory => Path.Combine(DataDirectory(), Constant.Logs); + + public static readonly string CacheDirectory = Path.Combine(DataDirectory(), Constant.Cache); + public static readonly string SettingsDirectory = Path.Combine(DataDirectory(), Constant.Settings); public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins); - public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins); + public static readonly string ThemesDirectory = Path.Combine(DataDirectory(), Constant.Themes); + + public static readonly string PluginSettingsDirectory = Path.Combine(SettingsDirectory, Constant.Plugins); + public static readonly string PluginCacheDirectory = Path.Combine(DataDirectory(), Constant.Cache, 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); } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/HttpProxy.cs b/Flow.Launcher.Infrastructure/UserSettings/HttpProxy.cs index 213193526..c8d76d2ba 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/HttpProxy.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/HttpProxy.cs @@ -1,6 +1,4 @@ -using System.ComponentModel; - -namespace Flow.Launcher.Infrastructure.UserSettings +namespace Flow.Launcher.Infrastructure.UserSettings { public enum ProxyProperty { diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index abce8b41e..7fb9b895a 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -1,32 +1,59 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings { public class PluginsSettings : BaseModel { - public string PythonDirectory { get; set; } - public Dictionary Plugins { get; set; } = new Dictionary(); + private string pythonExecutablePath = string.Empty; + public string PythonExecutablePath + { + get => pythonExecutablePath; + set + { + pythonExecutablePath = value; + Constant.PythonPath = value; + } + } + private string nodeExecutablePath = string.Empty; + public string NodeExecutablePath + { + get => nodeExecutablePath; + set + { + nodeExecutablePath = value; + Constant.NodePath = value; + } + } + + /// + /// Only used for serialization + /// + public Dictionary Plugins { get; set; } = new(); + + /// + /// Update plugin settings with metadata. + /// FL will get default values from metadata first and then load settings to metadata + /// + /// Parsed plugin metadatas public void UpdatePluginSettings(List metadatas) { foreach (var metadata in metadatas) { - if (Plugins.ContainsKey(metadata.ID)) + if (Plugins.TryGetValue(metadata.ID, out var settings)) { - var settings = Plugins[metadata.ID]; - - // TODO: Remove. This is backwards compatibility for 1.8.0 release. - // Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder. - if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count) - { - settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search - settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search - } - + // If settings exist, update settings & metadata value + // update settings values with metadata if (string.IsNullOrEmpty(settings.Version)) + { settings.Version = metadata.Version; + } + settings.DefaultActionKeywords = metadata.ActionKeywords; // metadata provides default values + settings.DefaultSearchDelayTime = metadata.SearchDelayTime; // metadata provides default values + // update metadata values with settings if (settings.ActionKeywords?.Count > 0) { metadata.ActionKeywords = settings.ActionKeywords; @@ -39,30 +66,64 @@ namespace Flow.Launcher.Infrastructure.UserSettings } metadata.Disabled = settings.Disabled; metadata.Priority = settings.Priority; + metadata.SearchDelayTime = settings.SearchDelayTime; } else { + // If settings does not exist, create a new one Plugins[metadata.ID] = new Plugin { ID = metadata.ID, Name = metadata.Name, Version = metadata.Version, - ActionKeywords = metadata.ActionKeywords, + DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values + ActionKeywords = metadata.ActionKeywords, // use default value Disabled = metadata.Disabled, - Priority = metadata.Priority + Priority = metadata.Priority, + DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values + SearchDelayTime = metadata.SearchDelayTime, // use default value }; } } } + + public Plugin GetPluginSettings(string id) + { + if (Plugins.TryGetValue(id, out var plugin)) + { + return plugin; + } + return null; + } + + public Plugin RemovePluginSettings(string id) + { + Plugins.Remove(id, out var plugin); + return plugin; + } } + public class Plugin { public string ID { get; set; } + public string Name { get; set; } + public string Version { get; set; } - public List ActionKeywords { get; set; } // a reference of the action keywords from plugin manager + + [JsonIgnore] + public List DefaultActionKeywords { get; set; } + + // a reference of the action keywords from plugin manager + public List ActionKeywords { get; set; } + public int Priority { get; set; } + [JsonIgnore] + public int? DefaultSearchDelayTime { get; set; } + + public int? SearchDelayTime { get; set; } + /// /// Used only to save the state of the plugin in settings /// diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index e7e7902d4..e304a1b50 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -1,29 +1,90 @@ -using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; using System.Text.Json.Serialization; +using System.Windows; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; +using Flow.Launcher.ViewModel; namespace Flow.Launcher.Infrastructure.UserSettings { - public class Settings : BaseModel + public class Settings : BaseModel, IHotkeySettings { - private string language = "en"; + private FlowLauncherJsonStorage _storage; + private StringMatcher _stringMatcher = null; + public void SetStorage(FlowLauncherJsonStorage storage) + { + _storage = storage; + } + + public void Initialize() + { + _stringMatcher = Ioc.Default.GetRequiredService(); + } + + public void Save() + { + _storage.Save(); + } + + private string language = Constant.SystemLanguageCode; + private string _theme = Constant.DefaultTheme; public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}"; public string OpenResultModifiers { get; set; } = KeyConstant.Alt; + public string ColorScheme { get; set; } = "System"; public bool ShowOpenResultHotkey { get; set; } = true; + public double WindowSize { get; set; } = 580; + public string PreviewHotkey { get; set; } = $"F1"; + public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab"; + public string AutoCompleteHotkey2 { get; set; } = $""; + public string SelectNextItemHotkey { get; set; } = $"Tab"; + public string SelectNextItemHotkey2 { get; set; } = $""; + public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab"; + public string SelectPrevItemHotkey2 { get; set; } = $""; + public string SelectNextPageHotkey { get; set; } = $"PageUp"; + public string SelectPrevPageHotkey { get; set; } = $"PageDown"; + public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O"; + public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; + public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; + public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; + public string Language { - get => language; set + get => language; + set { language = value; OnPropertyChanged(); } } - public string Theme { get; set; } = Constant.DefaultTheme; - public bool UseDropShadowEffect { get; set; } = false; + public string Theme + { + get => _theme; + set + { + if (value != _theme) + { + _theme = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(MaxResultsToShow)); + } + } + } + public bool UseDropShadowEffect { get; set; } = true; + public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None; + + /* Appearance Settings. It should be separated from the setting later.*/ + public double WindowHeightSize { get; set; } = 42; + public double ItemHeightSize { get; set; } = 58; + public double QueryBoxFontSize { get; set; } = 20; + public double ResultItemFontSize { get; set; } = 16; + public double ResultSubItemFontSize { get; set; } = 13; public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name; public string QueryBoxFontStyle { get; set; } public string QueryBoxFontWeight { get; set; } @@ -32,38 +93,164 @@ namespace Flow.Launcher.Infrastructure.UserSettings public string ResultFontStyle { get; set; } public string ResultFontWeight { get; set; } public string ResultFontStretch { get; set; } + public string ResultSubFont { get; set; } = FontFamily.GenericSansSerif.Name; + public string ResultSubFontStyle { get; set; } + public string ResultSubFontWeight { get; set; } + public string ResultSubFontStretch { get; set; } + public bool UseGlyphIcons { get; set; } = true; + public bool UseAnimation { get; set; } = true; + public bool UseSound { get; set; } = true; + public double SoundVolume { get; set; } = 50; + public bool UseClock { get; set; } = true; + public bool UseDate { get; set; } = false; + public string TimeFormat { get; set; } = "hh:mm tt"; + public string DateFormat { get; set; } = "MM'/'dd ddd"; + public bool FirstLaunch { get; set; } = true; + + public double SettingWindowWidth { get; set; } = 1000; + public double SettingWindowHeight { get; set; } = 700; + public double? SettingWindowTop { get; set; } = null; + public double? SettingWindowLeft { get; set; } = null; + public WindowState SettingWindowState { get; set; } = WindowState.Normal; + + bool _showPlaceholder { get; set; } = false; + public bool ShowPlaceholder + { + get => _showPlaceholder; + set + { + if (_showPlaceholder != value) + { + _showPlaceholder = value; + OnPropertyChanged(); + } + } + } + string _placeholderText { get; set; } = string.Empty; + public string PlaceholderText + { + get => _placeholderText; + set + { + if (_placeholderText != value) + { + _placeholderText = value; + OnPropertyChanged(); + } + } + } + + public int CustomExplorerIndex { get; set; } = 0; + + [JsonIgnore] + public CustomExplorerViewModel CustomExplorer + { + get => CustomExplorerList[CustomExplorerIndex < CustomExplorerList.Count ? CustomExplorerIndex : 0]; + set => CustomExplorerList[CustomExplorerIndex] = value; + } + + public List CustomExplorerList { get; set; } = new() + { + new() + { + Name = "Explorer", + Path = "explorer", + DirectoryArgument = "\"%d\"", + FileArgument = "/select, \"%f\"", + Editable = false + }, + new() + { + Name = "Total Commander", + Path = @"C:\Program Files\totalcmd\TOTALCMD64.exe", + DirectoryArgument = "/O /A /S /T \"%d\"", + FileArgument = "/O /A /S /T \"%f\"" + }, + new() + { + Name = "Directory Opus", + Path = @"C:\Program Files\GPSoftware\Directory Opus\dopusrt.exe", + DirectoryArgument = "/cmd Go \"%d\" NEW", + FileArgument = "/cmd Go \"%f\" NEW" + + }, + new() + { + Name = "Files", + Path = "Files", + DirectoryArgument = "-select \"%d\"", + FileArgument = "-select \"%f\"" + } + }; + + public int CustomBrowserIndex { get; set; } = 0; + + [JsonIgnore] + public CustomBrowserViewModel CustomBrowser + { + get => CustomBrowserList[CustomBrowserIndex]; + set => CustomBrowserList[CustomBrowserIndex] = value; + } + + public List CustomBrowserList { get; set; } = new() + { + new() + { + Name = "Default", + Path = "*", + PrivateArg = "", + EnablePrivate = false, + Editable = false + }, + new() + { + Name = "Google Chrome", + Path = "chrome", + PrivateArg = "-incognito", + EnablePrivate = false, + Editable = false + }, + new() + { + Name = "Mozilla Firefox", + Path = "firefox", + PrivateArg = "-private", + EnablePrivate = false, + Editable = false + }, + new() + { + Name = "MS Edge", + Path = "msedge", + PrivateArg = "-inPrivate", + EnablePrivate = false, + Editable = false + } + }; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO; /// /// when false Alphabet static service will always return empty results /// public bool ShouldUsePinyin { get; set; } = false; - internal SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular; + public bool AlwaysPreview { get; set; } = false; - [JsonIgnore] - public string QuerySearchPrecisionString + public bool AlwaysStartEn { get; set; } = false; + + private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular; + [JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))] + public SearchPrecisionScore QuerySearchPrecision { - get { return QuerySearchPrecision.ToString(); } + get => _querySearchPrecision; set { - try - { - var precisionScore = (SearchPrecisionScore)Enum - .Parse(typeof(SearchPrecisionScore), value); - - QuerySearchPrecision = precisionScore; - StringMatcher.Instance.UserSettingSearchPrecision = precisionScore; - } - catch (ArgumentException e) - { - Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e); - - QuerySearchPrecision = SearchPrecisionScore.Regular; - StringMatcher.Instance.UserSettingSearchPrecision = SearchPrecisionScore.Regular; - - throw; - } + _querySearchPrecision = value; + if (_stringMatcher != null) + _stringMatcher.UserSettingSearchPrecision = value; } } @@ -71,21 +258,59 @@ namespace Flow.Launcher.Infrastructure.UserSettings public double WindowLeft { get; set; } public double WindowTop { get; set; } + + /// + /// Custom left position on selected monitor + /// + public double CustomWindowLeft { get; set; } = 0; + + /// + /// Custom top position on selected monitor + /// + public double CustomWindowTop { get; set; } = 0; + + /// + /// Fixed window size + /// + private bool _keepMaxResults { get; set; } = false; + public bool KeepMaxResults + { + get => _keepMaxResults; + set + { + if (_keepMaxResults != value) + { + _keepMaxResults = value; + OnPropertyChanged(); + } + } + } + public int MaxResultsToShow { get; set; } = 5; + public int ActivateTimes { get; set; } - public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection(); + public ObservableCollection CustomShortcuts { get; set; } = new ObservableCollection(); + + [JsonIgnore] + public ObservableCollection BuiltinShortcuts { get; set; } = new() + { + new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), + new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) + }; + public bool DontPromptUpdateMsg { get; set; } public bool EnableUpdateLog { get; set; } - public bool StartFlowLauncherOnSystemStartup { get; set; } = true; - public bool HideOnStartup { get; set; } + public bool StartFlowLauncherOnSystemStartup { get; set; } = false; + public bool UseLogonTaskForStartup { get; set; } = false; + public bool HideOnStartup { get; set; } = true; bool _hideNotifyIcon { get; set; } public bool HideNotifyIcon { - get { return _hideNotifyIcon; } + get => _hideNotifyIcon; set { _hideNotifyIcon = value; @@ -93,26 +318,171 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } public bool LeaveCmdOpen { get; set; } - public bool HideWhenDeactive { get; set; } = true; - public bool RememberLastLaunchLocation { get; set; } - public bool IgnoreHotkeysOnFullscreen { get; set; } + public bool HideWhenDeactivated { get; set; } = true; - public bool AutoHideScrollBar { get; set; } + public bool SearchQueryResultsWithDelay { get; set; } + public int SearchDelayTime { get; set; } = 150; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public SearchWindowAligns SearchWindowAlign { get; set; } = SearchWindowAligns.Center; + + public int CustomScreenNumber { get; set; } = 1; + + public bool IgnoreHotkeysOnFullscreen { get; set; } public HttpProxy Proxy { get; set; } = new HttpProxy(); [JsonConverter(typeof(JsonStringEnumConverter))] public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected; + [JsonConverter(typeof(JsonStringEnumConverter))] + public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium; + public int CustomAnimationLength { get; set; } = 360; + + [JsonIgnore] + public bool WMPInstalled { get; set; } = true; // This needs to be loaded last by staying at the bottom public PluginsSettings PluginSettings { get; set; } = new PluginsSettings(); + + [JsonIgnore] + public List RegisteredHotkeys + { + get + { + var list = FixedHotkeys(); + + // Customizeable hotkeys + if(!string.IsNullOrEmpty(Hotkey)) + list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = "")); + if(!string.IsNullOrEmpty(PreviewHotkey)) + list.Add(new(PreviewHotkey, "previewHotkey", () => PreviewHotkey = "")); + if(!string.IsNullOrEmpty(AutoCompleteHotkey)) + list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = "")); + if(!string.IsNullOrEmpty(AutoCompleteHotkey2)) + list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = "")); + if(!string.IsNullOrEmpty(SelectNextItemHotkey)) + list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = "")); + if(!string.IsNullOrEmpty(SelectNextItemHotkey2)) + list.Add(new(SelectNextItemHotkey2, "SelectNextItemHotkey", () => SelectNextItemHotkey2 = "")); + if(!string.IsNullOrEmpty(SelectPrevItemHotkey)) + list.Add(new(SelectPrevItemHotkey, "SelectPrevItemHotkey", () => SelectPrevItemHotkey = "")); + if(!string.IsNullOrEmpty(SelectPrevItemHotkey2)) + list.Add(new(SelectPrevItemHotkey2, "SelectPrevItemHotkey", () => SelectPrevItemHotkey2 = "")); + if(!string.IsNullOrEmpty(SettingWindowHotkey)) + list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = "")); + if(!string.IsNullOrEmpty(OpenContextMenuHotkey)) + list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = "")); + if(!string.IsNullOrEmpty(SelectNextPageHotkey)) + list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = "")); + if(!string.IsNullOrEmpty(SelectPrevPageHotkey)) + list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = "")); + if (!string.IsNullOrEmpty(CycleHistoryUpHotkey)) + list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = "")); + if (!string.IsNullOrEmpty(CycleHistoryDownHotkey)) + list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = "")); + + // Custom Query Hotkeys + foreach (var customPluginHotkey in CustomPluginHotkeys) + { + if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey)) + list.Add(new(customPluginHotkey.Hotkey, "customQueryHotkey", () => customPluginHotkey.Hotkey = "")); + } + + return list; + } + } + + private List FixedHotkeys() + { + return new List + { + new("Up", "HotkeyLeftRightDesc"), + new("Down", "HotkeyLeftRightDesc"), + new("Left", "HotkeyUpDownDesc"), + new("Right", "HotkeyUpDownDesc"), + new("Escape", "HotkeyESCDesc"), + new("F5", "ReloadPluginHotkey"), + new("Alt+Home", "HotkeySelectFirstResult"), + new("Alt+End", "HotkeySelectLastResult"), + new("Ctrl+R", "HotkeyRequery"), + new("Ctrl+H", "ToggleHistoryHotkey"), + new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"), + new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"), + new("Ctrl+OemPlus", "QuickHeightHotkey"), + new("Ctrl+OemMinus", "QuickHeightHotkey"), + new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"), + new("Shift+Enter", "OpenContextMenuHotkey"), + new("Enter", "HotkeyRunDesc"), + new("Ctrl+Enter", "OpenContainFolderHotkey"), + new("Alt+Enter", "HotkeyOpenResult"), + new("Ctrl+F12", "ToggleGameModeHotkey"), + new("Ctrl+Shift+C", "CopyFilePathHotkey"), + + new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1), + new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2), + new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3), + new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4), + new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5), + new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6), + new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7), + new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8), + new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9), + new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10) + }; + } } public enum LastQueryMode { Selected, Empty, - Preserved + Preserved, + ActionKeywordPreserved, + ActionKeywordSelected } -} \ No newline at end of file + + public enum ColorSchemes + { + System, + Light, + Dark + } + + public enum SearchWindowScreens + { + RememberLastLaunchLocation, + Cursor, + Focus, + Primary, + Custom + } + + public enum SearchWindowAligns + { + Center, + CenterTop, + LeftTop, + RightTop, + Custom + } + + public enum AnimationSpeeds + { + Slow, + Medium, + Fast, + Custom + } + + public enum BackdropTypes + { + None, + Acrylic, + Mica, + MicaAlt + } +} diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs new file mode 100644 index 000000000..54604a271 --- /dev/null +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -0,0 +1,609 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using Flow.Launcher.Infrastructure.UserSettings; +using Microsoft.Win32; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Dwm; +using Windows.Win32.UI.Input.KeyboardAndMouse; +using Windows.Win32.UI.WindowsAndMessaging; +using Point = System.Windows.Point; + +namespace Flow.Launcher.Infrastructure +{ + public static class Win32Helper + { + #region Blur Handling + + public static bool IsBackdropSupported() + { + // Mica and Acrylic only supported Windows 11 22000+ + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 22000; + } + + public static unsafe bool DWMSetCloakForWindow(Window window, bool cloak) + { + var cloaked = cloak ? 1 : 0; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_CLOAK, + &cloaked, + (uint)Marshal.SizeOf()).Succeeded; + } + + public static unsafe bool DWMSetBackdropForWindow(Window window, BackdropTypes backdrop) + { + var backdropType = backdrop switch + { + BackdropTypes.Acrylic => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TRANSIENTWINDOW, + BackdropTypes.Mica => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_MAINWINDOW, + BackdropTypes.MicaAlt => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TABBEDWINDOW, + _ => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_AUTO + }; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_SYSTEMBACKDROP_TYPE, + &backdropType, + (uint)Marshal.SizeOf()).Succeeded; + } + + public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMode) + { + var darkMode = useDarkMode ? 1 : 0; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &darkMode, + (uint)Marshal.SizeOf()).Succeeded; + } + + /// + /// + /// + /// + /// DoNotRound, Round, RoundSmall, Default + /// + public static unsafe bool DWMSetCornerPreferenceForWindow(Window window, string cornerType) + { + var preference = cornerType switch + { + "DoNotRound" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND, + "Round" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND, + "RoundSmall" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUNDSMALL, + "Default" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DEFAULT, + _ => throw new InvalidOperationException("Invalid corner type") + }; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, + &preference, + (uint)Marshal.SizeOf()).Succeeded; + } + + #endregion + + #region Wallpaper + + public static unsafe string GetWallpaperPath() + { + var wallpaperPtr = stackalloc char[(int)PInvoke.MAX_PATH]; + PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, PInvoke.MAX_PATH, + wallpaperPtr, + 0); + var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr); + + return wallpaper.ToString(); + } + + #endregion + + #region Window Foreground + + public static nint GetForegroundWindow() + { + return PInvoke.GetForegroundWindow().Value; + } + + public static bool SetForegroundWindow(Window window) + { + return PInvoke.SetForegroundWindow(GetWindowHandle(window)); + } + + public static bool SetForegroundWindow(nint handle) + { + return PInvoke.SetForegroundWindow(new(handle)); + } + + public static bool IsForegroundWindow(Window window) + { + return IsForegroundWindow(GetWindowHandle(window)); + } + + internal static bool IsForegroundWindow(HWND handle) + { + return handle.Equals(PInvoke.GetForegroundWindow()); + } + + #endregion + + #region Task Switching + + /// + /// Hide windows in the Alt+Tab window list + /// + /// To hide a window + public static void HideFromAltTab(Window window) + { + var hwnd = GetWindowHandle(window); + + var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + // Add TOOLWINDOW style, remove APPWINDOW style + var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; + + SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); + } + + /// + /// Restore window display in the Alt+Tab window list. + /// + /// To restore the displayed window + public static void ShowInAltTab(Window window) + { + var hwnd = GetWindowHandle(window); + + var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + // Remove the TOOLWINDOW style and add the APPWINDOW style. + var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; + + SetWindowStyle(GetWindowHandle(window), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); + } + + /// + /// Disable windows toolbar's control box + /// This will also disable system menu with Alt+Space hotkey + /// + public static void DisableControlBox(Window window) + { + var hwnd = GetWindowHandle(window); + + var style = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + + style &= ~(int)WINDOW_STYLE.WS_SYSMENU; + + SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style); + } + + private static int GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex) + { + var style = PInvoke.GetWindowLong(hWnd, nIndex); + if (style == 0 && Marshal.GetLastPInvokeError() != 0) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + return style; + } + + private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong) + { + PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error + + var result = PInvoke.SetWindowLongPtr(hWnd, nIndex, dwNewLong); + if (result == 0 && Marshal.GetLastPInvokeError() != 0) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + + return result; + } + + #endregion + + #region Window Fullscreen + + private const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass"; + private const string WINDOW_CLASS_WINTAB = "Flip3D"; + private const string WINDOW_CLASS_PROGMAN = "Progman"; + private const string WINDOW_CLASS_WORKERW = "WorkerW"; + + private static HWND _hwnd_shell; + private static HWND HWND_SHELL => + _hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow(); + + private static HWND _hwnd_desktop; + private static HWND HWND_DESKTOP => + _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow(); + + public static unsafe bool IsForegroundWindowFullscreen() + { + // Get current active window + var hWnd = PInvoke.GetForegroundWindow(); + if (hWnd.Equals(HWND.Null)) + { + return false; + } + + // If current active window is desktop or shell, exit early + if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)) + { + return false; + } + + string windowClass; + const int capacity = 256; + Span buffer = stackalloc char[capacity]; + int validLength; + fixed (char* pBuffer = buffer) + { + validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity); + } + + windowClass = buffer[..validLength].ToString(); + + // For Win+Tab (Flip3D) + if (windowClass == WINDOW_CLASS_WINTAB) + { + return false; + } + + PInvoke.GetWindowRect(hWnd, out var appBounds); + + // For console (ConsoleWindowClass), we have to check for negative dimensions + if (windowClass == WINDOW_CLASS_CONSOLE) + { + return appBounds.top < 0 && appBounds.bottom < 0; + } + + // For desktop (Progman or WorkerW, depends on the system), we have to check + if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW) + { + var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null); + hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView"); + if (hWndDesktop.Value != IntPtr.Zero) + { + return false; + } + } + + var monitorInfo = MonitorInfo.GetNearestDisplayMonitor(hWnd); + return (appBounds.bottom - appBounds.top) == monitorInfo.RectMonitor.Height && + (appBounds.right - appBounds.left) == monitorInfo.RectMonitor.Width; + } + + #endregion + + #region Pixel to DIP + + /// + /// Transforms pixels to Device Independent Pixels used by WPF + /// + /// current window, required to get presentation source + /// horizontal position in pixels + /// vertical position in pixels + /// point containing device independent pixels + public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY) + { + Matrix matrix; + var source = PresentationSource.FromVisual(visual); + if (source is not null) + { + matrix = source.CompositionTarget.TransformFromDevice; + } + else + { + using var src = new HwndSource(new HwndSourceParameters()); + matrix = src.CompositionTarget.TransformFromDevice; + } + + return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY)); + } + + #endregion + + #region WndProc + + public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE; + public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE; + + #endregion + + #region Window Handle + + internal static HWND GetWindowHandle(Window window, bool ensure = false) + { + var windowHelper = new WindowInteropHelper(window); + if (ensure) + { + windowHelper.EnsureHandle(); + } + return new(windowHelper.Handle); + } + + #endregion + + #region Keyboard Layout + + private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f + private const string EnglishLanguageTag = "en"; + + private static readonly string[] ImeLanguageTags = + { + "zh", // Chinese + "ja", // Japanese + "ko", // Korean + }; + + private const uint KeyboardLayoutLoWord = 0xFFFF; + + // Store the previous keyboard layout + private static HKL _previousLayout; + + /// + /// Switches the keyboard layout to English if available. + /// + /// If true, the current keyboard layout will be stored for later restoration. + /// Thrown when there's an error getting the window thread process ID. + public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious) + { + // Find an installed English layout + var enHKL = FindEnglishKeyboardLayout(); + + // No installed English layout found + if (enHKL == HKL.Null) return; + + // When application is exiting, the Application.Current will be null + if (Application.Current == null) return; + + // Get the FL main window + var hwnd = GetWindowHandle(Application.Current.MainWindow, true); + if (hwnd == HWND.Null) return; + + // Check if the FL main window is the current foreground window + if (!IsForegroundWindow(hwnd)) + { + var result = PInvoke.SetForegroundWindow(hwnd); + if (!result) throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // Get the current foreground window thread ID + var threadId = PInvoke.GetWindowThreadProcessId(hwnd); + if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + + // If the current layout has an IME mode, disable it without switching to another layout. + // This is needed because for languages with IME mode, Flow Launcher just temporarily disables + // the IME mode instead of switching to another layout. + var currentLayout = PInvoke.GetKeyboardLayout(threadId); + var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord; + foreach (var imeLangTag in ImeLanguageTags) + { + var langTag = GetLanguageTag(currentLangId); + if (langTag.StartsWith(imeLangTag, StringComparison.OrdinalIgnoreCase)) return; + } + + // Backup current keyboard layout + if (backupPrevious) _previousLayout = currentLayout; + + // Switch to English layout + PInvoke.ActivateKeyboardLayout(enHKL, 0); + } + + /// + /// Restores the previously backed-up keyboard layout. + /// If it wasn't backed up or has already been restored, this method does nothing. + /// + public static void RestorePreviousKeyboardLayout() + { + if (_previousLayout == HKL.Null) return; + + var hwnd = PInvoke.GetForegroundWindow(); + if (hwnd == HWND.Null) return; + + PInvoke.PostMessage( + hwnd, + PInvoke.WM_INPUTLANGCHANGEREQUEST, + PInvoke.INPUTLANGCHANGE_FORWARD, + _previousLayout.Value + ); + + _previousLayout = HKL.Null; + } + + /// + /// Finds an installed English keyboard layout. + /// + /// + /// + private static unsafe HKL FindEnglishKeyboardLayout() + { + // Get the number of keyboard layouts + int count = PInvoke.GetKeyboardLayoutList(0, null); + if (count <= 0) return HKL.Null; + + // Get all keyboard layouts + var handles = new HKL[count]; + fixed (HKL* h = handles) + { + var result = PInvoke.GetKeyboardLayoutList(count, h); + if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // Look for any English keyboard layout + foreach (var hkl in handles) + { + // The lower word contains the language identifier + var langId = (uint)hkl.Value & KeyboardLayoutLoWord; + var langTag = GetLanguageTag(langId); + + // Check if it's an English layout + if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase)) + { + return hkl; + } + } + + return HKL.Null; + } + + /// + /// Returns the + /// + /// BCP 47 language tag + /// + /// of the current input language. + /// + /// + /// Edited from: https://github.com/dotnet/winforms + /// + private static string GetLanguageTag(uint langId) + { + // We need to convert the language identifier to a language tag, because they are deprecated and may have a + // transient value. + // https://learn.microsoft.com/globalization/locale/other-locale-names#lcid + // https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks + // + // It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect + // language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID" + // instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet). + // + // Try to extract proper language tag from registry as a workaround approved by a Windows team. + // https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949 + // + // NOTE: this logic may break in future versions of Windows since it is not documented. + if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD2 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD3 + or PInvoke.LOCALE_TRANSIENT_KEYBOARD4) + { + using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath); + if (key?.GetValue("Languages") is string[] languages) + { + foreach (string language in languages) + { + using var subKey = key.OpenSubKey(language); + if (subKey?.GetValue("TransientLangId") is int transientLangId + && transientLangId == langId) + { + return language; + } + } + } + } + + return CultureInfo.GetCultureInfo((int)langId).Name; + } + + #endregion + + #region Notification + + public static bool IsNotificationSupported() + { + // Notifications only supported on Windows 10 19041+ + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 19041; + } + + #endregion + + #region Korean IME + + public static bool IsWindows11() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 22000; + } + + public static bool IsKoreanIMEExist() + { + return GetLegacyKoreanIMERegistryValue() != null; + } + + public static bool IsLegacyKoreanIMEEnabled() + { + object value = GetLegacyKoreanIMERegistryValue(); + + if (value is int intValue) + { + return intValue == 1; + } + else if (value != null && int.TryParse(value.ToString(), out int parsedValue)) + { + return parsedValue == 1; + } + + return false; + } + + public static bool SetLegacyKoreanIMEEnabled(bool enable) + { + const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}"; + const string valueName = "NoTsf3Override5"; + + try + { + using RegistryKey key = Registry.CurrentUser.CreateSubKey(subKeyPath); + if (key != null) + { + int value = enable ? 1 : 0; + key.SetValue(valueName, value, RegistryValueKind.DWord); + return true; + } + } + catch (System.Exception) + { + // Ignored + } + + return false; + } + + public static object GetLegacyKoreanIMERegistryValue() + { + const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}"; + const string valueName = "NoTsf3Override5"; + + try + { + using RegistryKey key = Registry.CurrentUser.OpenSubKey(subKeyPath); + if (key != null) + { + return key.GetValue(valueName); + } + } + catch (System.Exception) + { + // Ignored + } + + return null; + } + + public static void OpenImeSettings() + { + try + { + Process.Start(new ProcessStartInfo("ms-settings:regionlanguage") { UseShellExecute = true }); + } + catch (System.Exception) + { + // Ignored + } + } + + #endregion + } +} diff --git a/Flow.Launcher.Infrastructure/pinyindb/pinyin_gwoyeu_mapping.xml b/Flow.Launcher.Infrastructure/pinyindb/pinyin_gwoyeu_mapping.xml deleted file mode 100644 index 2713c2d53..000000000 --- a/Flow.Launcher.Infrastructure/pinyindb/pinyin_gwoyeu_mapping.xml +++ /dev/null @@ -1,3283 +0,0 @@ - - - - a - a - ar - aa - ah - .a - - - ai - ai - air - ae - ay - .ai - - - an - an - arn - aan - ann - .an - - - ang - ang - arng - aang - anq - .ang - - - ao - au - aur - ao - aw - .au - - - ba - ba - bar - baa - bah - .ba - - - bai - bai - bair - bae - bay - .bai - - - ban - ban - barn - baan - bann - .ban - - - bang - bang - barng - baang - banq - .bang - - - bao - bau - baur - bao - baw - .bau - - - bei - bei - beir - beei - bey - .bei - - - ben - ben - bern - been - benn - .ben - - - beng - beng - berng - beeng - benq - .beng - - - bi - bi - byi - bii - bih - .bi - - - bian - bian - byan - bean - biann - .bian - - - biao - biau - byau - beau - biaw - .biau - - - bie - bie - bye - biee - bieh - .bie - - - bin - bin - byn - biin - binn - .bin - - - bing - bing - byng - biing - binq - .bing - - - bo - bo - bor - boo - boh - .bo - - - bu - bu - bwu - buu - buh - .bu - - - ca - tsa - tsar - tsaa - tsah - .tsa - - - cai - tsai - tsair - tsae - tsay - .tsai - - - can - tsan - tsarn - tsaan - tsann - .tsan - - - cang - tsang - tsarng - tsaang - tsanq - .tsang - - - cao - tsau - tsaur - tsao - tsaw - .tsau - - - ce - tse - tser - tsee - tseh - .tse - - - cen - tsen - tsern - tseen - tsenn - .tsen - - - ceng - tseng - tserng - tseeng - tsenq - .tseng - - - cha - cha - char - chaa - chah - .cha - - - chai - chai - chair - chae - chay - .chai - - - chan - chan - charn - chaan - chann - .chan - - - chang - chang - charng - chaang - chanq - .chang - - - chao - chau - chaur - chao - chaw - .chau - - - che - che - cher - chee - cheh - .che - - - chen - chen - chern - cheen - chenn - .chen - - - cheng - cheng - cherng - cheeng - chenq - .cheng - - - chi - chy - chyr - chyy - chyh - .chy - - - chong - chong - chorng - choong - chonq - .chong - - - chou - chou - chour - choou - chow - .chou - - - chu - chu - chwu - chuu - chuh - .chu - - - chua - chua - chuar - chuaa - chuah - .chua - - - chuai - chuai - chwai - choai - chuay - .chuai - - - chuan - chuan - chwan - choan - chuann - .chuan - - - chuang - chuang - chwang - choang - chuanq - .chuang - - - chui - chuei - chwei - choei - chuey - .chuei - - - chun - chuen - chwen - choen - chuenn - .chuen - - - chuo - chuo - chwo - chuoo - chuoh - .chuo - - - qi - chi - chyi - chii - chih - .chi - - - qia - chia - chya - chea - chiah - .chia - - - qian - chian - chyan - chean - chiann - .chian - - - qiang - chiang - chyang - cheang - chianq - .chiang - - - qiao - chiau - chyau - cheau - chiaw - .chiau - - - qie - chie - chye - chiee - chieh - .chie - - - ci - tsy - tsyr - tsyy - tsyh - .tsy - - - qin - chin - chyn - chiin - chinn - .chin - - - qing - ching - chyng - chiing - chinq - .ching - - - qiu - chiou - chyou - cheou - chiow - .chiou - - - cong - tsong - tsorng - tsoong - tsonq - .tsong - - - cou - tsou - tsour - tsoou - tsow - .tsou - - - cu - tsu - tswu - tsuu - tsuh - .tsu - - - cuan - tsuan - tswan - tsoan - tsuann - .tsuan - - - cui - tsuei - tswei - tsoei - tsuey - .tsuei - - - cun - tsuen - tswen - tsoen - tsuenn - .tsuen - - - cuo - tsuo - tswo - tsuoo - tsuoh - .tsuo - - - qiong - chiong - chyong - cheong - chionq - .chiong - - - qu - chiu - chyu - cheu - chiuh - .chiu - - - quan - chiuan - chyuan - cheuan - chiuann - .chiuan - - - que - chiue - chyue - cheue - chiueh - .chiue - - - qun - chiun - chyun - cheun - chiunn - .chiun - - - da - da - dar - daa - dah - .da - - - dai - dai - dair - dae - day - .dai - - - dan - dan - darn - daan - dann - .dan - - - dang - dang - darng - daang - danq - .dang - - - dao - dau - daur - dao - daw - .dau - - - de - de - der - dee - deh - .de - - - dei - dei - deir - deei - dey - .dei - - - den - den - dern - deen - denn - .den - - - deng - deng - derng - deeng - denq - .deng - - - di - di - dyi - dii - dih - .di - - - dian - dian - dyan - dean - diann - .dian - - - diang - diang - dyang - deang - dianq - .diang - - - diao - diau - dyau - deau - diaw - .diau - - - die - die - dye - diee - dieh - .die - - - ding - ding - dyng - diing - dinq - .ding - - - diu - diou - dyou - deou - diow - .diou - - - dong - dong - dorng - doong - donq - .dong - - - dou - dou - dour - doou - dow - .dou - - - du - du - dwu - duu - duh - .du - - - duan - duan - dwan - doan - duann - .duan - - - dui - duei - dwei - doei - duey - .duei - - - dun - duen - dwen - doen - duenn - .duen - - - duo - duo - dwo - duoo - duoh - .duo - - - e - e - er - ee - eh - .e - - - ei - ei - eir - eei - ey - .ei - - - en - en - ern - een - enn - .en - - - er - el - erl - eel - ell - .el - - - fa - fa - far - faa - fah - .fa - - - fan - fan - farn - faan - fann - .fan - - - fang - fang - farng - faang - fanq - .fang - - - fei - fei - feir - feei - fey - .fei - - - fen - fen - fern - feen - fenn - .fen - - - fo - fo - for - foo - foh - .fo - - - feng - feng - ferng - feeng - fenq - .feng - - - fou - fou - four - foou - fow - .fou - - - fu - fu - fwu - fuu - fuh - .fu - - - ga - ga - gar - gaa - gah - .ga - - - gai - gai - gair - gae - gay - .gai - - - gan - gan - garn - gaan - gann - .gan - - - gang - gang - garng - gaang - ganq - .gang - - - gao - gau - gaur - gao - gaw - .gau - - - ge - ge - ger - gee - geh - .ge - - - gei - gei - geir - geei - gey - .gei - - - gen - gen - gern - geen - genn - .gen - - - geng - geng - gerng - geeng - genq - .geng - - - gong - gong - gorng - goong - gonq - .gong - - - gou - gou - gour - goou - gow - .gou - - - gu - gu - gwu - guu - guh - .gu - - - gua - gua - gwa - goa - guah - .gua - - - guai - guai - gwai - goai - guay - .guai - - - guan - guan - gwan - goan - guann - .guan - - - guang - guang - gwang - goang - guanq - .guang - - - gui - guei - gwei - goei - guey - .guei - - - gun - guen - gwen - goen - guenn - .guen - - - guo - guo - gwo - guoo - guoh - .guo - - - ha - ha - har - haa - hah - .ha - - - hai - hai - hair - hae - hay - .hai - - - han - han - harn - haan - hann - .han - - - hang - hang - harng - haang - hanq - .hang - - - hao - hau - haur - hao - haw - .hau - - - he - he - her - hee - heh - .he - - - hei - hei - heir - heei - hey - .hei - - - hen - hen - hern - heen - henn - .hen - - - heng - heng - herng - heeng - henq - .heng - - - hong - hong - horng - hoong - honq - .hong - - - hou - hou - hour - hoou - how - .hou - - - hu - hu - hwu - huu - huh - .hu - - - hua - hua - hwa - hoa - huah - .hua - - - huai - huai - hwai - hoai - huay - .huai - - - huan - huan - hwan - hoan - huann - .huan - - - huang - huang - hwang - hoang - huanq - .huang - - - hui - huei - hwei - hoei - huey - .huei - - - hun - huen - hwen - hoen - huenn - .huen - - - huo - huo - hwo - huoo - huoh - .huo - - - zha - ja - jar - jaa - jah - .ja - - - zhai - jai - jair - jae - jay - .jai - - - zhan - jan - jarn - jaan - jann - .jan - - - zhang - jang - jarng - jaang - janq - .jang - - - zhao - jau - jaur - jao - jaw - .jau - - - zhe - je - jer - jee - jeh - .je - - - zhei - jei - jeir - jeei - jey - .jei - - - zhen - jen - jern - jeen - jenn - .jen - - - zheng - jeng - jerng - jeeng - jenq - .jeng - - - zhi - jy - jyr - jyy - jyh - .jy - - - zhong - jong - jorng - joong - jonq - .jong - - - zhou - jou - jour - joou - jow - .jou - - - zhu - ju - jwu - juu - juh - .ju - - - zhua - jua - jwa - joa - juah - .jua - - - zhuai - juai - jwai - joai - juay - .juai - - - zhuan - juan - jwan - joan - juann - .juan - - - zhuang - juang - jwang - joang - juanq - .juang - - - zhui - juei - jwei - joei - juey - .juei - - - zhun - juen - jwen - joen - juenn - .juen - - - zhuo - juo - jwo - juoo - juoh - .juo - - - ji - ji - jyi - jii - jih - .ji - - - jia - jia - jya - jea - jiah - .jia - - - jian - jian - jyan - jean - jiann - .jian - - - jiang - jiang - jyang - jeang - jianq - .jiang - - - jiao - jiau - jyau - jeau - jiaw - .jiau - - - jie - jie - jye - jiee - jieh - .jie - - - jin - jin - jyn - jiin - jinn - .jin - - - jing - jing - jyng - jiing - jinq - .jing - - - jiu - jiou - jyou - jeou - jiow - .jiou - - - jiong - jiong - jyong - jeong - jionq - .jiong - - - ju - jiu - jyu - jeu - jiuh - .jiu - - - juan - jiuan - jyuan - jeuan - jiuann - .jiuan - - - jue - jiue - jyue - jeue - jiueh - .jiue - - - jun - jiun - jyun - jeun - jiunn - .jiun - - - ka - ka - kar - kaa - kah - .ka - - - kai - kai - kair - kae - kay - .kai - - - kan - kan - karn - kaan - kann - .kan - - - kang - kang - karng - kaang - kanq - .kang - - - kao - kau - kaur - kao - kaw - .kau - - - ke - ke - ker - kee - keh - .ke - - - ken - ken - kern - keen - kenn - .ken - - - keng - keng - kerng - keeng - kenq - .keng - - - kong - kong - korng - koong - konq - .kong - - - kou - kou - kour - koou - kow - .kou - - - ku - ku - kwu - kuu - kuh - .ku - - - kua - kua - kwa - koa - kuah - .kua - - - kuai - kuai - kwai - koai - kuay - .kuai - - - kuan - kuan - kwan - koan - kuann - .kuan - - - kuang - kuang - kwang - koang - kuanq - .kuang - - - kui - kuei - kwei - koei - kuey - .kuei - - - kun - kuen - kwen - koen - kuenn - .kuen - - - kuo - kuo - kwo - kuoo - kuoh - .kuo - - - la - lha - la - laa - lah - .lha - - - lai - lhai - lai - lae - lay - .lhai - - - lan - lhan - lan - laan - lann - .lhan - - - lang - lhang - lang - laang - lanq - .lhang - - - lao - lhau - lau - lao - law - .lhau - - - le - lhe - le - lee - leh - .lhe - - - lei - lhei - lei - leei - ley - .lhei - - - leng - lheng - leng - leeng - lenq - .lheng - - - li - lhi - li - lii - lih - .lhi - - - lia - lhia - lia - lea - liah - .lhia - - - lian - lhian - lian - lean - liann - .lhian - - - liang - lhiang - liang - leang - lianq - .lhiang - - - liao - lhiau - liau - leau - liaw - .lhiau - - - lie - lhie - lie - liee - lieh - .lhie - - - lin - lhin - lin - liin - linn - .lhin - - - ling - lhing - ling - liing - linq - .lhing - - - liu - lhiou - liou - leou - liow - .lhiou - - - lo - lo - lor - loo - loh - .lo - - - long - lhong - long - loong - lonq - .lhong - - - lou - lhou - lou - loou - low - .lhou - - - lu - lhu - lu - luu - luh - .lhu - - - luan - lhuan - luan - loan - luann - .lhuan - - - lun - lhuen - luen - loen - luenn - .lhuen - - - luo - lhuo - luo - luoo - luoh - .lhuo - - - lu: - lhiu - liu - leu - liuh - .lhiu - - - lu:e - lhiue - liue - leue - liueh - .lhiue - - - lu:n - lhiun - liun - leun - liunn - .lhiun - - - ma - mha - ma - maa - mah - .mha - - - mai - mhai - mai - mae - may - .mhai - - - man - mhan - man - maan - mann - .mhan - - - mang - mhang - mang - maang - manq - .mhang - - - mao - mhau - mau - mao - maw - .mhau - - - me - me - mer - mee - meh - .me - - - mei - mhei - mei - meei - mey - .mhei - - - men - mhen - men - meen - menn - .mhen - - - meng - mheng - meng - meeng - menq - .mheng - - - mi - mhi - mi - mii - mih - .mhi - - - mian - mhian - mian - mean - miann - .mhian - - - miao - mhiau - miau - meau - miaw - .mhiau - - - mie - mhie - mie - miee - mieh - .mhie - - - min - mhin - min - miin - minn - .mhin - - - ming - mhing - ming - miing - minq - .mhing - - - miu - mhiou - miou - meou - miow - .mhiou - - - mo - mho - mo - moo - moh - .mho - - - mou - mhou - mou - moou - mow - .mhou - - - mu - mhu - mu - muu - muh - .mhu - - - na - nha - na - naa - nah - .nha - - - nai - nhai - nai - nae - nay - .nhai - - - nan - nhan - nan - naan - nann - .nhan - - - nang - nhang - nang - naang - nanq - .nhang - - - nao - nhau - nau - nao - naw - .nhau - - - ne - nhe - ne - nee - neh - .nhe - - - nei - nhei - nei - neei - ney - .nhei - - - nen - nhen - nen - neen - nenn - .nhen - - - neng - nheng - neng - neeng - nenq - .nheng - - - ni - nhi - ni - nii - nih - .nhi - - - nia - nia - niar - niaa - niah - .nia - - - nian - nhian - nian - nean - niann - .nhian - - - niang - nhiang - niang - neang - nianq - .nhiang - - - niao - nhiau - niau - neau - niaw - .nhiau - - - nie - nhie - nie - niee - nieh - .nhie - - - nin - nhin - nin - niin - ninn - .nhin - - - ning - nhing - ning - niing - ninq - .nhing - - - niu - nhiou - niou - neou - niow - .nhiou - - - nong - nhong - nong - noong - nonq - .nhong - - - nou - nhou - nou - noou - now - .nhou - - - nu - nhu - nu - nuu - nuh - .nhu - - - nuan - nhuan - nuan - noan - nuann - .nhuan - - - nun - nhuen - nuen - noen - nuenn - .nhuen - - - nuo - nhuo - nuo - nuoo - nuoh - .nhuo - - - nu: - nhiu - niu - neu - niuh - .nhiu - - - nu:e - nhiue - niue - neue - niueh - .nhiue - - - ou - ou - our - oou - ow - .ou - - - pa - pa - par - paa - pah - .pa - - - pai - pai - pair - pae - pay - .pai - - - pan - pan - parn - paan - pann - .pan - - - pang - pang - parng - paang - panq - .pang - - - pao - pau - paur - pao - paw - .pau - - - pei - pei - peir - peei - pey - .pei - - - pen - pen - pern - peen - penn - .pen - - - peng - peng - perng - peeng - penq - .peng - - - pi - pi - pyi - pii - pih - .pi - - - pian - pian - pyan - pean - piann - .pian - - - piao - piau - pyau - peau - piaw - .piau - - - pie - pie - pye - piee - pieh - .pie - - - pin - pin - pyn - piin - pinn - .pin - - - ping - ping - pyng - piing - pinq - .ping - - - po - po - por - poo - poh - .po - - - pou - pou - pour - poou - pow - .pou - - - pu - pu - pwu - puu - puh - .pu - - - ran - rhan - ran - raan - rann - .rhan - - - rang - rhang - rang - raang - ranq - .rhang - - - rao - rhau - rau - rao - raw - .rhau - - - re - rhe - re - ree - reh - .rhe - - - ren - rhen - ren - reen - renn - .rhen - - - reng - rheng - reng - reeng - renq - .rheng - - - ri - rhy - ry - ryy - ryh - .rhy - - - rong - rhong - rong - roong - ronq - .rhong - - - rou - rhou - rou - roou - row - .rhou - - - ru - rhu - ru - ruu - ruh - .rhu - - - ruan - rhuan - ruan - roan - ruann - .rhuan - - - rui - rhuei - ruei - roei - ruey - .rhuei - - - run - rhuen - ruen - roen - ruenn - .rhuen - - - ruo - rhuo - ruo - ruoo - ruoh - .rhuo - - - sa - sa - sar - saa - sah - .sa - - - sai - sai - sair - sae - say - .sai - - - san - san - sarn - saan - sann - .san - - - sang - sang - sarng - saang - sanq - .sang - - - sao - sau - saur - sao - saw - .sau - - - se - se - ser - see - seh - .se - - - sei - sei - seir - seei - sey - .sei - - - sen - sen - sern - seen - senn - .sen - - - seng - seng - serng - seeng - senq - .seng - - - sha - sha - shar - shaa - shah - .sha - - - shai - shai - shair - shae - shay - .shai - - - shan - shan - sharn - shaan - shann - .shan - - - shang - shang - sharng - shaang - shanq - .shang - - - shao - shau - shaur - shao - shaw - .shau - - - she - she - sher - shee - sheh - .she - - - shei - shei - sheir - sheei - shey - .shei - - - shen - shen - shern - sheen - shenn - .shen - - - sheng - sheng - sherng - sheeng - shenq - .sheng - - - shi - shy - shyr - shyy - shyh - .shy - - - shong - shong - shorng - shoong - shonq - .shong - - - shou - shou - shour - shoou - show - .shou - - - shu - shu - shwu - shuu - shuh - .shu - - - shua - shua - shwa - shoa - shuah - .shua - - - shuai - shuai - shwai - shoai - shuay - .shuai - - - shuan - shuan - shwan - shoan - shuann - .shuan - - - shuang - shuang - shwang - shoang - shuanq - .shuang - - - shui - shuei - shwei - shoei - shuey - .shuei - - - shun - shuen - shwen - shoen - shuenn - .shuen - - - shuo - shuo - shwo - shuoo - shuoh - .shuo - - - xi - shi - shyi - shii - shih - .shi - - - xia - shia - shya - shea - shiah - .shia - - - xian - shian - shyan - shean - shiann - .shian - - - xiang - shiang - shyang - sheang - shianq - .shiang - - - xiao - shiau - shyau - sheau - shiaw - .shiau - - - xie - shie - shye - shiee - shieh - .shie - - - si - sy - syr - syy - syh - .sy - - - xin - shin - shyn - shiin - shinn - .shin - - - xing - shing - shyng - shiing - shinq - .shing - - - xiu - shiou - shyou - sheou - shiow - .shiou - - - song - song - sorng - soong - sonq - .song - - - sou - sou - sour - soou - sow - .sou - - - su - su - swu - suu - suh - .su - - - suan - suan - swan - soan - suann - .suan - - - sui - suei - swei - soei - suey - .suei - - - sun - suen - swen - soen - suenn - .suen - - - suo - suo - swo - suoo - suoh - .suo - - - xiong - shiong - shyong - sheong - shionq - .shiong - - - xu - shiu - shyu - sheu - shiuh - .shiu - - - xuan - shiuan - shyuan - sheuan - shiuann - .shiuan - - - xue - shiue - shyue - sheue - shiueh - .shiue - - - xun - shiun - shyun - sheun - shiunn - .shiun - - - ta - ta - tar - taa - tah - .ta - - - tai - tai - tair - tae - tay - .tai - - - tan - tan - tarn - taan - tann - .tan - - - tang - tang - tarng - taang - tanq - .tang - - - tao - tau - taur - tao - taw - .tau - - - te - te - ter - tee - teh - .te - - - teng - teng - terng - teeng - tenq - .teng - - - ti - ti - tyi - tii - tih - .ti - - - tian - tian - tyan - tean - tiann - .tian - - - tiao - tiau - tyau - teau - tiaw - .tiau - - - tie - tie - tye - tiee - tieh - .tie - - - ting - ting - tyng - tiing - tinq - .ting - - - tong - tong - torng - toong - tonq - .tong - - - tou - tou - tour - toou - tow - .tou - - - tu - tu - twu - tuu - tuh - .tu - - - tuan - tuan - twan - toan - tuann - .tuan - - - tui - tuei - twei - toei - tuey - .tuei - - - tun - tuen - twen - toen - tuenn - .tuen - - - tuo - tuo - two - tuoo - tuoh - .tuo - - - wa - ua - wa - waa - wah - .ua - - - wai - uai - wai - woai - way - .uai - - - wan - uan - wan - woan - wann - .uan - - - wang - uang - wang - woang - wanq - .uang - - - wei - uei - wei - woei - wey - .uei - - - wo - uo - wo - woo - woh - .uo - - - weng - ueng - weng - woeng - wenq - .ueng - - - wu - u - wu - wuu - wuh - .u - - - wen - uen - wen - woen - wenn - .uen - - - ya - ia - ya - yea - yah - .ia - - - yan - ian - yan - yean - yann - .ian - - - yang - iang - yang - yeang - yanq - .iang - - - yao - iau - yau - yeau - yaw - .iau - - - ye - ie - ye - yee - yeh - .ie - - - yi - i - yi - yii - yih - .i - - - yin - in - yn - yiin - yinn - .in - - - ying - ing - yng - yiing - yinq - .ing - - - yong - iong - yong - yeong - yonq - .iong - - - you - iou - you - yeou - yow - .iou - - - yu - iu - yu - yeu - yuh - .iu - - - yuan - iuan - yuan - yeuan - yuann - .iuan - - - yue - iue - yue - yeue - yueh - .iue - - - yun - iun - yun - yeun - yunn - .iun - - - za - tza - tzar - tzaa - tzah - .tza - - - zai - tzai - tzair - tzae - tzay - .tzai - - - zan - tzan - tzarn - tzaan - tzann - .tzan - - - zang - tzang - tzarng - tzaang - tzanq - .tzang - - - zao - tzau - tzaur - tzao - tzaw - .tzau - - - ze - tze - tzer - tzee - tzeh - .tze - - - zei - tzei - tzeir - tzeei - tzey - .tzei - - - zen - tzen - tzern - tzeen - tzenn - .tzen - - - zeng - tzeng - tzerng - tzeeng - tzenq - .tzeng - - - zi - tzy - tzyr - tzyy - tzyh - .tzy - - - zong - tzong - tzorng - tzoong - tzonq - .tzong - - - zou - tzou - tzour - tzoou - tzow - .tzou - - - zu - tzu - tzwu - tzuu - tzuh - .tzu - - - zuan - tzuan - tzwan - tzoan - tzuann - .tzuan - - - zui - tzuei - tzwei - tzoei - tzuey - .tzuei - - - zun - tzuen - tzwen - tzoen - tzuenn - .tzuen - - - zuo - tzuo - tzwo - tzuoo - tzuoh - .tzuo - - diff --git a/Flow.Launcher.Infrastructure/pinyindb/pinyin_mapping.xml b/Flow.Launcher.Infrastructure/pinyindb/pinyin_mapping.xml deleted file mode 100644 index 9143e0ca6..000000000 --- a/Flow.Launcher.Infrastructure/pinyindb/pinyin_mapping.xml +++ /dev/null @@ -1,2873 +0,0 @@ - - - - a - a - a - a - a - - - ai - ai - ai - ai - ai - - - an - an - an - an - an - - - ang - ang - ang - ang - ang - - - ao - ao - au - au - ao - - - ba - pa - ba - ba - ba - - - bai - pai - bai - bai - bai - - - ban - pan - ban - ban - ban - - - bang - pang - bang - bang - bang - - - bao - pao - bau - bau - bao - - - bei - pei - bei - bei - bei - - - ben - pen - ben - ben - ben - - - beng - peng - beng - beng - beng - - - bi - pi - bi - bi - bi - - - bian - pien - bian - byan - bian - - - biao - piao - biau - byau - biao - - - bie - pieh - bie - bye - bie - - - bin - pin - bin - bin - bin - - - bing - ping - bing - bing - bing - - - bo - po - bo - bwo - bo - - - bu - pu - bu - bu - bu - - - ca - ts`a - tsa - tsa - ca - - - cai - ts`ai - tsai - tsai - cai - - - can - ts`an - tsan - tsan - can - - - cang - ts`ang - tsang - tsang - cang - - - cao - ts`ao - tsau - tsau - cao - - - ce - ts`e - tse - tse - ce - - - cen - ts`en - tsen - tsen - cen - - - ceng - ts`eng - tseng - tseng - ceng - - - cha - ch`a - cha - cha - cha - - - chai - ch`ai - chai - chai - chai - - - chan - ch`an - chan - chan - chan - - - chang - ch`ang - chang - chang - chang - - - chao - ch`ao - chau - chau - chao - - - che - ch`e - che - che - che - - - chen - ch`en - chen - chen - chen - - - cheng - ch`eng - cheng - cheng - cheng - - - chi - ch`ih - chr - chr - chih - - - chong - ch`ung - chung - chung - chong - - - chou - ch`ou - chou - chou - chou - - - chu - ch`u - chu - chu - chu - - - chua - ch`ua - chua - chwa - chua - - - chuai - ch`uai - chuai - chwai - chuai - - - chuan - ch`uan - chuan - chwan - chuan - - - chuang - ch`uang - chuang - chwang - chuang - - - chui - ch`ui - chuei - chwei - chuei - - - chun - ch`un - chuen - chwen - chun - - - chuo - ch`o - chuo - chwo - chuo - - - qi - ch`i - chi - chi - ci - - - qia - ch`ia - chia - chya - cia - - - qian - ch`ien - chian - chyan - cian - - - qiang - ch`iang - chiang - chyang - ciang - - - qiao - ch`iao - chiau - chyau - ciao - - - qie - ch`ieh - chie - chye - cie - - - ci - tz`u - tsz - tsz - cih - - - qin - ch`in - chin - chin - cin - - - qing - ch`ing - ching - ching - cing - - - qiu - ch`iu - chiou - chyou - ciou - - - cong - ts`ung - tsung - tsung - cong - - - cou - ts`ou - tsou - tsou - cou - - - cu - ts`u - tsu - tsu - cu - - - cuan - ts`uan - tsuan - tswan - cuan - - - cui - ts`ui - tsuei - tswei - cuei - - - cun - ts`un - tsuen - tswen - cun - - - cuo - ts`o - tsuo - tswo - cuo - - - qiong - ch`iung - chiung - chyung - cyong - - - qu - ch`u: - chiu - chyu - cyu - - - quan - ch`u:an - chiuan - chywan - cyuan - - - que - ch`u:eh - chiue - chywe - cyue - - - qun - ch`u:n - chiun - chyun - cyun - - - da - ta - da - da - da - - - dai - tai - dai - dai - dai - - - dan - tan - dan - dan - dan - - - dang - tang - dang - dang - dang - - - dao - tao - dau - dau - dao - - - de - te - de - de - de - - - dei - tei - dei - dei - dei - - - den - ten - den - den - den - - - deng - teng - deng - deng - deng - - - di - ti - di - di - di - - - dian - tien - dian - dyan - dian - - - diang - tiang - diang - dyang - diang - - - diao - tiao - diau - dyau - diao - - - die - tieh - die - dye - die - - - ding - ting - ding - ding - ding - - - diu - tiu - diou - dyou - diou - - - dong - tung - dung - dung - dong - - - dou - tou - dou - dou - dou - - - du - tu - du - du - du - - - duan - tuan - duan - dwan - duan - - - dui - tui - duei - dwei - duei - - - dun - tun - duen - dwen - dun - - - duo - to - duo - dwo - duo - - - e - e - e - e - e - - - ei - ei - ei - ei - ei - - - en - en - en - en - en - - - er - erh - er - er - er - - - fa - fa - fa - fa - fa - - - fan - fan - fan - fan - fan - - - fang - fang - fang - fang - fang - - - fei - fei - fei - fei - fei - - - fen - fen - fen - fen - fen - - - fo - fo - fo - fwo - fo - - - feng - feng - feng - feng - fong - - - fou - fou - fou - fou - fou - - - fu - fu - fu - fu - fu - - - ga - ka - ga - ga - ga - - - gai - kai - gai - gai - gai - - - gan - kan - gan - gan - gan - - - gang - kang - gang - gang - gang - - - gao - kao - gau - gau - gao - - - ge - ko - ge - ge - ge - - - gei - kei - gei - gei - gei - - - gen - ken - gen - gen - gen - - - geng - keng - geng - geng - geng - - - gong - kung - gung - gung - gong - - - gou - kou - gou - gou - gou - - - gu - ku - gu - gu - gu - - - gua - kua - gua - gwa - gua - - - guai - kuai - guai - gwai - guai - - - guan - kuan - guan - gwan - guan - - - guang - kuang - guang - gwang - guang - - - gui - kuei - guei - gwei - guei - - - gun - kun - guen - gwun - gun - - - guo - kuo - guo - gwo - guo - - - ha - ha - ha - ha - ha - - - hai - hai - hai - hai - hai - - - han - han - han - han - han - - - hang - hang - hang - hang - hang - - - hao - hao - hau - hau - hao - - - he - ho - he - he - he - - - hei - hei - hei - hei - hei - - - hen - hen - hen - hen - hen - - - heng - heng - heng - heng - heng - - - hong - hung - hung - hung - hong - - - hou - hou - hou - hou - hou - - - hu - hu - hu - hu - hu - - - hua - hua - hua - hwa - hua - - - huai - huai - huai - hwai - huai - - - huan - huan - huan - hwan - huan - - - huang - huang - huang - hwang - huang - - - hui - hui - huei - hwei - huei - - - hun - hun - huen - hwen - hun - - - huo - huo - huo - hwo - huo - - - zha - cha - ja - ja - jha - - - zhai - chai - jai - jai - jhai - - - zhan - chan - jan - jan - jhan - - - zhang - chang - jang - jang - jhang - - - zhao - chao - jau - jau - jhao - - - zhe - che - je - je - jhe - - - zhei - chei - jei - jei - jhei - - - zhen - chen - jen - jen - jhen - - - zheng - cheng - jeng - jeng - jheng - - - zhi - chih - jr - jr - jhih - - - zhong - chung - jung - jung - jhong - - - zhou - chou - jou - jou - jhou - - - zhu - chu - ju - ju - jhu - - - zhua - chua - jua - jwa - jhua - - - zhuai - chuai - juai - jwai - jhuai - - - zhuan - chuan - juan - jwan - jhuan - - - zhuang - chuang - juang - jwang - jhuang - - - zhui - chui - juei - jwei - jhuei - - - zhun - chun - juen - jwen - jhun - - - zhuo - cho - juo - jwo - jhuo - - - ji - chi - ji - ji - ji - - - jia - chia - jia - jya - jia - - - jian - chien - jian - jyan - jian - - - jiang - chiang - jiang - jyang - jiang - - - jiao - chiao - jiau - jyau - jiao - - - jie - chieh - jie - jye - jie - - - jin - chin - jin - jin - jin - - - jing - ching - jing - jing - jing - - - jiu - chiu - jiou - jyou - jiou - - - jiong - chiung - jiung - jyung - jyong - - - ju - chu: - jiu - jyu - jyu - - - juan - chu:an - jiuan - jywan - jyuan - - - jue - chu:eh - jiue - jywe - jyue - - - jun - chu:n - jiun - jyun - jyun - - - ka - k`a - ka - ka - ka - - - kai - k`ai - kai - kai - kai - - - kan - k`an - kan - kan - kan - - - kang - k`ang - kang - kang - kang - - - kao - k`ao - kau - kau - kao - - - ke - k`o - ke - ke - ke - - - ken - k`en - ken - ken - ken - - - keng - k`eng - keng - keng - keng - - - kong - k`ung - kung - kung - kong - - - kou - k`ou - kou - kou - kou - - - ku - k`u - ku - ku - ku - - - kua - k`ua - kua - kwa - kua - - - kuai - k`uai - kuai - kwai - kuai - - - kuan - k`uan - kuan - kwan - kuan - - - kuang - k`uang - kuang - kwang - kuang - - - kui - k`uei - kuei - kwei - kuei - - - kun - k`un - kuen - kwen - kun - - - kuo - k`uo - kuo - kwo - kuo - - - la - la - la - la - la - - - lai - lai - lai - lai - lai - - - lan - lan - lan - lan - lan - - - lang - lang - lang - lang - lang - - - lao - lao - lau - lau - lao - - - le - le - le - le - le - - - lei - lei - lei - lei - lei - - - leng - leng - leng - leng - leng - - - li - li - li - li - li - - - lia - lia - lia - lya - lia - - - lian - lien - lian - lyan - lian - - - liang - liang - liang - lyang - liang - - - liao - liao - liau - lyau - liao - - - lie - lieh - lie - lye - lie - - - lin - lin - lin - lin - lin - - - ling - ling - ling - ling - ling - - - liu - liu - liou - lyou - liou - - - lo - lo - lo - lo - lo - - - long - lung - lung - lung - long - - - lou - lou - lou - lou - lou - - - lu - lu - lu - lu - lu - - - luan - luan - luan - lwan - luan - - - lun - lun - luen - lwun - lun - - - luo - lo - luo - lwo - luo - - - lu: - lu: - liu - lyu - lyu - - - lu:e - lu:eh - liue - lywe - lyue - - - lu:n - lu:n - liun - lyun - lyun - - - ma - ma - ma - ma - ma - - - mai - mai - mai - mai - mai - - - man - man - man - man - man - - - mang - mang - mang - mang - mang - - - mao - mao - mau - mau - mao - - - me - me - me - me - me - - - mei - mei - mei - mei - mei - - - men - men - men - men - men - - - meng - meng - meng - meng - meng - - - mi - mi - mi - mi - mi - - - mian - mien - mian - myan - mian - - - miao - miao - miau - myau - miao - - - mie - mieh - mie - mye - mie - - - min - min - min - min - min - - - ming - ming - ming - ming - ming - - - miu - miu - miou - myou - miou - - - mo - mo - mo - mwo - mo - - - mou - mou - mou - mou - mou - - - mu - mu - mu - mu - mu - - - na - na - na - na - na - - - nai - nai - nai - nai - nai - - - nan - nan - nan - nan - nan - - - nang - nang - nang - nang - nang - - - nao - nao - nau - nau - nao - - - ne - ne - ne - ne - ne - - - nei - nei - nei - nei - nei - - - nen - nen - nen - nen - nen - - - neng - neng - neng - neng - neng - - - ni - ni - ni - ni - ni - - - nia - nia - nia - nya - nia - - - nian - nien - nian - nyan - nian - - - niang - niang - niang - nyang - niang - - - niao - niao - niau - nyau - niao - - - nie - nieh - nie - nye - nie - - - nin - nin - nin - nin - nin - - - ning - ning - ning - ning - ning - - - niu - niu - niou - nyou - niou - - - nong - nung - nung - nung - nong - - - nou - nou - nou - nou - nou - - - nu - nu - nu - nu - nu - - - nuan - nuan - nuan - nwan - nuan - - - nun - nun - nuen - nwen - nun - - - nuo - no - nuo - nwo - nuo - - - nu: - nu: - niu - nyu - nyu - - - nu:e - nu:eh - niue - nywe - nyue - - - ou - ou - ou - ou - ou - - - pa - p`a - pa - pa - pa - - - pai - p`ai - pai - pai - pai - - - pan - p`an - pan - pan - pan - - - pang - p`ang - pang - pang - pang - - - pao - p`ao - pau - pau - pao - - - pei - p`ei - pei - pei - pei - - - pen - p`en - pen - pen - pen - - - peng - p`eng - peng - peng - peng - - - pi - p`i - pi - pi - pi - - - pian - p`ien - pian - pyan - pian - - - piao - p`iao - piau - pyau - piao - - - pie - p`ieh - pie - pye - pie - - - pin - p`in - pin - pin - pin - - - ping - p`ing - ping - ping - ping - - - po - p`o - po - pwo - po - - - pou - p`ou - pou - pou - pou - - - pu - p`u - pu - pu - pu - - - ran - jan - ran - ran - ran - - - rang - jang - rang - rang - rang - - - rao - jao - rau - rau - rao - - - re - je - re - re - re - - - ren - jen - ren - ren - ren - - - reng - jeng - reng - reng - reng - - - ri - jih - r - r - rih - - - rong - jung - rung - rung - rong - - - rou - jou - rou - rou - rou - - - ru - ju - ru - ru - ru - - - ruan - juan - ruan - rwan - ruan - - - rui - jui - ruei - rwei - ruei - - - run - jun - ruen - rwun - run - - - ruo - jo - ruo - rwo - ruo - - - sa - sa - sa - sa - sa - - - sai - sai - sai - sai - sai - - - san - san - san - san - san - - - sang - sang - sang - sang - sang - - - sao - sao - sau - sau - sao - - - se - se - se - se - se - - - sei - sei - sei - sei - sei - - - sen - sen - sen - sen - sen - - - seng - seng - seng - seng - seng - - - sha - sha - sha - sha - sha - - - shai - shai - shai - shai - shai - - - shan - shan - shan - shan - shan - - - shang - shang - shang - shang - shang - - - shao - shao - shau - shau - shao - - - she - she - she - she - she - - - shei - shei - shei - shei - shei - - - shen - shen - shen - shen - shen - - - sheng - sheng - sheng - sheng - sheng - - - shi - shih - shr - shr - shih - - - shong - shung - shung - shung - shong - - - shou - shou - shou - shou - shou - - - shu - shu - shu - shu - shu - - - shua - shua - shua - shwa - shua - - - shuai - shuai - shuai - shwai - shuai - - - shuan - shuan - shuan - shwan - shuan - - - shuang - shuang - shuang - shwang - shuang - - - shui - shui - shuei - shwei - shuei - - - shun - shun - shuen - shwun - shun - - - shuo - shuo - shuo - shwo - shuo - - - xi - hsi - shi - syi - si - - - xia - hsia - shia - sya - sia - - - xian - hsien - shian - syan - sian - - - xiang - hsiang - shiang - syang - siang - - - xiao - hsiao - shiau - syau - siao - - - xie - hsieh - shie - sye - sie - - - si - ssu - sz - sz - sih - - - xin - hsin - shin - syin - sin - - - xing - hsing - shing - sying - sing - - - xiu - hsiu - shiou - syou - siou - - - song - sung - sung - sung - song - - - sou - sou - sou - sou - sou - - - su - su - su - su - su - - - suan - suan - suan - swan - suan - - - sui - sui - suei - swei - suei - - - sun - sun - suen - swen - sun - - - suo - so - suo - swo - suo - - - xiong - hsiung - shiung - syung - syong - - - xu - hsu: - shiu - syu - syu - - - xuan - hsu:an - shiuan - sywan - syuan - - - xue - hsu:eh - shiue - sywe - syue - - - xun - hsu:n - shiun - syun - syun - - - ta - t`a - ta - ta - ta - - - tai - t`ai - tai - tai - tai - - - tan - t`an - tan - tan - tan - - - tang - t`ang - tang - tang - tang - - - tao - t`ao - tau - tau - tao - - - te - t`e - te - te - te - - - teng - t`eng - teng - teng - teng - - - ti - t`i - ti - ti - ti - - - tian - t`ien - tian - tyan - tian - - - tiao - t`iao - tiau - tyau - tiao - - - tie - t`ieh - tie - tye - tie - - - ting - t`ing - ting - ting - ting - - - tong - t`ung - tung - tung - tong - - - tou - t`ou - tou - tou - tou - - - tu - t`u - tu - tu - tu - - - tuan - t`uan - tuan - twan - tuan - - - tui - t`ui - tuei - twei - tuei - - - tun - t`un - tuen - twen - tun - - - tuo - t`o - tuo - two - tuo - - - wa - wa - wa - wa - wa - - - wai - wai - wai - wai - wai - - - wan - wan - wan - wan - wan - - - wang - wang - wang - wang - wang - - - wei - wei - wei - wei - wei - - - wo - wo - wo - wo - wo - - - weng - weng - weng - weng - wong - - - wu - wu - wu - wu - wu - - - wen - wen - wen - wen - wun - - - ya - ya - ya - ya - ya - - - yan - yan - yan - yan - yan - - - yang - yang - yang - yang - yang - - - yao - yao - yau - yau - yao - - - ye - yeh - ye - ye - ye - - - yi - i - yi - yi - yi - - - yin - yin - yin - yin - yin - - - ying - ying - ying - ying - ying - - - yong - yung - yung - yung - yong - - - you - yu - you - you - you - - - yu - yu: - yu - yu - yu - - - yuan - yu:an - yuan - ywan - yuan - - - yue - yu:eh - yue - ywe - yue - - - yun - yu:n - yun - yun - yun - - - za - tsa - tza - dza - za - - - zai - tsai - tzai - dzai - zai - - - zan - tsan - tzan - dzan - zan - - - zang - tsang - tzang - dzang - zang - - - zao - tsao - tzau - dzau - zao - - - ze - tse - tze - dze - ze - - - zei - tsei - tzei - dzei - zei - - - zen - tsen - tzen - dzen - zen - - - zeng - tseng - tzeng - dzeng - zeng - - - zi - tzu - tz - dz - zih - - - zong - tsung - tzung - dzung - zong - - - zou - tsou - tzou - dzou - zou - - - zu - tsu - tzu - dzu - zu - - - zuan - tsuan - tzuan - dzwan - zuan - - - zui - tsui - tzuei - dzwei - zuei - - - zun - tsun - tzuen - dzwen - zun - - - zuo - tso - tzuo - dzwo - zuo - - diff --git a/Flow.Launcher.Infrastructure/pinyindb/unicode_to_hanyu_pinyin.txt b/Flow.Launcher.Infrastructure/pinyindb/unicode_to_hanyu_pinyin.txt deleted file mode 100644 index 8cb1c9cc7..000000000 --- a/Flow.Launcher.Infrastructure/pinyindb/unicode_to_hanyu_pinyin.txt +++ /dev/null @@ -1,20903 +0,0 @@ -3007 (ling2) -4E00 (yi1) -4E01 (ding1,zheng1) -4E02 (kao3) -4E03 (qi1) -4E04 (shang4,shang3) -4E05 (xia4) -4E06 (none0) -4E07 (wan4,mo4) -4E08 (zhang4) -4E09 (san1) -4E0A (shang4,shang3) -4E0B (xia4) -4E0C (ji1) -4E0D (bu4,bu2,fou3) -4E0E (yu3,yu4,yu2) -4E0F (mian3) -4E10 (gai4) -4E11 (chou3) -4E12 (chou3) -4E13 (zhuan1) -4E14 (qie3,ju1) -4E15 (pi1) -4E16 (shi4) -4E17 (shi4) -4E18 (qiu1) -4E19 (bing3) -4E1A (ye4) -4E1B (cong2) -4E1C (dong1) -4E1D (si1) -4E1E (cheng2) -4E1F (diu1) -4E20 (qiu1) -4E21 (liang3) -4E22 (diu1) -4E23 (you3) -4E24 (liang3) -4E25 (yan2) -4E26 (bing4) -4E27 (sang1,sang4,sang5) -4E28 (shu4) -4E29 (jiu1) -4E2A (ge4,ge3) -4E2B (ya1) -4E2C (qiang2,pan2) -4E2D (zhong1,zhong4) -4E2E (ji3) -4E2F (jie4) -4E30 (feng1) -4E31 (guan4) -4E32 (chuan4) -4E33 (chan3) -4E34 (lin2) -4E35 (zhuo1) -4E36 (zhu3,dian3) -4E37 (none0) -4E38 (wan2) -4E39 (dan1) -4E3A (wei2,wei4) -4E3B (zhu3) -4E3C (jing3,dan3) -4E3D (li4,li2) -4E3E (ju3) -4E3F (pie3) -4E40 (fu2) -4E41 (yi2) -4E42 (yi4,ai4) -4E43 (nai3) -4E44 (none0) -4E45 (jiu3) -4E46 (jiu3) -4E47 (tuo1) -4E48 (me5,ma5,yao1) -4E49 (yi4) -4E4A (none0) -4E4B (zhi1) -4E4C (wu1,wu4) -4E4D (zha4) -4E4E (hu1) -4E4F (fa2) -4E50 (le4,yue4) -4E51 (zhong4) -4E52 (ping1) -4E53 (pang1) -4E54 (qiao2) -4E55 (hu3,hu4) -4E56 (guai1) -4E57 (cheng2,sheng4) -4E58 (cheng2,sheng4) -4E59 (yi3) -4E5A (yin3) -4E5B (none0) -4E5C (mie1,nie4) -4E5D (jiu3) -4E5E (qi3) -4E5F (ye3) -4E60 (xi2) -4E61 (xiang1) -4E62 (gai4) -4E63 (diu1) -4E64 (none0) -4E65 (none0) -4E66 (shu1) -4E67 (none0) -4E68 (shi3) -4E69 (ji1) -4E6A (nang1) -4E6B (jia1) -4E6C (none0) -4E6D (shi2) -4E6E (none0) -4E6F (none0) -4E70 (mai3) -4E71 (luan4) -4E72 (none0) -4E73 (ru3) -4E74 (xi3) -4E75 (yan3) -4E76 (fu3) -4E77 (sha1) -4E78 (na3) -4E79 (gan1,qian2) -4E7A (none0) -4E7B (none0) -4E7C (none0) -4E7D (none0) -4E7E (qian2,gan1) -4E7F (zhi4) -4E80 (gui1,jun1,qiu1) -4E81 (gan1) -4E82 (luan4) -4E83 (lin3) -4E84 (yi4) -4E85 (jue2) -4E86 (le5,liao3,liao4) -4E87 (none0) -4E88 (yu3,yu2) -4E89 (zheng1) -4E8A (shi4) -4E8B (shi4) -4E8C (er4) -4E8D (chu4) -4E8E (yu2) -4E8F (kui1) -4E90 (yu2) -4E91 (yun2) -4E92 (hu4) -4E93 (qi2) -4E94 (wu3) -4E95 (jing3) -4E96 (si4) -4E97 (sui4) -4E98 (gen4) -4E99 (gen4,geng4) -4E9A (ya4) -4E9B (xie1,xie5) -4E9C (ya4) -4E9D (qi2) -4E9E (ya4,ya3) -4E9F (ji2,qi4) -4EA0 (tou2) -4EA1 (wang2,wu2) -4EA2 (kang4) -4EA3 (ta4) -4EA4 (jiao1) -4EA5 (hai4) -4EA6 (yi4) -4EA7 (chan3) -4EA8 (heng1) -4EA9 (mu3) -4EAA (none0) -4EAB (xiang3) -4EAC (jing1) -4EAD (ting2) -4EAE (liang4) -4EAF (heng1) -4EB0 (jing1) -4EB1 (ye4) -4EB2 (qin1,qin5,qing4) -4EB3 (bo2) -4EB4 (you4) -4EB5 (xie4) -4EB6 (dan3,dan4) -4EB7 (lian2) -4EB8 (duo3) -4EB9 (wei3,wei4) -4EBA (ren2) -4EBB (ren2) -4EBC (ji2) -4EBD (none0) -4EBE (wang2) -4EBF (yi4) -4EC0 (shen2,shi2,she2) -4EC1 (ren2) -4EC2 (le4) -4EC3 (ding1) -4EC4 (ze4) -4EC5 (jin3,jin4) -4EC6 (pu1,pu2) -4EC7 (chou2,qiu2) -4EC8 (ba1) -4EC9 (zhang3) -4ECA (jin1) -4ECB (jie4) -4ECC (bing1) -4ECD (reng2) -4ECE (cong2,cong1) -4ECF (fo2,fu2) -4ED0 (san3) -4ED1 (lun2) -4ED2 (none0) -4ED3 (cang1) -4ED4 (zi3,zai3,zi1) -4ED5 (shi4) -4ED6 (ta1) -4ED7 (zhang4) -4ED8 (fu4) -4ED9 (xian1) -4EDA (xian1) -4EDB (cha4) -4EDC (hong2) -4EDD (tong2) -4EDE (ren4) -4EDF (qian1) -4EE0 (gan3) -4EE1 (ge1,yi4) -4EE2 (di2) -4EE3 (dai4) -4EE4 (ling4,ling2,ling3) -4EE5 (yi3) -4EE6 (chao4) -4EE7 (chang2) -4EE8 (sa1) -4EE9 (shang4) -4EEA (yi2) -4EEB (mu4) -4EEC (men5,men2) -4EED (ren4) -4EEE (jia3,jia4) -4EEF (chao4) -4EF0 (yang3) -4EF1 (qian2) -4EF2 (zhong4) -4EF3 (pi3) -4EF4 (wan4) -4EF5 (wu3) -4EF6 (jian4) -4EF7 (jia4,jie4,jie5) -4EF8 (yao3) -4EF9 (feng1) -4EFA (cang1) -4EFB (ren4,ren2) -4EFC (wang2) -4EFD (fen4) -4EFE (di1) -4EFF (fang3) -4F00 (zhong1) -4F01 (qi3,qi4) -4F02 (pei4) -4F03 (yu2) -4F04 (diao4) -4F05 (dun4) -4F06 (wen4) -4F07 (yi4) -4F08 (xin3) -4F09 (kang4) -4F0A (yi1) -4F0B (ji2) -4F0C (ai4) -4F0D (wu3) -4F0E (ji4) -4F0F (fu2) -4F10 (fa2,fa1) -4F11 (xiu1) -4F12 (jin4) -4F13 (bei1) -4F14 (chen2) -4F15 (fu1) -4F16 (tang3) -4F17 (zhong4) -4F18 (you1) -4F19 (huo3) -4F1A (hui4,kuai4) -4F1B (yu3) -4F1C (cui4,zu2) -4F1D (yun2) -4F1E (san3) -4F1F (wei3) -4F20 (chuan2,zhuan4) -4F21 (che1) -4F22 (ya2) -4F23 (xian4) -4F24 (shang1) -4F25 (chang1,tang3) -4F26 (lun2) -4F27 (cang1,chen5) -4F28 (xun4) -4F29 (xin4) -4F2A (wei3) -4F2B (zhu4) -4F2C (chi3) -4F2D (xuan2) -4F2E (nao2,nu3) -4F2F (bo2,bai3,ba4) -4F30 (gu1,gu4) -4F31 (ni3) -4F32 (ni4) -4F33 (xie4) -4F34 (ban4) -4F35 (xu4) -4F36 (ling2) -4F37 (zhou4) -4F38 (shen1) -4F39 (qu1) -4F3A (si4,ci4) -4F3B (beng1) -4F3C (si4,shi4) -4F3D (jia1,ga1,qie2,qia1) -4F3E (pi1) -4F3F (yi4) -4F40 (si4,shi4) -4F41 (ai3) -4F42 (zheng1,zheng4) -4F43 (dian4,tian2) -4F44 (han2) -4F45 (mai4) -4F46 (dan4) -4F47 (zhu4) -4F48 (bu4) -4F49 (qu1) -4F4A (bi3) -4F4B (shao4) -4F4C (ci3) -4F4D (wei4) -4F4E (di1) -4F4F (zhu4) -4F50 (zuo3) -4F51 (you4) -4F52 (yang1) -4F53 (ti3,ben4,ti1) -4F54 (zhan4) -4F55 (he2,he4) -4F56 (bi4) -4F57 (tuo2) -4F58 (she2) -4F59 (yu2,tu2) -4F5A (yi4,die2) -4F5B (fo2,fu2) -4F5C (zuo4,zuo1,zuo2) -4F5D (gou1) -4F5E (ning4) -4F5F (tong2) -4F60 (ni3) -4F61 (xuan1,san3) -4F62 (ju4) -4F63 (yong1,yong4) -4F64 (wa3) -4F65 (qian1) -4F66 (none0) -4F67 (ka3) -4F68 (none0) -4F69 (pei4) -4F6A (huai2) -4F6B (he4) -4F6C (lao3) -4F6D (xiang2) -4F6E (ge2) -4F6F (yang2) -4F70 (bai3) -4F71 (fa3) -4F72 (ming2) -4F73 (jia1) -4F74 (nai4,er4) -4F75 (bing4) -4F76 (ji2) -4F77 (heng2) -4F78 (huo2) -4F79 (gui3) -4F7A (quan2) -4F7B (tiao1,tiao2) -4F7C (jiao3,jia3) -4F7D (ci4) -4F7E (yi4) -4F7F (shi3,shi4) -4F80 (xing2) -4F81 (shen1) -4F82 (tuo1) -4F83 (kan3) -4F84 (zhi2) -4F85 (gai1,kai1) -4F86 (lai2) -4F87 (yi2) -4F88 (chi3) -4F89 (kua3) -4F8A (guang1) -4F8B (li4) -4F8C (yin1) -4F8D (shi4) -4F8E (mi3) -4F8F (zhu1) -4F90 (xu4) -4F91 (you4) -4F92 (an1) -4F93 (lu4) -4F94 (mou2) -4F95 (er2) -4F96 (lun2) -4F97 (dong4,tong2,tong3) -4F98 (cha4) -4F99 (chi4) -4F9A (xun4) -4F9B (gong1,gong4) -4F9C (zhou1) -4F9D (yi1) -4F9E (ru3) -4F9F (jian4) -4FA0 (xia2) -4FA1 (jia4,jie4) -4FA2 (zai4) -4FA3 (lu:3) -4FA4 (none0) -4FA5 (jiao3,yao2,jia3) -4FA6 (zhen1) -4FA7 (ce4,ze4,zhai1) -4FA8 (qiao2) -4FA9 (kuai4) -4FAA (chai2) -4FAB (ning4) -4FAC (nong2) -4FAD (jin3,jin4) -4FAE (wu3) -4FAF (hou4,hou2) -4FB0 (jiong3) -4FB1 (cheng3) -4FB2 (zhen4) -4FB3 (cuo4) -4FB4 (chou3) -4FB5 (qin1) -4FB6 (lu:3) -4FB7 (ju2) -4FB8 (shu4) -4FB9 (ting3) -4FBA (shen4) -4FBB (tuo1) -4FBC (bo2) -4FBD (nan2) -4FBE (hao1) -4FBF (bian4,pian2) -4FC0 (tui3) -4FC1 (yu3) -4FC2 (xi4) -4FC3 (cu4) -4FC4 (e2,e4) -4FC5 (qiu2) -4FC6 (xu2) -4FC7 (kuang3) -4FC8 (ku4) -4FC9 (wu2) -4FCA (jun4,zun4,juan4) -4FCB (yi4) -4FCC (fu3) -4FCD (lang2) -4FCE (zu3) -4FCF (qiao4) -4FD0 (li4) -4FD1 (yong3) -4FD2 (hun4) -4FD3 (jing4) -4FD4 (xian4) -4FD5 (san4) -4FD6 (pai3) -4FD7 (su2) -4FD8 (fu2) -4FD9 (xi1) -4FDA (li3) -4FDB (mian3) -4FDC (ping1) -4FDD (bao3) -4FDE (yu2,shu4) -4FDF (si4,qi2) -4FE0 (xia2) -4FE1 (xin4,shen1) -4FE2 (xiu1) -4FE3 (yu3) -4FE4 (ti4) -4FE5 (che1) -4FE6 (chou2) -4FE7 (none0) -4FE8 (yan3) -4FE9 (liang3,lia3) -4FEA (li4) -4FEB (lai2) -4FEC (si1) -4FED (jian3) -4FEE (xiu1) -4FEF (fu3) -4FF0 (he2) -4FF1 (ju4,ju1) -4FF2 (xiao4) -4FF3 (pai2) -4FF4 (jian4) -4FF5 (biao4,biao3) -4FF6 (ti4,chu4) -4FF7 (fei4) -4FF8 (feng4) -4FF9 (ya4) -4FFA (an3) -4FFB (bei4) -4FFC (yu4,zhou1) -4FFD (xin1) -4FFE (bi3,bei1,bi4) -4FFF (chi2) -5000 (chang1) -5001 (zhi1) -5002 (bing4) -5003 (zan2) -5004 (yao2) -5005 (cui4) -5006 (lia3,liang3) -5007 (wan3) -5008 (lai2) -5009 (cang1) -500A (zong3) -500B (ge4,ge5) -500C (guan1) -500D (bei4) -500E (tian1) -500F (shu1,shu4) -5010 (shu1) -5011 (men5,men2) -5012 (dao3,dao4) -5013 (tan2) -5014 (jue2,jue4) -5015 (chui2) -5016 (xing4) -5017 (peng2) -5018 (tang3,chang2) -5019 (hou4) -501A (yi3) -501B (qi1) -501C (ti4) -501D (gan4) -501E (jing4,liang4) -501F (jie4) -5020 (xu1) -5021 (chang4,chang1) -5022 (jie2) -5023 (fang3) -5024 (zhi2) -5025 (kong1,kong3) -5026 (juan4) -5027 (zong1) -5028 (ju4) -5029 (qian4) -502A (ni2) -502B (lun2) -502C (zhuo1,zhuo2) -502D (wo1) -502E (luo3) -502F (song1) -5030 (leng2) -5031 (hun4) -5032 (dong1) -5033 (zi4) -5034 (ben4) -5035 (wu3) -5036 (ju4,ju1) -5037 (nai4) -5038 (cai3) -5039 (jian3) -503A (zhai4) -503B (ye1) -503C (zhi2) -503D (sha4) -503E (qing1) -503F (none0) -5040 (ying1) -5041 (cheng1,cheng4) -5042 (qian2) -5043 (yan3) -5044 (nuan4) -5045 (zhong4) -5046 (chun3) -5047 (jia3,jia4) -5048 (jie2,ji4) -5049 (wei3) -504A (yu3) -504B (bing4) -504C (ruo4) -504D (ti2) -504E (wei1) -504F (pian1) -5050 (yan4) -5051 (feng1) -5052 (tang3) -5053 (wo4) -5054 (e4) -5055 (xie2,jie1) -5056 (che3) -5057 (sheng3) -5058 (kan3) -5059 (di4) -505A (zuo4) -505B (cha1) -505C (ting2) -505D (bei1) -505E (ye4) -505F (huang2) -5060 (yao3) -5061 (zhan4) -5062 (qiu1) -5063 (yan1) -5064 (you2) -5065 (jian4) -5066 (xu3) -5067 (zha1) -5068 (chai1) -5069 (fu4) -506A (bi1) -506B (zhi4) -506C (zong3) -506D (mian3) -506E (ji2) -506F (yi3) -5070 (xie4) -5071 (xun2) -5072 (si1,cai1) -5073 (duan1) -5074 (ce4,ze4) -5075 (zhen1) -5076 (ou3) -5077 (tou1) -5078 (tou1) -5079 (bei4) -507A (za2,zan2) -507B (lou2,lu:3) -507C (jie2) -507D (wei4) -507E (fen4) -507F (chang2) -5080 (kui3,gui1) -5081 (sou3) -5082 (chi3) -5083 (su4) -5084 (xia1) -5085 (fu4) -5086 (yuan4) -5087 (rong3) -5088 (li4) -5089 (ru4) -508A (yun3) -508B (gou4) -508C (ma4) -508D (bang4,bang1) -508E (dian1) -508F (tang2) -5090 (hao1) -5091 (jie2) -5092 (xi1) -5093 (shan1) -5094 (qian4) -5095 (jue2) -5096 (cang1) -5097 (chu4) -5098 (san3) -5099 (bei4) -509A (xiao4) -509B (yong2) -509C (yao2) -509D (ta4) -509E (suo1) -509F (wang1) -50A0 (fa2) -50A1 (bing4,bing1) -50A2 (jia1) -50A3 (dai3) -50A4 (zai4) -50A5 (tang3) -50A6 (none0) -50A7 (bin1) -50A8 (chu3) -50A9 (nuo2) -50AA (zan1) -50AB (lei3) -50AC (cui1) -50AD (yong1,yong4,yong2) -50AE (zao1) -50AF (zong3) -50B0 (peng2) -50B1 (song3) -50B2 (ao4) -50B3 (chuan2,zhuan4) -50B4 (yu3) -50B5 (zhai4) -50B6 (zu2) -50B7 (shang1) -50B8 (qiang3) -50B9 (qiang1) -50BA (chi4) -50BB (sha3) -50BC (han4) -50BD (zhang1) -50BE (qing1,qing2) -50BF (yan4) -50C0 (di4) -50C1 (xi1) -50C2 (lou2,lu:3) -50C3 (bei4) -50C4 (piao1) -50C5 (jin3,jin4) -50C6 (lian3) -50C7 (lu4) -50C8 (man4) -50C9 (qian1) -50CA (xian1) -50CB (qiu2) -50CC (ying2) -50CD (dong4) -50CE (zhuan4) -50CF (xiang4) -50D0 (shan3) -50D1 (qiao2) -50D2 (jiong3) -50D3 (tui2) -50D4 (zun3) -50D5 (pu2,pu1) -50D6 (xi1) -50D7 (lao4) -50D8 (chang3) -50D9 (guang1) -50DA (liao2) -50DB (qi1) -50DC (deng4) -50DD (chan2) -50DE (wei3) -50DF (zhang3) -50E0 (fan1) -50E1 (hui4) -50E2 (chuan3) -50E3 (tie3) -50E4 (dan4) -50E5 (jiao3,yao2) -50E6 (jiu4) -50E7 (seng1) -50E8 (fen4) -50E9 (xian4) -50EA (jue2) -50EB (e4) -50EC (jiao1) -50ED (jian4) -50EE (tong2,zhuang4) -50EF (lin2) -50F0 (bo2) -50F1 (gu4) -50F2 (xian1) -50F3 (su4) -50F4 (xian4) -50F5 (jiang1) -50F6 (min3) -50F7 (ye4) -50F8 (jin4) -50F9 (jia4) -50FA (qiao4) -50FB (pi4) -50FC (feng1) -50FD (zhou4) -50FE (ai4) -50FF (sai4) -5100 (yi2) -5101 (jun4,juan4) -5102 (nong2) -5103 (shan4) -5104 (yi4) -5105 (dang1) -5106 (jing3) -5107 (xuan1) -5108 (kuai4) -5109 (jian3) -510A (chu4) -510B (dan1) -510C (jiao3) -510D (sha3) -510E (zai4,zai3) -510F (none0) -5110 (bin4,bin1) -5111 (an4) -5112 (ru2) -5113 (tai2) -5114 (chou2) -5115 (chai2) -5116 (lan2) -5117 (ni3) -5118 (jin3,jin4) -5119 (qian1) -511A (meng2) -511B (wu3) -511C (neng2) -511D (qiong2) -511E (ni3) -511F (chang2) -5120 (lie4) -5121 (lei3) -5122 (lu:3) -5123 (kuang3) -5124 (bao4) -5125 (du2) -5126 (biao1) -5127 (zan3) -5128 (zhi2) -5129 (si4) -512A (you1) -512B (hao2) -512C (qin1) -512D (chen4) -512E (li4) -512F (teng2) -5130 (wei3) -5131 (long2) -5132 (chu3,chu2) -5133 (chan4) -5134 (rang2) -5135 (shu4) -5136 (hui4) -5137 (li4) -5138 (luo2) -5139 (zan3,zuan3) -513A (nuo2) -513B (tang3) -513C (yan3) -513D (lei4) -513E (nang4) -513F (er2,r2) -5140 (wu4,wu1) -5141 (yun3) -5142 (zan1) -5143 (yuan2) -5144 (xiong1) -5145 (chong1) -5146 (zhao4) -5147 (xiong1) -5148 (xian1) -5149 (guang1) -514A (dui4) -514B (ke4) -514C (dui4) -514D (mian3,wen4) -514E (tu4) -514F (chang2,zhang3) -5150 (er2) -5151 (dui4) -5152 (er2,er1) -5153 (jin1) -5154 (tu4) -5155 (si4) -5156 (yan3) -5157 (yan3) -5158 (shi3) -5159 (shi2,ke4) -515A (dang3) -515B (qian1) -515C (dou1) -515D (fen1) -515E (mao2) -515F (xin1) -5160 (dou1) -5161 (bai3,ke4) -5162 (jing1) -5163 (li3) -5164 (kuang4) -5165 (ru4) -5166 (wang2,wu2) -5167 (nei4) -5168 (quan2) -5169 (liang3) -516A (yu2,shu4) -516B (ba1,ba2) -516C (gong1) -516D (liu4,lu4) -516E (xi1) -516F (none0) -5170 (lan2) -5171 (gong4,gong1,gong3) -5172 (tian1) -5173 (guan1) -5174 (xing1,xing4) -5175 (bing1) -5176 (qi2,ji1) -5177 (ju4) -5178 (dian3) -5179 (zi1,ci2) -517A (none0) -517B (yang3) -517C (jian1) -517D (shou4) -517E (ji4) -517F (yi4) -5180 (ji4) -5181 (chan3) -5182 (jiong1) -5183 (mao4) -5184 (ran3) -5185 (nei4) -5186 (yuan2) -5187 (mao3,mou3) -5188 (gang1) -5189 (ran3) -518A (ce4) -518B (jiong1) -518C (ce4) -518D (zai4) -518E (gua3) -518F (jiong3) -5190 (mao4,mo4) -5191 (zhou4) -5192 (mao4,mo4) -5193 (gou4) -5194 (xu3) -5195 (mian3) -5196 (mi4) -5197 (rong3) -5198 (yin2) -5199 (xie3,xie4) -519A (kan3) -519B (jun1) -519C (nong2) -519D (yi2) -519E (mi2) -519F (shi4) -51A0 (guan1,guan4) -51A1 (meng2) -51A2 (zhong3) -51A3 (zui4) -51A4 (yuan1) -51A5 (ming2) -51A6 (kou4) -51A7 (none0) -51A8 (fu4) -51A9 (xie3) -51AA (mi4) -51AB (bing1) -51AC (dong1) -51AD (tai4) -51AE (gang1) -51AF (feng2,ping2) -51B0 (bing1) -51B1 (hu4) -51B2 (chong1,chong4) -51B3 (jue2) -51B4 (hu4) -51B5 (kuang4) -51B6 (ye3) -51B7 (leng3) -51B8 (pan4) -51B9 (fu3) -51BA (min3) -51BB (dong4) -51BC (xian3) -51BD (lie4) -51BE (xia2) -51BF (jian1) -51C0 (jing4) -51C1 (shu4) -51C2 (mei3) -51C3 (shang4) -51C4 (qi1) -51C5 (gu4) -51C6 (zhun3) -51C7 (song1) -51C8 (jing4) -51C9 (liang2,liang4) -51CA (qing4,jing4) -51CB (diao1) -51CC (ling2) -51CD (dong4) -51CE (gan4) -51CF (jian3) -51D0 (yin1,yin2) -51D1 (cou4) -51D2 (ai2) -51D3 (li4) -51D4 (cang1) -51D5 (ming3) -51D6 (zhun3) -51D7 (cui2) -51D8 (si1) -51D9 (duo2) -51DA (jin4) -51DB (lin3) -51DC (lin3) -51DD (ning2) -51DE (xi1) -51DF (du2) -51E0 (ji3,ji1) -51E1 (fan2) -51E2 (fan2) -51E3 (fan2) -51E4 (feng4) -51E5 (ju1) -51E6 (chu3,chu4) -51E7 (none0) -51E8 (feng1) -51E9 (none0) -51EA (none0) -51EB (fu2) -51EC (feng1) -51ED (ping2) -51EE (feng1) -51EF (kai3) -51F0 (huang2) -51F1 (kai3) -51F2 (gan1) -51F3 (deng4) -51F4 (ping2) -51F5 (qu1,kan3) -51F6 (xiong1) -51F7 (kuai4) -51F8 (tu1,tu2,gu3) -51F9 (ao1,wa1) -51FA (chu1) -51FB (ji1) -51FC (dang4) -51FD (han2) -51FE (han2) -51FF (zao2,zuo4) -5200 (dao1) -5201 (diao1) -5202 (dao1) -5203 (ren4) -5204 (ren4) -5205 (chuang1) -5206 (fen1,fen4) -5207 (qie4,qie1) -5208 (yi4) -5209 (ji4) -520A (kan1) -520B (qian4) -520C (cun3) -520D (chu2) -520E (wen3) -520F (ji1) -5210 (dan3) -5211 (xing2) -5212 (hua4,hua2,huai5) -5213 (wan2) -5214 (jue2) -5215 (li2) -5216 (yue4) -5217 (lie4) -5218 (liu2) -5219 (ze2) -521A (gang1) -521B (chuang4,chuang1) -521C (fu2) -521D (chu1) -521E (qu4) -521F (ju1) -5220 (shan1) -5221 (min3) -5222 (ling2) -5223 (zhong1) -5224 (pan4) -5225 (bie2) -5226 (jie2) -5227 (jie2) -5228 (bao4,pao2) -5229 (li4) -522A (shan1) -522B (bie2,bie4) -522C (chan3) -522D (jing3) -522E (gua1) -522F (gen1) -5230 (dao4) -5231 (chuang4) -5232 (kui1) -5233 (ku1) -5234 (duo4) -5235 (er4) -5236 (zhi4) -5237 (shua1,shua4) -5238 (quan4,xuan4) -5239 (cha4,sha1) -523A (ci4,ci1) -523B (ke4,ke1) -523C (jie2) -523D (gui4) -523E (ci4) -523F (gui4) -5240 (kai3) -5241 (duo4) -5242 (ji4) -5243 (ti4) -5244 (jing3) -5245 (lou2) -5246 (luo2) -5247 (ze2) -5248 (yuan1) -5249 (cuo4) -524A (xue1,xiao1,xue4) -524B (ke4) -524C (la4,la2) -524D (qian2) -524E (cha4) -524F (chuan4) -5250 (gua3) -5251 (jian4) -5252 (cuo4) -5253 (li2) -5254 (ti1) -5255 (fei4) -5256 (pou1,pou3,po3) -5257 (chan3) -5258 (qi2) -5259 (chuang4) -525A (zi4) -525B (gang1) -525C (wan1) -525D (bo1) -525E (ji1,ji3) -525F (duo1) -5260 (qing2) -5261 (yan3,shan4) -5262 (zhuo2) -5263 (jian4) -5264 (ji4) -5265 (bo1,bao1) -5266 (yan1) -5267 (ju4) -5268 (huo4) -5269 (sheng4) -526A (jian3) -526B (duo2) -526C (duan1) -526D (wu1) -526E (gua3) -526F (fu4) -5270 (sheng4) -5271 (jian4) -5272 (ge1) -5273 (zha2) -5274 (kai3) -5275 (chuang4,chuang1) -5276 (juan1) -5277 (chan3) -5278 (tuan2,zhuan1) -5279 (lu4) -527A (li2) -527B (fou2) -527C (shan1) -527D (piao1,piao4) -527E (kou1) -527F (jiao3,chao1,jia3) -5280 (gua1) -5281 (qiao1) -5282 (jue2) -5283 (hua4,hua2) -5284 (zha2) -5285 (zhuo2) -5286 (lian2) -5287 (ju4) -5288 (pi1,pi3) -5289 (liu2) -528A (gui4) -528B (jiao3) -528C (gui4) -528D (jian4) -528E (jian4) -528F (tang1) -5290 (huo1) -5291 (ji4) -5292 (jian4) -5293 (yi4) -5294 (jian4) -5295 (zhi4) -5296 (chan2) -5297 (cuan2) -5298 (mo2) -5299 (li2) -529A (zhu2) -529B (li4) -529C (ya4) -529D (quan4) -529E (ban4) -529F (gong1) -52A0 (jia1) -52A1 (wu4) -52A2 (mai4) -52A3 (lie4) -52A4 (jing4) -52A5 (keng1) -52A6 (xie2) -52A7 (zhi3) -52A8 (dong4) -52A9 (zhu4) -52AA (nu3,nao2) -52AB (jie2) -52AC (qu2) -52AD (shao4) -52AE (yi4) -52AF (zhu1) -52B0 (mo4) -52B1 (li4) -52B2 (jing4,jin4) -52B3 (lao2) -52B4 (lao2) -52B5 (juan4) -52B6 (kou3) -52B7 (yang2) -52B8 (wa1) -52B9 (xiao4) -52BA (mou2) -52BB (kuang1) -52BC (jie2) -52BD (lie4) -52BE (he2) -52BF (shi4) -52C0 (ke4) -52C1 (jing4,jin4) -52C2 (hao2) -52C3 (bo2) -52C4 (min3) -52C5 (chi4) -52C6 (lang2) -52C7 (yong3) -52C8 (yong3) -52C9 (mian3) -52CA (ke4) -52CB (xun1) -52CC (juan4) -52CD (qing2) -52CE (lu4) -52CF (bu4) -52D0 (meng3) -52D1 (lai4) -52D2 (le4,lei1) -52D3 (kai4) -52D4 (mian3) -52D5 (dong4) -52D6 (xu4) -52D7 (xu4) -52D8 (kan1,kan4) -52D9 (wu4) -52DA (yi4) -52DB (xun1) -52DC (weng3) -52DD (sheng4,sheng1) -52DE (lao2,lao4) -52DF (mu4) -52E0 (lu4) -52E1 (piao1) -52E2 (shi4) -52E3 (ji1) -52E4 (qin2) -52E5 (qiang3) -52E6 (jiao3,chao1) -52E7 (quan4) -52E8 (xiang4) -52E9 (yi4) -52EA (qiao1) -52EB (fan2) -52EC (juan1) -52ED (tong2) -52EE (ju4) -52EF (dan1) -52F0 (xie2) -52F1 (mai4) -52F2 (xun1) -52F3 (xun1) -52F4 (lu:4) -52F5 (li4) -52F6 (che4) -52F7 (rang2) -52F8 (quan4) -52F9 (bao1) -52FA (shao2,shuo4,biao1) -52FB (yun2) -52FC (jiu1) -52FD (bao4) -52FE (gou1,gou4) -52FF (wu4) -5300 (yun2) -5301 (none0) -5302 (none0) -5303 (gai4) -5304 (gai4) -5305 (bao1) -5306 (cong1) -5307 (none0) -5308 (xiong1) -5309 (peng1) -530A (ju2) -530B (tao2) -530C (ge2) -530D (pu2) -530E (an4) -530F (pao2) -5310 (fu2) -5311 (gong1) -5312 (da2) -5313 (jiu4) -5314 (qiong1) -5315 (bi3) -5316 (hua4,hua1) -5317 (bei3) -5318 (nao3) -5319 (chi2,shi5) -531A (fang1,xi3) -531B (jiu4) -531C (yi2) -531D (za1) -531E (jiang4) -531F (kang4) -5320 (jiang4) -5321 (kuang1) -5322 (hu1) -5323 (xia2) -5324 (qu1) -5325 (fan2) -5326 (gui3) -5327 (qie4) -5328 (cang2,zang4) -5329 (kuang1) -532A (fei3) -532B (hu1) -532C (yu3) -532D (gui3) -532E (kui4) -532F (hui4) -5330 (dan1) -5331 (kui4) -5332 (lian2) -5333 (lian2) -5334 (suan3) -5335 (du2) -5336 (jiu4) -5337 (qu2) -5338 (xi4) -5339 (pi3,pi1,ya3) -533A (qu1,ou1) -533B (yi1) -533C (an4) -533D (yan3) -533E (bian3) -533F (ni4) -5340 (qu1,ou1) -5341 (shi2) -5342 (xin4) -5343 (qian1) -5344 (nian4) -5345 (sa4) -5346 (zu2) -5347 (sheng1) -5348 (wu3) -5349 (hui4) -534A (ban4) -534B (shi4) -534C (xi4) -534D (wan4) -534E (hua2,hua4,hua1) -534F (xie2) -5350 (wan4) -5351 (bei1) -5352 (zu2,cu4) -5353 (zhuo2,zhuo1) -5354 (xie2) -5355 (dan1,chan2,shan4) -5356 (mai4) -5357 (nan2,na1) -5358 (dan1,chan2) -5359 (ji2) -535A (bo2) -535B (shuai4,lu:4) -535C (bu3,bo5) -535D (kuang4) -535E (bian4) -535F (bu3) -5360 (zhan4,zhan1) -5361 (ka3,qia3) -5362 (lu2) -5363 (you3) -5364 (lu3) -5365 (xi1) -5366 (gua4) -5367 (wo4) -5368 (xie4) -5369 (jie2) -536A (jie2) -536B (wei4) -536C (ang2,yang3) -536D (qiong2) -536E (zhi1) -536F (mao3) -5370 (yin4) -5371 (wei1,wei2) -5372 (shao4) -5373 (ji2) -5374 (que4) -5375 (luan3) -5376 (shi4) -5377 (juan3,juan4,quan2) -5378 (xie4) -5379 (xu4) -537A (jin3) -537B (que4) -537C (wu4) -537D (ji2) -537E (e4) -537F (qing1) -5380 (xi1) -5381 (none0) -5382 (chang3,han3,an1) -5383 (han3) -5384 (e4) -5385 (ting1) -5386 (li4) -5387 (zhe2) -5388 (an1,chang3) -5389 (li4) -538A (ya3) -538B (ya1,ya4) -538C (yan4) -538D (she4) -538E (zhi3) -538F (zha3) -5390 (pang2) -5391 (none0) -5392 (ke4) -5393 (ya2) -5394 (zhi4) -5395 (ce4,si5) -5396 (pang2) -5397 (ti2) -5398 (li2) -5399 (she4) -539A (hou4) -539B (ting1) -539C (zui1) -539D (cuo4) -539E (fei4) -539F (yuan2) -53A0 (ce4,si5) -53A1 (yuan2) -53A2 (xiang1) -53A3 (yan3) -53A4 (li4) -53A5 (jue2) -53A6 (sha4,xia4) -53A7 (dian1) -53A8 (chu2) -53A9 (jiu4) -53AA (qin2,jin3) -53AB (ao2) -53AC (gui3) -53AD (yan4,yan1) -53AE (si1) -53AF (li4) -53B0 (chang3,an1) -53B1 (lan2) -53B2 (li4) -53B3 (yan2) -53B4 (yan3) -53B5 (yuan2) -53B6 (si1) -53B7 (si1) -53B8 (lin2) -53B9 (qiu2) -53BA (qu4) -53BB (qu4) -53BC (none0) -53BD (lei3) -53BE (du1) -53BF (xian4) -53C0 (zhuan1) -53C1 (san1) -53C2 (can1,cen1,shen1) -53C3 (can1,cen1,shen1,san1) -53C4 (san1) -53C5 (can1,cen1,shen1) -53C6 (ai4) -53C7 (dai4) -53C8 (you4) -53C9 (cha1,cha2,cha3,cha4) -53CA (ji2) -53CB (you3) -53CC (shuang1) -53CD (fan3) -53CE (shou1) -53CF (guai4) -53D0 (ba2) -53D1 (fa1,fa4) -53D2 (ruo4) -53D3 (shi4) -53D4 (shu1,shu2) -53D5 (zhui4) -53D6 (qu3) -53D7 (shou4) -53D8 (bian4) -53D9 (xu4) -53DA (jia3) -53DB (pan4) -53DC (sou3) -53DD (ji2) -53DE (yu4) -53DF (sou3) -53E0 (die2) -53E1 (rui4) -53E2 (cong2) -53E3 (kou3) -53E4 (gu3) -53E5 (ju4,gou1) -53E6 (ling4) -53E7 (gua3) -53E8 (tao1,dao1,dao2) -53E9 (kou4) -53EA (zhi3,zhi1) -53EB (jiao4) -53EC (zhao4,shao4,zhao1) -53ED (ba1) -53EE (ding1) -53EF (ke3,ke4) -53F0 (tai2,tai1) -53F1 (chi4) -53F2 (shi3) -53F3 (you4) -53F4 (qiu2) -53F5 (po3) -53F6 (ye4,xie2) -53F7 (hao4,hao2) -53F8 (si1) -53F9 (tan4) -53FA (chi3) -53FB (le4) -53FC (diao1) -53FD (ji1) -53FE (none0) -53FF (hong1) -5400 (mie1) -5401 (yu4,xu1,yu1) -5402 (mang2) -5403 (chi1,ji2) -5404 (ge4,ge3) -5405 (xuan1) -5406 (yao1) -5407 (zi3) -5408 (he2,ge3) -5409 (ji2) -540A (diao4) -540B (cun4) -540C (tong2,tong4) -540D (ming2) -540E (hou4) -540F (li4) -5410 (tu3,tu4) -5411 (xiang4) -5412 (zha4,zha1) -5413 (he4,xia4) -5414 (ye3) -5415 (lu:3) -5416 (a1) -5417 (ma5,ma3,ma2) -5418 (ou3) -5419 (xue1) -541A (yi1) -541B (jun1) -541C (chou3) -541D (lin4) -541E (tun1) -541F (yin2) -5420 (fei4) -5421 (bi3,pi3) -5422 (qin4) -5423 (qin4) -5424 (jie4) -5425 (pou1) -5426 (fou3,pi3) -5427 (ba5,ba1) -5428 (dun1) -5429 (fen1) -542A (e2) -542B (han2) -542C (ting1,yin3) -542D (hang2,keng1) -542E (shun3) -542F (qi3) -5430 (hu1) -5431 (zhi1,zi1) -5432 (yin3) -5433 (wu2) -5434 (wu2) -5435 (chao3,chao1) -5436 (na4) -5437 (chuo4) -5438 (xi1) -5439 (chui1,chui4) -543A (dou1) -543B (wen3) -543C (hou3) -543D (ou2,hong1) -543E (wu2) -543F (gao4,gu4) -5440 (ya1,ya5) -5441 (jun4) -5442 (lu:3) -5443 (e4,e5) -5444 (ge2) -5445 (mei2) -5446 (dai1,ai2) -5447 (qi3) -5448 (cheng2) -5449 (wu2) -544A (gao4,gu4) -544B (fu1) -544C (jiao4) -544D (hong1) -544E (chi3) -544F (sheng1) -5450 (na4,na5,ne4,ne5) -5451 (tun1) -5452 (m2) -5453 (yi4) -5454 (dai1,tai3) -5455 (ou3,ou4) -5456 (li4) -5457 (bei5,bai4) -5458 (yuan2,yun2,yun4) -5459 (guo1) -545A (none0) -545B (qiang1,qiang4) -545C (wu1) -545D (e4) -545E (shi1) -545F (quan3) -5460 (pen3) -5461 (wen3) -5462 (ni2,ne5,na4,ne4) -5463 (mou2) -5464 (ling4) -5465 (ran3) -5466 (you1) -5467 (di3) -5468 (zhou1) -5469 (shi4) -546A (zhou4) -546B (zhan1) -546C (ling2) -546D (yi4) -546E (qi4) -546F (ping2) -5470 (zi3) -5471 (gua1,gu1,wa1,gua3) -5472 (ci1,zi1) -5473 (wei4) -5474 (xu1) -5475 (he1,ke1,a1,a2,a3,a4,a5) -5476 (nao2) -5477 (xia1,xia2) -5478 (pei1) -5479 (yi4) -547A (xiao1) -547B (shen1) -547C (hu1) -547D (ming4) -547E (da2) -547F (qu1) -5480 (ju3,zui3) -5481 (gan1) -5482 (za1) -5483 (tuo1) -5484 (duo1,duo4) -5485 (pou3) -5486 (pao2) -5487 (bie2) -5488 (fu2) -5489 (bi4,fu2) -548A (he2,he4,huo2,huo4,huo5) -548B (za3,ze2,zha1,zha4) -548C (he2,he4,huo2,huo4,huo5,hai1,he5,hu2) -548D (hai1) -548E (jiu4) -548F (yong3) -5490 (fu4,fu5) -5491 (da1) -5492 (zhou4) -5493 (wa3) -5494 (ka3,ka1) -5495 (gu1) -5496 (ka1,ga1) -5497 (zuo3) -5498 (bu4) -5499 (long2) -549A (dong1) -549B (ning2) -549C (zha4) -549D (si1) -549E (xian4) -549F (huo4) -54A0 (qi1) -54A1 (er4) -54A2 (e4) -54A3 (guang1) -54A4 (zha4) -54A5 (xi4,die2) -54A6 (yi2) -54A7 (lie3,lie1,lie5) -54A8 (zi1) -54A9 (mie1) -54AA (mi1) -54AB (zhi3) -54AC (yao3) -54AD (ji1) -54AE (zhou4) -54AF (ge1,ka3,lo5,luo4,ge2) -54B0 (shuai4) -54B1 (zan2,za2,zan5) -54B2 (xiao4) -54B3 (ke2,hai1,ka3,kai4) -54B4 (hui1) -54B5 (kua1) -54B6 (huai4) -54B7 (tao2) -54B8 (xian2) -54B9 (e4) -54BA (xuan3) -54BB (xiu1) -54BC (guo1,kuai1) -54BD (yan1,yan4,ye4) -54BE (lao3) -54BF (yi1) -54C0 (ai1) -54C1 (pin3) -54C2 (shen3) -54C3 (tong2) -54C4 (hong1,hong3,hong4) -54C5 (xiong1,hong1) -54C6 (duo1) -54C7 (wa1,wa5) -54C8 (ha1,ha3,ha4,ka1) -54C9 (zai1) -54CA (you4) -54CB (di4) -54CC (pai4) -54CD (xiang3) -54CE (ai1,ai3,ai4) -54CF (gen2) -54D0 (kuang1) -54D1 (ya1,ya3) -54D2 (da1) -54D3 (xiao1) -54D4 (bi4) -54D5 (hui4,yue3) -54D6 (none0) -54D7 (hua1,hua2,ye4) -54D8 (none0) -54D9 (kuai4) -54DA (duo3) -54DB (none0) -54DC (ji4) -54DD (nong2) -54DE (mou1) -54DF (yo5,yo1) -54E0 (hao4) -54E1 (yuan2,yun2,yun4) -54E2 (long4) -54E3 (pou3) -54E4 (mang2) -54E5 (ge1) -54E6 (e2,o2,o4,wo2,wo4) -54E7 (chi1) -54E8 (shao4) -54E9 (li1,li3,li5) -54EA (na3,nei3,na5,ne2,nai3) -54EB (zu2) -54EC (he1) -54ED (ku1) -54EE (xiao4,xiao1) -54EF (xian4) -54F0 (lao2) -54F1 (bei4) -54F2 (zhe2) -54F3 (zha1) -54F4 (liang4) -54F5 (ba1) -54F6 (mi3) -54F7 (le4) -54F8 (sui1) -54F9 (fou2) -54FA (bu3) -54FB (han4) -54FC (heng1,hng5) -54FD (geng3) -54FE (shuo1) -54FF (ge3) -5500 (you4) -5501 (yan4) -5502 (gu3) -5503 (gu3) -5504 (bai4,bei5) -5505 (han1) -5506 (suo1) -5507 (chun2) -5508 (yi4) -5509 (ai1,ai4) -550A (jia2) -550B (tu3) -550C (xian2) -550D (guan1,guan3) -550E (li4) -550F (xi1) -5510 (tang2) -5511 (zuo4) -5512 (miu1) -5513 (che1) -5514 (wu2,n2,n3,ng2,ng3) -5515 (zao4) -5516 (ya1) -5517 (dou3) -5518 (qi3) -5519 (di2) -551A (qin4) -551B (ma4) -551C (none0) -551D (gong4) -551E (dou3) -551F (none0) -5520 (lao2,lao4) -5521 (liang3) -5522 (suo3) -5523 (zao4) -5524 (huan4) -5525 (none0) -5526 (gou4) -5527 (ji1) -5528 (zuo3) -5529 (wo1) -552A (feng3) -552B (yin2) -552C (hu3,xia4) -552D (qi1) -552E (shou4) -552F (wei2,wei3) -5530 (shua1) -5531 (chang4) -5532 (er2) -5533 (li4) -5534 (qiang4) -5535 (an3) -5536 (jie4) -5537 (yo1) -5538 (nian4) -5539 (yu2) -553A (tian3) -553B (lai2) -553C (sha4) -553D (xi1) -553E (tuo4) -553F (hu1) -5540 (ai2) -5541 (zhou1,zhao1) -5542 (nou4) -5543 (ken3) -5544 (zhuo2) -5545 (zhuo2) -5546 (shang1) -5547 (di1) -5548 (heng4) -5549 (lin2) -554A (a5,a1,a2,a3,a4) -554B (xiao1) -554C (xiang1) -554D (tun1) -554E (wu3) -554F (wen4) -5550 (cui4) -5551 (jie1) -5552 (hu1) -5553 (qi3) -5554 (qi3) -5555 (tao2) -5556 (dan4) -5557 (dan4) -5558 (wan3) -5559 (zi3) -555A (bi3) -555B (cui4) -555C (chuo4,chuai4) -555D (he2) -555E (ya3,ya1) -555F (qi3) -5560 (zhe2) -5561 (fei1) -5562 (liang3) -5563 (xian2) -5564 (pi2) -5565 (sha2) -5566 (la5,la1) -5567 (ze2) -5568 (qing1) -5569 (gua4) -556A (pa1) -556B (zhe3) -556C (se4) -556D (zhuan4) -556E (nie4) -556F (guo1) -5570 (luo1) -5571 (yan1) -5572 (di4) -5573 (quan2) -5574 (tan1,chan3) -5575 (bo5) -5576 (ding4) -5577 (lang1) -5578 (xiao4) -5579 (none0) -557A (tang2) -557B (chi4) -557C (ti2) -557D (an2) -557E (jiu1) -557F (dan4) -5580 (ka1,ke4,ka4) -5581 (yong2) -5582 (wei4) -5583 (nan2) -5584 (shan4) -5585 (yu4) -5586 (zhe2) -5587 (la3,la1,la2) -5588 (jie1) -5589 (hou2) -558A (han3) -558B (die2,zha2) -558C (zhou1) -558D (chai2) -558E (kuai1) -558F (re3,nuo4) -5590 (yu4) -5591 (yin1) -5592 (zan3) -5593 (yao1) -5594 (wo1,o1) -5595 (mian3) -5596 (hu2) -5597 (yun3) -5598 (chuan3) -5599 (hui4) -559A (huan4) -559B (huan4) -559C (xi3) -559D (he1,he4) -559E (ji1) -559F (kui4) -55A0 (zhong3) -55A1 (wei3) -55A2 (sha4) -55A3 (xu3) -55A4 (huang2) -55A5 (du4) -55A6 (nie4) -55A7 (xuan1) -55A8 (liang4) -55A9 (yu4) -55AA (sang1,sang4) -55AB (chi1) -55AC (qiao2) -55AD (yan4) -55AE (dan1,chan2,shan4) -55AF (pen1) -55B0 (shi2,si4) -55B1 (li2) -55B2 (yo5,yo1) -55B3 (zha1,cha1) -55B4 (wei1) -55B5 (miao1) -55B6 (ying2) -55B7 (pen1,pen4,pen5) -55B8 (none0) -55B9 (kui2) -55BA (xi4) -55BB (yu4) -55BC (jie2) -55BD (lou5,lou2) -55BE (ku4) -55BF (cao1) -55C0 (huo4) -55C1 (ti2) -55C2 (yao2) -55C3 (he4) -55C4 (a2,sha4) -55C5 (xiu4) -55C6 (qiang1,qiang4) -55C7 (se4) -55C8 (yong1) -55C9 (su4) -55CA (hong3) -55CB (xie1) -55CC (ai4,yi4) -55CD (suo1) -55CE (ma5,ma3) -55CF (cha1) -55D0 (hai4) -55D1 (ke4,ke1) -55D2 (da1,ta4) -55D3 (sang3) -55D4 (chen1) -55D5 (ru4,nou4) -55D6 (sou1) -55D7 (gong1) -55D8 (ji1) -55D9 (pang3) -55DA (wu1) -55DB (qian4) -55DC (shi4) -55DD (ge2) -55DE (zi1) -55DF (jie1,jue1) -55E0 (luo4) -55E1 (weng1) -55E2 (wa4) -55E3 (si4) -55E4 (chi1) -55E5 (hao2) -55E6 (suo1) -55E7 (jia1,lun2) -55E8 (hai1,hei1) -55E9 (suo3) -55EA (qin2) -55EB (nie4) -55EC (he1) -55ED (none0) -55EE (sai4) -55EF (ng4,ng2,ng3,n2,n3,n4) -55F0 (ge4) -55F1 (na2) -55F2 (dia3) -55F3 (ai4,ai3) -55F4 (none0) -55F5 (tong1) -55F6 (bi4) -55F7 (ao2) -55F8 (ao2) -55F9 (lian2) -55FA (cui1) -55FB (zhe1) -55FC (mo4) -55FD (sou4) -55FE (sou3,zu2) -55FF (tan3) -5600 (di2,di1) -5601 (qi1) -5602 (jiao4) -5603 (chong1) -5604 (jiao1) -5605 (kai3) -5606 (tan4) -5607 (san1) -5608 (cao2) -5609 (jia1) -560A (none0) -560B (xiao1) -560C (piao4) -560D (lou5,lou2) -560E (ga1,ga3,ga2) -560F (gu3,jia3) -5610 (xiao1) -5611 (hu1) -5612 (hui4) -5613 (guo1) -5614 (ou1,ou3,ou4) -5615 (xian1) -5616 (ze2) -5617 (chang2) -5618 (xu1,shi1) -5619 (po2) -561A (de1,dei1) -561B (ma5,ma2) -561C (ma4,ma3,mo4) -561D (hu2) -561E (lei5) -561F (du1) -5620 (ga1) -5621 (tang1) -5622 (ye3) -5623 (beng1) -5624 (ying1) -5625 (none0) -5626 (jiao4) -5627 (mi4) -5628 (xiao4) -5629 (hua1,hua2,ye4) -562A (mai3) -562B (ran2) -562C (zuo1,chuai4,zhuai4) -562D (peng1) -562E (lao2,lao4) -562F (xiao4) -5630 (ji1) -5631 (zhu3) -5632 (chao2,zhao1) -5633 (kui4) -5634 (zui3) -5635 (xiao1) -5636 (si1) -5637 (hao2) -5638 (fu3,m2) -5639 (liao2) -563A (qiao2) -563B (xi1) -563C (xu4) -563D (chan3) -563E (dan4) -563F (hei1,mo4,hai1) -5640 (xun4) -5641 (wu4) -5642 (zun3) -5643 (pan2) -5644 (chi1) -5645 (kui1) -5646 (can3) -5647 (zan3) -5648 (cu4) -5649 (dan4) -564A (yu4) -564B (tun1) -564C (cheng1,ceng1) -564D (jiao4) -564E (ye1) -564F (xi1) -5650 (qi4) -5651 (hao2) -5652 (lian2) -5653 (xu1,shi1) -5654 (deng1) -5655 (hui1) -5656 (yin2) -5657 (pu1) -5658 (jue1) -5659 (qin2) -565A (xun2) -565B (nie4) -565C (lu1) -565D (si1) -565E (yan4) -565F (ying4) -5660 (da1) -5661 (zhan1) -5662 (o1) -5663 (zhou4) -5664 (jin4) -5665 (nong2) -5666 (hui4,yue1) -5667 (hui4) -5668 (qi4) -5669 (e4) -566A (zao4) -566B (yi1,yi4) -566C (shi4) -566D (jiao4) -566E (yuan4) -566F (ai4,ai3) -5670 (yong1) -5671 (xue2,jue2) -5672 (kuai4) -5673 (yu3) -5674 (pen1,pen4) -5675 (dao4) -5676 (ga2) -5677 (xin1) -5678 (dun1,dun4) -5679 (dang1) -567A (none0) -567B (sai1) -567C (pi1) -567D (pi3) -567E (yin1) -567F (zui3) -5680 (ning2) -5681 (di2) -5682 (han3) -5683 (ta4) -5684 (huo4,huo1,o3) -5685 (ru2) -5686 (hao1) -5687 (xia4,he4) -5688 (yan4) -5689 (duo1) -568A (pi4) -568B (chou2) -568C (ji4) -568D (jin4) -568E (hao2) -568F (ti4) -5690 (chang2) -5691 (none0) -5692 (none0) -5693 (ca1,cha1) -5694 (ti4) -5695 (lu1) -5696 (hui4) -5697 (bao4) -5698 (you1) -5699 (nie4) -569A (yin2) -569B (hu4) -569C (mo4) -569D (huang1) -569E (zhe2) -569F (li2) -56A0 (liu2) -56A1 (none0) -56A2 (nang2,nang1) -56A3 (xiao1,ao2) -56A4 (mo2) -56A5 (yan4) -56A6 (li4) -56A7 (lu2) -56A8 (long2) -56A9 (mo2) -56AA (dan1) -56AB (chen4) -56AC (pin2) -56AD (pi3) -56AE (xiang4) -56AF (huo4) -56B0 (mo2) -56B1 (xi1) -56B2 (duo3) -56B3 (ku4) -56B4 (yan2) -56B5 (chan2) -56B6 (ying1) -56B7 (rang3,rang1) -56B8 (dian3) -56B9 (la1) -56BA (ta4) -56BB (xiao1) -56BC (jiao2,jiao4,jue2) -56BD (chuo4) -56BE (huan4) -56BF (huo4) -56C0 (zhuan4,zhuan3) -56C1 (nie4) -56C2 (xiao1,ao2) -56C3 (ca4) -56C4 (li2) -56C5 (chan3) -56C6 (chai4) -56C7 (li4) -56C8 (yi4) -56C9 (luo1,luo2) -56CA (nang2,nang1) -56CB (zan4) -56CC (su1) -56CD (xi3) -56CE (none0) -56CF (jian1) -56D0 (za2) -56D1 (zhu3) -56D2 (lan2) -56D3 (nie4) -56D4 (nang1) -56D5 (none0) -56D6 (none0) -56D7 (wei2) -56D8 (hui2) -56D9 (yin1) -56DA (qiu2) -56DB (si4) -56DC (nin2) -56DD (jian3,nan1) -56DE (hui2) -56DF (xin4) -56E0 (yin1) -56E1 (nan1) -56E2 (tuan2) -56E3 (tuan2) -56E4 (dun4,tun2) -56E5 (kang4) -56E6 (yuan1) -56E7 (jiong3) -56E8 (pian1) -56E9 (yun4) -56EA (cong1,chuang1) -56EB (hu2) -56EC (hui2) -56ED (yuan2) -56EE (e2) -56EF (guo2) -56F0 (kun4) -56F1 (cong1) -56F2 (wei2) -56F3 (tu2) -56F4 (wei2) -56F5 (lun2) -56F6 (guo2) -56F7 (jun1) -56F8 (ri4) -56F9 (ling2) -56FA (gu4) -56FB (guo2) -56FC (tai1) -56FD (guo2) -56FE (tu2) -56FF (you4) -5700 (guo2) -5701 (yin2) -5702 (hun4) -5703 (pu3) -5704 (yu3) -5705 (han2) -5706 (yuan2) -5707 (lun2) -5708 (quan1,juan1,juan4,quan3) -5709 (yu3) -570A (qing1) -570B (guo2) -570C (chui2) -570D (wei2) -570E (yuan2) -570F (quan1,juan1,juan4) -5710 (ku1) -5711 (pu3) -5712 (yuan2) -5713 (yuan2) -5714 (e4) -5715 (tu2,shu1,guan3) -5716 (tu2) -5717 (tu2) -5718 (tuan2) -5719 (lu:e4) -571A (hui4) -571B (yi4) -571C (yuan2,huan2) -571D (luan2) -571E (luan2) -571F (tu3) -5720 (ya4) -5721 (tu3) -5722 (ting3) -5723 (sheng4) -5724 (yan2) -5725 (lu2) -5726 (none0) -5727 (ya1,ya4) -5728 (zai4) -5729 (wei2,xu1) -572A (ge1) -572B (yu4) -572C (wu1) -572D (gui1) -572E (pi3) -572F (yi2) -5730 (di4,de5) -5731 (qian1) -5732 (qian1) -5733 (zhen4) -5734 (zhuo2,shao2) -5735 (dang4) -5736 (qia4) -5737 (none0) -5738 (none0) -5739 (kuang4) -573A (chang3,chang2,chang5) -573B (qi2,yin2) -573C (nie4) -573D (mo4) -573E (ji1) -573F (jia2) -5740 (zhi3) -5741 (zhi3) -5742 (ban3) -5743 (xun1) -5744 (tou2) -5745 (qin3) -5746 (fen2) -5747 (jun1,yun4) -5748 (keng1) -5749 (dun4) -574A (fang1,fang2) -574B (fen4) -574C (ben4) -574D (tan1) -574E (kan3) -574F (huai4,pi1,pei1) -5750 (zuo4) -5751 (keng1) -5752 (bi4) -5753 (xing2) -5754 (di4) -5755 (jing1) -5756 (ji4) -5757 (kuai4) -5758 (di3) -5759 (jing1) -575A (jian1) -575B (tan2) -575C (li4) -575D (ba4) -575E (wu4) -575F (fen2) -5760 (zhui4) -5761 (po1) -5762 (pan3) -5763 (tang1) -5764 (kun1) -5765 (qu1) -5766 (tan3) -5767 (zhi2) -5768 (tuo2) -5769 (gan1) -576A (ping2) -576B (dian4) -576C (wa1) -576D (ni2) -576E (tai2,tai1) -576F (pi1) -5770 (jiong1) -5771 (yang1) -5772 (fo2) -5773 (ao4) -5774 (liu4) -5775 (qiu1) -5776 (mu4) -5777 (ke3,ke1) -5778 (gou4) -5779 (xue4) -577A (ba2) -577B (chi2,di3) -577C (che4) -577D (ling2) -577E (zhu4) -577F (fu4) -5780 (hu1) -5781 (zhi4) -5782 (chui2) -5783 (la1) -5784 (long3) -5785 (long3) -5786 (lu2) -5787 (ao4) -5788 (none0) -5789 (pao2) -578A (none0) -578B (xing2) -578C (tong2,dong4) -578D (ji4) -578E (ke4) -578F (lu4) -5790 (ci2) -5791 (chi3) -5792 (lei3) -5793 (gai1) -5794 (yin1) -5795 (hou4) -5796 (dui1) -5797 (zhao4) -5798 (fu2) -5799 (guang1) -579A (yao2) -579B (duo3,duo4) -579C (duo3,duo4) -579D (gui3) -579E (cha2) -579F (yang2) -57A0 (yin2) -57A1 (fa2) -57A2 (gou4) -57A3 (yuan2) -57A4 (die2) -57A5 (xie2) -57A6 (ken3) -57A7 (shang3) -57A8 (shou3) -57A9 (e4) -57AA (none0) -57AB (dian4) -57AC (hong2) -57AD (ya1,ya4) -57AE (kua3) -57AF (da5) -57B0 (none0) -57B1 (dang4) -57B2 (kai3) -57B3 (none0) -57B4 (nao3) -57B5 (an1) -57B6 (xing1) -57B7 (xian4) -57B8 (huan4,huan2,yuan4) -57B9 (bang1) -57BA (pei1) -57BB (ba4) -57BC (yi4) -57BD (yin4) -57BE (han4) -57BF (xu4) -57C0 (chui2) -57C1 (cen2) -57C2 (geng3) -57C3 (ai1) -57C4 (peng2) -57C5 (fang2) -57C6 (que4) -57C7 (yong3) -57C8 (jun4) -57C9 (jia2) -57CA (di4) -57CB (mai2,man2) -57CC (lang4) -57CD (xuan4) -57CE (cheng2) -57CF (shan1) -57D0 (jin1) -57D1 (zhe2) -57D2 (lie4,le4) -57D3 (lie4) -57D4 (pu3,bu4,bu3) -57D5 (cheng2) -57D6 (none0) -57D7 (bu4) -57D8 (shi2) -57D9 (xun1) -57DA (guo1) -57DB (jiong1) -57DC (ye3) -57DD (nian4) -57DE (di1) -57DF (yu4) -57E0 (bu4) -57E1 (wu4,ya1) -57E2 (juan3) -57E3 (sui4) -57E4 (pi2,bei1,bi4) -57E5 (cheng1) -57E6 (wan3) -57E7 (ju4) -57E8 (lun3) -57E9 (zheng1) -57EA (kong1) -57EB (zhong3) -57EC (dong1) -57ED (dai4) -57EE (tan4) -57EF (an3) -57F0 (cai4) -57F1 (shu2) -57F2 (beng3) -57F3 (kan3) -57F4 (zhi2) -57F5 (duo3) -57F6 (yi4) -57F7 (zhi2) -57F8 (yi4) -57F9 (pei2) -57FA (ji1) -57FB (zhun3) -57FC (qi2) -57FD (sao4) -57FE (ju4) -57FF (ni2,ni4) -5800 (ku1,jue2) -5801 (ke3) -5802 (tang2) -5803 (kun1) -5804 (ni4) -5805 (jian1) -5806 (dui1,zui1) -5807 (jin3) -5808 (gang1) -5809 (yu4) -580A (e4,wu4) -580B (peng2,beng4,peng4) -580C (gu4) -580D (tu4) -580E (leng4,ling2) -580F (none0) -5810 (ya2) -5811 (qian4) -5812 (none0) -5813 (an4) -5814 (chen1) -5815 (duo4,hui1) -5816 (nao3) -5817 (tu1) -5818 (cheng2) -5819 (yin1) -581A (hun2) -581B (bi4) -581C (lian4) -581D (guo1) -581E (die2) -581F (zhuan4) -5820 (hou4) -5821 (bao3,bu3,pu4,pu3) -5822 (bao3) -5823 (yu2) -5824 (di1,ti2) -5825 (mao2) -5826 (jie1) -5827 (ruan2) -5828 (e4,ai4) -5829 (geng4) -582A (kan1) -582B (zong1) -582C (yu2) -582D (huang2) -582E (e4) -582F (yao2) -5830 (yan4) -5831 (bao4) -5832 (ji2) -5833 (mei2) -5834 (chang2,chang3) -5835 (du3) -5836 (tuo1) -5837 (an3) -5838 (feng2) -5839 (zhong4) -583A (jie4) -583B (zhen1) -583C (heng4) -583D (gang1) -583E (chuan3) -583F (jian3) -5840 (none0) -5841 (lei3) -5842 (gang3) -5843 (huang1) -5844 (leng2) -5845 (duan4) -5846 (wan1) -5847 (xuan1) -5848 (ji4) -5849 (ji2) -584A (kuai4) -584B (ying2) -584C (ta1) -584D (cheng2) -584E (yong3) -584F (kai3) -5850 (su4) -5851 (su4) -5852 (shi2) -5853 (mi4) -5854 (ta3,da5) -5855 (weng3) -5856 (cheng2) -5857 (tu2) -5858 (tang2) -5859 (qiao1) -585A (zhong3) -585B (li4) -585C (peng2) -585D (bang4) -585E (sai4,se4,sai1) -585F (zang4) -5860 (dui1) -5861 (tian2) -5862 (wu4) -5863 (cheng3) -5864 (xun1,xuan1) -5865 (ge2) -5866 (zhen4) -5867 (ai4) -5868 (gong1) -5869 (yan2) -586A (kan3) -586B (tian2) -586C (yuan2) -586D (wen1) -586E (xie4) -586F (liu4) -5870 (none0) -5871 (lang3) -5872 (chang2,chang3) -5873 (peng2) -5874 (beng1) -5875 (chen2) -5876 (lu4) -5877 (lu3) -5878 (ou1) -5879 (qian4) -587A (mei2) -587B (mo4) -587C (zhuan1) -587D (shuang3) -587E (shu2) -587F (lou3) -5880 (chi2) -5881 (man4) -5882 (biao1) -5883 (jing4) -5884 (ce4) -5885 (shu4) -5886 (di4) -5887 (zhang4) -5888 (kan4) -5889 (yong1) -588A (dian4) -588B (chen3) -588C (zhi2) -588D (ji4) -588E (guo1) -588F (qiang3) -5890 (jin3) -5891 (di1) -5892 (shang1) -5893 (mu4) -5894 (cui1) -5895 (yan4) -5896 (ta3,da5) -5897 (zeng1) -5898 (qi2) -5899 (qiang2) -589A (liang2) -589B (none0) -589C (zhui4) -589D (qiao1) -589E (zeng1) -589F (xu1) -58A0 (shan4) -58A1 (shan4) -58A2 (ba2) -58A3 (pu2) -58A4 (kuai4) -58A5 (dong3) -58A6 (fan2) -58A7 (que4) -58A8 (mo4) -58A9 (dun1) -58AA (dun1) -58AB (zun1,zun3) -58AC (zui4) -58AD (sheng4) -58AE (duo4,hui1) -58AF (duo4) -58B0 (tan2) -58B1 (deng4,yan4) -58B2 (mu2) -58B3 (fen2) -58B4 (huang2) -58B5 (tan2) -58B6 (da5) -58B7 (ye4) -58B8 (chu2) -58B9 (none0) -58BA (ao4) -58BB (qiang2) -58BC (ji1) -58BD (qiao1) -58BE (ken3) -58BF (yi4) -58C0 (pi2) -58C1 (bi4) -58C2 (dian4) -58C3 (jiang1) -58C4 (ye3) -58C5 (yong1,yong3) -58C6 (xue2) -58C7 (tan2) -58C8 (lan3) -58C9 (ju4) -58CA (huai4,pi1) -58CB (dang4) -58CC (rang3) -58CD (qian4) -58CE (xuan1) -58CF (lan4) -58D0 (mi2) -58D1 (he4,huo4) -58D2 (kai4) -58D3 (ya1,ya4) -58D4 (dao3) -58D5 (hao2) -58D6 (ruan2) -58D7 (none0) -58D8 (lei3) -58D9 (kuang4) -58DA (lu2) -58DB (yan2) -58DC (tan2) -58DD (wei3) -58DE (huai4,pi1) -58DF (long3) -58E0 (long3) -58E1 (rui4) -58E2 (li4) -58E3 (lin2) -58E4 (rang3) -58E5 (chan2) -58E6 (xun1) -58E7 (yan2) -58E8 (lei2) -58E9 (ba4) -58EA (none0) -58EB (shi4) -58EC (ren2) -58ED (none0) -58EE (zhuang4) -58EF (zhuang4) -58F0 (sheng1) -58F1 (yi1) -58F2 (mai4) -58F3 (qiao4,ke2) -58F4 (zhu3) -58F5 (zhuang4) -58F6 (hu2) -58F7 (hu2) -58F8 (kun3) -58F9 (yi1) -58FA (hu2) -58FB (xu4) -58FC (kun3) -58FD (shou4) -58FE (mang3) -58FF (zun1) -5900 (shou4) -5901 (yi1) -5902 (zhi3) -5903 (gu1) -5904 (chu3,chu4) -5905 (xiang2) -5906 (feng2) -5907 (bei4) -5908 (none0) -5909 (bian4) -590A (sui1) -590B (qun1) -590C (ling2) -590D (fu4) -590E (zuo4) -590F (xia4,jia3) -5910 (xiong4) -5911 (none0) -5912 (nao2) -5913 (xia4) -5914 (kui2) -5915 (xi1,xi4) -5916 (wai4) -5917 (yuan4) -5918 (mao3) -5919 (su4) -591A (duo1) -591B (duo1) -591C (ye4) -591D (qing2) -591E (none0) -591F (gou4) -5920 (gou4) -5921 (qi4) -5922 (meng4) -5923 (meng4) -5924 (yin2) -5925 (huo3) -5926 (chen4) -5927 (da4,dai4) -5928 (ze4) -5929 (tian1) -592A (tai4) -592B (fu1,fu2) -592C (guai4) -592D (yao1,yao3) -592E (yang1) -592F (hang1,ben4) -5930 (gao3) -5931 (shi1) -5932 (ben3,tao1) -5933 (tai4) -5934 (tou2) -5935 (yan3) -5936 (bi3) -5937 (yi2) -5938 (kua1) -5939 (jia1,jia2,ga1) -593A (duo2) -593B (none0) -593C (kuang3) -593D (yun4) -593E (jia1,jia2) -593F (ba1) -5940 (en1,mang2) -5941 (lian2) -5942 (huan4) -5943 (di1) -5944 (yan3,yan1) -5945 (pao4) -5946 (juan4) -5947 (qi2,ji1) -5948 (nai4,nai3) -5949 (feng4) -594A (xie2) -594B (fen4) -594C (dian3) -594D (none0) -594E (kui2) -594F (zou4) -5950 (huan4) -5951 (qi4,xie4,qie4) -5952 (kai1) -5953 (she1) -5954 (ben1,ben4) -5955 (yi4) -5956 (jiang3) -5957 (tao4) -5958 (zhuang3,zang4) -5959 (ben3) -595A (xi1) -595B (huang3) -595C (fei3) -595D (diao1) -595E (sui1) -595F (beng1) -5960 (dian4) -5961 (ao4) -5962 (she1) -5963 (weng1) -5964 (pan3) -5965 (ao4) -5966 (wu4) -5967 (ao4) -5968 (jiang3) -5969 (lian2) -596A (duo2) -596B (yun1) -596C (jiang3) -596D (shi4) -596E (fen4) -596F (huo4) -5970 (bei4) -5971 (lian2) -5972 (che3) -5973 (nu:3,ru3) -5974 (nu2) -5975 (ding3) -5976 (nai3) -5977 (qian1) -5978 (jian1) -5979 (ta1) -597A (jiu3) -597B (nan2) -597C (cha4) -597D (hao3,hao4) -597E (xian1) -597F (fan4) -5980 (ji3) -5981 (shuo4) -5982 (ru2) -5983 (fei1) -5984 (wang4) -5985 (hong2) -5986 (zhuang1) -5987 (fu4) -5988 (ma1) -5989 (dan1) -598A (ren4) -598B (fu1) -598C (jing4) -598D (yan2) -598E (xie4) -598F (wen4) -5990 (zhong1) -5991 (pa1) -5992 (du4) -5993 (ji4) -5994 (keng1) -5995 (zhong4) -5996 (yao1) -5997 (jin4) -5998 (yun2) -5999 (miao4) -599A (pei1) -599B (chi1) -599C (yue4) -599D (zhuang1) -599E (niu1) -599F (yan4) -59A0 (na4) -59A1 (xin1,xin4) -59A2 (fen2) -59A3 (bi3) -59A4 (yu2) -59A5 (tuo3) -59A6 (feng1) -59A7 (yuan2) -59A8 (fang2,fang1) -59A9 (wu3) -59AA (yu4) -59AB (gui1) -59AC (du4) -59AD (ba2) -59AE (ni1) -59AF (zhou2) -59B0 (zhou2) -59B1 (zhao1) -59B2 (da2) -59B3 (nai3,ni3) -59B4 (yuan3) -59B5 (tou3) -59B6 (xuan2) -59B7 (zhi2) -59B8 (e1) -59B9 (mei4) -59BA (mo4) -59BB (qi1,qi4) -59BC (bi4) -59BD (shen1) -59BE (qie4) -59BF (e1) -59C0 (he2) -59C1 (xu3) -59C2 (fa2) -59C3 (zheng1) -59C4 (ni1) -59C5 (ban4) -59C6 (mu3) -59C7 (fu1,fu4) -59C8 (ling2) -59C9 (zi3) -59CA (zi3) -59CB (shi3) -59CC (ran3) -59CD (shan1) -59CE (yang1) -59CF (qian2) -59D0 (jie3) -59D1 (gu1) -59D2 (si4) -59D3 (xing4) -59D4 (wei3,wei1) -59D5 (zi1) -59D6 (ju4) -59D7 (shan1) -59D8 (pin1) -59D9 (ren4) -59DA (yao2) -59DB (tong3) -59DC (jiang1) -59DD (shu1) -59DE (ji2) -59DF (gai1) -59E0 (shang4) -59E1 (kuo4) -59E2 (juan1) -59E3 (jiao1) -59E4 (gou4) -59E5 (lao3,mu3) -59E6 (jian1) -59E7 (jian1) -59E8 (yi2) -59E9 (nian2) -59EA (zhi2) -59EB (ji1) -59EC (ji1) -59ED (xian4) -59EE (heng2) -59EF (guang1) -59F0 (jun1) -59F1 (kua1) -59F2 (yan4) -59F3 (ming3) -59F4 (lie4) -59F5 (pei4) -59F6 (yan3) -59F7 (you4) -59F8 (yan2) -59F9 (cha4) -59FA (xian3) -59FB (yin1) -59FC (chi3) -59FD (gui3) -59FE (quan2) -59FF (zi1) -5A00 (song1) -5A01 (wei1) -5A02 (hong2) -5A03 (wa2) -5A04 (lou2) -5A05 (ya4) -5A06 (rao3,rao2) -5A07 (jiao1) -5A08 (luan2) -5A09 (ping1) -5A0A (xian4) -5A0B (shao4) -5A0C (li3) -5A0D (cheng2) -5A0E (xie4) -5A0F (mang2) -5A10 (none0) -5A11 (suo1) -5A12 (mu3) -5A13 (wei3) -5A14 (ke4) -5A15 (lai4) -5A16 (chuo4) -5A17 (ding4) -5A18 (niang2) -5A19 (keng1) -5A1A (nan2) -5A1B (yu2) -5A1C (na4,nuo2) -5A1D (pei1) -5A1E (sui1) -5A1F (juan1) -5A20 (shen1,chen2,zhen4) -5A21 (zhi4) -5A22 (han2) -5A23 (di4) -5A24 (zhuang1) -5A25 (e2) -5A26 (pin2) -5A27 (tui4) -5A28 (xian4) -5A29 (mian3,wan3) -5A2A (wu2) -5A2B (yan2) -5A2C (wu3) -5A2D (xi1) -5A2E (yan2) -5A2F (yu2) -5A30 (si4) -5A31 (yu2) -5A32 (wa1) -5A33 (li4) -5A34 (xian2) -5A35 (ju1) -5A36 (qu3,qu4) -5A37 (chui2) -5A38 (qi1) -5A39 (xian2) -5A3A (zhui1) -5A3B (dong1) -5A3C (chang1) -5A3D (lu4) -5A3E (ai2) -5A3F (e1) -5A40 (e1) -5A41 (lou2,lu:3) -5A42 (mian2) -5A43 (cong2) -5A44 (pou3) -5A45 (ju2) -5A46 (po2) -5A47 (cai3) -5A48 (ling2) -5A49 (wan3) -5A4A (biao3) -5A4B (xiao1) -5A4C (shu3) -5A4D (qi3) -5A4E (hui1) -5A4F (fu4) -5A50 (wo3) -5A51 (rui2) -5A52 (tan2) -5A53 (fei1) -5A54 (none0) -5A55 (jie2) -5A56 (tian1) -5A57 (ni2) -5A58 (quan2) -5A59 (jing4) -5A5A (hun1) -5A5B (jing1) -5A5C (qian1) -5A5D (dian4) -5A5E (xing4) -5A5F (hu4) -5A60 (wan2) -5A61 (lai2) -5A62 (bi4) -5A63 (yin1) -5A64 (chou1,zhou1) -5A65 (chuo4) -5A66 (fu4) -5A67 (jing4) -5A68 (lun2) -5A69 (yan4,an4) -5A6A (lan2) -5A6B (kun1) -5A6C (yin2) -5A6D (ya4) -5A6E (none0) -5A6F (li4) -5A70 (dian3) -5A71 (xian2) -5A72 (none0) -5A73 (hua4) -5A74 (ying1) -5A75 (chan2) -5A76 (shen3) -5A77 (ting2) -5A78 (yang2) -5A79 (yao3) -5A7A (wu4) -5A7B (nan4) -5A7C (chuo4) -5A7D (jia3) -5A7E (tou1) -5A7F (xu4) -5A80 (yu2) -5A81 (wei1) -5A82 (ti2) -5A83 (rou2) -5A84 (mei3) -5A85 (dan1) -5A86 (ruan3) -5A87 (qin1) -5A88 (none0) -5A89 (wu1) -5A8A (qian2) -5A8B (chun1) -5A8C (mao2) -5A8D (fu4) -5A8E (jie3) -5A8F (duan1) -5A90 (xi1) -5A91 (zhong4) -5A92 (mei2) -5A93 (huang2) -5A94 (mian2) -5A95 (an1) -5A96 (ying2) -5A97 (xuan1) -5A98 (none0) -5A99 (wei1) -5A9A (mei4) -5A9B (yuan2,yuan4) -5A9C (zhen1) -5A9D (qiu1) -5A9E (ti2,shi4) -5A9F (xie4) -5AA0 (tuo3) -5AA1 (lian4) -5AA2 (mao4) -5AA3 (ran3) -5AA4 (si1) -5AA5 (pian1) -5AA6 (wei4) -5AA7 (wa1) -5AA8 (jiu4) -5AA9 (hu2) -5AAA (ao3) -5AAB (none0) -5AAC (bao3) -5AAD (xu1) -5AAE (tou1,tou3) -5AAF (gui1) -5AB0 (zou1) -5AB1 (yao2) -5AB2 (pi4) -5AB3 (xi2) -5AB4 (yuan2) -5AB5 (ying4) -5AB6 (rong2) -5AB7 (ru4) -5AB8 (chi1) -5AB9 (liu2) -5ABA (mei3) -5ABB (pan2) -5ABC (ao3) -5ABD (ma1) -5ABE (gou4) -5ABF (kui4) -5AC0 (qin2) -5AC1 (jia4) -5AC2 (sao3) -5AC3 (zhen1) -5AC4 (yuan2) -5AC5 (cha1) -5AC6 (yong2) -5AC7 (ming2) -5AC8 (ying1) -5AC9 (ji2) -5ACA (su4) -5ACB (niao3) -5ACC (xian2) -5ACD (tao1,yao3) -5ACE (pang2) -5ACF (lang2) -5AD0 (niao3) -5AD1 (bao2) -5AD2 (ai4) -5AD3 (pi4) -5AD4 (pin2) -5AD5 (yi4) -5AD6 (piao2) -5AD7 (yu4) -5AD8 (lei2) -5AD9 (xuan2) -5ADA (man4,man1) -5ADB (yi1) -5ADC (zhang1) -5ADD (kang1) -5ADE (yong2) -5ADF (ni4) -5AE0 (li2) -5AE1 (di2) -5AE2 (gui1) -5AE3 (yan1) -5AE4 (jin4) -5AE5 (zhuan1) -5AE6 (chang2) -5AE7 (ce4) -5AE8 (han1,ran3) -5AE9 (nen4) -5AEA (lao4) -5AEB (mo2) -5AEC (zhe1) -5AED (hu4) -5AEE (hu4) -5AEF (ao4) -5AF0 (nen4) -5AF1 (qiang2) -5AF2 (none0) -5AF3 (bi4) -5AF4 (gu1) -5AF5 (wu3) -5AF6 (qiao2) -5AF7 (tuo3) -5AF8 (zhan3) -5AF9 (mao2) -5AFA (xian2) -5AFB (xian2) -5AFC (mo4) -5AFD (liao2) -5AFE (lian2) -5AFF (hua4) -5B00 (gui1) -5B01 (deng1) -5B02 (zhi2) -5B03 (xu1) -5B04 (none0) -5B05 (hua4) -5B06 (xi1) -5B07 (hui4) -5B08 (rao3,rao2) -5B09 (xi1) -5B0A (yan4) -5B0B (chan2) -5B0C (jiao1) -5B0D (mei3) -5B0E (fan4) -5B0F (fan1) -5B10 (xian1) -5B11 (yi4) -5B12 (wei4) -5B13 (chan1) -5B14 (fan4) -5B15 (shi4) -5B16 (bi4) -5B17 (shan4) -5B18 (sui4) -5B19 (qiang2) -5B1A (lian2) -5B1B (huan2,qiong2,xuan1) -5B1C (none0) -5B1D (niao3) -5B1E (dong3) -5B1F (yi4) -5B20 (can2) -5B21 (ai4) -5B22 (niang2) -5B23 (ning2) -5B24 (ma1) -5B25 (tiao3) -5B26 (chou2) -5B27 (jin4) -5B28 (ci2) -5B29 (yu2) -5B2A (pin2) -5B2B (none0) -5B2C (xu1) -5B2D (nai3) -5B2E (yan1) -5B2F (tai2) -5B30 (ying1) -5B31 (can2) -5B32 (niao3) -5B33 (none0) -5B34 (ying2) -5B35 (mian2) -5B36 (none0) -5B37 (ma1) -5B38 (shen3) -5B39 (xing4) -5B3A (ni4) -5B3B (du2) -5B3C (liu2) -5B3D (yuan1) -5B3E (lan3) -5B3F (yan4) -5B40 (shuang1) -5B41 (ling2) -5B42 (jiao3) -5B43 (niang2) -5B44 (lan3) -5B45 (xian1) -5B46 (ying1) -5B47 (shuang1) -5B48 (shuai1) -5B49 (quan2) -5B4A (mi3) -5B4B (li2) -5B4C (luan2) -5B4D (yan2) -5B4E (zhu3) -5B4F (lan3) -5B50 (zi5,zi3,zi2) -5B51 (jie2) -5B52 (jue2) -5B53 (jue2) -5B54 (kong3) -5B55 (yun4) -5B56 (zi1) -5B57 (zi4) -5B58 (cun2) -5B59 (sun1) -5B5A (fu2) -5B5B (bei4,bo2) -5B5C (zi1) -5B5D (xiao4) -5B5E (xin4) -5B5F (meng4) -5B60 (si4) -5B61 (tai1) -5B62 (bao1) -5B63 (ji4) -5B64 (gu1) -5B65 (nu2) -5B66 (xue2) -5B67 (none0) -5B68 (chan2) -5B69 (hai2) -5B6A (luan2) -5B6B (sun1) -5B6C (nao1) -5B6D (mie1) -5B6E (cong2) -5B6F (jian1) -5B70 (shu2) -5B71 (chan2,can4) -5B72 (ya1) -5B73 (zi1,zi4) -5B74 (ni3) -5B75 (fu1) -5B76 (zi1) -5B77 (li2) -5B78 (xue2,xiao2) -5B79 (bo4) -5B7A (ru2,ru4) -5B7B (nai2) -5B7C (nie4) -5B7D (nie4) -5B7E (ying1) -5B7F (luan2) -5B80 (mian2) -5B81 (ning2,ning4) -5B82 (rong3) -5B83 (ta1) -5B84 (gui3) -5B85 (zhai2,zhe4) -5B86 (qiong2) -5B87 (yu3) -5B88 (shou3) -5B89 (an1) -5B8A (tu1) -5B8B (song4) -5B8C (wan2) -5B8D (rou4) -5B8E (yao3) -5B8F (hong2) -5B90 (yi2) -5B91 (jing3) -5B92 (zhun1) -5B93 (mi4) -5B94 (guai1) -5B95 (dang4) -5B96 (hong2) -5B97 (zong1) -5B98 (guan1) -5B99 (zhou4) -5B9A (ding4) -5B9B (wan3,yuan1) -5B9C (yi2) -5B9D (bao3) -5B9E (shi2) -5B9F (shi2) -5BA0 (chong3) -5BA1 (shen3) -5BA2 (ke4) -5BA3 (xuan1) -5BA4 (shi4) -5BA5 (you4) -5BA6 (huan4) -5BA7 (yi2) -5BA8 (tiao3) -5BA9 (shi3) -5BAA (xian4) -5BAB (gong1) -5BAC (cheng2) -5BAD (qun2) -5BAE (gong1) -5BAF (xiao1) -5BB0 (zai3) -5BB1 (zha1) -5BB2 (bao3) -5BB3 (hai4) -5BB4 (yan4) -5BB5 (xiao1) -5BB6 (jia1,gu1,jie5,jia5) -5BB7 (shen3) -5BB8 (chen2) -5BB9 (rong2) -5BBA (huang3) -5BBB (mi4) -5BBC (kou4) -5BBD (kuan1) -5BBE (bin1) -5BBF (su4,xiu3,xiu4) -5BC0 (cai3,shen3) -5BC1 (zan3) -5BC2 (ji4,ji2) -5BC3 (yuan1) -5BC4 (ji4) -5BC5 (yin2) -5BC6 (mi4) -5BC7 (kou4) -5BC8 (qing1) -5BC9 (he4) -5BCA (zhen1) -5BCB (jian3) -5BCC (fu4) -5BCD (ning2) -5BCE (bing4) -5BCF (huan2) -5BD0 (mei4) -5BD1 (qin3) -5BD2 (han2) -5BD3 (yu4) -5BD4 (shi2) -5BD5 (ning2) -5BD6 (jin4) -5BD7 (ning2) -5BD8 (zhi4) -5BD9 (yu3) -5BDA (bao3) -5BDB (kuan1) -5BDC (ning2) -5BDD (qin3) -5BDE (mo4) -5BDF (cha2) -5BE0 (ju4) -5BE1 (gua3) -5BE2 (qin3) -5BE3 (hu1) -5BE4 (wu4) -5BE5 (liao2) -5BE6 (shi2) -5BE7 (ning2,ning4) -5BE8 (zhai4) -5BE9 (shen3) -5BEA (wei3) -5BEB (xie3) -5BEC (kuan1) -5BED (hui4) -5BEE (liao2) -5BEF (jun4) -5BF0 (huan2) -5BF1 (yi4) -5BF2 (yi2) -5BF3 (bao3) -5BF4 (qin1) -5BF5 (chong3) -5BF6 (bao3) -5BF7 (feng1) -5BF8 (cun4) -5BF9 (dui4) -5BFA (si4) -5BFB (xun2,xin2) -5BFC (dao3) -5BFD (lu:3,luo1) -5BFE (dui4) -5BFF (shou4) -5C00 (po3) -5C01 (feng1) -5C02 (zhuan1) -5C03 (fu1) -5C04 (she4,shi2,ye4) -5C05 (ke4) -5C06 (jiang1,jiang4,qiang1) -5C07 (jiang1,jiang4,qiang1) -5C08 (zhuan1) -5C09 (wei4,yu4) -5C0A (zun1) -5C0B (xun2,xin2) -5C0C (shu4) -5C0D (dui4) -5C0E (dao3,dao4) -5C0F (xiao3) -5C10 (ji1) -5C11 (shao3,shao4) -5C12 (er3) -5C13 (er3) -5C14 (er3) -5C15 (ga3) -5C16 (jian1) -5C17 (shu1) -5C18 (chen2) -5C19 (shang4) -5C1A (shang4) -5C1B (yuan2) -5C1C (ga2) -5C1D (chang2) -5C1E (liao2,liao3) -5C1F (xian3) -5C20 (xian1) -5C21 (none0) -5C22 (wang1,you2) -5C23 (wang1,you2) -5C24 (you2) -5C25 (liao4) -5C26 (liao4) -5C27 (yao2) -5C28 (mang2,pang2) -5C29 (wang1) -5C2A (wang1) -5C2B (wang1) -5C2C (ga4) -5C2D (yao2) -5C2E (duo4) -5C2F (kui4) -5C30 (zhong3,zhong4) -5C31 (jiu4) -5C32 (gan1) -5C33 (gu3) -5C34 (gan1) -5C35 (gan1) -5C36 (gan1) -5C37 (gan1) -5C38 (shi1) -5C39 (yin3) -5C3A (chi3,che3) -5C3B (kao1) -5C3C (ni2) -5C3D (jin3,jin4) -5C3E (wei3,yi3) -5C3F (niao4,sui1,ni4) -5C40 (ju2) -5C41 (pi4) -5C42 (ceng2) -5C43 (xi4) -5C44 (bi1,bi3) -5C45 (ju1,ji1) -5C46 (jie4) -5C47 (tian1) -5C48 (qu1) -5C49 (ti4) -5C4A (jie4) -5C4B (wu1) -5C4C (diao3) -5C4D (shi1) -5C4E (shi3) -5C4F (ping2,bing3) -5C50 (ji1) -5C51 (xie4) -5C52 (chen2) -5C53 (xi4) -5C54 (ni2) -5C55 (zhan3) -5C56 (xi1) -5C57 (none0) -5C58 (man3) -5C59 (e1) -5C5A (lou4) -5C5B (ping2) -5C5C (ti4) -5C5D (fei4) -5C5E (shu3,zhu3) -5C5F (xie4) -5C60 (tu2) -5C61 (lu:3) -5C62 (lu:3) -5C63 (xi3) -5C64 (ceng2) -5C65 (lu:3) -5C66 (ju4) -5C67 (xie4) -5C68 (ju4) -5C69 (jue2) -5C6A (liao2) -5C6B (jue2) -5C6C (shu3,zhu3) -5C6D (xi4) -5C6E (che4) -5C6F (tun2,zhun1) -5C70 (ni4) -5C71 (shan1) -5C72 (wa1) -5C73 (xian1) -5C74 (li4) -5C75 (e4) -5C76 (none0) -5C77 (none0) -5C78 (long2) -5C79 (yi4,ge1) -5C7A (qi3) -5C7B (ren4) -5C7C (wu4) -5C7D (han4) -5C7E (shen1) -5C7F (yu3) -5C80 (chu1) -5C81 (sui4) -5C82 (qi3) -5C83 (none0) -5C84 (yue4) -5C85 (ban3) -5C86 (yao3) -5C87 (ang2) -5C88 (ya2) -5C89 (wu4) -5C8A (jie2) -5C8B (e4) -5C8C (ji2) -5C8D (qian1) -5C8E (fen1) -5C8F (wan2) -5C90 (qi2) -5C91 (cen2) -5C92 (qian2) -5C93 (qi2) -5C94 (cha4) -5C95 (jie4) -5C96 (qu1) -5C97 (gang3,gang1) -5C98 (xian4) -5C99 (ao4) -5C9A (lan2) -5C9B (dao3) -5C9C (ba1) -5C9D (zhai3) -5C9E (zuo4) -5C9F (yang3) -5CA0 (ju4) -5CA1 (gang1) -5CA2 (ke3) -5CA3 (gou3) -5CA4 (xue4) -5CA5 (bo1) -5CA6 (li4) -5CA7 (tiao2) -5CA8 (qu1) -5CA9 (yan2) -5CAA (fu2) -5CAB (xiu4) -5CAC (jia3) -5CAD (ling3) -5CAE (tuo2) -5CAF (pei1) -5CB0 (you3) -5CB1 (dai4) -5CB2 (kuang4) -5CB3 (yue4) -5CB4 (qu1) -5CB5 (hu4) -5CB6 (po4) -5CB7 (min2) -5CB8 (an4) -5CB9 (tiao2) -5CBA (ling3) -5CBB (chi2) -5CBC (none0) -5CBD (dong1) -5CBE (none0) -5CBF (kui1) -5CC0 (xiu4) -5CC1 (mao3) -5CC2 (tong2) -5CC3 (xue2) -5CC4 (yi4) -5CC5 (none0) -5CC6 (he1) -5CC7 (ke1) -5CC8 (luo4) -5CC9 (e1) -5CCA (fu4) -5CCB (xun2) -5CCC (die2) -5CCD (lu4) -5CCE (lang3) -5CCF (er3) -5CD0 (gai1) -5CD1 (quan2) -5CD2 (tong2,dong4) -5CD3 (yi2) -5CD4 (mu3) -5CD5 (shi2) -5CD6 (an1) -5CD7 (wei3) -5CD8 (hu1) -5CD9 (zhi4,shi4) -5CDA (mi4) -5CDB (li3) -5CDC (ji1) -5CDD (tong2) -5CDE (kui3) -5CDF (you4) -5CE0 (none0) -5CE1 (xia2) -5CE2 (li3) -5CE3 (yao2) -5CE4 (jiao4,qiao2,jiao2) -5CE5 (zheng1) -5CE6 (luan2) -5CE7 (jiao1) -5CE8 (e2) -5CE9 (e2) -5CEA (yu4) -5CEB (ye2) -5CEC (bu1) -5CED (qiao4) -5CEE (qun1) -5CEF (feng1) -5CF0 (feng1) -5CF1 (nao2,nao1) -5CF2 (li4) -5CF3 (you2) -5CF4 (xian4) -5CF5 (hong2) -5CF6 (dao3) -5CF7 (shen1) -5CF8 (cheng2) -5CF9 (tu2) -5CFA (geng3) -5CFB (jun4) -5CFC (hao4) -5CFD (xia2) -5CFE (yin1) -5CFF (wu2,yu1) -5D00 (lang4) -5D01 (kan3) -5D02 (lao2) -5D03 (lai2) -5D04 (xian3) -5D05 (que4) -5D06 (kong1) -5D07 (chong2) -5D08 (chong2) -5D09 (ta4) -5D0A (none0) -5D0B (hua2,hua4) -5D0C (ju1) -5D0D (lai2) -5D0E (qi2) -5D0F (min2) -5D10 (kun1) -5D11 (kun1) -5D12 (zu2) -5D13 (gu4) -5D14 (cui1) -5D15 (ya2) -5D16 (ya2,ai2) -5D17 (gang3,gang1) -5D18 (lun2) -5D19 (lun2) -5D1A (leng2) -5D1B (jue2) -5D1C (duo1) -5D1D (cheng1) -5D1E (guo1) -5D1F (yin2) -5D20 (dong1) -5D21 (han2) -5D22 (zheng1) -5D23 (wei3) -5D24 (yao2,xiao2) -5D25 (pi3) -5D26 (yan1) -5D27 (song1) -5D28 (jie2) -5D29 (beng1) -5D2A (zu2) -5D2B (jue2) -5D2C (dong1) -5D2D (zhan3) -5D2E (gu4) -5D2F (yin2) -5D30 (zi1) -5D31 (ze2) -5D32 (huang2) -5D33 (yu2) -5D34 (wei1,wai3) -5D35 (yang2) -5D36 (feng1) -5D37 (qiu2) -5D38 (dun4) -5D39 (ti2) -5D3A (yi3) -5D3B (zhi4) -5D3C (shi4) -5D3D (zai3) -5D3E (yao3) -5D3F (e4) -5D40 (zhu4) -5D41 (kan1) -5D42 (lu:4) -5D43 (yan3) -5D44 (mei3) -5D45 (gan1,gan4) -5D46 (ji1) -5D47 (ji1) -5D48 (huan4) -5D49 (ting2) -5D4A (sheng4) -5D4B (mei2) -5D4C (qian4,qian1,kan4) -5D4D (wu4) -5D4E (yu2) -5D4F (zong1) -5D50 (lan2) -5D51 (jie2,he2) -5D52 (yan2) -5D53 (yan2) -5D54 (wei3) -5D55 (zong1) -5D56 (cha2) -5D57 (sui4) -5D58 (rong2) -5D59 (ke1) -5D5A (qin1) -5D5B (yu2) -5D5C (qi2) -5D5D (lou3) -5D5E (tu2) -5D5F (dui1) -5D60 (xi1) -5D61 (weng1) -5D62 (cang1) -5D63 (dang1) -5D64 (rong2) -5D65 (jie2) -5D66 (ai2) -5D67 (liu2) -5D68 (wu3) -5D69 (song1) -5D6A (qiao1) -5D6B (zi1) -5D6C (wei2) -5D6D (beng1) -5D6E (dian1) -5D6F (cuo2) -5D70 (qian3) -5D71 (yong2) -5D72 (nie4) -5D73 (cuo2) -5D74 (ji2) -5D75 (none0) -5D76 (none0) -5D77 (song3) -5D78 (zong1) -5D79 (jiang4) -5D7A (liao2) -5D7B (none0) -5D7C (chan3) -5D7D (di4) -5D7E (cen1) -5D7F (ding3) -5D80 (tu1,die2) -5D81 (lou3) -5D82 (zhang4) -5D83 (zhan3) -5D84 (zhan3) -5D85 (ao2) -5D86 (cao2) -5D87 (qu1) -5D88 (qiang1) -5D89 (zui1) -5D8A (zui3) -5D8B (dao3) -5D8C (dao3) -5D8D (xi2) -5D8E (yu4) -5D8F (bo2) -5D90 (long2) -5D91 (xiang3) -5D92 (ceng2) -5D93 (bo1) -5D94 (qin1) -5D95 (jiao1) -5D96 (yan3) -5D97 (lao2) -5D98 (zhan4) -5D99 (lin2) -5D9A (liao2) -5D9B (liao2) -5D9C (jin1) -5D9D (deng4) -5D9E (duo4) -5D9F (zun1) -5DA0 (jiao4,qiao2) -5DA1 (gui4) -5DA2 (yao2) -5DA3 (qiao2) -5DA4 (yao2) -5DA5 (jue2) -5DA6 (zhan1) -5DA7 (yi4) -5DA8 (xue1) -5DA9 (nao2) -5DAA (ye4) -5DAB (ye4) -5DAC (yi1) -5DAD (e4) -5DAE (xian3) -5DAF (ji2) -5DB0 (xie4) -5DB1 (ke3) -5DB2 (sui3) -5DB3 (di4) -5DB4 (ao4) -5DB5 (zui4) -5DB6 (none0) -5DB7 (yi2) -5DB8 (rong2) -5DB9 (dao3) -5DBA (ling3) -5DBB (za2) -5DBC (yu3) -5DBD (yue4) -5DBE (yin3) -5DBF (none0) -5DC0 (jie2) -5DC1 (li4) -5DC2 (sui3,xi1) -5DC3 (long2) -5DC4 (long2) -5DC5 (dian1) -5DC6 (ying2) -5DC7 (xi1) -5DC8 (ju2) -5DC9 (chan2) -5DCA (ying3) -5DCB (kui1) -5DCC (yan2) -5DCD (wei1,wei2) -5DCE (nao2) -5DCF (quan2) -5DD0 (chao1) -5DD1 (cuan2) -5DD2 (luan2) -5DD3 (dian1) -5DD4 (dian1) -5DD5 (nie4) -5DD6 (yan2) -5DD7 (yan2) -5DD8 (yan3) -5DD9 (nao2) -5DDA (yan3) -5DDB (chuan1) -5DDC (gui4) -5DDD (chuan1) -5DDE (zhou1) -5DDF (huang1) -5DE0 (jing1) -5DE1 (xun2) -5DE2 (chao2) -5DE3 (chao2) -5DE4 (lie4) -5DE5 (gong1) -5DE6 (zuo3) -5DE7 (qiao3) -5DE8 (ju4) -5DE9 (gong3) -5DEA (none0) -5DEB (wu1,wu2) -5DEC (none0) -5DED (none0) -5DEE (cha1,cha4,chai1,ci1) -5DEF (qiu2) -5DF0 (qiu2) -5DF1 (ji3) -5DF2 (yi3) -5DF3 (si4) -5DF4 (ba1) -5DF5 (zhi1) -5DF6 (zhao1) -5DF7 (xiang4,hang4) -5DF8 (yi2) -5DF9 (jin3) -5DFA (xun4) -5DFB (juan4,juan3) -5DFC (none0) -5DFD (xun4) -5DFE (jin1) -5DFF (fu2) -5E00 (za1) -5E01 (bi4) -5E02 (shi4) -5E03 (bu4) -5E04 (ding1) -5E05 (shuai4) -5E06 (fan1,fan2) -5E07 (nie4) -5E08 (shi1) -5E09 (fen1) -5E0A (pa4) -5E0B (zhi3) -5E0C (xi1) -5E0D (hu4) -5E0E (dan4) -5E0F (wei2) -5E10 (zhang4) -5E11 (tang3) -5E12 (dai4) -5E13 (ma4) -5E14 (pei4) -5E15 (pa4) -5E16 (tie1,tie3,tie4) -5E17 (fu2) -5E18 (lian2) -5E19 (zhi4) -5E1A (zhou3) -5E1B (bo2) -5E1C (zhi4) -5E1D (di4) -5E1E (mo4) -5E1F (yi4) -5E20 (yi4) -5E21 (ping2) -5E22 (qia4) -5E23 (juan4) -5E24 (ru2) -5E25 (shuai4,shuo4) -5E26 (dai4) -5E27 (zhen1) -5E28 (shui4) -5E29 (qiao4) -5E2A (zhen1) -5E2B (shi1) -5E2C (qun2) -5E2D (xi2) -5E2E (bang1) -5E2F (dai4) -5E30 (gui1) -5E31 (chou2,dao4) -5E32 (ping2) -5E33 (zhang4) -5E34 (sha1) -5E35 (wan1) -5E36 (dai4) -5E37 (wei2) -5E38 (chang2) -5E39 (sha4) -5E3A (qi2) -5E3B (ze2) -5E3C (guo2) -5E3D (mao4) -5E3E (du3) -5E3F (hou2) -5E40 (zhen1,zheng4) -5E41 (xu1) -5E42 (mi4) -5E43 (wei2) -5E44 (wo4) -5E45 (fu2) -5E46 (yi4) -5E47 (bang1) -5E48 (ping2) -5E49 (none0) -5E4A (gong1) -5E4B (pan2) -5E4C (huang3) -5E4D (dao1,tao1) -5E4E (mi4) -5E4F (jia1) -5E50 (teng2) -5E51 (hui1) -5E52 (zhong1) -5E53 (sen1) -5E54 (man4) -5E55 (mu4) -5E56 (biao1) -5E57 (guo2) -5E58 (ze2) -5E59 (mu4) -5E5A (bang1) -5E5B (zhang4) -5E5C (jiong3) -5E5D (chan3) -5E5E (fu2) -5E5F (zhi4) -5E60 (hu1) -5E61 (fan1) -5E62 (chuang2,zhuang4) -5E63 (bi4) -5E64 (bi4) -5E65 (none0) -5E66 (mi4) -5E67 (qiao1) -5E68 (dan4) -5E69 (fen2) -5E6A (meng2) -5E6B (bang1) -5E6C (chou2,dao4) -5E6D (mie4) -5E6E (chu2) -5E6F (jie1) -5E70 (xian3) -5E71 (lan2) -5E72 (gan1,gan4) -5E73 (ping2) -5E74 (nian2) -5E75 (jian1) -5E76 (bing4,bing1) -5E77 (bing4) -5E78 (xing4) -5E79 (gan4,gan1) -5E7A (yao1) -5E7B (huan4) -5E7C (you4) -5E7D (you1) -5E7E (ji3,ji1) -5E7F (guang3,an1) -5E80 (pi3) -5E81 (ting1) -5E82 (ze4) -5E83 (guang3,an1) -5E84 (zhuang1) -5E85 (mo5) -5E86 (qing4) -5E87 (bi4) -5E88 (qin2) -5E89 (dun4) -5E8A (chuang2) -5E8B (gui3) -5E8C (ya3) -5E8D (bai4) -5E8E (jie4) -5E8F (xu4) -5E90 (lu2) -5E91 (wu3) -5E92 (none0) -5E93 (ku4) -5E94 (ying1,ying4) -5E95 (di3,de5) -5E96 (pao2) -5E97 (dian4) -5E98 (ya1) -5E99 (miao4) -5E9A (geng1) -5E9B (ci1) -5E9C (fu3) -5E9D (tong2) -5E9E (pang2) -5E9F (fei4) -5EA0 (xiang2) -5EA1 (yi3) -5EA2 (zhi4) -5EA3 (tiao1) -5EA4 (zhi4) -5EA5 (xiu1) -5EA6 (du4,duo2,duo4) -5EA7 (zuo4) -5EA8 (xiao1) -5EA9 (tu2) -5EAA (gui3) -5EAB (ku4) -5EAC (pang2,mang3) -5EAD (ting2) -5EAE (you3) -5EAF (bu1) -5EB0 (bing3) -5EB1 (cheng3) -5EB2 (lai2) -5EB3 (bi4,bei1) -5EB4 (ji2) -5EB5 (an1) -5EB6 (shu4) -5EB7 (kang1) -5EB8 (yong1) -5EB9 (tuo3) -5EBA (song1) -5EBB (shu4) -5EBC (qing3) -5EBD (yu4) -5EBE (yu3) -5EBF (miao4) -5EC0 (sou1) -5EC1 (ce4,ci4) -5EC2 (xiang1) -5EC3 (fei4) -5EC4 (jiu4) -5EC5 (he2) -5EC6 (hui4) -5EC7 (liu4) -5EC8 (sha4,xia4) -5EC9 (lian2) -5ECA (lang2) -5ECB (sou1) -5ECC (jian1) -5ECD (pou3) -5ECE (qing3) -5ECF (jiu4) -5ED0 (jiu4) -5ED1 (qin2,jin3) -5ED2 (ao2) -5ED3 (kuo4) -5ED4 (lou2) -5ED5 (yin4) -5ED6 (liao4) -5ED7 (dai4) -5ED8 (lu4) -5ED9 (yi4) -5EDA (chu2) -5EDB (chan2) -5EDC (tu2) -5EDD (si1) -5EDE (xin1) -5EDF (miao4) -5EE0 (chang3,an1) -5EE1 (wu3) -5EE2 (fei4) -5EE3 (guang3,an1) -5EE4 (none0) -5EE5 (guai4) -5EE6 (bi4) -5EE7 (qiang2) -5EE8 (xie4) -5EE9 (lin3) -5EEA (lin3) -5EEB (liao2) -5EEC (lu2) -5EED (none0) -5EEE (ying2) -5EEF (xian1) -5EF0 (ting1) -5EF1 (yong1) -5EF2 (li2) -5EF3 (ting1) -5EF4 (yin3) -5EF5 (xun2) -5EF6 (yan2) -5EF7 (ting2) -5EF8 (di2) -5EF9 (po4) -5EFA (jian4) -5EFB (hui2) -5EFC (nai3) -5EFD (hui2) -5EFE (gong3) -5EFF (nian4) -5F00 (kai1) -5F01 (bian4) -5F02 (yi4) -5F03 (qi4) -5F04 (nong4,long4) -5F05 (fen2) -5F06 (ju3) -5F07 (yan3) -5F08 (yi4) -5F09 (zang4,zhuang3) -5F0A (bi4) -5F0B (yi4) -5F0C (yi1) -5F0D (er4) -5F0E (san1) -5F0F (shi4) -5F10 (er4) -5F11 (shi4) -5F12 (shi4) -5F13 (gong1) -5F14 (diao4) -5F15 (yin3) -5F16 (hu4) -5F17 (fu2) -5F18 (hong2) -5F19 (wu1) -5F1A (tui2) -5F1B (chi2) -5F1C (qiang2,qiang3) -5F1D (ba4) -5F1E (shen3) -5F1F (di4) -5F20 (zhang1) -5F21 (jue2) -5F22 (tao1) -5F23 (fu3) -5F24 (di3) -5F25 (mi2) -5F26 (xian2) -5F27 (hu2) -5F28 (chao1) -5F29 (nu3) -5F2A (jing4) -5F2B (zhen3) -5F2C (yi2) -5F2D (mi3) -5F2E (quan1) -5F2F (wan1) -5F30 (shao1) -5F31 (ruo4) -5F32 (xuan1) -5F33 (jing4) -5F34 (diao1) -5F35 (zhang1) -5F36 (jiang4) -5F37 (qiang2,jiang4,qiang3) -5F38 (beng1) -5F39 (dan4,tan2) -5F3A (qiang2,qiang3,jiang4) -5F3B (bi4) -5F3C (bi4) -5F3D (she4) -5F3E (dan4,tan2) -5F3F (jian3) -5F40 (gou4) -5F41 (none0) -5F42 (fa1) -5F43 (bi4) -5F44 (kou1) -5F45 (none0) -5F46 (bie4) -5F47 (xiao1) -5F48 (dan4,tan2) -5F49 (kuang4) -5F4A (qiang2,jiang4) -5F4B (hong2) -5F4C (mi2) -5F4D (kuo4) -5F4E (wan1) -5F4F (jue2) -5F50 (ji4) -5F51 (ji4) -5F52 (gui1) -5F53 (dang1,dang4) -5F54 (lu4) -5F55 (lu4) -5F56 (tuan4) -5F57 (hui4) -5F58 (zhi4) -5F59 (hui4) -5F5A (hui4) -5F5B (yi2) -5F5C (yi2) -5F5D (yi2) -5F5E (yi2) -5F5F (huo4) -5F60 (huo4) -5F61 (shan1) -5F62 (xing2) -5F63 (zhang1) -5F64 (tong2) -5F65 (yan4) -5F66 (yan4) -5F67 (yu4) -5F68 (chi1) -5F69 (cai3) -5F6A (biao1) -5F6B (diao1) -5F6C (bin1) -5F6D (peng2) -5F6E (yong3) -5F6F (piao4) -5F70 (zhang1) -5F71 (ying3) -5F72 (chi1) -5F73 (chi4) -5F74 (zhuo2) -5F75 (tuo3) -5F76 (ji2) -5F77 (pang2,fang3) -5F78 (zhong1) -5F79 (yi4) -5F7A (wang2) -5F7B (che4) -5F7C (bi3) -5F7D (di1) -5F7E (ling3) -5F7F (fu2) -5F80 (wang3,wang4) -5F81 (zheng1) -5F82 (cu2) -5F83 (wang3,wang4) -5F84 (jing4) -5F85 (dai4,dai1) -5F86 (xi1) -5F87 (xun4,xun2) -5F88 (hen3) -5F89 (yang2) -5F8A (huai2,hui2) -5F8B (lu:4) -5F8C (hou4) -5F8D (wang3) -5F8E (cheng3) -5F8F (zhi4) -5F90 (xu2) -5F91 (jing4) -5F92 (tu2) -5F93 (cong2) -5F94 (none0) -5F95 (lai2) -5F96 (cong2) -5F97 (de2,de5,dei3) -5F98 (pai2) -5F99 (xi3) -5F9A (none0) -5F9B (qi1) -5F9C (chang2) -5F9D (zhi4) -5F9E (cong2,cong1,zong4) -5F9F (zhou1) -5FA0 (lai2) -5FA1 (yu4) -5FA2 (xie4) -5FA3 (jie4) -5FA4 (jian4) -5FA5 (chi2,shi4) -5FA6 (jia3) -5FA7 (bian4) -5FA8 (huang2) -5FA9 (fu4) -5FAA (xun2) -5FAB (wei3) -5FAC (pang2) -5FAD (yao2) -5FAE (wei1,wei2) -5FAF (xi1) -5FB0 (zheng1) -5FB1 (piao4) -5FB2 (chi2) -5FB3 (de2) -5FB4 (zheng1) -5FB5 (zhi3,zheng1) -5FB6 (bie2) -5FB7 (de2) -5FB8 (chong1) -5FB9 (che4) -5FBA (jiao3) -5FBB (wei4) -5FBC (jiao4,jiao3,jia3) -5FBD (hui1) -5FBE (mei2) -5FBF (long4) -5FC0 (xiang1) -5FC1 (bao4) -5FC2 (qu2) -5FC3 (xin1) -5FC4 (xin1) -5FC5 (bi4) -5FC6 (yi4) -5FC7 (le4) -5FC8 (ren2) -5FC9 (dao1) -5FCA (ding4) -5FCB (gai3) -5FCC (ji4) -5FCD (ren3) -5FCE (ren2) -5FCF (chan4) -5FD0 (tan3) -5FD1 (te4) -5FD2 (te4,tui1,tei1) -5FD3 (gan1) -5FD4 (qi4) -5FD5 (dai4) -5FD6 (cun3) -5FD7 (zhi4) -5FD8 (wang4,wang2) -5FD9 (mang2) -5FDA (xi1) -5FDB (fan2) -5FDC (ying1,ying4) -5FDD (tian3) -5FDE (min2) -5FDF (min2) -5FE0 (zhong1) -5FE1 (chong1) -5FE2 (wu4) -5FE3 (ji2) -5FE4 (wu3) -5FE5 (xi4) -5FE6 (ye4) -5FE7 (you1) -5FE8 (wan4) -5FE9 (zong3) -5FEA (zhong1,song1) -5FEB (kuai4) -5FEC (yu4) -5FED (bian4) -5FEE (zhi4) -5FEF (chi2) -5FF0 (cui4) -5FF1 (chen2) -5FF2 (tai4) -5FF3 (tun2) -5FF4 (qian2) -5FF5 (nian4) -5FF6 (hun2) -5FF7 (xiong1) -5FF8 (niu3,nu:4) -5FF9 (wang3) -5FFA (xian1) -5FFB (xin1) -5FFC (kang1) -5FFD (hu1) -5FFE (kai4) -5FFF (fen4) -6000 (huai2) -6001 (tai4) -6002 (song3) -6003 (wu3) -6004 (ou4) -6005 (chang4) -6006 (chuang4) -6007 (ju4) -6008 (yi4) -6009 (bao3) -600A (chao1) -600B (min2) -600C (pi1) -600D (zuo4) -600E (zen3,ze3) -600F (yang4) -6010 (kou4) -6011 (ban4) -6012 (nu4) -6013 (nao2) -6014 (zheng1,zheng4) -6015 (pa4) -6016 (bu4) -6017 (tie1) -6018 (hu4) -6019 (hu4) -601A (ju4) -601B (da2) -601C (lian2,ling2) -601D (si1,sai1) -601E (zhou4) -601F (di4) -6020 (dai4) -6021 (yi2) -6022 (tu2) -6023 (you2) -6024 (fu1) -6025 (ji2) -6026 (peng1) -6027 (xing4) -6028 (yuan4) -6029 (ni2) -602A (guai4) -602B (fu2,fei4) -602C (xi4) -602D (bi4) -602E (you1) -602F (qie4,que4) -6030 (xuan4) -6031 (zong1) -6032 (bing3) -6033 (huang3) -6034 (xu4) -6035 (chu4) -6036 (pi1) -6037 (xi1) -6038 (xi1) -6039 (tan1) -603A (none0) -603B (zong3) -603C (dui4) -603D (none0) -603E (none0) -603F (yi4) -6040 (chi3) -6041 (nen4,ren4,nin2) -6042 (xun2) -6043 (shi4) -6044 (xi4) -6045 (lao3) -6046 (heng2) -6047 (kuang1) -6048 (mou2,mu4) -6049 (zhi3) -604A (xie2) -604B (lian4) -604C (tiao1) -604D (huang3) -604E (die2) -604F (hao3) -6050 (kong3) -6051 (gui3) -6052 (heng2) -6053 (xi1) -6054 (xiao4) -6055 (shu4) -6056 (sai1,si1) -6057 (hu1) -6058 (qiu1) -6059 (yang4) -605A (hui4) -605B (hui2) -605C (chi4) -605D (jia2) -605E (yi2) -605F (xiong1) -6060 (guai4) -6061 (lin4) -6062 (hui1) -6063 (zi4) -6064 (xu4) -6065 (chi3) -6066 (xiang4) -6067 (nu:4) -6068 (hen4) -6069 (en1) -606A (ke4,que4) -606B (dong4,tong1) -606C (tian2) -606D (gong1) -606E (quan2) -606F (xi1,xi2) -6070 (qia4) -6071 (yue4) -6072 (peng1) -6073 (ken3) -6074 (de2) -6075 (hui4) -6076 (e4,e3,wu4,wu1) -6077 (none0) -6078 (tong4) -6079 (yan1) -607A (kai3) -607B (ce4) -607C (nao3) -607D (yun4) -607E (mang2) -607F (yong3) -6080 (yong3) -6081 (juan4) -6082 (mang2) -6083 (kun3) -6084 (qiao3,qiao1) -6085 (yue4) -6086 (yu4) -6087 (yu4) -6088 (jie4) -6089 (xi1) -608A (zhe2,qi1) -608B (lin4) -608C (ti4) -608D (han4) -608E (hao4) -608F (qie4) -6090 (ti4) -6091 (bu4) -6092 (yi4) -6093 (qian4) -6094 (hui3) -6095 (xi1) -6096 (bei4) -6097 (man2) -6098 (yi2) -6099 (heng1) -609A (song3) -609B (quan1) -609C (cheng3) -609D (kui1,li3) -609E (wu4) -609F (wu4) -60A0 (you1) -60A1 (li2) -60A2 (liang4) -60A3 (huan4) -60A4 (cong1) -60A5 (yi4) -60A6 (yue4) -60A7 (li4) -60A8 (nin2) -60A9 (nao3) -60AA (e4,e3,wu4) -60AB (que4) -60AC (xuan2) -60AD (qian1) -60AE (wu4) -60AF (min3) -60B0 (cong2) -60B1 (fei3) -60B2 (bei1) -60B3 (de2) -60B4 (cui4) -60B5 (chang4) -60B6 (men4,men1) -60B7 (li4) -60B8 (ji4) -60B9 (guan4) -60BA (guan4) -60BB (xing4) -60BC (dao4) -60BD (qi1) -60BE (kong1) -60BF (tian3) -60C0 (lun2) -60C1 (xi1) -60C2 (kan3) -60C3 (kun1) -60C4 (ni4) -60C5 (qing2,qing5) -60C6 (chou2) -60C7 (dun1) -60C8 (guo3) -60C9 (chan1) -60CA (jing1) -60CB (wan3) -60CC (yuan1) -60CD (jin1) -60CE (ji4) -60CF (lin2) -60D0 (yu4) -60D1 (huo4) -60D2 (he2) -60D3 (quan1) -60D4 (yan3) -60D5 (ti4) -60D6 (ti4) -60D7 (nie1) -60D8 (wang3) -60D9 (chuo4) -60DA (hu1) -60DB (hun1) -60DC (xi1,xi2) -60DD (chang3) -60DE (xin1) -60DF (wei2) -60E0 (hui4) -60E1 (e4,e3,wu4,wu1) -60E2 (rui3) -60E3 (zong3) -60E4 (jian1) -60E5 (yong3) -60E6 (dian4) -60E7 (ju4) -60E8 (can3,can4) -60E9 (cheng2) -60EA (de2) -60EB (bei4) -60EC (qie4) -60ED (can2) -60EE (dan4) -60EF (guan4) -60F0 (duo4) -60F1 (nao3) -60F2 (yun4) -60F3 (xiang3) -60F4 (zhui4) -60F5 (die2) -60F6 (huang2) -60F7 (chun3) -60F8 (qiong2) -60F9 (re3) -60FA (xing1) -60FB (ce4) -60FC (bian3) -60FD (hun1) -60FE (zong1) -60FF (ti2) -6100 (qiao3) -6101 (chou2) -6102 (bei4) -6103 (xuan1) -6104 (wei1) -6105 (ge2) -6106 (qian1) -6107 (wei3) -6108 (yu4) -6109 (yu2) -610A (bi4) -610B (xuan1) -610C (huan4) -610D (min3) -610E (bi4) -610F (yi4) -6110 (mian3) -6111 (yong3) -6112 (kai4,qi4) -6113 (dang4) -6114 (yin1) -6115 (e4) -6116 (chen2) -6117 (mou4) -6118 (qia4) -6119 (ke4) -611A (yu2) -611B (ai4) -611C (qie4) -611D (yan3) -611E (nuo4) -611F (gan3) -6120 (yun4) -6121 (zong3) -6122 (sai1) -6123 (leng4) -6124 (fen4) -6125 (none0) -6126 (kui4) -6127 (kui4) -6128 (que4) -6129 (gong1) -612A (yun2) -612B (su4) -612C (su4) -612D (qi2) -612E (yao2) -612F (song3) -6130 (huang4) -6131 (none0) -6132 (gu3) -6133 (ju4) -6134 (chuang4) -6135 (ta1) -6136 (xie2) -6137 (kai3) -6138 (zheng3) -6139 (yong3) -613A (cao3) -613B (sun4) -613C (shen4) -613D (bo2) -613E (kai4) -613F (yuan4) -6140 (xie2) -6141 (hun4) -6142 (yong3) -6143 (yang3) -6144 (li4) -6145 (sao1) -6146 (tao1) -6147 (yin1) -6148 (ci2) -6149 (xu4) -614A (qian4,qie4) -614B (tai4) -614C (huang1,huang5) -614D (yun4) -614E (shen4) -614F (ming3) -6150 (none0) -6151 (she4) -6152 (cong2) -6153 (piao1) -6154 (mo4) -6155 (mu4) -6156 (guo2) -6157 (chi4) -6158 (can3,can4) -6159 (can2) -615A (can2) -615B (cui2) -615C (min3) -615D (ni4,te4) -615E (zhang1) -615F (tong4) -6160 (ao4) -6161 (shuang3) -6162 (man4) -6163 (guan4) -6164 (que4) -6165 (zao4) -6166 (jiu4) -6167 (hui4) -6168 (kai3,kai4) -6169 (lian2) -616A (ou4,ou1) -616B (song3) -616C (jin3) -616D (yin4) -616E (lu:4) -616F (shang1) -6170 (wei4) -6171 (tuan2) -6172 (man2) -6173 (qian1) -6174 (zhe2) -6175 (yong1) -6176 (qing4) -6177 (kang1,kang3) -6178 (di4) -6179 (zhi2) -617A (lu:2) -617B (juan4) -617C (qi1) -617D (qi1) -617E (yu4) -617F (ping2) -6180 (liao2) -6181 (zong3) -6182 (you1) -6183 (chuang1) -6184 (zhi4) -6185 (tong4) -6186 (cheng1) -6187 (qi4) -6188 (qu1) -6189 (peng2) -618A (bei4) -618B (bie1) -618C (chun2) -618D (jiao1) -618E (zeng1) -618F (chi4) -6190 (lian2) -6191 (ping2) -6192 (kui4) -6193 (hui4) -6194 (qiao2) -6195 (cheng2) -6196 (yin4) -6197 (yin4) -6198 (xi3) -6199 (xi3) -619A (dan4) -619B (tan2) -619C (duo4) -619D (dui4) -619E (dui4) -619F (su4) -61A0 (jue4) -61A1 (ce4) -61A2 (xiao1) -61A3 (fan2) -61A4 (fen4) -61A5 (lao2) -61A6 (lao4) -61A7 (chong1) -61A8 (han1) -61A9 (qi4) -61AA (xian2) -61AB (min3) -61AC (jing3) -61AD (liao3) -61AE (wu3) -61AF (can3) -61B0 (jue2) -61B1 (chou4) -61B2 (xian4) -61B3 (tan3) -61B4 (sheng2) -61B5 (pi1) -61B6 (yi4) -61B7 (chu4) -61B8 (xian1) -61B9 (nao2) -61BA (dan4) -61BB (tan3) -61BC (jing3) -61BD (song1) -61BE (han4) -61BF (jiao1) -61C0 (wei4) -61C1 (huan2) -61C2 (dong3) -61C3 (qin2) -61C4 (qin2) -61C5 (qu2) -61C6 (cao3) -61C7 (ken3) -61C8 (xie4) -61C9 (ying4,ying1) -61CA (ao4) -61CB (mao4) -61CC (yi4) -61CD (lin3) -61CE (se4) -61CF (jun4) -61D0 (huai2) -61D1 (men4) -61D2 (lan3) -61D3 (ai4) -61D4 (lin3) -61D5 (yan1) -61D6 (gua1) -61D7 (xia4) -61D8 (chi4) -61D9 (yu3) -61DA (yin4) -61DB (dai1) -61DC (meng3) -61DD (ai4) -61DE (meng3) -61DF (dui4) -61E0 (qi2) -61E1 (mo3) -61E2 (lan2) -61E3 (men4) -61E4 (chou2) -61E5 (zhi4) -61E6 (nuo4) -61E7 (nuo4) -61E8 (yan1,yan4) -61E9 (yang3) -61EA (bo2) -61EB (zhi2) -61EC (xing4) -61ED (kuang4) -61EE (you1) -61EF (fu2) -61F0 (liu2) -61F1 (mie4) -61F2 (cheng2) -61F3 (none0) -61F4 (chan4) -61F5 (meng3) -61F6 (lan3) -61F7 (huai2) -61F8 (xuan2) -61F9 (rang4) -61FA (chan4) -61FB (ji4) -61FC (ju4) -61FD (huan1) -61FE (she4,zhe2) -61FF (yi4) -6200 (lian4) -6201 (nan3) -6202 (mi2) -6203 (tang3) -6204 (jue2) -6205 (gang4) -6206 (gang4,zhuang4) -6207 (zhuang4) -6208 (ge1) -6209 (yue4) -620A (wu4) -620B (jian1) -620C (xu1,qu5) -620D (shu4) -620E (rong2) -620F (xi4,hu1) -6210 (cheng2) -6211 (wo3) -6212 (jie4) -6213 (ge1) -6214 (jian1) -6215 (qiang1) -6216 (huo4) -6217 (qiang1,qiang4) -6218 (zhan4) -6219 (dong4) -621A (qi1) -621B (jia2) -621C (die2) -621D (cai2) -621E (jia2) -621F (ji3) -6220 (shi4,chi4) -6221 (kan1) -6222 (ji2) -6223 (kui2) -6224 (gai4) -6225 (deng3) -6226 (zhan4) -6227 (chuang1,qiang1,qiang4) -6228 (ge1) -6229 (jian3) -622A (jie2) -622B (yu4) -622C (jian3) -622D (yan3) -622E (lu4) -622F (xi4,hu1) -6230 (zhan4) -6231 (xi4) -6232 (xi4,hu1) -6233 (chuo1) -6234 (dai4) -6235 (qu2) -6236 (hu4) -6237 (hu4) -6238 (hu4) -6239 (e4) -623A (shi4) -623B (li4) -623C (mao3) -623D (hu4) -623E (li4) -623F (fang2) -6240 (suo3) -6241 (bian3,pian1) -6242 (dian4) -6243 (jiong1) -6244 (shang3) -6245 (yi2) -6246 (yi3) -6247 (shan4,shan1) -6248 (hu4) -6249 (fei1) -624A (yan3) -624B (shou3) -624C (shou3) -624D (cai2) -624E (zha1,za1,zha2) -624F (qiu2) -6250 (le4) -6251 (pu1) -6252 (ba1,pa2,pa1) -6253 (da3,da2) -6254 (reng1) -6255 (fu2,bi4) -6256 (none0) -6257 (zai4) -6258 (tuo1) -6259 (zhang4) -625A (diao1) -625B (kang2,gang1) -625C (yu1) -625D (ku1) -625E (han4) -625F (shen1) -6260 (cha1) -6261 (chi3) -6262 (gu3) -6263 (kou4) -6264 (wu4) -6265 (tuo1) -6266 (qian1) -6267 (zhi2) -6268 (cha1) -6269 (kuo4) -626A (men2) -626B (sao3,sao4) -626C (yang2) -626D (niu3) -626E (ban4) -626F (che3) -6270 (rao3) -6271 (xi1) -6272 (qian2) -6273 (ban1,pan1) -6274 (jia2) -6275 (yu2) -6276 (fu2) -6277 (ao4) -6278 (xi1) -6279 (pi1) -627A (zhi3) -627B (zi4) -627C (e4) -627D (dun4) -627E (zhao3) -627F (cheng2) -6280 (ji4) -6281 (yan3) -6282 (kuang2) -6283 (bian4) -6284 (chao1) -6285 (ju1) -6286 (wen4) -6287 (hu2) -6288 (yue4) -6289 (jue2) -628A (ba3,ba4) -628B (qin4) -628C (zhen3) -628D (zheng3) -628E (yun3) -628F (wan2) -6290 (na4) -6291 (yi4) -6292 (shu1) -6293 (zhua1) -6294 (pou2) -6295 (tou2) -6296 (dou3) -6297 (kang4) -6298 (zhe2,she2,zhe1) -6299 (pou2) -629A (fu3) -629B (pao1) -629C (ba2) -629D (ao3) -629E (ze2,zhai2) -629F (tuan2) -62A0 (kou1) -62A1 (lun2,lun1) -62A2 (qiang3,qiang1) -62A3 (none0) -62A4 (hu4) -62A5 (bao4) -62A6 (bing3) -62A7 (zhi3) -62A8 (peng1) -62A9 (tan1) -62AA (pu1) -62AB (pi1) -62AC (tai2) -62AD (yao3) -62AE (zhen3) -62AF (zha1) -62B0 (yang3) -62B1 (bao4) -62B2 (he1) -62B3 (ni3) -62B4 (yi4) -62B5 (di3) -62B6 (chi4) -62B7 (pi1) -62B8 (za1) -62B9 (mo3,ma1,mo4) -62BA (mo3) -62BB (chen1,shen4) -62BC (ya1,ya2) -62BD (chou1) -62BE (qu1) -62BF (min3) -62C0 (chu4) -62C1 (jia1) -62C2 (fu2,bi4) -62C3 (zha3) -62C4 (zhu3) -62C5 (dan1,dan4,dan3) -62C6 (chai1,ca1) -62C7 (mu3) -62C8 (nian1,nian3) -62C9 (la1,la2,la3,la4) -62CA (fu3) -62CB (pao1) -62CC (ban4) -62CD (pai1) -62CE (lin1) -62CF (na2) -62D0 (guai3) -62D1 (qian2) -62D2 (ju4) -62D3 (tuo4,ta4) -62D4 (ba2) -62D5 (tuo1) -62D6 (tuo1) -62D7 (ao4,niu4,yao4,ao3) -62D8 (ju1) -62D9 (zhuo1,zhuo2) -62DA (pan4,pin1) -62DB (zhao1) -62DC (bai4) -62DD (bai4) -62DE (di3) -62DF (ni3) -62E0 (ju4,ju1) -62E1 (kuo4) -62E2 (long3) -62E3 (jian3) -62E4 (qia2) -62E5 (yong1) -62E6 (lan2) -62E7 (ning3,ning2,ning4) -62E8 (bo1) -62E9 (ze2,zhai2) -62EA (qian1) -62EB (hen2) -62EC (kuo4,gua1) -62ED (shi4) -62EE (jie2) -62EF (zheng3) -62F0 (nin3) -62F1 (gong3) -62F2 (gong3) -62F3 (quan2) -62F4 (shuan1) -62F5 (tun2) -62F6 (zan3,za1) -62F7 (kao3) -62F8 (chi3) -62F9 (xie2) -62FA (ce4) -62FB (hui1) -62FC (pin1) -62FD (zhuai4,ye4,zhuai1) -62FE (shi2,she4) -62FF (na2) -6300 (bo4) -6301 (chi2) -6302 (gua4) -6303 (zhi4) -6304 (kuo4) -6305 (duo3) -6306 (duo3) -6307 (zhi3,zhi1,zhi2) -6308 (qie4) -6309 (an4) -630A (nong4,long4) -630B (zhen4) -630C (ge2) -630D (jiao4) -630E (kua4) -630F (dong4) -6310 (ru2,na2) -6311 (tiao3,tiao1) -6312 (lie4) -6313 (zha1) -6314 (lu:3) -6315 (die2) -6316 (wa1) -6317 (jue2) -6318 (none0) -6319 (ju3) -631A (zhi4) -631B (luan2) -631C (ya4) -631D (wo1,zhua1) -631E (ta4) -631F (xie2,jia1) -6320 (nao2) -6321 (dang3,dang4) -6322 (jiao3,jia3) -6323 (zheng1,zheng4) -6324 (ji3) -6325 (hui1) -6326 (xian2) -6327 (none0) -6328 (ai1,ai2) -6329 (tuo1) -632A (nuo2) -632B (cuo4) -632C (bo2) -632D (geng3) -632E (ti3) -632F (zhen4) -6330 (cheng2) -6331 (suo1) -6332 (suo1,sa1,sha1) -6333 (keng1,keng3) -6334 (mei3) -6335 (long4,nong4) -6336 (ju2) -6337 (peng2) -6338 (jian3) -6339 (yi4) -633A (ting3) -633B (shan1) -633C (nuo4) -633D (wan3) -633E (xie2,jia2,xia2) -633F (cha1) -6340 (feng1) -6341 (jiao3,jia3) -6342 (wu3,wu2) -6343 (jun4) -6344 (jiu4) -6345 (tong3) -6346 (kun3) -6347 (huo4) -6348 (tu2) -6349 (zhuo1) -634A (pou2) -634B (lu:3,luo1) -634C (ba1) -634D (han4) -634E (shao1,shao4) -634F (nie1) -6350 (juan1) -6351 (she4) -6352 (shu4) -6353 (ye2) -6354 (jue2) -6355 (bu3) -6356 (huan2) -6357 (bu4) -6358 (jun4) -6359 (yi4) -635A (zhai1) -635B (lu:3) -635C (sou1) -635D (tuo1) -635E (lao1) -635F (sun3) -6360 (bang1) -6361 (jian3) -6362 (huan4) -6363 (dao3) -6364 (none0) -6365 (wan4) -6366 (qin2) -6367 (peng3) -6368 (she3,she4) -6369 (lie4) -636A (min2) -636B (men2) -636C (fu3) -636D (bai3) -636E (ju4,ju1) -636F (dao2) -6370 (wo3) -6371 (ai2,ai1) -6372 (juan3) -6373 (yue4) -6374 (zong3) -6375 (chen3) -6376 (chui2) -6377 (jie2) -6378 (tu1) -6379 (ben4) -637A (na4) -637B (nian3) -637C (nuo2) -637D (zu2) -637E (wo4) -637F (xi1,qi1) -6380 (xian1) -6381 (cheng2) -6382 (dian1) -6383 (sao3,sao4) -6384 (lun2,lun1) -6385 (qing4) -6386 (gang1) -6387 (duo1,duo5) -6388 (shou4) -6389 (diao4) -638A (pou2,pou3) -638B (di3) -638C (zhang3) -638D (gun3) -638E (ji3) -638F (tao1) -6390 (qia1) -6391 (qi2) -6392 (pai2,pai3) -6393 (shu2) -6394 (qian1) -6395 (ling2) -6396 (ye4,ye1,yi4) -6397 (ya3) -6398 (jue2) -6399 (zheng1,zheng4) -639A (liang3) -639B (gua4) -639C (yi3) -639D (huo4) -639E (shan4) -639F (ding4) -63A0 (lu:e4,lu:e3) -63A1 (cai3) -63A2 (tan4,tan1) -63A3 (che4) -63A4 (bing1) -63A5 (jie1) -63A6 (ti1) -63A7 (kong4) -63A8 (tui1) -63A9 (yan3) -63AA (cuo4) -63AB (zou1) -63AC (ju1,ju2) -63AD (tian4) -63AE (qian2) -63AF (ken4) -63B0 (bai1,bo4) -63B1 (shou3,pa2) -63B2 (jie1) -63B3 (lu3) -63B4 (guai1,guo2) -63B5 (none0) -63B6 (none0) -63B7 (zhi4,zhi1) -63B8 (dan3,shan4,shan3) -63B9 (none0) -63BA (chan1,shan3,can4) -63BB (sao1) -63BC (guan4) -63BD (peng4) -63BE (yuan4) -63BF (nuo4) -63C0 (jian3) -63C1 (zheng1) -63C2 (jiu1) -63C3 (jian1) -63C4 (yu2) -63C5 (yan2) -63C6 (kui2) -63C7 (nan3) -63C8 (hong1) -63C9 (rou2) -63CA (pi4) -63CB (wei1) -63CC (sai1) -63CD (zou4) -63CE (xuan1) -63CF (miao2) -63D0 (ti2,di1,shi2) -63D1 (nie1) -63D2 (cha1) -63D3 (shi4) -63D4 (zong3) -63D5 (zhen4) -63D6 (yi1) -63D7 (shun3) -63D8 (heng2) -63D9 (bian4) -63DA (yang2) -63DB (huan4) -63DC (yan3) -63DD (zan3) -63DE (an3) -63DF (xu1,ju1) -63E0 (ya4) -63E1 (wo4) -63E2 (ke4) -63E3 (chuai3,chuai1,chuai4) -63E4 (ji2,jie2) -63E5 (ti4) -63E6 (la2) -63E7 (la4) -63E8 (cheng2) -63E9 (kai1) -63EA (jiu1) -63EB (jiu1) -63EC (tu2) -63ED (jie1) -63EE (hui1) -63EF (geng1) -63F0 (chong4) -63F1 (shuo4) -63F2 (she2,die2) -63F3 (xie1) -63F4 (yuan2) -63F5 (qian2) -63F6 (ye2) -63F7 (cha1) -63F8 (zha1) -63F9 (bei1) -63FA (yao2) -63FB (none0) -63FC (none0) -63FD (lan3) -63FE (wen4) -63FF (qin4) -6400 (chan1) -6401 (ge1,ge2) -6402 (lou3,lou1) -6403 (zong3) -6404 (geng1) -6405 (jiao3,jia3) -6406 (gou4) -6407 (qin4) -6408 (yong3) -6409 (que4) -640A (chou1) -640B (chuai1) -640C (zhan3) -640D (sun3) -640E (sun1) -640F (bo2) -6410 (chu4) -6411 (rong3) -6412 (bang4,peng2) -6413 (cuo1) -6414 (sao1) -6415 (ke4) -6416 (yao2) -6417 (dao3) -6418 (zhi1) -6419 (nu4) -641A (xie2) -641B (jian1) -641C (sou1) -641D (qiu3) -641E (gao3) -641F (xian3) -6420 (shuo4) -6421 (sang3) -6422 (jin4) -6423 (mie4) -6424 (e4) -6425 (chui2) -6426 (nuo4) -6427 (shan1) -6428 (ta4) -6429 (jie2) -642A (tang2) -642B (pan2) -642C (ban1) -642D (da1) -642E (li4) -642F (tao1) -6430 (hu2) -6431 (zhi4) -6432 (wa1) -6433 (xia2) -6434 (qian1) -6435 (wen4) -6436 (qiang3,chuang3,qiang1) -6437 (chen1) -6438 (zhen1) -6439 (e4) -643A (xie2) -643B (nuo4) -643C (quan2) -643D (cha2) -643E (zha4) -643F (ge2) -6440 (wu3) -6441 (en4) -6442 (she4) -6443 (gong4) -6444 (she4) -6445 (shu1) -6446 (bai3) -6447 (yao2) -6448 (bin4) -6449 (sou1) -644A (tan1) -644B (sha1) -644C (chan3) -644D (suo1) -644E (liao2) -644F (chong1) -6450 (chuang1) -6451 (guo2,guai1) -6452 (bing4) -6453 (feng2) -6454 (shuai1) -6455 (di4) -6456 (qi4) -6457 (none0) -6458 (zhai1,zhe2) -6459 (lian3) -645A (cheng1) -645B (chi1) -645C (guan4) -645D (lu4) -645E (luo4) -645F (lou3,lou1) -6460 (zong3) -6461 (gai4,xi4) -6462 (hu4) -6463 (zha1) -6464 (chuang3) -6465 (tang4) -6466 (hua4) -6467 (cui1) -6468 (nai2) -6469 (mo2,ma1) -646A (jiang1) -646B (gui1) -646C (ying4) -646D (zhi2) -646E (ao2) -646F (zhi4) -6470 (chi4) -6471 (man2) -6472 (shan4) -6473 (kou1) -6474 (shu1) -6475 (suo3) -6476 (tuan2) -6477 (zhao1) -6478 (mo1,mo2) -6479 (mo2) -647A (zhe2) -647B (chan1,shan3) -647C (keng1) -647D (biao1,biao4) -647E (jiang4) -647F (yin1) -6480 (gou4) -6481 (qian1) -6482 (liao4,liao1,liao2) -6483 (ji1) -6484 (ying1) -6485 (jue1) -6486 (pie1) -6487 (pie3,pie1) -6488 (lao1) -6489 (dun1) -648A (xian4) -648B (ruan2) -648C (kui4) -648D (zan3) -648E (yi4) -648F (xian2) -6490 (cheng1) -6491 (cheng1) -6492 (sa1,sa3) -6493 (nao2) -6494 (heng4) -6495 (si1) -6496 (han4) -6497 (huang2) -6498 (da1) -6499 (zun3) -649A (nian3) -649B (lin3) -649C (zheng3) -649D (hui1) -649E (zhuang4,chuang2) -649F (jiao3,jia3) -64A0 (ji3) -64A1 (cao1) -64A2 (dan3) -64A3 (dan3,shan3,shan4) -64A4 (che4) -64A5 (bo1) -64A6 (che3) -64A7 (jue2) -64A8 (xiao1) -64A9 (liao2,liao1,liao4) -64AA (ben4) -64AB (fu3) -64AC (qiao4) -64AD (bo1,bo4) -64AE (cuo1,zuo3,cuo4,cuo3) -64AF (zhuo2) -64B0 (zhuan4) -64B1 (tuo3) -64B2 (pu1) -64B3 (qin4) -64B4 (dun1) -64B5 (nian3) -64B6 (none0) -64B7 (xie2) -64B8 (lu1) -64B9 (jiao3,jia3) -64BA (cuan1) -64BB (ta4) -64BC (han4) -64BD (qiao4) -64BE (zhua1,wo1) -64BF (jian3) -64C0 (gan3) -64C1 (yong3,yong1) -64C2 (lei2,lei4,lei1) -64C3 (kuo3) -64C4 (lu3) -64C5 (shan4) -64C6 (zhuo2) -64C7 (ze2,zhai2) -64C8 (pu1) -64C9 (chuo4) -64CA (ji2,ji1) -64CB (dang3,dang4) -64CC (se4) -64CD (cao1,cao4) -64CE (qing2) -64CF (jing4) -64D0 (huan4) -64D1 (jie1) -64D2 (qin2) -64D3 (kuai3) -64D4 (dan1,dan3,dan4) -64D5 (xie2) -64D6 (ge3) -64D7 (pi3) -64D8 (bo4,bai1) -64D9 (ao4) -64DA (ju4,ju1) -64DB (ye4) -64DC (none0) -64DD (none0) -64DE (sou3,sou4) -64DF (mi2) -64E0 (ji3) -64E1 (tai2) -64E2 (zhuo2) -64E3 (dao3) -64E4 (xing3) -64E5 (lan3) -64E6 (ca1) -64E7 (ju3) -64E8 (ye2) -64E9 (ru3) -64EA (ye4) -64EB (ye4) -64EC (ni3) -64ED (huo4) -64EE (ji2) -64EF (bin4) -64F0 (ning3,ning2) -64F1 (ge1,ge2) -64F2 (zhi4,zhi2) -64F3 (jie2) -64F4 (kuo4) -64F5 (mo2,ma1) -64F6 (jian4) -64F7 (xie2) -64F8 (lie4) -64F9 (tan1) -64FA (bai3) -64FB (sou3,sou4) -64FC (lu1) -64FD (lu:e4) -64FE (rao3) -64FF (zhi2) -6500 (pan1) -6501 (yang3) -6502 (lei4) -6503 (sa4) -6504 (shu1) -6505 (zan3,cuan2) -6506 (nian3) -6507 (xian3) -6508 (jun4) -6509 (huo1) -650A (lu:e4) -650B (la4) -650C (han4) -650D (ying2) -650E (lu2) -650F (long3) -6510 (qian1) -6511 (qian1) -6512 (zan3,cuan2) -6513 (qian1) -6514 (lan2) -6515 (san1) -6516 (ying1) -6517 (mei2) -6518 (rang3,rang2) -6519 (chan1) -651A (none0) -651B (cuan1) -651C (xie2,xi1) -651D (she4) -651E (luo1) -651F (jun4) -6520 (mi2) -6521 (li2) -6522 (zan3,cuan2) -6523 (luan2) -6524 (tan1) -6525 (zuan4) -6526 (li4) -6527 (dian1) -6528 (wa1) -6529 (dang3) -652A (jiao3,gao3,jia3) -652B (jue2) -652C (lan3) -652D (li4) -652E (nang3) -652F (zhi1) -6530 (gui4) -6531 (gui3) -6532 (qi1) -6533 (xin2) -6534 (po1) -6535 (po1) -6536 (shou1) -6537 (kao3) -6538 (you1) -6539 (gai3) -653A (gai3) -653B (gong1) -653C (gan1) -653D (ban1) -653E (fang4) -653F (zheng4) -6540 (bo2) -6541 (dian1) -6542 (kou4) -6543 (min3) -6544 (wu4) -6545 (gu4) -6546 (ge2) -6547 (ce4) -6548 (xiao4) -6549 (mi3) -654A (chu4) -654B (ge2) -654C (di2) -654D (xu4) -654E (jiao4) -654F (min3) -6550 (chen2) -6551 (jiu4) -6552 (shen1) -6553 (duo2) -6554 (yu3) -6555 (chi4) -6556 (ao2) -6557 (bai4) -6558 (xu4) -6559 (jiao4,jiao1) -655A (duo2) -655B (lian3) -655C (nie4) -655D (bi4) -655E (chang3,tang3) -655F (dian4) -6560 (duo2) -6561 (yi4) -6562 (gan3) -6563 (san4,san3,san5) -6564 (ke3) -6565 (yan4) -6566 (dun1,dui4) -6567 (qi3) -6568 (dou3) -6569 (xiao4) -656A (duo2) -656B (jiao3,jia3) -656C (jing4) -656D (yang2) -656E (xia2) -656F (hun1,min3) -6570 (shu4,shu3,shuo4) -6571 (ai2) -6572 (qiao1) -6573 (ai2) -6574 (zheng3) -6575 (di2) -6576 (zhen4) -6577 (fu1) -6578 (shu4,shu3,shuo4) -6579 (liao2) -657A (qu1) -657B (xiong4) -657C (xi3) -657D (jiao3) -657E (none0) -657F (qiao2) -6580 (zhuo2) -6581 (yi4,du4) -6582 (lian4,lian3) -6583 (bi4) -6584 (li4) -6585 (xue2) -6586 (xiao4) -6587 (wen2,wen4) -6588 (xue2) -6589 (qi2,ji4,qi4) -658A (qi2,ji4,qi4) -658B (zhai1) -658C (bin1) -658D (jue2) -658E (zhai1) -658F (lang3) -6590 (fei3) -6591 (ban1) -6592 (ban1) -6593 (lan2) -6594 (yu3) -6595 (lan2) -6596 (wei3) -6597 (dou4,dou3) -6598 (sheng1) -6599 (liao4) -659A (jia3) -659B (hu2) -659C (xie2,xia2) -659D (jia3) -659E (yu3) -659F (zhen1) -65A0 (jiao4) -65A1 (wo4) -65A2 (tiao3,tou4) -65A3 (dou4) -65A4 (jin1,jin5) -65A5 (chi4) -65A6 (yin2) -65A7 (fu3) -65A8 (qiang1) -65A9 (zhan3) -65AA (qu2,ju1) -65AB (zhuo2) -65AC (zhan3) -65AD (duan4) -65AE (zhuo2) -65AF (si1) -65B0 (xin1) -65B1 (zhuo2) -65B2 (zhuo2) -65B3 (qin2) -65B4 (lin2) -65B5 (zhuo2) -65B6 (chu4) -65B7 (duan4) -65B8 (zhu3) -65B9 (fang1) -65BA (xie4) -65BB (hang2) -65BC (wu1,yu1,yu2) -65BD (shi1) -65BE (pei4) -65BF (you2) -65C0 (none0) -65C1 (pang2,bang4) -65C2 (qi2) -65C3 (zhan1) -65C4 (mao2,mao4) -65C5 (lu:3) -65C6 (pei4) -65C7 (pi1) -65C8 (liu2) -65C9 (fu1) -65CA (fang3) -65CB (xuan2,xuan4) -65CC (jing1) -65CD (jing1) -65CE (ni3) -65CF (zu2) -65D0 (zhao4) -65D1 (yi3) -65D2 (liu2) -65D3 (shao1) -65D4 (jian4) -65D5 (none0) -65D6 (yi3) -65D7 (qi2) -65D8 (zhi4) -65D9 (fan1) -65DA (piao1) -65DB (fan1) -65DC (zhan1) -65DD (guai4) -65DE (sui4) -65DF (yu2) -65E0 (wu2,mo2) -65E1 (ji4) -65E2 (ji4) -65E3 (ji4) -65E4 (huo4) -65E5 (ri4) -65E6 (dan4) -65E7 (jiu4) -65E8 (zhi3) -65E9 (zao3) -65EA (xie2) -65EB (tiao1) -65EC (xun2) -65ED (xu4) -65EE (ga1) -65EF (la2) -65F0 (gan4) -65F1 (han4) -65F2 (tai2) -65F3 (di4) -65F4 (xu1) -65F5 (chan3) -65F6 (shi2) -65F7 (kuang4) -65F8 (yang2) -65F9 (shi2) -65FA (wang4) -65FB (min2) -65FC (min2) -65FD (tun1) -65FE (chun1) -65FF (wu4) -6600 (yun2) -6601 (bei4) -6602 (ang2) -6603 (ze4) -6604 (ban3) -6605 (jie2) -6606 (kun1) -6607 (sheng1) -6608 (hu4) -6609 (fang3) -660A (hao4) -660B (gui4) -660C (chang1) -660D (xuan1) -660E (ming2) -660F (hun1) -6610 (fen1) -6611 (qin3) -6612 (hu1) -6613 (yi4) -6614 (xi1,xi2) -6615 (xin1) -6616 (yan2) -6617 (ze4) -6618 (fang3) -6619 (tan2) -661A (shen4) -661B (ju4) -661C (yang2) -661D (zan3) -661E (bing3) -661F (xing1) -6620 (ying4) -6621 (xuan4) -6622 (pei3) -6623 (zhen3) -6624 (ling2) -6625 (chun1) -6626 (hao4) -6627 (mei4) -6628 (zuo2) -6629 (mo4) -662A (bian4) -662B (xu4) -662C (hun1) -662D (zhao1) -662E (zong4) -662F (shi4) -6630 (shi4) -6631 (yu4) -6632 (fei4,fu2) -6633 (die2) -6634 (mao3) -6635 (ni4,ni3) -6636 (chang3) -6637 (wen1) -6638 (dong1) -6639 (ai3) -663A (bing3) -663B (ang2) -663C (zhou4) -663D (long2) -663E (xian3) -663F (kuang4) -6640 (tiao3) -6641 (chao2,zhao1,zhao4) -6642 (shi2) -6643 (huang3,huang4) -6644 (huang3,huang4) -6645 (xuan1) -6646 (kui2) -6647 (xu1,kua1) -6648 (jiao3) -6649 (jin4) -664A (zhi3) -664B (jin4) -664C (shang3) -664D (tong2) -664E (hong3) -664F (yan4) -6650 (gai1) -6651 (xiang3) -6652 (shai4) -6653 (xiao3) -6654 (ye4) -6655 (yun1,yun4) -6656 (hui1) -6657 (han2) -6658 (han4) -6659 (jun4) -665A (wan3) -665B (xian4) -665C (kun1) -665D (zhou4) -665E (xi1) -665F (sheng4,cheng2) -6660 (sheng4) -6661 (bu1) -6662 (zhe2,zhe1,zhe5) -6663 (zhe1) -6664 (wu4) -6665 (han4) -6666 (hui4) -6667 (hao4) -6668 (chen2) -6669 (wan3) -666A (tian3) -666B (zhuo1) -666C (zui4) -666D (zhou3) -666E (pu3) -666F (jing3,ying3) -6670 (xi1) -6671 (shan3) -6672 (yi3) -6673 (xi1) -6674 (qing2) -6675 (qi3) -6676 (jing1) -6677 (gui3) -6678 (zhen3) -6679 (yi4) -667A (zhi4) -667B (an3) -667C (wan3) -667D (lin2) -667E (liang4) -667F (chang1) -6680 (wang3) -6681 (xiao3) -6682 (zan4) -6683 (none0) -6684 (xuan1) -6685 (geng4) -6686 (yi2) -6687 (xia2,xia4) -6688 (yun1,yun4) -6689 (hui1) -668A (fu3) -668B (min3,min2) -668C (kui2) -668D (he4) -668E (ying4) -668F (du3) -6690 (wei3) -6691 (shu3) -6692 (qing2) -6693 (mao4) -6694 (nan2) -6695 (jian3) -6696 (nuan3) -6697 (an4) -6698 (yang2) -6699 (chun1) -669A (yao2) -669B (suo3) -669C (pu3) -669D (ming2,ming4) -669E (jiao3) -669F (kai3) -66A0 (gao3) -66A1 (weng3) -66A2 (chang4) -66A3 (qi4) -66A4 (hao4) -66A5 (yan4) -66A6 (li4) -66A7 (ai4) -66A8 (ji4) -66A9 (gui4) -66AA (men3) -66AB (zan4,zhan4) -66AC (xie4) -66AD (hao4) -66AE (mu4) -66AF (mo4) -66B0 (cong1) -66B1 (ni4) -66B2 (zhang1) -66B3 (hui4) -66B4 (bao4,pu4) -66B5 (han4) -66B6 (xuan2) -66B7 (chuan2) -66B8 (liao3) -66B9 (xian1) -66BA (dan4) -66BB (jing3) -66BC (pie1) -66BD (lin2) -66BE (tun1) -66BF (xi3) -66C0 (yi4) -66C1 (ji4) -66C2 (kuang4) -66C3 (dai4) -66C4 (ye4) -66C5 (ye4) -66C6 (li4) -66C7 (tan2) -66C8 (tong2) -66C9 (xiao3) -66CA (fei4) -66CB (qin3) -66CC (zhao4) -66CD (hao4) -66CE (yi4) -66CF (xiang4) -66D0 (xing1) -66D1 (sen1) -66D2 (jiao3) -66D3 (bao4) -66D4 (jing4) -66D5 (none0) -66D6 (ai4) -66D7 (ye4) -66D8 (ru2) -66D9 (shu3,shu4) -66DA (meng2) -66DB (xun1) -66DC (yao4,yue4) -66DD (pu4,bao4) -66DE (li4) -66DF (chen2) -66E0 (kuang4) -66E1 (die2) -66E2 (none0) -66E3 (yan4) -66E4 (huo4) -66E5 (lu2) -66E6 (xi1) -66E7 (rong2) -66E8 (long2) -66E9 (nang3) -66EA (luo3) -66EB (luan2) -66EC (shai4) -66ED (tang3) -66EE (yan3) -66EF (chu2) -66F0 (yue1) -66F1 (yue1) -66F2 (qu3,qu1) -66F3 (ye4,zhuai4,yi4) -66F4 (geng4,geng1) -66F5 (zhuai4) -66F6 (hu1) -66F7 (he2) -66F8 (shu1) -66F9 (cao2) -66FA (cao2) -66FB (sheng1) -66FC (man4) -66FD (ceng2,zeng1) -66FE (ceng2,zeng1) -66FF (ti4) -6700 (zui4) -6701 (can3) -6702 (xu4) -6703 (hui4,hui3,kuai4) -6704 (yin4) -6705 (qie4) -6706 (fen1) -6707 (pi2,bi4) -6708 (yue4) -6709 (you3,you4) -670A (ruan3) -670B (peng2) -670C (ban1) -670D (fu2,fu4) -670E (ling2) -670F (fei3) -6710 (qu2) -6711 (none0) -6712 (nu:4) -6713 (tiao4,tiao3) -6714 (shuo4) -6715 (zhen4) -6716 (lang3) -6717 (lang3) -6718 (juan1,zui1) -6719 (ming2) -671A (huang1) -671B (wang4) -671C (tun1) -671D (chao2,zhao1) -671E (ji1,qi1) -671F (qi1,ji1,qi2) -6720 (ying1) -6721 (zong3) -6722 (wang4) -6723 (tong2) -6724 (lang3) -6725 (none0) -6726 (meng2) -6727 (long2) -6728 (mu4) -6729 (deng3) -672A (wei4) -672B (mo4) -672C (ben3) -672D (zha2) -672E (zhu2) -672F (shu4,zhu2) -6730 (none0) -6731 (zhu1) -6732 (ren2) -6733 (ba1) -6734 (po4,piao2,pu3,po1,pu2) -6735 (duo3) -6736 (duo3) -6737 (dao1) -6738 (li4) -6739 (qiu2) -673A (ji1) -673B (jiu1) -673C (bi3) -673D (xiu3) -673E (ting2) -673F (ci4) -6740 (sha1) -6741 (none0) -6742 (za2) -6743 (quan2) -6744 (qian1) -6745 (yu2) -6746 (gan1,gan3) -6747 (wu1) -6748 (cha1,cha4) -6749 (shan1,sha1) -674A (xun2) -674B (fan2) -674C (wu4) -674D (zi3) -674E (li3) -674F (xing4) -6750 (cai2) -6751 (cun1) -6752 (ren4) -6753 (shao2,biao1) -6754 (zhe2) -6755 (di4) -6756 (zhang4) -6757 (mang2) -6758 (chi4) -6759 (yi4) -675A (gu3) -675B (gong1) -675C (du4) -675D (yi2) -675E (qi3) -675F (shu4) -6760 (gang1,gang4) -6761 (tiao2) -6762 (none0) -6763 (none0) -6764 (none0) -6765 (lai2) -6766 (shan1) -6767 (mang2) -6768 (yang2) -6769 (ma4) -676A (miao3) -676B (si4) -676C (yuan2) -676D (hang2) -676E (fei4) -676F (bei1) -6770 (jie2) -6771 (dong1) -6772 (gao3) -6773 (yao3,miao3) -6774 (xian1) -6775 (chu3) -6776 (chun1) -6777 (pa2,ba5) -6778 (shu1) -6779 (hua4) -677A (xin2) -677B (chou3,niu3) -677C (zhu4) -677D (chou3) -677E (song1) -677F (ban3) -6780 (song1) -6781 (ji2) -6782 (yue4) -6783 (yun2) -6784 (gou4) -6785 (ji1) -6786 (mao2) -6787 (pi2) -6788 (bi4) -6789 (wang3) -678A (ang4) -678B (fang1) -678C (fen2) -678D (yi4) -678E (fu2) -678F (nan2) -6790 (xi1) -6791 (hu4) -6792 (ya2) -6793 (dou3) -6794 (xun2) -6795 (zhen3,zhen4) -6796 (yao3) -6797 (lin2) -6798 (rui4) -6799 (e4) -679A (mei2) -679B (zhao4) -679C (guo3) -679D (zhi1,qi2) -679E (zong1,cong1) -679F (yun4) -67A0 (none0) -67A1 (dou3) -67A2 (shu1) -67A3 (zao3) -67A4 (none0) -67A5 (li4) -67A6 (lu2) -67A7 (jian3) -67A8 (cheng2) -67A9 (song1) -67AA (qiang1) -67AB (feng1) -67AC (nan2) -67AD (xiao1) -67AE (xian1) -67AF (ku1) -67B0 (ping2) -67B1 (tai2) -67B2 (xi3) -67B3 (zhi3,zhi1) -67B4 (guai3) -67B5 (xiao1) -67B6 (jia4) -67B7 (jia1) -67B8 (gou3,gou1,ju3) -67B9 (bao1,fu2) -67BA (mo4) -67BB (yi4) -67BC (ye4) -67BD (sang1) -67BE (shi4) -67BF (nie4) -67C0 (bi3) -67C1 (tuo2,duo4,duo1) -67C2 (yi2) -67C3 (ling2) -67C4 (bing3,bing4) -67C5 (ni3) -67C6 (la1) -67C7 (he2) -67C8 (ban4) -67C9 (fan2,bian1) -67CA (zhong1) -67CB (dai4) -67CC (ci2) -67CD (yang1) -67CE (fu1) -67CF (bo2,bai3,bo4) -67D0 (mou3) -67D1 (gan1) -67D2 (qi1) -67D3 (ran3) -67D4 (rou2) -67D5 (mao4) -67D6 (zhao1) -67D7 (song1) -67D8 (zhe4) -67D9 (xia2) -67DA (you4,you2) -67DB (shen1) -67DC (ju3,gui4) -67DD (tuo4) -67DE (zuo4,zha4) -67DF (nan2) -67E0 (ning2) -67E1 (yong3) -67E2 (di3) -67E3 (zhi2) -67E4 (zha1) -67E5 (cha2,zha1) -67E6 (dan4) -67E7 (gu1) -67E8 (none0) -67E9 (jiu4) -67EA (ao1) -67EB (fu2,bi4) -67EC (jian3) -67ED (bo1) -67EE (duo4) -67EF (ke1) -67F0 (nai4) -67F1 (zhu4) -67F2 (bi4) -67F3 (liu3) -67F4 (chai2) -67F5 (zha4) -67F6 (si4) -67F7 (zhu4) -67F8 (pei1) -67F9 (shi4) -67FA (guai3) -67FB (cha2,zha1) -67FC (yao2) -67FD (cheng1) -67FE (jiu4) -67FF (shi4) -6800 (zhi1) -6801 (liu3) -6802 (mei2) -6803 (none0) -6804 (rong2) -6805 (zha4,shan1) -6806 (none0) -6807 (biao1) -6808 (zhan4) -6809 (zhi4) -680A (long2) -680B (dong4) -680C (lu2) -680D (none0) -680E (li4,yue4) -680F (lan2) -6810 (yong3) -6811 (shu4) -6812 (xun2) -6813 (shuan1) -6814 (qi4) -6815 (zhen1) -6816 (qi1,xi1) -6817 (li4) -6818 (chi2,yi2) -6819 (xiang2) -681A (zhen4) -681B (li4) -681C (su4) -681D (gua1,kuo4) -681E (kan1) -681F (bing1,ben1) -6820 (ren3) -6821 (xiao4,jiao4) -6822 (bo2,bai3) -6823 (ren3) -6824 (bing4) -6825 (zi1) -6826 (chou2) -6827 (yi4) -6828 (ci4) -6829 (xu3) -682A (zhu1) -682B (jian4) -682C (zui4) -682D (er2) -682E (er3) -682F (yu4) -6830 (fa2) -6831 (gong3) -6832 (kao3) -6833 (lao3) -6834 (zhan1) -6835 (li4) -6836 (none0) -6837 (yang4) -6838 (he2,hu2) -6839 (gen1) -683A (zhi3) -683B (chi4) -683C (ge2) -683D (zai1) -683E (luan2) -683F (fa2) -6840 (jie2) -6841 (heng2,hang2) -6842 (gui4) -6843 (tao2) -6844 (guang4,guang1) -6845 (wei2) -6846 (kuang4,kuang1) -6847 (ru2) -6848 (an4) -6849 (an1) -684A (juan4) -684B (yi2) -684C (zhuo1) -684D (ku1) -684E (zhi4,zhi2) -684F (qiong2) -6850 (tong2) -6851 (sang1) -6852 (sang1) -6853 (huan2) -6854 (jie2,ju2) -6855 (jiu4) -6856 (xue4) -6857 (duo4) -6858 (zhui1) -6859 (yu2) -685A (zan3) -685B (none0) -685C (ying1) -685D (none0) -685E (none0) -685F (zhan4) -6860 (ya1) -6861 (rao2) -6862 (zhen1) -6863 (dang4) -6864 (qi1) -6865 (qiao2) -6866 (hua4) -6867 (gui4,hui4) -6868 (jiang3) -6869 (zhuang1) -686A (xun2) -686B (suo1) -686C (suo1) -686D (zhen4) -686E (bei1) -686F (ting1) -6870 (kuo4) -6871 (jing4) -6872 (bo2) -6873 (ben4) -6874 (fu2) -6875 (rui3) -6876 (tong3) -6877 (jue2) -6878 (xi1) -6879 (lang2) -687A (liu3) -687B (feng1) -687C (qi1) -687D (wen3) -687E (jun1) -687F (gan3) -6880 (cu4) -6881 (liang2) -6882 (qiu2) -6883 (ting3,ting4) -6884 (you3) -6885 (mei2) -6886 (bang1) -6887 (long4) -6888 (peng1) -6889 (zhuang1) -688A (di4) -688B (xuan1) -688C (tu2) -688D (zao4) -688E (ao1) -688F (gu4) -6890 (bi4) -6891 (di2) -6892 (han2) -6893 (zi3) -6894 (zhi1) -6895 (ren4) -6896 (bei4) -6897 (geng3) -6898 (jian3) -6899 (huan4) -689A (wan3) -689B (nuo2) -689C (jia2) -689D (tiao2) -689E (ji4) -689F (xiao1) -68A0 (lu:3) -68A1 (kuan3) -68A2 (shao1,sao4) -68A3 (cen2) -68A4 (fen1) -68A5 (song1) -68A6 (meng4) -68A7 (wu2) -68A8 (li2) -68A9 (li2) -68AA (dou4) -68AB (cen1) -68AC (ying3) -68AD (suo1) -68AE (ju2) -68AF (ti1) -68B0 (xie4) -68B1 (kun3) -68B2 (zhuo2) -68B3 (shu1) -68B4 (chan1) -68B5 (fan4) -68B6 (wei3) -68B7 (jing4) -68B8 (li2) -68B9 (bing1,bin1) -68BA (none0) -68BB (none0) -68BC (tao2) -68BD (zhi4) -68BE (lai2) -68BF (lian2) -68C0 (jian3) -68C1 (zhuo1) -68C2 (ling2) -68C3 (li2) -68C4 (qi4) -68C5 (bing3) -68C6 (lun2) -68C7 (cong1) -68C8 (qian4) -68C9 (mian2) -68CA (qi2) -68CB (qi2) -68CC (cai3) -68CD (gun4) -68CE (chan2) -68CF (de2) -68D0 (fei3) -68D1 (pai2) -68D2 (bang4) -68D3 (pou3,bang4) -68D4 (hun1) -68D5 (zong1) -68D6 (cheng2) -68D7 (zao3) -68D8 (ji2) -68D9 (li4) -68DA (peng2) -68DB (yu4) -68DC (yu4) -68DD (gu4) -68DE (hun2) -68DF (dong4) -68E0 (tang2) -68E1 (gang1) -68E2 (wang3) -68E3 (di4) -68E4 (xi2) -68E5 (fan2) -68E6 (cheng1) -68E7 (zhan4) -68E8 (qi3) -68E9 (yuan1) -68EA (yan3) -68EB (yu4) -68EC (quan1) -68ED (yi4) -68EE (sen1) -68EF (ren3) -68F0 (chui2) -68F1 (leng2,ling2,leng1) -68F2 (qi1,xi1) -68F3 (zhuo2) -68F4 (fu2) -68F5 (ke1) -68F6 (lai2) -68F7 (zou1) -68F8 (zou1) -68F9 (zhao4,zhuo1) -68FA (guan1) -68FB (fen1) -68FC (fen2) -68FD (chen1) -68FE (qiong2) -68FF (nie4) -6900 (wan3) -6901 (guo3) -6902 (lu4) -6903 (hao2) -6904 (jie1) -6905 (yi3,yi1) -6906 (chou2) -6907 (ju3) -6908 (ju2) -6909 (cheng2,sheng4) -690A (zuo2) -690B (liang2) -690C (qiang1) -690D (zhi2) -690E (zhui1,chui2) -690F (ya1) -6910 (ju1) -6911 (bei1,pi2) -6912 (jiao1) -6913 (zhuo2) -6914 (zi1) -6915 (bin1) -6916 (peng2) -6917 (ding4) -6918 (chu3) -6919 (shan1) -691A (none0) -691B (none0) -691C (jian3) -691D (gui1) -691E (xi4) -691F (du2) -6920 (qian4) -6921 (none0) -6922 (kui4) -6923 (none0) -6924 (luo2) -6925 (zhi1) -6926 (none0) -6927 (none0) -6928 (none0) -6929 (none0) -692A (peng4) -692B (shan4) -692C (none0) -692D (tuo3) -692E (sen1) -692F (duo2) -6930 (ye1,ye2) -6931 (fu4) -6932 (wei3) -6933 (wei1) -6934 (duan4) -6935 (jia3) -6936 (zong1) -6937 (jian1) -6938 (yi2) -6939 (shen4,zhen1,zhen4) -693A (xi2) -693B (yan4) -693C (yan3) -693D (chuan2) -693E (zhan4) -693F (chun1) -6940 (yu3,ju3) -6941 (he2) -6942 (zha1,cha2) -6943 (wo4) -6944 (bian1) -6945 (bi4) -6946 (yao1) -6947 (huo4) -6948 (xu1) -6949 (ruo4) -694A (yang2) -694B (la4) -694C (yan2) -694D (ben3) -694E (hun2) -694F (kui2) -6950 (jie4) -6951 (kui2) -6952 (si1) -6953 (feng1) -6954 (xie1,xie4) -6955 (tuo3) -6956 (ji2) -6957 (jian4) -6958 (mu4) -6959 (mao4) -695A (chu3) -695B (hu4,ku3) -695C (hu2) -695D (lian4) -695E (leng2,leng4) -695F (ting2) -6960 (nan2) -6961 (yu2) -6962 (you2) -6963 (mei2) -6964 (song3) -6965 (xuan4) -6966 (xuan4) -6967 (ying1) -6968 (zhen1) -6969 (pian2) -696A (die2) -696B (ji2) -696C (jie2) -696D (ye4) -696E (chu3) -696F (shun3,dun4) -6970 (yu2) -6971 (cou4) -6972 (wei1) -6973 (mei2) -6974 (di4) -6975 (ji2) -6976 (jie2) -6977 (kai3,jie1) -6978 (qiu1) -6979 (ying2) -697A (rou2) -697B (heng2) -697C (lou2) -697D (le4,yue4) -697E (none0) -697F (gui4) -6980 (pin3) -6981 (none0) -6982 (gai4) -6983 (tan2) -6984 (lan3) -6985 (yun2) -6986 (yu2) -6987 (chen4) -6988 (lu:2) -6989 (ju3) -698A (none0) -698B (none0) -698C (none0) -698D (xie4) -698E (jia3) -698F (yi4) -6990 (zhan3) -6991 (fu4) -6992 (nuo4) -6993 (mi4) -6994 (lang2) -6995 (rong2) -6996 (gu3) -6997 (jian4) -6998 (ju4) -6999 (ta3) -699A (yao3) -699B (zhen1) -699C (bang3) -699D (sha1) -699E (yuan2) -699F (zi3) -69A0 (ming2) -69A1 (su4) -69A2 (jia4) -69A3 (yao2) -69A4 (jie2) -69A5 (huang3) -69A6 (gan4,han2) -69A7 (fei3) -69A8 (zha4) -69A9 (qian2) -69AA (ma4) -69AB (sun3) -69AC (yuan2) -69AD (xie4) -69AE (rong2) -69AF (shi2) -69B0 (zhi1) -69B1 (cui1) -69B2 (yun2) -69B3 (ting2) -69B4 (liu2) -69B5 (rong2) -69B6 (tang2) -69B7 (que4) -69B8 (zhai1) -69B9 (si1) -69BA (sheng4) -69BB (ta4) -69BC (ke4,ke2) -69BD (xi1) -69BE (gu4) -69BF (qi1) -69C0 (kao3) -69C1 (gao3) -69C2 (sun1) -69C3 (pan2) -69C4 (tao1) -69C5 (ge2) -69C6 (xun2) -69C7 (dian1,zhen3) -69C8 (nou4) -69C9 (ji2) -69CA (shuo4) -69CB (gou4) -69CC (chui2) -69CD (qiang1) -69CE (cha2) -69CF (qian3) -69D0 (huai2) -69D1 (mei2) -69D2 (xu4) -69D3 (gang4) -69D4 (gao1) -69D5 (zhuo2) -69D6 (tuo2) -69D7 (qiao2) -69D8 (yang4) -69D9 (dian1) -69DA (jia3) -69DB (jian4,kan3) -69DC (zui4) -69DD (none0) -69DE (long2) -69DF (bin1,bing1) -69E0 (zhu1) -69E1 (none0) -69E2 (xi2) -69E3 (qi3) -69E4 (lian2) -69E5 (hui4) -69E6 (yong2) -69E7 (qian4) -69E8 (guo3) -69E9 (gai4) -69EA (gai4) -69EB (tuan2) -69EC (hua4) -69ED (qi1,cu4,qi4) -69EE (sen1) -69EF (cui1) -69F0 (beng4) -69F1 (you3) -69F2 (hu2) -69F3 (jiang3) -69F4 (hu4) -69F5 (huan4) -69F6 (kui4) -69F7 (yi4) -69F8 (yi4) -69F9 (gao1) -69FA (kang1) -69FB (gui1) -69FC (gui1) -69FD (cao2) -69FE (man2,man4) -69FF (jin3) -6A00 (di4) -6A01 (zhuang1) -6A02 (le4,yue4) -6A03 (lang3) -6A04 (chen2) -6A05 (cong1,zong1) -6A06 (li2) -6A07 (xiu1) -6A08 (qing2) -6A09 (shuang3) -6A0A (fan2) -6A0B (tong1) -6A0C (guan4) -6A0D (ji1) -6A0E (suo1) -6A0F (lei3) -6A10 (lu3) -6A11 (liang2) -6A12 (mi4) -6A13 (lou2) -6A14 (chao2) -6A15 (su4) -6A16 (ke1) -6A17 (chu1,shu1) -6A18 (tang2) -6A19 (biao1) -6A1A (lu4) -6A1B (jiu1) -6A1C (shu4) -6A1D (zha1) -6A1E (shu1) -6A1F (zhang1) -6A20 (men2) -6A21 (mo2,mu2) -6A22 (niao3) -6A23 (yang4) -6A24 (tiao2) -6A25 (peng2) -6A26 (zhu4) -6A27 (sha1) -6A28 (xi1) -6A29 (quan2) -6A2A (heng2,heng4) -6A2B (jian1) -6A2C (cong1) -6A2D (none0) -6A2E (none0) -6A2F (qiang2) -6A30 (none0) -6A31 (ying1) -6A32 (er4) -6A33 (xin2) -6A34 (zhi2) -6A35 (qiao2) -6A36 (zui1) -6A37 (cong2) -6A38 (pu2,po4,pu3) -6A39 (shu4) -6A3A (hua4,hua2) -6A3B (kui4) -6A3C (zhen1) -6A3D (zun1) -6A3E (yue4) -6A3F (zhan3) -6A40 (xi1) -6A41 (xun2) -6A42 (dian4) -6A43 (fa1) -6A44 (gan3) -6A45 (mo2,mu2) -6A46 (wu3) -6A47 (qiao1,cui4) -6A48 (rao2,nao2) -6A49 (lin4) -6A4A (liu2) -6A4B (qiao2) -6A4C (xian4) -6A4D (run4) -6A4E (fan2) -6A4F (zhan3) -6A50 (tuo2) -6A51 (lao3) -6A52 (yun2) -6A53 (shun4) -6A54 (tui2) -6A55 (cheng1) -6A56 (tang2) -6A57 (meng2) -6A58 (ju2) -6A59 (cheng2,chen2) -6A5A (su4) -6A5B (jue2) -6A5C (jue2) -6A5D (tan2) -6A5E (hui4) -6A5F (ji1) -6A60 (nuo3) -6A61 (xiang4) -6A62 (tuo3) -6A63 (ning3) -6A64 (rui3) -6A65 (zhu1) -6A66 (tong2,chuang2) -6A67 (zeng1) -6A68 (fen4) -6A69 (qiong2) -6A6A (ran3) -6A6B (heng2,heng4) -6A6C (cen2) -6A6D (gu1,ku1) -6A6E (liu3) -6A6F (lao4) -6A70 (gao1) -6A71 (chu2) -6A72 (none0) -6A73 (none0) -6A74 (none0) -6A75 (none0) -6A76 (ji2) -6A77 (dou1) -6A78 (none0) -6A79 (lu3) -6A7A (none0) -6A7B (none0) -6A7C (yuan2) -6A7D (ta4) -6A7E (shu1) -6A7F (jiang1) -6A80 (tan2) -6A81 (lin3) -6A82 (nong2) -6A83 (yin3) -6A84 (xi2) -6A85 (sui4) -6A86 (shan1) -6A87 (zui4) -6A88 (xuan2) -6A89 (cheng1) -6A8A (gan4) -6A8B (ju4) -6A8C (zui4) -6A8D (yi4) -6A8E (qin2) -6A8F (pu3) -6A90 (yan2,yin2) -6A91 (lei2) -6A92 (feng1) -6A93 (hui3) -6A94 (dang3) -6A95 (ji4) -6A96 (sui4) -6A97 (bo4,bo2) -6A98 (bi4) -6A99 (ding3) -6A9A (chu3) -6A9B (zhua1) -6A9C (gui4,hui4,kuai4) -6A9D (ji4) -6A9E (jia3) -6A9F (jia3) -6AA0 (qing2) -6AA1 (zhe4) -6AA2 (jian3) -6AA3 (qiang2) -6AA4 (dao4) -6AA5 (yi3) -6AA6 (biao3) -6AA7 (song1) -6AA8 (she1) -6AA9 (lin3) -6AAA (li4,yue4) -6AAB (cha2) -6AAC (meng2) -6AAD (yin2) -6AAE (tao2) -6AAF (tai2) -6AB0 (mian2) -6AB1 (qi2) -6AB2 (none0) -6AB3 (bin1,bing1) -6AB4 (huo4) -6AB5 (ji4) -6AB6 (qian1) -6AB7 (mi2,ni3) -6AB8 (ning2) -6AB9 (yi1) -6ABA (gao3) -6ABB (jian4,kan3) -6ABC (yin4) -6ABD (er2) -6ABE (qing3) -6ABF (yan3) -6AC0 (qi2) -6AC1 (mi4) -6AC2 (zhao4) -6AC3 (gui4,ju3) -6AC4 (chun1) -6AC5 (ji1) -6AC6 (kui2) -6AC7 (po2) -6AC8 (deng4) -6AC9 (chu2) -6ACA (none0) -6ACB (mian2) -6ACC (you1) -6ACD (zhi4) -6ACE (guang4) -6ACF (qian1) -6AD0 (lei3) -6AD1 (lei2,lei3) -6AD2 (sa4) -6AD3 (lu3) -6AD4 (none0) -6AD5 (cuan2) -6AD6 (lu:2) -6AD7 (mie4) -6AD8 (hui4) -6AD9 (ou1) -6ADA (lu:2) -6ADB (zhi4,jie2) -6ADC (gao1) -6ADD (du2) -6ADE (yuan2) -6ADF (li4,yue4) -6AE0 (fei4) -6AE1 (zhu4) -6AE2 (sou3) -6AE3 (lian2) -6AE4 (none0) -6AE5 (chu2) -6AE6 (none0) -6AE7 (zhu1) -6AE8 (lu2) -6AE9 (yan2) -6AEA (li4) -6AEB (zhu1) -6AEC (chen4) -6AED (jie2) -6AEE (e4) -6AEF (su1) -6AF0 (huai2) -6AF1 (nie4) -6AF2 (yu4) -6AF3 (long2) -6AF4 (lai4) -6AF5 (none0) -6AF6 (xian3) -6AF7 (none0) -6AF8 (ju3) -6AF9 (xiao1) -6AFA (ling2) -6AFB (ying1) -6AFC (jian1) -6AFD (yin3) -6AFE (you2) -6AFF (ying2) -6B00 (xiang1) -6B01 (nong2) -6B02 (bo2) -6B03 (chan1) -6B04 (lan2) -6B05 (ju3) -6B06 (shuang1) -6B07 (she4) -6B08 (wei2) -6B09 (cong4) -6B0A (quan2) -6B0B (qu2) -6B0C (none0) -6B0D (none0) -6B0E (yu4) -6B0F (luo2) -6B10 (li4) -6B11 (zan4) -6B12 (luan2) -6B13 (dang3) -6B14 (jue2) -6B15 (none0) -6B16 (lan3) -6B17 (lan2) -6B18 (zhu3) -6B19 (lei2) -6B1A (li3,ji1) -6B1B (ba4,ba3) -6B1C (nang2) -6B1D (yu4) -6B1E (ling2) -6B1F (none0) -6B20 (qian4,qian5) -6B21 (ci4) -6B22 (huan1) -6B23 (xin1) -6B24 (yu2) -6B25 (yu4) -6B26 (qian1) -6B27 (ou1) -6B28 (xu1) -6B29 (chao1) -6B2A (chu4) -6B2B (qi4) -6B2C (kai4) -6B2D (yi4) -6B2E (jue2) -6B2F (xi2) -6B30 (xu1) -6B31 (xia4) -6B32 (yu4) -6B33 (kuai4) -6B34 (lang2) -6B35 (kuan3) -6B36 (shuo4) -6B37 (xi1) -6B38 (e^1,e^2,e^3,e^4,ai3,ai4) -6B39 (yi1,qi1) -6B3A (qi1) -6B3B (hu1) -6B3C (chi3) -6B3D (qin1) -6B3E (kuan3) -6B3F (kan3) -6B40 (kuan3) -6B41 (kan3) -6B42 (chuan2) -6B43 (sha4) -6B44 (none0) -6B45 (yin1) -6B46 (xin1) -6B47 (xie1) -6B48 (yu2) -6B49 (qian4) -6B4A (xiao1) -6B4B (yi2) -6B4C (ge1) -6B4D (wu1) -6B4E (tan4) -6B4F (jin4) -6B50 (ou1) -6B51 (hu1) -6B52 (ti4) -6B53 (huan1) -6B54 (xu1) -6B55 (pen1) -6B56 (xi1) -6B57 (xiao4) -6B58 (hu1) -6B59 (she4,xi1,xi4) -6B5A (none0) -6B5B (lian4) -6B5C (chu4) -6B5D (yi4) -6B5E (kan3) -6B5F (yu2) -6B60 (chuo4) -6B61 (huan1) -6B62 (zhi3) -6B63 (zheng4,zheng1) -6B64 (ci3) -6B65 (bu4) -6B66 (wu3) -6B67 (qi2) -6B68 (bu4) -6B69 (bu4) -6B6A (wai1) -6B6B (ju4) -6B6C (qian2) -6B6D (chi2) -6B6E (se4) -6B6F (chi3) -6B70 (se4) -6B71 (zhong3) -6B72 (sui4) -6B73 (sui4) -6B74 (li4) -6B75 (cuo4) -6B76 (yu2) -6B77 (li4) -6B78 (gui1) -6B79 (dai3) -6B7A (dai3) -6B7B (si3) -6B7C (jian1) -6B7D (zhe2) -6B7E (mo4) -6B7F (mo4) -6B80 (yao3) -6B81 (mo4) -6B82 (cu2) -6B83 (yang1) -6B84 (tian3) -6B85 (sheng1) -6B86 (dai4) -6B87 (shang1) -6B88 (xu1) -6B89 (xun4) -6B8A (shu1) -6B8B (can2) -6B8C (jue2) -6B8D (piao3) -6B8E (qia4) -6B8F (qiu2) -6B90 (su4) -6B91 (qing2) -6B92 (yun3) -6B93 (lian4) -6B94 (yi4) -6B95 (fou3) -6B96 (zhi2,shi5) -6B97 (ye4) -6B98 (can2) -6B99 (hun1) -6B9A (dan1) -6B9B (ji2) -6B9C (ye4) -6B9D (none0) -6B9E (yun3) -6B9F (wen1) -6BA0 (chou4,xiu4) -6BA1 (bin4) -6BA2 (ti4) -6BA3 (jin4) -6BA4 (shang1) -6BA5 (yin2) -6BA6 (diao1) -6BA7 (cu4) -6BA8 (hui4) -6BA9 (cuan4) -6BAA (yi4) -6BAB (dan1,dan4) -6BAC (du4) -6BAD (jiang1) -6BAE (lian4) -6BAF (bin4) -6BB0 (du2) -6BB1 (jian1) -6BB2 (jian1) -6BB3 (shu1) -6BB4 (ou1) -6BB5 (duan4) -6BB6 (zhu4) -6BB7 (yin1,yan1,yin3) -6BB8 (qing4) -6BB9 (yi1) -6BBA (sha1,shai4) -6BBB (ke2,qiao4) -6BBC (ke2,qiao4,que4) -6BBD (yao2) -6BBE (xun4) -6BBF (dian4) -6BC0 (hui3) -6BC1 (hui3) -6BC2 (gu3,gu1) -6BC3 (que4) -6BC4 (ji1) -6BC5 (yi4) -6BC6 (ou1) -6BC7 (hui3) -6BC8 (duan4) -6BC9 (yi1) -6BCA (xiao1) -6BCB (wu2) -6BCC (guan4) -6BCD (mu3) -6BCE (mei3) -6BCF (mei3) -6BD0 (ai3) -6BD1 (zuo3) -6BD2 (du2) -6BD3 (yu4) -6BD4 (bi3,bi4) -6BD5 (bi4) -6BD6 (bi4) -6BD7 (pi2) -6BD8 (pi2) -6BD9 (bi4) -6BDA (chan2) -6BDB (mao2) -6BDC (none0) -6BDD (none0) -6BDE (pi2) -6BDF (none0) -6BE0 (jia1) -6BE1 (zhan1) -6BE2 (sai1) -6BE3 (mu4) -6BE4 (tuo4) -6BE5 (xun2) -6BE6 (er4) -6BE7 (rong2) -6BE8 (xian3) -6BE9 (ju2) -6BEA (mu2) -6BEB (hao2) -6BEC (qiu2) -6BED (dou4) -6BEE (none0) -6BEF (tan3) -6BF0 (pei2) -6BF1 (ju1) -6BF2 (duo2) -6BF3 (cui4) -6BF4 (bi1) -6BF5 (san1) -6BF6 (none0) -6BF7 (mao4) -6BF8 (sui1) -6BF9 (shu1) -6BFA (yu1) -6BFB (tuo4) -6BFC (he2) -6BFD (jian4) -6BFE (ta4) -6BFF (san1) -6C00 (lu:2) -6C01 (mu2) -6C02 (li2) -6C03 (tong2) -6C04 (rong3) -6C05 (chang3) -6C06 (pu3) -6C07 (lu3,lu5) -6C08 (zhan1) -6C09 (sao4) -6C0A (zhan1) -6C0B (meng2) -6C0C (lu3) -6C0D (qu2) -6C0E (die2) -6C0F (shi4,zhi1) -6C10 (di3,di1) -6C11 (min2) -6C12 (jue2) -6C13 (mang2,meng2) -6C14 (qi4) -6C15 (pie1) -6C16 (nai3) -6C17 (qi4) -6C18 (dao1) -6C19 (xian1) -6C1A (chuan1) -6C1B (fen1) -6C1C (ri4) -6C1D (nei4,nai3) -6C1E (none0) -6C1F (fu2) -6C20 (shen1) -6C21 (dong1) -6C22 (qing1) -6C23 (qi4) -6C24 (yin1) -6C25 (xi1) -6C26 (hai4) -6C27 (yang3) -6C28 (an1) -6C29 (ya4) -6C2A (ke4) -6C2B (qing1) -6C2C (ya4) -6C2D (dong1) -6C2E (dan4) -6C2F (lu:4) -6C30 (qing2) -6C31 (yang3) -6C32 (yun1) -6C33 (yun1) -6C34 (shui3) -6C35 (shui3) -6C36 (zheng3) -6C37 (bing1) -6C38 (yong3) -6C39 (dang4) -6C3A (shui3) -6C3B (le4) -6C3C (ni4) -6C3D (tun3) -6C3E (fan4) -6C3F (gui3) -6C40 (ting1) -6C41 (zhi1) -6C42 (qiu2) -6C43 (bin1) -6C44 (ze4) -6C45 (mian3) -6C46 (cuan1) -6C47 (hui4) -6C48 (diao1) -6C49 (han4) -6C4A (cha4) -6C4B (zhuo2) -6C4C (chuan4) -6C4D (wan2) -6C4E (fan4) -6C4F (dai4) -6C50 (xi1,xi4) -6C51 (tuo1) -6C52 (mang3) -6C53 (qiu2) -6C54 (qi4) -6C55 (shan4) -6C56 (pai4) -6C57 (han4,han2) -6C58 (qian1) -6C59 (wu1) -6C5A (wu1) -6C5B (xun4) -6C5C (si4) -6C5D (ru3) -6C5E (gong3,hong4) -6C5F (jiang1) -6C60 (chi2) -6C61 (wu1) -6C62 (none0) -6C63 (none0) -6C64 (tang1,shang1) -6C65 (zhi1) -6C66 (chi2) -6C67 (qian1) -6C68 (mi4) -6C69 (gu3) -6C6A (wang1) -6C6B (qing4) -6C6C (jing3) -6C6D (rui4) -6C6E (jun1) -6C6F (hong2) -6C70 (tai4) -6C71 (quan3) -6C72 (ji2) -6C73 (bian4) -6C74 (bian4) -6C75 (gan4) -6C76 (wen4) -6C77 (zhong1) -6C78 (fang1) -6C79 (xiong1) -6C7A (jue2) -6C7B (hu3) -6C7C (none0) -6C7D (qi4) -6C7E (fen2) -6C7F (xu4) -6C80 (xu4) -6C81 (qin4,shen4) -6C82 (yi2) -6C83 (wo4) -6C84 (yun2) -6C85 (yuan2) -6C86 (hang4) -6C87 (yan3) -6C88 (shen3,chen2) -6C89 (chen2) -6C8A (dan4) -6C8B (you2) -6C8C (dun4,zhuan4) -6C8D (hu4) -6C8E (huo4) -6C8F (qi1) -6C90 (mu4) -6C91 (rou2) -6C92 (mei2,mo4) -6C93 (ta4,da2,ta5) -6C94 (mian3) -6C95 (wu4) -6C96 (chong1) -6C97 (tian1) -6C98 (bi3) -6C99 (sha1,sha4) -6C9A (zhi3) -6C9B (pei4) -6C9C (pan4) -6C9D (zhui3) -6C9E (za1) -6C9F (gou1) -6CA0 (liu2) -6CA1 (mei2,mo4) -6CA2 (ze2) -6CA3 (feng1) -6CA4 (ou4,ou1) -6CA5 (li4) -6CA6 (lun2) -6CA7 (cang1) -6CA8 (feng1) -6CA9 (wei2) -6CAA (hu4) -6CAB (mo4) -6CAC (mei4) -6CAD (shu4) -6CAE (ju3,ju4,ju1) -6CAF (zan3) -6CB0 (tuo1) -6CB1 (tuo2) -6CB2 (duo4) -6CB3 (he2) -6CB4 (li4) -6CB5 (mi3) -6CB6 (yi2) -6CB7 (fu2) -6CB8 (fei4) -6CB9 (you2) -6CBA (tian2) -6CBB (zhi4) -6CBC (zhao3) -6CBD (gu1) -6CBE (zhan1) -6CBF (yan2,yan4) -6CC0 (si1) -6CC1 (kuang4) -6CC2 (jiong3) -6CC3 (ju1) -6CC4 (xie4) -6CC5 (qiu2) -6CC6 (yi4) -6CC7 (jia1) -6CC8 (zhong1) -6CC9 (quan2) -6CCA (bo2,po1,po4) -6CCB (hui4) -6CCC (mi4,bi4) -6CCD (ben1) -6CCE (zhuo2) -6CCF (chu4) -6CD0 (le4) -6CD1 (you3) -6CD2 (gu1) -6CD3 (hong2) -6CD4 (gan1) -6CD5 (fa3) -6CD6 (mao3) -6CD7 (si4) -6CD8 (hu1) -6CD9 (ping2) -6CDA (ci3) -6CDB (fan4,fan2) -6CDC (zhi1) -6CDD (su4) -6CDE (ning4) -6CDF (cheng1) -6CE0 (ling2) -6CE1 (pao4,pao1) -6CE2 (bo1,po1) -6CE3 (qi4,xie4) -6CE4 (si4) -6CE5 (ni2,ni4) -6CE6 (ju2) -6CE7 (yue4) -6CE8 (zhu4) -6CE9 (sheng1) -6CEA (lei4) -6CEB (xuan4) -6CEC (xue4) -6CED (fu1) -6CEE (pan4) -6CEF (min3) -6CF0 (tai4) -6CF1 (yang1) -6CF2 (ji3) -6CF3 (yong3) -6CF4 (guan4) -6CF5 (beng4) -6CF6 (xue2) -6CF7 (long2,shuang1) -6CF8 (lu2) -6CF9 (dan4) -6CFA (luo4,po1) -6CFB (xie4) -6CFC (po1) -6CFD (ze2) -6CFE (jing1) -6CFF (yin2) -6D00 (zhou1) -6D01 (jie2) -6D02 (yi4) -6D03 (hui1) -6D04 (hui2) -6D05 (zui3) -6D06 (cheng2) -6D07 (yin1) -6D08 (wei2) -6D09 (hou4) -6D0A (jian4) -6D0B (yang2) -6D0C (lie4) -6D0D (si4) -6D0E (ji4) -6D0F (er2) -6D10 (xing2) -6D11 (fu2,fu4) -6D12 (sa3) -6D13 (zi4) -6D14 (zhi3) -6D15 (yin1) -6D16 (wu2) -6D17 (xi3,xian3) -6D18 (kao3) -6D19 (zhu1) -6D1A (jiang4) -6D1B (luo4) -6D1C (none0) -6D1D (an4) -6D1E (dong4) -6D1F (yi2) -6D20 (mou2) -6D21 (lei3) -6D22 (yi1) -6D23 (mi3) -6D24 (quan2) -6D25 (jin1) -6D26 (po4) -6D27 (wei3) -6D28 (xiao2) -6D29 (xie4) -6D2A (hong2) -6D2B (xu4) -6D2C (su4) -6D2D (kuang1) -6D2E (tao2) -6D2F (qie4,jie2) -6D30 (ju4) -6D31 (er3) -6D32 (zhou1) -6D33 (ru4) -6D34 (ping2) -6D35 (xun2) -6D36 (xiong1) -6D37 (zhi4) -6D38 (guang1,huang3) -6D39 (huan2) -6D3A (ming2) -6D3B (huo2) -6D3C (wa1) -6D3D (qia4,xia2) -6D3E (pai4,pa1) -6D3F (wu1) -6D40 (qu3) -6D41 (liu2) -6D42 (yi4) -6D43 (jia1) -6D44 (jing4) -6D45 (qian3,jian1) -6D46 (jiang1,jiang4) -6D47 (jiao1) -6D48 (zhen1) -6D49 (shi1) -6D4A (zhuo2) -6D4B (ce4) -6D4C (none0) -6D4D (hui4,kuai4) -6D4E (ji4,ji3) -6D4F (liu2) -6D50 (chan3) -6D51 (hun2) -6D52 (hu3,xu3) -6D53 (nong2) -6D54 (xun2) -6D55 (jin4) -6D56 (lie4) -6D57 (qiu2) -6D58 (wei3) -6D59 (zhe4) -6D5A (jun4,xun4) -6D5B (han2) -6D5C (bang1) -6D5D (mang2) -6D5E (zhuo2) -6D5F (you2) -6D60 (xi1) -6D61 (bo2) -6D62 (dou4) -6D63 (huan4,wan3) -6D64 (hong2) -6D65 (yi4) -6D66 (pu3) -6D67 (ying3) -6D68 (lan3) -6D69 (hao4) -6D6A (lang4) -6D6B (han3) -6D6C (li3) -6D6D (geng1) -6D6E (fu2) -6D6F (wu2) -6D70 (li4) -6D71 (chun2) -6D72 (feng2) -6D73 (yi4) -6D74 (yu4) -6D75 (tong2) -6D76 (lao2) -6D77 (hai3) -6D78 (jin4,jin1) -6D79 (jia2,jia1) -6D7A (chong1) -6D7B (weng3) -6D7C (mei3) -6D7D (sui1) -6D7E (cheng1) -6D7F (pei4) -6D80 (xian4) -6D81 (shen4) -6D82 (tu2) -6D83 (kun4) -6D84 (pin1) -6D85 (nie4) -6D86 (han4) -6D87 (jing1) -6D88 (xiao1) -6D89 (she4) -6D8A (nian3) -6D8B (tu1) -6D8C (yong3,chong1) -6D8D (xiao1) -6D8E (xian2) -6D8F (ting3) -6D90 (e2) -6D91 (su4) -6D92 (tun1) -6D93 (juan1) -6D94 (cen2) -6D95 (ti4) -6D96 (li4) -6D97 (shui4) -6D98 (si4) -6D99 (lei4) -6D9A (shui4) -6D9B (tao1) -6D9C (du2) -6D9D (lao4) -6D9E (lai2) -6D9F (lian2) -6DA0 (wei2) -6DA1 (wo1,guo1) -6DA2 (yun2) -6DA3 (huan4) -6DA4 (di2) -6DA5 (none0) -6DA6 (run4) -6DA7 (jian4) -6DA8 (zhang3,zhang4) -6DA9 (se4) -6DAA (fu2) -6DAB (guan4) -6DAC (xing4) -6DAD (shou4) -6DAE (shuan4) -6DAF (ya2) -6DB0 (chuo4) -6DB1 (zhang4) -6DB2 (ye4,yi4) -6DB3 (kong1) -6DB4 (wan3) -6DB5 (han2) -6DB6 (tuo1) -6DB7 (dong1) -6DB8 (he2,hao4) -6DB9 (wo1) -6DBA (ju1) -6DBB (gan4) -6DBC (liang2,liang4) -6DBD (hun1) -6DBE (ta4) -6DBF (zhuo1) -6DC0 (dian4) -6DC1 (qie4) -6DC2 (de2) -6DC3 (juan4) -6DC4 (zi1) -6DC5 (xi1) -6DC6 (xiao2,yao2) -6DC7 (qi2) -6DC8 (gu3) -6DC9 (guo3) -6DCA (han4) -6DCB (lin2,lin4) -6DCC (tang3) -6DCD (zhou1) -6DCE (peng3) -6DCF (hao4) -6DD0 (chang1) -6DD1 (shu1,shu2) -6DD2 (qi1) -6DD3 (fang1) -6DD4 (chi4) -6DD5 (lu4) -6DD6 (nao4) -6DD7 (ju2) -6DD8 (tao2) -6DD9 (cong2) -6DDA (lei4) -6DDB (zhi4) -6DDC (peng2) -6DDD (fei2) -6DDE (song1) -6DDF (tian3) -6DE0 (pi4) -6DE1 (dan4) -6DE2 (yu4) -6DE3 (ni2) -6DE4 (yu1) -6DE5 (lu4) -6DE6 (gan4) -6DE7 (mi4) -6DE8 (jing4) -6DE9 (ling2) -6DEA (lun2) -6DEB (yin2) -6DEC (cui4) -6DED (qu2) -6DEE (huai2) -6DEF (yu4) -6DF0 (nian4) -6DF1 (shen1) -6DF2 (piao2,hu1) -6DF3 (chun2) -6DF4 (hu1) -6DF5 (yuan1) -6DF6 (lai2) -6DF7 (hun4,hun2,hun3) -6DF8 (qing1) -6DF9 (yan1,yan4) -6DFA (qian3,jian1) -6DFB (tian1) -6DFC (miao3) -6DFD (zhi3) -6DFE (yin3) -6DFF (mi4) -6E00 (ben1) -6E01 (yuan1) -6E02 (wen4) -6E03 (re4) -6E04 (fei1) -6E05 (qing1) -6E06 (yuan1) -6E07 (ke3) -6E08 (ji4) -6E09 (she4) -6E0A (yuan1) -6E0B (se4) -6E0C (lu4) -6E0D (zi4) -6E0E (du2) -6E0F (none0) -6E10 (jian4,jian1) -6E11 (mian3,sheng2) -6E12 (pi4) -6E13 (xi1) -6E14 (yu2) -6E15 (yuan1) -6E16 (shen3) -6E17 (shen4) -6E18 (rou2) -6E19 (huan4) -6E1A (zhu3) -6E1B (jian3) -6E1C (nuan3) -6E1D (yu2) -6E1E (qiu2) -6E1F (ting2) -6E20 (qu2) -6E21 (du4) -6E22 (feng2) -6E23 (zha1,zha3) -6E24 (bo2) -6E25 (wo4) -6E26 (wo1,guo1) -6E27 (di4) -6E28 (wei1) -6E29 (wen1) -6E2A (ru2) -6E2B (xie4) -6E2C (ce4) -6E2D (wei4) -6E2E (ge1) -6E2F (gang3) -6E30 (yan3) -6E31 (hong2) -6E32 (xuan4) -6E33 (mi3) -6E34 (ke3) -6E35 (mao2) -6E36 (ying1) -6E37 (yan3) -6E38 (you2) -6E39 (hong1) -6E3A (miao3) -6E3B (xing3) -6E3C (mei3) -6E3D (zai1) -6E3E (hun2,hun4) -6E3F (nai4) -6E40 (kui2) -6E41 (shi2) -6E42 (e4) -6E43 (pai4) -6E44 (mei2) -6E45 (lian4) -6E46 (qi4) -6E47 (qi4) -6E48 (mei2) -6E49 (tian2) -6E4A (cou4) -6E4B (wei2) -6E4C (can1) -6E4D (tuan1) -6E4E (mian3) -6E4F (xu1) -6E50 (mo4) -6E51 (xu3) -6E52 (ji2) -6E53 (pen2) -6E54 (jian1) -6E55 (jian3) -6E56 (hu2) -6E57 (feng4) -6E58 (xiang1) -6E59 (yi4) -6E5A (yin4) -6E5B (zhan4) -6E5C (shi2) -6E5D (jie1) -6E5E (zhen1) -6E5F (huang2) -6E60 (tan4) -6E61 (yu2) -6E62 (bi4) -6E63 (min3) -6E64 (shi1) -6E65 (tu2) -6E66 (sheng1) -6E67 (yong3,chong1) -6E68 (ju2) -6E69 (zhong4) -6E6A (none0) -6E6B (qiu1,jia3,jiao3,jiu1) -6E6C (jiao3) -6E6D (none0) -6E6E (yin1,yan1) -6E6F (tang1,shang1) -6E70 (long2) -6E71 (huo4) -6E72 (yuan2) -6E73 (nan3) -6E74 (ban4) -6E75 (you3) -6E76 (quan2) -6E77 (chui2) -6E78 (liang4) -6E79 (chan2) -6E7A (yan2) -6E7B (chun2) -6E7C (nie4) -6E7D (zi1) -6E7E (wan1) -6E7F (shi1) -6E80 (man3) -6E81 (ying2) -6E82 (la4) -6E83 (kui4,hui4) -6E84 (none0) -6E85 (jian4,jian1) -6E86 (xu4) -6E87 (lou2) -6E88 (gui1) -6E89 (gai4) -6E8A (none0) -6E8B (none0) -6E8C (po1) -6E8D (jin4) -6E8E (gui4) -6E8F (tang2) -6E90 (yuan2) -6E91 (suo3) -6E92 (yuan2) -6E93 (lian2) -6E94 (yao3) -6E95 (meng4) -6E96 (zhun3) -6E97 (sheng2) -6E98 (ke4) -6E99 (tai4) -6E9A (ta3) -6E9B (wa1) -6E9C (liu1,liu4) -6E9D (gou1) -6E9E (sao1) -6E9F (ming2) -6EA0 (zha4) -6EA1 (shi2) -6EA2 (yi4) -6EA3 (lun4) -6EA4 (ma3) -6EA5 (pu3) -6EA6 (wei1) -6EA7 (li4) -6EA8 (cai2) -6EA9 (wu4) -6EAA (xi1,qi1) -6EAB (wen1) -6EAC (qiang1) -6EAD (ce4) -6EAE (shi1) -6EAF (su4) -6EB0 (yi1) -6EB1 (zhen1,qin2) -6EB2 (sou1) -6EB3 (yun2) -6EB4 (xiu4) -6EB5 (yin1) -6EB6 (rong2) -6EB7 (hun4) -6EB8 (su4) -6EB9 (su4) -6EBA (ni4,niao4) -6EBB (ta4,ta1) -6EBC (shi1) -6EBD (ru4) -6EBE (wei1) -6EBF (pan4) -6EC0 (chu4) -6EC1 (chu2) -6EC2 (pang1) -6EC3 (weng1) -6EC4 (cang1) -6EC5 (mie4) -6EC6 (he2) -6EC7 (dian1) -6EC8 (hao4) -6EC9 (huang3) -6ECA (xi4) -6ECB (zi1) -6ECC (di2) -6ECD (zhi4) -6ECE (ying2,xing2) -6ECF (fu3) -6ED0 (jie2) -6ED1 (hua2,gu3) -6ED2 (ge1) -6ED3 (zi3) -6ED4 (tao1) -6ED5 (teng2) -6ED6 (sui1) -6ED7 (bi4) -6ED8 (jiao4) -6ED9 (hui4) -6EDA (gun3) -6EDB (yin2) -6EDC (gao1) -6EDD (long2,shuang1) -6EDE (zhi4) -6EDF (yan4) -6EE0 (she4) -6EE1 (man3) -6EE2 (ying2) -6EE3 (chun2) -6EE4 (lu:4) -6EE5 (lan4) -6EE6 (luan2) -6EE7 (xiao4) -6EE8 (bin1) -6EE9 (tan1) -6EEA (yu4) -6EEB (xiu3) -6EEC (hu4) -6EED (bi4) -6EEE (biao1) -6EEF (zhi4) -6EF0 (jiang3) -6EF1 (kou4) -6EF2 (shen4) -6EF3 (shang1) -6EF4 (di1) -6EF5 (mi4) -6EF6 (ao2) -6EF7 (lu3) -6EF8 (hu3,xu3) -6EF9 (hu1) -6EFA (you2) -6EFB (chan3) -6EFC (fan4) -6EFD (yong1) -6EFE (gun3) -6EFF (man3) -6F00 (qing4) -6F01 (yu2) -6F02 (piao1,piao3,piao4) -6F03 (ji2) -6F04 (ya2) -6F05 (jiao3) -6F06 (qi1,qu4,xi1) -6F07 (xi3) -6F08 (ji4) -6F09 (lu4) -6F0A (lu:3,lou2) -6F0B (long2) -6F0C (jin3) -6F0D (guo2) -6F0E (cong2) -6F0F (lou4) -6F10 (zhi2) -6F11 (gai4) -6F12 (qiang2) -6F13 (li2) -6F14 (yan3) -6F15 (cao2) -6F16 (jiao4) -6F17 (cong1) -6F18 (chun2) -6F19 (tuan2) -6F1A (ou4,ou1) -6F1B (teng2) -6F1C (ye3) -6F1D (xi2) -6F1E (mi4) -6F1F (tang2) -6F20 (mo4) -6F21 (shang1) -6F22 (han4) -6F23 (lian2) -6F24 (lan3) -6F25 (wa1) -6F26 (li2) -6F27 (qian2) -6F28 (feng2) -6F29 (xuan2) -6F2A (yi1) -6F2B (man4,man2) -6F2C (zi4) -6F2D (mang3) -6F2E (kang1) -6F2F (luo4,ta4) -6F30 (peng1) -6F31 (shu4) -6F32 (zhang3,zhang4) -6F33 (zhang1) -6F34 (chong2) -6F35 (xu4) -6F36 (huan4) -6F37 (kuo4,huo3) -6F38 (jian4,jian1) -6F39 (yan1) -6F3A (chuang3,shuang3) -6F3B (liao2) -6F3C (cui3) -6F3D (ti2) -6F3E (yang4) -6F3F (jiang1,jiang4) -6F40 (cong2) -6F41 (ying3) -6F42 (hong2) -6F43 (xiu1) -6F44 (shu4) -6F45 (guan4) -6F46 (ying2) -6F47 (xiao1) -6F48 (none0) -6F49 (none0) -6F4A (xu4) -6F4B (lian4) -6F4C (zhi4) -6F4D (wei2) -6F4E (pi4) -6F4F (yu4) -6F50 (jiao4) -6F51 (po1) -6F52 (xiang4) -6F53 (hui4) -6F54 (jie2) -6F55 (wu3) -6F56 (pa2) -6F57 (ji2) -6F58 (pan1) -6F59 (wei2) -6F5A (xiao1,su4) -6F5B (qian2) -6F5C (qian2) -6F5D (xi1) -6F5E (lu4) -6F5F (xi4) -6F60 (sun4) -6F61 (dun4) -6F62 (huang2) -6F63 (min3) -6F64 (run4) -6F65 (su4) -6F66 (liao3,liao2,lao3) -6F67 (zhen1) -6F68 (zhong1) -6F69 (yi4) -6F6A (di2) -6F6B (wan1) -6F6C (dan4) -6F6D (tan2) -6F6E (chao2) -6F6F (xun2) -6F70 (kui4,hui4) -6F71 (none0) -6F72 (shao4) -6F73 (tu2) -6F74 (zhu1) -6F75 (sa3) -6F76 (hei1) -6F77 (bi3,bi4) -6F78 (shan1) -6F79 (chan2) -6F7A (chan2) -6F7B (shu3) -6F7C (tong2) -6F7D (pu1) -6F7E (lin2) -6F7F (wei2) -6F80 (se4) -6F81 (se4) -6F82 (cheng2,deng4) -6F83 (jiong3) -6F84 (cheng2,deng4) -6F85 (hua4) -6F86 (jiao1) -6F87 (lao4,lao2) -6F88 (che4) -6F89 (gan3) -6F8A (cun1) -6F8B (heng4) -6F8C (si1) -6F8D (shu4) -6F8E (peng2,peng1) -6F8F (han4) -6F90 (yun2) -6F91 (liu4,liu1) -6F92 (hong4) -6F93 (fu2) -6F94 (hao4) -6F95 (he2) -6F96 (xian1) -6F97 (jian4) -6F98 (shan1) -6F99 (xi4) -6F9A (ao4) -6F9B (lu3) -6F9C (lan2) -6F9D (none0) -6F9E (yu2) -6F9F (lin3) -6FA0 (min3,mian3,sheng2) -6FA1 (zao3) -6FA2 (dang1) -6FA3 (huan3) -6FA4 (ze2) -6FA5 (xie4) -6FA6 (yu4) -6FA7 (li3) -6FA8 (shi4) -6FA9 (xue2) -6FAA (ling2) -6FAB (man4) -6FAC (zi1) -6FAD (yong1) -6FAE (kuai4,hui4) -6FAF (can4) -6FB0 (lian4) -6FB1 (dian4) -6FB2 (ye4) -6FB3 (ao4) -6FB4 (huan2) -6FB5 (lian4) -6FB6 (chan2) -6FB7 (man4) -6FB8 (dan3) -6FB9 (dan4,tan2) -6FBA (yi4) -6FBB (sui4) -6FBC (pi4) -6FBD (ju4) -6FBE (ta4) -6FBF (qin2) -6FC0 (ji1) -6FC1 (zhuo2) -6FC2 (lian2) -6FC3 (nong2) -6FC4 (guo1) -6FC5 (jin4) -6FC6 (fen2) -6FC7 (se4) -6FC8 (ji2) -6FC9 (sui1) -6FCA (hui4) -6FCB (chu3) -6FCC (ta4) -6FCD (song1) -6FCE (ding3) -6FCF (se4) -6FD0 (zhu3) -6FD1 (lai4) -6FD2 (bin1) -6FD3 (lian2) -6FD4 (mi3) -6FD5 (shi1) -6FD6 (shu4) -6FD7 (mi4) -6FD8 (ning4,neng4) -6FD9 (ying2) -6FDA (ying2,xing2) -6FDB (meng2) -6FDC (jin4) -6FDD (qi2) -6FDE (bi4) -6FDF (ji4,ji3) -6FE0 (hao2) -6FE1 (ru2) -6FE2 (zui3,cui4) -6FE3 (wo4) -6FE4 (tao1,tao2) -6FE5 (yin4) -6FE6 (yin3) -6FE7 (dui4) -6FE8 (ci2) -6FE9 (huo4) -6FEA (jing4) -6FEB (lan4) -6FEC (jun4) -6FED (ai4) -6FEE (pu2) -6FEF (zhuo2) -6FF0 (wei2) -6FF1 (bin1) -6FF2 (gu3) -6FF3 (qian2) -6FF4 (xing2) -6FF5 (bin1) -6FF6 (kuo4) -6FF7 (fei4) -6FF8 (none0) -6FF9 (bin1) -6FFA (jian4,jian1) -6FFB (dui4,wei2) -6FFC (luo4) -6FFD (luo4) -6FFE (lu:4) -6FFF (li4) -7000 (you1) -7001 (yang4) -7002 (lu3) -7003 (si4) -7004 (jie2) -7005 (ying4,ying2) -7006 (du2) -7007 (wang3) -7008 (hui1) -7009 (xie4) -700A (pan2) -700B (shen3) -700C (biao1) -700D (chan2) -700E (mie4) -700F (liu2) -7010 (jian1) -7011 (pu4,bao4) -7012 (se4) -7013 (cheng2) -7014 (gu3) -7015 (bin1,pin2) -7016 (huo4) -7017 (xian4) -7018 (lu2) -7019 (qin1) -701A (han4) -701B (ying2) -701C (rong2) -701D (li4) -701E (jing4) -701F (xiao1) -7020 (ying2) -7021 (sui3) -7022 (wei2) -7023 (xie4) -7024 (huai2) -7025 (hao4) -7026 (zhu1) -7027 (long2,shuang1) -7028 (lai4) -7029 (dui4) -702A (fan2) -702B (hu2) -702C (lai4) -702D (none0) -702E (none0) -702F (ying2) -7030 (mi2) -7031 (ji4) -7032 (lian4) -7033 (jian4) -7034 (ying3) -7035 (fen4) -7036 (lin2) -7037 (yi4) -7038 (jian1) -7039 (yue4) -703A (chan2) -703B (dai4) -703C (rang2,rang4) -703D (jian3) -703E (lan2) -703F (fan2) -7040 (shuang4) -7041 (yuan1) -7042 (zhuo2) -7043 (feng1) -7044 (she4) -7045 (lei3) -7046 (lan2) -7047 (cong2) -7048 (qu2) -7049 (yong1) -704A (qian2) -704B (fa3) -704C (guan4) -704D (que4) -704E (yan4) -704F (hao4) -7050 (none0) -7051 (sa3) -7052 (zan4) -7053 (luan2) -7054 (yan4) -7055 (li2) -7056 (mi3) -7057 (dan4) -7058 (tan1) -7059 (dang3) -705A (jiao3) -705B (chan3) -705C (none0) -705D (hao4) -705E (ba4) -705F (zhu2) -7060 (lan3) -7061 (lan2) -7062 (nang3) -7063 (wan1) -7064 (luan2) -7065 (quan2) -7066 (xian1) -7067 (yan4) -7068 (gan4) -7069 (yan4) -706A (yu4) -706B (huo3) -706C (biao1) -706D (mie4) -706E (guang1) -706F (deng1) -7070 (hui1) -7071 (xiao1) -7072 (xiao1) -7073 (none0) -7074 (hong2) -7075 (ling2) -7076 (zao4) -7077 (zhuan4) -7078 (jiu3) -7079 (zha4) -707A (xie4) -707B (chi4) -707C (zhuo2) -707D (zai1) -707E (zai1) -707F (can4) -7080 (yang2) -7081 (qi4) -7082 (zhong1) -7083 (fen2) -7084 (niu3) -7085 (gui4,jiong3) -7086 (wen2) -7087 (po4) -7088 (yi4) -7089 (lu2) -708A (chui1,chui4) -708B (pi1) -708C (kai4) -708D (pan4) -708E (yan2) -708F (kai4) -7090 (pang4) -7091 (mu4) -7092 (chao3) -7093 (liao4) -7094 (gui4,que1) -7095 (kang4) -7096 (dun4) -7097 (guang1) -7098 (xin1) -7099 (zhi4) -709A (guang1) -709B (xin1) -709C (wei3) -709D (qiang4) -709E (bian4) -709F (da2) -70A0 (xia2) -70A1 (zheng1) -70A2 (zhu2) -70A3 (ke3) -70A4 (zhao4) -70A5 (fu2) -70A6 (ba2) -70A7 (duo4) -70A8 (duo4) -70A9 (ling4) -70AA (zhuo2) -70AB (xuan4) -70AC (ju4) -70AD (tan4) -70AE (pao4,bao1,pao2,pao1) -70AF (jiong3) -70B0 (pao2) -70B1 (tai2) -70B2 (tai2) -70B3 (bing3) -70B4 (yang3) -70B5 (tong1,dong1) -70B6 (han1) -70B7 (zhu4) -70B8 (zha4,zha2) -70B9 (dian3,dian5) -70BA (wei4,wei2) -70BB (shi2) -70BC (lian4) -70BD (chi4) -70BE (ping2) -70BF (none0) -70C0 (hu1) -70C1 (shuo4) -70C2 (lan4) -70C3 (ting1) -70C4 (jiao3) -70C5 (xu4) -70C6 (xing2) -70C7 (quan4) -70C8 (lie4) -70C9 (huan4) -70CA (yang2,yang4) -70CB (xiao1) -70CC (xiu1) -70CD (xian3) -70CE (yin2) -70CF (wu1,wu4) -70D0 (zhou1) -70D1 (yao2) -70D2 (shi4) -70D3 (wei1) -70D4 (tong2) -70D5 (tong2) -70D6 (zai1) -70D7 (kai4) -70D8 (hong1) -70D9 (luo4,lao4) -70DA (xia2) -70DB (zhu2) -70DC (xuan3) -70DD (zheng1) -70DE (po4) -70DF (yan1,yin1) -70E0 (hui3) -70E1 (guang1) -70E2 (zhe4) -70E3 (hui1) -70E4 (kao3) -70E5 (none0) -70E6 (fan2) -70E7 (shao1) -70E8 (ye4) -70E9 (hui4) -70EA (none0) -70EB (tang4) -70EC (jin4) -70ED (re4) -70EE (none0) -70EF (xi1) -70F0 (fu2) -70F1 (jiong3) -70F2 (che4) -70F3 (pu3) -70F4 (jing3,ting1) -70F5 (zhuo2) -70F6 (ting3) -70F7 (wan2) -70F8 (hai3) -70F9 (peng1) -70FA (lang3) -70FB (shan1) -70FC (hu1) -70FD (feng1) -70FE (chi4) -70FF (rong2) -7100 (hu2) -7101 (none0) -7102 (shu2) -7103 (lang3) -7104 (xun1) -7105 (xun1) -7106 (jue2) -7107 (xiao1) -7108 (xi1) -7109 (yan1) -710A (han4) -710B (zhuang4) -710C (qu1,jun4) -710D (di4,ti1) -710E (xie4) -710F (qi4) -7110 (wu4) -7111 (none0) -7112 (none0) -7113 (han2) -7114 (yan4) -7115 (huan4) -7116 (men4) -7117 (ju2) -7118 (dao4,tao1) -7119 (bei4) -711A (fen2) -711B (lin4) -711C (kun1) -711D (hun4) -711E (chun1) -711F (xi2) -7120 (cui4) -7121 (wu2,mo2) -7122 (hong1) -7123 (ju4) -7124 (fu3) -7125 (yue1) -7126 (jiao1) -7127 (cong1) -7128 (feng4) -7129 (ping1) -712A (qiong1) -712B (cui4) -712C (xi2) -712D (qiong2) -712E (xin4) -712F (zhuo2,chao1,zhuo1) -7130 (yan4) -7131 (yan4) -7132 (yi4) -7133 (jue2) -7134 (yu4) -7135 (gang4) -7136 (ran2) -7137 (pi2) -7138 (yan4) -7139 (none0) -713A (sheng1) -713B (chang4) -713C (shao1) -713D (none0) -713E (none0) -713F (none0) -7140 (none0) -7141 (chen2) -7142 (he4) -7143 (kui3) -7144 (zhong1) -7145 (duan4) -7146 (ya1) -7147 (hui1) -7148 (feng4) -7149 (lian4) -714A (xuan1) -714B (xing1) -714C (huang2) -714D (jiao3) -714E (jian1) -714F (bi4) -7150 (ying1) -7151 (zhu3) -7152 (wei3) -7153 (tuan1) -7154 (tian4) -7155 (xi1) -7156 (nuan3,xuan1) -7157 (nuan3) -7158 (chan2) -7159 (yan1) -715A (jiong3) -715B (jiong3) -715C (yu4) -715D (mei4) -715E (sha4,sha1) -715F (wu4) -7160 (ye4) -7161 (xin4) -7162 (qiong2) -7163 (rou3) -7164 (mei2) -7165 (huan4) -7166 (xu4,xu3) -7167 (zhao4) -7168 (wei1) -7169 (fan2) -716A (qiu2) -716B (sui4) -716C (yang2,yang4) -716D (lie4) -716E (zhu3) -716F (none0) -7170 (gao4) -7171 (gua1) -7172 (bao1) -7173 (hu2) -7174 (yun1) -7175 (xia1) -7176 (none0) -7177 (none0) -7178 (bian1) -7179 (wei1) -717A (tui4) -717B (tang2) -717C (chao3) -717D (shan1,shan4) -717E (yun1) -717F (bo2) -7180 (huang3) -7181 (xie2) -7182 (xi4) -7183 (wu4) -7184 (xi1,xi2) -7185 (yun2) -7186 (he2) -7187 (he4) -7188 (xi1) -7189 (yun1) -718A (xiong2) -718B (nai2) -718C (kao3) -718D (none0) -718E (yao4) -718F (xun1,xun4) -7190 (ming2) -7191 (lian2) -7192 (ying2) -7193 (wen4) -7194 (rong2) -7195 (none0) -7196 (none0) -7197 (qiang4) -7198 (liu4,liu1) -7199 (xi1) -719A (bi4) -719B (biao1) -719C (cong1) -719D (lu4) -719E (jian1) -719F (shu2,shou2) -71A0 (yi4) -71A1 (lou2) -71A2 (feng1) -71A3 (sui1) -71A4 (yi4) -71A5 (teng1) -71A6 (jue2) -71A7 (zong1) -71A8 (yun4,yu4) -71A9 (hu4) -71AA (yi2) -71AB (zhi4) -71AC (ao2,ao1) -71AD (wei4) -71AE (liao2) -71AF (han4) -71B0 (ou1) -71B1 (re4) -71B2 (jiong3) -71B3 (man4) -71B4 (none0) -71B5 (shang1) -71B6 (cuan4) -71B7 (zeng1) -71B8 (jian1) -71B9 (xi1) -71BA (xi1) -71BB (xi1) -71BC (yi4) -71BD (xiao4) -71BE (chi4) -71BF (huang2) -71C0 (chan3) -71C1 (ye4) -71C2 (qian2) -71C3 (ran2) -71C4 (yan4) -71C5 (xian2) -71C6 (qiao2) -71C7 (zun1) -71C8 (deng1) -71C9 (dun4) -71CA (shen1) -71CB (jiao1) -71CC (fen2) -71CD (si1) -71CE (liao2,liao3,liao4) -71CF (yu4) -71D0 (lin2) -71D1 (tong2) -71D2 (shao1) -71D3 (fen2) -71D4 (fan2) -71D5 (yan4,yan1) -71D6 (xun2) -71D7 (lan4) -71D8 (mei3) -71D9 (tang4) -71DA (yi1) -71DB (jing3) -71DC (men4) -71DD (none0) -71DE (none0) -71DF (ying2) -71E0 (yu4) -71E1 (yi4) -71E2 (xue2) -71E3 (lan2) -71E4 (tai4) -71E5 (zao4) -71E6 (can4) -71E7 (sui4) -71E8 (xi1) -71E9 (que4) -71EA (cong1) -71EB (lian2) -71EC (hui3) -71ED (zhu2) -71EE (xie4) -71EF (ling2) -71F0 (wei1) -71F1 (yi4) -71F2 (xie2) -71F3 (zhao4) -71F4 (hui4) -71F5 (none0) -71F6 (none0) -71F7 (lan2) -71F8 (ru2) -71F9 (xian3) -71FA (kao3) -71FB (xun1) -71FC (jin4) -71FD (chou2) -71FE (dao4,tao1,tao2) -71FF (yao4) -7200 (he4) -7201 (lan4) -7202 (biao1) -7203 (rong2) -7204 (li4) -7205 (mo4) -7206 (bao4) -7207 (ruo4) -7208 (di4) -7209 (lu:4) -720A (ao2) -720B (xun4) -720C (kuang4) -720D (shuo4) -720E (none0) -720F (li4) -7210 (lu2) -7211 (jue2) -7212 (liao4) -7213 (yan4) -7214 (xi1) -7215 (xie4) -7216 (long2) -7217 (yan4) -7218 (none0) -7219 (rang3,shang4) -721A (yue4) -721B (lan4) -721C (cong2) -721D (jue2,jiao4) -721E (tong2) -721F (guan4) -7220 (none0) -7221 (che4) -7222 (mi2) -7223 (tang3) -7224 (lan4) -7225 (zhu2) -7226 (lan3) -7227 (ling2) -7228 (cuan4) -7229 (yu4) -722A (zhua3,zhao3) -722B (lan4) -722C (pa2) -722D (zheng1) -722E (pao2) -722F (zhao3) -7230 (yuan2) -7231 (ai4) -7232 (wei4,wei2) -7233 (none0) -7234 (jue2) -7235 (jue2) -7236 (fu4,fu3) -7237 (ye2) -7238 (ba4) -7239 (die1) -723A (ye2) -723B (yao2) -723C (zu3) -723D (shuang3) -723E (er3) -723F (pan2,qiang2,ban4) -7240 (chuan2) -7241 (ke1) -7242 (zang1) -7243 (zang1) -7244 (qiang1) -7245 (die2) -7246 (qiang2) -7247 (pian4,pian1) -7248 (ban3) -7249 (pan4) -724A (shao2) -724B (jian1) -724C (pai2) -724D (du2) -724E (yong1) -724F (tou2) -7250 (tou2) -7251 (bian1) -7252 (die2) -7253 (bang3) -7254 (bo2) -7255 (bang3) -7256 (you3) -7257 (none0) -7258 (du2) -7259 (ya2) -725A (cheng4,cheng1) -725B (niu2) -725C (cheng1) -725D (pin4) -725E (jiu1) -725F (mou2,mu4) -7260 (ta1) -7261 (mu3) -7262 (lao2) -7263 (ren4) -7264 (mang2) -7265 (fang1) -7266 (mao2) -7267 (mu4) -7268 (ren4) -7269 (wu4) -726A (yan4) -726B (fa2) -726C (bei4) -726D (si4) -726E (jian4) -726F (gu3) -7270 (you4) -7271 (gu3) -7272 (sheng1) -7273 (mu3) -7274 (di3) -7275 (qian1) -7276 (quan4) -7277 (quan2) -7278 (zi4) -7279 (te4) -727A (xi1) -727B (mang2) -727C (keng1) -727D (qian1) -727E (wu3,wu2) -727F (gu4) -7280 (xi1) -7281 (li2) -7282 (li2) -7283 (pou3) -7284 (ji1) -7285 (gang1) -7286 (zhi2) -7287 (ben1,ben4) -7288 (quan2) -7289 (run1) -728A (du2) -728B (ju4) -728C (jia1) -728D (jian1,qian2) -728E (feng1) -728F (pian1) -7290 (ke1) -7291 (ju2) -7292 (kao4,di2) -7293 (chu2) -7294 (xi4) -7295 (bei4) -7296 (luo4) -7297 (jie4) -7298 (ma2) -7299 (san1) -729A (wei4) -729B (li2) -729C (dun1) -729D (tong2) -729E (se4) -729F (jiang4) -72A0 (xi1) -72A1 (li4) -72A2 (du2) -72A3 (lie4) -72A4 (pi2) -72A5 (piao3) -72A6 (bao4) -72A7 (xi1) -72A8 (chou1) -72A9 (wei4) -72AA (kui2) -72AB (chou1) -72AC (quan3,quan2) -72AD (quan3) -72AE (ba2) -72AF (fan4) -72B0 (qiu2) -72B1 (bo2) -72B2 (chai2) -72B3 (chuo2) -72B4 (an4,han1) -72B5 (jie2) -72B6 (zhuang4) -72B7 (guang3) -72B8 (ma3) -72B9 (you2) -72BA (kang4) -72BB (bo2) -72BC (hou3) -72BD (ya2) -72BE (han4) -72BF (huan1) -72C0 (zhuang4) -72C1 (yun3) -72C2 (kuang2) -72C3 (niu3) -72C4 (di2) -72C5 (qing1) -72C6 (zhong4) -72C7 (yun3) -72C8 (bei4) -72C9 (pi1) -72CA (ju2) -72CB (ni2) -72CC (sheng1) -72CD (pao2) -72CE (xia2) -72CF (tuo2) -72D0 (hu2) -72D1 (ling2) -72D2 (fei4) -72D3 (pi1) -72D4 (ni2) -72D5 (sheng1) -72D6 (you4) -72D7 (gou3) -72D8 (yue4) -72D9 (ju1) -72DA (dan4) -72DB (bo4) -72DC (gu3) -72DD (xian3) -72DE (ning2) -72DF (huan2) -72E0 (hen3) -72E1 (jiao3,jia3) -72E2 (he2,hao2,mo4) -72E3 (zhao4) -72E4 (ji2) -72E5 (huan2) -72E6 (shan1) -72E7 (ta4) -72E8 (rong2) -72E9 (shou4) -72EA (tong1) -72EB (lao3) -72EC (du2) -72ED (xia2) -72EE (shi1) -72EF (kuai4) -72F0 (zheng1) -72F1 (yu4) -72F2 (sun1) -72F3 (yu2) -72F4 (bi4) -72F5 (mang2) -72F6 (xi3) -72F7 (juan4) -72F8 (li2) -72F9 (xia2) -72FA (yin2) -72FB (suan1) -72FC (lang2) -72FD (bei4) -72FE (zhi4) -72FF (yan2) -7300 (sha1) -7301 (li4) -7302 (zhi4) -7303 (xian3) -7304 (jing1) -7305 (han4) -7306 (fei3) -7307 (yao2) -7308 (ba4,pi2) -7309 (qi2) -730A (ni2) -730B (biao1) -730C (yin4) -730D (li2) -730E (lie4) -730F (jian1) -7310 (qiang1) -7311 (kun1) -7312 (yan1) -7313 (guo3) -7314 (zong4) -7315 (mi2) -7316 (chang1) -7317 (yi1) -7318 (zhi4) -7319 (zheng1) -731A (ya2) -731B (meng3) -731C (cai1) -731D (cu4) -731E (she1) -731F (lie4) -7320 (none0) -7321 (luo2) -7322 (hu2) -7323 (zong1) -7324 (hu2) -7325 (wei3) -7326 (feng1) -7327 (wo1) -7328 (yuan2) -7329 (xing1) -732A (zhu1) -732B (mao1,mao2) -732C (wei4) -732D (yuan2) -732E (xian4) -732F (tuan1) -7330 (ya4) -7331 (nao2) -7332 (xie1,he4) -7333 (jia1) -7334 (hou2) -7335 (bian1) -7336 (you2) -7337 (you2) -7338 (mei2) -7339 (cha2) -733A (yao2) -733B (sun1) -733C (bo2) -733D (ming2) -733E (hua2) -733F (yuan2) -7340 (sou1) -7341 (ma3) -7342 (yuan2) -7343 (dai1) -7344 (yu4) -7345 (shi1) -7346 (hao2) -7347 (none0) -7348 (yi4) -7349 (zhen1) -734A (chuang4) -734B (hao2) -734C (man4) -734D (jing4) -734E (jiang3) -734F (mo4) -7350 (zhang1) -7351 (chan2) -7352 (ao2) -7353 (ao2) -7354 (hao2) -7355 (cui1) -7356 (ben4) -7357 (jue2) -7358 (bi4) -7359 (bi4) -735A (huang2) -735B (bu3) -735C (lin2) -735D (yu4) -735E (tong2) -735F (yao4) -7360 (liao2) -7361 (shuo4) -7362 (xiao1) -7363 (shou4) -7364 (none0) -7365 (xi2) -7366 (ge2) -7367 (juan4) -7368 (du2) -7369 (hui4) -736A (kuai4) -736B (xian3) -736C (xie4) -736D (ta3) -736E (xian3) -736F (xun1) -7370 (ning2) -7371 (bian1) -7372 (huo4) -7373 (nou2) -7374 (meng3) -7375 (lie4) -7376 (nao2) -7377 (guang3) -7378 (shou4) -7379 (lu2) -737A (ta4,ta3) -737B (xian4) -737C (mi2) -737D (rang2) -737E (huan1) -737F (nao2) -7380 (luo2) -7381 (xian3) -7382 (qi2) -7383 (qu2) -7384 (xuan2) -7385 (miao4) -7386 (zi1) -7387 (lu:4,shuai4,shuo4) -7388 (lu2) -7389 (yu4) -738A (su4) -738B (wang2,wang4) -738C (qiu2) -738D (ga3) -738E (ding1) -738F (le4) -7390 (ba1) -7391 (ji1) -7392 (hong2) -7393 (di4) -7394 (chuan4) -7395 (gan1) -7396 (jiu3) -7397 (yu2) -7398 (qi3) -7399 (yu2) -739A (yang2,chang4) -739B (ma3) -739C (hong2) -739D (wu3) -739E (fu1) -739F (min2,wen2) -73A0 (jie4) -73A1 (ya2) -73A2 (bin1,fen1) -73A3 (bian4) -73A4 (beng3) -73A5 (yue4) -73A6 (jue2) -73A7 (yun3) -73A8 (jue2) -73A9 (wan2,wan4) -73AA (jian1) -73AB (mei2) -73AC (dan3) -73AD (pi2) -73AE (wei3) -73AF (huan2) -73B0 (xian4) -73B1 (qiang1) -73B2 (ling2) -73B3 (dai4) -73B4 (yi4) -73B5 (an2) -73B6 (ping2) -73B7 (dian4) -73B8 (fu2) -73B9 (xuan2) -73BA (xi3) -73BB (bo1) -73BC (ci3) -73BD (gou3) -73BE (jia3) -73BF (shao2) -73C0 (po4) -73C1 (ci2) -73C2 (ke1) -73C3 (ran3) -73C4 (sheng1) -73C5 (shen1) -73C6 (yi2) -73C7 (zu3) -73C8 (jia1) -73C9 (min2) -73CA (shan1) -73CB (liu3) -73CC (bi4) -73CD (zhen1) -73CE (zhen1) -73CF (jue2) -73D0 (fa3,fa4) -73D1 (long2) -73D2 (jin1) -73D3 (jiao4) -73D4 (jian4) -73D5 (li4) -73D6 (guang1) -73D7 (xian1) -73D8 (zhou1) -73D9 (gong3) -73DA (yan1) -73DB (xiu4) -73DC (yang2) -73DD (xu3) -73DE (luo4) -73DF (su4) -73E0 (zhu1) -73E1 (qin2) -73E2 (ken4) -73E3 (xun2) -73E4 (bao3) -73E5 (er3) -73E6 (xiang2) -73E7 (yao2) -73E8 (xia2) -73E9 (heng2,hang2) -73EA (gui1) -73EB (chong1) -73EC (xu4) -73ED (ban1) -73EE (pei4) -73EF (none0) -73F0 (dang1) -73F1 (ying1) -73F2 (hun2,hui1) -73F3 (wen2) -73F4 (e2) -73F5 (cheng2) -73F6 (ti2,di4) -73F7 (wu3) -73F8 (wu2) -73F9 (cheng2) -73FA (jun4) -73FB (mei2) -73FC (bei4) -73FD (ting3) -73FE (xian4) -73FF (chuo4) -7400 (han2) -7401 (xuan2) -7402 (yan2) -7403 (qiu2) -7404 (quan3) -7405 (lang2) -7406 (li3) -7407 (xiu4) -7408 (fu2) -7409 (liu2) -740A (ya2,ye2) -740B (xi1) -740C (ling2) -740D (li4) -740E (jin1) -740F (lian3) -7410 (suo3) -7411 (suo3) -7412 (none0) -7413 (wan2) -7414 (dian4) -7415 (bing3) -7416 (zhan3) -7417 (cui4) -7418 (min2) -7419 (yu4) -741A (ju1) -741B (chen1) -741C (lai2) -741D (wen2) -741E (sheng4) -741F (wei2) -7420 (dian3) -7421 (chu4) -7422 (zhuo2,zuo2) -7423 (pei3) -7424 (cheng1) -7425 (hu3) -7426 (qi2) -7427 (e4) -7428 (kun1) -7429 (chang1) -742A (qi2) -742B (beng3) -742C (wan3) -742D (lu4) -742E (cong2) -742F (guan3) -7430 (yan3) -7431 (diao1) -7432 (bei4) -7433 (lin2) -7434 (qin2) -7435 (pi2) -7436 (pa2,ba5) -7437 (qiang1) -7438 (zhuo2) -7439 (qin2) -743A (fa4) -743B (none0) -743C (qiong2) -743D (du3) -743E (jie4) -743F (hun2,hui1) -7440 (yu3) -7441 (mao4,mei4) -7442 (mei2) -7443 (chun1) -7444 (xuan1) -7445 (ti2) -7446 (xing1) -7447 (dai4) -7448 (rou2) -7449 (min2) -744A (zhen1) -744B (wei3) -744C (ruan3) -744D (huan4) -744E (xie2) -744F (chuan1) -7450 (jian3) -7451 (zhuan4) -7452 (yang2) -7453 (lian4) -7454 (quan2) -7455 (xia2) -7456 (duan4) -7457 (yuan4) -7458 (ye2) -7459 (nao3) -745A (hu2) -745B (ying1) -745C (yu2) -745D (huang2) -745E (rui4) -745F (se4) -7460 (liu2) -7461 (none0) -7462 (rong2) -7463 (suo3) -7464 (yao2) -7465 (wen1) -7466 (wu3) -7467 (jin1) -7468 (jin4) -7469 (ying2) -746A (ma3) -746B (tao1) -746C (liu2) -746D (tang2) -746E (li4) -746F (lang2) -7470 (gui1) -7471 (tian4) -7472 (qiang1) -7473 (cuo3) -7474 (jue2) -7475 (zhao3) -7476 (yao2) -7477 (ai4) -7478 (bin1) -7479 (tu2) -747A (chang2) -747B (kun1) -747C (zhuan1) -747D (cong1) -747E (jin3) -747F (yi1) -7480 (cui3) -7481 (cong1) -7482 (qi2) -7483 (li2,li5) -7484 (ying3) -7485 (suo3) -7486 (qiu2) -7487 (xuan2) -7488 (ao2) -7489 (lian2,lian3) -748A (man2) -748B (zhang1) -748C (yin2) -748D (none0) -748E (ying1) -748F (wei4) -7490 (lu4) -7491 (wu2) -7492 (deng1) -7493 (none0) -7494 (zeng1) -7495 (xun2) -7496 (qu2) -7497 (dang4) -7498 (lin2) -7499 (liao2) -749A (qiong2) -749B (su4) -749C (huang2) -749D (gui1) -749E (pu2) -749F (jing3) -74A0 (fan2) -74A1 (jin4) -74A2 (liu2) -74A3 (ji1) -74A4 (none0) -74A5 (jing3) -74A6 (ai4) -74A7 (bi4) -74A8 (can4) -74A9 (qu2) -74AA (zao3) -74AB (dang1) -74AC (jiao3) -74AD (gun4) -74AE (tan3) -74AF (hui4) -74B0 (huan2) -74B1 (se4) -74B2 (sui4) -74B3 (tian2) -74B4 (none0) -74B5 (yu2) -74B6 (jin4) -74B7 (fu1) -74B8 (bin1) -74B9 (shu2) -74BA (wen4,wen2) -74BB (zui3) -74BC (lan2) -74BD (xi3) -74BE (ji4) -74BF (xuan2) -74C0 (ruan3) -74C1 (huo4) -74C2 (gai4) -74C3 (lei2) -74C4 (du2) -74C5 (li4) -74C6 (zhi2) -74C7 (rou2) -74C8 (li2) -74C9 (zan4) -74CA (qiong2) -74CB (zhe2) -74CC (gui1) -74CD (sui4) -74CE (la4) -74CF (long2) -74D0 (lu2) -74D1 (li4) -74D2 (zan4) -74D3 (lan4) -74D4 (ying1) -74D5 (mi2) -74D6 (xiang1) -74D7 (xi1) -74D8 (guan4) -74D9 (dao4) -74DA (zan4) -74DB (huan2) -74DC (gua1) -74DD (bao2) -74DE (die2) -74DF (pao2) -74E0 (hu4) -74E1 (zhi2) -74E2 (piao2) -74E3 (ban4) -74E4 (rang2) -74E5 (li4) -74E6 (wa3,wa4) -74E7 (none0) -74E8 (jiang1,hong2) -74E9 (qian2,wa3) -74EA (ban3) -74EB (pen2) -74EC (fang3) -74ED (dan3) -74EE (weng4) -74EF (ou1) -74F0 (none0) -74F1 (none0) -74F2 (none0) -74F3 (hu2) -74F4 (ling2) -74F5 (yi2) -74F6 (ping2) -74F7 (ci2) -74F8 (none0) -74F9 (juan4) -74FA (chang2) -74FB (chi1) -74FC (none0) -74FD (dang4) -74FE (meng3) -74FF (bu4) -7500 (chui2) -7501 (ping2) -7502 (bian1) -7503 (zhou4) -7504 (zhen1) -7505 (none0) -7506 (ci2) -7507 (ying1) -7508 (qi4) -7509 (xian2) -750A (lou3) -750B (di4) -750C (ou1) -750D (meng2) -750E (zhuan1) -750F (beng4) -7510 (lin2) -7511 (zeng4) -7512 (wu3) -7513 (pi4) -7514 (dan1) -7515 (weng4) -7516 (ying1) -7517 (yan3) -7518 (gan1) -7519 (dai4) -751A (shen4,shen2,she2) -751B (tian2) -751C (tian2) -751D (han1) -751E (chang2) -751F (sheng1) -7520 (qing2) -7521 (shen1) -7522 (chan3) -7523 (chan3) -7524 (rui2) -7525 (sheng1) -7526 (su1) -7527 (shen1) -7528 (yong4) -7529 (shuai3) -752A (lu4) -752B (fu3) -752C (yong3) -752D (beng2) -752E (none0) -752F (ning2) -7530 (tian2) -7531 (you2) -7532 (jia3) -7533 (shen1) -7534 (zha2) -7535 (dian4) -7536 (fu2) -7537 (nan2) -7538 (dian4) -7539 (ping2) -753A (ding1,ting3) -753B (hua4) -753C (ting3,ding1) -753D (quan3) -753E (zai1) -753F (meng2) -7540 (bi4) -7541 (qi2) -7542 (liu4) -7543 (xun2) -7544 (liu2) -7545 (chang4) -7546 (mu3) -7547 (yun2) -7548 (fan4) -7549 (fu2) -754A (geng1) -754B (tian2) -754C (jie4) -754D (jie4) -754E (quan3) -754F (wei4) -7550 (fu4) -7551 (tian2) -7552 (mu3) -7553 (none0) -7554 (pan4) -7555 (jiang1) -7556 (wa1) -7557 (da2) -7558 (nan2) -7559 (liu2) -755A (ben3) -755B (zhen3) -755C (chu4,xu4) -755D (mu3) -755E (mu3) -755F (ce4) -7560 (none0) -7561 (gai1) -7562 (bi4) -7563 (da2) -7564 (zhi4) -7565 (lu:e4) -7566 (qi2,xi1) -7567 (lu:e4) -7568 (pan1) -7569 (none0) -756A (fan1,pan1) -756B (hua4) -756C (yu2) -756D (yu2) -756E (mu3) -756F (jun4) -7570 (yi4) -7571 (liu2) -7572 (she1) -7573 (die2) -7574 (chou2) -7575 (hua4) -7576 (dang1,dang4) -7577 (chuo4) -7578 (ji1) -7579 (wan3) -757A (jiang1) -757B (cheng2) -757C (chang4) -757D (tun3) -757E (lei2) -757F (ji1) -7580 (cha1) -7581 (liu2) -7582 (die2) -7583 (tuan3) -7584 (lin2) -7585 (jiang1) -7586 (jiang1) -7587 (chou2) -7588 (bo4) -7589 (die2) -758A (die2) -758B (pi3,shu1,ya3) -758C (nie4) -758D (dan4) -758E (shu1) -758F (shu1,shu4) -7590 (zhi4) -7591 (yi2) -7592 (chuang2) -7593 (nai3) -7594 (ding1) -7595 (bi3) -7596 (jie1) -7597 (liao2) -7598 (gong1,gang1) -7599 (ge1) -759A (jiu4) -759B (zhou3) -759C (xia4) -759D (shan4) -759E (xu1) -759F (nu:e4,yao4) -75A0 (li4) -75A1 (yang2) -75A2 (chen4) -75A3 (you2) -75A4 (ba1) -75A5 (jie4) -75A6 (jue2) -75A7 (xi1) -75A8 (xia1) -75A9 (cui4) -75AA (bi4) -75AB (yi4) -75AC (li4) -75AD (zong4) -75AE (chuang1) -75AF (feng1) -75B0 (zhu4) -75B1 (pao4) -75B2 (pi2) -75B3 (gan1) -75B4 (ke1) -75B5 (ci1,ci2) -75B6 (xie4) -75B7 (qi2) -75B8 (dan3,da5) -75B9 (zhen3) -75BA (fa2) -75BB (zhi3) -75BC (teng2) -75BD (ju1) -75BE (ji2) -75BF (fei4) -75C0 (ju1) -75C1 (dian4) -75C2 (jia1) -75C3 (xuan2,xian2) -75C4 (zha4) -75C5 (bing4) -75C6 (nie4) -75C7 (zheng4,zheng1) -75C8 (yong1) -75C9 (jing4) -75CA (quan2) -75CB (chong2) -75CC (tong1) -75CD (yi2) -75CE (jie4) -75CF (wei3) -75D0 (hui2) -75D1 (duo3) -75D2 (yang3) -75D3 (chi4) -75D4 (zhi4) -75D5 (hen2) -75D6 (ya3) -75D7 (mei4) -75D8 (dou4) -75D9 (jing4) -75DA (xiao1) -75DB (tong4) -75DC (tu1) -75DD (mang2) -75DE (pi3) -75DF (xiao1) -75E0 (suan1) -75E1 (pu1) -75E2 (li4) -75E3 (zhi4) -75E4 (cuo2) -75E5 (duo2) -75E6 (wu4) -75E7 (sha1) -75E8 (lao2) -75E9 (shou4) -75EA (huan4) -75EB (xian2) -75EC (yi4) -75ED (peng2) -75EE (zhang4) -75EF (guan3) -75F0 (tan2) -75F1 (fei4) -75F2 (ma2) -75F3 (lin2) -75F4 (chi1) -75F5 (ji4) -75F6 (tian3) -75F7 (an1) -75F8 (chi4) -75F9 (bi4) -75FA (bi4) -75FB (min2) -75FC (gu4,gu1) -75FD (dui1) -75FE (e1) -75FF (wei3) -7600 (yu1) -7601 (cui4) -7602 (ya3) -7603 (zhu2) -7604 (xi1) -7605 (dan4,dan1,dan3) -7606 (shen4) -7607 (zhong3) -7608 (ji4,zhi4) -7609 (yu4) -760A (hou2) -760B (feng1) -760C (la4) -760D (yang2) -760E (shen4) -760F (tu2) -7610 (yu3) -7611 (gua1) -7612 (wen2) -7613 (huan4) -7614 (ku4) -7615 (jia3,xia2) -7616 (yin1) -7617 (yi4) -7618 (lou4) -7619 (sao4) -761A (jue2) -761B (chi4) -761C (xi2) -761D (guan1) -761E (yi4) -761F (wen1) -7620 (ji2) -7621 (chuang1) -7622 (ban1) -7623 (lei3) -7624 (liu2) -7625 (chai4,cuo2) -7626 (shou4) -7627 (nu:e4,yao4) -7628 (dian1) -7629 (da2,da5) -762A (bie1,bie3) -762B (tan1) -762C (zhang4) -762D (biao1) -762E (shen4) -762F (cu4) -7630 (luo3) -7631 (yi4) -7632 (zong4) -7633 (chou1) -7634 (zhang4) -7635 (zhai4) -7636 (sou4) -7637 (suo3) -7638 (que2) -7639 (diao4) -763A (lou4) -763B (lou4) -763C (mo4) -763D (jin4) -763E (yin3) -763F (ying3) -7640 (huang2) -7641 (fu2) -7642 (liao2) -7643 (long2) -7644 (qiao2) -7645 (liu2) -7646 (lao2) -7647 (xian2) -7648 (fei4) -7649 (dan4,dan1,dan3) -764A (yin4) -764B (he4) -764C (ai2,yan2) -764D (ban1) -764E (xian2) -764F (guan1) -7650 (guai4) -7651 (nong2) -7652 (yu4) -7653 (wei2) -7654 (yi4) -7655 (yong1) -7656 (pi3,pi4) -7657 (lei3) -7658 (li4,ji1) -7659 (shu3) -765A (dan4) -765B (lin3) -765C (dian4) -765D (lin3) -765E (lai4) -765F (bie1,bie3) -7660 (ji4) -7661 (chi1) -7662 (yang3) -7663 (xuan3) -7664 (jie1) -7665 (zheng1,zheng4) -7666 (none0) -7667 (li4) -7668 (huo4) -7669 (lai4) -766A (ji1) -766B (dian1) -766C (xian3,xuan3) -766D (ying3) -766E (yin3) -766F (qu2) -7670 (yong1) -7671 (tan1) -7672 (dian1) -7673 (luo3) -7674 (luan2) -7675 (luan2) -7676 (bo1) -7677 (none0) -7678 (gui3) -7679 (po1) -767A (fa1) -767B (deng1) -767C (fa1) -767D (bai2) -767E (bai3,bo2) -767F (qie2) -7680 (bi1) -7681 (zao4) -7682 (zao4) -7683 (mao4) -7684 (de5,di4,di2) -7685 (pa1) -7686 (jie1) -7687 (huang2) -7688 (gui1) -7689 (ci3) -768A (ling2) -768B (gao1) -768C (mo4) -768D (ji4) -768E (jiao3,jia3) -768F (peng3) -7690 (gao1) -7691 (ai2) -7692 (e2) -7693 (hao4) -7694 (han4) -7695 (bi4) -7696 (wan3,huan3) -7697 (chou2) -7698 (qian4) -7699 (xi1) -769A (ai2) -769B (jiong3) -769C (hao4) -769D (huang3) -769E (hao4) -769F (ze2) -76A0 (cui2) -76A1 (hao4) -76A2 (xiao3) -76A3 (ye4) -76A4 (po2) -76A5 (hao4) -76A6 (jiao3) -76A7 (ai4) -76A8 (xing1) -76A9 (huang4) -76AA (li4) -76AB (piao3) -76AC (he4) -76AD (jiao4) -76AE (pi2) -76AF (gan3) -76B0 (pao4) -76B1 (zhou4) -76B2 (jun1) -76B3 (qiu2) -76B4 (cun1) -76B5 (que4) -76B6 (zha1) -76B7 (gu3) -76B8 (jun1) -76B9 (jun1) -76BA (zhou4) -76BB (zha1) -76BC (gu3) -76BD (zhan3) -76BE (du2) -76BF (min3) -76C0 (qi3) -76C1 (ying2) -76C2 (yu2) -76C3 (bei1) -76C4 (zhao1) -76C5 (zhong1) -76C6 (pen2) -76C7 (he2) -76C8 (ying2) -76C9 (he2) -76CA (yi4) -76CB (bo1) -76CC (wan3) -76CD (he2) -76CE (ang4) -76CF (zhan3) -76D0 (yan2) -76D1 (jian1,jian4) -76D2 (he2) -76D3 (yu1) -76D4 (kui1) -76D5 (fan4) -76D6 (gai4,ge3) -76D7 (dao4) -76D8 (pan2) -76D9 (fu3) -76DA (qiu2) -76DB (sheng4,cheng2) -76DC (dao4) -76DD (lu4) -76DE (zhan3) -76DF (meng2,ming2) -76E0 (lu4) -76E1 (jin4,jin3) -76E2 (xu4) -76E3 (jian1,jian4) -76E4 (pan2) -76E5 (guan4) -76E6 (an1) -76E7 (lu2) -76E8 (xu3) -76E9 (zhou1) -76EA (dang4) -76EB (an1) -76EC (gu3) -76ED (li4) -76EE (mu4) -76EF (ding1) -76F0 (gan3) -76F1 (xu1) -76F2 (mang2) -76F3 (mang2) -76F4 (zhi2) -76F5 (qi4) -76F6 (wan3) -76F7 (tian2) -76F8 (xiang1,xiang4) -76F9 (dun3) -76FA (xin1) -76FB (xi1) -76FC (pan4) -76FD (feng1) -76FE (dun4,shun3) -76FF (min2) -7700 (ming2) -7701 (sheng3,xing3) -7702 (shi4) -7703 (yun2) -7704 (mian3,mian4) -7705 (pan1) -7706 (fang3) -7707 (miao3) -7708 (dan1) -7709 (mei2) -770A (mao4) -770B (kan4,kan1) -770C (xian4) -770D (kou1) -770E (shi4) -770F (yang1) -7710 (zheng1) -7711 (yao3) -7712 (shen1) -7713 (huo4) -7714 (da4) -7715 (zhen3) -7716 (kuang4) -7717 (ju1) -7718 (shen4) -7719 (yi2) -771A (sheng3) -771B (mei4) -771C (mo4) -771D (zhu3) -771E (zhen1) -771F (zhen1) -7720 (mian2) -7721 (di1) -7722 (yuan1) -7723 (die2) -7724 (yi2) -7725 (zi4) -7726 (zi4) -7727 (chao3) -7728 (zha3) -7729 (xuan4) -772A (bing3) -772B (mi3) -772C (long2) -772D (sui1) -772E (tong2) -772F (mi1,mi2) -7730 (die2) -7731 (yi2) -7732 (er4) -7733 (ming3) -7734 (xuan4) -7735 (chi1) -7736 (kuang4) -7737 (juan4) -7738 (mou2) -7739 (zhen4) -773A (tiao4) -773B (yang2) -773C (yan3) -773D (mo4) -773E (zhong4) -773F (mai4) -7740 (zhe5,zhuo2,zhao2,zhao1) -7741 (zheng1) -7742 (mei2) -7743 (suo1) -7744 (shao4) -7745 (han4) -7746 (huan3) -7747 (di4) -7748 (cheng3) -7749 (cuo1) -774A (juan4) -774B (e2) -774C (wan3) -774D (xian4) -774E (xi1) -774F (kun4) -7750 (lai4) -7751 (jian3) -7752 (shan3) -7753 (tian3) -7754 (hun3) -7755 (wan3) -7756 (ling2) -7757 (shi4) -7758 (qiong2) -7759 (lie4) -775A (ya2,ai2) -775B (jing1) -775C (zheng1) -775D (li2) -775E (lai4) -775F (sui4) -7760 (juan4) -7761 (shui4) -7762 (sui1) -7763 (du1) -7764 (pi4) -7765 (pi4,bi4) -7766 (mu4) -7767 (hun1) -7768 (ni4) -7769 (lu4) -776A (gao1) -776B (jie2) -776C (cai3) -776D (zhou3) -776E (yu2) -776F (hun1) -7770 (ma4) -7771 (xia4) -7772 (xing3) -7773 (hui1) -7774 (gun4) -7775 (none0) -7776 (chun3) -7777 (jian1) -7778 (mei4) -7779 (du3) -777A (hou2) -777B (xuan1) -777C (ti2) -777D (kui2) -777E (gao1) -777F (rui4) -7780 (mao4) -7781 (xu4) -7782 (fa1) -7783 (wen1) -7784 (miao2) -7785 (chou3) -7786 (kui4) -7787 (mi1) -7788 (weng3) -7789 (kou4) -778A (dang4) -778B (chen1) -778C (ke1) -778D (sou3) -778E (xia1) -778F (qiong2) -7790 (mao4) -7791 (ming2) -7792 (man2) -7793 (shui4) -7794 (ze2) -7795 (zhang4) -7796 (yi4) -7797 (diao1) -7798 (kou1) -7799 (mo4) -779A (shun4) -779B (cong1) -779C (lou2) -779D (chi1) -779E (man2) -779F (piao3) -77A0 (cheng1) -77A1 (ji4) -77A2 (meng2) -77A3 (huan4) -77A4 (run2) -77A5 (pie1) -77A6 (xi1) -77A7 (qiao2,ya3) -77A8 (pu1) -77A9 (zhu3) -77AA (deng4) -77AB (shen3) -77AC (shun4) -77AD (liao4,liao3) -77AE (che4) -77AF (xian2) -77B0 (kan4) -77B1 (ye4) -77B2 (xu4) -77B3 (tong2) -77B4 (wu2) -77B5 (lin2) -77B6 (kui4) -77B7 (jian4) -77B8 (ye4) -77B9 (ai4) -77BA (hui4) -77BB (zhan1) -77BC (jian3) -77BD (gu3) -77BE (zhao4) -77BF (ju4,qu2,qu1) -77C0 (wei2) -77C1 (chou3) -77C2 (ji4) -77C3 (ning3) -77C4 (xun1) -77C5 (yao4) -77C6 (huo4) -77C7 (meng2) -77C8 (mian2) -77C9 (bin1,pin2) -77CA (mian2) -77CB (li4) -77CC (guang4) -77CD (jue2) -77CE (xuan1) -77CF (mian2) -77D0 (huo4) -77D1 (lu2) -77D2 (meng2) -77D3 (long2) -77D4 (guan4) -77D5 (man3) -77D6 (xi3) -77D7 (chu4) -77D8 (tang3) -77D9 (kan4) -77DA (zhu3) -77DB (mao2) -77DC (jin1,qin2,guan1) -77DD (lin2) -77DE (yu4) -77DF (shuo4) -77E0 (ce4) -77E1 (jue2) -77E2 (shi3) -77E3 (yi3) -77E4 (shen3) -77E5 (zhi1,zhi4) -77E6 (hou2,hou4) -77E7 (shen3) -77E8 (ying3) -77E9 (ju3,ju5) -77EA (zhou1) -77EB (jiao3,jia3,jiao2) -77EC (cuo2) -77ED (duan3) -77EE (ai3) -77EF (jiao3,jia3,jiao2) -77F0 (zeng1) -77F1 (huo4) -77F2 (bai3,bai4,pai3) -77F3 (shi2,dan4) -77F4 (ding4) -77F5 (qi4) -77F6 (ji1) -77F7 (zi3) -77F8 (gan1) -77F9 (wu4) -77FA (tuo1) -77FB (ku1) -77FC (qiang1) -77FD (xi1) -77FE (fan2) -77FF (kuang4) -7800 (dang4) -7801 (ma3) -7802 (sha1) -7803 (dan1) -7804 (jue2) -7805 (li4) -7806 (fu1) -7807 (min2) -7808 (nuo3) -7809 (hua1,xu1) -780A (kang4) -780B (zhi3) -780C (qi4,qie4) -780D (kan3) -780E (jie4) -780F (fen1) -7810 (e4) -7811 (ya4) -7812 (pi1) -7813 (zhe2) -7814 (yan2,yan4) -7815 (sui4) -7816 (zhuan1) -7817 (che1) -7818 (dun4) -7819 (pan1) -781A (yan4) -781B (none0) -781C (feng1) -781D (fa3) -781E (mo4) -781F (zha3,zuo4) -7820 (qu1) -7821 (yu4) -7822 (ke1) -7823 (tuo2) -7824 (tuo2) -7825 (di3) -7826 (zhai4) -7827 (zhen1) -7828 (e4) -7829 (fu2,fei4) -782A (mu3) -782B (zhu3) -782C (la2,li4) -782D (bian1) -782E (nu3) -782F (ping1) -7830 (peng1) -7831 (ling2) -7832 (pao4) -7833 (le4) -7834 (po4) -7835 (bo1) -7836 (po4) -7837 (shen1) -7838 (za2) -7839 (ai4) -783A (li4) -783B (long2) -783C (tong2) -783D (none0) -783E (li4) -783F (kuang4) -7840 (chu3) -7841 (keng1) -7842 (quan2) -7843 (zhu1) -7844 (kuang1) -7845 (gui1,huo4) -7846 (e4) -7847 (nao2) -7848 (jia2) -7849 (lu4) -784A (wei3,kui4) -784B (ai4) -784C (luo4,ge4) -784D (ken4) -784E (xing2) -784F (yan2) -7850 (dong4,dong3) -7851 (peng1) -7852 (xi1) -7853 (none0) -7854 (hong2) -7855 (shuo4) -7856 (xia2) -7857 (qiao1) -7858 (none0) -7859 (wei4) -785A (qiao2) -785B (none0) -785C (keng1) -785D (xiao1) -785E (que4) -785F (chan4) -7860 (lang3) -7861 (hong1) -7862 (yu2) -7863 (xiao1) -7864 (xia2) -7865 (mang3) -7866 (long4) -7867 (none0) -7868 (che1) -7869 (che4) -786A (wo4) -786B (liu2) -786C (ying4) -786D (mang2) -786E (que4) -786F (yan4) -7870 (cuo3) -7871 (kun3) -7872 (yu4) -7873 (none0) -7874 (none0) -7875 (lu3) -7876 (chen3) -7877 (jian3) -7878 (none0) -7879 (song1) -787A (zhuo2) -787B (keng1) -787C (peng2) -787D (yan3) -787E (zhui4) -787F (kong1) -7880 (ceng2) -7881 (qi2) -7882 (zong4) -7883 (qing4) -7884 (lin2) -7885 (jun1) -7886 (bo1) -7887 (ding4) -7888 (min2) -7889 (diao1) -788A (jian1) -788B (he4) -788C (liu4,lu4) -788D (ai4) -788E (sui4) -788F (que4) -7890 (ling2) -7891 (bei1) -7892 (yin2) -7893 (dui4) -7894 (wu3) -7895 (qi2) -7896 (lun2) -7897 (wan3) -7898 (dian3) -7899 (gang1) -789A (bei4) -789B (qi4) -789C (chen3) -789D (ruan3) -789E (yan2) -789F (die2) -78A0 (ding4) -78A1 (zhou5,zhou2) -78A2 (tuo2) -78A3 (jie2) -78A4 (ying1) -78A5 (bian3) -78A6 (ke4) -78A7 (bi4) -78A8 (wei1) -78A9 (shuo4,shi2) -78AA (zhen1) -78AB (duan4) -78AC (xia2) -78AD (dang4) -78AE (ti2) -78AF (nao3) -78B0 (peng4) -78B1 (jian3) -78B2 (di4) -78B3 (tan4) -78B4 (cha2,cha1) -78B5 (none0) -78B6 (qi4) -78B7 (none0) -78B8 (feng1) -78B9 (xuan4) -78BA (que4) -78BB (que4) -78BC (ma3) -78BD (gong1) -78BE (nian3) -78BF (su4) -78C0 (e2) -78C1 (ci2) -78C2 (liu4) -78C3 (si1) -78C4 (tang2) -78C5 (bang4,pang2,pang1) -78C6 (hua2) -78C7 (pi1) -78C8 (wei3) -78C9 (sang3) -78CA (lei3) -78CB (cuo1) -78CC (tian2) -78CD (xia2) -78CE (xi1) -78CF (lian2) -78D0 (pan2) -78D1 (wei4) -78D2 (yun3) -78D3 (dui1) -78D4 (zhe2) -78D5 (ke1) -78D6 (la2) -78D7 (none0) -78D8 (qing4) -78D9 (gun3) -78DA (zhuan1) -78DB (chan2) -78DC (qi4) -78DD (ao2) -78DE (peng1) -78DF (lu4) -78E0 (lu3) -78E1 (kan4) -78E2 (qiang3) -78E3 (chen3) -78E4 (yin3) -78E5 (lei3) -78E6 (biao1) -78E7 (qi4) -78E8 (mo2,mo4) -78E9 (qi1) -78EA (cui1) -78EB (zong1) -78EC (qing4) -78ED (chuo4) -78EE (none0) -78EF (ji1) -78F0 (shan4) -78F1 (lao2) -78F2 (qu2) -78F3 (zeng1) -78F4 (deng4) -78F5 (jian4) -78F6 (xi4) -78F7 (lin2) -78F8 (ding4) -78F9 (dian4) -78FA (huang2) -78FB (pan2) -78FC (za2) -78FD (qiao1) -78FE (di1) -78FF (li4) -7900 (jian4) -7901 (jiao1) -7902 (xi1) -7903 (zhang3) -7904 (qiao2) -7905 (dun1) -7906 (jian3) -7907 (yu4) -7908 (zhui4) -7909 (he2) -790A (huo4) -790B (zhai2) -790C (lei2) -790D (ke3) -790E (chu3) -790F (ji2) -7910 (que4) -7911 (dang4) -7912 (wo4,yi3) -7913 (jiang1) -7914 (pi4) -7915 (pi1) -7916 (yu4) -7917 (pin1) -7918 (qi4) -7919 (ai4) -791A (ke1) -791B (jian1) -791C (yu4) -791D (ruan3) -791E (meng2) -791F (pao4) -7920 (zi1) -7921 (bo2) -7922 (none0) -7923 (mie4) -7924 (ca3) -7925 (xian2) -7926 (kuang4,gong3) -7927 (lei3) -7928 (lei3) -7929 (zhi4) -792A (li4) -792B (li4) -792C (fan2) -792D (que4) -792E (pao4) -792F (ying1) -7930 (li4) -7931 (long2) -7932 (long2) -7933 (mo4) -7934 (bo2) -7935 (shuang1) -7936 (guan4) -7937 (lan2) -7938 (zan3) -7939 (yan2) -793A (shi4) -793B (shi4) -793C (li3) -793D (reng2) -793E (she4) -793F (yue4) -7940 (si4) -7941 (qi2) -7942 (ta1) -7943 (ma4) -7944 (xie4) -7945 (yao1) -7946 (xian1) -7947 (zhi3,qi2,zhi1) -7948 (qi2) -7949 (zhi3) -794A (beng1) -794B (shu1) -794C (chong1) -794D (none0) -794E (yi1) -794F (shi2) -7950 (you4) -7951 (zhi4) -7952 (tiao2) -7953 (fu2) -7954 (fu4) -7955 (mi4) -7956 (zu3) -7957 (zhi1) -7958 (suan4) -7959 (mei4) -795A (zuo4) -795B (qu1) -795C (hu4) -795D (zhu4) -795E (shen2) -795F (sui4) -7960 (ci2) -7961 (chai2) -7962 (mi2,ni3) -7963 (lu:3) -7964 (yu3) -7965 (xiang2) -7966 (wu2) -7967 (tiao1) -7968 (piao4) -7969 (zhu1) -796A (gui3) -796B (xia2) -796C (zhi1) -796D (ji4,zhai4) -796E (gao4) -796F (zhen1) -7970 (gao4) -7971 (shui4) -7972 (jin4) -7973 (zhen3) -7974 (gai1,jie4) -7975 (kun3) -7976 (di4) -7977 (dao3) -7978 (huo4) -7979 (tao2) -797A (qi2) -797B (gu4) -797C (guan4) -797D (zui4) -797E (ling2) -797F (lu4) -7980 (bing3) -7981 (jin4,jin1) -7982 (dao3) -7983 (zhi2) -7984 (lu4) -7985 (shan4,chan2) -7986 (bei1) -7987 (zhe3) -7988 (hui1) -7989 (you3) -798A (xi4) -798B (yin1) -798C (zi1) -798D (huo4) -798E (zhen1) -798F (fu2) -7990 (yuan4) -7991 (wu2) -7992 (xian3) -7993 (yang2) -7994 (ti2) -7995 (yi1) -7996 (mei2) -7997 (si1) -7998 (di4) -7999 (none0) -799A (zhuo2) -799B (zhen1) -799C (yong3) -799D (ji4) -799E (gao4) -799F (tang2) -79A0 (chi3) -79A1 (ma4) -79A2 (ta1) -79A3 (none0) -79A4 (xuan1) -79A5 (qi2) -79A6 (yu4) -79A7 (xi3) -79A8 (ji1) -79A9 (si4) -79AA (chan2,shan4) -79AB (xuan1) -79AC (hui4) -79AD (sui4) -79AE (li3) -79AF (nong2) -79B0 (ni3,mi2) -79B1 (dao3) -79B2 (li4) -79B3 (rang2,rang3) -79B4 (yue4) -79B5 (ti2) -79B6 (zan3) -79B7 (lei4) -79B8 (rou2) -79B9 (yu3) -79BA (yu2,yu4) -79BB (li2) -79BC (xie4) -79BD (qin2) -79BE (he2) -79BF (tu1) -79C0 (xiu4) -79C1 (si1) -79C2 (ren2) -79C3 (tu1) -79C4 (zi3) -79C5 (cha2) -79C6 (gan3) -79C7 (yi4) -79C8 (xian1) -79C9 (bing3) -79CA (nian2) -79CB (qiu1) -79CC (qiu1) -79CD (zhong3,chong2,zhong4) -79CE (fen4) -79CF (hao4) -79D0 (yun2) -79D1 (ke1) -79D2 (miao3) -79D3 (zhi1) -79D4 (jing1) -79D5 (bi3) -79D6 (zhi1) -79D7 (yu4) -79D8 (mi4,bi4,lin2) -79D9 (ku4) -79DA (ban4) -79DB (pi1) -79DC (ni2) -79DD (li4) -79DE (you2) -79DF (zu1) -79E0 (pi1) -79E1 (ba2) -79E2 (ling2) -79E3 (mo4) -79E4 (cheng4,chen4,cheng1) -79E5 (nian2) -79E6 (qin2) -79E7 (yang1) -79E8 (zuo2) -79E9 (zhi4) -79EA (zhi1) -79EB (shu2) -79EC (ju4) -79ED (zi3) -79EE (tai2) -79EF (ji1) -79F0 (cheng1,chen4,cheng4) -79F1 (tong2) -79F2 (zhi4) -79F3 (huo2) -79F4 (he2) -79F5 (yin1) -79F6 (zi1) -79F7 (zhi2) -79F8 (jie1) -79F9 (ren3) -79FA (du4) -79FB (yi2) -79FC (zhu4) -79FD (hui4) -79FE (nong2) -79FF (fu3) -7A00 (xi1) -7A01 (kao3) -7A02 (lang2) -7A03 (fu1,fu2) -7A04 (ze4) -7A05 (shui4) -7A06 (lu:3) -7A07 (kun3) -7A08 (gan3) -7A09 (jing1) -7A0A (ti2) -7A0B (cheng2) -7A0C (tu2) -7A0D (shao1,shao4) -7A0E (shui4) -7A0F (ya4) -7A10 (lun3) -7A11 (lu4) -7A12 (gu4) -7A13 (zuo2) -7A14 (ren3) -7A15 (zhun4) -7A16 (bang4) -7A17 (bai4,bi4) -7A18 (ji1,qi2) -7A19 (zhi2) -7A1A (zhi4) -7A1B (kun3) -7A1C (leng2) -7A1D (peng2) -7A1E (ke1) -7A1F (bing3) -7A20 (chou2) -7A21 (zui4) -7A22 (yu4) -7A23 (su1) -7A24 (none0) -7A25 (none0) -7A26 (yi1) -7A27 (xi4) -7A28 (bian1) -7A29 (ji4) -7A2A (fu4) -7A2B (bi1) -7A2C (nuo4) -7A2D (jie1) -7A2E (zhong3,chong2,zhong4) -7A2F (zong1) -7A30 (xu1) -7A31 (cheng1,chen4,cheng4) -7A32 (dao4) -7A33 (wen3) -7A34 (lian2) -7A35 (zi1) -7A36 (yu4) -7A37 (ji4) -7A38 (xu4) -7A39 (zhen3) -7A3A (zhi4) -7A3B (dao4) -7A3C (jia4) -7A3D (ji1,qi3) -7A3E (gao3) -7A3F (gao3) -7A40 (gu3) -7A41 (rong2) -7A42 (sui4) -7A43 (none0) -7A44 (ji4) -7A45 (kang1) -7A46 (mu4) -7A47 (shan1) -7A48 (men2) -7A49 (zhi4) -7A4A (ji4) -7A4B (lu4) -7A4C (su1,wei4) -7A4D (ji1) -7A4E (ying3) -7A4F (wen3) -7A50 (qiu1) -7A51 (se4) -7A52 (none0) -7A53 (yi4) -7A54 (huang2) -7A55 (qie4) -7A56 (ji3) -7A57 (sui4) -7A58 (xiao1) -7A59 (pu2) -7A5A (jiao1) -7A5B (zhuo1) -7A5C (tong2) -7A5D (none0) -7A5E (lu:3) -7A5F (sui4) -7A60 (nong2) -7A61 (se4) -7A62 (hui4) -7A63 (rang2) -7A64 (nuo4) -7A65 (yu4) -7A66 (none0) -7A67 (ji4) -7A68 (tui2) -7A69 (wen3) -7A6A (cheng1,chen4,cheng4) -7A6B (huo4) -7A6C (gong3) -7A6D (lu:3) -7A6E (biao1) -7A6F (none0) -7A70 (rang2) -7A71 (jue2) -7A72 (li2) -7A73 (zan4) -7A74 (xue2,xue4) -7A75 (wa1) -7A76 (jiu1,jiu4) -7A77 (qiong2) -7A78 (xi1) -7A79 (qiong2,qiong1) -7A7A (kong1,kong4) -7A7B (yu1) -7A7C (sen1) -7A7D (jing3) -7A7E (yao4) -7A7F (chuan1) -7A80 (zhun1) -7A81 (tu1,tu2) -7A82 (lao2) -7A83 (qie4) -7A84 (zhai3,ze2) -7A85 (yao3) -7A86 (bian3) -7A87 (bao2) -7A88 (yao3) -7A89 (bing3) -7A8A (yu3) -7A8B (zhu2) -7A8C (jiao4) -7A8D (qiao4) -7A8E (diao4) -7A8F (wu1) -7A90 (gui1) -7A91 (yao2) -7A92 (zhi4) -7A93 (chuan1) -7A94 (yao3) -7A95 (tiao3) -7A96 (jiao4) -7A97 (chuang1) -7A98 (jiong3,jun3) -7A99 (xiao1) -7A9A (cheng2) -7A9B (kou4) -7A9C (cuan4) -7A9D (wo1) -7A9E (dan4) -7A9F (ku1) -7AA0 (ke1) -7AA1 (zhui4) -7AA2 (xu4) -7AA3 (su1) -7AA4 (none0) -7AA5 (kui1) -7AA6 (dou4) -7AA7 (none0) -7AA8 (yin4,xun1) -7AA9 (wo1) -7AAA (wa1) -7AAB (ya4) -7AAC (yu2) -7AAD (ju4) -7AAE (qiong2) -7AAF (yao2) -7AB0 (yao2) -7AB1 (tiao4) -7AB2 (liao4) -7AB3 (yu3) -7AB4 (tian2) -7AB5 (diao4) -7AB6 (ju4) -7AB7 (liao2) -7AB8 (xi1) -7AB9 (wu4) -7ABA (kui1) -7ABB (chuang1) -7ABC (ju3) -7ABD (none0) -7ABE (kuan3) -7ABF (long2) -7AC0 (cheng1) -7AC1 (cui4) -7AC2 (piao2) -7AC3 (zao4) -7AC4 (cuan4) -7AC5 (qiao4) -7AC6 (qiong2) -7AC7 (dou4) -7AC8 (zao4) -7AC9 (zao4) -7ACA (qie4) -7ACB (li4) -7ACC (chu4) -7ACD (shi2,gong1,sheng1) -7ACE (fu4) -7ACF (qian1,gong1,sheng1) -7AD0 (chu4) -7AD1 (hong2) -7AD2 (qi2,ji1) -7AD3 (qian1,fen1,zhi1,yi1,gong1,sheng1) -7AD4 (gong1,sheng1) -7AD5 (shi2,fen1,zhi1,yi1,gong1,sheng1) -7AD6 (shu4) -7AD7 (miao4) -7AD8 (ju3) -7AD9 (zhan4) -7ADA (zhu4) -7ADB (ling2) -7ADC (long2) -7ADD (bing4,bing1) -7ADE (jing4) -7ADF (jing4) -7AE0 (zhang1) -7AE1 (yi1,gong1,sheng1,bai3,bei4,si4) -7AE2 (si4,qi2) -7AE3 (jun4) -7AE4 (hong2) -7AE5 (tong2) -7AE6 (song3) -7AE7 (jing4) -7AE8 (diao4) -7AE9 (yi4) -7AEA (shu4) -7AEB (jing4) -7AEC (qu3) -7AED (jie2) -7AEE (ping2) -7AEF (duan1) -7AF0 (shao2) -7AF1 (zhuan3) -7AF2 (ceng2) -7AF3 (deng1) -7AF4 (cun1) -7AF5 (huai1) -7AF6 (jing4) -7AF7 (kan4) -7AF8 (jing4) -7AF9 (zhu2) -7AFA (zhu2) -7AFB (le4) -7AFC (peng2) -7AFD (yu2) -7AFE (chi2) -7AFF (gan1) -7B00 (mang2) -7B01 (zhu2) -7B02 (none0) -7B03 (du3) -7B04 (ji1) -7B05 (xiao2) -7B06 (ba1) -7B07 (suan4) -7B08 (ji2) -7B09 (zhen3) -7B0A (zhao4) -7B0B (sun3) -7B0C (ya2) -7B0D (zhui4) -7B0E (yuan2) -7B0F (hu4) -7B10 (gang1) -7B11 (xiao4) -7B12 (cen2) -7B13 (pi2) -7B14 (bi3) -7B15 (jian3) -7B16 (yi3) -7B17 (dong1) -7B18 (shan1) -7B19 (sheng1) -7B1A (xia2) -7B1B (di2) -7B1C (zhu2) -7B1D (na4) -7B1E (chi1) -7B1F (gu1) -7B20 (li4) -7B21 (qie4) -7B22 (min3) -7B23 (bao1) -7B24 (tiao2) -7B25 (si4) -7B26 (fu2) -7B27 (ce4) -7B28 (ben4) -7B29 (fa2) -7B2A (da2) -7B2B (zi3) -7B2C (di4) -7B2D (ling2) -7B2E (ze2,zuo2) -7B2F (nu2) -7B30 (fu2) -7B31 (gou3) -7B32 (fan2) -7B33 (jia1) -7B34 (ge3) -7B35 (fan4) -7B36 (shi3) -7B37 (mao3) -7B38 (po3) -7B39 (none0) -7B3A (jian1) -7B3B (qiong2) -7B3C (long3,long2) -7B3D (none0) -7B3E (bian1) -7B3F (luo4) -7B40 (gui4) -7B41 (qu3,qu1) -7B42 (chi2) -7B43 (yin1) -7B44 (yao4) -7B45 (xian3) -7B46 (bi3) -7B47 (qiong2) -7B48 (gua1) -7B49 (deng3) -7B4A (jiao3) -7B4B (jin1) -7B4C (quan2) -7B4D (sun3) -7B4E (ru2) -7B4F (fa2) -7B50 (kuang1) -7B51 (zhu4,zhu2) -7B52 (tong3) -7B53 (ji1) -7B54 (da2,da1) -7B55 (hang2) -7B56 (ce4) -7B57 (zhong4) -7B58 (kou4) -7B59 (lai2) -7B5A (bi4) -7B5B (shai1) -7B5C (dang1) -7B5D (zheng1) -7B5E (ce4) -7B5F (fu1) -7B60 (yun2,jun1) -7B61 (tu2) -7B62 (pa2) -7B63 (li4) -7B64 (lang2) -7B65 (ju3) -7B66 (guan3) -7B67 (jian3) -7B68 (han2) -7B69 (tong3) -7B6A (xia2) -7B6B (zhi4) -7B6C (cheng2) -7B6D (suan4) -7B6E (shi4) -7B6F (zhu4) -7B70 (zuo2) -7B71 (xiao3) -7B72 (shao1) -7B73 (ting2) -7B74 (jia2,ce4) -7B75 (yan2) -7B76 (gao3) -7B77 (kuai4) -7B78 (gan1) -7B79 (chou2) -7B7A (kuang1) -7B7B (gang4) -7B7C (yun2) -7B7D (none0) -7B7E (qian1) -7B7F (xiao3) -7B80 (jian3) -7B81 (pu2) -7B82 (lai2) -7B83 (zou1) -7B84 (bi4) -7B85 (bi4) -7B86 (bi4) -7B87 (ge4) -7B88 (chi2) -7B89 (guai3) -7B8A (yu1) -7B8B (jian1) -7B8C (zhao4) -7B8D (gu1) -7B8E (chi2) -7B8F (zheng1) -7B90 (qing4) -7B91 (sha4) -7B92 (zhou3) -7B93 (lu4) -7B94 (bo2) -7B95 (ji1,ji5) -7B96 (lin2) -7B97 (suan4) -7B98 (jun4) -7B99 (fu1) -7B9A (zha2) -7B9B (gu1) -7B9C (kong1) -7B9D (qian2) -7B9E (qian1) -7B9F (jun4,jun1) -7BA0 (chui2) -7BA1 (guan3) -7BA2 (yuan1) -7BA3 (ce4) -7BA4 (ju2) -7BA5 (bo3) -7BA6 (ze2) -7BA7 (qie4) -7BA8 (tuo4) -7BA9 (luo2) -7BAA (dan1) -7BAB (xiao1) -7BAC (ruo4) -7BAD (jian4) -7BAE (none0) -7BAF (bian1) -7BB0 (sun3) -7BB1 (xiang1) -7BB2 (xian3) -7BB3 (ping2) -7BB4 (zhen1) -7BB5 (sheng3) -7BB6 (hu2) -7BB7 (shi1,yi2) -7BB8 (zhu4) -7BB9 (yue1) -7BBA (chun1) -7BBB (fu1) -7BBC (wu1) -7BBD (dong3) -7BBE (shuo4) -7BBF (ji2) -7BC0 (jie2,jie1) -7BC1 (huang2) -7BC2 (xing1) -7BC3 (mei2) -7BC4 (fan4) -7BC5 (chuan2) -7BC6 (zhuan4) -7BC7 (pian1) -7BC8 (feng1) -7BC9 (zhu2,zhu4) -7BCA (hong2) -7BCB (qie4) -7BCC (hou2) -7BCD (qiu1) -7BCE (miao3) -7BCF (qian4) -7BD0 (none0) -7BD1 (kui4) -7BD2 (none0) -7BD3 (lou3) -7BD4 (yun2) -7BD5 (he2) -7BD6 (tang2) -7BD7 (yue4) -7BD8 (chou1) -7BD9 (gao1) -7BDA (fei3) -7BDB (ruo4) -7BDC (zheng1) -7BDD (gou1) -7BDE (nie4) -7BDF (qian4) -7BE0 (xiao3) -7BE1 (cuan4) -7BE2 (gong1) -7BE3 (pang2) -7BE4 (du3) -7BE5 (li4) -7BE6 (bi4) -7BE7 (zhuo2) -7BE8 (chu2) -7BE9 (shai1) -7BEA (chi2) -7BEB (zhu2) -7BEC (qiang1) -7BED (long2,long3) -7BEE (lan2) -7BEF (jian1) -7BF0 (bu4) -7BF1 (li2) -7BF2 (hui4) -7BF3 (bi4) -7BF4 (di2) -7BF5 (cong1) -7BF6 (yan1) -7BF7 (peng2) -7BF8 (sen1) -7BF9 (cuan4) -7BFA (pai2) -7BFB (piao4) -7BFC (dou1) -7BFD (yu3) -7BFE (mie4) -7BFF (zhuan1) -7C00 (ze2,kui4) -7C01 (xi3) -7C02 (guo2) -7C03 (yi2) -7C04 (hu4) -7C05 (chan3) -7C06 (kou4) -7C07 (cu4) -7C08 (ping2) -7C09 (zao4) -7C0A (ji1) -7C0B (gui3) -7C0C (su4) -7C0D (lou3) -7C0E (zha4) -7C0F (lu4) -7C10 (nian3) -7C11 (suo1) -7C12 (cuan4) -7C13 (none0) -7C14 (suo1) -7C15 (le4) -7C16 (duan4) -7C17 (liang2) -7C18 (xiao1) -7C19 (bo2) -7C1A (mi4) -7C1B (shai1) -7C1C (dang4) -7C1D (liao2) -7C1E (dan1) -7C1F (dian4) -7C20 (fu3) -7C21 (jian3) -7C22 (min3) -7C23 (kui4) -7C24 (dai4) -7C25 (qiao2) -7C26 (deng1) -7C27 (huang2) -7C28 (sun3) -7C29 (lao2) -7C2A (zan1) -7C2B (xiao1) -7C2C (lu4) -7C2D (shi4) -7C2E (zan1) -7C2F (none0) -7C30 (pai2) -7C31 (qi2) -7C32 (pai2) -7C33 (gan4) -7C34 (ju4) -7C35 (du4) -7C36 (lu4) -7C37 (yan2) -7C38 (bo4,bo3) -7C39 (dang1) -7C3A (sai4) -7C3B (ke1) -7C3C (gou4) -7C3D (qian1) -7C3E (lian2) -7C3F (bu4) -7C40 (zhou4) -7C41 (lai4) -7C42 (none0) -7C43 (lan2) -7C44 (kui4) -7C45 (yu2) -7C46 (yue4) -7C47 (hao2) -7C48 (zhen1) -7C49 (tai2) -7C4A (ti4) -7C4B (mi2) -7C4C (chou2) -7C4D (ji2) -7C4E (none0) -7C4F (qi2) -7C50 (teng2) -7C51 (zhuan4) -7C52 (zhou4) -7C53 (fan1) -7C54 (sou3) -7C55 (zhou4) -7C56 (qian1) -7C57 (kuo4) -7C58 (teng2) -7C59 (lu4) -7C5A (lu2) -7C5B (jian1) -7C5C (tuo4) -7C5D (ying2) -7C5E (yu4) -7C5F (lai4) -7C60 (long2,long3) -7C61 (none0) -7C62 (lian2) -7C63 (lan2) -7C64 (qian1) -7C65 (yue4) -7C66 (zhong1) -7C67 (qu2) -7C68 (lian2) -7C69 (bian1) -7C6A (duan4) -7C6B (zuan3) -7C6C (li2) -7C6D (shai1) -7C6E (luo2) -7C6F (ying2) -7C70 (yue4) -7C71 (zhuo2) -7C72 (xu1,yu1,yu4) -7C73 (mi3) -7C74 (di2) -7C75 (fan2) -7C76 (shen1) -7C77 (zhe2) -7C78 (shen1) -7C79 (nu:3) -7C7A (xie2) -7C7B (lei4) -7C7C (xian1) -7C7D (zi3) -7C7E (ni2) -7C7F (cun4) -7C80 (zhang4) -7C81 (qian1) -7C82 (none0) -7C83 (bi3) -7C84 (ban3) -7C85 (wu4) -7C86 (sha1) -7C87 (kang1) -7C88 (rou3) -7C89 (fen3) -7C8A (bi4) -7C8B (cui4,sui4) -7C8C (yin3) -7C8D (li2) -7C8E (chi3) -7C8F (tai4) -7C90 (none0) -7C91 (ba1) -7C92 (li4) -7C93 (gan1) -7C94 (ju4) -7C95 (po4) -7C96 (mo4) -7C97 (cu1) -7C98 (zhan1,nian2) -7C99 (zhou4) -7C9A (li2) -7C9B (su4) -7C9C (tiao4) -7C9D (li4) -7C9E (xi1) -7C9F (su4) -7CA0 (hong2) -7CA1 (tong2) -7CA2 (zi1,ci2) -7CA3 (ce4) -7CA4 (yue4) -7CA5 (zhou1,yu4) -7CA6 (lin2) -7CA7 (zhuang1) -7CA8 (bai3) -7CA9 (none0) -7CAA (fen4) -7CAB (mian4) -7CAC (qu1) -7CAD (none0) -7CAE (liang2) -7CAF (xian4) -7CB0 (fu1) -7CB1 (liang2) -7CB2 (can4) -7CB3 (jing1,geng1) -7CB4 (li3) -7CB5 (yue4) -7CB6 (lu4) -7CB7 (ju2) -7CB8 (qi2) -7CB9 (cui4) -7CBA (bai4) -7CBB (chang2) -7CBC (lin2) -7CBD (zong4) -7CBE (jing1) -7CBF (guo3) -7CC0 (none0) -7CC1 (san3,shen1) -7CC2 (san3,shen1) -7CC3 (tang2) -7CC4 (bian3) -7CC5 (rou2,rou3) -7CC6 (mian4) -7CC7 (hou2) -7CC8 (xu3) -7CC9 (zong4) -7CCA (hu2,hu1,hu4) -7CCB (jian4) -7CCC (zan1) -7CCD (ci2) -7CCE (li2) -7CCF (xie4) -7CD0 (fu1) -7CD1 (nuo4) -7CD2 (bei4) -7CD3 (gu3,yu4) -7CD4 (xiu3) -7CD5 (gao1) -7CD6 (tang2) -7CD7 (qiu3) -7CD8 (none0) -7CD9 (cao1) -7CDA (zhuang1) -7CDB (tang2) -7CDC (mi2,mei2) -7CDD (san3,shen1) -7CDE (fen4) -7CDF (zao1) -7CE0 (kang1) -7CE1 (jiang4) -7CE2 (mo2) -7CE3 (san3) -7CE4 (san3) -7CE5 (nuo4) -7CE6 (chi4) -7CE7 (liang2) -7CE8 (jiang4) -7CE9 (kuai1) -7CEA (bo2) -7CEB (huan2) -7CEC (shu3) -7CED (zong4) -7CEE (jian4) -7CEF (nuo4) -7CF0 (tuan2) -7CF1 (nie4) -7CF2 (li4) -7CF3 (zuo4) -7CF4 (di2) -7CF5 (nie4) -7CF6 (tiao4) -7CF7 (lan2) -7CF8 (mi4) -7CF9 (mi4) -7CFA (jiu1) -7CFB (xi4,ji4) -7CFC (gong1) -7CFD (zheng3) -7CFE (jiu1,jiu3) -7CFF (you4) -7D00 (ji4) -7D01 (cha4) -7D02 (zhou4) -7D03 (xun2) -7D04 (yue1,yao1) -7D05 (hong2,gong1) -7D06 (yu1) -7D07 (he2,ge1) -7D08 (wan2) -7D09 (ren4) -7D0A (wen3,wen4) -7D0B (wen2,wen4) -7D0C (qiu2) -7D0D (na4) -7D0E (zi1) -7D0F (tou3) -7D10 (niu3) -7D11 (fou2) -7D12 (jie4) -7D13 (shu1) -7D14 (chun2) -7D15 (pi2,pi1) -7D16 (yin3) -7D17 (sha1) -7D18 (hong2) -7D19 (zhi3) -7D1A (ji2) -7D1B (fen1) -7D1C (yun2) -7D1D (ren2) -7D1E (dan3) -7D1F (jin1) -7D20 (su4) -7D21 (fang3) -7D22 (suo3) -7D23 (cui4) -7D24 (jiu3) -7D25 (zha2) -7D26 (ba1) -7D27 (jin3) -7D28 (fu1) -7D29 (zhi4) -7D2A (qi1) -7D2B (zi3) -7D2C (chou2) -7D2D (hong2) -7D2E (zha2,za1) -7D2F (lei3,lei4,lei2) -7D30 (xi4) -7D31 (fu2) -7D32 (xie4) -7D33 (shen1) -7D34 (bei4) -7D35 (zhu4) -7D36 (qu3,qu1) -7D37 (ling2) -7D38 (zhu4) -7D39 (shao4) -7D3A (gan4) -7D3B (yang1) -7D3C (fu2) -7D3D (tuo2) -7D3E (zhen3) -7D3F (dai4) -7D40 (chu4) -7D41 (shi1) -7D42 (zhong1) -7D43 (xian2) -7D44 (zu3) -7D45 (jiong3) -7D46 (ban4) -7D47 (ju4) -7D48 (pa4) -7D49 (shu4) -7D4A (zui4) -7D4B (kuang4) -7D4C (jing1,jing4) -7D4D (ren4) -7D4E (heng4,hang2) -7D4F (xie4) -7D50 (jie2,jie1) -7D51 (zhu1) -7D52 (chou2) -7D53 (gua4) -7D54 (bai3) -7D55 (jue2) -7D56 (kuang4) -7D57 (hu2) -7D58 (ci4) -7D59 (geng1) -7D5A (geng1) -7D5B (tao1) -7D5C (xie2,jie2) -7D5D (ku4) -7D5E (jiao3,jia3) -7D5F (quan1) -7D60 (gai3) -7D61 (luo4,lao4) -7D62 (xuan4) -7D63 (beng1,ping3) -7D64 (xian4) -7D65 (fu2) -7D66 (gei3,ji3) -7D67 (tong2) -7D68 (rong2) -7D69 (tiao4) -7D6A (yin1) -7D6B (lei3) -7D6C (xie4) -7D6D (quan4) -7D6E (xu4) -7D6F (hai4) -7D70 (die2) -7D71 (tong3) -7D72 (si1) -7D73 (jiang4) -7D74 (xiang2) -7D75 (hui4) -7D76 (jue2) -7D77 (zhi2) -7D78 (jian3) -7D79 (juan4) -7D7A (chi1) -7D7B (mian3) -7D7C (zhen3) -7D7D (lu:3) -7D7E (cheng2) -7D7F (qiu2) -7D80 (shu1) -7D81 (bang3) -7D82 (tong3) -7D83 (xiao1) -7D84 (wan4) -7D85 (qin1) -7D86 (geng3) -7D87 (xiu3) -7D88 (ti2,di4,ti4) -7D89 (xiu4) -7D8A (xie2) -7D8B (hong2) -7D8C (xi4) -7D8D (fu2) -7D8E (ting2) -7D8F (sui1,sui2) -7D90 (dui4) -7D91 (kun3) -7D92 (fu1) -7D93 (jing1,jing4) -7D94 (hu4) -7D95 (zhi1) -7D96 (yan2) -7D97 (jiong3) -7D98 (feng2) -7D99 (ji4) -7D9A (xu4) -7D9B (none0) -7D9C (zong4,zeng4,zong1) -7D9D (lin3) -7D9E (duo3) -7D9F (li4) -7DA0 (lu:4) -7DA1 (liang2) -7DA2 (chou2) -7DA3 (quan3) -7DA4 (shao4) -7DA5 (qi4) -7DA6 (qi2) -7DA7 (zhun3) -7DA8 (qi2) -7DA9 (wan3) -7DAA (qian4) -7DAB (xian4) -7DAC (shou4) -7DAD (wei2) -7DAE (qi3,qing4,qing3) -7DAF (tao2) -7DB0 (wan3) -7DB1 (gang1) -7DB2 (wang3) -7DB3 (beng1,beng3,beng4) -7DB4 (zhui4) -7DB5 (cai3) -7DB6 (guo3) -7DB7 (cui4) -7DB8 (lun2,guan1) -7DB9 (liu3) -7DBA (qi3) -7DBB (zhan4) -7DBC (bei1) -7DBD (chuo4) -7DBE (ling2) -7DBF (mian2) -7DC0 (qi1) -7DC1 (jie2) -7DC2 (tan1) -7DC3 (zong1) -7DC4 (gun3) -7DC5 (zou1) -7DC6 (yi4) -7DC7 (zi1) -7DC8 (xing4) -7DC9 (liang3) -7DCA (jin3) -7DCB (fei1) -7DCC (rui2) -7DCD (min2) -7DCE (yu4) -7DCF (zong3) -7DD0 (fan2) -7DD1 (lu:4) -7DD2 (xu4) -7DD3 (none0) -7DD4 (shang4) -7DD5 (none0) -7DD6 (xu4) -7DD7 (xiang1) -7DD8 (jian1) -7DD9 (ke4) -7DDA (xian4) -7DDB (ruan3) -7DDC (mian2) -7DDD (ji1,qi1,qi4) -7DDE (duan4) -7DDF (zhong4) -7DE0 (di4) -7DE1 (min2) -7DE2 (miao2) -7DE3 (yuan2) -7DE4 (xie4) -7DE5 (bao3) -7DE6 (si1) -7DE7 (qiu1) -7DE8 (bian1) -7DE9 (huan3) -7DEA (geng1) -7DEB (zong3) -7DEC (mian3) -7DED (wei4) -7DEE (fu4) -7DEF (wei3) -7DF0 (yu2) -7DF1 (gou1) -7DF2 (miao3) -7DF3 (jie2) -7DF4 (lian4) -7DF5 (zong1) -7DF6 (bian4,pian2) -7DF7 (yun4) -7DF8 (yin1) -7DF9 (ti2) -7DFA (gua1) -7DFB (zhi4) -7DFC (yun1) -7DFD (cheng1) -7DFE (chan2) -7DFF (dai4) -7E00 (jia1) -7E01 (yuan2) -7E02 (zong3) -7E03 (xu1) -7E04 (sheng2) -7E05 (none0) -7E06 (geng1) -7E07 (none0) -7E08 (ying2) -7E09 (jin4) -7E0A (yi4) -7E0B (zhui4) -7E0C (ni4) -7E0D (bang1) -7E0E (gu3) -7E0F (pan2) -7E10 (zhou4) -7E11 (jian1) -7E12 (cuo3) -7E13 (quan2) -7E14 (shuang3) -7E15 (yun1) -7E16 (xia2) -7E17 (shuai1) -7E18 (xi1) -7E19 (rong2) -7E1A (tao1) -7E1B (fu2) -7E1C (yun2) -7E1D (zhen3) -7E1E (gao3) -7E1F (ru4) -7E20 (hu2) -7E21 (zai3) -7E22 (teng2) -7E23 (xian4,xuan2) -7E24 (su4) -7E25 (zhen3) -7E26 (zong4) -7E27 (tao1) -7E28 (huang3) -7E29 (cai4) -7E2A (bi4,bie4) -7E2B (feng2,feng4) -7E2C (cu4) -7E2D (li2) -7E2E (suo1,su4) -7E2F (yin3) -7E30 (xi3) -7E31 (zong4,zong1) -7E32 (lei2) -7E33 (zhuan4) -7E34 (qian4) -7E35 (man4) -7E36 (zhi2) -7E37 (lu:3) -7E38 (mo4) -7E39 (piao3,piao1) -7E3A (lian2) -7E3B (mi2) -7E3C (xuan4) -7E3D (zong3) -7E3E (ji1) -7E3F (shan1) -7E40 (sui4) -7E41 (fan2,po2) -7E42 (shuai4) -7E43 (beng1) -7E44 (yi1) -7E45 (sao1) -7E46 (mou2,miao4,miu4) -7E47 (zhou4,yao2,you2) -7E48 (qiang3) -7E49 (hun2) -7E4A (xian1) -7E4B (xi4,ji4) -7E4C (none0) -7E4D (xiu4) -7E4E (ran2) -7E4F (xuan4) -7E50 (hui4) -7E51 (qiao1) -7E52 (zeng1,zeng4) -7E53 (zuo3) -7E54 (zhi1) -7E55 (shan4) -7E56 (san3) -7E57 (lin2) -7E58 (yu4) -7E59 (fan1) -7E5A (liao2) -7E5B (chuo4) -7E5C (zun1) -7E5D (jian4) -7E5E (rao4,rao3) -7E5F (chan3) -7E60 (rui3) -7E61 (xiu4) -7E62 (hui4) -7E63 (hua4) -7E64 (zuan3) -7E65 (xi1) -7E66 (qiang3) -7E67 (none0) -7E68 (da2) -7E69 (sheng2) -7E6A (hui4) -7E6B (xi4,ji4) -7E6C (se4) -7E6D (jian3) -7E6E (jiang1) -7E6F (huan2) -7E70 (qiao1,zao3) -7E71 (cong1) -7E72 (jie4) -7E73 (jiao3,jia3,zhuo2) -7E74 (bo4) -7E75 (chan2) -7E76 (yi4) -7E77 (nao2) -7E78 (sui4) -7E79 (yi4) -7E7A (shai3) -7E7B (xu1) -7E7C (ji4) -7E7D (bin1) -7E7E (qian3) -7E7F (jian4,kan3) -7E80 (pu2) -7E81 (xun1) -7E82 (zuan3) -7E83 (qi2) -7E84 (peng2) -7E85 (li4) -7E86 (mo4) -7E87 (lei4) -7E88 (xie2) -7E89 (zuan3) -7E8A (kuang4) -7E8B (you1) -7E8C (xu4) -7E8D (lei2) -7E8E (xian1) -7E8F (chan2) -7E90 (none0) -7E91 (lu2) -7E92 (chan2) -7E93 (ying1) -7E94 (cai2) -7E95 (xiang1) -7E96 (xian1) -7E97 (zui1) -7E98 (zuan3) -7E99 (luo4) -7E9A (xi3) -7E9B (dao4,du2) -7E9C (lan4,lan3) -7E9D (lei2) -7E9E (lian4) -7E9F (mi4) -7EA0 (jiu1) -7EA1 (yu1) -7EA2 (hong2,gong1) -7EA3 (zhou4) -7EA4 (xian1,qian4) -7EA5 (he2,ge1) -7EA6 (yue1,yao1) -7EA7 (ji2) -7EA8 (wan2) -7EA9 (kuang4) -7EAA (ji4,ji3) -7EAB (ren4) -7EAC (wei3) -7EAD (yun2) -7EAE (hong2) -7EAF (chun2) -7EB0 (pi1) -7EB1 (sha1) -7EB2 (gang1) -7EB3 (na4) -7EB4 (ren4) -7EB5 (zong4) -7EB6 (lun2,guan1) -7EB7 (fen1) -7EB8 (zhi3) -7EB9 (wen2,wen4) -7EBA (fang3) -7EBB (zhu4) -7EBC (zhen4) -7EBD (niu3) -7EBE (shu1) -7EBF (xian4) -7EC0 (gan4) -7EC1 (xie4) -7EC2 (fu2) -7EC3 (lian4) -7EC4 (zu3) -7EC5 (shen1) -7EC6 (xi4) -7EC7 (zhi1) -7EC8 (zhong1) -7EC9 (zhou4) -7ECA (ban4) -7ECB (fu2) -7ECC (chu4) -7ECD (shao4) -7ECE (yi4) -7ECF (jing1,jing4) -7ED0 (dai4) -7ED1 (bang3) -7ED2 (rong2) -7ED3 (jie2,jie1) -7ED4 (ku4) -7ED5 (rao3,rao4,rao5) -7ED6 (die2) -7ED7 (hang2) -7ED8 (hui4) -7ED9 (ji3,gei3) -7EDA (xuan4) -7EDB (jiang4) -7EDC (luo4,lao4) -7EDD (jue2) -7EDE (jiao3,jia3) -7EDF (tong3) -7EE0 (geng3) -7EE1 (xiao1) -7EE2 (juan4) -7EE3 (xiu4) -7EE4 (xi4) -7EE5 (sui2) -7EE6 (tao1) -7EE7 (ji4) -7EE8 (ti2,ti4,di4) -7EE9 (ji4,ji1) -7EEA (xu4) -7EEB (ling2) -7EEC (yin1) -7EED (xu4) -7EEE (qi3) -7EEF (fei1) -7EF0 (chuo4,chao1,chuo5) -7EF1 (shang4) -7EF2 (gun3) -7EF3 (sheng2) -7EF4 (wei2) -7EF5 (mian2) -7EF6 (shou4) -7EF7 (beng1,beng3,beng4) -7EF8 (chou2) -7EF9 (tao2) -7EFA (liu3) -7EFB (quan3) -7EFC (zong1,zeng4) -7EFD (zhan4) -7EFE (wan3) -7EFF (lu:4,lu4) -7F00 (zhui4) -7F01 (zi1) -7F02 (ke4) -7F03 (xiang1) -7F04 (jian1) -7F05 (mian3) -7F06 (lan3) -7F07 (ti2) -7F08 (miao3) -7F09 (ji1,qi1) -7F0A (yun4) -7F0B (hui4) -7F0C (si1) -7F0D (duo3) -7F0E (duan4) -7F0F (bian4,pian2) -7F10 (xian4) -7F11 (gou1) -7F12 (zhui4) -7F13 (huan3) -7F14 (di4) -7F15 (lu:3) -7F16 (bian1) -7F17 (min2) -7F18 (yuan2) -7F19 (jin4) -7F1A (fu4) -7F1B (ru4) -7F1C (zhen3) -7F1D (feng2,feng4) -7F1E (cui1) -7F1F (gao3) -7F20 (chan2) -7F21 (li2) -7F22 (yi4) -7F23 (jian1) -7F24 (bin1) -7F25 (piao1,piao3) -7F26 (man4) -7F27 (lei2) -7F28 (ying1) -7F29 (suo1,su4) -7F2A (mou2,miao4,miu4) -7F2B (sao1) -7F2C (xie2) -7F2D (liao2) -7F2E (shan4) -7F2F (zeng1,zeng4) -7F30 (jiang1) -7F31 (qian3) -7F32 (qiao1,sao1,zao3) -7F33 (huan2) -7F34 (jiao3,zhuo2,jia3) -7F35 (zuan3) -7F36 (fou3) -7F37 (xie4) -7F38 (gang1) -7F39 (fou3) -7F3A (que1) -7F3B (fou3) -7F3C (que1) -7F3D (bo1) -7F3E (ping2) -7F3F (hou4) -7F40 (none0) -7F41 (gang1) -7F42 (ying1) -7F43 (ying1) -7F44 (qing4) -7F45 (xia4) -7F46 (guan4) -7F47 (zun1) -7F48 (tan2) -7F49 (none0) -7F4A (qing4) -7F4B (weng4) -7F4C (ying1) -7F4D (lei2) -7F4E (tan2) -7F4F (lu2) -7F50 (guan4) -7F51 (wang3) -7F52 (gang1) -7F53 (wang3) -7F54 (wang3) -7F55 (han3) -7F56 (none0) -7F57 (luo2,luo1,luo5) -7F58 (fu2,fou2) -7F59 (mi2) -7F5A (fa2) -7F5B (gu1) -7F5C (zhu3) -7F5D (ju1) -7F5E (mao2) -7F5F (gu3) -7F60 (min2) -7F61 (gang1) -7F62 (ba4,ba5) -7F63 (gua4) -7F64 (ti2) -7F65 (juan4) -7F66 (fu1) -7F67 (lin2,sen1) -7F68 (yan3) -7F69 (zhao4) -7F6A (zui4) -7F6B (gua4) -7F6C (zhuo2) -7F6D (yu4) -7F6E (zhi4) -7F6F (an3) -7F70 (fa2) -7F71 (lan3) -7F72 (shu3,shu4) -7F73 (si1) -7F74 (pi2) -7F75 (ma4) -7F76 (liu3) -7F77 (ba4,ba5,pi2) -7F78 (fa2) -7F79 (li2) -7F7A (chao1) -7F7B (wei4) -7F7C (bi4) -7F7D (ji4) -7F7E (zeng1) -7F7F (tong2) -7F80 (liu3) -7F81 (ji1) -7F82 (juan4) -7F83 (mi4) -7F84 (zhao4) -7F85 (luo2,luo1) -7F86 (pi2,biao1) -7F87 (ji1) -7F88 (ji1) -7F89 (luan2) -7F8A (yang2) -7F8B (mie1) -7F8C (qiang1) -7F8D (ta4) -7F8E (mei3) -7F8F (yang3) -7F90 (you3) -7F91 (you3) -7F92 (fen2) -7F93 (ba1) -7F94 (gao1) -7F95 (yang4) -7F96 (gu3) -7F97 (qiang1) -7F98 (zang1) -7F99 (gao1) -7F9A (ling2) -7F9B (yi4) -7F9C (zhu4) -7F9D (di1) -7F9E (xiu1) -7F9F (qiang3) -7FA0 (yi2) -7FA1 (xian4) -7FA2 (rong2) -7FA3 (qun2) -7FA4 (qun2) -7FA5 (qian1,qiang3) -7FA6 (huan2) -7FA7 (suo1) -7FA8 (xian4) -7FA9 (yi4) -7FAA (yang3) -7FAB (qiang1) -7FAC (xian2) -7FAD (yu2) -7FAE (geng1) -7FAF (jie2) -7FB0 (tang1) -7FB1 (yuan2) -7FB2 (xi1) -7FB3 (fan2) -7FB4 (shan1) -7FB5 (fen4) -7FB6 (shan1) -7FB7 (lian3) -7FB8 (lei2) -7FB9 (geng1) -7FBA (nou2) -7FBB (qiang4) -7FBC (chan4) -7FBD (yu3) -7FBE (gong4) -7FBF (yi4) -7FC0 (chong2,chong1) -7FC1 (weng1) -7FC2 (fen1) -7FC3 (hong2) -7FC4 (chi4) -7FC5 (chi4) -7FC6 (cui2) -7FC7 (fu2,pei4) -7FC8 (xia2) -7FC9 (pen3) -7FCA (yi4) -7FCB (la1) -7FCC (yi4) -7FCD (pi1,po1) -7FCE (ling2) -7FCF (liu4) -7FD0 (zhi4) -7FD1 (qu2) -7FD2 (xi2) -7FD3 (xie2) -7FD4 (xiang2) -7FD5 (xi1,xi4) -7FD6 (xi4) -7FD7 (qi2) -7FD8 (qiao2,qiao4) -7FD9 (hui4) -7FDA (hui1) -7FDB (shu4) -7FDC (se4) -7FDD (hong2) -7FDE (jiang1) -7FDF (zhai2,di2) -7FE0 (cui4) -7FE1 (fei3) -7FE2 (tao1) -7FE3 (sha4) -7FE4 (chi4) -7FE5 (zhu4) -7FE6 (jian3) -7FE7 (xuan1) -7FE8 (shi4) -7FE9 (pian1) -7FEA (zong1) -7FEB (wan4) -7FEC (hui1) -7FED (hou2) -7FEE (he2) -7FEF (he4) -7FF0 (han4) -7FF1 (ao2) -7FF2 (piao1) -7FF3 (yi4) -7FF4 (lian2) -7FF5 (qu2) -7FF6 (none0) -7FF7 (lin2) -7FF8 (pen3) -7FF9 (qiao2,qiao4) -7FFA (ao2) -7FFB (fan1) -7FFC (yi4) -7FFD (hui4) -7FFE (xuan1) -7FFF (dao4) -8000 (yao4,yue4) -8001 (lao3) -8002 (none0) -8003 (kao3) -8004 (mao4) -8005 (zhe3) -8006 (qi2) -8007 (gou3) -8008 (gou2) -8009 (gou3) -800A (die2) -800B (die2) -800C (er2) -800D (shua3) -800E (ruan3) -800F (er2) -8010 (nai4) -8011 (zhuan1,duan1) -8012 (lei3) -8013 (ting1) -8014 (zi3) -8015 (geng1) -8016 (chao4) -8017 (hao4) -8018 (yun2) -8019 (pa2,ba4) -801A (pi1) -801B (chi2) -801C (si4) -801D (qu4) -801E (jia1) -801F (ju4) -8020 (huo1) -8021 (chu2) -8022 (lao4) -8023 (lun3) -8024 (ji2) -8025 (tang1,tang3) -8026 (ou3) -8027 (lou2) -8028 (nou4) -8029 (jiang3) -802A (pang3) -802B (ze2) -802C (lou2) -802D (ji1) -802E (lao4) -802F (huo4) -8030 (you1) -8031 (mo4) -8032 (huai2) -8033 (er3) -8034 (zhe2) -8035 (ding1) -8036 (ye1,ye2) -8037 (da1) -8038 (song3) -8039 (qin2) -803A (yun2) -803B (chi3) -803C (dan1) -803D (dan1) -803E (hong2) -803F (geng3) -8040 (zhi2) -8041 (none0) -8042 (nie4) -8043 (dan1) -8044 (zhen3) -8045 (che4) -8046 (ling2) -8047 (zheng1) -8048 (you3) -8049 (wa1) -804A (liao2) -804B (long2) -804C (zhi2) -804D (ning2) -804E (tiao1) -804F (er2,er4) -8050 (ya4) -8051 (die2) -8052 (guo1,gua1) -8053 (none0) -8054 (lian2) -8055 (hao4) -8056 (sheng4) -8057 (lie4) -8058 (pin4) -8059 (jing1) -805A (ju4) -805B (bi4) -805C (di3) -805D (guo2) -805E (wen2,wen4) -805F (xu4) -8060 (ping1) -8061 (cong1) -8062 (none0) -8063 (none0) -8064 (ting2) -8065 (yu3) -8066 (cong1) -8067 (kui2) -8068 (lian2) -8069 (kui4) -806A (cong1) -806B (lian2) -806C (weng3) -806D (kui4) -806E (lian2) -806F (lian2) -8070 (cong1) -8071 (ao2) -8072 (sheng1) -8073 (song3) -8074 (ting1) -8075 (kui4) -8076 (nie4) -8077 (zhi2) -8078 (dan1) -8079 (ning2) -807A (none0) -807B (ji1) -807C (ting1) -807D (ting1,ting4) -807E (long2) -807F (yu4) -8080 (yu4) -8081 (zhao4) -8082 (si4) -8083 (su4) -8084 (yi4) -8085 (su4) -8086 (si4) -8087 (zhao4) -8088 (zhao4) -8089 (rou4) -808A (yi4) -808B (lei4,le4,le1) -808C (ji1) -808D (qiu2) -808E (ken3) -808F (cao4) -8090 (ge1) -8091 (di4) -8092 (huan2) -8093 (huang1) -8094 (yi3) -8095 (ren4) -8096 (xiao4,xiao1) -8097 (ru3) -8098 (zhou3) -8099 (yuan1) -809A (du4,du3) -809B (gang1) -809C (rong2) -809D (gan1) -809E (cha1) -809F (wo4) -80A0 (chang2) -80A1 (gu3) -80A2 (zhi1) -80A3 (qin2) -80A4 (fu1) -80A5 (fei2) -80A6 (ban1) -80A7 (pei1) -80A8 (pang4) -80A9 (jian1) -80AA (fang2) -80AB (zhun1) -80AC (you2) -80AD (na4) -80AE (ang1) -80AF (ken3) -80B0 (ran2) -80B1 (gong1) -80B2 (yu4,yo1) -80B3 (wen3) -80B4 (yao2) -80B5 (jin4) -80B6 (pi2) -80B7 (qian3) -80B8 (xi4) -80B9 (xi4) -80BA (fei4) -80BB (ken3) -80BC (jing3) -80BD (tai4) -80BE (shen4) -80BF (zhong3) -80C0 (zhang4) -80C1 (xie2) -80C2 (shen4) -80C3 (wei4) -80C4 (zhou4) -80C5 (die2) -80C6 (dan3) -80C7 (fei4) -80C8 (ba2) -80C9 (bo2) -80CA (qu2) -80CB (tian2) -80CC (bei4,bei1) -80CD (gua1) -80CE (tai1) -80CF (zi3) -80D0 (ku1) -80D1 (zhi1) -80D2 (ni4) -80D3 (ping2) -80D4 (zi4) -80D5 (fu4) -80D6 (pang4,pan2) -80D7 (zhen1) -80D8 (xian2) -80D9 (zuo4) -80DA (pei1) -80DB (jia3) -80DC (sheng4,sheng1) -80DD (zhi1) -80DE (bao1) -80DF (mu3) -80E0 (qu1) -80E1 (hu2) -80E2 (ke1) -80E3 (yi3) -80E4 (yin4) -80E5 (xu1) -80E6 (yang1) -80E7 (long2) -80E8 (dong4) -80E9 (ka3) -80EA (lu2) -80EB (jing4) -80EC (nu3) -80ED (yan1) -80EE (pang1) -80EF (kua4) -80F0 (yi2) -80F1 (guang1) -80F2 (hai3,gai3) -80F3 (ge1,ga1,ge2) -80F4 (dong4) -80F5 (zhi4) -80F6 (jiao1) -80F7 (xiong1) -80F8 (xiong1) -80F9 (er2) -80FA (an4) -80FB (xing2) -80FC (pian2) -80FD (neng2) -80FE (zi4) -80FF (none0) -8100 (cheng2) -8101 (tiao4) -8102 (zhi1) -8103 (cui4) -8104 (mei2) -8105 (xie2) -8106 (cui4) -8107 (xie2) -8108 (mo4,mai4) -8109 (mai4,mo4) -810A (ji3,ji2) -810B (xie2) -810C (none0) -810D (kuai4) -810E (sa4) -810F (zang4,zang1) -8110 (qi2) -8111 (nao3) -8112 (mi3) -8113 (nong2) -8114 (luan2) -8115 (wan3) -8116 (bo2) -8117 (wen3) -8118 (wan3) -8119 (qiu2) -811A (jiao3,jue2,jia3) -811B (jing4) -811C (you3) -811D (heng1) -811E (cuo3) -811F (lie4) -8120 (shan1) -8121 (ting3) -8122 (mei2) -8123 (chun2) -8124 (shen4) -8125 (xie2) -8126 (none0) -8127 (juan1) -8128 (cu4) -8129 (xiu1) -812A (xin4) -812B (tuo1) -812C (pao1) -812D (cheng2) -812E (nei3) -812F (fu3,pu2) -8130 (dou4) -8131 (tuo1) -8132 (niao4) -8133 (nao3) -8134 (pi3) -8135 (gu3) -8136 (luo2) -8137 (li4) -8138 (lian3) -8139 (zhang4) -813A (cui4) -813B (jie2) -813C (liang3) -813D (shui2) -813E (pi2) -813F (biao1) -8140 (lun2) -8141 (pian2) -8142 (guo4) -8143 (juan4) -8144 (chui2,zhui1) -8145 (dan4) -8146 (tian3) -8147 (nei3) -8148 (jing1) -8149 (jie1) -814A (la4,xi1) -814B (ye4,yi4) -814C (a1,yan1) -814D (ren3) -814E (shen4) -814F (chuo4,duo2) -8150 (fu3) -8151 (fu3) -8152 (ju1) -8153 (fei2) -8154 (qiang1) -8155 (wan4) -8156 (dong4) -8157 (pi2) -8158 (guo2) -8159 (zong1) -815A (ding4) -815B (wu1) -815C (mei2) -815D (ruan3) -815E (zhuan4,dun4) -815F (zhi4) -8160 (cou4) -8161 (gua1,luo2) -8162 (ou3) -8163 (di4) -8164 (an1) -8165 (xing1) -8166 (nao3) -8167 (shu4) -8168 (shuan4) -8169 (nan3) -816A (yun4) -816B (zhong3) -816C (rou2) -816D (e4) -816E (sai1) -816F (tu2) -8170 (yao1) -8171 (jian4) -8172 (wei3) -8173 (jiao3) -8174 (yu2) -8175 (jia1) -8176 (duan4) -8177 (bi4) -8178 (chang2) -8179 (fu4) -817A (xian4) -817B (ni4) -817C (mian3) -817D (wa4) -817E (teng2) -817F (tui3) -8180 (bang3,pang1,pang2,bang4) -8181 (qian3) -8182 (lu:3) -8183 (wa4) -8184 (shou4) -8185 (tang2) -8186 (su4) -8187 (zhui4) -8188 (ge2) -8189 (yi4) -818A (bo2,bo5) -818B (liao2) -818C (ji2) -818D (pi2) -818E (xie2) -818F (gao1,gao4) -8190 (lu:3) -8191 (bin4) -8192 (none0) -8193 (chang2) -8194 (lu4) -8195 (guo2) -8196 (pang1) -8197 (chuai2) -8198 (biao1) -8199 (jiang3) -819A (fu1) -819B (tang2) -819C (mo2,mo4) -819D (xi1) -819E (zhuan1) -819F (lu:4) -81A0 (jiao1) -81A1 (ying4) -81A2 (lu:2) -81A3 (zhi4) -81A4 (xue3) -81A5 (chun1) -81A6 (lin4) -81A7 (tong2) -81A8 (peng2) -81A9 (ni4) -81AA (chuai4) -81AB (liao2) -81AC (cui4) -81AD (gui1) -81AE (xiao1) -81AF (teng1) -81B0 (fan2) -81B1 (zhi2) -81B2 (jiao1) -81B3 (shan4) -81B4 (hu1) -81B5 (cui4) -81B6 (run4) -81B7 (xin4) -81B8 (sui3) -81B9 (fen4) -81BA (ying1) -81BB (shan1) -81BC (gua1) -81BD (dan3) -81BE (kuai4) -81BF (nong2) -81C0 (tun2) -81C1 (lian2) -81C2 (bei5,bi4,bei4) -81C3 (yong1) -81C4 (jue2) -81C5 (chu4) -81C6 (yi4) -81C7 (juan3) -81C8 (la4,xi1) -81C9 (lian3) -81CA (sao1,sao4) -81CB (tun2) -81CC (gu3) -81CD (qi2) -81CE (cui4) -81CF (bin4) -81D0 (xun1) -81D1 (nao4,ru2) -81D2 (huo4) -81D3 (zang4) -81D4 (xian4) -81D5 (biao1) -81D6 (xing4) -81D7 (kuan1) -81D8 (la4,xi1) -81D9 (yan1) -81DA (lu2) -81DB (hu4) -81DC (za1) -81DD (luo3) -81DE (qu2) -81DF (zang4) -81E0 (luan2) -81E1 (ni2) -81E2 (za1) -81E3 (chen2) -81E4 (qian1) -81E5 (wo4) -81E6 (guang4,wang3) -81E7 (zang1) -81E8 (lin2) -81E9 (guang4) -81EA (zi4) -81EB (jiao3) -81EC (nie4) -81ED (chou4,xiu4) -81EE (ji4) -81EF (gao1) -81F0 (chou4,xiu4) -81F1 (mian2) -81F2 (nie4) -81F3 (zhi4) -81F4 (zhi4) -81F5 (ge2) -81F6 (jian4) -81F7 (die2) -81F8 (zhi4) -81F9 (xiu1) -81FA (tai2,tai1) -81FB (zhen1) -81FC (jiu4) -81FD (xian4) -81FE (yu2) -81FF (cha2) -8200 (yao3) -8201 (yu2) -8202 (chong1) -8203 (xi4) -8204 (xi4) -8205 (jiu4) -8206 (yu2) -8207 (yu3,yu4) -8208 (xing1,xing4) -8209 (ju3) -820A (jiu4) -820B (xin4) -820C (she2) -820D (she4,she3) -820E (she3,she4) -820F (jiu3) -8210 (shi4) -8211 (tan1) -8212 (shu1) -8213 (shi4) -8214 (tian3) -8215 (dan4) -8216 (pu4) -8217 (pu4,pu1) -8218 (guan3) -8219 (hua4) -821A (tian4) -821B (chuan3) -821C (shun4) -821D (xia2) -821E (wu3) -821F (zhou1) -8220 (dao1) -8221 (chuan2) -8222 (shan1) -8223 (yi3) -8224 (none0) -8225 (pa1) -8226 (tai4) -8227 (fan2) -8228 (ban3) -8229 (chuan2) -822A (hang2) -822B (fang3) -822C (ban1,bo1,pan2) -822D (bi3) -822E (lu2) -822F (zhong1) -8230 (jian4) -8231 (cang1) -8232 (ling2) -8233 (zhu2) -8234 (ze2) -8235 (duo4,tuo2) -8236 (bo2) -8237 (xian2) -8238 (ge3) -8239 (chuan2) -823A (jia2,xia2) -823B (lu2) -823C (hong2) -823D (pang2) -823E (xi1) -823F (none0) -8240 (fu2) -8241 (zao4) -8242 (feng2) -8243 (li2) -8244 (shao1) -8245 (yu2) -8246 (lang2) -8247 (ting3) -8248 (none0) -8249 (wei3) -824A (bo2) -824B (meng3) -824C (nian4) -824D (ju1) -824E (huang2) -824F (shou3) -8250 (zong1) -8251 (bian4) -8252 (mao4) -8253 (die2) -8254 (none0) -8255 (bang4) -8256 (cha1) -8257 (yi4) -8258 (sou1,sao1) -8259 (cang1) -825A (cao2) -825B (lou2) -825C (dai4) -825D (none0) -825E (yao4) -825F (chong1,tong2) -8260 (none0) -8261 (dang1) -8262 (qiang2) -8263 (lu3) -8264 (yi3) -8265 (jie4) -8266 (jian4) -8267 (huo4) -8268 (meng2) -8269 (qi2) -826A (lu3) -826B (lu2) -826C (chan2) -826D (shuang1) -826E (gen4,gen3) -826F (liang2) -8270 (jian1) -8271 (jian1) -8272 (se4,shai3) -8273 (yan4) -8274 (fu2) -8275 (ping2) -8276 (yan4) -8277 (yan4) -8278 (cao3) -8279 (cao3) -827A (yi4) -827B (le4) -827C (ting1) -827D (jiao1) -827E (ai4,yi4) -827F (nai3) -8280 (tiao2) -8281 (jiao1) -8282 (jie2,jie1) -8283 (peng2) -8284 (wan2) -8285 (yi4) -8286 (chai1) -8287 (mian2) -8288 (mi3) -8289 (gan4) -828A (qian1) -828B (yu4) -828C (yu4) -828D (shao2) -828E (xiong1) -828F (du4) -8290 (xia4) -8291 (qi3) -8292 (mang2,wang2) -8293 (zi3) -8294 (hui4) -8295 (sui1) -8296 (zhi4) -8297 (xiang1) -8298 (bi4,pi2) -8299 (fu2) -829A (tun2) -829B (wei3) -829C (wu2) -829D (zhi1) -829E (qi3) -829F (shan1) -82A0 (wen2) -82A1 (qian4) -82A2 (ren2) -82A3 (fou2) -82A4 (kou1) -82A5 (jie4,gai4) -82A6 (lu2,lu3) -82A7 (zhu4) -82A8 (ji1) -82A9 (qin2) -82AA (qi2) -82AB (yan2,yuan2) -82AC (fen1) -82AD (ba1) -82AE (rui4) -82AF (xin1,xin4) -82B0 (ji4) -82B1 (hua1) -82B2 (hua1) -82B3 (fang1) -82B4 (wu4) -82B5 (jue2) -82B6 (gou1) -82B7 (zhi3) -82B8 (yun2) -82B9 (qin2) -82BA (ao3) -82BB (chu2) -82BC (mao4) -82BD (ya2,di2) -82BE (fei4,fu2) -82BF (reng4) -82C0 (hang2) -82C1 (cong1) -82C2 (yin2) -82C3 (you3) -82C4 (bian4) -82C5 (yi4) -82C6 (none0) -82C7 (wei3) -82C8 (li4) -82C9 (pi3) -82CA (e4) -82CB (xian4) -82CC (chang2) -82CD (cang1) -82CE (zhu4,ning2) -82CF (su1) -82D0 (yi2,ti2) -82D1 (yuan4) -82D2 (ran3) -82D3 (ling2) -82D4 (tai2,tai1) -82D5 (tiao2,shao2) -82D6 (di2) -82D7 (miao2) -82D8 (qing3) -82D9 (li4) -82DA (rao2) -82DB (ke1) -82DC (mu4) -82DD (pei4) -82DE (bao1) -82DF (gou3) -82E0 (min2) -82E1 (yi3) -82E2 (yi3) -82E3 (ju4,qu3) -82E4 (pie3) -82E5 (ruo4,re3) -82E6 (ku3) -82E7 (zhu4,ning2) -82E8 (ni3) -82E9 (bo2) -82EA (bing3) -82EB (shan1,shan4) -82EC (qiu2) -82ED (yao3) -82EE (xian1) -82EF (ben3) -82F0 (hong2) -82F1 (ying1) -82F2 (zha3) -82F3 (dong1) -82F4 (ju1) -82F5 (die2) -82F6 (nie2) -82F7 (gan1) -82F8 (hu1) -82F9 (ping2,pin2) -82FA (mei2) -82FB (fu2) -82FC (sheng1) -82FD (gu1) -82FE (bi4) -82FF (wei4) -8300 (fu2) -8301 (zhuo2) -8302 (mao4) -8303 (fan4) -8304 (qie2,jia1) -8305 (mao2) -8306 (mao2,mao3) -8307 (ba2) -8308 (zi3,ci2) -8309 (mo4) -830A (zi1) -830B (di3) -830C (chi2) -830D (gou3) -830E (jing1) -830F (long2) -8310 (none0) -8311 (niao3) -8312 (none0) -8313 (xue2) -8314 (ying2) -8315 (qiong2) -8316 (ge2) -8317 (ming2) -8318 (li4) -8319 (rong2) -831A (yin4) -831B (gen4) -831C (qian4,xi1) -831D (chai3) -831E (chen2) -831F (yu4) -8320 (xiu1) -8321 (zi4) -8322 (lie4) -8323 (wu2) -8324 (duo1) -8325 (kui1) -8326 (ce4) -8327 (jian3) -8328 (ci2) -8329 (gou3) -832A (guang1) -832B (mang2) -832C (cha2,zha1) -832D (jiao1) -832E (jiao1) -832F (fu2) -8330 (yu2) -8331 (zhu1) -8332 (zi1) -8333 (jiang1) -8334 (hui2) -8335 (yin1) -8336 (cha2) -8337 (fa2) -8338 (rong2,rong3) -8339 (ru2,ru4) -833A (chong1) -833B (mang3) -833C (tong2) -833D (zhong4) -833E (none0) -833F (zhu2) -8340 (xun2) -8341 (huan2) -8342 (kua1) -8343 (quan2) -8344 (gai1) -8345 (da2) -8346 (jing1) -8347 (xing4) -8348 (chuan3) -8349 (cao3) -834A (jing1) -834B (er2) -834C (an4) -834D (shou1) -834E (chi2) -834F (ren3) -8350 (jian4) -8351 (ti2,yi2) -8352 (huang1,huang5) -8353 (ping2) -8354 (li4) -8355 (jin1) -8356 (lao3,pei2) -8357 (rong2) -8358 (zhuang1) -8359 (da2) -835A (jia2) -835B (rao2) -835C (bi4) -835D (ce4) -835E (qiao2) -835F (hui4) -8360 (ji4,qi2) -8361 (dang4) -8362 (none0) -8363 (rong2) -8364 (hun1,xun1) -8365 (ying2,xing2) -8366 (luo4) -8367 (ying2) -8368 (qian2,xun2) -8369 (jin4) -836A (sun1) -836B (yin4,yin1) -836C (mai3) -836D (hong2) -836E (zhou4) -836F (yao4) -8370 (du4) -8371 (wei3) -8372 (chu4) -8373 (dou4) -8374 (fu1) -8375 (ren3) -8376 (yin2) -8377 (he2,he4) -8378 (bi2) -8379 (bu4) -837A (yun2) -837B (di2) -837C (tu2) -837D (sui1) -837E (sui1) -837F (cheng2) -8380 (chen2) -8381 (wu2) -8382 (bie2) -8383 (xi1) -8384 (geng3) -8385 (li4) -8386 (pu2) -8387 (zhu4) -8388 (mo4) -8389 (li4) -838A (zhuang1) -838B (ji2) -838C (duo2) -838D (qiu2) -838E (sha1,suo1) -838F (suo1) -8390 (chen2) -8391 (feng1) -8392 (ju3) -8393 (mei2) -8394 (meng2) -8395 (xing4) -8396 (jing1) -8397 (che1) -8398 (xin1,shen1) -8399 (jun1) -839A (yan2,yan4) -839B (ting2) -839C (you2) -839D (cuo4) -839E (guan1,guan3,wan3,wan1) -839F (han4) -83A0 (you3) -83A1 (cuo4) -83A2 (jia2) -83A3 (wang4) -83A4 (you2) -83A5 (niu3,chou3) -83A6 (shao1) -83A7 (xian4) -83A8 (lang4,liang2,lang2) -83A9 (fu2,piao3) -83AA (e2) -83AB (mo4) -83AC (wen4) -83AD (jie2) -83AE (nan2) -83AF (mu4) -83B0 (kan3) -83B1 (lai2) -83B2 (lian2) -83B3 (shi4,shi2) -83B4 (wo1,zhua1) -83B5 (tu2,tu4) -83B6 (xian1) -83B7 (huo4) -83B8 (you2) -83B9 (ying2) -83BA (ying1) -83BB (none0) -83BC (chun2) -83BD (mang3) -83BE (mang3) -83BF (ci4) -83C0 (yu4,wan3) -83C1 (jing1) -83C2 (di4) -83C3 (qu2) -83C4 (dong1) -83C5 (jian1) -83C6 (zou1) -83C7 (gu1) -83C8 (la1) -83C9 (lu4,lu:4) -83CA (ju2) -83CB (wei4) -83CC (jun1,jun4) -83CD (nie4) -83CE (kun1) -83CF (he2) -83D0 (pu2) -83D1 (zai1) -83D2 (gao3) -83D3 (guo3) -83D4 (fu2) -83D5 (lun2) -83D6 (chang1) -83D7 (chou2) -83D8 (song1) -83D9 (chui2) -83DA (zhan4) -83DB (men2) -83DC (cai4) -83DD (ba2) -83DE (li2) -83DF (tu4,tu2) -83E0 (bo1) -83E1 (han4) -83E2 (bao4) -83E3 (qin4) -83E4 (juan3) -83E5 (xi1) -83E6 (qin2) -83E7 (di3) -83E8 (jie1) -83E9 (pu2) -83EA (dang4) -83EB (jin3) -83EC (zhao3) -83ED (tai2) -83EE (geng1) -83EF (hua2,hua4,hua1) -83F0 (gu1) -83F1 (ling2) -83F2 (fei1,fei3) -83F3 (jin1) -83F4 (an4) -83F5 (wang3) -83F6 (beng3) -83F7 (zhou3) -83F8 (yan1) -83F9 (zu1) -83FA (jian1) -83FB (lin3) -83FC (tan3) -83FD (shu1,shu2) -83FE (tian2) -83FF (dao4) -8400 (hu3) -8401 (ji1,qi2) -8402 (he2) -8403 (cui4) -8404 (tao2) -8405 (chun1) -8406 (bei1,bi4) -8407 (chang2) -8408 (huan2) -8409 (fei2) -840A (lai2) -840B (qi1) -840C (meng2) -840D (ping2) -840E (wei3,wei1) -840F (dan4) -8410 (sha4) -8411 (huan2) -8412 (yan3) -8413 (yi2) -8414 (tiao2) -8415 (qi2,ji4) -8416 (wan3) -8417 (ce4) -8418 (nai4) -8419 (none0) -841A (tuo4) -841B (jiu1) -841C (tie1) -841D (luo2) -841E (none0) -841F (none0) -8420 (meng2) -8421 (none0) -8422 (none0) -8423 (ding4) -8424 (ying2) -8425 (ying2) -8426 (ying2) -8427 (xiao1) -8428 (sa4) -8429 (qiu1) -842A (ke1) -842B (xiang4) -842C (wan4,mo4) -842D (yu3) -842E (yu4) -842F (fu4) -8430 (lian4) -8431 (xuan1) -8432 (xuan1) -8433 (nan2) -8434 (ze2) -8435 (wo1) -8436 (chun3) -8437 (xiao1) -8438 (yu2) -8439 (pian1,bian3) -843A (mao4) -843B (an1) -843C (e4) -843D (luo4,la4,lao4,luo1) -843E (ying2) -843F (huo2) -8440 (gua1) -8441 (jiang1) -8442 (wan3) -8443 (zuo2) -8444 (zuo4) -8445 (ju1) -8446 (bao3) -8447 (rou2) -8448 (xi3) -8449 (xie2,ye4,she4) -844A (an1) -844B (qu2) -844C (jian1) -844D (fu2) -844E (lu:4) -844F (lu:4) -8450 (pen2) -8451 (feng1,feng4) -8452 (hong2) -8453 (hong2) -8454 (hou2) -8455 (yan2) -8456 (tu1) -8457 (zhu4,zhe5,zhao1,zhao2,zhu3,zi1,zhuo2) -8458 (zi1) -8459 (xiang1) -845A (shen4,ren4) -845B (ge3,ge2) -845C (qia1) -845D (jing4) -845E (mi3) -845F (huang2) -8460 (shen1) -8461 (pu2) -8462 (ge3) -8463 (dong3) -8464 (zhou4) -8465 (qian2) -8466 (wei3) -8467 (bo2) -8468 (wei1) -8469 (pa1) -846A (ji4) -846B (hu2) -846C (zang4) -846D (jia1) -846E (duan4) -846F (yao4) -8470 (jun4) -8471 (cong1) -8472 (quan2) -8473 (wei1) -8474 (xian2) -8475 (kui2) -8476 (ting2) -8477 (hun1,xun1) -8478 (xi3) -8479 (shi1) -847A (qi4) -847B (lan2) -847C (zong1) -847D (yao1) -847E (yuan1) -847F (mei2) -8480 (yun1) -8481 (shu4) -8482 (di4) -8483 (zhuan4) -8484 (guan1) -8485 (none0) -8486 (qiong2) -8487 (chan3) -8488 (kai3) -8489 (kui4) -848A (none0) -848B (jiang3) -848C (lou2) -848D (wei3) -848E (pai4) -848F (none0) -8490 (sou1) -8491 (yin1) -8492 (shi1) -8493 (chun2) -8494 (shi4,shi2) -8495 (yun1) -8496 (zhen1) -8497 (lang4) -8498 (nu2) -8499 (meng3,meng2,meng1) -849A (he2) -849B (que1) -849C (suan4) -849D (yuan2) -849E (li4) -849F (ju3) -84A0 (xi2) -84A1 (bang4,pang2) -84A2 (chu2) -84A3 (xu2) -84A4 (tu2) -84A5 (liu2) -84A6 (huo4) -84A7 (zhen1) -84A8 (qian4) -84A9 (zu1) -84AA (po4) -84AB (cuo1) -84AC (yuan1) -84AD (chu2) -84AE (yu4) -84AF (kuai3) -84B0 (pan2) -84B1 (pu2) -84B2 (pu2) -84B3 (na4) -84B4 (shuo4) -84B5 (xi1) -84B6 (fen2) -84B7 (yun2) -84B8 (zheng1) -84B9 (jian1) -84BA (ji2) -84BB (ruo4) -84BC (cang1) -84BD (en1) -84BE (mi2) -84BF (hao1) -84C0 (sun1) -84C1 (zhen1) -84C2 (ming2) -84C3 (none0) -84C4 (xu4) -84C5 (liu2) -84C6 (xi2) -84C7 (gu1) -84C8 (lang2) -84C9 (rong2) -84CA (weng3) -84CB (gai4,ge3,he2) -84CC (cuo4) -84CD (shi1) -84CE (tang2) -84CF (luo3) -84D0 (ru4) -84D1 (suo1) -84D2 (xian1) -84D3 (bei4) -84D4 (yao3) -84D5 (gui4) -84D6 (bi4) -84D7 (zong3) -84D8 (gun3) -84D9 (none0) -84DA (xiu1) -84DB (ce4) -84DC (none0) -84DD (lan2,lan5,la5) -84DE (none0) -84DF (ji4) -84E0 (li2) -84E1 (can1) -84E2 (lang2) -84E3 (yu4) -84E4 (none0) -84E5 (ying2) -84E6 (mo4) -84E7 (diao4) -84E8 (xiu1) -84E9 (wu4) -84EA (tong1) -84EB (zhu2) -84EC (peng2) -84ED (an1) -84EE (lian2) -84EF (cong1) -84F0 (xi3) -84F1 (ping2) -84F2 (qiu1,ou1) -84F3 (jin4) -84F4 (chun2) -84F5 (jie2) -84F6 (wei3) -84F7 (tui1) -84F8 (cao2) -84F9 (yu4) -84FA (yi4) -84FB (ji2) -84FC (liao3,lu4) -84FD (bi4) -84FE (lu3) -84FF (xu5,su4) -8500 (bu4) -8501 (zhang1) -8502 (luo2) -8503 (qiang2) -8504 (man4) -8505 (yan2) -8506 (leng2) -8507 (ji4) -8508 (biao1,piao4) -8509 (gun3) -850A (han3) -850B (di2) -850C (su4) -850D (lu4) -850E (she4) -850F (shang1) -8510 (di2) -8511 (mie4) -8512 (xun1) -8513 (man4,wan4,man2) -8514 (bo5,bu3,bo2) -8515 (di4) -8516 (cuo2) -8517 (zhe4) -8518 (sen1) -8519 (xuan4) -851A (yu4,wei4) -851B (hu2) -851C (ao2) -851D (mi3) -851E (lou2) -851F (cu4) -8520 (zhong1) -8521 (cai4) -8522 (po2) -8523 (jiang3) -8524 (mi4) -8525 (cong1) -8526 (niao3) -8527 (hui4) -8528 (jun4) -8529 (yin2) -852A (jian4) -852B (nian1) -852C (shu1) -852D (yin4,yin1) -852E (kui4) -852F (chen2) -8530 (hu4) -8531 (sha1) -8532 (kou4) -8533 (qian4) -8534 (ma2,ma1) -8535 (cang2,zang4) -8536 (ze2) -8537 (qiang2) -8538 (dou1) -8539 (lian3) -853A (lin4) -853B (kou4) -853C (ai3) -853D (bi4) -853E (li2) -853F (wei3) -8540 (ji2) -8541 (qian2,xun2) -8542 (sheng4) -8543 (fan2,fan1,bo1) -8544 (meng2) -8545 (ou3) -8546 (chan3) -8547 (dian3) -8548 (xun4,jun4) -8549 (jiao1,qiao2) -854A (rui3) -854B (rui3) -854C (lei3) -854D (yu2) -854E (qiao2) -854F (chu2) -8550 (hua2,hua1,hua4) -8551 (jian1) -8552 (mai3) -8553 (yun2) -8554 (bao1) -8555 (you2) -8556 (qu2) -8557 (lu4) -8558 (rao2) -8559 (hui4) -855A (e4) -855B (ti2) -855C (fei3) -855D (jue2) -855E (zui4) -855F (fa4) -8560 (ru2) -8561 (fen2) -8562 (kui4) -8563 (shun4) -8564 (rui2) -8565 (ya3) -8566 (xu1) -8567 (fu4) -8568 (jue2) -8569 (dang4) -856A (wu2) -856B (tong2) -856C (si1) -856D (xiao1) -856E (xi4) -856F (yong1) -8570 (wen1) -8571 (shao1) -8572 (qi2) -8573 (jian1) -8574 (yun4) -8575 (sun1) -8576 (ling2) -8577 (yu4) -8578 (xia2) -8579 (weng4) -857A (ji2) -857B (hong2,hong4) -857C (si4) -857D (deng1) -857E (lei3) -857F (xuan1) -8580 (yun4) -8581 (yu4) -8582 (xi2) -8583 (hao4) -8584 (bo2,bao2,bo4) -8585 (hao1) -8586 (ai4) -8587 (wei1,wei2) -8588 (hui4) -8589 (wei4) -858A (ji4) -858B (ci2) -858C (xiang1) -858D (luan4) -858E (mie4) -858F (yi4) -8590 (leng2) -8591 (jiang1) -8592 (can4) -8593 (shen1,can1,cen1) -8594 (qiang2) -8595 (lian2) -8596 (ke1) -8597 (yuan2) -8598 (da2) -8599 (ti4) -859A (tang2) -859B (xue1) -859C (bi4,bo4) -859D (zhan1) -859E (sun1) -859F (lian4,xian1) -85A0 (fan2) -85A1 (ding3) -85A2 (xiao4) -85A3 (gu3) -85A4 (xie4) -85A5 (shu3) -85A6 (jian4) -85A7 (kao3) -85A8 (hong1) -85A9 (sa4) -85AA (xin1) -85AB (xun1) -85AC (yao4) -85AD (bai4) -85AE (sou3) -85AF (shu3) -85B0 (xun1) -85B1 (dui4) -85B2 (pin2) -85B3 (wei3) -85B4 (neng2) -85B5 (chou2) -85B6 (mai2) -85B7 (ru2) -85B8 (piao1) -85B9 (tai2) -85BA (qi2,ji4) -85BB (zao3) -85BC (chen2) -85BD (zhen1) -85BE (er3) -85BF (ni3) -85C0 (ying2) -85C1 (gao3) -85C2 (cong4) -85C3 (xiao1) -85C4 (qi2) -85C5 (fa2) -85C6 (jian3) -85C7 (xu4) -85C8 (kui1) -85C9 (jie4,ji2) -85CA (bian3) -85CB (di2) -85CC (mi4) -85CD (lan2,la5) -85CE (jin4) -85CF (zang4,cang2) -85D0 (miao3) -85D1 (qiong2) -85D2 (qie4) -85D3 (xian3,li4) -85D4 (none0) -85D5 (ou3) -85D6 (xian2) -85D7 (su4) -85D8 (lu:2) -85D9 (yi4) -85DA (xu4) -85DB (xie3) -85DC (li2) -85DD (yi4) -85DE (la3) -85DF (lei3) -85E0 (jiao4) -85E1 (di2) -85E2 (zhi3) -85E3 (pi2) -85E4 (teng2) -85E5 (yao4,yue4) -85E6 (mo4,mo2) -85E7 (huan3) -85E8 (biao1) -85E9 (fan1,fan2) -85EA (sou3) -85EB (tan2) -85EC (tui1) -85ED (qiong2) -85EE (qiao2) -85EF (wei4) -85F0 (liu2) -85F1 (hui2) -85F2 (shu1) -85F3 (gao3) -85F4 (yun4) -85F5 (none0) -85F6 (li4) -85F7 (zhu1,shu3) -85F8 (zhu1) -85F9 (ai3) -85FA (lin4) -85FB (zao3) -85FC (xuan1) -85FD (chen4) -85FE (lai4) -85FF (huo4) -8600 (tuo4) -8601 (wu4,e4) -8602 (rui3) -8603 (rui3) -8604 (qi2) -8605 (heng2) -8606 (lu2,lu3) -8607 (su1) -8608 (tui2) -8609 (mang2) -860A (yun4) -860B (pin2,ping2) -860C (yu3) -860D (xun1) -860E (ji4) -860F (jiong1) -8610 (xuan1) -8611 (mo2) -8612 (none0) -8613 (su1) -8614 (jiong1) -8615 (none0) -8616 (nie4) -8617 (bo4,nie4) -8618 (rang2) -8619 (yi4) -861A (xian3,li4) -861B (yu2) -861C (ju2) -861D (lian4) -861E (lian4,lian3) -861F (yin3) -8620 (qiang2) -8621 (ying1) -8622 (long2) -8623 (tou3) -8624 (wei3) -8625 (yue4) -8626 (ling2) -8627 (qu2) -8628 (yao2) -8629 (fan2) -862A (mi2) -862B (lan2) -862C (kui1) -862D (lan2) -862E (ji4) -862F (dang4) -8630 (none0) -8631 (lei4) -8632 (lei3) -8633 (tong4) -8634 (feng1) -8635 (zhi2) -8636 (wei4) -8637 (kui2) -8638 (zhan4) -8639 (huai4) -863A (li2) -863B (ji4) -863C (mi2) -863D (lei3) -863E (huai4) -863F (luo2) -8640 (ji1) -8641 (nao2) -8642 (lu4) -8643 (jian1) -8644 (none0) -8645 (none0) -8646 (lei3) -8647 (quan3) -8648 (xiao1) -8649 (yi4) -864A (luan2) -864B (men2) -864C (bie1) -864D (hu1) -864E (hu3,hu4) -864F (lu3) -8650 (nu:e4) -8651 (lu:4) -8652 (zhi4) -8653 (xiao1) -8654 (qian2) -8655 (chu4,chu3) -8656 (hu1) -8657 (xu1) -8658 (cuo2) -8659 (fu2) -865A (xu1) -865B (xu1) -865C (lu3) -865D (hu3,hu4) -865E (yu2) -865F (hao4,hao2) -8660 (jiao1) -8661 (ju4) -8662 (guo2) -8663 (bao4) -8664 (yan2) -8665 (zhan4) -8666 (zhan4) -8667 (kui1) -8668 (ban1) -8669 (xi4) -866A (shu2) -866B (chong2,hui3) -866C (qiu2) -866D (diao1) -866E (ji3) -866F (qiu2) -8670 (ding1) -8671 (shi1) -8672 (none0) -8673 (di4) -8674 (zhe2) -8675 (she2,yi2) -8676 (yu1) -8677 (gan1) -8678 (zi3) -8679 (hong2,jiang4) -867A (hui3,hui1) -867B (meng2) -867C (ge4) -867D (sui1) -867E (xia1,ha2) -867F (chai4) -8680 (shi2) -8681 (yi3) -8682 (ma3,ma1,ma4) -8683 (xiang4) -8684 (fang1) -8685 (e4) -8686 (pa1) -8687 (chi3) -8688 (qian1) -8689 (wen2) -868A (wen2) -868B (rui4) -868C (bang4,beng4) -868D (pi2) -868E (yue4) -868F (yue4) -8690 (jun1) -8691 (qi2) -8692 (ran2) -8693 (yin3) -8694 (qi2,chi2) -8695 (can2,tian3) -8696 (yuan2) -8697 (jue2) -8698 (hui2) -8699 (qian2) -869A (qi2) -869B (zhong4) -869C (ya2) -869D (hao2) -869E (mu4) -869F (wang2) -86A0 (fen2) -86A1 (fen2) -86A2 (hang2) -86A3 (gong1) -86A4 (zao3) -86A5 (fu3) -86A6 (ran2) -86A7 (jie4) -86A8 (fu2) -86A9 (chi1) -86AA (dou3) -86AB (bao4) -86AC (xian3) -86AD (ni3) -86AE (te4) -86AF (qiu1) -86B0 (you2) -86B1 (zha4) -86B2 (ping2) -86B3 (chi2) -86B4 (you4) -86B5 (he2,ke4) -86B6 (han1) -86B7 (ju4) -86B8 (li4) -86B9 (fu4) -86BA (ran2) -86BB (zha2) -86BC (gou3) -86BD (pi2) -86BE (bo3) -86BF (xian2) -86C0 (zhu4) -86C1 (diao1) -86C2 (bie3) -86C3 (bing3) -86C4 (gu1,gu3) -86C5 (ran2) -86C6 (qu1,ju1) -86C7 (she2,yi2) -86C8 (tie4) -86C9 (ling2) -86CA (gu3) -86CB (dan4) -86CC (gu3) -86CD (ying2) -86CE (li4) -86CF (cheng1) -86D0 (qu1) -86D1 (mou2) -86D2 (ge2) -86D3 (ci4) -86D4 (hui2) -86D5 (hui2) -86D6 (mang2) -86D7 (fu4) -86D8 (yang2) -86D9 (wa1) -86DA (lie4) -86DB (zhu1) -86DC (yi1) -86DD (xian2) -86DE (kuo4) -86DF (jiao1) -86E0 (li4) -86E1 (yi4) -86E2 (ping2) -86E3 (ji1) -86E4 (ha2,ge2) -86E5 (she2) -86E6 (yi2) -86E7 (wang3) -86E8 (mo4) -86E9 (qiong2) -86EA (qie4) -86EB (gui3) -86EC (gong3) -86ED (zhi4) -86EE (man2) -86EF (none0) -86F0 (zhe2) -86F1 (jia2) -86F2 (nao2) -86F3 (si1) -86F4 (qi2) -86F5 (xing1) -86F6 (lie4) -86F7 (qiu2) -86F8 (shao1,xiao1) -86F9 (yong3) -86FA (jia2) -86FB (tui4,shui4) -86FC (che1) -86FD (bai4) -86FE (e2,yi3) -86FF (han4) -8700 (shu3) -8701 (xuan2) -8702 (feng1) -8703 (shen4) -8704 (zhen4) -8705 (fu3) -8706 (xian4,xian3) -8707 (zhe2,zhe1) -8708 (wu2) -8709 (fu2) -870A (li2) -870B (lang2) -870C (bi4) -870D (chu2) -870E (yuan1) -870F (you3) -8710 (jie2) -8711 (dan4) -8712 (yan2) -8713 (ting2) -8714 (dian4) -8715 (tui4) -8716 (hui2) -8717 (wo1) -8718 (zhi1) -8719 (song1) -871A (fei1,fei3) -871B (ju1) -871C (mi4) -871D (qi2) -871E (qi2) -871F (yu4) -8720 (jun3) -8721 (la4,zha4) -8722 (meng3) -8723 (qiang1) -8724 (si1) -8725 (xi1) -8726 (lun2) -8727 (li4) -8728 (die2) -8729 (tiao2) -872A (tao1) -872B (kun1) -872C (gan1) -872D (han4) -872E (yu4) -872F (bang4,beng4) -8730 (fei2) -8731 (pi2) -8732 (wei3) -8733 (dun1) -8734 (yi4) -8735 (yuan1) -8736 (su4) -8737 (quan2) -8738 (qian3) -8739 (rui4) -873A (ni2) -873B (qing1) -873C (wei4) -873D (liang3) -873E (guo3) -873F (wan1,wan3) -8740 (dong1) -8741 (e4) -8742 (ban3) -8743 (zhuo2) -8744 (wang3) -8745 (can2) -8746 (yang3) -8747 (ying2) -8748 (guo1) -8749 (chan2) -874A (none0) -874B (la4,zha4) -874C (ke1) -874D (ji2) -874E (xie1,he2) -874F (ting2) -8750 (mai4) -8751 (xu1) -8752 (mian2) -8753 (yu2) -8754 (jie1) -8755 (shi2) -8756 (xuan1) -8757 (huang2) -8758 (yan3) -8759 (bian1) -875A (rou2) -875B (wei1) -875C (fu4) -875D (yuan2) -875E (mei4) -875F (wei4) -8760 (fu2) -8761 (ruan3) -8762 (xie2) -8763 (you2) -8764 (you2,qiu2) -8765 (mao2) -8766 (xia1,ha2) -8767 (ying1) -8768 (shi1) -8769 (chong2) -876A (tang1) -876B (zhu1) -876C (zong1) -876D (ti2) -876E (fu4) -876F (yuan2) -8770 (kui2) -8771 (meng2) -8772 (la4) -8773 (du2) -8774 (hu2) -8775 (qiu1) -8776 (die2) -8777 (li4,xi2) -8778 (gua1,wo1) -8779 (yun1) -877A (ju3) -877B (nan3) -877C (lou2) -877D (chun1) -877E (rong2) -877F (ying2) -8780 (jiang1) -8781 (tun4) -8782 (lang2) -8783 (pang2) -8784 (si1,shi1) -8785 (xi1) -8786 (xi1) -8787 (xi1) -8788 (yuan2) -8789 (weng1) -878A (lian2) -878B (sou1) -878C (ban1) -878D (rong2) -878E (rong2) -878F (ji2) -8790 (wu1) -8791 (xiu4) -8792 (han4) -8793 (qin2) -8794 (yi2) -8795 (bi1) -8796 (hua2) -8797 (tang2) -8798 (yi3) -8799 (du4) -879A (nai4) -879B (he2) -879C (hu2) -879D (xi1) -879E (ma3,ma1,ma4) -879F (ming2) -87A0 (yi4) -87A1 (wen2) -87A2 (ying2) -87A3 (teng2,te4) -87A4 (yu3) -87A5 (cang1) -87A6 (none0) -87A7 (none0) -87A8 (man3) -87A9 (none0) -87AA (shang1) -87AB (shi4,zhe1) -87AC (cao2) -87AD (chi1) -87AE (di4) -87AF (ao2) -87B0 (lu4) -87B1 (wei4) -87B2 (zhi4) -87B3 (tang2) -87B4 (chen2) -87B5 (piao1) -87B6 (qu2) -87B7 (pi2) -87B8 (yu2) -87B9 (jian4) -87BA (luo2) -87BB (lou2) -87BC (qin3) -87BD (zhong1) -87BE (yin3) -87BF (jiang1) -87C0 (shuai4,shuo4) -87C1 (wen2) -87C2 (jiao1) -87C3 (wan4) -87C4 (zhe2,zhi2) -87C5 (zhe4) -87C6 (ma2,ma5) -87C7 (ma2) -87C8 (guo1) -87C9 (liao4) -87CA (mao2) -87CB (xi1) -87CC (cong1) -87CD (li2) -87CE (man3) -87CF (xiao1) -87D0 (none0) -87D1 (zhang1) -87D2 (mang3) -87D3 (xiang4) -87D4 (mo4) -87D5 (zi1) -87D6 (si1) -87D7 (qiu1) -87D8 (te4) -87D9 (zhi2) -87DA (peng2) -87DB (peng2) -87DC (jiao3) -87DD (qu2) -87DE (bie2) -87DF (liao3) -87E0 (pan2) -87E1 (gui3) -87E2 (xi3) -87E3 (ji3) -87E4 (zhuan1) -87E5 (huang2) -87E6 (fei4) -87E7 (lao2) -87E8 (jue2) -87E9 (jue2) -87EA (hui4) -87EB (yin2) -87EC (chan2) -87ED (jiao1) -87EE (shan4) -87EF (rao2,nao2) -87F0 (xiao1) -87F1 (wu2) -87F2 (chong2) -87F3 (xun2) -87F4 (si1) -87F5 (none0) -87F6 (cheng1) -87F7 (dang1) -87F8 (li3,li2) -87F9 (xie4) -87FA (shan4) -87FB (yi3) -87FC (jing3) -87FD (da2) -87FE (chan2) -87FF (qi4) -8800 (zi1) -8801 (xiang4) -8802 (she4) -8803 (luo3) -8804 (qin2) -8805 (ying2) -8806 (chai4) -8807 (li4) -8808 (ze2) -8809 (xuan1) -880A (lian2) -880B (zhu2) -880C (ze2) -880D (xie1) -880E (mang3) -880F (xie4,xie3) -8810 (qi2) -8811 (rong2) -8812 (jian3) -8813 (meng3) -8814 (hao2) -8815 (ru2,ruan3) -8816 (huo4) -8817 (zhuo2) -8818 (jie2) -8819 (bin1) -881A (he4) -881B (mie4) -881C (fan2) -881D (lei2) -881E (jie2) -881F (la4,zha4) -8820 (mi4) -8821 (li3,li2) -8822 (chun3) -8823 (li4) -8824 (qiu1) -8825 (nie4) -8826 (lu2) -8827 (du4) -8828 (xiao1) -8829 (zhu1) -882A (long2) -882B (li4) -882C (long2) -882D (feng1) -882E (ye1) -882F (pi2) -8830 (rang2) -8831 (gu3) -8832 (juan1) -8833 (ying1) -8834 (none0) -8835 (xi1) -8836 (can2) -8837 (qu2) -8838 (quan2) -8839 (du4) -883A (can2) -883B (man2) -883C (qu2) -883D (jie2) -883E (zhu2) -883F (zha2) -8840 (xue4,xie3) -8841 (huang1) -8842 (nu:4) -8843 (pei1) -8844 (nu:4) -8845 (xin4) -8846 (zhong4) -8847 (mo4) -8848 (er4) -8849 (mie4) -884A (mie4) -884B (shi4) -884C (xing2,hang2,hang4,xing4,heng2) -884D (yan3) -884E (kan4) -884F (yuan4) -8850 (none0) -8851 (ling2) -8852 (xuan4) -8853 (shu4,zhu2) -8854 (xian2) -8855 (tong4) -8856 (long4) -8857 (jie1) -8858 (xian2) -8859 (ya2) -885A (hu2) -885B (wei4) -885C (dao4) -885D (chong1,chong4) -885E (wei4) -885F (dao4) -8860 (zhun1) -8861 (heng2) -8862 (qu2) -8863 (yi1,yi4,yi3) -8864 (yi1) -8865 (bu3) -8866 (gan3) -8867 (yu2) -8868 (biao3) -8869 (cha4,cha3) -886A (yi2) -886B (shan1) -886C (chen4) -886D (fu1) -886E (gun3) -886F (fen1) -8870 (shuai1,cui1) -8871 (jie2,ji2) -8872 (na4) -8873 (zhong1) -8874 (dan3) -8875 (ri4) -8876 (zhong4) -8877 (zhong1) -8878 (xie4) -8879 (qi2,zhi1) -887A (xie2) -887B (ran2) -887C (zhi1) -887D (ren4) -887E (qin1) -887F (jin1) -8880 (jun1) -8881 (yuan2) -8882 (mei4) -8883 (chai4) -8884 (ao3) -8885 (niao3) -8886 (hui1) -8887 (ran2) -8888 (jia1) -8889 (tuo2) -888A (ling3) -888B (dai4) -888C (bao4) -888D (pao2) -888E (yao4) -888F (zuo4) -8890 (bi4) -8891 (shao4) -8892 (tan3) -8893 (ju3) -8894 (he4) -8895 (xue4) -8896 (xiu4) -8897 (zhen3) -8898 (yi2) -8899 (pa4) -889A (bo1,fu2) -889B (di1) -889C (wa4) -889D (fu4) -889E (gun3) -889F (zhi4) -88A0 (zhi4) -88A1 (ran2) -88A2 (pan4) -88A3 (yi4) -88A4 (mao4) -88A5 (none0) -88A6 (na4) -88A7 (kou1) -88A8 (xuan4) -88A9 (chan1) -88AA (qu1) -88AB (bei4,pi1) -88AC (yu4) -88AD (xi2) -88AE (none0) -88AF (bo2) -88B0 (none0) -88B1 (fu2) -88B2 (yi2) -88B3 (chi3) -88B4 (ku4) -88B5 (ren4) -88B6 (jiang4) -88B7 (jia2,qia1) -88B8 (cun2) -88B9 (mo4) -88BA (jie2) -88BB (er2) -88BC (ge1) -88BD (ru2) -88BE (zhu1) -88BF (gui1) -88C0 (yin1) -88C1 (cai2) -88C2 (lie4,lie3) -88C3 (none0) -88C4 (none0) -88C5 (zhuang1) -88C6 (dang1) -88C7 (none0) -88C8 (kun1) -88C9 (ken4) -88CA (niao3) -88CB (shu4) -88CC (jia2) -88CD (kun3) -88CE (cheng2,cheng3) -88CF (li3) -88D0 (juan1) -88D1 (shen1) -88D2 (pou2) -88D3 (ge2) -88D4 (yi4) -88D5 (yu4) -88D6 (chen3) -88D7 (liu2) -88D8 (qiu2) -88D9 (qun2) -88DA (ji4) -88DB (yi4) -88DC (bu3) -88DD (zhuang1) -88DE (shui4) -88DF (sha1) -88E0 (qun2) -88E1 (li3) -88E2 (lian2) -88E3 (lian3) -88E4 (ku4) -88E5 (jian3) -88E6 (fou2) -88E7 (tan3) -88E8 (bi4,pi2,bei1) -88E9 (gun1) -88EA (tao2) -88EB (yuan1) -88EC (ling2) -88ED (chi3) -88EE (chang1) -88EF (chou2) -88F0 (duo1) -88F1 (biao3) -88F2 (liang3) -88F3 (shang5,chang2) -88F4 (pei2) -88F5 (pei2) -88F6 (fei1) -88F7 (yuan1) -88F8 (luo3) -88F9 (guo3) -88FA (yan3) -88FB (du3) -88FC (ti4,xi1,xi2) -88FD (zhi4) -88FE (ju1) -88FF (qi3) -8900 (ji4) -8901 (zhi2) -8902 (gua4) -8903 (ken4) -8904 (none0) -8905 (ti4) -8906 (shi4) -8907 (fu4) -8908 (chong2) -8909 (xie1) -890A (bian3) -890B (die2) -890C (kun1,hui1) -890D (duan1) -890E (xiu4) -890F (xiu4) -8910 (he4,he2) -8911 (yuan4) -8912 (bao1) -8913 (bao3) -8914 (fu4) -8915 (yu2) -8916 (tuan4) -8917 (yan3) -8918 (hui1) -8919 (bei4) -891A (chu3,zhu3) -891B (lu:3) -891C (none0) -891D (none0) -891E (yun3) -891F (ta1) -8920 (gou1) -8921 (da1) -8922 (huai2) -8923 (rong2) -8924 (yuan4) -8925 (ru4) -8926 (nai4) -8927 (jiong3) -8928 (suo3) -8929 (ban1) -892A (tun4,tui4) -892B (chi3) -892C (sang3) -892D (niao3) -892E (ying1) -892F (jie4) -8930 (qian1) -8931 (huai2) -8932 (ku4) -8933 (lian2) -8934 (lan2) -8935 (li2) -8936 (zhe3,zhe2,xi2) -8937 (shi1) -8938 (lu:3) -8939 (yi4) -893A (die1) -893B (xie4) -893C (xian1) -893D (wei4) -893E (biao3) -893F (cao2) -8940 (ji1) -8941 (qiang3) -8942 (sen1) -8943 (bao1) -8944 (xiang1) -8945 (none0) -8946 (pu2) -8947 (jian3) -8948 (zhuan4) -8949 (jian4) -894A (zui4) -894B (ji2) -894C (dan1) -894D (za2) -894E (fan2) -894F (bo1) -8950 (xiang4) -8951 (xin2) -8952 (bie2,bi4) -8953 (rao2) -8954 (man3) -8955 (lan2) -8956 (ao3) -8957 (duo2) -8958 (hui4) -8959 (cao4) -895A (sui4) -895B (nong2) -895C (chan1) -895D (lian4,lian3) -895E (bi4) -895F (jin1) -8960 (dang1) -8961 (shu2) -8962 (tan3) -8963 (bi4) -8964 (lan2) -8965 (pu2) -8966 (ru2) -8967 (zhi3) -8968 (none0) -8969 (shu3) -896A (wa4) -896B (shi4) -896C (bai3) -896D (xie2) -896E (bo2) -896F (chen4) -8970 (lai3) -8971 (long2) -8972 (xi2) -8973 (xian1) -8974 (lan2) -8975 (zhe2) -8976 (dai4) -8977 (none0) -8978 (zan4) -8979 (shi1) -897A (jian3) -897B (pan4) -897C (yi4) -897D (none0) -897E (ya4) -897F (xi1) -8980 (xi1) -8981 (yao4,yao1) -8982 (feng3) -8983 (tan2,qin2) -8984 (none0) -8985 (none0) -8986 (fu4) -8987 (ba4) -8988 (he2) -8989 (ji1) -898A (ji1) -898B (jian4,xian4) -898C (guan1) -898D (bian4) -898E (yan4) -898F (gui1) -8990 (jue2,jiao4) -8991 (pian3) -8992 (mao2) -8993 (mi4) -8994 (mi4) -8995 (mie4) -8996 (shi4) -8997 (si1) -8998 (zhan1,chan1) -8999 (luo2) -899A (jue2,jiao4) -899B (mo4) -899C (tiao4) -899D (lian2) -899E (yao4) -899F (zhi4) -89A0 (jun1) -89A1 (xi2) -89A2 (shan3) -89A3 (wei1) -89A4 (xi4) -89A5 (tian3) -89A6 (yu2) -89A7 (lan3) -89A8 (e4) -89A9 (du3) -89AA (qin1,qing4) -89AB (pang3) -89AC (ji4) -89AD (ming2) -89AE (ping1) -89AF (gou4) -89B0 (qu4,qu1) -89B1 (zhan4) -89B2 (jin3,jin4) -89B3 (guan1,guan4) -89B4 (deng1) -89B5 (jian4) -89B6 (luo2) -89B7 (qu4,qu1) -89B8 (jian4) -89B9 (wei2) -89BA (jue2,jiao4) -89BB (qu4) -89BC (luo2) -89BD (lan3) -89BE (shen3) -89BF (di2) -89C0 (guan1,guan4) -89C1 (jian4,xian4) -89C2 (guan1,guan4) -89C3 (yan4) -89C4 (gui1) -89C5 (mi4) -89C6 (shi4) -89C7 (chan1) -89C8 (lan3) -89C9 (jue2,jiao4) -89CA (ji4) -89CB (xi2) -89CC (di2) -89CD (tian3) -89CE (yu2) -89CF (gou4) -89D0 (jin4) -89D1 (qu4,qu1) -89D2 (jiao3,jue2,jia3) -89D3 (jiu1) -89D4 (jin1) -89D5 (cu1) -89D6 (jue2) -89D7 (zhi4) -89D8 (chao4) -89D9 (ji2) -89DA (gu1) -89DB (dan4) -89DC (zui3,zi1) -89DD (di3) -89DE (shang1) -89DF (hua4) -89E0 (quan2) -89E1 (ge2) -89E2 (zhi4) -89E3 (jie3,jie4,xie4) -89E4 (gui3) -89E5 (gong1) -89E6 (chu4) -89E7 (jie3,jie4,xie4) -89E8 (huan4) -89E9 (qiu2) -89EA (xing1) -89EB (su4) -89EC (ni2) -89ED (ji1) -89EE (lu4) -89EF (zhi4) -89F0 (zhu1) -89F1 (bi4) -89F2 (xing1) -89F3 (hu2) -89F4 (shang1) -89F5 (gong1) -89F6 (zhi4) -89F7 (xue2) -89F8 (chu4) -89F9 (xi1) -89FA (yi2) -89FB (li4) -89FC (jue2) -89FD (xi1) -89FE (yan4) -89FF (xi1) -8A00 (yan2) -8A01 (yan2) -8A02 (ding4) -8A03 (fu4) -8A04 (qiu2) -8A05 (qiu2) -8A06 (jiao4) -8A07 (hong1) -8A08 (ji4) -8A09 (fan4) -8A0A (xun4) -8A0B (diao4) -8A0C (hong2,hong4) -8A0D (cha4) -8A0E (tao3) -8A0F (xu1) -8A10 (jie2) -8A11 (yi2) -8A12 (ren4) -8A13 (xun4) -8A14 (yin2) -8A15 (shan4) -8A16 (qi4) -8A17 (tuo1) -8A18 (ji4) -8A19 (xun4) -8A1A (yin2) -8A1B (e2) -8A1C (fen1) -8A1D (ya4) -8A1E (yao1) -8A1F (song4) -8A20 (shen3) -8A21 (yin2) -8A22 (xin1) -8A23 (jue2) -8A24 (xiao2) -8A25 (ne4,na4) -8A26 (chen2) -8A27 (you2) -8A28 (zhi3) -8A29 (xiong1) -8A2A (fang3) -8A2B (xin4) -8A2C (chao1) -8A2D (she4) -8A2E (xian1) -8A2F (sa3) -8A30 (zhun1) -8A31 (xu3,hu3) -8A32 (yi4) -8A33 (yi4) -8A34 (su4) -8A35 (chi1) -8A36 (he1) -8A37 (shen1) -8A38 (he2) -8A39 (xu4) -8A3A (zhen3,zhen1) -8A3B (zhu4) -8A3C (zheng4) -8A3D (gou4) -8A3E (zi3,zi1) -8A3F (zi3) -8A40 (zhan1) -8A41 (gu3) -8A42 (fu4) -8A43 (jian3) -8A44 (die2) -8A45 (ling2) -8A46 (di3) -8A47 (yang4) -8A48 (li4) -8A49 (nao2) -8A4A (pan4) -8A4B (zhou4) -8A4C (gan4) -8A4D (shi4) -8A4E (ju4) -8A4F (ao4) -8A50 (zha4) -8A51 (tuo1) -8A52 (yi2) -8A53 (qu3) -8A54 (zhao4) -8A55 (ping2) -8A56 (bi4) -8A57 (xiong4) -8A58 (chu4,qu1) -8A59 (ba2) -8A5A (da2) -8A5B (zu3) -8A5C (tao1) -8A5D (zhu3) -8A5E (ci2) -8A5F (zhe2) -8A60 (yong3) -8A61 (xu3) -8A62 (xun2) -8A63 (yi4) -8A64 (huang3) -8A65 (he2) -8A66 (shi4) -8A67 (cha2) -8A68 (jiao1) -8A69 (shi1) -8A6A (hen3) -8A6B (cha4) -8A6C (gou4) -8A6D (gui3) -8A6E (quan2) -8A6F (hui4) -8A70 (jie2) -8A71 (hua4) -8A72 (gai1) -8A73 (xiang2) -8A74 (hui4) -8A75 (shen1) -8A76 (chou2) -8A77 (tong2) -8A78 (mi2) -8A79 (zhan1) -8A7A (ming2) -8A7B (e4) -8A7C (hui1) -8A7D (yan2) -8A7E (xiong1) -8A7F (gua4) -8A80 (er4) -8A81 (beng3) -8A82 (tiao3,diao4) -8A83 (chi3) -8A84 (lei3) -8A85 (zhu1) -8A86 (kuang1) -8A87 (kua1) -8A88 (wu2) -8A89 (yu4) -8A8A (teng2) -8A8B (ji4) -8A8C (zhi4) -8A8D (ren4) -8A8E (su4) -8A8F (lang3) -8A90 (e2) -8A91 (kuang2) -8A92 (e^1,e^2,e^3,e^4) -8A93 (shi4) -8A94 (ting3) -8A95 (dan4) -8A96 (bei4) -8A97 (chan2) -8A98 (you4) -8A99 (heng2) -8A9A (qiao4) -8A9B (qin1) -8A9C (shua4) -8A9D (an1) -8A9E (yu3,yu4) -8A9F (xiao4) -8AA0 (cheng2) -8AA1 (jie4) -8AA2 (xian4) -8AA3 (wu1,wu2) -8AA4 (wu4) -8AA5 (gao4) -8AA6 (song4) -8AA7 (pu3) -8AA8 (hui4,hui3) -8AA9 (jing4) -8AAA (shuo1,shui4,yue4) -8AAB (zhen4) -8AAC (shuo1,shui4,yue4) -8AAD (du2,dou4) -8AAE (none0) -8AAF (chang4) -8AB0 (shui2,shei2) -8AB1 (jie2) -8AB2 (ke4) -8AB3 (qu1) -8AB4 (cong2) -8AB5 (xiao2) -8AB6 (sui4) -8AB7 (wang3) -8AB8 (xuan2) -8AB9 (fei3) -8ABA (chi1) -8ABB (ta4) -8ABC (yi2,yi4) -8ABD (na2) -8ABE (yin2) -8ABF (diao4,tiao2) -8AC0 (pi3) -8AC1 (chuo4) -8AC2 (chan3) -8AC3 (chen1) -8AC4 (zhun1) -8AC5 (ji1) -8AC6 (qi1) -8AC7 (tan2) -8AC8 (chui4) -8AC9 (wei3) -8ACA (ju1) -8ACB (qing3) -8ACC (jian4) -8ACD (zheng1,zheng4) -8ACE (ze2) -8ACF (zou1) -8AD0 (qian1) -8AD1 (zhuo2) -8AD2 (liang4) -8AD3 (jian4) -8AD4 (zhu4) -8AD5 (hao2) -8AD6 (lun4,lun2) -8AD7 (shen3) -8AD8 (biao3) -8AD9 (huai4) -8ADA (pian2) -8ADB (yu2) -8ADC (die2) -8ADD (xu3) -8ADE (pian2,pian3) -8ADF (shi4) -8AE0 (xuan1) -8AE1 (shi4) -8AE2 (hun4) -8AE3 (hua4) -8AE4 (e4) -8AE5 (zhong4) -8AE6 (di4) -8AE7 (xie2) -8AE8 (fu2) -8AE9 (pu3) -8AEA (ting2) -8AEB (jian4) -8AEC (qi3) -8AED (yu4) -8AEE (zi1) -8AEF (chuan2) -8AF0 (xi3) -8AF1 (hui4) -8AF2 (yin1) -8AF3 (an1) -8AF4 (xian2) -8AF5 (nan2) -8AF6 (chen2) -8AF7 (feng1,feng3,feng4) -8AF8 (zhu1) -8AF9 (yang2) -8AFA (yan4) -8AFB (heng1,heng2) -8AFC (xuan1) -8AFD (ge2) -8AFE (nuo4) -8AFF (qi4) -8B00 (mou2) -8B01 (ye4) -8B02 (wei4) -8B03 (none0) -8B04 (teng2) -8B05 (zou1,zhou1) -8B06 (shan4) -8B07 (jian3) -8B08 (bo2) -8B09 (none0) -8B0A (huang3) -8B0B (huo4) -8B0C (ge1) -8B0D (ying2) -8B0E (mi2,mei4) -8B0F (xiao3,sou3) -8B10 (mi4) -8B11 (xi4) -8B12 (qiang1) -8B13 (chen1) -8B14 (nu:e4,xue4) -8B15 (si1) -8B16 (su4) -8B17 (bang4) -8B18 (chi2) -8B19 (qian1) -8B1A (shi4,yi4) -8B1B (jiang3) -8B1C (yuan4) -8B1D (xie4) -8B1E (xue4) -8B1F (tao1) -8B20 (yao2) -8B21 (yao2) -8B22 (hu4) -8B23 (yu2) -8B24 (biao1) -8B25 (cong4) -8B26 (qing3,qing4) -8B27 (li2) -8B28 (mo2) -8B29 (mo4) -8B2A (shang1) -8B2B (zhe2) -8B2C (miu4) -8B2D (jian3) -8B2E (ze2) -8B2F (zha1) -8B30 (lian2) -8B31 (lou2) -8B32 (can1) -8B33 (ou1) -8B34 (guan4) -8B35 (xi2) -8B36 (zhuo2) -8B37 (ao2) -8B38 (ao2) -8B39 (jin3) -8B3A (zhe2) -8B3B (yi2) -8B3C (hu1) -8B3D (jiang4) -8B3E (man2,man4) -8B3F (chao2) -8B40 (han4) -8B41 (hua2) -8B42 (chan3) -8B43 (xu1) -8B44 (zeng1) -8B45 (se4) -8B46 (xi1) -8B47 (she1) -8B48 (dui4) -8B49 (zheng4) -8B4A (nao2) -8B4B (lan2) -8B4C (e2) -8B4D (ying4) -8B4E (jue2) -8B4F (ji1) -8B50 (zun3) -8B51 (jiao3) -8B52 (bo4) -8B53 (hui4) -8B54 (zhuan4) -8B55 (wu2) -8B56 (jian4,zen4) -8B57 (zha2) -8B58 (shi4,zhi4) -8B59 (qiao2,qiao4) -8B5A (tan2) -8B5B (zen4) -8B5C (pu3) -8B5D (sheng2) -8B5E (xuan1) -8B5F (zao4) -8B60 (zhan1) -8B61 (dang3) -8B62 (sui4) -8B63 (qian1) -8B64 (ji1) -8B65 (jiao4) -8B66 (jing3) -8B67 (lian2) -8B68 (nou4) -8B69 (yi1) -8B6A (ai4) -8B6B (zhan1) -8B6C (pi4) -8B6D (hui3) -8B6E (hua4) -8B6F (yi4) -8B70 (yi4) -8B71 (shan4) -8B72 (rang4) -8B73 (nou4) -8B74 (qian3) -8B75 (zhui4) -8B76 (ta4) -8B77 (hu4) -8B78 (zhou1) -8B79 (hao2) -8B7A (ni3) -8B7B (ying1) -8B7C (jian4,jian1) -8B7D (yu4) -8B7E (jian3) -8B7F (hui4) -8B80 (du2,dou4) -8B81 (zhe2) -8B82 (xuan4) -8B83 (zan4) -8B84 (lei3) -8B85 (shen3) -8B86 (wei4) -8B87 (chan3) -8B88 (li4) -8B89 (yi2) -8B8A (bian4) -8B8B (zhe2) -8B8C (yan4) -8B8D (e4) -8B8E (chou2) -8B8F (wei4) -8B90 (chou2) -8B91 (yao4) -8B92 (chan2) -8B93 (rang4) -8B94 (yin3) -8B95 (lan2) -8B96 (chen4) -8B97 (huo4) -8B98 (zhe2) -8B99 (huan1) -8B9A (zan4) -8B9B (yi4) -8B9C (dang3) -8B9D (zhan1) -8B9E (yan4) -8B9F (du2) -8BA0 (yan2) -8BA1 (ji4) -8BA2 (ding4) -8BA3 (fu4) -8BA4 (ren4) -8BA5 (ji1) -8BA6 (jie2) -8BA7 (hong4) -8BA8 (tao3) -8BA9 (rang4) -8BAA (shan4) -8BAB (qi4) -8BAC (tuo1) -8BAD (xun4) -8BAE (yi4) -8BAF (xun4) -8BB0 (ji4) -8BB1 (ren4) -8BB2 (jiang3) -8BB3 (hui4) -8BB4 (ou1) -8BB5 (ju4) -8BB6 (ya4) -8BB7 (ne4) -8BB8 (xu3) -8BB9 (e2) -8BBA (lun4,lun2) -8BBB (xiong1) -8BBC (song4) -8BBD (feng3) -8BBE (she4) -8BBF (fang3) -8BC0 (jue2) -8BC1 (zheng4) -8BC2 (gu3) -8BC3 (he1) -8BC4 (ping2) -8BC5 (zu3) -8BC6 (shi1,zhi4) -8BC7 (xiong4) -8BC8 (zha4) -8BC9 (su4) -8BCA (zhen3) -8BCB (di3) -8BCC (zhou1) -8BCD (ci2) -8BCE (qu1) -8BCF (zhao4) -8BD0 (bi4) -8BD1 (yi4) -8BD2 (yi2) -8BD3 (kuang1) -8BD4 (lei3) -8BD5 (shi4) -8BD6 (gua4) -8BD7 (shi1) -8BD8 (jie2,ji2) -8BD9 (hui1) -8BDA (cheng2) -8BDB (zhu1) -8BDC (shen1) -8BDD (hua4) -8BDE (dan4) -8BDF (gou4) -8BE0 (quan2) -8BE1 (gui3) -8BE2 (xun2) -8BE3 (yi4) -8BE4 (zheng4) -8BE5 (gai1) -8BE6 (xiang2) -8BE7 (cha4) -8BE8 (hun4) -8BE9 (xu3) -8BEA (zhou1) -8BEB (jie4) -8BEC (wu1) -8BED (yu3,yu4) -8BEE (qiao4) -8BEF (wu4) -8BF0 (gao4) -8BF1 (you4) -8BF2 (hui4) -8BF3 (kuang2) -8BF4 (shuo1,shui4,yue4) -8BF5 (song4) -8BF6 (ei4,ei2,ei3,ai3,e^1,e^2,e^3,e^4) -8BF7 (qing3) -8BF8 (zhu1) -8BF9 (zou1) -8BFA (nuo4) -8BFB (du2,dou4) -8BFC (zhuo2) -8BFD (fei3) -8BFE (ke4) -8BFF (wei3) -8C00 (yu2) -8C01 (shei2,shui2) -8C02 (shen3) -8C03 (diao4,tiao2) -8C04 (chan3) -8C05 (liang4) -8C06 (zhun1) -8C07 (sui4) -8C08 (tan2) -8C09 (shen3) -8C0A (yi4) -8C0B (mou2) -8C0C (chen2) -8C0D (die2) -8C0E (huang3) -8C0F (jian4) -8C10 (xie2) -8C11 (xue4) -8C12 (ye4) -8C13 (wei4) -8C14 (e4) -8C15 (yu4) -8C16 (xuan1) -8C17 (chan2) -8C18 (zi1) -8C19 (an1) -8C1A (yan4) -8C1B (di4) -8C1C (mi2,mei4) -8C1D (pian3) -8C1E (xu3) -8C1F (mo2) -8C20 (dang3) -8C21 (su4) -8C22 (xie4) -8C23 (yao2) -8C24 (bang4) -8C25 (shi4) -8C26 (qian1) -8C27 (mi4) -8C28 (jin3) -8C29 (man2,man4) -8C2A (zhe2) -8C2B (jian3) -8C2C (miu4) -8C2D (tan2) -8C2E (jian4,zen4) -8C2F (qiao2,qiao4) -8C30 (lan2) -8C31 (pu3) -8C32 (jue2) -8C33 (yan4) -8C34 (qian3) -8C35 (zhan1) -8C36 (chen4) -8C37 (gu3,yu4) -8C38 (qian1) -8C39 (hong2) -8C3A (ya2) -8C3B (jue2) -8C3C (hong2) -8C3D (han1) -8C3E (hong1) -8C3F (qi1,xi1) -8C40 (xi1) -8C41 (huo4,huo1,hua2) -8C42 (liao2) -8C43 (han3) -8C44 (du2) -8C45 (long2) -8C46 (dou4) -8C47 (jiang1) -8C48 (qi3,kai3) -8C49 (chi3) -8C4A (feng1,li3) -8C4B (deng1) -8C4C (wan1) -8C4D (bi1) -8C4E (shu4) -8C4F (xian4) -8C50 (feng1) -8C51 (zhi4) -8C52 (zhi4) -8C53 (yan4) -8C54 (yan4) -8C55 (shi3) -8C56 (chu4) -8C57 (hui1) -8C58 (tun2) -8C59 (yi4) -8C5A (tun2) -8C5B (yi4) -8C5C (jian1) -8C5D (ba1) -8C5E (hou4) -8C5F (e4) -8C60 (cu2) -8C61 (xiang4) -8C62 (huan4) -8C63 (jian1) -8C64 (ken3) -8C65 (gai1) -8C66 (qu2) -8C67 (fu1) -8C68 (xi1) -8C69 (bin1) -8C6A (hao2) -8C6B (yu4) -8C6C (zhu1) -8C6D (jia1) -8C6E (fen2) -8C6F (xi1) -8C70 (hu4) -8C71 (wen1) -8C72 (huan2) -8C73 (bin1) -8C74 (di2) -8C75 (zong1) -8C76 (fen2) -8C77 (yi4) -8C78 (zhi4) -8C79 (bao4) -8C7A (chai2) -8C7B (han4,an4) -8C7C (pi2) -8C7D (na4) -8C7E (pi1) -8C7F (gou3) -8C80 (duo4) -8C81 (you4) -8C82 (diao1) -8C83 (mo4) -8C84 (si4) -8C85 (xiu1) -8C86 (huan2) -8C87 (kun1) -8C88 (he2) -8C89 (he2,hao2,mo4) -8C8A (mo4) -8C8B (an4) -8C8C (mao4) -8C8D (li2) -8C8E (ni2) -8C8F (bi3) -8C90 (yu3) -8C91 (jia1) -8C92 (tuan1) -8C93 (mao1) -8C94 (pi2) -8C95 (xi1) -8C96 (e4) -8C97 (ju4) -8C98 (mo4) -8C99 (chu1) -8C9A (tan2) -8C9B (huan1) -8C9C (qu2) -8C9D (bei4) -8C9E (zhen1) -8C9F (yuan2,yun2,yun4) -8CA0 (fu4) -8CA1 (cai2) -8CA2 (gong4) -8CA3 (te4) -8CA4 (yi2) -8CA5 (hang2) -8CA6 (wan4) -8CA7 (pin2) -8CA8 (huo4) -8CA9 (fan4) -8CAA (tan1) -8CAB (guan4) -8CAC (ze2,zhai4) -8CAD (zhi4) -8CAE (er4) -8CAF (zhu3,zhu4) -8CB0 (shi4) -8CB1 (bi4) -8CB2 (zi1) -8CB3 (er4) -8CB4 (gui4) -8CB5 (pian1) -8CB6 (bian3) -8CB7 (mai3) -8CB8 (dai4) -8CB9 (sheng4) -8CBA (kuang4) -8CBB (fei4) -8CBC (tie1) -8CBD (yi2) -8CBE (chi2,chi1) -8CBF (mao4) -8CC0 (he4) -8CC1 (bi4,ben1) -8CC2 (lu4) -8CC3 (lin4,ren4) -8CC4 (hui4,hui3) -8CC5 (gai1) -8CC6 (pian2) -8CC7 (zi1) -8CC8 (jia3,gu3,jia4) -8CC9 (xu4) -8CCA (zei2,ze2) -8CCB (jiao3) -8CCC (gai1) -8CCD (zang1) -8CCE (jian4) -8CCF (ying4) -8CD0 (xun4) -8CD1 (zhen4) -8CD2 (she1) -8CD3 (bin1) -8CD4 (bin1) -8CD5 (qiu2) -8CD6 (she1) -8CD7 (chuan4) -8CD8 (zang1) -8CD9 (zhou1) -8CDA (lai4) -8CDB (zan4) -8CDC (si4,ci4) -8CDD (chen1) -8CDE (shang3) -8CDF (tian3) -8CE0 (pei2) -8CE1 (geng1) -8CE2 (xian2) -8CE3 (mai4) -8CE4 (jian4) -8CE5 (sui4) -8CE6 (fu4) -8CE7 (dan3) -8CE8 (cong2) -8CE9 (cong2) -8CEA (zhi2,zhi4) -8CEB (ji1) -8CEC (zhang4) -8CED (du3) -8CEE (jin4) -8CEF (xiong1) -8CF0 (shun3) -8CF1 (yun3) -8CF2 (bao3) -8CF3 (zai1) -8CF4 (lai4) -8CF5 (feng4) -8CF6 (cang4) -8CF7 (ji1) -8CF8 (sheng4) -8CF9 (ai4) -8CFA (zhuan4,zuan4) -8CFB (fu4) -8CFC (gou4) -8CFD (sai4) -8CFE (ze2) -8CFF (liao2) -8D00 (wei4) -8D01 (bai4) -8D02 (chen3) -8D03 (zhuan4) -8D04 (zhi4) -8D05 (zhui4) -8D06 (biao1) -8D07 (yun1) -8D08 (zeng4) -8D09 (tan3) -8D0A (zan4) -8D0B (yan4) -8D0C (none0) -8D0D (shan4) -8D0E (wan4) -8D0F (ying2) -8D10 (jin4) -8D11 (gan3) -8D12 (xian2) -8D13 (zang1) -8D14 (bi4) -8D15 (du2) -8D16 (shu2) -8D17 (yan4) -8D18 (none0) -8D19 (xuan4) -8D1A (long4) -8D1B (gan4) -8D1C (zang1) -8D1D (bei4) -8D1E (zhen1) -8D1F (fu4) -8D20 (yuan2,yun2,yun4) -8D21 (gong4) -8D22 (cai2) -8D23 (ze2) -8D24 (xian2) -8D25 (bai4) -8D26 (zhang4) -8D27 (huo4) -8D28 (zhi4) -8D29 (fan4) -8D2A (tan1) -8D2B (pin2) -8D2C (bian3) -8D2D (gou4) -8D2E (zhu4) -8D2F (guan4) -8D30 (er4) -8D31 (jian4) -8D32 (bi4,ben1) -8D33 (shi4) -8D34 (tie1) -8D35 (gui4) -8D36 (kuang4) -8D37 (dai4) -8D38 (mao4) -8D39 (fei4) -8D3A (he4) -8D3B (yi2) -8D3C (zei2) -8D3D (zhi4) -8D3E (jia3,gu3) -8D3F (hui4) -8D40 (zi1) -8D41 (lin4) -8D42 (lu4) -8D43 (zang1) -8D44 (zi1) -8D45 (gai1) -8D46 (jin4) -8D47 (qiu2) -8D48 (zhen4) -8D49 (lai4) -8D4A (she1) -8D4B (fu4) -8D4C (du3) -8D4D (ji1) -8D4E (shu2) -8D4F (shang3) -8D50 (ci4) -8D51 (bi4) -8D52 (zhou1) -8D53 (geng1) -8D54 (pei2) -8D55 (dan3) -8D56 (lai4) -8D57 (feng4) -8D58 (zhui4) -8D59 (fu4) -8D5A (zhuan4,zuan4) -8D5B (sai4) -8D5C (ze2) -8D5D (yan4) -8D5E (zan4) -8D5F (yun1) -8D60 (zeng4) -8D61 (shan4) -8D62 (ying2) -8D63 (gan4) -8D64 (chi4) -8D65 (xi1) -8D66 (she4) -8D67 (nan3) -8D68 (xiong2) -8D69 (xi4) -8D6A (cheng1) -8D6B (he4) -8D6C (cheng1) -8D6D (zhe3) -8D6E (xia2) -8D6F (tang2) -8D70 (zou3) -8D71 (zou3) -8D72 (li4) -8D73 (jiu1,jiu3) -8D74 (fu4) -8D75 (zhao4) -8D76 (gan3) -8D77 (qi3) -8D78 (shan4) -8D79 (qiong2) -8D7A (qin2) -8D7B (xian3) -8D7C (ci1) -8D7D (jue2) -8D7E (qin3) -8D7F (chi2) -8D80 (ci1) -8D81 (chen4) -8D82 (chen4) -8D83 (die2) -8D84 (ju1,qie4,qie5,ju4) -8D85 (chao1) -8D86 (di1) -8D87 (se4) -8D88 (zhan1) -8D89 (zhu2) -8D8A (yue4) -8D8B (qu1) -8D8C (jie2) -8D8D (chi2) -8D8E (chu2) -8D8F (gua1) -8D90 (xue4) -8D91 (zi1) -8D92 (tiao2) -8D93 (duo3) -8D94 (lie4) -8D95 (gan3) -8D96 (suo1) -8D97 (cu4) -8D98 (xi2) -8D99 (zhao4) -8D9A (su4) -8D9B (yin3) -8D9C (ju2) -8D9D (jian4) -8D9E (que4) -8D9F (tang4,tang1) -8DA0 (chuo4) -8DA1 (cui3) -8DA2 (lu4) -8DA3 (qu4,cu4) -8DA4 (dang4) -8DA5 (qiu1) -8DA6 (zi1) -8DA7 (ti2) -8DA8 (qu1,cu4) -8DA9 (chi4) -8DAA (huang2) -8DAB (qiao2) -8DAC (qiao1) -8DAD (yao4) -8DAE (zao4) -8DAF (yue4) -8DB0 (none0) -8DB1 (zan3) -8DB2 (zan3,zan4) -8DB3 (zu2,ju4) -8DB4 (pa1) -8DB5 (bao4,bo1) -8DB6 (ku4) -8DB7 (he2) -8DB8 (dun3) -8DB9 (jue2) -8DBA (fu1) -8DBB (chen3) -8DBC (jian3) -8DBD (fang4) -8DBE (zhi3) -8DBF (ta1) -8DC0 (yue4) -8DC1 (pa2) -8DC2 (qi2) -8DC3 (yue4) -8DC4 (qiang1,qiang4) -8DC5 (tuo4) -8DC6 (tai2) -8DC7 (yi4) -8DC8 (nian3) -8DC9 (ling2) -8DCA (mei4) -8DCB (ba2) -8DCC (die1,die2) -8DCD (ku1) -8DCE (tuo2) -8DCF (jia1) -8DD0 (ci3) -8DD1 (pao3,pao2) -8DD2 (qia3) -8DD3 (zhu4) -8DD4 (ju1) -8DD5 (die2) -8DD6 (zhi2,zhi1) -8DD7 (fu1) -8DD8 (pan2) -8DD9 (ju3) -8DDA (shan1) -8DDB (bo3) -8DDC (ni2) -8DDD (ju4) -8DDE (li4,luo4) -8DDF (gen1) -8DE0 (yi2) -8DE1 (ji1) -8DE2 (dai4) -8DE3 (xian3) -8DE4 (jiao1) -8DE5 (duo4) -8DE6 (chu2) -8DE7 (quan2) -8DE8 (kua4) -8DE9 (zhuai3,shi4) -8DEA (gui4) -8DEB (qiong2) -8DEC (kui3) -8DED (xiang2) -8DEE (chi4) -8DEF (lu4) -8DF0 (beng4) -8DF1 (zhi4) -8DF2 (jia2) -8DF3 (tiao4) -8DF4 (cai3) -8DF5 (jian4) -8DF6 (da5) -8DF7 (qiao1) -8DF8 (bi4) -8DF9 (xian1) -8DFA (duo4) -8DFB (ji1) -8DFC (ju2) -8DFD (ji4) -8DFE (shu2) -8DFF (tu2) -8E00 (chu4) -8E01 (xing4) -8E02 (nie4) -8E03 (xiao1) -8E04 (bo2) -8E05 (xue2) -8E06 (qun1) -8E07 (mou3) -8E08 (shu1) -8E09 (liang4,liang2) -8E0A (yong3) -8E0B (jiao3,jia3,jue2) -8E0C (chou2) -8E0D (xiao4) -8E0E (none0) -8E0F (ta4,ta1) -8E10 (jian4) -8E11 (qi2) -8E12 (wo1) -8E13 (wei3) -8E14 (chuo1) -8E15 (jie2) -8E16 (ji2) -8E17 (nie1) -8E18 (ju2) -8E19 (ju1) -8E1A (lun2) -8E1B (lu4) -8E1C (leng4) -8E1D (huai2) -8E1E (ju4) -8E1F (chi2) -8E20 (wan4) -8E21 (quan2) -8E22 (ti1) -8E23 (bo2) -8E24 (zu2) -8E25 (qie4) -8E26 (qi1) -8E27 (cu4) -8E28 (zong1) -8E29 (cai3) -8E2A (zong1) -8E2B (pan2) -8E2C (zhi4) -8E2D (zheng1) -8E2E (dian3,die1) -8E2F (zhi2) -8E30 (yu2) -8E31 (duo2) -8E32 (dun4) -8E33 (chun3) -8E34 (yong3) -8E35 (zhong3) -8E36 (di4) -8E37 (zha3) -8E38 (chen3) -8E39 (chuai4) -8E3A (jian4) -8E3B (gua1) -8E3C (tang2) -8E3D (ju3) -8E3E (fu2) -8E3F (zu2) -8E40 (die2) -8E41 (pian2) -8E42 (rou2) -8E43 (nuo4) -8E44 (ti2) -8E45 (cha3) -8E46 (tui3) -8E47 (jian3) -8E48 (dao3) -8E49 (cuo1) -8E4A (xi1,qi1) -8E4B (ta4) -8E4C (qiang1,qiang4) -8E4D (zhan3) -8E4E (dian1) -8E4F (ti2) -8E50 (ji2) -8E51 (nie4) -8E52 (pan2) -8E53 (liu4) -8E54 (zhan4) -8E55 (bi4) -8E56 (chong1) -8E57 (lu4) -8E58 (liao2) -8E59 (cu4) -8E5A (tang1) -8E5B (dai4) -8E5C (su4) -8E5D (xi3) -8E5E (kui3) -8E5F (ji1) -8E60 (zhi2) -8E61 (qiang1) -8E62 (di2,zhi2) -8E63 (man2,pan2) -8E64 (zong1) -8E65 (lian2) -8E66 (beng4) -8E67 (zao1) -8E68 (nian3) -8E69 (bie2) -8E6A (tui2) -8E6B (ju2) -8E6C (deng4,deng1) -8E6D (ceng4) -8E6E (xian1) -8E6F (fan2) -8E70 (chu2) -8E71 (zhong1) -8E72 (dun1,cun2) -8E73 (bo1) -8E74 (cu4) -8E75 (zu2) -8E76 (jue2,jue3) -8E77 (jue2) -8E78 (lin2) -8E79 (ta4) -8E7A (qiao1) -8E7B (qiao1) -8E7C (pu3,pu2) -8E7D (liao1) -8E7E (dun1) -8E7F (cuan1) -8E80 (kuang4) -8E81 (zao4) -8E82 (ta4) -8E83 (bi4) -8E84 (bi4) -8E85 (zhu2) -8E86 (ju4) -8E87 (chu2) -8E88 (qiao4) -8E89 (dun3) -8E8A (chou2) -8E8B (ji1,ji4) -8E8C (wu3) -8E8D (yue4) -8E8E (nian3) -8E8F (lin4) -8E90 (lie4) -8E91 (zhi2) -8E92 (li4) -8E93 (zhi4) -8E94 (chan2) -8E95 (chu2) -8E96 (duan4) -8E97 (wei4) -8E98 (long2) -8E99 (lin4) -8E9A (xian1) -8E9B (wei4) -8E9C (zuan1) -8E9D (lan2) -8E9E (xie4) -8E9F (rang2) -8EA0 (xie4) -8EA1 (nie4) -8EA2 (ta4) -8EA3 (qu2) -8EA4 (jie4) -8EA5 (cuan1) -8EA6 (zuan1) -8EA7 (xi3) -8EA8 (kui2) -8EA9 (jue2) -8EAA (lin4) -8EAB (shen1,juan1) -8EAC (gong1) -8EAD (dan1) -8EAE (none0) -8EAF (qu1) -8EB0 (ti3) -8EB1 (duo3) -8EB2 (duo3) -8EB3 (gong1) -8EB4 (lang2) -8EB5 (none0) -8EB6 (luo3) -8EB7 (ai3) -8EB8 (ji1) -8EB9 (ju2) -8EBA (tang3) -8EBB (none0) -8EBC (none0) -8EBD (yan3) -8EBE (none0) -8EBF (kang1) -8EC0 (qu1) -8EC1 (lou2) -8EC2 (lao4) -8EC3 (duo3) -8EC4 (zhi2) -8EC5 (none0) -8EC6 (ti3) -8EC7 (dao4) -8EC8 (none0) -8EC9 (yu4) -8ECA (che1,ju1) -8ECB (ya4,zha2,ga2) -8ECC (gui3) -8ECD (jun1) -8ECE (wei4) -8ECF (yue4) -8ED0 (xin4) -8ED1 (di4) -8ED2 (xuan1) -8ED3 (fan4) -8ED4 (ren4) -8ED5 (shan1) -8ED6 (qiang2) -8ED7 (shu1) -8ED8 (tun2) -8ED9 (chen2) -8EDA (dai4) -8EDB (e4) -8EDC (na4) -8EDD (qi2) -8EDE (mao2) -8EDF (ruan3) -8EE0 (ren4) -8EE1 (qian2) -8EE2 (zhuan3,zhuai3,zhuan4) -8EE3 (hong1) -8EE4 (hu1) -8EE5 (qu2) -8EE6 (huang4) -8EE7 (di3) -8EE8 (ling2) -8EE9 (dai4) -8EEA (ao1) -8EEB (zhen3) -8EEC (fan4) -8EED (kuang1) -8EEE (ang3) -8EEF (peng1) -8EF0 (bei4) -8EF1 (gu1) -8EF2 (gu1) -8EF3 (pao2) -8EF4 (zhu4) -8EF5 (rong3,fu3) -8EF6 (e4) -8EF7 (ba2) -8EF8 (zhou2,zhou4,zhu2) -8EF9 (zhi3) -8EFA (yao2) -8EFB (ke1,ke3) -8EFC (yi4) -8EFD (qing1) -8EFE (shi4) -8EFF (ping2) -8F00 (er2) -8F01 (qiong2) -8F02 (ju2) -8F03 (jiao4,jiao3) -8F04 (guang1) -8F05 (lu4) -8F06 (kai3) -8F07 (quan2) -8F08 (zhou1) -8F09 (zai4,zai3) -8F0A (zhi4) -8F0B (ju1) -8F0C (liang4) -8F0D (yu4) -8F0E (shao1) -8F0F (you2) -8F10 (huan3) -8F11 (yun3) -8F12 (zhe2) -8F13 (wan3) -8F14 (fu3) -8F15 (qing1) -8F16 (zhou1) -8F17 (ni2) -8F18 (ling2) -8F19 (zhe2) -8F1A (zhan4) -8F1B (liang4) -8F1C (zi1) -8F1D (hui1) -8F1E (wang3) -8F1F (chuo4) -8F20 (guo3) -8F21 (kan3) -8F22 (yi3) -8F23 (peng2) -8F24 (qian4) -8F25 (gun3) -8F26 (nian3) -8F27 (ping2) -8F28 (guan3) -8F29 (bei4) -8F2A (lun2) -8F2B (pai2) -8F2C (liang2) -8F2D (ruan3) -8F2E (rou2) -8F2F (ji2) -8F30 (yang2) -8F31 (xian2) -8F32 (chuan2) -8F33 (cou4) -8F34 (chun1) -8F35 (ge2) -8F36 (you2) -8F37 (hong1) -8F38 (shu1) -8F39 (fu4) -8F3A (zi1) -8F3B (fu2) -8F3C (wen1) -8F3D (ben4) -8F3E (zhan3) -8F3F (yu2) -8F40 (wen1) -8F41 (tao1) -8F42 (gu3,gu1) -8F43 (zhen1) -8F44 (xia2) -8F45 (yuan2) -8F46 (lu4) -8F47 (jiu1) -8F48 (chao2) -8F49 (zhuan3,zhuai3,zhuan4) -8F4A (wei4) -8F4B (hun2) -8F4C (none0) -8F4D (che4,zhe2) -8F4E (jiao4) -8F4F (zhan4) -8F50 (pu2) -8F51 (lao3) -8F52 (fen2) -8F53 (fan1) -8F54 (lin2) -8F55 (ge2) -8F56 (se4) -8F57 (kan3) -8F58 (huan4) -8F59 (yi3) -8F5A (ji2) -8F5B (dui4) -8F5C (er2) -8F5D (yu2) -8F5E (xian4) -8F5F (hong1) -8F60 (lei2) -8F61 (pei4) -8F62 (li4) -8F63 (li4) -8F64 (lu2) -8F65 (lin2) -8F66 (che1,ju1) -8F67 (ya4,zha2,ga2) -8F68 (gui3) -8F69 (xuan1) -8F6A (dai4) -8F6B (ren4) -8F6C (zhuan3,zhuan4,zhuai3) -8F6D (e4) -8F6E (lun2) -8F6F (ruan3) -8F70 (hong1) -8F71 (gu1) -8F72 (ke1,ke3) -8F73 (lu2,lu5) -8F74 (zhou2,zhou4) -8F75 (zhi3) -8F76 (yi4) -8F77 (hu1) -8F78 (zhen3) -8F79 (li4) -8F7A (yao2) -8F7B (qing1) -8F7C (shi4) -8F7D (zai3,zai4) -8F7E (zhi4) -8F7F (jiao4) -8F80 (zhou1) -8F81 (quan2) -8F82 (lu4) -8F83 (jiao4) -8F84 (zhe2) -8F85 (fu3) -8F86 (liang4) -8F87 (nian3) -8F88 (bei4) -8F89 (hui1) -8F8A (gun3) -8F8B (wang3) -8F8C (liang2) -8F8D (chuo4) -8F8E (zi1) -8F8F (cou4) -8F90 (fu2) -8F91 (ji2,ji5) -8F92 (wen1) -8F93 (shu1) -8F94 (pei4) -8F95 (yuan2) -8F96 (xia2) -8F97 (zhan3) -8F98 (lu4) -8F99 (zhe2) -8F9A (lin2) -8F9B (xin1) -8F9C (gu1) -8F9D (ci2) -8F9E (ci2) -8F9F (pi4,bi4,pi1) -8FA0 (zui4) -8FA1 (bian4) -8FA2 (la4) -8FA3 (la4) -8FA4 (ci2) -8FA5 (xue1) -8FA6 (ban4) -8FA7 (bian4) -8FA8 (bian4) -8FA9 (bian4) -8FAA (none0) -8FAB (bian4) -8FAC (ban1) -8FAD (ci2) -8FAE (bian4) -8FAF (bian4) -8FB0 (chen2) -8FB1 (ru3,ru4) -8FB2 (nong2) -8FB3 (nong2) -8FB4 (zhen3) -8FB5 (chuo4) -8FB6 (chuo4) -8FB7 (none0) -8FB8 (reng2) -8FB9 (bian1,bian5) -8FBA (bian1) -8FBB (none0) -8FBC (none0) -8FBD (liao2) -8FBE (da2) -8FBF (chan1) -8FC0 (gan1) -8FC1 (qian1) -8FC2 (yu1) -8FC3 (yu1) -8FC4 (qi4) -8FC5 (xun4) -8FC6 (yi3,yi2) -8FC7 (guo4,guo1) -8FC8 (mai4) -8FC9 (qi2) -8FCA (za1) -8FCB (wang4) -8FCC (none0) -8FCD (zhun1) -8FCE (ying2) -8FCF (ti4) -8FD0 (yun4) -8FD1 (jin4) -8FD2 (hang2) -8FD3 (ya4) -8FD4 (fan3) -8FD5 (wu3) -8FD6 (ta4) -8FD7 (e2) -8FD8 (hai2,huan2) -8FD9 (zhei4,zhe4) -8FDA (none0) -8FDB (jin4) -8FDC (yuan3) -8FDD (wei2) -8FDE (lian2) -8FDF (chi2) -8FE0 (che4) -8FE1 (ni4) -8FE2 (tiao2) -8FE3 (zhi4) -8FE4 (yi3,yi2) -8FE5 (jiong3) -8FE6 (jia1) -8FE7 (chen2) -8FE8 (dai4) -8FE9 (er3) -8FEA (di2) -8FEB (po4,pai3) -8FEC (wang3) -8FED (die2) -8FEE (ze2) -8FEF (tao2) -8FF0 (shu4) -8FF1 (tuo2) -8FF2 (none0) -8FF3 (jing4) -8FF4 (hui2) -8FF5 (tong2) -8FF6 (you4) -8FF7 (mi2) -8FF8 (beng4) -8FF9 (ji1,ji4) -8FFA (nai3) -8FFB (yi2) -8FFC (jie2) -8FFD (zhui1) -8FFE (lie4) -8FFF (xun4) -9000 (tui4) -9001 (song4) -9002 (shi4,kuo4) -9003 (tao2) -9004 (pang2) -9005 (hou4) -9006 (ni4) -9007 (dun4) -9008 (jiong3) -9009 (xuan3) -900A (xun4) -900B (bu1) -900C (you2) -900D (xiao1) -900E (qiu2) -900F (tou4) -9010 (zhu2) -9011 (qiu2) -9012 (di4) -9013 (di4) -9014 (tu2) -9015 (jing4) -9016 (ti4) -9017 (dou4) -9018 (yi3) -9019 (zhe4,zhei4) -901A (tong1,tong4) -901B (guang4) -901C (wu4) -901D (shi4) -901E (cheng3) -901F (su4) -9020 (zao4) -9021 (qun1) -9022 (feng2) -9023 (lian2) -9024 (suo4) -9025 (hui2) -9026 (li3) -9027 (none0) -9028 (zui4) -9029 (ben1) -902A (cuo4) -902B (jue2) -902C (beng4) -902D (huan4) -902E (dai4,dai3) -902F (lu4) -9030 (you2) -9031 (zhou1) -9032 (jin4) -9033 (yu4) -9034 (chuo4) -9035 (kui2) -9036 (wei1) -9037 (ti4) -9038 (yi4) -9039 (da2) -903A (yuan3) -903B (luo2) -903C (bi1) -903D (nuo4) -903E (yu2) -903F (dang4) -9040 (sui2) -9041 (dun4) -9042 (sui4,sui2) -9043 (yan3) -9044 (chuan2) -9045 (chi2) -9046 (ti2) -9047 (yu4) -9048 (shi2) -9049 (zhen1) -904A (you2) -904B (yun4) -904C (e4) -904D (bian4,pian4) -904E (guo4,guo1) -904F (e4) -9050 (xia2) -9051 (huang2) -9052 (qiu2) -9053 (dao4) -9054 (da2) -9055 (wei2) -9056 (none0) -9057 (yi2,wei4) -9058 (gou4) -9059 (yao2) -905A (chu4) -905B (liu2,liu4) -905C (xun4) -905D (ta4) -905E (di4) -905F (chi2) -9060 (yuan3,yuan4) -9061 (su4) -9062 (ta4,ta1) -9063 (qian3) -9064 (none0) -9065 (yao2) -9066 (guan4) -9067 (zhang1) -9068 (ao2) -9069 (shi4,kuo4) -906A (ce4) -906B (su4) -906C (su4) -906D (zao1) -906E (zhe1,zhe5) -906F (dun4) -9070 (zhi4) -9071 (lou2) -9072 (chi2) -9073 (cuo1) -9074 (lin2) -9075 (zun1) -9076 (rao4) -9077 (qian1) -9078 (xuan3) -9079 (yu4) -907A (yi2,wei4) -907B (wu4) -907C (liao2) -907D (ju4) -907E (shi4) -907F (bi4) -9080 (yao1) -9081 (mai4) -9082 (xie4) -9083 (sui4) -9084 (huan2,hai2,xuan2) -9085 (zhan1) -9086 (deng4) -9087 (er3) -9088 (miao3) -9089 (bian1) -908A (bian1) -908B (la1) -908C (li2) -908D (yuan2) -908E (you2) -908F (luo2) -9090 (li3) -9091 (yi4) -9092 (ting2) -9093 (deng4) -9094 (qi3) -9095 (yong1) -9096 (shan1) -9097 (han2) -9098 (yu2) -9099 (mang2) -909A (ru2) -909B (qiong2) -909C (none0) -909D (kuang4) -909E (fu1) -909F (kang4) -90A0 (bin1) -90A1 (fang1) -90A2 (xing2) -90A3 (nei4,na4,na1,na3) -90A4 (none0) -90A5 (shen3) -90A6 (bang1) -90A7 (yuan2) -90A8 (cun1) -90A9 (huo3) -90AA (xie2,ye2) -90AB (bang1) -90AC (wu1) -90AD (ju4) -90AE (you2) -90AF (han2) -90B0 (tai2) -90B1 (qiu1) -90B2 (bi4) -90B3 (pi1) -90B4 (bing3) -90B5 (shao4) -90B6 (bei4) -90B7 (wa3) -90B8 (di3) -90B9 (zou1) -90BA (ye4) -90BB (lin2) -90BC (kuang1) -90BD (gui1) -90BE (zhu1) -90BF (shi1) -90C0 (ku1) -90C1 (yu4) -90C2 (gai1) -90C3 (he2) -90C4 (qie4) -90C5 (zhi4) -90C6 (ji2) -90C7 (xun2,huan2) -90C8 (hou4) -90C9 (xing2) -90CA (jiao1) -90CB (xi1) -90CC (gui1) -90CD (nuo2) -90CE (lang2,lang4) -90CF (jia2) -90D0 (kuai4) -90D1 (zheng4) -90D2 (lang2,lang4) -90D3 (yun4) -90D4 (yan2) -90D5 (cheng2) -90D6 (dou1) -90D7 (xi1) -90D8 (lu:3) -90D9 (fu3) -90DA (wu2) -90DB (fu2) -90DC (gao4) -90DD (hao3) -90DE (lang2) -90DF (jia2) -90E0 (geng3) -90E1 (jun4) -90E2 (ying3) -90E3 (bo2) -90E4 (xi4) -90E5 (bei4) -90E6 (li4) -90E7 (yun2) -90E8 (bu4) -90E9 (xiao2) -90EA (qi1) -90EB (pi2) -90EC (qing1) -90ED (guo1) -90EE (none0) -90EF (tan2) -90F0 (zou1) -90F1 (ping2) -90F2 (lai2) -90F3 (ni2) -90F4 (chen1) -90F5 (you2) -90F6 (bu4) -90F7 (xiang1) -90F8 (dan1) -90F9 (ju2) -90FA (yong1) -90FB (qiao1) -90FC (yi1) -90FD (dou1,du1) -90FE (yan3) -90FF (mei2) -9100 (ruo4) -9101 (bei4) -9102 (e4) -9103 (yu2) -9104 (juan4) -9105 (yu3) -9106 (yun4) -9107 (hou4) -9108 (kui2) -9109 (xiang1) -910A (xiang1) -910B (sou1) -910C (tang2) -910D (ming2) -910E (xi4) -910F (ru4) -9110 (chu4) -9111 (zi1) -9112 (zou1) -9113 (ju2) -9114 (wu1) -9115 (xiang1) -9116 (yun2) -9117 (hao4) -9118 (yong1) -9119 (bi3,bi4) -911A (mao4) -911B (chao2) -911C (fu1) -911D (liao3) -911E (yin2) -911F (zhuan1) -9120 (hu4) -9121 (qiao1) -9122 (yan1) -9123 (zhang1) -9124 (fan4) -9125 (wu3) -9126 (xu3) -9127 (deng4) -9128 (bi4) -9129 (xin2) -912A (bi4) -912B (ceng2) -912C (wei2) -912D (zheng4) -912E (mao4) -912F (shan4) -9130 (lin2) -9131 (po2) -9132 (dan1) -9133 (meng2) -9134 (ye4) -9135 (cao1) -9136 (kuai4) -9137 (feng1) -9138 (meng2) -9139 (zou1) -913A (kuang4) -913B (lian3) -913C (zan4) -913D (chan2) -913E (you1) -913F (qi2) -9140 (yan1) -9141 (chan2) -9142 (cuo2) -9143 (ling2) -9144 (huan1) -9145 (xi1) -9146 (feng1) -9147 (zan4) -9148 (li4) -9149 (you3) -914A (ding3,ding1) -914B (qiu2) -914C (zhuo2) -914D (pei4) -914E (zhou4) -914F (yi2,yi4,yi3) -9150 (gan1) -9151 (yu3) -9152 (jiu3) -9153 (yan3) -9154 (zui4) -9155 (mao2) -9156 (dan1,zhen4) -9157 (xu4) -9158 (tou2) -9159 (zhen1) -915A (fen1) -915B (none0) -915C (none0) -915D (yun4) -915E (tai4) -915F (tian1) -9160 (qia3) -9161 (tuo2) -9162 (zuo4,cu4) -9163 (han1) -9164 (gu1) -9165 (su1) -9166 (fa1,po1) -9167 (chou2) -9168 (dai4) -9169 (ming3,ming2) -916A (lao4,luo4) -916B (chuo4) -916C (chou2) -916D (you4) -916E (tong2) -916F (zhi3) -9170 (xian1) -9171 (jiang4) -9172 (cheng2) -9173 (yin4) -9174 (tu2) -9175 (jiao4,xiao4) -9176 (mei2) -9177 (ku4) -9178 (suan1) -9179 (lei4) -917A (pu2) -917B (zui4) -917C (hai3) -917D (yan4) -917E (shi1,shai1) -917F (niang4,nian4,niang2) -9180 (wei2) -9181 (lu4) -9182 (lan3) -9183 (yan1) -9184 (tao2) -9185 (pei1) -9186 (zhan3) -9187 (chun2) -9188 (tan2) -9189 (zui4) -918A (chuo4) -918B (cu4) -918C (kun1) -918D (ti2) -918E (xian2) -918F (du1) -9190 (hu2) -9191 (xu3) -9192 (xing3) -9193 (tan3) -9194 (qiu2) -9195 (chun2) -9196 (yun4) -9197 (fa1,po1) -9198 (ke1) -9199 (sou1) -919A (mi2) -919B (quan2) -919C (chou3) -919D (cuo2) -919E (yun4) -919F (yong4) -91A0 (ang4) -91A1 (zha4) -91A2 (hai3) -91A3 (tang2) -91A4 (jiang4) -91A5 (piao3) -91A6 (lao2) -91A7 (yu4) -91A8 (li2) -91A9 (zao2) -91AA (lao2) -91AB (yi1) -91AC (jiang4) -91AD (bu2) -91AE (jiao4) -91AF (xi1,xi4) -91B0 (tan2) -91B1 (fa1,po4) -91B2 (nong2) -91B3 (yi4) -91B4 (li3) -91B5 (ju4) -91B6 (yan4) -91B7 (yi4) -91B8 (niang4) -91B9 (ru2) -91BA (xun1) -91BB (chou2) -91BC (yan4) -91BD (ling2) -91BE (mi2) -91BF (mi2) -91C0 (niang4) -91C1 (xin4) -91C2 (jiao4) -91C3 (shi1) -91C4 (mi2) -91C5 (yan4) -91C6 (bian4) -91C7 (cai3,cai4) -91C8 (shi4) -91C9 (you4) -91CA (shi4) -91CB (shi4) -91CC (li3,li5) -91CD (zhong4,chong2) -91CE (ye3) -91CF (liang4,liang2,liang5) -91D0 (li2,xi1) -91D1 (jin1) -91D2 (jin1) -91D3 (ga2) -91D4 (yi3) -91D5 (liao3,liao4) -91D6 (dao1) -91D7 (zhao1) -91D8 (ding1,ding4) -91D9 (li4) -91DA (qiu2) -91DB (he2) -91DC (fu3) -91DD (zhen1) -91DE (zhi2) -91DF (ba1) -91E0 (luan4) -91E1 (fu3) -91E2 (nai3) -91E3 (diao4) -91E4 (shan4,shan1) -91E5 (qiao3) -91E6 (kou4) -91E7 (chuan4) -91E8 (zi3) -91E9 (fan2) -91EA (yu2) -91EB (hua2) -91EC (han4) -91ED (gong1,gang1) -91EE (qi2) -91EF (mang2) -91F0 (jian4) -91F1 (di4) -91F2 (si4) -91F3 (xi4) -91F4 (yi4) -91F5 (chai1) -91F6 (ta1,tuo2) -91F7 (tu3) -91F8 (xi4) -91F9 (nu:3) -91FA (qian1) -91FB (none0) -91FC (jian4) -91FD (pi1) -91FE (ye2) -91FF (yin2) -9200 (ba3,pa2) -9201 (fang1) -9202 (chen2) -9203 (jian1) -9204 (tou3) -9205 (yue4) -9206 (yan2) -9207 (fu1) -9208 (bu4) -9209 (na4) -920A (xin1) -920B (e2) -920C (jue2) -920D (dun4) -920E (gou1) -920F (yin3) -9210 (qian2) -9211 (ban3) -9212 (ji2) -9213 (ren2) -9214 (chao1) -9215 (niu3) -9216 (fen1) -9217 (yun3) -9218 (yi2) -9219 (qin2) -921A (pi2) -921B (guo1) -921C (hong2) -921D (yin2) -921E (jun1) -921F (shi1) -9220 (yi4) -9221 (zhong1) -9222 (nie1) -9223 (gai4) -9224 (ri4) -9225 (huo2,huo3) -9226 (tai4) -9227 (kang4) -9228 (none0) -9229 (lu2) -922A (none0) -922B (none0) -922C (duo2) -922D (zi1) -922E (ni2) -922F (tu2) -9230 (shi4) -9231 (min2) -9232 (gu1) -9233 (ke1) -9234 (ling2) -9235 (bing3) -9236 (yi2) -9237 (gu1,gu3) -9238 (ba2) -9239 (pi1,pi2) -923A (yu4) -923B (si4) -923C (zuo2) -923D (bu4,bu1) -923E (you2) -923F (dian4,tian2) -9240 (jia3) -9241 (zhen1) -9242 (shi3) -9243 (shi4) -9244 (tie3) -9245 (ju4) -9246 (zhan1) -9247 (ta1,tuo2) -9248 (she2,tuo2,ta1) -9249 (xuan4) -924A (zhao1) -924B (bao4) -924C (he2) -924D (bi4) -924E (sheng1) -924F (chu2) -9250 (shi2) -9251 (bo2) -9252 (zhu4) -9253 (chi4) -9254 (za1) -9255 (po3) -9256 (tong2) -9257 (qian2) -9258 (fu2) -9259 (zhai3) -925A (liu3,mao3) -925B (qian1,yan2) -925C (fu2) -925D (li4) -925E (yue4) -925F (pi1) -9260 (yang1) -9261 (ban4) -9262 (bo1) -9263 (jie2) -9264 (gou1) -9265 (shu4) -9266 (zheng1) -9267 (mu3) -9268 (ni2) -9269 (xi3) -926A (di4) -926B (jia1) -926C (mu4) -926D (tan3) -926E (shen1) -926F (yi3) -9270 (si1) -9271 (kuang4) -9272 (ka1) -9273 (bei3) -9274 (jian4) -9275 (tong2) -9276 (xing2) -9277 (hong2) -9278 (jiao3,jia3) -9279 (chi3) -927A (er3) -927B (ge4) -927C (bing3) -927D (shi4) -927E (mou2) -927F (jia2,ha1) -9280 (yin2) -9281 (jun1) -9282 (zhou1) -9283 (chong4) -9284 (shang4) -9285 (tong2) -9286 (mo4) -9287 (lei4) -9288 (ji1) -9289 (yu4) -928A (xu4) -928B (ren2) -928C (cun4) -928D (zhi4) -928E (qiong2) -928F (shan4) -9290 (chi4) -9291 (xian3,xi3) -9292 (xing2) -9293 (quan2) -9294 (pi1) -9295 (yi2) -9296 (zhu1) -9297 (hou2) -9298 (ming2) -9299 (kua3) -929A (yao2,diao4,tiao2) -929B (xian1) -929C (xian2) -929D (xiu1) -929E (jun1) -929F (cha1) -92A0 (lao3) -92A1 (ji2) -92A2 (yong3) -92A3 (ru2) -92A4 (mi3) -92A5 (yi1) -92A6 (yin1) -92A7 (guang1) -92A8 (an1,an3) -92A9 (diu1) -92AA (you3) -92AB (se4) -92AC (kao4) -92AD (qian2) -92AE (luan2) -92AF (none0) -92B0 (ai1) -92B1 (diao4) -92B2 (han4) -92B3 (rui4) -92B4 (shi4) -92B5 (keng1) -92B6 (qiu2) -92B7 (xiao1) -92B8 (zhe2) -92B9 (xiu4) -92BA (zang4) -92BB (ti4,ti1) -92BC (cuo4) -92BD (gua1) -92BE (gong3) -92BF (zhong1) -92C0 (dou4) -92C1 (lu:3) -92C2 (mei2) -92C3 (lang2) -92C4 (wan3) -92C5 (xin1) -92C6 (yun2) -92C7 (bei4) -92C8 (wu4) -92C9 (su4) -92CA (yu4) -92CB (chan2) -92CC (ting3,ding4) -92CD (bo2) -92CE (han4) -92CF (jia2) -92D0 (hong2) -92D1 (cuan1) -92D2 (feng1) -92D3 (chan1) -92D4 (wan3) -92D5 (zhi4) -92D6 (si1) -92D7 (xuan1) -92D8 (wu2) -92D9 (wu2) -92DA (tiao2) -92DB (gong3) -92DC (zhuo2) -92DD (lu:e4) -92DE (xing2) -92DF (qin3) -92E0 (shen4) -92E1 (han2) -92E2 (none0) -92E3 (ye2) -92E4 (chu2) -92E5 (zeng4) -92E6 (ju2,ju1) -92E7 (xian4) -92E8 (e2) -92E9 (mang2) -92EA (pu1,pu4) -92EB (li2) -92EC (shi4) -92ED (rui4) -92EE (cheng2) -92EF (gao4) -92F0 (li3) -92F1 (te4) -92F2 (none0) -92F3 (zhu4) -92F4 (none0) -92F5 (tu1) -92F6 (liu3) -92F7 (zui4) -92F8 (ju4,ju1) -92F9 (chang3) -92FA (yuan1) -92FB (jian4) -92FC (gang1,gang4) -92FD (diao4) -92FE (tao2) -92FF (chang2) -9300 (lun2) -9301 (guo3) -9302 (ling2) -9303 (bei1) -9304 (lu4) -9305 (li2) -9306 (qing1,qiang1) -9307 (pei2) -9308 (juan3) -9309 (min2) -930A (zui4) -930B (peng2) -930C (an4) -930D (pi2) -930E (xian4) -930F (ya4) -9310 (zhui1) -9311 (lei4) -9312 (a1,e1) -9313 (kong1) -9314 (ta4) -9315 (kun1) -9316 (du3) -9317 (wei4) -9318 (chui2) -9319 (zi1) -931A (zheng1,zheng4) -931B (ben1) -931C (nie4) -931D (cong2) -931E (chun2) -931F (tan2) -9320 (ding4) -9321 (qi2) -9322 (qian2) -9323 (zhuo2) -9324 (qi2) -9325 (yu4) -9326 (jin3) -9327 (guan3) -9328 (mao2) -9329 (chang1) -932A (dian3) -932B (xi2,xi1) -932C (lian4) -932D (tao2) -932E (gu4) -932F (cuo4,cu4) -9330 (shu4) -9331 (zhen1) -9332 (lu4) -9333 (meng3) -9334 (lu4) -9335 (hua1) -9336 (biao3) -9337 (ga2) -9338 (lai2) -9339 (ken3) -933A (zhui4) -933B (none0) -933C (nai4) -933D (wan3) -933E (zan4) -933F (none0) -9340 (de2) -9341 (xian1) -9342 (none0) -9343 (huo1) -9344 (liang4) -9345 (none0) -9346 (men2) -9347 (kai3) -9348 (ying1) -9349 (di1) -934A (lian4) -934B (guo1) -934C (xian3) -934D (du4) -934E (tu2) -934F (wei2) -9350 (cong1) -9351 (fu4) -9352 (rou2) -9353 (ji2) -9354 (e4) -9355 (rou3) -9356 (chen3) -9357 (ti2) -9358 (zha2) -9359 (hong4) -935A (yang2) -935B (duan4) -935C (xia1) -935D (yu2) -935E (keng1) -935F (xing1) -9360 (huang2) -9361 (wei3) -9362 (fu4) -9363 (zhao1) -9364 (cha2,cha1) -9365 (qie4) -9366 (she2) -9367 (hong1) -9368 (kui2) -9369 (nuo4) -936A (mou2) -936B (qiao1) -936C (qiao1) -936D (hou2) -936E (zhen1) -936F (huo1) -9370 (huan2) -9371 (ye4) -9372 (min2) -9373 (jian4) -9374 (duan3) -9375 (jian4) -9376 (si1) -9377 (kui1) -9378 (hu2) -9379 (xuan1) -937A (zang1,zhe3) -937B (jie2) -937C (zhen1) -937D (bian1) -937E (zhong1) -937F (zi1) -9380 (xiu1) -9381 (ye2) -9382 (mei3) -9383 (pai4) -9384 (ai1) -9385 (jie4) -9386 (none0) -9387 (mei2) -9388 (cha1) -9389 (ta4) -938A (bang4) -938B (xia2) -938C (lian2) -938D (suo3) -938E (xi4) -938F (liu2) -9390 (zu2) -9391 (ye4) -9392 (nou4) -9393 (weng1) -9394 (rong2) -9395 (tang2) -9396 (suo3) -9397 (qiang1) -9398 (ge2) -9399 (shuo4) -939A (chui2) -939B (bo2) -939C (pan2) -939D (ta3) -939E (bi4) -939F (sang3) -93A0 (gang1) -93A1 (zi1) -93A2 (wu1) -93A3 (ying4,ying2) -93A4 (huang3) -93A5 (tiao2) -93A6 (liu2,liu4) -93A7 (kai3) -93A8 (sun3) -93A9 (sha1) -93AA (sou1) -93AB (wan4) -93AC (hao4,gao3) -93AD (zhen4) -93AE (zhen4) -93AF (luo3) -93B0 (yi4) -93B1 (yuan2) -93B2 (tang3) -93B3 (nie4) -93B4 (xi2) -93B5 (jia1) -93B6 (ge1) -93B7 (ma3) -93B8 (juan1) -93B9 (rong2) -93BA (none0) -93BB (suo3) -93BC (none0) -93BD (none0) -93BE (none0) -93BF (na2) -93C0 (lu3) -93C1 (suo3) -93C2 (kou1) -93C3 (zu2,cu4) -93C4 (tuan2) -93C5 (xiu1) -93C6 (guan4) -93C7 (xuan4) -93C8 (lian4) -93C9 (shou4) -93CA (ao4) -93CB (man3) -93CC (mo4) -93CD (luo2) -93CE (bi4) -93CF (wei4) -93D0 (liu2) -93D1 (di2,di1) -93D2 (qiao1) -93D3 (huo1) -93D4 (yin2) -93D5 (lu4) -93D6 (ao2) -93D7 (keng1) -93D8 (qiang1) -93D9 (cui1) -93DA (qi4) -93DB (chang2) -93DC (tang1,tang2) -93DD (man4) -93DE (yong1) -93DF (chan3) -93E0 (feng1) -93E1 (jing4) -93E2 (biao1) -93E3 (shu4) -93E4 (lou4) -93E5 (xiu4) -93E6 (cong1) -93E7 (long2) -93E8 (zan4) -93E9 (jian4) -93EA (cao2) -93EB (li2) -93EC (xia4) -93ED (xi1) -93EE (kang1) -93EF (none0) -93F0 (beng4) -93F1 (none0) -93F2 (none0) -93F3 (zheng1) -93F4 (lu4) -93F5 (hua2) -93F6 (ji2) -93F7 (pu2) -93F8 (hui4) -93F9 (qiang1,qiang3) -93FA (po1) -93FB (lin2) -93FC (suo3) -93FD (xiu4) -93FE (san3) -93FF (cheng1) -9400 (kui4) -9401 (san3) -9402 (liu4,liu2) -9403 (nao2) -9404 (huang2) -9405 (pie3) -9406 (sui4) -9407 (fan2) -9408 (qiao2) -9409 (chuan1) -940A (yang2) -940B (tang4,tang1) -940C (xiang4) -940D (jue2) -940E (jiao1) -940F (zun1) -9410 (liao2,liao4) -9411 (jie2) -9412 (lao2) -9413 (dui4,dun1) -9414 (tan2,chan2,xin2) -9415 (zan1) -9416 (ji1) -9417 (jian3,jian4) -9418 (zhong1) -9419 (deng1,deng4) -941A (lou4,lue2) -941B (ying4) -941C (dui4) -941D (jue2) -941E (nou4) -941F (ti4) -9420 (pu3) -9421 (tie3) -9422 (none0) -9423 (none0) -9424 (ding3) -9425 (shan4) -9426 (kai1) -9427 (jian3,jian4) -9428 (fei4) -9429 (sui4) -942A (lu3) -942B (juan1) -942C (hui4) -942D (yu4) -942E (lian2) -942F (zhuo2) -9430 (qiao1) -9431 (qian1) -9432 (zhuo2) -9433 (lei2) -9434 (bi4) -9435 (tie3) -9436 (huan2) -9437 (ye4) -9438 (duo2) -9439 (guo3) -943A (dang1,cheng1) -943B (ju4) -943C (fen2) -943D (da2) -943E (bei4) -943F (yi4) -9440 (ai4) -9441 (dang1,zheng1) -9442 (xun4) -9443 (diao4,yao2) -9444 (zhu4) -9445 (heng2) -9446 (zhui4) -9447 (ji1) -9448 (nie1) -9449 (ta4) -944A (huo4) -944B (qing4) -944C (bin1) -944D (ying1) -944E (kui4) -944F (ning2) -9450 (xu1) -9451 (jian4) -9452 (jian4) -9453 (qiang1) -9454 (cha3) -9455 (zhi4) -9456 (mie4) -9457 (li2) -9458 (lei2) -9459 (ji1) -945A (zuan4,zuan1) -945B (kuang4) -945C (shang3) -945D (peng2) -945E (la4) -945F (du2) -9460 (shuo4) -9461 (chuo4) -9462 (lu:4) -9463 (biao1) -9464 (bao4) -9465 (lu3) -9466 (none0) -9467 (none0) -9468 (long2) -9469 (e4) -946A (lu2) -946B (xin1) -946C (jian4) -946D (lan4,lan2) -946E (bo2) -946F (jian1) -9470 (yao4,yue4) -9471 (chan2) -9472 (xiang1) -9473 (jian4) -9474 (xi4) -9475 (guan4) -9476 (cang2) -9477 (nie4) -9478 (lei3) -9479 (cuan1) -947A (qu2) -947B (pan4) -947C (luo2) -947D (zuan4,zuan1,zuan3) -947E (luan2) -947F (zao2,zuo4) -9480 (nie4) -9481 (jue2) -9482 (tang3) -9483 (shu3) -9484 (lan2) -9485 (jin1) -9486 (ga2) -9487 (yi3) -9488 (zhen1) -9489 (ding1,ding4) -948A (zhao1) -948B (po1) -948C (liao3,liao4) -948D (tu3) -948E (qian1) -948F (chuan4) -9490 (shan4,shan1) -9491 (sa4) -9492 (fan2) -9493 (diao4) -9494 (men2) -9495 (nu:3) -9496 (yang2) -9497 (chai1) -9498 (xing2) -9499 (gai4) -949A (bu4) -949B (tai4) -949C (ju4) -949D (dun4) -949E (chao1) -949F (zhong1) -94A0 (na4) -94A1 (bei4) -94A2 (gang1,gang4) -94A3 (ban3) -94A4 (qian2) -94A5 (yao4,yue4) -94A6 (qin1) -94A7 (jun1) -94A8 (wu1) -94A9 (gou1) -94AA (kang4) -94AB (fang1) -94AC (huo3) -94AD (tou3) -94AE (niu3) -94AF (ba3,pa2) -94B0 (yu4) -94B1 (qian2) -94B2 (zheng1,zheng4) -94B3 (qian2) -94B4 (gu3) -94B5 (bo1) -94B6 (ke1) -94B7 (po3) -94B8 (bu1) -94B9 (bo2) -94BA (yue4) -94BB (zuan4,zuan1) -94BC (mu4) -94BD (tan3) -94BE (jia3) -94BF (dian4,tian2) -94C0 (you2) -94C1 (tie3) -94C2 (bo2) -94C3 (ling2) -94C4 (shuo4) -94C5 (qian1,yan2) -94C6 (mao3) -94C7 (bao4) -94C8 (shi4) -94C9 (xuan4) -94CA (tuo2,she2,ta1) -94CB (bi4) -94CC (ni2) -94CD (pi2,pi1) -94CE (duo2) -94CF (xing2) -94D0 (kao4) -94D1 (lao3) -94D2 (er3) -94D3 (mang2) -94D4 (ya4) -94D5 (you3) -94D6 (cheng2) -94D7 (jia2) -94D8 (ye2) -94D9 (nao2) -94DA (zhi4) -94DB (dang1,cheng1) -94DC (tong2) -94DD (lu:3) -94DE (diao4) -94DF (yin1) -94E0 (kai3) -94E1 (zha2) -94E2 (zhu1) -94E3 (xian3,xi3) -94E4 (ting3,ding4) -94E5 (diu1) -94E6 (xian1) -94E7 (hua2) -94E8 (quan2) -94E9 (sha1) -94EA (ha1) -94EB (yao2,diao4,tiao2) -94EC (ge4) -94ED (ming2) -94EE (zheng1,zheng4) -94EF (se4) -94F0 (jiao3,jia3) -94F1 (yi1) -94F2 (chan3) -94F3 (chong4) -94F4 (tang1) -94F5 (an3) -94F6 (yin2) -94F7 (ru2) -94F8 (zhu4) -94F9 (lao2) -94FA (pu1,pu4) -94FB (wu2) -94FC (lai2) -94FD (te4) -94FE (lian4) -94FF (keng1) -9500 (xiao1) -9501 (suo3) -9502 (li3) -9503 (zeng4) -9504 (chu2) -9505 (guo1) -9506 (gao4) -9507 (e2) -9508 (xiu4) -9509 (cuo4) -950A (lu:e4) -950B (feng1) -950C (xin1) -950D (liu3) -950E (kai1) -950F (jian3,jian4) -9510 (rui4) -9511 (ti1) -9512 (lang2) -9513 (qin3) -9514 (ju1,ju2) -9515 (a1) -9516 (qing1,qiang1) -9517 (zhe3,zang1) -9518 (nuo4) -9519 (cuo4) -951A (mao2) -951B (ben1) -951C (qi2) -951D (de2) -951E (ke4) -951F (kun1) -9520 (chang1) -9521 (xi1) -9522 (gu4) -9523 (luo2) -9524 (chui2) -9525 (zhui1) -9526 (jin3) -9527 (zhi4) -9528 (xian1) -9529 (juan3) -952A (huo4,huo1) -952B (pei2) -952C (tan2) -952D (ding4) -952E (jian4) -952F (ju4,ju1) -9530 (meng3) -9531 (zi1) -9532 (qie4) -9533 (ying1) -9534 (kai3) -9535 (qiang1) -9536 (si1) -9537 (e4) -9538 (cha1) -9539 (qiao1) -953A (zhong1) -953B (duan4) -953C (sou1) -953D (huang2) -953E (huan2) -953F (ai1) -9540 (du4) -9541 (mei3) -9542 (lou4) -9543 (zi1) -9544 (fei4) -9545 (mei2) -9546 (mo4) -9547 (zhen4) -9548 (bo2) -9549 (ge2) -954A (nie4) -954B (tang3) -954C (juan1) -954D (nie4) -954E (na2) -954F (liu2,liu4) -9550 (hao4,gao3) -9551 (bang4) -9552 (yi4) -9553 (jia1) -9554 (bin1) -9555 (rong2) -9556 (biao1) -9557 (tang1,tang2) -9558 (man4) -9559 (luo2) -955A (beng4) -955B (yong1) -955C (jing4) -955D (di2,di1) -955E (zu2) -955F (xuan4) -9560 (liu2) -9561 (chan2,xin2,tan2) -9562 (jue2) -9563 (liao4) -9564 (pu2) -9565 (lu3) -9566 (dun1,dui4) -9567 (lan2) -9568 (pu3) -9569 (cuan1) -956A (qiang1,qiang3) -956B (deng4) -956C (huo4) -956D (lei2) -956E (huan2) -956F (zhuo2) -9570 (lian2) -9571 (yi4) -9572 (cha3) -9573 (biao1) -9574 (la4) -9575 (chan2) -9576 (xiang1) -9577 (chang2,zhang3) -9578 (chang2,zhang3) -9579 (jiu3) -957A (ao3) -957B (die2) -957C (qu1) -957D (liao2) -957E (mi2) -957F (zhang3,chang2) -9580 (men2) -9581 (ma4) -9582 (shuan1) -9583 (shan3) -9584 (huo2) -9585 (men2) -9586 (yan4,yan2) -9587 (bi4) -9588 (han4) -9589 (bi4) -958A (none0) -958B (kai1) -958C (kang4,kang1) -958D (beng1) -958E (hong2) -958F (run4) -9590 (san4) -9591 (xian2) -9592 (xian2) -9593 (jian1,jian4) -9594 (min3) -9595 (xia1) -9596 (min3) -9597 (dou4) -9598 (zha2) -9599 (nao4) -959A (none0) -959B (peng1) -959C (ke3) -959D (ling2) -959E (bian4) -959F (bi4) -95A0 (run4) -95A1 (he2) -95A2 (guan1) -95A3 (ge2) -95A4 (he2,ge2) -95A5 (fa2) -95A6 (chu4) -95A7 (hong4,hong1,hong3) -95A8 (gui1) -95A9 (min3) -95AA (none0) -95AB (kun3) -95AC (lang3,lang2,lang4) -95AD (lu:2) -95AE (ting2) -95AF (sha4) -95B0 (yan2) -95B1 (yue4) -95B2 (yue4) -95B3 (chan3) -95B4 (qu4) -95B5 (lin4) -95B6 (chang1) -95B7 (shai4) -95B8 (kun3) -95B9 (yan1) -95BA (min2,wen2) -95BB (yan2) -95BC (e4,yan1) -95BD (hun1) -95BE (yu4) -95BF (wen2) -95C0 (xiang4) -95C1 (none0) -95C2 (xiang4) -95C3 (qu4) -95C4 (yao3) -95C5 (wen2) -95C6 (ban3) -95C7 (an4) -95C8 (wei2) -95C9 (yin1) -95CA (kuo4) -95CB (que4) -95CC (lan2) -95CD (du1,she2) -95CE (none0) -95CF (none0) -95D0 (tian2) -95D1 (nie4) -95D2 (da2,ta4) -95D3 (kai3) -95D4 (he2) -95D5 (que4,que1) -95D6 (chuang3,chuang4) -95D7 (guan1) -95D8 (dou4,dou3) -95D9 (qi3) -95DA (kui1) -95DB (tang2) -95DC (guan1) -95DD (piao2) -95DE (kan4,han3,kan3) -95DF (xi1) -95E0 (hui4) -95E1 (chan3) -95E2 (pi4,bi4,pi1) -95E3 (dang4) -95E4 (huan2) -95E5 (ta4) -95E6 (wen2) -95E7 (none0) -95E8 (men2) -95E9 (shuan1) -95EA (shan3) -95EB (yan2) -95EC (han4) -95ED (bi4) -95EE (wen4) -95EF (chuang3) -95F0 (run4) -95F1 (wei2) -95F2 (xian2) -95F3 (hong2) -95F4 (jian1,jian4) -95F5 (min3) -95F6 (kang1,kang4) -95F7 (men4,men1) -95F8 (zha2) -95F9 (nao4) -95FA (gui1) -95FB (wen2) -95FC (ta4) -95FD (min3) -95FE (lu:2) -95FF (kai3) -9600 (fa2) -9601 (ge2) -9602 (he2) -9603 (kun3) -9604 (jiu1) -9605 (yue4) -9606 (lang2,lang4) -9607 (du1,she2) -9608 (yu4) -9609 (yan1) -960A (chang1) -960B (xi4) -960C (wen2) -960D (hun1) -960E (yan2) -960F (yan1,e4) -9610 (chan3) -9611 (lan2) -9612 (qu4) -9613 (hui4) -9614 (kuo4) -9615 (que4) -9616 (he2) -9617 (tian2) -9618 (da2,ta4) -9619 (que1,que4) -961A (kan4,han3,kan3) -961B (huan2) -961C (fu4) -961D (fu4,yi4) -961E (le4) -961F (dui4) -9620 (xin4,shen1) -9621 (qian1) -9622 (wu4) -9623 (yi4) -9624 (tuo2) -9625 (yin1) -9626 (yang2) -9627 (dou3) -9628 (e4) -9629 (sheng1) -962A (ban3) -962B (pei2) -962C (keng1) -962D (yun3) -962E (ruan3) -962F (zhi3) -9630 (pi2) -9631 (jing3) -9632 (fang2) -9633 (yang2) -9634 (yin1) -9635 (zhen4) -9636 (jie1) -9637 (cheng1) -9638 (e4) -9639 (qu1) -963A (di3) -963B (zu3) -963C (zuo4) -963D (dian4,yan2) -963E (ling3) -963F (a1,e1,a5,a2,a4) -9640 (tuo2) -9641 (tuo2) -9642 (po1,bei1,pi2) -9643 (bing3) -9644 (fu4) -9645 (ji4) -9646 (lu4,liu4) -9647 (long3) -9648 (chen2) -9649 (xing2) -964A (duo4) -964B (lou4) -964C (mo4) -964D (jiang4,xiang2) -964E (shu1) -964F (duo4) -9650 (xian4) -9651 (er2) -9652 (gui3) -9653 (wu1) -9654 (gai1) -9655 (shan3) -9656 (jun4) -9657 (qiao4) -9658 (xing2) -9659 (chun2) -965A (fu4) -965B (bi4) -965C (shan3) -965D (shan3,xia2) -965E (sheng1) -965F (zhi4) -9660 (pu1) -9661 (dou3) -9662 (yuan4) -9663 (zhen4) -9664 (chu2) -9665 (xian4) -9666 (zhi4) -9667 (nie4) -9668 (yun3) -9669 (xian3) -966A (pei2) -966B (pei2) -966C (zou1) -966D (yi1,yi3) -966E (dui4) -966F (lun2) -9670 (yin1) -9671 (ju2) -9672 (chui2) -9673 (chen2) -9674 (pi2) -9675 (ling2) -9676 (tao2,yao2) -9677 (xian4) -9678 (lu4,liu4) -9679 (none0) -967A (xian3) -967B (yin1) -967C (zhu3) -967D (yang2) -967E (reng2) -967F (shan3) -9680 (chong2) -9681 (yan4) -9682 (yin1) -9683 (yu2) -9684 (ti2) -9685 (yu2) -9686 (long2,long1) -9687 (wei1) -9688 (wei1) -9689 (nie4) -968A (dui4) -968B (sui2) -968C (an3) -968D (huang2) -968E (jie1) -968F (sui2) -9690 (yin3) -9691 (gai1) -9692 (yan3) -9693 (hui1) -9694 (ge2) -9695 (yun3) -9696 (wu4) -9697 (wei3,kui2) -9698 (ai4) -9699 (xi4) -969A (tang2) -969B (ji4) -969C (zhang4) -969D (dao3) -969E (ao2) -969F (xi4) -96A0 (yin3) -96A1 (sa4) -96A2 (rao4) -96A3 (lin2) -96A4 (tui2) -96A5 (deng4) -96A6 (pi2) -96A7 (sui4) -96A8 (sui2) -96A9 (yu4) -96AA (xian3) -96AB (fen1) -96AC (ni3) -96AD (er2) -96AE (ji1) -96AF (dao3) -96B0 (xi2) -96B1 (yin3,yin4) -96B2 (zhi4) -96B3 (hui1) -96B4 (long3) -96B5 (xi1) -96B6 (li4) -96B7 (li4) -96B8 (li4) -96B9 (zhui1,cui1) -96BA (he4) -96BB (zhi1) -96BC (sun3,zhun3) -96BD (juan4,jun4) -96BE (nan2,nan4) -96BF (yi4) -96C0 (que4,qiao1,qiao3) -96C1 (yan4) -96C2 (qin2) -96C3 (ya3) -96C4 (xiong2) -96C5 (ya3,ya1) -96C6 (ji2) -96C7 (gu4) -96C8 (huan2) -96C9 (zhi4) -96CA (gou4) -96CB (jun4,juan4) -96CC (ci2,ci1) -96CD (yong1) -96CE (ju1) -96CF (chu2) -96D0 (hu1) -96D1 (za2) -96D2 (luo4) -96D3 (yu2) -96D4 (chou2) -96D5 (diao1) -96D6 (sui1) -96D7 (han4) -96D8 (huo4) -96D9 (shuang1) -96DA (guan4) -96DB (chu2) -96DC (za2) -96DD (yong1) -96DE (ji1) -96DF (sui2) -96E0 (chou2) -96E1 (liu4) -96E2 (li2) -96E3 (nan2,nan4) -96E4 (xue2) -96E5 (za2) -96E6 (ji2) -96E7 (ji2) -96E8 (yu3,yu4) -96E9 (yu2) -96EA (xue3,xue4) -96EB (na3) -96EC (fou3) -96ED (se4) -96EE (mu4) -96EF (wen2) -96F0 (fen1) -96F1 (pang2) -96F2 (yun2) -96F3 (li4) -96F4 (li4) -96F5 (yang1) -96F6 (ling2) -96F7 (lei2) -96F8 (an2) -96F9 (bao2) -96FA (meng2) -96FB (dian4) -96FC (dang4) -96FD (hang2,yu2) -96FE (wu4) -96FF (zhao4) -9700 (xu1) -9701 (ji4) -9702 (mu4) -9703 (chen2) -9704 (xiao1) -9705 (zha2) -9706 (ting2) -9707 (zhen4) -9708 (pei4) -9709 (mei2) -970A (ling2) -970B (qi1) -970C (chou1) -970D (huo4) -970E (sha4) -970F (fei1) -9710 (weng1) -9711 (zhan1) -9712 (ying1) -9713 (ni2) -9714 (chou4) -9715 (tun2) -9716 (lin2) -9717 (none0) -9718 (dong4) -9719 (ying1,ji1) -971A (wu4) -971B (ling2) -971C (shuang1) -971D (ling2) -971E (xia2) -971F (hong2) -9720 (yin1) -9721 (mai4) -9722 (mo4) -9723 (yun3) -9724 (liu4) -9725 (meng4) -9726 (bin1) -9727 (wu4) -9728 (wei4) -9729 (kuo4) -972A (yin2) -972B (xi2) -972C (yi4) -972D (ai3) -972E (dan4) -972F (deng4) -9730 (xian4,san3) -9731 (yu4) -9732 (lu4,lou4) -9733 (long2,long1) -9734 (dai4) -9735 (ji2) -9736 (pang2) -9737 (yang2) -9738 (ba4) -9739 (pi1) -973A (wei2) -973B (none0) -973C (xi3) -973D (ji4) -973E (mai2) -973F (meng4) -9740 (meng2) -9741 (lei2) -9742 (li4) -9743 (huo4,sui3) -9744 (ai3) -9745 (fei4) -9746 (dai4) -9747 (long2) -9748 (ling2) -9749 (ai4) -974A (feng1) -974B (li4) -974C (bao3) -974D (none0) -974E (he4) -974F (he4) -9750 (bing4) -9751 (qing1) -9752 (qing1) -9753 (jing4,liang4) -9754 (qi2) -9755 (zhen1) -9756 (jing4) -9757 (cheng1) -9758 (qing4) -9759 (jing4) -975A (jing4,liang4) -975B (dian4) -975C (jing4) -975D (tian1) -975E (fei1) -975F (fei1) -9760 (kao4) -9761 (mi3,mi2) -9762 (mian4) -9763 (mian4) -9764 (pao4) -9765 (ye4) -9766 (tian3,mian3) -9767 (hui4) -9768 (ye4) -9769 (ge2,ji2,ji3) -976A (ding1) -976B (ren4) -976C (jian1) -976D (ren4) -976E (di2) -976F (du4) -9770 (wu4) -9771 (ren4) -9772 (qin2) -9773 (jin4) -9774 (xue1) -9775 (niu3) -9776 (ba3) -9777 (yin3) -9778 (sa3) -9779 (ren4) -977A (mo4) -977B (zu3) -977C (da2) -977D (ban4) -977E (yi4) -977F (yao4) -9780 (tao2) -9781 (bei4,tuo2) -9782 (jia2) -9783 (hong2) -9784 (pao2) -9785 (yang1,yang4,yang3) -9786 (mo4) -9787 (yin1) -9788 (jia2) -9789 (tao2) -978A (ji2) -978B (xie2) -978C (an1) -978D (an1) -978E (hen2) -978F (gong3) -9790 (gong3) -9791 (da2) -9792 (qiao2) -9793 (ting1) -9794 (man2,wan3) -9795 (ying4) -9796 (sui1) -9797 (tiao2) -9798 (qiao4,shao1) -9799 (xuan4) -979A (kong4) -979B (beng3) -979C (ta4) -979D (zhang3) -979E (bing3) -979F (kuo4) -97A0 (ju1,ju2) -97A1 (la5) -97A2 (xie4) -97A3 (rou2) -97A4 (bang1) -97A5 (yi4,eng1) -97A6 (qiu1) -97A7 (qiu1) -97A8 (he2) -97A9 (xiao4) -97AA (mu4) -97AB (ju1,ju2) -97AC (jian1) -97AD (bian1) -97AE (di1) -97AF (jian1) -97B0 (none0) -97B1 (tao1) -97B2 (gou1) -97B3 (ta4) -97B4 (bei4) -97B5 (xie2) -97B6 (pan2) -97B7 (ge2) -97B8 (bi4) -97B9 (kuo4,kui1) -97BA (tang1) -97BB (lou2) -97BC (gui4) -97BD (qiao2) -97BE (xue1) -97BF (ji1) -97C0 (jian1) -97C1 (jiang1) -97C2 (chan4) -97C3 (da2) -97C4 (huo4) -97C5 (xian3) -97C6 (qian1) -97C7 (du2) -97C8 (wa4) -97C9 (jian1) -97CA (lan2) -97CB (wei2) -97CC (ren4) -97CD (fu2) -97CE (mei4) -97CF (juan4) -97D0 (ge2) -97D1 (wei3) -97D2 (qiao4) -97D3 (han2) -97D4 (chang4) -97D5 (none0) -97D6 (rou2) -97D7 (xun4) -97D8 (she4) -97D9 (wei3) -97DA (ge2) -97DB (bei4) -97DC (tao1) -97DD (gou4) -97DE (yun4) -97DF (gao1) -97E0 (bi4) -97E1 (wei3) -97E2 (hui4) -97E3 (shu3) -97E4 (wa4) -97E5 (du2) -97E6 (wei2) -97E7 (ren4) -97E8 (fu2) -97E9 (han2) -97EA (wei3) -97EB (yun4) -97EC (tao1) -97ED (jiu3) -97EE (jiu3) -97EF (xian1) -97F0 (xie4) -97F1 (xian1) -97F2 (ji1) -97F3 (yin1) -97F4 (za2) -97F5 (yun4) -97F6 (shao2) -97F7 (luo4) -97F8 (peng2) -97F9 (huang2) -97FA (ying1) -97FB (yun4) -97FC (peng2) -97FD (yin1,an1) -97FE (yin1) -97FF (xiang3) -9800 (hu4) -9801 (ye4) -9802 (ding3) -9803 (qing3,qing1) -9804 (pan4) -9805 (xiang4) -9806 (shun4) -9807 (han1) -9808 (xu1) -9809 (yi2) -980A (xu1) -980B (gu4) -980C (song4) -980D (kui3) -980E (qi2) -980F (hang2) -9810 (yu4) -9811 (wan2) -9812 (ban1) -9813 (dun4,du2) -9814 (di2) -9815 (dan1) -9816 (pan4) -9817 (po3) -9818 (ling3) -9819 (cheng1) -981A (jing3,geng3) -981B (lei3) -981C (he2,han4) -981D (qiao1) -981E (e4) -981F (e2) -9820 (wei3) -9821 (jie2,xie2) -9822 (gua1) -9823 (shen3) -9824 (yi2) -9825 (yi2) -9826 (ke1,ke2) -9827 (dui1) -9828 (pian1) -9829 (ping1) -982A (lei4) -982B (fu3) -982C (jia2) -982D (tou2,tou5) -982E (hui4) -982F (kui2) -9830 (jia2) -9831 (le4) -9832 (ting3) -9833 (cheng1) -9834 (ying3) -9835 (jun1) -9836 (hu2) -9837 (han4) -9838 (jing3,geng3) -9839 (tui2) -983A (tui2) -983B (pin2) -983C (lai4) -983D (tui2) -983E (zi1) -983F (zi1) -9840 (chui2) -9841 (ding4) -9842 (lai4) -9843 (yan2) -9844 (han4) -9845 (qian1) -9846 (ke1) -9847 (cui4) -9848 (jiong3) -9849 (qin3) -984A (yi2) -984B (sai1) -984C (ti2) -984D (e2) -984E (e4) -984F (yan2) -9850 (hun2) -9851 (kan3) -9852 (yong2) -9853 (zhuan1) -9854 (yan2) -9855 (xian3) -9856 (xin4) -9857 (yi3) -9858 (yuan4) -9859 (sang3) -985A (dian1) -985B (dian1) -985C (jiang3) -985D (ku1) -985E (lei4) -985F (liao2) -9860 (piao4) -9861 (yi4) -9862 (man2,man1) -9863 (qi1) -9864 (yao2) -9865 (hao4) -9866 (qiao2) -9867 (gu4) -9868 (xun4) -9869 (qian1) -986A (hui1) -986B (zhan4,chan4) -986C (ru2) -986D (hong1) -986E (bin1) -986F (xian3) -9870 (pin2) -9871 (lu2) -9872 (lan3) -9873 (nie4) -9874 (quan2) -9875 (ye4) -9876 (ding3) -9877 (qing3) -9878 (han1) -9879 (xiang4) -987A (shun4) -987B (xu1) -987C (xu1) -987D (wan2) -987E (gu4) -987F (dun4,du2) -9880 (qi2) -9881 (ban1) -9882 (song4) -9883 (hang2) -9884 (yu4) -9885 (lu2) -9886 (ling3) -9887 (po1) -9888 (jing3,geng3) -9889 (jie2,xie2) -988A (jia2) -988B (ting3) -988C (he2,ge2) -988D (ying3) -988E (jiong3) -988F (ke1,ke2) -9890 (yi2) -9891 (pin2) -9892 (hui4) -9893 (tui2) -9894 (han4) -9895 (ying3) -9896 (ying3) -9897 (ke1) -9898 (ti2) -9899 (yong2) -989A (e4) -989B (zhuan1) -989C (yan2) -989D (e2) -989E (nie4) -989F (man1) -98A0 (dian1) -98A1 (sang3) -98A2 (hao4) -98A3 (lei4) -98A4 (zhan4,chan4) -98A5 (ru2) -98A6 (pin2) -98A7 (quan2) -98A8 (feng1) -98A9 (biao1) -98AA (none0) -98AB (fu2) -98AC (xia1) -98AD (zhan3) -98AE (biao1) -98AF (sa4) -98B0 (fa1) -98B1 (tai2) -98B2 (lie4) -98B3 (gua1) -98B4 (xuan4) -98B5 (shao4) -98B6 (ju4) -98B7 (biao1) -98B8 (si1) -98B9 (wei3) -98BA (yang2) -98BB (yao2) -98BC (sou1) -98BD (kai3) -98BE (sao1) -98BF (fan2) -98C0 (liu2) -98C1 (xi2) -98C2 (liao2) -98C3 (piao1) -98C4 (piao1) -98C5 (liu2) -98C6 (biao1) -98C7 (biao1) -98C8 (biao1) -98C9 (liao2) -98CA (none0) -98CB (se4) -98CC (feng1) -98CD (biao1) -98CE (feng1) -98CF (yang2) -98D0 (zhan3) -98D1 (biao1) -98D2 (sa4) -98D3 (ju4) -98D4 (si1) -98D5 (sou1) -98D6 (yao2) -98D7 (liu2) -98D8 (piao1) -98D9 (biao1) -98DA (biao1) -98DB (fei1) -98DC (fan1) -98DD (fei1) -98DE (fei1) -98DF (shi2,si4,yi4) -98E0 (shi2,si4) -98E1 (can1) -98E2 (ji1) -98E3 (ding4) -98E4 (si4) -98E5 (tuo2) -98E6 (jian1) -98E7 (sun1) -98E8 (xiang3) -98E9 (tun5,tun2) -98EA (ren4) -98EB (yu4) -98EC (juan4) -98ED (chi4) -98EE (yin3,yin4) -98EF (fan4) -98F0 (fan4) -98F1 (sun1) -98F2 (yin3,yin4) -98F3 (zhu4) -98F4 (yi2) -98F5 (zhai3) -98F6 (bi4) -98F7 (jie3) -98F8 (tao1) -98F9 (liu3) -98FA (ci2) -98FB (tie4) -98FC (si4) -98FD (bao3) -98FE (shi4) -98FF (duo4) -9900 (hai4) -9901 (ren4) -9902 (tian3) -9903 (jiao3,jia3) -9904 (jia2) -9905 (bing3) -9906 (yao2) -9907 (tong2) -9908 (ci2) -9909 (xiang3) -990A (yang3,yang4) -990B (yang3) -990C (er3) -990D (yan4) -990E (le5) -990F (yi1) -9910 (can1) -9911 (bo1) -9912 (nei3) -9913 (e4) -9914 (bu1) -9915 (jun4) -9916 (dou4) -9917 (su4) -9918 (yu2) -9919 (shi4) -991A (yao2) -991B (hun2) -991C (guo3) -991D (shi4) -991E (jian4) -991F (zhui4) -9920 (bing3) -9921 (xian4) -9922 (bu4) -9923 (ye4) -9924 (tan2) -9925 (fei3) -9926 (zhang1) -9927 (wei4) -9928 (guan3) -9929 (e4) -992A (nuan3) -992B (hun2) -992C (hu2,hu1,hu4) -992D (huang2) -992E (tie4) -992F (hui4) -9930 (jian1) -9931 (hou2) -9932 (he2) -9933 (xing2,tang2) -9934 (fen1) -9935 (wei4) -9936 (gu3) -9937 (cha1) -9938 (song4) -9939 (tang2,xing2) -993A (bo2) -993B (gao1) -993C (xi4) -993D (kui4) -993E (liu4,liu2) -993F (sou1) -9940 (tao2) -9941 (ye4) -9942 (yun2) -9943 (mo2) -9944 (tang2) -9945 (man2) -9946 (bi4) -9947 (yu4) -9948 (xiu1) -9949 (jin3) -994A (san3) -994B (kui4) -994C (zhuan4) -994D (shan4) -994E (chi4) -994F (dan4) -9950 (yi4) -9951 (ji1) -9952 (rao2) -9953 (cheng1) -9954 (yong1) -9955 (tao1) -9956 (hui4) -9957 (xiang3) -9958 (zhan1) -9959 (fen1) -995A (hai4) -995B (meng2) -995C (yan4) -995D (mo2) -995E (chan2) -995F (xiang3) -9960 (luo2) -9961 (zuan4,zan4) -9962 (nang3,nang2) -9963 (shi2,si4) -9964 (ding4) -9965 (ji1) -9966 (tuo1) -9967 (xing2,tang2) -9968 (tun5,tun2) -9969 (xi4) -996A (ren4) -996B (yu4) -996C (chi4) -996D (fan4) -996E (yin3,yin4) -996F (jian4) -9970 (shi4) -9971 (bao3) -9972 (si4) -9973 (duo4) -9974 (yi2) -9975 (er3) -9976 (rao2) -9977 (xiang3) -9978 (he2) -9979 (le5) -997A (jiao3,jia3) -997B (xi1) -997C (bing3) -997D (bo1) -997E (dou4) -997F (e4) -9980 (yu2) -9981 (nei3) -9982 (jun4) -9983 (guo3) -9984 (hun2) -9985 (xian4) -9986 (guan3) -9987 (cha1) -9988 (kui4) -9989 (gu3) -998A (sou1) -998B (chan2) -998C (ye4) -998D (mo2) -998E (bo2) -998F (liu4,liu2) -9990 (xiu1) -9991 (jin3) -9992 (man2) -9993 (san3) -9994 (zhuan4) -9995 (nang3,nang2) -9996 (shou3) -9997 (kui2) -9998 (guo2) -9999 (xiang1) -999A (fen2) -999B (ba2) -999C (ni3) -999D (bi4) -999E (bo2) -999F (tu2) -99A0 (han1) -99A1 (fei1) -99A2 (jian1) -99A3 (yan3) -99A4 (ai3) -99A5 (fu4) -99A6 (xian1) -99A7 (wen1) -99A8 (xin1,xing1) -99A9 (fen2) -99AA (bin1) -99AB (xing1) -99AC (ma3) -99AD (yu4) -99AE (feng2,ping2) -99AF (han4) -99B0 (di4) -99B1 (tuo2,duo4) -99B2 (tuo1) -99B3 (chi2) -99B4 (xun4,xun2) -99B5 (zhu4) -99B6 (zhi1) -99B7 (pei4) -99B8 (xin4) -99B9 (ri4) -99BA (sa4) -99BB (yin3) -99BC (wen2) -99BD (zhi2) -99BE (dan4) -99BF (lu:2) -99C0 (you2) -99C1 (bo2) -99C2 (bao3) -99C3 (kuai4) -99C4 (tuo2,duo4) -99C5 (yi4) -99C6 (qu1) -99C7 (wen2) -99C8 (qu1) -99C9 (jiong1) -99CA (bo3) -99CB (zhao1) -99CC (yuan1) -99CD (peng1) -99CE (zhou4) -99CF (ju4) -99D0 (zhu4) -99D1 (nu2) -99D2 (ju1) -99D3 (pi1) -99D4 (zang3) -99D5 (jia4) -99D6 (ling2) -99D7 (zhen1) -99D8 (tai2) -99D9 (fu4) -99DA (yang3) -99DB (shi3) -99DC (bi4) -99DD (tuo2) -99DE (tuo2) -99DF (si4) -99E0 (liu2) -99E1 (ma4) -99E2 (pian2) -99E3 (tao2) -99E4 (zhi4) -99E5 (rong2) -99E6 (teng2) -99E7 (dong4) -99E8 (xun2) -99E9 (quan2) -99EA (shen1) -99EB (jiong1) -99EC (er3) -99ED (hai4,xie4) -99EE (bo2) -99EF (none0) -99F0 (yin1) -99F1 (luo4) -99F2 (none0) -99F3 (dan4) -99F4 (xie4) -99F5 (liu2) -99F6 (ju2) -99F7 (song3) -99F8 (qin1) -99F9 (mang2) -99FA (liang2) -99FB (han4) -99FC (tu2) -99FD (xuan4) -99FE (tui4) -99FF (jun4) -9A00 (e2) -9A01 (cheng3) -9A02 (xing1) -9A03 (ai2) -9A04 (lu4) -9A05 (zhui1) -9A06 (zhou1) -9A07 (she4) -9A08 (pian2) -9A09 (kun1) -9A0A (tao2) -9A0B (lai2) -9A0C (zong1) -9A0D (ke4) -9A0E (qi2,ji4) -9A0F (qi2) -9A10 (yan4) -9A11 (fei1) -9A12 (sao1) -9A13 (yan4) -9A14 (jie2,ge3) -9A15 (yao3) -9A16 (wu4) -9A17 (pian4) -9A18 (cong1) -9A19 (pian4) -9A1A (qian2) -9A1B (fei1) -9A1C (huang2) -9A1D (jian1) -9A1E (huo4) -9A1F (yu4) -9A20 (ti2) -9A21 (quan2) -9A22 (xia2) -9A23 (zong1) -9A24 (kui2) -9A25 (rou2) -9A26 (si1) -9A27 (gua1) -9A28 (tuo2,tan2) -9A29 (kui4) -9A2A (sou1) -9A2B (qian1) -9A2C (cheng2) -9A2D (zhi4) -9A2E (liu2) -9A2F (pang2) -9A30 (teng2) -9A31 (xi1) -9A32 (cao3) -9A33 (du2) -9A34 (yan4) -9A35 (yuan2) -9A36 (zou1) -9A37 (sao1) -9A38 (shan4) -9A39 (li2) -9A3A (zhi4) -9A3B (shuang3) -9A3C (lu4) -9A3D (xi2) -9A3E (luo2) -9A3F (zhang1) -9A40 (mo4) -9A41 (ao4) -9A42 (can1) -9A43 (piao4,biao1) -9A44 (cong1) -9A45 (qu1) -9A46 (bi4) -9A47 (zhi4) -9A48 (yu4) -9A49 (xu1) -9A4A (hua2) -9A4B (bo1) -9A4C (su4) -9A4D (xiao1) -9A4E (lin2) -9A4F (zhan4) -9A50 (dun1) -9A51 (liu2) -9A52 (tuo2) -9A53 (zeng1) -9A54 (tan2) -9A55 (jiao1) -9A56 (tie3) -9A57 (yan4) -9A58 (luo2) -9A59 (zhan1) -9A5A (jing1) -9A5B (yi4) -9A5C (ye4) -9A5D (tuo4) -9A5E (bin1) -9A5F (zou4,zhou4) -9A60 (yan4) -9A61 (peng2) -9A62 (lu:2) -9A63 (teng2) -9A64 (xiang1) -9A65 (ji4) -9A66 (shuang1) -9A67 (ju2) -9A68 (xi1) -9A69 (huan1) -9A6A (li2) -9A6B (biao1) -9A6C (ma3) -9A6D (yu4) -9A6E (tuo2,duo4) -9A6F (xun4) -9A70 (chi2) -9A71 (qu1) -9A72 (ri4) -9A73 (bo2) -9A74 (lu:2) -9A75 (zang3) -9A76 (shi3) -9A77 (si4) -9A78 (fu4) -9A79 (ju1) -9A7A (zou1) -9A7B (zhu4) -9A7C (tuo2) -9A7D (nu2) -9A7E (jia4) -9A7F (yi4) -9A80 (tai2,dai4) -9A81 (xiao1) -9A82 (ma4) -9A83 (yin1) -9A84 (jiao1) -9A85 (hua2) -9A86 (luo4) -9A87 (hai4) -9A88 (pian2) -9A89 (biao1) -9A8A (li2) -9A8B (cheng3) -9A8C (yan4) -9A8D (xing1) -9A8E (qin1) -9A8F (jun4) -9A90 (qi2) -9A91 (qi2) -9A92 (ke4) -9A93 (zhui1) -9A94 (zong1) -9A95 (su4) -9A96 (can1) -9A97 (pian4) -9A98 (zhi4) -9A99 (kui2) -9A9A (sao1) -9A9B (wu4) -9A9C (ao4) -9A9D (liu2) -9A9E (qian1) -9A9F (shan4) -9AA0 (piao4,biao1) -9AA1 (luo2) -9AA2 (cong1) -9AA3 (zhan4,chan3) -9AA4 (zhou4) -9AA5 (ji4) -9AA6 (shuang1) -9AA7 (xiang1) -9AA8 (gu2,gu3,gu1) -9AA9 (wei3) -9AAA (wei3) -9AAB (wei3) -9AAC (yu2) -9AAD (gan4) -9AAE (yi4) -9AAF (ang1) -9AB0 (tou2,shai3) -9AB1 (jie4,xie4) -9AB2 (bo2) -9AB3 (bi4) -9AB4 (ci1) -9AB5 (ti3) -9AB6 (di3) -9AB7 (ku1) -9AB8 (hai2) -9AB9 (qiao1) -9ABA (hou2) -9ABB (kua4) -9ABC (ge2) -9ABD (tui3) -9ABE (geng3) -9ABF (pian2) -9AC0 (bi4) -9AC1 (ke1) -9AC2 (qia4) -9AC3 (yu2) -9AC4 (sui3) -9AC5 (lou2) -9AC6 (bo2) -9AC7 (xiao1) -9AC8 (bang3) -9AC9 (bo1) -9ACA (cuo1) -9ACB (kuan1) -9ACC (bin4) -9ACD (mo2) -9ACE (liao2) -9ACF (lou2) -9AD0 (nao2) -9AD1 (du2) -9AD2 (zang1) -9AD3 (sui3) -9AD4 (ti3) -9AD5 (bin4) -9AD6 (kuan1) -9AD7 (lu2) -9AD8 (gao1) -9AD9 (gao1) -9ADA (qiao4) -9ADB (kao1) -9ADC (qiao1) -9ADD (lao4) -9ADE (zao4) -9ADF (biao1,shan1) -9AE0 (kun1) -9AE1 (kun1) -9AE2 (ti4) -9AE3 (fang3) -9AE4 (xiu1) -9AE5 (ran2) -9AE6 (mao2) -9AE7 (dan4) -9AE8 (kun1) -9AE9 (bin4) -9AEA (fa4) -9AEB (tiao2) -9AEC (pi1) -9AED (zi1) -9AEE (fa4,fa3) -9AEF (ran2,ran3) -9AF0 (ti4) -9AF1 (pao4) -9AF2 (pi4) -9AF3 (mao2) -9AF4 (fu2,fo2) -9AF5 (er2) -9AF6 (rong2) -9AF7 (qu1) -9AF8 (none0) -9AF9 (xiu1) -9AFA (gua4) -9AFB (ji4) -9AFC (peng2) -9AFD (zhua1) -9AFE (shao1) -9AFF (sha1) -9B00 (ti4) -9B01 (li4) -9B02 (bin4) -9B03 (zong1) -9B04 (ti4) -9B05 (peng2) -9B06 (song1) -9B07 (zheng1) -9B08 (quan2,qian2) -9B09 (zong1) -9B0A (shun4) -9B0B (jian1) -9B0C (duo3) -9B0D (hu2) -9B0E (la4) -9B0F (jiu1) -9B10 (qi2) -9B11 (lian2) -9B12 (zhen3) -9B13 (bin4) -9B14 (peng2) -9B15 (mo4) -9B16 (san1) -9B17 (man2) -9B18 (man2) -9B19 (seng1) -9B1A (xu1) -9B1B (lie4) -9B1C (qian1) -9B1D (qian1) -9B1E (nong2) -9B1F (huan2) -9B20 (kuai4) -9B21 (ning2) -9B22 (bin4) -9B23 (lie4) -9B24 (rang2) -9B25 (dou4,dou3) -9B26 (dou4,dou3) -9B27 (nao4) -9B28 (hong4) -9B29 (xi4) -9B2A (dou4,dou3) -9B2B (kan4) -9B2C (dou4) -9B2D (dou4,dou3) -9B2E (jiu1) -9B2F (chang4) -9B30 (yu4) -9B31 (yu4) -9B32 (li4,ge2) -9B33 (juan4) -9B34 (fu3) -9B35 (qian2) -9B36 (gui1) -9B37 (zong1) -9B38 (liu4) -9B39 (gui1) -9B3A (shang1) -9B3B (yu4) -9B3C (gui3) -9B3D (mei4) -9B3E (ji4) -9B3F (qi2) -9B40 (jie4) -9B41 (kui2) -9B42 (hun2) -9B43 (ba2) -9B44 (po4,tuo4,bo2) -9B45 (mei4) -9B46 (xu1) -9B47 (yan3) -9B48 (xiao1) -9B49 (liang3) -9B4A (yu4) -9B4B (tui2) -9B4C (qi1) -9B4D (wang3) -9B4E (liang3) -9B4F (wei4) -9B50 (jian1) -9B51 (chi1) -9B52 (piao1) -9B53 (bi4) -9B54 (mo2) -9B55 (ji3) -9B56 (xu1) -9B57 (chou3) -9B58 (yan3) -9B59 (zhan3) -9B5A (yu2) -9B5B (dao1) -9B5C (ren2) -9B5D (ji4) -9B5E (ba1) -9B5F (hong1) -9B60 (tuo1) -9B61 (diao4) -9B62 (ji3) -9B63 (yu2) -9B64 (e2) -9B65 (que4) -9B66 (sha1) -9B67 (hang2) -9B68 (tun2) -9B69 (mo4) -9B6A (gai4) -9B6B (shen3) -9B6C (fan3) -9B6D (yuan2) -9B6E (pi2) -9B6F (lu3) -9B70 (wen2) -9B71 (hu2) -9B72 (lu2) -9B73 (za2) -9B74 (fang2) -9B75 (fen4) -9B76 (na4) -9B77 (you2) -9B78 (none0) -9B79 (none0) -9B7A (he2,ge3) -9B7B (xia2) -9B7C (qu1) -9B7D (han1) -9B7E (pi2) -9B7F (ling2) -9B80 (tuo2) -9B81 (ba4) -9B82 (qiu2) -9B83 (ping2) -9B84 (fu2) -9B85 (bi4) -9B86 (ji4) -9B87 (wei4) -9B88 (ju1) -9B89 (diao1) -9B8A (ba4) -9B8B (you2) -9B8C (gun3) -9B8D (pi2) -9B8E (nian2) -9B8F (xing1) -9B90 (tai2) -9B91 (bao4) -9B92 (fu4) -9B93 (zha3,zha4) -9B94 (ju4) -9B95 (gu1) -9B96 (none0) -9B97 (none0) -9B98 (none0) -9B99 (ta4) -9B9A (jie2) -9B9B (shua1) -9B9C (hou4) -9B9D (xiang3) -9B9E (er2) -9B9F (an4) -9BA0 (wei2) -9BA1 (tiao1) -9BA2 (zhu1) -9BA3 (yin4) -9BA4 (lie4) -9BA5 (luo4) -9BA6 (tong2) -9BA7 (yi2) -9BA8 (qi2) -9BA9 (bing4) -9BAA (wei3) -9BAB (jiao1) -9BAC (pu4) -9BAD (gui1,xie2) -9BAE (xian1,xian3) -9BAF (ge2) -9BB0 (hui2) -9BB1 (none0) -9BB2 (none0) -9BB3 (kao3) -9BB4 (none0) -9BB5 (duo2) -9BB6 (jun1) -9BB7 (ti2) -9BB8 (mian3) -9BB9 (shao1) -9BBA (za3) -9BBB (suo1) -9BBC (qin1) -9BBD (yu2) -9BBE (nei3) -9BBF (zhe2) -9BC0 (gun3) -9BC1 (geng3) -9BC2 (none0) -9BC3 (wu2) -9BC4 (qiu2) -9BC5 (ting2) -9BC6 (fu3) -9BC7 (huan4) -9BC8 (chou2) -9BC9 (li3) -9BCA (sha1) -9BCB (sha1) -9BCC (gao4) -9BCD (meng2) -9BCE (none0) -9BCF (none0) -9BD0 (none0) -9BD1 (none0) -9BD2 (yong3) -9BD3 (ni2) -9BD4 (zi1) -9BD5 (qi2) -9BD6 (qing1,zheng1) -9BD7 (xiang3) -9BD8 (nei3) -9BD9 (chun2) -9BDA (ji4) -9BDB (diao1) -9BDC (qie4) -9BDD (gu4) -9BDE (zhou3) -9BDF (dong1) -9BE0 (lai2) -9BE1 (fei1) -9BE2 (ni2) -9BE3 (yi4) -9BE4 (kun1) -9BE5 (lu4) -9BE6 (jiu4) -9BE7 (chang1) -9BE8 (jing1) -9BE9 (lun2) -9BEA (ling2) -9BEB (zou1) -9BEC (li2) -9BED (meng3) -9BEE (zong1) -9BEF (zhi2) -9BF0 (nian2,nian3) -9BF1 (none0) -9BF2 (none0) -9BF3 (none0) -9BF4 (shi1) -9BF5 (sao1) -9BF6 (hun3) -9BF7 (ti2) -9BF8 (hou2) -9BF9 (xing1) -9BFA (ju1) -9BFB (la4) -9BFC (zong1) -9BFD (ji4) -9BFE (bian1) -9BFF (bian1) -9C00 (huan4) -9C01 (quan2) -9C02 (ji4) -9C03 (wei1) -9C04 (wei1) -9C05 (yu2) -9C06 (chun1) -9C07 (rou2) -9C08 (die2) -9C09 (huang2) -9C0A (lian4) -9C0B (yan3) -9C0C (qiu1) -9C0D (qiu1) -9C0E (jian4) -9C0F (bi4) -9C10 (e4) -9C11 (yang2) -9C12 (fu4) -9C13 (sai1,xi3) -9C14 (jian3) -9C15 (ha2,xia1) -9C16 (tuo3) -9C17 (hu2) -9C18 (none0) -9C19 (ruo4) -9C1A (none0) -9C1B (wen1) -9C1C (jian1) -9C1D (hao4) -9C1E (wu1,wu4) -9C1F (pang2) -9C20 (sao1) -9C21 (liu2) -9C22 (ma3) -9C23 (shi2) -9C24 (shi1) -9C25 (guan1) -9C26 (zi1) -9C27 (teng2) -9C28 (ta4,ta3) -9C29 (yao2) -9C2A (ge2) -9C2B (rong2) -9C2C (qian2) -9C2D (qi2) -9C2E (wen1) -9C2F (ruo4) -9C30 (none0) -9C31 (lian2) -9C32 (ao2) -9C33 (le4) -9C34 (hui1) -9C35 (min3) -9C36 (ji4) -9C37 (tiao2) -9C38 (qu1) -9C39 (jian1) -9C3A (sao1) -9C3B (man2) -9C3C (xi2) -9C3D (qiu2) -9C3E (biao4) -9C3F (ji1) -9C40 (ji4) -9C41 (zhu2) -9C42 (jiang1) -9C43 (qiu1) -9C44 (zhuan1) -9C45 (yong1) -9C46 (zhang1) -9C47 (kang1) -9C48 (xue3) -9C49 (bie1) -9C4A (jue2) -9C4B (qu1) -9C4C (xiang4) -9C4D (bo1) -9C4E (jiao1) -9C4F (xun2) -9C50 (su4) -9C51 (huang2) -9C52 (zun1,zun4) -9C53 (shan4) -9C54 (shan4) -9C55 (fan1) -9C56 (gui4) -9C57 (lin2) -9C58 (xun2) -9C59 (miao2) -9C5A (xi3) -9C5B (none0) -9C5C (xiang1) -9C5D (fen4) -9C5E (guan1) -9C5F (hou4) -9C60 (kuai4) -9C61 (zei2) -9C62 (sao1) -9C63 (zhan1) -9C64 (gan3) -9C65 (gui4) -9C66 (sheng2) -9C67 (li3) -9C68 (chang2) -9C69 (none0) -9C6A (none0) -9C6B (ai4) -9C6C (ru2) -9C6D (ji4) -9C6E (xu4) -9C6F (huo4) -9C70 (none0) -9C71 (li4) -9C72 (lie4) -9C73 (li4) -9C74 (mie4) -9C75 (zhen1) -9C76 (xiang3) -9C77 (e4) -9C78 (lu2) -9C79 (guan4) -9C7A (li2) -9C7B (xian1) -9C7C (yu2) -9C7D (dao1) -9C7E (ji3) -9C7F (you2) -9C80 (tun2) -9C81 (lu3) -9C82 (fang2) -9C83 (ba1) -9C84 (ke3) -9C85 (ba4) -9C86 (ping2) -9C87 (nian2) -9C88 (lu2) -9C89 (you2) -9C8A (zha3) -9C8B (fu4) -9C8C (ba4,bo2) -9C8D (bao4) -9C8E (hou4) -9C8F (pi2) -9C90 (tai2) -9C91 (gui1,xie2) -9C92 (jie2) -9C93 (kao4) -9C94 (wei3) -9C95 (er2) -9C96 (tong2) -9C97 (zei2) -9C98 (hou4) -9C99 (kuai4) -9C9A (ji4) -9C9B (jiao1) -9C9C (xian1,xian3) -9C9D (zha3) -9C9E (xiang3) -9C9F (xun2) -9CA0 (geng3) -9CA1 (li2) -9CA2 (lian2) -9CA3 (jian1) -9CA4 (li3) -9CA5 (shi2) -9CA6 (tiao2) -9CA7 (gun3) -9CA8 (sha1) -9CA9 (huan4) -9CAA (jun1) -9CAB (ji4) -9CAC (yong3) -9CAD (qing1,zheng1) -9CAE (ling2) -9CAF (qi2) -9CB0 (zou1) -9CB1 (fei1) -9CB2 (kun1) -9CB3 (chang1) -9CB4 (gu4) -9CB5 (ni2) -9CB6 (nian2) -9CB7 (diao1) -9CB8 (jing1) -9CB9 (shen1) -9CBA (shi1) -9CBB (zi1) -9CBC (fen4) -9CBD (die2) -9CBE (bi1) -9CBF (chang2) -9CC0 (ti2) -9CC1 (wen1) -9CC2 (wei1) -9CC3 (sai1) -9CC4 (e4) -9CC5 (qiu1) -9CC6 (fu4) -9CC7 (huang2) -9CC8 (quan2) -9CC9 (jiang1) -9CCA (bian1) -9CCB (sao1) -9CCC (ao2) -9CCD (qi2) -9CCE (ta3) -9CCF (guan1) -9CD0 (yao2) -9CD1 (pang2) -9CD2 (jian1) -9CD3 (le4) -9CD4 (biao4) -9CD5 (xue3) -9CD6 (bie1) -9CD7 (man2) -9CD8 (min3) -9CD9 (yong1) -9CDA (wei4) -9CDB (xi2) -9CDC (gui4) -9CDD (shan4) -9CDE (lin2) -9CDF (zun1) -9CE0 (hu4) -9CE1 (gan3) -9CE2 (li3) -9CE3 (shan4) -9CE4 (guan3) -9CE5 (niao3) -9CE6 (yi3) -9CE7 (fu2) -9CE8 (li4) -9CE9 (jiu1) -9CEA (bu3) -9CEB (yan4) -9CEC (fu2) -9CED (diao1) -9CEE (ji1) -9CEF (feng4) -9CF0 (none0) -9CF1 (gan1) -9CF2 (shi1) -9CF3 (feng4) -9CF4 (ming2) -9CF5 (bao3) -9CF6 (yuan1) -9CF7 (zhi1) -9CF8 (hu4) -9CF9 (qian2) -9CFA (fu1) -9CFB (fen1) -9CFC (wen2) -9CFD (jian1) -9CFE (shi1) -9CFF (yu4) -9D00 (fou3) -9D01 (yiao1) -9D02 (ju2) -9D03 (jue2) -9D04 (pi1) -9D05 (huan1) -9D06 (zhen4) -9D07 (bao3) -9D08 (yan4) -9D09 (ya1) -9D0A (zheng4) -9D0B (fang1) -9D0C (feng4) -9D0D (wen2) -9D0E (ou1) -9D0F (te4) -9D10 (jia1) -9D11 (nu1) -9D12 (ling2) -9D13 (mie4) -9D14 (fu2) -9D15 (tuo2) -9D16 (wen2) -9D17 (li4) -9D18 (bian4) -9D19 (zhi4) -9D1A (ge1) -9D1B (yuan1) -9D1C (zi1) -9D1D (qu2) -9D1E (xiao1) -9D1F (chi1,zhi1) -9D20 (dan4) -9D21 (ju1) -9D22 (you4) -9D23 (gu1) -9D24 (zhong1) -9D25 (yu4) -9D26 (yang1) -9D27 (rong4) -9D28 (ya1) -9D29 (zhi4) -9D2A (yu4) -9D2B (none0) -9D2C (ying1) -9D2D (zhui1) -9D2E (wu1) -9D2F (er2) -9D30 (gua1) -9D31 (ai4) -9D32 (zhi1) -9D33 (yan4) -9D34 (heng2) -9D35 (jiao1) -9D36 (ji2) -9D37 (lie4) -9D38 (zhu1) -9D39 (ren2) -9D3A (ti2) -9D3B (hong2) -9D3C (luo4) -9D3D (ru2) -9D3E (mou2) -9D3F (ge1) -9D40 (ren4) -9D41 (jiao1) -9D42 (xiu1) -9D43 (zhou1) -9D44 (chi1) -9D45 (luo4) -9D46 (none0) -9D47 (none0) -9D48 (none0) -9D49 (luan2) -9D4A (jia2) -9D4B (ji4) -9D4C (yu2) -9D4D (huan1) -9D4E (tuo3) -9D4F (bu1) -9D50 (wu2) -9D51 (juan1) -9D52 (yu4) -9D53 (bo2) -9D54 (xun4) -9D55 (xun4) -9D56 (bi4) -9D57 (xi1) -9D58 (jun4) -9D59 (ju2) -9D5A (tu2,tu1) -9D5B (jing1) -9D5C (ti2,ti4) -9D5D (e2) -9D5E (e2) -9D5F (kuang2) -9D60 (hu2,gu3) -9D61 (wu3) -9D62 (shen1) -9D63 (la4) -9D64 (none0) -9D65 (none0) -9D66 (lu4) -9D67 (bing4) -9D68 (shu1) -9D69 (fu2) -9D6A (an1) -9D6B (zhao4) -9D6C (peng2) -9D6D (qin2) -9D6E (qian1) -9D6F (bei1) -9D70 (diao1) -9D71 (lu4) -9D72 (que4,qiao3) -9D73 (jian1) -9D74 (ju2) -9D75 (tu4) -9D76 (ya1) -9D77 (yuan1) -9D78 (qi2) -9D79 (li2) -9D7A (ye4) -9D7B (zhui1) -9D7C (kong1) -9D7D (duo4) -9D7E (kun1) -9D7F (sheng1) -9D80 (qi2) -9D81 (jing1) -9D82 (ni2) -9D83 (e4) -9D84 (jing1) -9D85 (zi1) -9D86 (lai2) -9D87 (dong1) -9D88 (qi1) -9D89 (chun2) -9D8A (geng1) -9D8B (ju1) -9D8C (qu1) -9D8D (none0) -9D8E (none0) -9D8F (ji1) -9D90 (shu4) -9D91 (none0) -9D92 (chi3) -9D93 (miao2) -9D94 (rou2) -9D95 (fu2) -9D96 (qiu1) -9D97 (ti2) -9D98 (hu2) -9D99 (ti2) -9D9A (e4) -9D9B (jie1) -9D9C (mao2) -9D9D (fu2) -9D9E (chun1) -9D9F (tu2) -9DA0 (yan3) -9DA1 (he2) -9DA2 (yuan2) -9DA3 (pian1,bin4) -9DA4 (yun4) -9DA5 (mei2) -9DA6 (hu2) -9DA7 (ying1) -9DA8 (dun4) -9DA9 (mu4,wu4) -9DAA (ju2) -9DAB (none0) -9DAC (cang1) -9DAD (fang3) -9DAE (ge4) -9DAF (ying1) -9DB0 (yuan2) -9DB1 (xuan1) -9DB2 (weng1) -9DB3 (shi1) -9DB4 (he4,hao2) -9DB5 (chu2) -9DB6 (tang2) -9DB7 (xia4) -9DB8 (ruo4) -9DB9 (liu2) -9DBA (ji2) -9DBB (gu3,hu2,gu2) -9DBC (jian1) -9DBD (zhun3) -9DBE (han4) -9DBF (zi1) -9DC0 (ci2) -9DC1 (yi4,ni4) -9DC2 (yao4) -9DC3 (yan4) -9DC4 (ji1) -9DC5 (li4,piao3) -9DC6 (tian2) -9DC7 (kou4) -9DC8 (ti1) -9DC9 (ti1) -9DCA (ni4) -9DCB (tu2) -9DCC (ma3) -9DCD (jiao1) -9DCE (liu2) -9DCF (zhen1) -9DD0 (chen2) -9DD1 (li4) -9DD2 (zhuan1) -9DD3 (zhe4) -9DD4 (ao2) -9DD5 (yao3) -9DD6 (yi1) -9DD7 (ou1) -9DD8 (chi4) -9DD9 (zhi4) -9DDA (liao2,liu4) -9DDB (rong2) -9DDC (lou2) -9DDD (bi4) -9DDE (shuang1) -9DDF (zhuo2) -9DE0 (yu2) -9DE1 (wu2) -9DE2 (jue2) -9DE3 (yin2) -9DE4 (tan2) -9DE5 (si1) -9DE6 (jiao1) -9DE7 (yi4) -9DE8 (hua1) -9DE9 (bi4) -9DEA (ying1) -9DEB (su4) -9DEC (huang2) -9DED (fan2) -9DEE (jiao1) -9DEF (liao2) -9DF0 (yan4) -9DF1 (kao1) -9DF2 (jiu4) -9DF3 (xian2) -9DF4 (xian2) -9DF5 (tu2) -9DF6 (mai3) -9DF7 (zun1) -9DF8 (yu4) -9DF9 (ying1) -9DFA (lu4) -9DFB (tuan2) -9DFC (xian2) -9DFD (xue2) -9DFE (yi4) -9DFF (pi4) -9E00 (shu2) -9E01 (luo2) -9E02 (qi1) -9E03 (yi2) -9E04 (ji1) -9E05 (zhe2) -9E06 (yu2) -9E07 (zhan1) -9E08 (ye4) -9E09 (yang2) -9E0A (pi4) -9E0B (ning2) -9E0C (hu4) -9E0D (mi2) -9E0E (ying1) -9E0F (meng2) -9E10 (di2) -9E11 (yue4) -9E12 (yu2) -9E13 (lei3) -9E14 (bo2) -9E15 (lu2) -9E16 (he4) -9E17 (long2) -9E18 (shuang1) -9E19 (yue4) -9E1A (ying1) -9E1B (guan4) -9E1C (qu2) -9E1D (li2) -9E1E (luan2) -9E1F (niao3,diao3) -9E20 (jiu1) -9E21 (ji1) -9E22 (yuan1) -9E23 (ming2) -9E24 (shi1) -9E25 (ou1) -9E26 (ya1) -9E27 (cang1) -9E28 (bao3) -9E29 (zhen4) -9E2A (gu1) -9E2B (dong1) -9E2C (lu2) -9E2D (ya1) -9E2E (xiao1) -9E2F (yang1) -9E30 (ling2) -9E31 (chi1) -9E32 (qu2) -9E33 (yuan1) -9E34 (xue2) -9E35 (tuo2) -9E36 (si1) -9E37 (zhi4) -9E38 (er2) -9E39 (gua1) -9E3A (xiu1) -9E3B (heng2) -9E3C (zhou1) -9E3D (ge1) -9E3E (luan2) -9E3F (hong2) -9E40 (wu2) -9E41 (bo2) -9E42 (li2) -9E43 (juan1) -9E44 (hu2,gu3) -9E45 (e2) -9E46 (yu4) -9E47 (xian2) -9E48 (ti2) -9E49 (wu3) -9E4A (que4) -9E4B (miao2) -9E4C (an1) -9E4D (kun1) -9E4E (bei1) -9E4F (peng2) -9E50 (qian1) -9E51 (chun2) -9E52 (geng1) -9E53 (yuan1) -9E54 (su4) -9E55 (hu2) -9E56 (he2) -9E57 (e4) -9E58 (gu3,hu2) -9E59 (qiu1) -9E5A (ci2) -9E5B (mei2) -9E5C (wu4) -9E5D (yi4) -9E5E (yao4) -9E5F (weng1) -9E60 (liu2) -9E61 (ji2) -9E62 (yi4) -9E63 (jian1) -9E64 (he4) -9E65 (yi1) -9E66 (ying1) -9E67 (zhe4) -9E68 (liu4) -9E69 (liao2) -9E6A (jiao1) -9E6B (jiu4) -9E6C (yu4) -9E6D (lu4) -9E6E (huan2) -9E6F (zhan1) -9E70 (ying1) -9E71 (hu4) -9E72 (meng2) -9E73 (guan4) -9E74 (shuang1) -9E75 (lu3) -9E76 (jin1) -9E77 (ling2) -9E78 (jian3) -9E79 (xian2) -9E7A (cuo2) -9E7B (jian3) -9E7C (jian3) -9E7D (yan2) -9E7E (cuo2) -9E7F (lu4) -9E80 (you1) -9E81 (cu1) -9E82 (ji3) -9E83 (biao1) -9E84 (cu1) -9E85 (pao2) -9E86 (zhu4) -9E87 (jun1,qun2) -9E88 (zhu3) -9E89 (jian1,qian1) -9E8A (mi2) -9E8B (mi2) -9E8C (wu2) -9E8D (liu2) -9E8E (chen2) -9E8F (jun1,qun2) -9E90 (lin2) -9E91 (ni2) -9E92 (qi2) -9E93 (lu4) -9E94 (jiu4) -9E95 (jun1,qun2) -9E96 (jing1) -9E97 (li4,li2) -9E98 (xiang1) -9E99 (yan2) -9E9A (jia1) -9E9B (mi2) -9E9C (li4) -9E9D (she4) -9E9E (zhang1) -9E9F (lin2) -9EA0 (jing1) -9EA1 (qi2) -9EA2 (ling2) -9EA3 (yan2) -9EA4 (cu1) -9EA5 (mai4) -9EA6 (mai4) -9EA7 (ge1) -9EA8 (chao3) -9EA9 (fu1) -9EAA (mian4) -9EAB (mian3) -9EAC (fu1) -9EAD (pao4) -9EAE (qu4) -9EAF (qu1,qu3) -9EB0 (mou2) -9EB1 (fu1) -9EB2 (xian4) -9EB3 (lai2) -9EB4 (qu1,qu2) -9EB5 (mian4) -9EB6 (chi1,li2) -9EB7 (feng1) -9EB8 (fu1) -9EB9 (qu1) -9EBA (mian4) -9EBB (ma2,ma1) -9EBC (ma2,me5,mo5) -9EBD (mo2,me5) -9EBE (hui1) -9EBF (none0) -9EC0 (zou1) -9EC1 (nen1) -9EC2 (fen2) -9EC3 (huang2) -9EC4 (huang2) -9EC5 (jin1) -9EC6 (guang1) -9EC7 (tian1) -9EC8 (tou3) -9EC9 (hong2) -9ECA (xi1) -9ECB (kuang4) -9ECC (hong2) -9ECD (shu3) -9ECE (li2) -9ECF (nian2) -9ED0 (chi1,li2) -9ED1 (hei1) -9ED2 (hei1) -9ED3 (yi4) -9ED4 (qian2) -9ED5 (zhen3) -9ED6 (xi4) -9ED7 (tuan3) -9ED8 (mo4) -9ED9 (mo4) -9EDA (qian2) -9EDB (dai4) -9EDC (chu4) -9EDD (you3) -9EDE (dian3) -9EDF (yi1) -9EE0 (xia2) -9EE1 (yan3) -9EE2 (qu1) -9EE3 (mei3) -9EE4 (yan3) -9EE5 (qing2,jing1) -9EE6 (yu4) -9EE7 (li2) -9EE8 (dang3) -9EE9 (du2) -9EEA (can3) -9EEB (yin1) -9EEC (an4) -9EED (yan3) -9EEE (tan2) -9EEF (an4) -9EF0 (zhen3) -9EF1 (dai4) -9EF2 (can3) -9EF3 (yi1) -9EF4 (mei2) -9EF5 (dan3) -9EF6 (yan3) -9EF7 (du2) -9EF8 (lu2) -9EF9 (zhi3) -9EFA (fen3) -9EFB (fu2) -9EFC (fu3) -9EFD (min3,mian3) -9EFE (min3,mian3) -9EFF (yuan2) -9F00 (cu4) -9F01 (qu4) -9F02 (chao2) -9F03 (wa1) -9F04 (zhu1) -9F05 (zhi1) -9F06 (mang2) -9F07 (ao2) -9F08 (bie1) -9F09 (tuo2) -9F0A (bi4) -9F0B (yuan2) -9F0C (chao2) -9F0D (tuo2) -9F0E (ding3) -9F0F (mi4) -9F10 (nai4) -9F11 (ding3) -9F12 (zi1) -9F13 (gu3,hu2) -9F14 (gu3) -9F15 (dong1) -9F16 (fen2) -9F17 (tao2) -9F18 (yuan1) -9F19 (pi2) -9F1A (chang1) -9F1B (gao1) -9F1C (qi4) -9F1D (yuan1) -9F1E (tang1) -9F1F (teng1) -9F20 (shu3) -9F21 (shu3) -9F22 (fen2) -9F23 (fei4) -9F24 (wen2) -9F25 (ba2) -9F26 (diao1) -9F27 (tuo2) -9F28 (tong2) -9F29 (qu2) -9F2A (sheng1) -9F2B (shi2) -9F2C (you4) -9F2D (shi2) -9F2E (ting2) -9F2F (wu2) -9F30 (nian4) -9F31 (jing1) -9F32 (hun2) -9F33 (ju2) -9F34 (yan3) -9F35 (tu2) -9F36 (si1) -9F37 (xi1) -9F38 (xian3) -9F39 (yan3) -9F3A (lei2) -9F3B (bi2) -9F3C (yao2) -9F3D (yan3,qui2) -9F3E (han1) -9F3F (hui1) -9F40 (wu4) -9F41 (hou1) -9F42 (xi4) -9F43 (ge2) -9F44 (zha1) -9F45 (xiu4) -9F46 (weng4) -9F47 (zha1) -9F48 (nong2) -9F49 (nang4) -9F4A (qi2,zhai1) -9F4B (zhai1) -9F4C (ji4) -9F4D (zi1,ji1) -9F4E (ji1) -9F4F (ji1) -9F50 (qi2,ji4,qi4) -9F51 (ji1) -9F52 (chi3) -9F53 (chen3) -9F54 (chen4) -9F55 (he2) -9F56 (ya2) -9F57 (ken3) -9F58 (xie4) -9F59 (bao1) -9F5A (ze2) -9F5B (shi4) -9F5C (zi1) -9F5D (chi1) -9F5E (nian4) -9F5F (ju3) -9F60 (tiao2) -9F61 (ling2) -9F62 (ling2) -9F63 (chu1) -9F64 (quan2) -9F65 (xie4) -9F66 (yin2,ken3) -9F67 (nie4) -9F68 (jiu4) -9F69 (nie4) -9F6A (chuo4) -9F6B (kun3) -9F6C (yu3) -9F6D (chu3) -9F6E (yi3) -9F6F (ni2) -9F70 (cuo4) -9F71 (chuo4) -9F72 (qu3) -9F73 (nian3) -9F74 (xian3) -9F75 (yu2) -9F76 (e4) -9F77 (wo4) -9F78 (yi4) -9F79 (chi1) -9F7A (zou1) -9F7B (dian1) -9F7C (chu3) -9F7D (jin4) -9F7E (ya4) -9F7F (chi3) -9F80 (chen4) -9F81 (he2) -9F82 (yin2) -9F83 (ju3) -9F84 (ling2) -9F85 (bao1) -9F86 (tiao2) -9F87 (zi1) -9F88 (yin2,ken3) -9F89 (yu3) -9F8A (chuo4) -9F8B (qu3) -9F8C (wo4) -9F8D (long2) -9F8E (pang2) -9F8F (gong1) -9F90 (pang2) -9F91 (yan3) -9F92 (long2) -9F93 (long2) -9F94 (gong1) -9F95 (kan1) -9F96 (ta4) -9F97 (ling2) -9F98 (ta4) -9F99 (long2) -9F9A (gong1) -9F9B (kan1) -9F9C (gui1,jun1,qiu1) -9F9D (qiu1) -9F9E (bie1) -9F9F (gui1,jun1,qiu1) -9FA0 (yue4) -9FA1 (chui1) -9FA2 (he2) -9FA3 (jue2) -9FA4 (xie2) -9FA5 (yue4) diff --git a/Flow.Launcher.Plugin/ActionContext.cs b/Flow.Launcher.Plugin/ActionContext.cs index b9e499d2b..9e05bbd06 100644 --- a/Flow.Launcher.Plugin/ActionContext.cs +++ b/Flow.Launcher.Plugin/ActionContext.cs @@ -1,15 +1,64 @@ -namespace Flow.Launcher.Plugin +using System.Windows.Input; + +namespace Flow.Launcher.Plugin { + /// + /// Context provided as a parameter when invoking a + /// or + /// public class ActionContext { + /// + /// Contains the press state of certain special keys. + /// public SpecialKeyState SpecialKeyState { get; set; } } + /// + /// Contains the press state of certain special keys. + /// public class SpecialKeyState { + /// + /// True if the Ctrl key is pressed. + /// public bool CtrlPressed { get; set; } + + /// + /// True if the Shift key is pressed. + /// public bool ShiftPressed { get; set; } + + /// + /// True if the Alt key is pressed. + /// public bool AltPressed { get; set; } + + /// + /// True if the Windows key is pressed. + /// public bool WinPressed { get; set; } + + /// + /// Get this object represented as a flag combination. + /// + /// + public ModifierKeys ToModifierKeys() + { + return (CtrlPressed ? ModifierKeys.Control : ModifierKeys.None) | + (ShiftPressed ? ModifierKeys.Shift : ModifierKeys.None) | + (AltPressed ? ModifierKeys.Alt : ModifierKeys.None) | + (WinPressed ? ModifierKeys.Windows : ModifierKeys.None); + } + + /// + /// Default object with all keys not pressed. + /// + public static readonly SpecialKeyState Default = new () { + CtrlPressed = false, + ShiftPressed = false, + AltPressed = false, + WinPressed = false + }; } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/AllowedLanguage.cs b/Flow.Launcher.Plugin/AllowedLanguage.cs index 827958a7b..619a94deb 100644 --- a/Flow.Launcher.Plugin/AllowedLanguage.cs +++ b/Flow.Launcher.Plugin/AllowedLanguage.cs @@ -1,38 +1,90 @@ -namespace Flow.Launcher.Plugin +using System; + +namespace Flow.Launcher.Plugin { + /// + /// Allowed plugin languages + /// public static class AllowedLanguage { - public static string Python - { - get { return "PYTHON"; } - } + /// + /// Python + /// + public const string Python = "Python"; - public static string CSharp - { - get { return "CSHARP"; } - } + /// + /// Python V2 + /// + public const string PythonV2 = "Python_v2"; - public static string FSharp - { - get { return "FSHARP"; } - } + /// + /// C# + /// + public const string CSharp = "CSharp"; - public static string Executable - { - get { return "EXECUTABLE"; } - } + /// + /// F# + /// + public const string FSharp = "FSharp"; + /// + /// Standard .exe + /// + public const string Executable = "Executable"; + + /// + /// Standard .exe + /// + public const string ExecutableV2 = "Executable_V2"; + + /// + /// TypeScript + /// + public const string TypeScript = "TypeScript"; + + /// + /// TypeScript + /// + public const string TypeScriptV2 = "TypeScript_V2"; + + /// + /// JavaScript + /// + public const string JavaScript = "JavaScript"; + + /// + /// JavaScript + /// + public const string JavaScriptV2 = "JavaScript_V2"; + + /// + /// Determines if this language is a .NET language + /// + /// + /// public static bool IsDotNet(string language) { - return language.ToUpper() == CSharp - || language.ToUpper() == FSharp; + return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase) + || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); } + /// + /// Determines if this language is supported + /// + /// + /// public static bool IsAllowed(string language) { return IsDotNet(language) - || language.ToUpper() == Python.ToUpper() - || language.ToUpper() == Executable.ToUpper(); + || language.Equals(Python, StringComparison.OrdinalIgnoreCase) + || language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(Executable, StringComparison.OrdinalIgnoreCase) + || language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(TypeScriptV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScriptV2, StringComparison.OrdinalIgnoreCase); + ; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/BaseModel.cs b/Flow.Launcher.Plugin/BaseModel.cs index 5bb558702..a4d666ced 100644 --- a/Flow.Launcher.Plugin/BaseModel.cs +++ b/Flow.Launcher.Plugin/BaseModel.cs @@ -4,14 +4,24 @@ using JetBrains.Annotations; namespace Flow.Launcher.Plugin { + /// + /// Base model for plugin classes + /// public class BaseModel : INotifyPropertyChanged { + /// + /// Property changed event handler + /// public event PropertyChangedEventHandler PropertyChanged; + /// + /// Invoked when a property changes + /// + /// [NotifyPropertyChangedInvocator] protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/EventHandler.cs b/Flow.Launcher.Plugin/EventHandler.cs index 476668584..893b0ba80 100644 --- a/Flow.Launcher.Plugin/EventHandler.cs +++ b/Flow.Launcher.Plugin/EventHandler.cs @@ -1,11 +1,27 @@ -using System.Windows; +using System; +using System.Windows; using System.Windows.Input; namespace Flow.Launcher.Plugin { + /// + /// Delegate for key down event + /// + /// public delegate void FlowLauncherKeyDownEventHandler(FlowLauncherKeyDownEventArgs e); + + /// + /// Delegate for query event + /// + /// public delegate void AfterFlowLauncherQueryEventHandler(FlowLauncherQueryEventArgs e); + /// + /// Delegate for drop events [unused?] + /// + /// + /// + /// public delegate void ResultItemDropEventHandler(Result result, IDataObject dropObject, DragEventArgs e); /// @@ -17,14 +33,48 @@ namespace Flow.Launcher.Plugin /// return true to continue handling, return false to intercept system handling public delegate bool FlowLauncherGlobalKeyboardEventHandler(int keyevent, int vkcode, SpecialKeyState state); + /// + /// A delegate for when the visibility is changed + /// + /// + /// + public delegate void VisibilityChangedEventHandler(object sender, VisibilityChangedEventArgs args); + + /// + /// The event args for + /// + public class VisibilityChangedEventArgs : EventArgs + { + /// + /// if the main window has become visible + /// + public bool IsVisible { get; init; } + } + + /// + /// Arguments container for the Key Down event + /// public class FlowLauncherKeyDownEventArgs { + /// + /// The actual query + /// public string Query { get; set; } + + /// + /// Relevant key events for this event + /// public KeyEventArgs keyEventArgs { get; set; } } + /// + /// Arguments container for the Query event + /// public class FlowLauncherQueryEventArgs { + /// + /// The actual query + /// public Query Query { get; set; } } } diff --git a/Flow.Launcher.Plugin/Features.cs b/Flow.Launcher.Plugin/Features.cs index 5b9c6a7b9..b4a1eccc1 100644 --- a/Flow.Launcher.Plugin/Features.cs +++ b/Flow.Launcher.Plugin/Features.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Threading; - -namespace Flow.Launcher.Plugin +namespace Flow.Launcher.Plugin { /// /// Base Interface for Flow's special plugin feature interface diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index e3d26f5d3..1472813b8 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -1,7 +1,7 @@ - + - net5.0-windows + net7.0-windows {8451ECDD-2EA4-4966-BB0A-7BBC40138E80} true Library @@ -14,10 +14,10 @@ - 1.4.0 - 1.4.0 - 1.4.0 - 1.4.0 + 4.4.0 + 4.4.0 + 4.4.0 + 4.4.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -26,6 +26,7 @@ flowlauncher true true + Readme.md @@ -42,6 +43,7 @@ 4 AnyCPU false + ..\Output\Debug\Flow.Launcher.Plugin.xml @@ -55,13 +57,26 @@ - + + + + + - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Flow.Launcher.Plugin/GlyphInfo.cs b/Flow.Launcher.Plugin/GlyphInfo.cs new file mode 100644 index 000000000..45b90b09e --- /dev/null +++ b/Flow.Launcher.Plugin/GlyphInfo.cs @@ -0,0 +1,9 @@ +namespace Flow.Launcher.Plugin +{ + /// + /// Text with FontFamily specified + /// + /// Font Family of this Glyph + /// Text/Unicode of the Glyph + public record GlyphInfo(string FontFamily, string Glyph); +} diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs new file mode 100644 index 000000000..cc4f94c56 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// This interface is for plugins that wish to provide file preview (external preview) + /// via a third party app instead of the default preview. + /// + public interface IAsyncExternalPreview : IFeatures + { + /// + /// Method for opening/showing the preview. + /// + /// The file path to open the preview for + /// Whether to send a toast message notification on failure for the user + public Task OpenPreviewAsync(string path, bool sendFailToast = true); + + /// + /// Method for closing/hiding the preview. + /// + public Task ClosePreviewAsync(); + + /// + /// Method for switching the preview to the next file result. + /// This requires the external preview be already open/showing + /// + /// The file path to switch the preview for + /// Whether to send a toast message notification on failure for the user + public Task SwitchPreviewAsync(string path, bool sendFailToast = true); + + /// + /// Allows the preview plugin to override the AlwaysPreview setting. Typically useful if plugin's preview does not + /// fully work well with being shown together when the query window appears with results. + /// When AlwaysPreview setting is on and this is set to false, the preview will not be shown when query + /// window appears with results, instead the internal preview will be shown. + /// + /// + public bool AllowAlwaysPreview(); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs index bd4500a7e..3d5f44a0d 100644 --- a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs @@ -15,6 +15,10 @@ namespace Flow.Launcher.Plugin /// public interface IAsyncReloadable : IFeatures { + /// + /// Reload plugin data + /// + /// Task ReloadDataAsync(); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs b/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs index 5befbf5c9..06398fb1b 100644 --- a/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs +++ b/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs @@ -1,9 +1,18 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Flow.Launcher.Plugin { + /// + /// Adds support for presenting additional options for a given from a context menu. + /// public interface IContextMenu : IFeatures { + /// + /// Load context menu items for the given result. + /// + /// + /// The for which the user has activated the context menu. + /// List LoadContextMenus(Result selectedResult); } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs b/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs index e332d450e..bbc998fcb 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs @@ -1,12 +1,28 @@ -namespace Flow.Launcher.Plugin +using System.Globalization; + +namespace Flow.Launcher.Plugin { /// /// Represent plugins that support internationalization /// public interface IPluginI18n : IFeatures { + /// + /// Get a localised version of the plugin's title + /// string GetTranslatedPluginTitle(); + /// + /// Get a localised version of the plugin's description + /// string GetTranslatedPluginDescription(); + + /// + /// The method will be invoked when language of flow changed + /// + void OnCultureInfoChanged(CultureInfo newCulture) + { + + } } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index d9cdf5581..a73ead814 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -1,11 +1,14 @@ -using Flow.Launcher.Plugin.SharedModels; -using JetBrains.Annotations; -using System; +using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using Flow.Launcher.Plugin.SharedModels; +using JetBrains.Annotations; namespace Flow.Launcher.Plugin { @@ -15,12 +18,13 @@ namespace Flow.Launcher.Plugin public interface IPublicAPI { /// - /// Change Flow.Launcher query + /// Change Flow.Launcher query. + /// When current results are from context menu or history, it will go back to query results before changing query. /// /// query text /// - /// force requery By default, Flow Launcher will not fire query if your query is same with existing one. - /// Set this to true to force Flow Launcher requerying + /// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one. + /// Set this to to force Flow Launcher requerying /// void ChangeQuery(string query, bool requery = false); @@ -29,6 +33,27 @@ namespace Flow.Launcher.Plugin /// void RestartApp(); + /// + /// Run a shell command + /// + /// The command or program to run + /// the shell type to run, e.g. powershell.exe + /// Thrown when unable to find the file specified in the command + /// Thrown when error occurs during the execution of the command + void ShellRun(string cmd, string filename = "cmd.exe"); + + /// + /// Copies the passed in text and shows a message indicating whether the operation was completed successfully. + /// When directCopy is set to true and passed in text is the path to a file or directory, + /// the actual file/directory will be copied to clipboard. Otherwise the text itself will still be copied to clipboard. + /// + /// Text to save on clipboard + /// When true it will directly copy the file/folder from the path specified in text + /// Whether to show the default notification from this method after copy is done. + /// It will show file/folder/text is copied successfully. + /// Turn this off to show your own notification after copy is done.> + public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true); + /// /// Save everything, all of Flow Launcher and plugins' data and settings /// @@ -59,6 +84,27 @@ namespace Flow.Launcher.Plugin /// Optional message subtitle void ShowMsgError(string title, string subTitle = ""); + /// + /// Show the MainWindow when hiding + /// + void ShowMainWindow(); + + /// + /// Hide MainWindow + /// + void HideMainWindow(); + + /// + /// Representing whether the main window is visible + /// + /// + bool IsMainWindowVisible(); + + /// + /// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off. + /// + event VisibilityChangedEventHandler VisibilityChanged; + /// /// Show message box /// @@ -96,10 +142,48 @@ namespace Flow.Launcher.Plugin List GetAllPlugins(); /// - /// Fired after global keyboard events - /// if you want to hook something like Ctrl+R, you should use this event + /// Registers a callback function for global keyboard events. /// - event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; + /// + /// The callback function to invoke when a global keyboard event occurs. + /// + /// Parameters: + /// + /// int: The type of (key down, key up, etc.) + /// int: The virtual key code of the pressed/released key + /// : The state of modifier keys (Ctrl, Alt, Shift, etc.) + /// + /// + /// + /// Returns: true to allow normal system processing of the key event, + /// or false to intercept and prevent default handling. + /// + /// + /// + /// This callback will be invoked for all keyboard events system-wide. + /// Use with caution as intercepting system keys may affect normal system operation. + /// + public void RegisterGlobalKeyboardCallback(Func callback); + + /// + /// Remove a callback for Global Keyboard Event + /// + /// + /// The callback function to invoke when a global keyboard event occurs. + /// + /// Parameters: + /// + /// int: The type of (key down, key up, etc.) + /// int: The virtual key code of the pressed/released key + /// : The state of modifier keys (Ctrl, Alt, Shift, etc.) + /// + /// + /// + /// Returns: true to allow normal system processing of the key event, + /// or false to intercept and prevent default handling. + /// + /// + public void RemoveGlobalKeyboardCallback(Func callback); /// /// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses @@ -129,24 +213,37 @@ namespace Flow.Launcher.Plugin /// Download the specific url to a cretain file path /// /// URL to download file + /// path to save downloaded file + /// + /// Action to report progress. The input of the action is the progress value which is a double value between 0 and 100. + /// It will be called if url support range request and the reportProgress is not null. + /// /// place to store file /// Task showing the progress - Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default); + Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default); /// - /// Add ActionKeyword for specific plugin + /// Add ActionKeyword and update action keyword metadata for specific plugin + /// Before adding, please check if action keyword is already assigned by /// /// ID for plugin that needs to add action keyword /// The actionkeyword that is supposed to be added void AddActionKeyword(string pluginId, string newActionKeyword); /// - /// Remove ActionKeyword for specific plugin + /// Remove ActionKeyword and update action keyword metadata for specific plugin /// /// ID for plugin that needs to remove action keyword - /// The actionkeyword that is supposed to be removed + /// The actionkeyword that is supposed to be removed void RemoveActionKeyword(string pluginId, string oldActionKeyword); + /// + /// Check whether specific ActionKeyword is assigned to any of the plugin + /// + /// The actionkeyword for checking + /// True if the actionkeyword is already assigned, False otherwise + bool ActionKeywordAssigned(string actionKeyword); + /// /// Log debug message /// Message will only be logged in Debug mode @@ -163,6 +260,11 @@ namespace Flow.Launcher.Plugin /// void LogWarn(string className, string message, [CallerMemberName] string methodName = ""); + /// + /// Log error message. Preferred error logging method for plugins. + /// + void LogError(string className, string message, [CallerMemberName] string methodName = ""); + /// /// Log an Exception. Will throw if in debug mode so developer will be aware, /// otherwise logs the eror message. This is the primary logging method used for Flow @@ -185,5 +287,254 @@ namespace Flow.Launcher.Plugin /// Type for Serialization /// void SaveSettingJsonStorage() where T : new(); + + /// + /// Open directory in an explorer configured by user via Flow's Settings. The default is Windows Explorer + /// + /// Directory Path to open + /// Extra FileName Info + public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null); + + /// + /// Opens the URL with the given Uri object. + /// The browser and mode used is based on what's configured in Flow's default browser settings. + /// + public void OpenUrl(Uri url, bool? inPrivate = null); + + /// + /// Opens the URL with the given string. + /// The browser and mode used is based on what's configured in Flow's default browser settings. + /// Non-C# plugins should use this method. + /// + public void OpenUrl(string url, bool? inPrivate = null); + + /// + /// Opens the application URI with the given Uri object, e.g. obsidian://search-query-example + /// + public void OpenAppUri(Uri appUri); + + /// + /// Opens the application URI with the given string, e.g. obsidian://search-query-example + /// Non-C# plugins should use this method + /// + public void OpenAppUri(string appUri); + + /// + /// Toggles Game Mode. off -> on and backwards + /// + public void ToggleGameMode(); + + /// + /// Switches Game Mode to given value + /// + /// New Game Mode status + public void SetGameMode(bool value); + + /// + /// Representing Game Mode status + /// + /// + public bool IsGameModeOn(); + + /// + /// Reloads the query. + /// When current results are from context menu or history, it will go back to query results before requerying. + /// + /// Choose the first result after reload if true; keep the last selected result if false. Default is true. + public void ReQuery(bool reselect = true); + + /// + /// Back to the query results. + /// This method should run when selected item is from context menu or history. + /// + public void BackToQueryResults(); + + /// + /// Displays a standardised Flow message box. + /// + /// The message of the message box. + /// The caption of the message box. + /// Specifies which button or buttons to display. + /// Specifies the icon to display. + /// Specifies the default result of the message box. + /// Specifies which message box button is clicked by the user. + public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK); + + /// + /// Displays a standardised Flow progress box. + /// + /// The caption of the progress box. + /// + /// Time-consuming task function, whose input is the action to report progress. + /// The input of the action is the progress value which is a double value between 0 and 100. + /// If there are any exceptions, this action will be null. + /// + /// When user cancel the progress, this action will be called. + /// + public Task ShowProgressBoxAsync(string caption, Func, Task> reportProgressAsync, Action cancelProgress = null); + + /// + /// Start the loading bar in main window + /// + public void StartLoadingBar(); + + /// + /// Stop the loading bar in main window + /// + public void StopLoadingBar(); + + /// + /// Get all available themes + /// + /// + public List GetAvailableThemes(); + + /// + /// Get the current theme + /// + /// + public ThemeData GetCurrentTheme(); + + /// + /// Set the current theme + /// + /// + /// + /// True if the theme is set successfully, false otherwise. + /// + public bool SetCurrentTheme(ThemeData theme); + + /// + /// Save all Flow's plugins caches + /// + void SavePluginCaches(); + + /// + /// Load BinaryStorage for current plugin's cache. This is the method used to load cache from binary in Flow. + /// When the file is not exist, it will create a new instance for the specific type. + /// + /// Type for deserialization + /// Cache file name + /// Cache directory from plugin metadata + /// Default data to return + /// + /// + /// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable + /// + Task LoadCacheBinaryStorageAsync(string cacheName, string cacheDirectory, T defaultData) where T : new(); + + /// + /// Save BinaryStorage for current plugin's cache. This is the method used to save cache to binary in Flow.Launcher + /// This method will save the original instance loaded with LoadCacheBinaryStorageAsync. + /// This API call is for manually Save. Flow will automatically save all cache type that has called LoadCacheBinaryStorageAsync or SaveCacheBinaryStorageAsync previously. + /// + /// Type for Serialization + /// Cache file name + /// Cache directory from plugin metadata + /// + /// + /// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable + /// + Task SaveCacheBinaryStorageAsync(string cacheName, string cacheDirectory) where T : new(); + + /// + /// Load image from path. + /// Support local, remote and data:image url. + /// Support png, jpg, jpeg, gif, bmp, tiff, ico, svg image files. + /// If image path is missing, it will return a missing icon. + /// + /// The path of the image. + /// + /// Load full image or not. + /// + /// + /// Cache the image or not. Cached image will be stored in FL cache. + /// If the image is just used one time, it's better to set this to false. + /// + /// + ValueTask LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true); + + /// + /// Update the plugin manifest + /// + /// + /// FL has multiple urls to download the plugin manifest. Set this to true to only use the primary url. + /// + /// + /// True if the manifest is updated successfully, false otherwise + public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default); + + /// + /// Get the plugin manifest + /// + /// + public IReadOnlyList GetPluginManifest(); + + /// + /// Check if the plugin has been modified. + /// If this plugin is updated, installed or uninstalled and users do not restart the app, + /// it will be marked as modified + /// + /// Plugin id + /// + public bool PluginModified(string id); + + /// + /// 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 + /// + /// The metadata of the old plugin to update + /// The new plugin to update + /// + /// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed. + /// + /// + public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath); + + /// + /// Install a plugin. By default will remove the zip file if installation is from url, + /// unless it's a local path installation + /// + /// The plugin to install + /// + /// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed. + /// + public void InstallPlugin(UserPlugin plugin, string zipFilePath); + + /// + /// Uninstall a plugin + /// + /// The metadata of the plugin to uninstall + /// + /// Plugin has their own settings. If this is set to true, the plugin settings will be removed. + /// + /// + public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false); + + /// + /// Log debug message of the time taken to execute a method + /// Message will only be logged in Debug mode + /// + /// The time taken to execute the method in milliseconds + public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = ""); + + /// + /// Log debug message of the time taken to execute a method asynchronously + /// Message will only be logged in Debug mode + /// + /// The time taken to execute the method in milliseconds + public Task StopwatchLogDebugAsync(string className, string message, Func action, [CallerMemberName] string methodName = ""); + + /// + /// Log info message of the time taken to execute a method + /// + /// The time taken to execute the method in milliseconds + public long StopwatchLogInfo(string className, string message, Action action, [CallerMemberName] string methodName = ""); + + /// + /// Log info message of the time taken to execute a method asynchronously + /// + /// The time taken to execute the method in milliseconds + public Task StopwatchLogInfoAsync(string className, string message, Func action, [CallerMemberName] string methodName = ""); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs index bd1ad406e..707f8d92c 100644 --- a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs +++ b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs @@ -17,6 +17,9 @@ /// public interface IReloadable : IFeatures { + /// + /// Synchronously reload plugin data + /// void ReloadData(); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs index fd21460ac..aa4e4a56d 100644 --- a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs +++ b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs @@ -4,17 +4,42 @@ using System.Threading; namespace Flow.Launcher.Plugin { + /// + /// Interface for plugins that want to manually update their results + /// public interface IResultUpdated : IFeatures { + /// + /// Event that is triggered when the results are updated + /// event ResultUpdatedEventHandler ResultsUpdated; } + /// + /// Delegate for the ResultsUpdated event + /// + /// + /// public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e); + /// + /// Event arguments for the ResultsUpdated event + /// public class ResultUpdatedEventArgs : EventArgs { + /// + /// List of results that should be displayed + /// public List Results; + + /// + /// Query that triggered the update + /// public Query Query; + + /// + /// Token that can be used to cancel the update + /// public CancellationToken Token { get; init; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/Interfaces/ISavable.cs b/Flow.Launcher.Plugin/Interfaces/ISavable.cs index 2d13eaa6e..38cbf8e08 100644 --- a/Flow.Launcher.Plugin/Interfaces/ISavable.cs +++ b/Flow.Launcher.Plugin/Interfaces/ISavable.cs @@ -1,12 +1,21 @@ namespace Flow.Launcher.Plugin { /// - /// Save addtional plugin data. Inherit this interface if additional data e.g. cache needs to be saved, - /// Otherwise if LoadSettingJsonStorage or SaveSettingJsonStorage has been callded, - /// plugin settings will be automatically saved (see Flow.Launcher/PublicAPIInstance.SavePluginSettings) by Flow + /// Inherit this interface if you need to save additional data which is not a setting or cache, + /// please implement this interface. /// + /// + /// For storing plugin settings, prefer + /// or . + /// For storing plugin caches, prefer + /// or . + /// Once called, those settings and caches will be automatically saved by Flow. + /// public interface ISavable : IFeatures { + /// + /// Save additional plugin data. + /// void Save(); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs index d5ffba20b..f034243c3 100644 --- a/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs +++ b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs @@ -2,8 +2,15 @@ namespace Flow.Launcher.Plugin { + /// + /// This interface is used to create settings panel for .Net plugins + /// public interface ISettingProvider { + /// + /// Create settings panel control for .Net plugins + /// + /// Control CreateSettingPanel(); } } diff --git a/Flow.Launcher.Plugin/KeyEvent.cs b/Flow.Launcher.Plugin/KeyEvent.cs new file mode 100644 index 000000000..321f17cc1 --- /dev/null +++ b/Flow.Launcher.Plugin/KeyEvent.cs @@ -0,0 +1,32 @@ +using Windows.Win32; + +namespace Flow.Launcher.Plugin +{ + /// + /// Enumeration of key events for + /// + /// and + /// + public enum KeyEvent + { + /// + /// Key down + /// + WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN, + + /// + /// Key up + /// + WM_KEYUP = (int)PInvoke.WM_KEYUP, + + /// + /// System key up + /// + WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP, + + /// + /// System key down + /// + WM_SYSKEYDOWN = (int)PInvoke.WM_SYSKEYDOWN + } +} diff --git a/Flow.Launcher.Plugin/NativeMethods.txt b/Flow.Launcher.Plugin/NativeMethods.txt new file mode 100644 index 000000000..0596691cc --- /dev/null +++ b/Flow.Launcher.Plugin/NativeMethods.txt @@ -0,0 +1,8 @@ +EnumThreadWindows +GetWindowText +GetWindowTextLength + +WM_KEYDOWN +WM_KEYUP +WM_SYSKEYDOWN +WM_SYSKEYUP \ No newline at end of file diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs index 04f20e984..a42e3930c 100644 --- a/Flow.Launcher.Plugin/PluginInitContext.cs +++ b/Flow.Launcher.Plugin/PluginInitContext.cs @@ -1,19 +1,31 @@ -using System; - -namespace Flow.Launcher.Plugin +namespace Flow.Launcher.Plugin { + /// + /// Carries data passed to a plugin when it gets initialized. + /// public class PluginInitContext { + /// + /// Default constructor. + /// public PluginInitContext() { } + /// + /// Constructor. + /// + /// + /// public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api) { CurrentPluginMetadata = currentPluginMetadata; API = api; } + /// + /// The metadata of the plugin being initialized. + /// public PluginMetadata CurrentPluginMetadata { get; internal set; } /// diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index e8f5cf744..da10bc6a5 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -1,28 +1,80 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { + /// + /// Plugin metadata + /// public class PluginMetadata : BaseModel { - private string _pluginDirectory; + /// + /// Plugin ID. + /// public string ID { get; set; } - public string Name { get; set; } - public string Author { get; set; } - public string Version { get; set; } - public string Language { get; set; } - public string Description { get; set; } - public string Website { get; set; } - public bool Disabled { get; set; } - public string ExecuteFilePath { get; private set;} + /// + /// Plugin name. + /// + public string Name { get; set; } + + /// + /// Plugin author. + /// + public string Author { get; set; } + + /// + /// Plugin version. + /// + public string Version { get; set; } + + /// + /// Plugin language. + /// See + /// + public string Language { get; set; } + + /// + /// Plugin description. + /// + public string Description { get; set; } + + /// + /// Plugin website. + /// + public string Website { get; set; } + + /// + /// Whether plugin is disabled. + /// + public bool Disabled { get; set; } + + /// + /// Plugin execute file path. + /// + public string ExecuteFilePath { get; private set; } + + /// + /// Plugin execute file name. + /// public string ExecuteFileName { get; set; } + /// + /// Plugin assembly name. + /// Only available for .Net plugins. + /// + [JsonIgnore] + public string AssemblyName { get; internal set; } + + private string _pluginDirectory; + + /// + /// Plugin source directory. + /// public string PluginDirectory { - get { return _pluginDirectory; } + get => _pluginDirectory; internal set { _pluginDirectory = value; @@ -31,28 +83,77 @@ namespace Flow.Launcher.Plugin } } + /// + /// The first action keyword of plugin. + /// public string ActionKeyword { get; set; } + /// + /// All action keywords of plugin. + /// public List ActionKeywords { get; set; } - public string IcoPath { get; set;} - - public override string ToString() - { - return Name; - } + /// + /// Hide plugin keyword setting panel. + /// + public bool HideActionKeywordPanel { get; set; } + /// + /// Plugin search delay time in ms. Null means use default search delay time. + /// + public int? SearchDelayTime { get; set; } = null; + + /// + /// Plugin icon path. + /// + public string IcoPath { get; set;} + + /// + /// Plugin priority. + /// [JsonIgnore] public int Priority { get; set; } /// - /// Init time include both plugin load time and init time + /// Init time include both plugin load time and init time. /// [JsonIgnore] public long InitTime { get; set; } + + /// + /// Average query time. + /// [JsonIgnore] public long AvgQueryTime { get; set; } + + /// + /// Query count. + /// [JsonIgnore] public int QueryCount { get; set; } + + /// + /// The path to the plugin settings directory which is not validated. + /// It is used to store plugin settings files and data files. + /// When plugin is deleted, FL will ask users whether to keep its settings. + /// If users do not want to keep, this directory will be deleted. + /// + public string PluginSettingsDirectoryPath { get; internal set; } + + /// + /// The path to the plugin cache directory which is not validated. + /// It is used to store cache files. + /// When plugin is deleted, this directory will be deleted as well. + /// + public string PluginCacheDirectoryPath { get; internal set; } + + /// + /// Convert to string. + /// + /// + public override string ToString() + { + return Name; + } } } diff --git a/Flow.Launcher.Plugin/PluginPair.cs b/Flow.Launcher.Plugin/PluginPair.cs index 7bf634691..f2c14d70c 100644 --- a/Flow.Launcher.Plugin/PluginPair.cs +++ b/Flow.Launcher.Plugin/PluginPair.cs @@ -1,21 +1,37 @@ namespace Flow.Launcher.Plugin { + /// + /// Plugin instance and plugin metadata + /// public class PluginPair { + /// + /// Plugin instance + /// public IAsyncPlugin Plugin { get; internal set; } + + /// + /// Plugin metadata + /// public PluginMetadata Metadata { get; internal set; } - - + /// + /// Convert to string + /// + /// public override string ToString() { return Metadata.Name; } + /// + /// Compare by plugin metadata ID + /// + /// + /// public override bool Equals(object obj) { - PluginPair r = obj as PluginPair; - if (r != null) + if (obj is PluginPair r) { return string.Equals(r.Metadata.ID, Metadata.ID); } @@ -25,6 +41,10 @@ } } + /// + /// Get hash code + /// + /// public override int GetHashCode() { var hashcode = Metadata.ID?.GetHashCode() ?? 0; diff --git a/Flow.Launcher.Plugin/Properties/AssemblyInfo.cs b/Flow.Launcher.Plugin/Properties/AssemblyInfo.cs index 4cdadffc9..0a602a472 100644 --- a/Flow.Launcher.Plugin/Properties/AssemblyInfo.cs +++ b/Flow.Launcher.Plugin/Properties/AssemblyInfo.cs @@ -2,4 +2,4 @@ [assembly: InternalsVisibleTo("Flow.Launcher")] [assembly: InternalsVisibleTo("Flow.Launcher.Core")] -[assembly: InternalsVisibleTo("Flow.Launcher.Test")] \ No newline at end of file +[assembly: InternalsVisibleTo("Flow.Launcher.Test")] diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 1eb5c9c14..913dc31ae 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -1,98 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { + /// + /// Represents a query that is sent to a plugin. + /// public class Query { - public Query() { } - - /// - /// to allow unit tests for plug ins - /// - public Query(string rawQuery, string search, string[] terms, string actionKeyword = "") - { - Search = search; - RawQuery = rawQuery; - Terms = terms; - ActionKeyword = actionKeyword; - } - /// /// Raw query, this includes action keyword if it has /// We didn't recommend use this property directly. You should always use Search property. /// - public string RawQuery { get; internal set; } + public string RawQuery { get; internal init; } + + /// + /// Determines whether the query was forced to execute again. + /// For example, the value will be true when the user presses Ctrl + R. + /// When this property is true, plugins handling this query should avoid serving cached results. + /// + public bool IsReQuery { get; internal set; } = false; /// /// Search part of a query. /// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as RawQuery. - /// Since we allow user to switch a exclusive plugin to generic plugin, + /// Since we allow user to switch a exclusive plugin to generic plugin, /// so this property will always give you the "real" query part of the query /// - public string Search { get; internal set; } + public string Search { get; internal init; } /// - /// The raw query splited into a string array. + /// The search string split into a string array. + /// Does not include the . /// - public string[] Terms { get; set; } + public string[] SearchTerms { get; init; } /// /// Query can be splited into multiple terms by whitespace /// - public const string TermSeperater = " "; - /// - /// User can set multiple action keywords seperated by ';' - /// - public const string ActionKeywordSeperater = ";"; + public const string TermSeparator = " "; /// - /// '*' is used for System Plugin + /// User can set multiple action keywords seperated by whitespace + /// + public const string ActionKeywordSeparator = TermSeparator; + + /// + /// Wildcard action keyword. Plugins using this value will be queried on every search. /// public const string GlobalPluginWildcardSign = "*"; - public string ActionKeyword { get; set; } + /// + /// The action keyword part of this query. + /// For global plugins this value will be empty. + /// + public string ActionKeyword { get; init; } /// - /// Return first search split by space if it has + /// Splits by spaces and returns the first item. /// + /// + /// returns an empty string when does not have enough items. + /// + [JsonIgnore] public string FirstSearch => SplitSearch(0); - + + [JsonIgnore] + private string _secondToEndSearch; + /// /// strings from second search (including) to last search /// - public string SecondToEndSearch - { - get - { - var index = string.IsNullOrEmpty(ActionKeyword) ? 1 : 2; - return string.Join(TermSeperater, Terms.Skip(index).ToArray()); - } - } + [JsonIgnore] + public string SecondToEndSearch => SearchTerms.Length > 1 ? (_secondToEndSearch ??= string.Join(' ', SearchTerms[1..])) : ""; /// - /// Return second search split by space if it has + /// Splits by spaces and returns the second item. /// + /// + /// returns an empty string when does not have enough items. + /// + [JsonIgnore] public string SecondSearch => SplitSearch(1); /// - /// Return third search split by space if it has + /// Splits by spaces and returns the third item. /// + /// + /// returns an empty string when does not have enough items. + /// + [JsonIgnore] public string ThirdSearch => SplitSearch(2); private string SplitSearch(int index) { - try - { - return string.IsNullOrEmpty(ActionKeyword) ? Terms[index] : Terms[index + 1]; - } - catch (IndexOutOfRangeException) - { - return string.Empty; - } + return index < SearchTerms.Length ? SearchTerms[index] : string.Empty; } + /// public override string ToString() => RawQuery; } } diff --git a/Flow.Launcher.Plugin/README.md b/Flow.Launcher.Plugin/README.md index 5c5b7c3ed..f3091a1fc 100644 --- a/Flow.Launcher.Plugin/README.md +++ b/Flow.Launcher.Plugin/README.md @@ -1,6 +1,7 @@ -What does Flow.Launcher.Plugin do? -==== +Reference this package to develop a plugin for [Flow Launcher](https://github.com/Flow-Launcher/Flow.Launcher). -* Defines base objects and interfaces for plugins -* Plugin authors making C# plugins should reference this DLL via nuget -* Contains commands and models that can be used by plugins +Useful links: + +* [General plugin development guide](https://www.flowlauncher.com/docs/#/plugin-dev) +* [.Net plugin development guide](https://www.flowlauncher.com/docs/#/develop-dotnet-plugins) +* [Package API Reference](https://www.flowlauncher.com/docs/#/API-Reference/Flow.Launcher.Plugin) diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index ac9c10df0..910485438 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,20 +1,25 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; +using System.Windows.Controls; using System.Windows.Media; namespace Flow.Launcher.Plugin { - + /// + /// Describes a result of a executed by a plugin + /// public class Result { - private string _pluginDirectory; - + private string _icoPath; + private string _copyText = string.Empty; + /// - /// Provides the title of the result. This is always required. + /// The title of the result. This is always required. /// public string Title { get; set; } @@ -24,19 +29,49 @@ namespace Flow.Launcher.Plugin public string SubTitle { get; set; } = string.Empty; /// - /// This holds the action keyword that triggered the result. + /// This holds the action keyword that triggered the result. /// If result is triggered by global keyword: *, this should be empty. /// public string ActionKeywordAssigned { get; set; } + /// + /// This holds the text which can be provided by plugin to be copied to the + /// user's clipboard when Ctrl + C is pressed on a result. If the text is a file/directory path + /// flow will copy the actual file/folder instead of just the path text. + /// + public string CopyText + { + get => string.IsNullOrEmpty(_copyText) ? SubTitle : _copyText; + set => _copyText = value; + } + + /// + /// This holds the text which can be provided by plugin to help Flow autocomplete text + /// for user on the plugin result. If autocomplete action for example is tab, pressing tab will have + /// the default constructed autocomplete text (result's Title), or the text provided here if not empty. + /// + /// When a value is not set, the will be used. + public string AutoCompleteText { get; set; } + + /// + /// The image to be displayed for the result. + /// + /// Can be a local file path or a URL. + /// GlyphInfo is prioritized if not null public string IcoPath { get { return _icoPath; } set { - if (!string.IsNullOrEmpty(PluginDirectory) && !Path.IsPathRooted(value)) + // As a standard this property will handle prepping and converting to absolute local path for icon image processing + if (!string.IsNullOrEmpty(value) + && !string.IsNullOrEmpty(PluginDirectory) + && !Path.IsPathRooted(value) + && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { - _icoPath = Path.Combine(value, IcoPath); + _icoPath = Path.Combine(PluginDirectory, value); } else { @@ -45,16 +80,52 @@ namespace Flow.Launcher.Plugin } } + /// + /// Determines if Icon has a border radius + /// + public bool RoundedIcon { get; set; } = false; + + /// + /// Delegate function that produces an + /// + /// public delegate ImageSource IconDelegate(); + /// + /// Delegate to load an icon for this result. + /// public IconDelegate Icon; + /// + /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) + /// + public GlyphInfo Glyph { get; init; } + /// - /// return true to hide flowlauncher after select result + /// An action to take in the form of a function call when the result has been selected. /// + /// + /// The function is invoked with an as the only parameter. + /// Its result determines what happens to Flow Launcher's query form: + /// when true, the form will be hidden; when false, it will stay in focus. + /// public Func Action { get; set; } + /// + /// An async action to take in the form of a function call when the result has been selected. + /// + /// + /// The function is invoked with an as the only parameter and awaited. + /// Its result determines what happens to Flow Launcher's query form: + /// when true, the form will be hidden; when false, it will stay in focus. + /// + public Func> AsyncAction { get; set; } + + /// + /// Priority of the current result + /// + /// default: 0 public int Score { get; set; } /// @@ -63,12 +134,7 @@ namespace Flow.Launcher.Plugin public IList TitleHighlightData { get; set; } /// - /// A list of indexes for the characters to be highlighted in SubTitle - /// - public IList SubTitleHighlightData { get; set; } - - /// - /// Only results that originQuery match with current query will be displayed in the panel + /// Query information associated with the result /// internal Query OriginQuery { get; set; } @@ -81,43 +147,61 @@ namespace Flow.Launcher.Plugin set { _pluginDirectory = value; - if (!string.IsNullOrEmpty(IcoPath) && !Path.IsPathRooted(IcoPath)) - { - IcoPath = Path.Combine(value, IcoPath); - } + + // When the Result object is returned from the query call, PluginDirectory is not provided until + // UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available + // we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded. + IcoPath = _icoPath; } } - public override bool Equals(object obj) - { - var r = obj as Result; - - var equality = string.Equals(r?.Title, Title) && - string.Equals(r?.SubTitle, SubTitle) && - string.Equals(r?.IcoPath, IcoPath) && - TitleHighlightData == r.TitleHighlightData && - SubTitleHighlightData == r.SubTitleHighlightData; - - return equality; - } - - public override int GetHashCode() - { - var hashcode = (Title?.GetHashCode() ?? 0) ^ - (SubTitle?.GetHashCode() ?? 0); - return hashcode; - } - + /// public override string ToString() { - return Title + SubTitle; + return Title + SubTitle + Score; } - public Result() { } + /// + /// Clones the current result + /// + public Result Clone() + { + return new Result + { + Title = Title, + SubTitle = SubTitle, + ActionKeywordAssigned = ActionKeywordAssigned, + CopyText = CopyText, + AutoCompleteText = AutoCompleteText, + IcoPath = IcoPath, + RoundedIcon = RoundedIcon, + Icon = Icon, + Glyph = Glyph, + Action = Action, + AsyncAction = AsyncAction, + Score = Score, + TitleHighlightData = TitleHighlightData, + OriginQuery = OriginQuery, + PluginDirectory = PluginDirectory, + ContextData = ContextData, + PluginID = PluginID, + TitleToolTip = TitleToolTip, + SubTitleToolTip = SubTitleToolTip, + PreviewPanel = PreviewPanel, + ProgressBar = ProgressBar, + ProgressBarColor = ProgressBarColor, + Preview = Preview, + AddSelectedCount = AddSelectedCount, + RecordKey = RecordKey + }; + } /// - /// Additional data associate with this result + /// Additional data associated with this result /// + /// + /// As external information for ContextMenu + /// public object ContextData { get; set; } /// @@ -134,5 +218,100 @@ namespace Flow.Launcher.Plugin /// Show message as ToolTip on result SubTitle hover over /// public string SubTitleToolTip { get; set; } + + /// + /// Customized Preview Panel + /// + public Lazy PreviewPanel { get; set; } + + /// + /// Run this result, asynchronously + /// + /// + /// + public ValueTask ExecuteAsync(ActionContext context) + { + return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false); + } + + /// + /// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result + /// + public int? ProgressBar { get; set; } + + /// + /// Optionally set the color of the progress bar + /// + /// #26a0da (blue) + public string ProgressBarColor { get; set; } = "#26a0da"; + + /// + /// Contains data used to populate the preview section of this result. + /// + public PreviewInfo Preview { get; set; } = PreviewInfo.Default; + + /// + /// Determines if the user selection count should be added to the score. This can be useful when set to false to allow the result sequence order to be the same everytime instead of changing based on selection. + /// + public bool AddSelectedCount { get; set; } = true; + + /// + /// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users. + /// + public const int MaxScore = int.MaxValue; + + /// + /// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records. + /// This can be useful when your plugin will change the Title or SubTitle of the result dynamically. + /// If the plugin does not specific this, FL just uses Title and SubTitle to identify this result. + /// Note: Because old data does not have this key, we should use null as the default value for consistency. + /// + public string RecordKey { get; set; } = null; + + /// + /// Info of the preview section of a + /// + public record PreviewInfo + { + /// + /// Full image used for preview panel + /// + public string PreviewImagePath { get; set; } = null; + + /// + /// Determines if the preview image should occupy the full width of the preview panel. + /// + public bool IsMedia { get; set; } = false; + + /// + /// Result description text that is shown at the bottom of the preview panel. + /// + /// + /// When a value is not set, the will be used. + /// + public string Description { get; set; } = null; + + /// + /// Delegate to get the preview panel's image + /// + public IconDelegate PreviewDelegate { get; set; } = null; + + /// + /// File path of the result. For third-party programs providing external preview. + /// + public string FilePath { get; set; } = null; + + /// + /// Default instance of + /// + public static PreviewInfo Default { get; } = new() + { + PreviewImagePath = null, + Description = null, + IsMedia = false, + PreviewDelegate = null, + FilePath = null, + }; + } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index be33bd86c..6c506cfc0 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -1,23 +1,28 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; +#pragma warning disable IDE0005 using System.Windows; +#pragma warning restore IDE0005 namespace Flow.Launcher.Plugin.SharedCommands { + /// + /// Commands that are useful to run on files... and folders! + /// public static class FilesFolders { private const string FileExplorerProgramName = "explorer"; - private const string FileExplorerProgramEXE = "explorer.exe"; - /// /// Copies the folder and all of its files and folders /// including subfolders to the target location /// /// /// - public static void CopyAll(this string sourcePath, string targetPath) + /// + public static void CopyAll(this string sourcePath, string targetPath, Func messageBoxExShow = null) { // Get the subdirectories for the specified directory. DirectoryInfo dir = new DirectoryInfo(sourcePath); @@ -50,22 +55,31 @@ namespace Flow.Launcher.Plugin.SharedCommands foreach (DirectoryInfo subdir in dirs) { string temppath = Path.Combine(targetPath, subdir.Name); - CopyAll(subdir.FullName, temppath); + CopyAll(subdir.FullName, temppath, messageBoxExShow); } } - catch (Exception e) + catch (Exception) { #if DEBUG - throw e; + throw; #else - MessageBox.Show(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath)); - RemoveFolderIfExists(targetPath); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath)); + RemoveFolderIfExists(targetPath, messageBoxExShow); #endif } } - public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath) + /// + /// Check if the files and directories are identical between + /// and + /// + /// + /// + /// + /// + public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath, Func messageBoxExShow = null) { try { @@ -80,66 +94,139 @@ namespace Flow.Launcher.Plugin.SharedCommands return true; } - catch (Exception e) + catch (Exception) { #if DEBUG - throw e; + throw; #else - MessageBox.Show(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath)); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath)); return false; #endif } } - public static void RemoveFolderIfExists(this string path) + /// + /// Deletes a folder if it exists + /// + /// + /// + public static void RemoveFolderIfExists(this string path, Func messageBoxExShow = null) { try { if (Directory.Exists(path)) Directory.Delete(path, true); } - catch (Exception e) + catch (Exception) { #if DEBUG - throw e; + throw; #else - MessageBox.Show(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path)); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path)); #endif } } + /// + /// Checks if a directory exists + /// + /// + /// public static bool LocationExists(this string path) { return Directory.Exists(path); } + /// + /// Checks if a file exists + /// + /// + /// public static bool FileExists(this string filePath) { return File.Exists(filePath); } - public static void OpenPath(string fileOrFolderPath) + /// + /// Open a directory window (using the OS's default handler, usually explorer) + /// + /// + /// + public static void OpenPath(string fileOrFolderPath, Func messageBoxExShow = null) { - var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' }; + var psi = new ProcessStartInfo + { + FileName = FileExplorerProgramName, + UseShellExecute = true, + Arguments = '"' + fileOrFolderPath + '"' + }; try { if (LocationExists(fileOrFolderPath) || FileExists(fileOrFolderPath)) Process.Start(psi); } - catch (Exception e) + catch (Exception) { #if DEBUG - throw e; + throw; #else - MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath)); + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath)); #endif } } - public static void OpenContainingFolder(string path) + /// + /// Open a file with associated application + /// + /// File path + /// Working directory + /// Open as Administrator + /// + public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false, Func messageBoxExShow = null) { - Process.Start(FileExplorerProgramEXE, $" /select,\"{path}\""); + var psi = new ProcessStartInfo + { + FileName = filePath, + UseShellExecute = true, + WorkingDirectory = workingDir, + Verb = asAdmin ? "runas" : string.Empty + }; + try + { + if (FileExists(filePath)) + Process.Start(psi); + } + catch (Exception) + { +#if DEBUG + throw; +#else + messageBoxExShow ??= MessageBox.Show; + messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", filePath)); +#endif + } + } + + /// + /// This checks whether a given string is a zip file path. + /// By default does not check if the zip file actually exist on disk, can do so by + /// setting checkFileExists = true. + /// + public static bool IsZipFilePath(string querySearchString, bool checkFileExists = false) + { + if (IsLocationPathString(querySearchString) && querySearchString.Split('.').Last() == "zip") + { + if (checkFileExists) + return FileExists(querySearchString); + + return true; + } + + return false; } /// @@ -174,22 +261,16 @@ namespace Flow.Launcher.Plugin.SharedCommands /// public static string GetPreviousExistingDirectory(Func locationExists, string path) { - var previousDirectoryPath = ""; var index = path.LastIndexOf('\\'); if (index > 0 && index < (path.Length - 1)) { - previousDirectoryPath = path.Substring(0, index + 1); - if (!locationExists(previousDirectoryPath)) - { - return ""; - } + string previousDirectoryPath = path[..(index + 1)]; + return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty; } else { - return ""; + return string.Empty; } - - return previousDirectoryPath; } /// @@ -204,10 +285,84 @@ namespace Flow.Launcher.Plugin.SharedCommands // not full path, get previous level directory string var indexOfSeparator = path.LastIndexOf('\\'); - return path.Substring(0, indexOfSeparator + 1); + return path[..(indexOfSeparator + 1)]; } return path; } + + /// + /// Returns if contains . Equal paths are not considered to be contained by default. + /// From https://stackoverflow.com/a/66877016 + /// + /// Parent path + /// Sub path + /// If , when and are equal, returns + /// + public static bool PathContains(string parentPath, string subPath, bool allowEqual = false) + { + var rel = Path.GetRelativePath(parentPath.EnsureTrailingSlash(), subPath); + return (rel != "." || allowEqual) + && rel != ".." + && !rel.StartsWith("../") + && !rel.StartsWith(@"..\") + && !Path.IsPathRooted(rel); + } + + /// + /// Returns path ended with "\" + /// + /// + /// + public static string EnsureTrailingSlash(this string path) + { + return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + } + + /// + /// Validates a directory, creating it if it doesn't exist + /// + /// + public static void ValidateDirectory(string path) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + + /// + /// Validates a data directory, synchronizing it by ensuring all files from a bundled source directory exist in it. + /// If files are missing or outdated, they are copied from the bundled directory to the data directory. + /// + /// + /// + public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory) + { + if (!Directory.Exists(dataDirectory)) + { + Directory.CreateDirectory(dataDirectory); + } + + foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory)) + { + var data = Path.GetFileName(bundledDataPath); + if (data == null) continue; + var dataPath = Path.Combine(dataDirectory, data); + if (!File.Exists(dataPath)) + { + File.Copy(bundledDataPath, dataPath); + } + else + { + var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc; + var time2 = new FileInfo(dataPath).LastWriteTimeUtc; + if (time1 != time2) + { + File.Copy(bundledDataPath, dataPath, true); + } + } + } + } } } diff --git a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs index 95d057707..752c85933 100644 --- a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs +++ b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs @@ -1,4 +1,4 @@ -using Microsoft.Win32; +using Microsoft.Win32; using System; using System.Diagnostics; using System.IO; @@ -6,6 +6,9 @@ using System.Linq; namespace Flow.Launcher.Plugin.SharedCommands { + /// + /// Contains methods to open a search in a new browser window or tab. + /// public static class SearchWeb { private static string GetDefaultBrowserPath() @@ -35,18 +38,21 @@ namespace Flow.Launcher.Plugin.SharedCommands /// Opens search in a new browser. If no browser path is passed in then Chrome is used. /// Leave browser path blank to use Chrome. /// - public static void NewBrowserWindow(this string url, string browserPath = "") + public static void OpenInBrowserWindow(this string url, string browserPath = "", bool inPrivate = false, string privateArg = "") { browserPath = string.IsNullOrEmpty(browserPath) ? GetDefaultBrowserPath() : browserPath; var browserExecutableName = browserPath? - .Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None) - .Last(); + .Split(new[] + { + Path.DirectorySeparatorChar + }, StringSplitOptions.None) + .Last(); var browser = string.IsNullOrEmpty(browserExecutableName) ? "chrome" : browserPath; // Internet Explorer will open url in new browser window, and does not take the --new-window parameter - var browserArguements = browserExecutableName == "iexplore.exe" ? url : "--new-window " + url; + var browserArguements = (browserExecutableName == "iexplore.exe" ? "" : "--new-window ") + (inPrivate ? $"{privateArg} " : "") + url; var psi = new ProcessStartInfo { @@ -57,40 +63,49 @@ namespace Flow.Launcher.Plugin.SharedCommands try { - Process.Start(psi); + Process.Start(psi)?.Dispose(); } catch (System.ComponentModel.Win32Exception) { - Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); + Process.Start(new ProcessStartInfo + { + FileName = url, UseShellExecute = true + }); } } /// /// Opens search as a tab in the default browser chosen in Windows settings. /// - public static void NewTabInBrowser(this string url, string browserPath = "") + public static void OpenInBrowserTab(this string url, string browserPath = "", bool inPrivate = false, string privateArg = "") { browserPath = string.IsNullOrEmpty(browserPath) ? GetDefaultBrowserPath() : browserPath; - var psi = new ProcessStartInfo() { UseShellExecute = true }; + var psi = new ProcessStartInfo() + { + UseShellExecute = true + }; try { if (!string.IsNullOrEmpty(browserPath)) { psi.FileName = browserPath; - psi.Arguments = url; + psi.Arguments = (inPrivate ? $"{privateArg} " : "") + url; } else { psi.FileName = url; } - Process.Start(psi); + Process.Start(psi)?.Dispose(); } // This error may be thrown if browser path is incorrect catch (System.ComponentModel.Win32Exception) { - Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); + Process.Start(new ProcessStartInfo + { + FileName = url, UseShellExecute = true + }); } } } diff --git a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs index c5d43a3d9..288222d4f 100644 --- a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs +++ b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs @@ -1,23 +1,33 @@ using System; -using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; +using System.IO; using System.Threading; -using System.Threading.Tasks; +using Windows.Win32; +using Windows.Win32.Foundation; namespace Flow.Launcher.Plugin.SharedCommands { + /// + /// Contains methods for running shell commands + /// public static class ShellCommand { + /// + /// Delegate for EnumThreadWindows + /// + /// + /// + /// public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam); - [DllImport("user32.dll")] static extern bool EnumThreadWindows(uint threadId, EnumThreadDelegate lpfn, IntPtr lParam); - [DllImport("user32.dll")] static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount); - [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hwnd); private static bool containsSecurityWindow; + /// + /// Runs a windows command using the provided ProcessStartInfo + /// + /// + /// public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo) { processStartInfo.Verb = "RunAsUser"; @@ -29,6 +39,7 @@ namespace Flow.Launcher.Plugin.SharedCommands CheckSecurityWindow(); Thread.Sleep(25); } + while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open { containsSecurityWindow = false; @@ -43,34 +54,75 @@ namespace Flow.Launcher.Plugin.SharedCommands { ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads; for (int i = 0; i < ptc.Count; i++) - EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero); + PInvoke.EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero); } - private static bool CheckSecurityThread(IntPtr hwnd, IntPtr lParam) + private static BOOL CheckSecurityThread(HWND hwnd, LPARAM lParam) { if (GetWindowTitle(hwnd) == "Windows Security") containsSecurityWindow = true; return true; } - private static string GetWindowTitle(IntPtr hwnd) + private static unsafe string GetWindowTitle(HWND hwnd) { - StringBuilder sb = new StringBuilder(GetWindowTextLength(hwnd) + 1); - GetWindowText(hwnd, sb, sb.Capacity); - return sb.ToString(); + var capacity = PInvoke.GetWindowTextLength(hwnd) + 1; + int length; + Span buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity]; + fixed (char* pBuffer = buffer) + { + // If the window has no title bar or text, if the title bar is empty, + // or if the window or control handle is invalid, the return value is zero. + length = PInvoke.GetWindowText(hwnd, pBuffer, capacity); + } + + return buffer[..length].ToString(); } - public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", string arguments = "", string verb = "") + /// + /// Runs a windows command using the provided ProcessStartInfo + /// + /// + /// + /// + /// + /// + /// + public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", + string arguments = "", string verb = "", bool createNoWindow = false) { var info = new ProcessStartInfo { FileName = fileName, WorkingDirectory = workingDirectory, Arguments = arguments, - Verb = verb + Verb = verb, + CreateNoWindow = createNoWindow }; return info; } + + /// + /// Runs a windows command using the provided ProcessStartInfo + /// + /// Thrown when unable to find the file specified in the command + /// Thrown when error occurs during the execution of the command + public static void Execute(ProcessStartInfo info) + { + Execute(Process.Start, info); + } + + /// + /// Runs a windows command using the provided ProcessStartInfo using a custom execute command function + /// + /// allows you to pass in a custom command execution function + /// allows you to pass in the info that will be passed to startProcess + /// Thrown when unable to find the file specified in the command + /// Thrown when error occurs during the execution of the command + public static void Execute(Func startProcess, ProcessStartInfo info) + { + startProcess(info); + } } } diff --git a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs index 5144eb61d..36677d4bb 100644 --- a/Flow.Launcher.Plugin/SharedModels/MatchResult.cs +++ b/Flow.Launcher.Plugin/SharedModels/MatchResult.cs @@ -2,14 +2,29 @@ namespace Flow.Launcher.Plugin.SharedModels { + /// + /// Represents the result of a match operation. + /// public class MatchResult { + /// + /// Initializes a new instance of the class. + /// + /// + /// public MatchResult(bool success, SearchPrecisionScore searchPrecision) { Success = success; SearchPrecision = searchPrecision; } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore) { Success = success; @@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels RawScore = rawScore; } + /// + /// Whether the match operation was successful. + /// public bool Success { get; set; } /// @@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels /// private int _rawScore; + /// + /// The raw calculated search score without any search precision filtering applied. + /// public int RawScore { get { return _rawScore; } @@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels /// public List MatchData { get; set; } + /// + /// The search precision score used to filter the search results. + /// public SearchPrecisionScore SearchPrecision { get; set; } + /// + /// Determines if the search precision score is met. + /// + /// public bool IsSearchPrecisionScoreMet() { return IsSearchPrecisionScoreMet(_rawScore); @@ -63,10 +91,24 @@ namespace Flow.Launcher.Plugin.SharedModels } } + /// + /// Represents the search precision score used to filter search results. + /// public enum SearchPrecisionScore { + /// + /// The highest search precision score. + /// Regular = 50, + + /// + /// The medium search precision score. + /// Low = 20, + + /// + /// The lowest search precision score. + /// None = 0 } } diff --git a/Flow.Launcher.Plugin/SharedModels/ThemeData.cs b/Flow.Launcher.Plugin/SharedModels/ThemeData.cs new file mode 100644 index 000000000..cb389c21f --- /dev/null +++ b/Flow.Launcher.Plugin/SharedModels/ThemeData.cs @@ -0,0 +1,77 @@ +using System; + +namespace Flow.Launcher.Plugin.SharedModels; + +/// +/// Theme data model +/// +public class ThemeData +{ + /// + /// Theme file name without extension + /// + public string FileNameWithoutExtension { get; private init; } + + /// + /// Theme name + /// + public string Name { get; private init; } + + /// + /// Indicates whether the theme supports dark mode + /// + public bool? IsDark { get; private init; } + + /// + /// Indicates whether the theme supports blur effects + /// + public bool? HasBlur { get; private init; } + + /// + /// Theme data constructor + /// + public ThemeData(string fileNameWithoutExtension, string name, bool? isDark = null, bool? hasBlur = null) + { + FileNameWithoutExtension = fileNameWithoutExtension; + Name = name; + IsDark = isDark; + HasBlur = hasBlur; + } + + /// + public static bool operator ==(ThemeData left, ThemeData right) + { + if (left is null && right is null) + return true; + if (left is null || right is null) + return false; + return left.Equals(right); + } + + /// + public static bool operator !=(ThemeData left, ThemeData right) + { + return !(left == right); + } + + /// + public override bool Equals(object obj) + { + if (obj is not ThemeData other) + return false; + return FileNameWithoutExtension == other.FileNameWithoutExtension && + Name == other.Name; + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(FileNameWithoutExtension, Name); + } + + /// + public override string ToString() + { + return Name; + } +} diff --git a/Flow.Launcher.Plugin/UserPlugin.cs b/Flow.Launcher.Plugin/UserPlugin.cs new file mode 100644 index 000000000..74a16b83d --- /dev/null +++ b/Flow.Launcher.Plugin/UserPlugin.cs @@ -0,0 +1,80 @@ +using System; + +namespace Flow.Launcher.Plugin +{ + /// + /// User Plugin Model for Flow Launcher + /// + public record UserPlugin + { + /// + /// Unique identifier of the plugin + /// + public string ID { get; set; } + + /// + /// Name of the plugin + /// + public string Name { get; set; } + + /// + /// Description of the plugin + /// + public string Description { get; set; } + + /// + /// Author of the plugin + /// + public string Author { get; set; } + + /// + /// Version of the plugin + /// + public string Version { get; set; } + + /// + /// Allow language of the plugin + /// + public string Language { get; set; } + + /// + /// Website of the plugin + /// + public string Website { get; set; } + + /// + /// URL to download the plugin + /// + public string UrlDownload { get; set; } + + /// + /// URL to the source code of the plugin + /// + public string UrlSourceCode { get; set; } + + /// + /// Local path where the plugin is installed + /// + public string LocalInstallPath { get; set; } + + /// + /// Icon path of the plugin + /// + public string IcoPath { get; set; } + + /// + /// The date when the plugin was last updated + /// + public DateTime? LatestReleaseDate { get; set; } + + /// + /// The date when the plugin was added to the local system + /// + public DateTime? DateAdded { get; set; } + + /// + /// Indicates whether the plugin is installed from a local path + /// + public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath); + } +} diff --git a/Flow.Launcher.Test/FilesFoldersTest.cs b/Flow.Launcher.Test/FilesFoldersTest.cs new file mode 100644 index 000000000..2621fc2da --- /dev/null +++ b/Flow.Launcher.Test/FilesFoldersTest.cs @@ -0,0 +1,54 @@ +using Flow.Launcher.Plugin.SharedCommands; +using NUnit.Framework; +using NUnit.Framework.Legacy; + +namespace Flow.Launcher.Test +{ + [TestFixture] + + public class FilesFoldersTest + { + // Testcases from https://stackoverflow.com/a/31941905/20703207 + // Disk + [TestCase(@"c:", @"c:\foo", true)] + [TestCase(@"c:\", @"c:\foo", true)] + // Slash + [TestCase(@"c:\foo\bar\", @"c:\foo\", false)] + [TestCase(@"c:\foo\bar", @"c:\foo\", false)] + [TestCase(@"c:\foo", @"c:\foo\bar", true)] + [TestCase(@"c:\foo\", @"c:\foo\bar", true)] + // File + [TestCase(@"c:\foo", @"c:\foo\a.txt", true)] + [TestCase(@"c:\foo", @"c:/foo/a.txt", true)] + [TestCase(@"c:\FOO\a.txt", @"c:\foo", false)] + [TestCase(@"c:\foo\a.txt", @"c:\foo\", false)] + [TestCase(@"c:\foobar\a.txt", @"c:\foo", false)] + [TestCase(@"c:\foobar\a.txt", @"c:\foo\", false)] + [TestCase(@"c:\foo\", @"c:\foo.txt", false)] + // Prefix + [TestCase(@"c:\foo", @"c:\foobar", false)] + [TestCase(@"C:\Program", @"C:\Program Files\", false)] + [TestCase(@"c:\foobar", @"c:\foo\a.txt", false)] + [TestCase(@"c:\foobar\", @"c:\foo\a.txt", false)] + // Edge case + [TestCase(@"c:\foo", @"c:\foo\..\bar\baz", false)] + [TestCase(@"c:\bar", @"c:\foo\..\bar\baz", true)] + [TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)] + public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) + { + ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); + } + + // Equality + [TestCase(@"c:\foo", @"c:\foo", false)] + [TestCase(@"c:\foo\", @"c:\foo", false)] + [TestCase(@"c:\foo", @"c:\foo\", false)] + [TestCase(@"c:\foo", @"c:\foo", true)] + [TestCase(@"c:\foo\", @"c:\foo", true)] + [TestCase(@"c:\foo", @"c:\foo\", true)] + public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) + { + ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult)); + } + } +} diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index 84c9137b7..0241a374e 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -1,7 +1,7 @@ - + - net5.0-windows10.0.19041.0 + net7.0-windows10.0.19041.0 {FF742965-9A80-41A5-B042-D6C7D3A21708} Library Properties @@ -48,13 +48,13 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index bbddcbd2a..090719642 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; @@ -21,6 +22,8 @@ namespace Flow.Launcher.Test private const string MicrosoftSqlServerManagementStudio = "Microsoft SQL Server Management Studio"; private const string VisualStudioCode = "Visual Studio Code"; + private readonly IAlphabet alphabet = null; + public List GetSearchStrings() => new List { @@ -34,7 +37,7 @@ namespace Flow.Launcher.Test OneOneOneOne }; - public List GetPrecisionScores() + public static List GetPrecisionScores() { var listToReturn = new List(); @@ -59,7 +62,7 @@ namespace Flow.Launcher.Test }; var results = new List(); - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); foreach (var str in sources) { results.Add(new Result @@ -71,20 +74,20 @@ namespace Flow.Launcher.Test results = results.Where(x => x.Score > 0).OrderByDescending(x => x.Score).ToList(); - Assert.IsTrue(results.Count == 3); - Assert.IsTrue(results[0].Title == "Inste"); - Assert.IsTrue(results[1].Title == "Install Package"); - Assert.IsTrue(results[2].Title == "file open in browser-test"); + ClassicAssert.IsTrue(results.Count == 3); + ClassicAssert.IsTrue(results[0].Title == "Inste"); + ClassicAssert.IsTrue(results[1].Title == "Install Package"); + ClassicAssert.IsTrue(results[2].Title == "file open in browser-test"); } [TestCase("Chrome")] public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString) { var compareString = "Can have rum only in my glass"; - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore; - Assert.True(scoreResult == 0); + ClassicAssert.True(scoreResult == 0); } [TestCase("chr")] @@ -97,7 +100,7 @@ namespace Flow.Launcher.Test string searchTerm) { var results = new List(); - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); foreach (var str in GetSearchStrings()) { results.Add(new Result @@ -125,27 +128,33 @@ namespace Flow.Launcher.Test Debug.WriteLine("###############################################"); Debug.WriteLine(""); - Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); + ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); } } + + /// + /// These are standard match scenarios + /// The intention of this test is provide a bench mark for how much the score has increased from a change. + /// Usually the increase in scoring should not be drastic, increase of less than 10 is acceptable. + /// [TestCase(Chrome, Chrome, 157)] - [TestCase(Chrome, LastIsChrome, 147)] + [TestCase(Chrome, LastIsChrome, 145)] [TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)] [TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)] [TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)] [TestCase(Chrome, CandyCrushSagaFromKing, 0)] - [TestCase("sql", MicrosoftSqlServerManagementStudio, 110)] - [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended + [TestCase("sql", MicrosoftSqlServerManagementStudio, 109)] + [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 120)] //double spacing intended public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( string queryString, string compareString, int expectedScore) { // When, Given - var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; + var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore; // Should - Assert.AreEqual(expectedScore, rawScore, + ClassicAssert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}"); } @@ -175,7 +184,7 @@ namespace Flow.Launcher.Test bool expectedPrecisionResult) { // When - var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore}; + var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore}; // Given var matchResult = matcher.FuzzyMatch(queryString, compareString); @@ -184,12 +193,12 @@ namespace Flow.Launcher.Test Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); Debug.WriteLine( - $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); + $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); // Should - Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), $"Query: {queryString}{Environment.NewLine} " + $"Compare: {compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + @@ -226,7 +235,7 @@ namespace Flow.Launcher.Test bool expectedPrecisionResult) { // When - var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore}; + var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore}; // Given var matchResult = matcher.FuzzyMatch(queryString, compareString); @@ -235,12 +244,12 @@ namespace Flow.Launcher.Test Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); Debug.WriteLine( - $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); + $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); // Should - Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), $"Query:{queryString}{Environment.NewLine} " + $"Compare:{compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + @@ -254,7 +263,7 @@ namespace Flow.Launcher.Test string queryString, string compareString1, string compareString2) { // When - var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; + var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; // Given var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); @@ -271,11 +280,44 @@ namespace Flow.Launcher.Test Debug.WriteLine(""); // Should - Assert.True(compareString1Result.Score > compareString2Result.Score, + ClassicAssert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + - $"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}"); + $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); + } + + [TestCase("red", "red colour", "metro red")] + [TestCase("red", "this red colour", "this colour red")] + [TestCase("red", "this red colour", "this colour is very red")] + [TestCase("red", "this red colour", "this colour is surprisingly super awesome red and cool")] + [TestCase("red", "this colour is surprisingly super red very and cool", "this colour is surprisingly super very red and cool")] + public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToIndexZero( + string queryString, string compareString1, string compareString2) + { + // When + var matcher = new StringMatcher(alphabet) { UserSettingSearchPrecision = SearchPrecisionScore.Regular }; + + // Given + var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); + var compareString2Result = matcher.FuzzyMatch(queryString, compareString2); + + Debug.WriteLine(""); + Debug.WriteLine("###############################################"); + Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}"); + Debug.WriteLine( + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}"); + Debug.WriteLine( + $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); + Debug.WriteLine("###############################################"); + Debug.WriteLine(""); + + // Should + ClassicAssert.True(compareString1Result.Score > compareString2Result.Score, + $"Query: \"{queryString}\"{Environment.NewLine} " + + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + + $"Should be greater than{Environment.NewLine}" + + $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); } [TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")] @@ -284,7 +326,7 @@ namespace Flow.Launcher.Test string secondName, string secondDescription, string secondExecutableName) { // Act - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore; var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore; var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore; @@ -297,7 +339,7 @@ namespace Flow.Launcher.Test var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max(); // Assert - Assert.IsTrue(firstScore > secondScore, + ClassicAssert.IsTrue(firstScore > secondScore, $"Query: \"{queryString}\"{Environment.NewLine} " + $"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -319,9 +361,9 @@ namespace Flow.Launcher.Test public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString, int desiredScore) { - var matcher = new StringMatcher(); + var matcher = new StringMatcher(alphabet); var score = matcher.FuzzyMatch(queryString, compareString).Score; - Assert.IsTrue(score == desiredScore, + ClassicAssert.IsTrue(score == desiredScore, $@"Query: ""{queryString}"" CompareString: ""{compareString}"" Score: {score} diff --git a/Flow.Launcher.Test/HttpTest.cs b/Flow.Launcher.Test/HttpTest.cs index 637747a07..4f135978a 100644 --- a/Flow.Launcher.Test/HttpTest.cs +++ b/Flow.Launcher.Test/HttpTest.cs @@ -1,7 +1,6 @@ -using NUnit.Framework; +using NUnit.Framework; +using NUnit.Framework.Legacy; using System; -using System.Collections.Generic; -using System.Text; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.Http; @@ -18,16 +17,16 @@ namespace Flow.Launcher.Test proxy.Enabled = true; proxy.Server = "127.0.0.1"; - Assert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}")); - Assert.IsNull(Http.WebProxy.Credentials); + ClassicAssert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}")); + ClassicAssert.IsNull(Http.WebProxy.Credentials); proxy.UserName = "test"; - Assert.NotNull(Http.WebProxy.Credentials); - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName); - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, ""); + ClassicAssert.NotNull(Http.WebProxy.Credentials); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, ""); proxy.Password = "test password"; - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password); } } } diff --git a/Flow.Launcher.Test/PluginLoadTest.cs b/Flow.Launcher.Test/PluginLoadTest.cs new file mode 100644 index 000000000..2cc05f95a --- /dev/null +++ b/Flow.Launcher.Test/PluginLoadTest.cs @@ -0,0 +1,93 @@ +using NUnit.Framework; +using NUnit.Framework.Legacy; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Plugin; +using System.Collections.Generic; +using System.Linq; + +namespace Flow.Launcher.Test +{ + [TestFixture] + class PluginLoadTest + { + [Test] + public void GivenDuplicatePluginMetadatasWhenLoadedThenShouldReturnOnlyUniqueList() + { + // Given + var duplicateList = new List + { + new() + { + ID = "CEA0TYUC6D3B4085823D60DC76F28855", + Version = "1.0.0" + }, + new() + { + ID = "CEA0TYUC6D3B4085823D60DC76F28855", + Version = "1.0.1" + }, + new() + { + ID = "CEA0TYUC6D3B4085823D60DC76F28855", + Version = "1.0.2" + }, + new() + { + ID = "CEA0TYUC6D3B4085823D60DC76F28855", + Version = "1.0.0" + }, + new() + { + ID = "CEA0TYUC6D3B4085823D60DC76F28855", + Version = "1.0.0" + }, + new() + { + ID = "ABC0TYUC6D3B7855823D60DC76F28855", + Version = "1.0.0" + }, + new() + { + ID = "ABC0TYUC6D3B7855823D60DC76F28855", + Version = "1.0.0" + } + }; + + // When + (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); + + // Then + ClassicAssert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); + ClassicAssert.True(unique.Count == 1); + + ClassicAssert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); + ClassicAssert.True(duplicates.Count == 6); + } + + [Test] + public void GivenDuplicatePluginMetadatasWithNoUniquePluginWhenLoadedThenShouldReturnEmptyList() + { + // Given + var duplicateList = new List + { + new() + { + ID = "CEA0TYUC6D3B7855823D60DC76F28855", + Version = "1.0.0" + }, + new() + { + ID = "CEA0TYUC6D3B7855823D60DC76F28855", + Version = "1.0.0" + } + }; + + // When + (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); + + // Then + ClassicAssert.True(unique.Count == 0); + ClassicAssert.True(duplicates.Count == 2); + } + } +} diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 3d0a9a64f..420da266d 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -1,14 +1,15 @@ -using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.Explorer; using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo; using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; +using NUnit.Framework.Legacy; using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using System.IO; +using System.Runtime.Versioning; +using static Flow.Launcher.Plugin.Explorer.Search.SearchManager; namespace Flow.Launcher.Test.Plugins { @@ -19,47 +20,25 @@ namespace Flow.Launcher.Test.Plugins [TestFixture] public class ExplorerTest { - private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken) - { - return new List(); - } - - private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token) - { - return new List - { - new Result - { - Title="Result 1" - }, - - new Result - { - Title="Result 2" - } - }; - } - private bool PreviousLocationExistsReturnsTrue(string dummyString) => true; private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false; + [SupportedOSPlatform("windows7.0")] [TestCase("C:\\SomeFolder\\", "directory='file:C:\\SomeFolder\\'")] public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestrictionsShouldUseDirectoryString(string path, string expectedString) { - // Given - var queryConstructor = new QueryConstructor(new Settings()); - // When var folderPath = path; - var result = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(folderPath); + var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath); // Then - Assert.IsTrue(result == expectedString, + ClassicAssert.IsTrue(result == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual: {result}{Environment.NewLine}"); } + [SupportedOSPlatform("windows7.0")] [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")] [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString) @@ -68,141 +47,87 @@ namespace Flow.Launcher.Test.Plugins var queryConstructor = new QueryConstructor(new Settings()); //When - var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath); + var queryString = queryConstructor.Directory(folderPath); // Then - Assert.IsTrue(queryString == expectedString, + ClassicAssert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), $"Expected string: {expectedString}{Environment.NewLine} " + $"Actual string was: {queryString}{Environment.NewLine}"); } - [TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + - "FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " + - "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" + - " AND directory='file:C:\\SomeFolder' ORDER BY System.FileName")] + [SupportedOSPlatform("windows7.0")] + [TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" + + " FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" + + " AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" + + " ORDER BY System.FileName")] public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString( - string userSearchString, string expectedString) + string folderPath, string userSearchString, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); //When - var queryString = queryConstructor.QueryForTopLevelDirectorySearch(userSearchString); + var queryString = queryConstructor.Directory(folderPath, userSearchString); // Then - Assert.IsTrue(queryString == expectedString, - $"Expected string: {expectedString}{Environment.NewLine} " + - $"Actual string was: {queryString}{Environment.NewLine}"); - } - - [TestCase("C:\\SomeFolder\\SomeApp", "(System.FileName LIKE 'SomeApp%' " + - "OR CONTAINS(System.FileName,'\"SomeApp*\"',1033))" + - " AND directory='file:C:\\SomeFolder'")] - public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryWhereRestrictionsShouldUseDirectoryString( - string userSearchString, string expectedString) - { - // Given - var queryConstructor = new QueryConstructor(new Settings()); - - //When - var queryString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(userSearchString); - - // Then - Assert.IsTrue(queryString == expectedString, - $"Expected string: {expectedString}{Environment.NewLine} " + - $"Actual string was: {queryString}{Environment.NewLine}"); + ClassicAssert.AreEqual(expectedString, queryString); } + [SupportedOSPlatform("windows7.0")] [TestCase("scope='file:'")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) { //When - var resultString = QueryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch; + const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch; // Then - Assert.IsTrue(resultString == expectedString, - $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + - $"Actual string was: {resultString}{Environment.NewLine}"); + ClassicAssert.AreEqual(expectedString, resultString); } + [SupportedOSPlatform("windows7.0")] [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " + - "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + - "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] + "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] + [TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { // Given var queryConstructor = new QueryConstructor(new Settings()); + var baseQuery = queryConstructor.CreateBaseQuery(); + + // system running this test could have different locale than the hard-coded 1033 LCID en-US. + var queryKeywordLocale = baseQuery.QueryKeywordLocale; + expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString()); //When - var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString); + var resultString = queryConstructor.FilesAndFolders(userSearchString); // Then - Assert.IsTrue(resultString == expectedString, - $"Expected query string: {expectedString}{Environment.NewLine} " + - $"Actual string was: {resultString}{Environment.NewLine}"); + ClassicAssert.AreEqual(expectedString, resultString); } - [TestCase] - public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch() - { - // Given - var searchManager = new SearchManager(new Settings(), new PluginInitContext()); - - // When - var results = await searchManager.TopLevelDirectorySearchBehaviourAsync( - MethodWindowsIndexSearchReturnsZeroResultsAsync, - MethodDirectoryInfoClassSearchReturnsTwoResults, - false, - new Query(), - "string not used", - default); - - // Then - Assert.IsTrue(results.Count == 2, - $"Expected to have 2 results from DirectoryInfoClassSearch {Environment.NewLine} " + - $"Actual number of results is {results.Count} {Environment.NewLine}"); - } - - [TestCase] - public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch() - { - // Given - var searchManager = new SearchManager(new Settings(), new PluginInitContext()); - - // When - var results = await searchManager.TopLevelDirectorySearchBehaviourAsync( - MethodWindowsIndexSearchReturnsZeroResultsAsync, - MethodDirectoryInfoClassSearchReturnsTwoResults, - true, - new Query(), - "string not used", - default); - - // Then - Assert.IsTrue(results.Count == 0, - $"Expected to have 0 results because location is indexed {Environment.NewLine} " + - $"Actual number of results is {results.Count} {Environment.NewLine}"); - } + [SupportedOSPlatform("windows7.0")] [TestCase(@"some words", @"FREETEXT('some words')")] public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString( string querySearchString, string expectedString) { // Given - var queryConstructor = new QueryConstructor(new Settings()); + _ = new QueryConstructor(new Settings()); //When - var resultString = queryConstructor.QueryWhereRestrictionsForFileContentSearch(querySearchString); + var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString); // Then - Assert.IsTrue(resultString == expectedString, + ClassicAssert.IsTrue(resultString == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } + [SupportedOSPlatform("windows7.0")] [TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + - "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] + "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -210,18 +135,21 @@ namespace Flow.Launcher.Test.Plugins var queryConstructor = new QueryConstructor(new Settings()); //When - var resultString = queryConstructor.QueryForFileContentSearch(userSearchString); + var resultString = queryConstructor.FileContent(userSearchString); // Then - Assert.IsTrue(resultString == expectedString, + ClassicAssert.IsTrue(resultString == expectedString, $"Expected query string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } - public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue() + public static void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue() { // Given - var query = new Query { ActionKeyword = "doc:", Search = "search term" }; + var query = new Query + { + ActionKeyword = "doc:", Search = "search term" + }; var searchManager = new SearchManager(new Settings(), new PluginInitContext()); @@ -229,7 +157,7 @@ namespace Flow.Launcher.Test.Plugins var result = searchManager.IsFileContentSearch(query.ActionKeyword); // Then - Assert.IsTrue(result, + ClassicAssert.IsTrue(result, $"Expected True for file content search. {Environment.NewLine} " + $"Actual result was: {result}{Environment.NewLine}"); } @@ -239,17 +167,22 @@ namespace Flow.Launcher.Test.Plugins [TestCase(@"\c:\", false)] [TestCase(@"cc:\", false)] [TestCase(@"\\\SomeNetworkLocation\", false)] + [TestCase(@"\\SomeNetworkLocation\", true)] [TestCase("RandomFile", false)] [TestCase(@"c:\>*", true)] [TestCase(@"c:\>", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] + [TestCase(@"c:\SomeLocation\SomeOtherLocation", true)] + [TestCase(@"c:\SomeLocation\SomeOtherLocation\SomeFile.exe", true)] + [TestCase(@"\\SomeNetworkLocation\SomeFile.exe", true)] + public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) { // When, Given var result = FilesFolders.IsLocationPathString(querySearchString); //Then - Assert.IsTrue(result == expectedResult, + ClassicAssert.IsTrue(result == expectedResult, $"Expected query search string check result is: {expectedResult} {Environment.NewLine} " + $"Actual check result is {result} {Environment.NewLine}"); @@ -276,7 +209,7 @@ namespace Flow.Launcher.Test.Plugins var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path); //Then - Assert.IsTrue(previousDirectoryPath == expectedString, + ClassicAssert.IsTrue(previousDirectoryPath == expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {previousDirectoryPath} {Environment.NewLine}"); } @@ -289,29 +222,24 @@ namespace Flow.Launcher.Test.Plugins var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); //Then - Assert.IsTrue(returnedPath == expectedString, + ClassicAssert.IsTrue(returnedPath == expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {returnedPath} {Environment.NewLine}"); } - [TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")] - [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " - + "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " - + "scope='file:c:\\SomeFolder'")] - public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) + [SupportedOSPlatform("windows7.0")] + [TestCase("c:\\SomeFolder", "scope='file:c:\\SomeFolder'")] + [TestCase("c:\\OtherFolder", "scope='file:c:\\OtherFolder'")] + public void GivenFilePath_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) { - // Given - var queryConstructor = new QueryConstructor(new Settings()); - //When - var resultString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path); + var resultString = QueryConstructor.RecursiveDirectoryConstraint(path); // Then - Assert.IsTrue(resultString == expectedString, - $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + - $"Actual string was: {resultString}{Environment.NewLine}"); + ClassicAssert.AreEqual(expectedString, resultString); } + [SupportedOSPlatform("windows7.0")] [TestCase("c:\\somefolder\\>somefile", "*somefile*")] [TestCase("c:\\somefolder\\somefile", "somefile*")] [TestCase("c:\\somefolder\\", "*")] @@ -322,9 +250,194 @@ namespace Flow.Launcher.Test.Plugins var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path); // Then - Assert.IsTrue(resultString == expectedString, - $"Expected criteria string: {expectedString}{Environment.NewLine} " + - $"Actual criteria string was: {resultString}{Environment.NewLine}"); + ClassicAssert.AreEqual(expectedString, resultString); + } + + [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")] + [TestCase("c:\\somefolder\\someotherfolder\\", ResultType.Folder, "irrelevant", true, true, "c:\\somefolder\\someotherfolder\\")] + [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", true, false, "p c:\\somefolder\\someotherfolder\\")] + [TestCase("c:\\somefolder\\someotherfolder\\", ResultType.Folder, "irrelevant", false, false, "c:\\somefolder\\someotherfolder\\")] + [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "p", true, false, "p c:\\somefolder\\someotherfolder\\")] + [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "", true, true, "c:\\somefolder\\someotherfolder\\")] + public void GivenFolderResult_WhenGetPath_ThenPathShouldBeExpectedString( + string path, + ResultType type, + string actionKeyword, + bool pathSearchKeywordEnabled, + bool searchActionKeywordEnabled, + string expectedResult) + { + // Given + var settings = new Settings() + { + PathSearchKeywordEnabled = pathSearchKeywordEnabled, + PathSearchActionKeyword = "p", + SearchActionKeywordEnabled = searchActionKeywordEnabled, + SearchActionKeyword = Query.GlobalPluginWildcardSign + }; + ResultManager.Init(new PluginInitContext(), settings); + + // When + var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); + + // Then + ClassicAssert.AreEqual(result, expectedResult); + } + + [TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")] + [TestCase("c:\\somefolder\\somefile", ResultType.File, "p", true, false, "p c:\\somefolder\\somefile")] + [TestCase("c:\\somefolder\\somefile", ResultType.File, "e", true, true, "e c:\\somefolder\\somefile")] + [TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, false, "e c:\\somefolder\\somefile")] + public void GivenFileResult_WhenGetPath_ThenPathShouldBeExpectedString( + string path, + ResultType type, + string actionKeyword, + bool pathSearchKeywordEnabled, + bool searchActionKeywordEnabled, + string expectedResult) + { + // Given + var settings = new Settings() + { + PathSearchKeywordEnabled = pathSearchKeywordEnabled, + PathSearchActionKeyword = "p", + SearchActionKeywordEnabled = searchActionKeywordEnabled, + SearchActionKeyword = "e" + }; + ResultManager.Init(new PluginInitContext(), settings); + + // When + var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); + + // Then + ClassicAssert.AreEqual(result, expectedResult); + } + + [TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")] + [TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "i", true, false, "p c:\\somefolder\\")] + [TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "irrelevant", true, true, "c:\\somefolder\\")] + public void GivenQueryWithFolderTypeResult_WhenGetAutoComplete_ThenResultShouldBeExpectedString( + string title, + string path, + ResultType resultType, + string actionKeyword, + bool pathSearchKeywordEnabled, + bool searchActionKeywordEnabled, + string expectedResult) + { + // Given + var query = new Query() { ActionKeyword = actionKeyword }; + var settings = new Settings() + { + PathSearchKeywordEnabled = pathSearchKeywordEnabled, + PathSearchActionKeyword = "p", + SearchActionKeywordEnabled = searchActionKeywordEnabled, + SearchActionKeyword = Query.GlobalPluginWildcardSign, + QuickAccessActionKeyword = "q", + IndexSearchActionKeyword = "i" + }; + ResultManager.Init(new PluginInitContext(), settings); + + // When + var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); + + // Then + ClassicAssert.AreEqual(result, expectedResult); + } + + [TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")] + [TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "i", true, false, "p c:\\somefolder\\somefile")] + [TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "irrelevant", true, true, "c:\\somefolder\\somefile")] + public void GivenQueryWithFileTypeResult_WhenGetAutoComplete_ThenResultShouldBeExpectedString( + string title, + string path, + ResultType resultType, + string actionKeyword, + bool pathSearchKeywordEnabled, + bool searchActionKeywordEnabled, + string expectedResult) + { + // Given + var query = new Query() { ActionKeyword = actionKeyword }; + var settings = new Settings() + { + QuickAccessActionKeyword = "q", + IndexSearchActionKeyword = "i", + PathSearchActionKeyword = "p", + PathSearchKeywordEnabled = pathSearchKeywordEnabled, + SearchActionKeywordEnabled = searchActionKeywordEnabled, + SearchActionKeyword = Query.GlobalPluginWildcardSign + }; + ResultManager.Init(new PluginInitContext(), settings); + + // When + var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); + + // Then + ClassicAssert.AreEqual(result, expectedResult); + } + + [TestCase(@"c:\foo", @"c:\foo", true)] + [TestCase(@"C:\Foo\", @"c:\foo\", true)] + [TestCase(@"c:\foo", @"c:\foo\", false)] + public void GivenTwoPaths_WhenCompared_ThenShouldBeExpectedSameOrDifferent(string path1, string path2, bool expectedResult) + { + // Given + var comparator = PathEqualityComparator.Instance; + var result1 = new Result + { + Title = Path.GetFileName(path1), + SubTitle = path1 + }; + var result2 = new Result + { + Title = Path.GetFileName(path2), + SubTitle = path2 + }; + + // When, Then + ClassicAssert.AreEqual(expectedResult, comparator.Equals(result1, result2)); + } + + [TestCase(@"c:\foo\", @"c:\foo\")] + [TestCase(@"C:\Foo\", @"c:\foo\")] + public void GivenTwoPaths_WhenComparedHasCode_ThenShouldBeSame(string path1, string path2) + { + // Given + var comparator = PathEqualityComparator.Instance; + var result1 = new Result + { + Title = Path.GetFileName(path1), + SubTitle = path1 + }; + var result2 = new Result + { + Title = Path.GetFileName(path2), + SubTitle = path2 + }; + + var hash1 = comparator.GetHashCode(result1); + var hash2 = comparator.GetHashCode(result2); + + // When, Then + ClassicAssert.IsTrue(hash1 == hash2); + } + + [TestCase(@"%appdata%", true)] + [TestCase(@"%appdata%\123", true)] + [TestCase(@"c:\foo %appdata%\", false)] + [TestCase(@"c:\users\%USERNAME%\downloads", true)] + [TestCase(@"c:\downloads", false)] + [TestCase(@"%", false)] + [TestCase(@"%%", false)] + [TestCase(@"%bla%blabla%", false)] + public void GivenPath_WhenHavingEnvironmentVariableOrNot_ThenShouldBeExpected(string path, bool expectedResult) + { + // When + var result = EnvironmentVariables.HasEnvironmentVar(path); + + // Then + ClassicAssert.AreEqual(result, expectedResult); } } } diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index 2b9512692..497f874e7 100644 --- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -1,13 +1,11 @@ -using NUnit; -using NUnit.Framework; +using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; using System.Threading.Tasks; using System.IO; using System.Threading; using System.Text; -using System.Text.Json; -using System.Linq; using System.Collections.Generic; namespace Flow.Launcher.Test.Plugins @@ -16,21 +14,14 @@ namespace Flow.Launcher.Test.Plugins // ReSharper disable once InconsistentNaming internal class JsonRPCPluginTest : JsonRPCPlugin { - public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable; - - protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) + protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { throw new System.NotImplementedException(); } - protected override string ExecuteContextMenu(Result selectedResult) + protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - throw new System.NotImplementedException(); - } - - protected override Task ExecuteQueryAsync(Query query, CancellationToken token) - { - var byteInfo = Encoding.UTF8.GetBytes(query.RawQuery); + var byteInfo = Encoding.UTF8.GetBytes(request.Parameters[0] as string ?? string.Empty); var resultStream = new MemoryStream(byteInfo); return Task.FromResult((Stream)resultStream); @@ -45,61 +36,30 @@ namespace Flow.Launcher.Test.Plugins { var results = await QueryAsync(new Query { - RawQuery = resultText + Search = resultText }, default); - Assert.IsNotNull(results); + ClassicAssert.IsNotNull(results); foreach (var result in results) { - Assert.IsNotNull(result); - Assert.IsNotNull(result.Action); - Assert.IsNotNull(result.Title); + ClassicAssert.IsNotNull(result); + ClassicAssert.IsNotNull(result.AsyncAction); + ClassicAssert.IsNotNull(result.Title); } } public static List ResponseModelsSource = new() { - new() + new JsonRPCQueryResponseModel(0, new List()), + new JsonRPCQueryResponseModel(0, new List { - Result = new() - }, - new() - { - Result = new() + new() { - new JsonRPCResult - { - Title = "Test1", - SubTitle = "Test2" - } + Title = "Test1", SubTitle = "Test2" } - } + }) }; - - [TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))] - public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference) - { - var camelText = JsonSerializer.Serialize(reference, new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - - var pascalText = JsonSerializer.Serialize(reference); - - var results1 = await QueryAsync(new Query { RawQuery = camelText }, default); - var results2 = await QueryAsync(new Query { RawQuery = pascalText }, default); - - Assert.IsNotNull(results1); - Assert.IsNotNull(results2); - - foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result)) - { - Assert.AreEqual(result1, result2); - Assert.AreEqual(result1, referenceResult); - - Assert.IsNotNull(result1); - Assert.IsNotNull(result1.Action); - } - } - } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Test/Plugins/PluginInitTest.cs b/Flow.Launcher.Test/Plugins/PluginInitTest.cs deleted file mode 100644 index 299a837ee..000000000 --- a/Flow.Launcher.Test/Plugins/PluginInitTest.cs +++ /dev/null @@ -1,17 +0,0 @@ -using NUnit.Framework; -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Infrastructure.Exception; - -namespace Flow.Launcher.Test.Plugins -{ - - [TestFixture] - public class PluginInitTest - { - [Test] - public void PublicAPIIsNullTest() - { - //Assert.Throws(typeof(Flow.LauncherFatalException), () => PluginManager.Initialize(null)); - } - } -} diff --git a/Flow.Launcher.Test/Plugins/ProgramTest.cs b/Flow.Launcher.Test/Plugins/ProgramTest.cs deleted file mode 100644 index a0d2243ce..000000000 --- a/Flow.Launcher.Test/Plugins/ProgramTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Flow.Launcher.Plugin.Program.Programs; -using NUnit.Framework; -using System; -using Windows.ApplicationModel; - -namespace Flow.Launcher.Test.Plugins -{ - [TestFixture] - public class ProgramTest - { - [TestCase("Microsoft.WindowsCamera", "ms-resource:LensSDK/Resources/AppTitle", "ms-resource://Microsoft.WindowsCamera/LensSDK/Resources/AppTitle")] - [TestCase("microsoft.windowscommunicationsapps", "ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName", - "ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName")] - [TestCase("windows.immersivecontrolpanel", "ms-resource:DisplayName", "ms-resource://windows.immersivecontrolpanel/Resources/DisplayName")] - [TestCase("Microsoft.MSPaint", "ms-resource:AppName", "ms-resource://Microsoft.MSPaint/Resources/AppName")] - public void WhenGivenPriReferenceValueShouldReturnCorrectFormat(string packageName, string rawPriReferenceValue, string expectedFormat) - { - // Arrange - var app = new UWP.Application(); - - // Act - var result = app.FormattedPriReferenceValue(packageName, rawPriReferenceValue); - - // Assert - Assert.IsTrue(result == expectedFormat, - $"Expected Pri reference format: {expectedFormat}{Environment.NewLine} " + - $"Actual: {result}{Environment.NewLine}"); - } - } -} diff --git a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs index 7ccac5bd5..0dd1fe489 100644 --- a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs @@ -1,7 +1,8 @@ using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Plugin.Url; -namespace Flow.Launcher.Test +namespace Flow.Launcher.Test.Plugins { [TestFixture] public class UrlPluginTest @@ -10,23 +11,23 @@ namespace Flow.Launcher.Test public void URLMatchTest() { var plugin = new Main(); - Assert.IsTrue(plugin.IsURL("http://www.google.com")); - Assert.IsTrue(plugin.IsURL("https://www.google.com")); - Assert.IsTrue(plugin.IsURL("http://google.com")); - Assert.IsTrue(plugin.IsURL("www.google.com")); - Assert.IsTrue(plugin.IsURL("google.com")); - Assert.IsTrue(plugin.IsURL("http://localhost")); - Assert.IsTrue(plugin.IsURL("https://localhost")); - Assert.IsTrue(plugin.IsURL("http://localhost:80")); - Assert.IsTrue(plugin.IsURL("https://localhost:80")); - Assert.IsTrue(plugin.IsURL("http://110.10.10.10")); - Assert.IsTrue(plugin.IsURL("110.10.10.10")); - Assert.IsTrue(plugin.IsURL("ftp://110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("http://www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("https://www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("http://google.com")); + ClassicAssert.IsTrue(plugin.IsURL("www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("google.com")); + ClassicAssert.IsTrue(plugin.IsURL("http://localhost")); + ClassicAssert.IsTrue(plugin.IsURL("https://localhost")); + ClassicAssert.IsTrue(plugin.IsURL("http://localhost:80")); + ClassicAssert.IsTrue(plugin.IsURL("https://localhost:80")); + ClassicAssert.IsTrue(plugin.IsURL("http://110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("ftp://110.10.10.10")); - Assert.IsFalse(plugin.IsURL("wwww")); - Assert.IsFalse(plugin.IsURL("wwww.c")); - Assert.IsFalse(plugin.IsURL("wwww.c")); + ClassicAssert.IsFalse(plugin.IsURL("wwww")); + ClassicAssert.IsFalse(plugin.IsURL("wwww.c")); + ClassicAssert.IsFalse(plugin.IsURL("wwww.c")); } } } diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index 6090ecc65..c8ac17748 100644 --- a/Flow.Launcher.Test/QueryBuilderTest.cs +++ b/Flow.Launcher.Test/QueryBuilderTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; @@ -15,10 +16,19 @@ namespace Flow.Launcher.Test {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}}}} }; - Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins); + Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); - Assert.AreEqual("file.txt file2 file3", q.Search); - Assert.AreEqual(">", q.ActionKeyword); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); + ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); + ClassicAssert.AreEqual(">", q.ActionKeyword); + + ClassicAssert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match."); + + ClassicAssert.AreEqual("ping", q.FirstSearch); + ClassicAssert.AreEqual("google.com", q.SecondSearch); + ClassicAssert.AreEqual("-n", q.ThirdSearch); + + ClassicAssert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); } [Test] @@ -29,9 +39,13 @@ namespace Flow.Launcher.Test {">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List {">"}, Disabled = true}}} }; - Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins); + Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); - Assert.AreEqual("> file.txt file2 file3", q.Search); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search); + ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); + ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); + ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); + ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); } [Test] @@ -39,13 +53,13 @@ namespace Flow.Launcher.Test { Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary()); - Assert.AreEqual("file.txt file2 file3", q.Search); - Assert.AreEqual("", q.ActionKeyword); + ClassicAssert.AreEqual("file.txt file2 file3", q.Search); + ClassicAssert.AreEqual("", q.ActionKeyword); - Assert.AreEqual("file.txt", q.FirstSearch); - Assert.AreEqual("file2", q.SecondSearch); - Assert.AreEqual("file3", q.ThirdSearch); - Assert.AreEqual("file2 file3", q.SecondToEndSearch); + ClassicAssert.AreEqual("file.txt", q.FirstSearch); + ClassicAssert.AreEqual("file2", q.SecondSearch); + ClassicAssert.AreEqual("file3", q.ThirdSearch); + ClassicAssert.AreEqual("file2 file3", q.SecondToEndSearch); } } } diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln index 21c3b47dc..e44b23232 100644 --- a/Flow.Launcher.sln +++ b/Flow.Launcher.sln @@ -1,19 +1,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29806.167 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Test", "Flow.Launcher.Test\Flow.Launcher.Test.csproj", "{FF742965-9A80-41A5-B042-D6C7D3A21708}" - ProjectSection(ProjectDependencies) = postProject - {DB90F671-D861-46BB-93A3-F1304F5BA1C5} = {DB90F671-D861-46BB-93A3-F1304F5BA1C5} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin", "Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj", "{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher", "Flow.Launcher\Flow.Launcher.csproj", "{DB90F671-D861-46BB-93A3-F1304F5BA1C5}" ProjectSection(ProjectDependencies) = postProject - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0} = {1EE20B48-82FB-48A2-8086-675D6DDAB4F0} {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} = {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} {4792A74A-0CEA-4173-A8B2-30E6764C6217} = {4792A74A-0CEA-4173-A8B2-30E6764C6217} {FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {FDB3555B-58EF-4AE6-B5F1-904719637AB4} @@ -23,10 +13,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher", "Flow.Launc {9B130CC5-14FB-41FF-B310-0A95B6894C37} = {9B130CC5-14FB-41FF-B310-0A95B6894C37} {FDED22C8-B637-42E8-824A-63B5B6E05A3A} = {FDED22C8-B637-42E8-824A-63B5B6E05A3A} {A3DCCBCA-ACC1-421D-B16E-210896234C26} = {A3DCCBCA-ACC1-421D-B16E-210896234C26} + {5043CECE-E6A7-4867-9CBE-02D27D83747A} = {5043CECE-E6A7-4867-9CBE-02D27D83747A} {403B57F2-1856-4FC7-8A24-36AB346B763E} = {403B57F2-1856-4FC7-8A24-36AB346B763E} {588088F4-3262-4F9F-9663-A05DE12534C3} = {588088F4-3262-4F9F-9663-A05DE12534C3} EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Test", "Flow.Launcher.Test\Flow.Launcher.Test.csproj", "{FF742965-9A80-41A5-B042-D6C7D3A21708}" + ProjectSection(ProjectDependencies) = postProject + {DB90F671-D861-46BB-93A3-F1304F5BA1C5} = {DB90F671-D861-46BB-93A3-F1304F5BA1C5} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin", "Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj", "{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Infrastructure", "Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj", "{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Core", "Flow.Launcher.Core\Flow.Launcher.Core.csproj", "{B749F0DB-8E75-47DB-9E5E-265D16D0C0D2}" @@ -35,8 +35,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Progra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.WebSearch", "Plugins\Flow.Launcher.Plugin.WebSearch\Flow.Launcher.Plugin.WebSearch.csproj", "{403B57F2-1856-4FC7-8A24-36AB346B763E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.ControlPanel", "Plugins\Flow.Launcher.Plugin.ControlPanel\Flow.Launcher.Plugin.ControlPanel.csproj", "{1EE20B48-82FB-48A2-8086-675D6DDAB4F0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.PluginIndicator", "Plugins\Flow.Launcher.Plugin.PluginIndicator\Flow.Launcher.Plugin.PluginIndicator.csproj", "{FDED22C8-B637-42E8-824A-63B5B6E05A3A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Sys", "Plugins\Flow.Launcher.Plugin.Sys\Flow.Launcher.Plugin.Sys.csproj", "{0B9DE348-9361-4940-ADB6-F5953BFFCCEC}" @@ -45,15 +43,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Url", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FFD651C7-0546-441F-BC8C-D4EE8FD01EA7}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .gitattributes = .gitattributes .gitignore = .gitignore appveyor.yml = appveyor.yml + Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets Scripts\flowlauncher.nuspec = Scripts\flowlauncher.nuspec LICENSE = LICENSE Scripts\post_build.ps1 = Scripts\post_build.ps1 README.md = README.md SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs + Settings.XamlStyler = Settings.XamlStyler EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Shell", "Plugins\Flow.Launcher.Plugin.Shell\Flow.Launcher.Plugin.Shell.csproj", "{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}" @@ -68,6 +69,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Proces EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.PluginsManager", "Plugins\Flow.Launcher.Plugin.PluginsManager\Flow.Launcher.Plugin.PluginsManager.csproj", "{4792A74A-0CEA-4173-A8B2-30E6764C6217}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.WindowsSettings", "Plugins\Flow.Launcher.Plugin.WindowsSettings\Flow.Launcher.Plugin.WindowsSettings.csproj", "{5043CECE-E6A7-4867-9CBE-02D27D83747A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,7 +82,7 @@ Global EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -162,18 +165,6 @@ Global {403B57F2-1856-4FC7-8A24-36AB346B763E}.Release|x64.Build.0 = Release|Any CPU {403B57F2-1856-4FC7-8A24-36AB346B763E}.Release|x86.ActiveCfg = Release|Any CPU {403B57F2-1856-4FC7-8A24-36AB346B763E}.Release|x86.Build.0 = Release|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Debug|x64.ActiveCfg = Debug|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Debug|x64.Build.0 = Debug|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Debug|x86.ActiveCfg = Debug|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Debug|x86.Build.0 = Debug|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|Any CPU.Build.0 = Release|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x64.ActiveCfg = Release|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x64.Build.0 = Release|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x86.ActiveCfg = Release|Any CPU - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x86.Build.0 = Release|Any CPU {FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU {FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -283,6 +274,18 @@ Global {4792A74A-0CEA-4173-A8B2-30E6764C6217}.Release|x64.Build.0 = Release|Any CPU {4792A74A-0CEA-4173-A8B2-30E6764C6217}.Release|x86.ActiveCfg = Release|Any CPU {4792A74A-0CEA-4173-A8B2-30E6764C6217}.Release|x86.Build.0 = Release|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x64.ActiveCfg = Debug|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x64.Build.0 = Debug|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x86.ActiveCfg = Debug|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x86.Build.0 = Debug|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|Any CPU.Build.0 = Release|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.ActiveCfg = Release|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.Build.0 = Release|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.ActiveCfg = Release|Any CPU + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -290,7 +293,6 @@ Global GlobalSection(NestedProjects) = preSolution {FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {403B57F2-1856-4FC7-8A24-36AB346B763E} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} - {1EE20B48-82FB-48A2-8086-675D6DDAB4F0} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {FDED22C8-B637-42E8-824A-63B5B6E05A3A} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {A3DCCBCA-ACC1-421D-B16E-210896234C26} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} @@ -300,6 +302,7 @@ Global {F9C4C081-4CC3-4146-95F1-E102B4E10A5F} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {588088F4-3262-4F9F-9663-A05DE12534C3} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {4792A74A-0CEA-4173-A8B2-30E6764C6217} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} + {5043CECE-E6A7-4867-9CBE-02D27D83747A} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F26ACB50-3F6C-4907-B0C9-1ADACC1D0DED} diff --git a/Flow.Launcher/ActionKeywords.xaml b/Flow.Launcher/ActionKeywords.xaml index 9d032efd9..740b0d402 100644 --- a/Flow.Launcher/ActionKeywords.xaml +++ b/Flow.Launcher/ActionKeywords.xaml @@ -1,43 +1,136 @@ - + + + + - - - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - + + + \ No newline at end of file diff --git a/Flow.Launcher/ActionKeywords.xaml.cs b/Flow.Launcher/ActionKeywords.xaml.cs index 4a2368347..ad24f2a76 100644 --- a/Flow.Launcher/ActionKeywords.xaml.cs +++ b/Flow.Launcher/ActionKeywords.xaml.cs @@ -1,36 +1,26 @@ using System.Windows; -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; -using Flow.Launcher.Infrastructure.Exception; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; +using System.Linq; +using System.Collections.Generic; namespace Flow.Launcher { - public partial class ActionKeywords : Window + public partial class ActionKeywords { - private readonly PluginPair plugin; - private Settings settings; - private readonly Internationalization translater = InternationalizationManager.Instance; - private readonly PluginViewModel pluginViewModel; + private readonly PluginPair _plugin; + private readonly PluginViewModel _pluginViewModel; - public ActionKeywords(string pluginId, Settings settings, PluginViewModel pluginViewModel) + public ActionKeywords(PluginViewModel pluginViewModel) { InitializeComponent(); - plugin = PluginManager.GetPluginForId(pluginId); - this.settings = settings; - this.pluginViewModel = pluginViewModel; - if (plugin == null) - { - MessageBox.Show(translater.GetTranslation("cannotFindSpecifiedPlugin")); - Close(); - } + _plugin = pluginViewModel.PluginPair; + _pluginViewModel = pluginViewModel; } private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e) { - tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeperater, plugin.Metadata.ActionKeywords.ToArray()); + tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, _plugin.Metadata.ActionKeywords.ToArray()); tbAction.Focus(); } @@ -41,19 +31,57 @@ namespace Flow.Launcher private void btnDone_OnClick(object sender, RoutedEventArgs _) { - var oldActionKeyword = plugin.Metadata.ActionKeywords[0]; - var newActionKeyword = tbAction.Text.Trim(); - newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*"; - if (!pluginViewModel.IsActionKeywordRegistered(newActionKeyword)) + var oldActionKeywords = _plugin.Metadata.ActionKeywords; + + var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList(); + newActionKeywords.RemoveAll(string.IsNullOrEmpty); + newActionKeywords = newActionKeywords.Distinct().ToList(); + + newActionKeywords = newActionKeywords.Count > 0 ? newActionKeywords : new() { Query.GlobalPluginWildcardSign }; + + var addedActionKeywords = newActionKeywords.Except(oldActionKeywords).ToList(); + var removedActionKeywords = oldActionKeywords.Except(newActionKeywords).ToList(); + if (!addedActionKeywords.Any(App.API.ActionKeywordAssigned)) { - pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword); - Close(); + if (oldActionKeywords.Count != newActionKeywords.Count) + { + ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords); + return; + } + + var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList(); + var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList(); + + if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords)) + { + // User just changes the sequence of action keywords + App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld")); + } + else + { + ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords); + } } else { - string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned"); - MessageBox.Show(msg); + App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned")); } } + + private void ReplaceActionKeyword(string id, IReadOnlyList removedActionKeywords, IReadOnlyList addedActionKeywords) + { + foreach (var actionKeyword in removedActionKeywords) + { + App.API.RemoveActionKeyword(id, actionKeyword); + } + foreach (var actionKeyword in addedActionKeywords) + { + App.API.AddActionKeyword(id, actionKeyword); + } + + // Update action keywords text and close window + _pluginViewModel.OnActionKeywordsChanged(); + Close(); + } } } diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index 18addac73..565bbe3c7 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -1,15 +1,36 @@ - + - + + + + + + + + + + + + + + + + + + + - + + + diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7b808dd54..a9cd1a8b9 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -1,11 +1,13 @@ -using System; +using System; using System.Diagnostics; using System.Text; +using System.Threading; using System.Threading.Tasks; -using System.Timers; using System.Windows; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core; using Flow.Launcher.Core.Configuration; +using Flow.Launcher.Core.ExternalPlugins.Environments; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; @@ -13,81 +15,174 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; -using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Flow.Launcher { public partial class App : IDisposable, ISingleInstanceApp { - public static PublicAPIInstance API { get; private set; } - private const string Unique = "Flow.Launcher_Unique_Application_Mutex"; + #region Public Properties + + public static IPublicAPI API { get; private set; } + + #endregion + + #region Private Fields + + private static readonly string ClassName = nameof(App); + private static bool _disposed; - private Settings _settings; - private MainViewModel _mainVM; - private SettingWindowViewModel _settingsVM; - private readonly Updater _updater = new Updater(Flow.Launcher.Properties.Settings.Default.GithubRepo); - private readonly Portable _portable = new Portable(); - private readonly PinyinAlphabet _alphabet = new PinyinAlphabet(); - private StringMatcher _stringMatcher; + private MainWindow _mainWindow; + private readonly MainViewModel _mainVM; + private readonly Settings _settings; + + // To prevent two disposals running at the same time. + private static readonly object _disposingLock = new(); + + #endregion + + #region Constructor + + public App() + { + // Initialize settings + try + { + var storage = new FlowLauncherJsonStorage(); + _settings = storage.Load(); + _settings.SetStorage(storage); + _settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled(); + } + catch (Exception e) + { + ShowErrorMsgBoxAndFailFast("Cannot load setting storage, please check local data directory", e); + return; + } + + // Configure the dependency injection container + try + { + var host = Host.CreateDefaultBuilder() + .UseContentRoot(AppContext.BaseDirectory) + .ConfigureServices(services => services + .AddSingleton(_ => _settings) + .AddSingleton(sp => new Updater(sp.GetRequiredService(), Launcher.Properties.Settings.Default.GithubRepo)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + ).Build(); + Ioc.Default.ConfigureServices(host.Services); + } + catch (Exception e) + { + ShowErrorMsgBoxAndFailFast("Cannot configure dependency injection container, please open new issue in Flow.Launcher", e); + return; + } + + // Initialize the public API and Settings first + try + { + API = Ioc.Default.GetRequiredService(); + _settings.Initialize(); + _mainVM = Ioc.Default.GetRequiredService(); + } + catch (Exception e) + { + ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); + return; + } + + // Local function + static void ShowErrorMsgBoxAndFailFast(string message, Exception e) + { + // Firstly show users the message + MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + + // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. + Environment.FailFast(message, e); + } + } + + #endregion + + #region Main [STAThread] public static void Main() { - if (SingleInstance.InitializeAsFirstInstance(Unique)) + if (SingleInstance.InitializeAsFirstInstance()) { - using (var application = new App()) - { - application.InitializeComponent(); - application.Run(); - } + using var application = new App(); + application.InitializeComponent(); + application.Run(); } } - private async void OnStartupAsync(object sender, StartupEventArgs e) + #endregion + + #region App Events + +#pragma warning disable VSTHRD100 // Avoid async void methods + + private async void OnStartup(object sender, StartupEventArgs e) { - await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => + await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () => { - _portable.PreStartCleanUpAfterPortabilityUpdate(); + // Because new message box api uses MessageBoxEx window, + // if it is created and closed before main window is created, it will cause the application to exit. + // So set to OnExplicitShutdown to prevent the application from shutting down before main window is created + Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; + + Log.SetLogLevel(_settings.LogLevel); + + Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate(); Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------"); Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}"); + RegisterAppDomainExceptions(); RegisterDispatcherUnhandledException(); + RegisterTaskSchedulerUnhandledException(); - ImageLoader.Initialize(); + var imageLoadertask = ImageLoader.InitializeAsync(); - _settingsVM = new SettingWindowViewModel(_updater, _portable); - _settings = _settingsVM.Settings; - - _alphabet.Initialize(_settings); - _stringMatcher = new StringMatcher(_alphabet); - StringMatcher.Instance = _stringMatcher; - _stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision; + AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); PluginManager.LoadPlugins(_settings.PluginSettings); - _mainVM = new MainViewModel(_settings); - API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet); - Http.API = API; + // Register ResultsUpdated event after all plugins are loaded + Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); + Http.Proxy = _settings.Proxy; - - await PluginManager.InitializePlugins(API); - var window = new MainWindow(_settings, _mainVM); + + await PluginManager.InitializePluginsAsync(); + + // Change language after all plugins are initialized because we need to update plugin title based on their api + // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future + await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + + await imageLoadertask; + + _mainWindow = new MainWindow(); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); - Current.MainWindow = window; + Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; - // happlebao todo temp fix for instance code logic - // load plugin before change language, because plugin language also needs be changed - InternationalizationManager.Instance.Settings = _settings; - InternationalizationManager.Instance.ChangeLanguage(_settings.Language); - // main windows needs initialized before theme change because of blur settigns - ThemeManager.Instance.Settings = _settings; - ThemeManager.Instance.ChangeTheme(_settings.Theme); + // main windows needs initialized before theme change because of blur settings + Ioc.Default.GetRequiredService().ChangeTheme(); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -96,49 +191,80 @@ namespace Flow.Launcher AutoStartup(); AutoUpdates(); - _mainVM.MainWindowVisibility = _settings.HideOnStartup ? Visibility.Hidden : Visibility.Visible; - Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); + API.SaveAppAllSettings(); + Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------"); }); } +#pragma warning restore VSTHRD100 // Avoid async void methods private void AutoStartup() { - if (_settings.StartFlowLauncherOnSystemStartup) + // we try to enable auto-startup on first launch, or reenable if it was removed + // but the user still has the setting set + if (_settings.StartFlowLauncherOnSystemStartup && !Helper.AutoStartup.IsEnabled) { - if (!SettingWindow.StartupSet()) + try { - SettingWindow.SetStartup(); + if (_settings.UseLogonTaskForStartup) + { + Helper.AutoStartup.EnableViaLogonTask(); + } + else + { + Helper.AutoStartup.EnableViaRegistry(); + } + } + catch (Exception e) + { + // but if it fails (permissions, etc) then don't keep retrying + // this also gives the user a visual indication in the Settings widget + _settings.StartFlowLauncherOnSystemStartup = false; + API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message); } } } - //[Conditional("RELEASE")] private void AutoUpdates() { - Task.Run(async () => + _ = Task.Run(async () => { if (_settings.AutoUpdates) { - // check udpate every 5 hours - var timer = new Timer(1000 * 60 * 60 * 5); - timer.Elapsed += async (s, e) => - { - await _updater.UpdateApp(API); - }; - timer.Start(); + // check update every 5 hours + var timer = new PeriodicTimer(TimeSpan.FromHours(5)); + await Ioc.Default.GetRequiredService().UpdateAppAsync(); - // check updates on startup - await _updater.UpdateApp(API); + while (await timer.WaitForNextTickAsync()) + // check updates on startup + await Ioc.Default.GetRequiredService().UpdateAppAsync(); } }); } + #endregion + + #region Register Events + private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose(); - Current.Exit += (s, e) => Dispose(); - Current.SessionEnding += (s, e) => Dispose(); + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Process Exit"); + Dispose(); + }; + + Current.Exit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Application Exit"); + Dispose(); + }; + + Current.SessionEnding += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Session Ending"); + Dispose(); + }; } /// @@ -150,7 +276,6 @@ namespace Flow.Launcher DispatcherUnhandledException += ErrorReporting.DispatcherUnhandledException; } - /// /// let exception throw as normal is better for Debug /// @@ -160,20 +285,77 @@ namespace Flow.Launcher AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } - public void Dispose() + /// + /// let exception throw as normal is better for Debug + /// + [Conditional("RELEASE")] + private static void RegisterTaskSchedulerUnhandledException() { - // if sessionending is called, exit proverbially be called when log off / shutdown - // but if sessionending is not called, exit won't be called when log off / shutdown - if (!_disposed) + TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException; + } + + #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + // Prevent two disposes at the same time. + lock (_disposingLock) { - API.SaveAppAllSettings(); + if (!disposing) + { + return; + } + + if (_disposed) + { + return; + } + + // If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close() + // Accessing _mainWindow?.Dispatcher will cause the application stuck + // So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher + if (!_mainWindow.CanClose) + { + return; + } + _disposed = true; } + + API.StopwatchLogInfo(ClassName, "Dispose cost", () => + { + Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); + + if (disposing) + { + // Dispose needs to be called on the main Windows thread, + // since some resources owned by the thread need to be disposed. + _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); + _mainVM?.Dispose(); + } + + Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); + }); } + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + #region ISingleInstanceApp + public void OnSecondAppStarted() { - Current.MainWindow.Visibility = Visibility.Visible; + API.ShowMainWindow(); } + + #endregion } -} \ No newline at end of file +} diff --git a/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs b/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs new file mode 100644 index 000000000..41e879913 --- /dev/null +++ b/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Input; + +namespace Flow.Launcher.Converters; + +internal class BoolToIMEConversionModeConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value switch + { + true => ImeConversionModeValues.Alphanumeric, + _ => ImeConversionModeValues.DoNotCare + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +internal class BoolToIMEStateConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value switch + { + true => InputMethodState.Off, + _ => InputMethodState.DoNotCare + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/Flow.Launcher/Converters/BoolToVisibilityConverter.cs b/Flow.Launcher/Converters/BoolToVisibilityConverter.cs new file mode 100644 index 000000000..c0fe6ab55 --- /dev/null +++ b/Flow.Launcher/Converters/BoolToVisibilityConverter.cs @@ -0,0 +1,40 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Flow.Launcher.Converters; + +public class BoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture) + { + return (value, parameter) switch + { + (true, not null) => Visibility.Collapsed, + (_, not null) => Visibility.Visible, + + (true, null) => Visibility.Visible, + (_, null) => Visibility.Collapsed + }; + } + + public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); +} + +public class SplitterConverter : IValueConverter +/* Prevents the dragging part of the preview area from working when preview is turned off. */ +{ + public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture) + { + return (value, parameter) switch + { + (true, not null) => 0, + (_, not null) => 5, + + (true, null) => 5, + (_, null) => 0 + }; + } + + public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); +} diff --git a/Flow.Launcher/Converters/BorderClipConverter.cs b/Flow.Launcher/Converters/BorderClipConverter.cs new file mode 100644 index 000000000..9b0579c33 --- /dev/null +++ b/Flow.Launcher/Converters/BorderClipConverter.cs @@ -0,0 +1,47 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; +using System.Windows.Shapes; + +// For Clipping inside listbox item + +namespace Flow.Launcher.Converters; + +public class BorderClipConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values is not [double width, double height, CornerRadius radius]) + { + return DependencyProperty.UnsetValue; + } + + Path myPath = new Path(); + if (width < Double.Epsilon || height < Double.Epsilon) + { + return Geometry.Empty; + } + var radiusHeight = radius.TopLeft; + + // Drawing Round box for bottom round, and rect for top area of listbox. + var corner = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft); + var box = new RectangleGeometry(new Rect(0, 0, width, radiusHeight), 0, 0); + + GeometryGroup myGeometryGroup = new GeometryGroup(); + myGeometryGroup.Children.Add(corner); + myGeometryGroup.Children.Add(box); + + CombinedGeometry c1 = new CombinedGeometry(GeometryCombineMode.Union, corner, box); + myPath.Data = c1; + + myPath.Data.Freeze(); + return myPath.Data; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/Flow.Launcher/Converters/DateTimeFormatToNowConverter.cs b/Flow.Launcher/Converters/DateTimeFormatToNowConverter.cs new file mode 100644 index 000000000..7d75ffd5c --- /dev/null +++ b/Flow.Launcher/Converters/DateTimeFormatToNowConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace Flow.Launcher.Converters; + +public class DateTimeFormatToNowConverter : IValueConverter +{ + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is not string format ? null : DateTime.Now.ToString(format); + } + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/Flow.Launcher/Converters/DiameterToCenterPointConverter.cs b/Flow.Launcher/Converters/DiameterToCenterPointConverter.cs new file mode 100644 index 000000000..24a316603 --- /dev/null +++ b/Flow.Launcher/Converters/DiameterToCenterPointConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Flow.Launcher.Converters; + +public class DiameterToCenterPointConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double d) + { + return new Point(d / 2, d / 2); + } + + return new Point(0, 0); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/Flow.Launcher/Converters/HighlightTextConverter.cs b/Flow.Launcher/Converters/HighlightTextConverter.cs index 006e52320..436f5fed5 100644 --- a/Flow.Launcher/Converters/HighlightTextConverter.cs +++ b/Flow.Launcher/Converters/HighlightTextConverter.cs @@ -1,53 +1,48 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Data; using System.Windows.Documents; -namespace Flow.Launcher.Converters +namespace Flow.Launcher.Converters; + +public class HighlightTextConverter : IMultiValueConverter { - public class HighlightTextConverter : IMultiValueConverter + public object Convert(object[] value, Type targetType, object parameter, CultureInfo cultureInfo) { - public object Convert(object[] value, Type targetType, object parameter, CultureInfo cultureInfo) + if (value.Length < 2) + return new Run(string.Empty); + + if (value[0] is not string text) + return new Run(string.Empty); + + if (value[1] is not List { Count: > 0 } highlightData) + // No highlight data, just return the text + return new Run(text); + + var highlightStyle = (Style)Application.Current.FindResource("HighlightStyle"); + var textBlock = new Span(); + + for (var i = 0; i < text.Length; i++) { - var text = value[0] as string; - var highlightData = value[1] as List; - - var textBlock = new Span(); - - if (highlightData == null || !highlightData.Any()) + var currentCharacter = text.Substring(i, 1); + var run = new Run(currentCharacter) { - // No highlight data, just return the text - return new Run(text); - } - - for (var i = 0; i < text.Length; i++) - { - var currentCharacter = text.Substring(i, 1); - if (this.ShouldHighlight(highlightData, i)) - { - textBlock.Inlines.Add(new Bold(new Run(currentCharacter))); - } - else - { - textBlock.Inlines.Add(new Run(currentCharacter)); - } - } - return textBlock; + Style = ShouldHighlight(highlightData, i) ? highlightStyle : null + }; + textBlock.Inlines.Add(run); } + return textBlock; + } - public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture) - { - return new[] { DependencyProperty.UnsetValue, DependencyProperty.UnsetValue }; - } + public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture) + { + return new[] { DependencyProperty.UnsetValue, DependencyProperty.UnsetValue }; + } - private bool ShouldHighlight(List highlightData, int index) - { - return highlightData.Contains(index); - } + private bool ShouldHighlight(List highlightData, int index) + { + return highlightData.Contains(index); } } diff --git a/Flow.Launcher/Converters/IconRadiusConverter.cs b/Flow.Launcher/Converters/IconRadiusConverter.cs new file mode 100644 index 000000000..d26b39d6b --- /dev/null +++ b/Flow.Launcher/Converters/IconRadiusConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace Flow.Launcher.Converters; + +public class IconRadiusConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values is not [double size, bool isIconCircular]) + throw new ArgumentException("IconRadiusConverter must have 2 parameters: [double, bool]"); + + return isIconCircular ? size / 2 : size; + } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs b/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs index 7de5af79a..7ab13190a 100644 --- a/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs +++ b/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs @@ -1,31 +1,26 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; -namespace Flow.Launcher.Converters +namespace Flow.Launcher.Converters; + +[ValueConversion(typeof(bool), typeof(Visibility))] +public class OpenResultHotkeyVisibilityConverter : IValueConverter { - [ValueConversion(typeof(bool), typeof(Visibility))] - public class OpenResultHotkeyVisibilityConverter : IValueConverter + private const int MaxVisibleHotkeys = 10; + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - private const int MaxVisibleHotkeys = 9; + var number = int.MaxValue; - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - var hotkeyNumber = int.MaxValue; - - if (value is ListBoxItem listBoxItem) - { - ListBox listBox = ItemsControl.ItemsControlFromItemContainer(listBoxItem) as ListBox; - hotkeyNumber = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; - } + if (value is ListBoxItem listBoxItem + && ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox) + number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; - return hotkeyNumber <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); + return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed; } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); } diff --git a/Flow.Launcher/Converters/OrdinalConverter.cs b/Flow.Launcher/Converters/OrdinalConverter.cs index 970ed183c..9aed2e07b 100644 --- a/Flow.Launcher/Converters/OrdinalConverter.cs +++ b/Flow.Launcher/Converters/OrdinalConverter.cs @@ -1,22 +1,24 @@ -using System.Globalization; +using System; +using System.Globalization; using System.Windows.Controls; using System.Windows.Data; -namespace Flow.Launcher.Converters -{ - public class OrdinalConverter : IValueConverter - { - public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture) - { - if (value is ListBoxItem listBoxItem) - { - ListBox listBox = ItemsControl.ItemsControlFromItemContainer(listBoxItem) as ListBox; - return listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; - } +namespace Flow.Launcher.Converters; +public class OrdinalConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not ListBoxItem listBoxItem + || ItemsControl.ItemsControlFromItemContainer(listBoxItem) is not ListBox listBox) + { return 0; } - public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); + var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; + return res == 10 ? 0 : res; // 10th item => HOTKEY+0 + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); } diff --git a/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs b/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs index c70796a6d..0d6f2e469 100644 --- a/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs +++ b/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs @@ -1,61 +1,76 @@ using System; using System.Globalization; +using System.Windows; +using System.Windows.Controls; using System.Windows.Data; +using System.Windows.Media; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.ViewModel; -namespace Flow.Launcher.Converters +namespace Flow.Launcher.Converters; + +public class QuerySuggestionBoxConverter : IMultiValueConverter { - public class QuerySuggestionBoxConverter : IMultiValueConverter + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + // values[0] is TextBox: The textbox displaying the autocomplete suggestion + // values[1] is ResultViewModel: Currently selected item in the list + // values[2] is string: Query text + if ( + values.Length != 3 || + values[0] is not TextBox queryTextBox || + values[1] is null || + values[2] is not string queryText || + string.IsNullOrEmpty(queryText) + ) + return string.Empty; + + if (values[1] is not ResultViewModel selectedItem) + return Binding.DoNothing; + + try { - if (values.Length != 2) - { - return string.Empty; - } + var selectedResult = selectedItem.Result; + var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " "; + var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title; - // first prop is the current query string - var queryText = (string)values[0]; - - if (string.IsNullOrEmpty(queryText)) + if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase)) return string.Empty; - // second prop is the current selected item result - var val = values[1]; - if (val == null) - { + + // For AutocompleteQueryCommand. + // When user typed lower case and result title is uppercase, we still want to display suggestion + selectedItem.QuerySuggestionText = queryText + selectedResultPossibleSuggestion.Substring(queryText.Length); + + // Check if Text will be larger than our QueryTextBox + Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch); + var dpi = VisualTreeHelper.GetDpi(queryTextBox); + var ft = new FormattedText( + queryTextBox.Text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + queryTextBox.FontSize, + Brushes.Black, + dpi.PixelsPerDip + ); + + var offset = queryTextBox.Padding.Right; + + if (ft.Width + offset > queryTextBox.ActualWidth || queryTextBox.HorizontalOffset != 0) return string.Empty; - } - if (!(val is ResultViewModel)) - { - return System.Windows.Data.Binding.DoNothing; - } - try - { - var selectedItem = (ResultViewModel)val; - - var selectedResult = selectedItem.Result; - var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " "; - var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title; - - if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase)) - return string.Empty; - - // When user typed lower case and result title is uppercase, we still want to display suggestion - return queryText + selectedResultPossibleSuggestion.Substring(queryText.Length); - } - catch (Exception e) - { - Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e); - return string.Empty; - } + return selectedItem.QuerySuggestionText; } - - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + catch (Exception e) { - throw new NotImplementedException(); + Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e); + return string.Empty; } } -} \ No newline at end of file + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/Flow.Launcher/Converters/StringToKeyBindingConverter.cs b/Flow.Launcher/Converters/StringToKeyBindingConverter.cs new file mode 100644 index 000000000..21bf584e7 --- /dev/null +++ b/Flow.Launcher/Converters/StringToKeyBindingConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Input; + +namespace Flow.Launcher.Converters; + +class StringToKeyBindingConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is not string mode || value is not string hotkeyStr) + return null; + + var converter = new KeyGestureConverter(); + var key = (KeyGesture)converter.ConvertFromString(hotkeyStr); + return mode switch + { + "key" => key?.Key, + "modifiers" => key?.Modifiers, + _ => null + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/Flow.Launcher/Converters/TextConverter.cs b/Flow.Launcher/Converters/TextConverter.cs new file mode 100644 index 000000000..2c4dad4a9 --- /dev/null +++ b/Flow.Launcher/Converters/TextConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using Flow.Launcher.ViewModel; + +namespace Flow.Launcher.Converters; + +public class TextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var id = value?.ToString(); + var translationKey = id switch + { + PluginStoreItemViewModel.NewRelease => "pluginStore_NewRelease", + PluginStoreItemViewModel.RecentlyUpdated => "pluginStore_RecentlyUpdated", + PluginStoreItemViewModel.None => "pluginStore_None", + PluginStoreItemViewModel.Installed => "pluginStore_Installed", + _ => null + }; + + if (translationKey is null) + return id; + + return App.API.GetTranslation(translationKey); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); +} diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml index a97f90733..0171e6d79 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml @@ -1,45 +1,158 @@ - + + + + - + - + - - + - - - - - - + + + + + + + + + + + + + + - - - - - + + + - \ No newline at end of file + diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs index 813c5e254..8b193fa1d 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs @@ -1,28 +1,23 @@ -using Flow.Launcher.Core.Resource; -using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.UserSettings; -using NHotkey; -using NHotkey.Wpf; -using System; using System.Collections.ObjectModel; using System.Linq; using System.Windows; using System.Windows.Input; +using System.Windows.Controls; namespace Flow.Launcher { public partial class CustomQueryHotkeySetting : Window { - private SettingWindow _settingWidow; + private readonly Settings _settings; private bool update; private CustomPluginHotkey updateCustomHotkey; - private Settings _settings; - public CustomQueryHotkeySetting(SettingWindow settingWidow, Settings settings) + public CustomQueryHotkeySetting(Settings settings) { - _settingWidow = settingWidow; - InitializeComponent(); _settings = settings; + InitializeComponent(); } private void BtnCancel_OnClick(object sender, RoutedEventArgs e) @@ -34,106 +29,64 @@ namespace Flow.Launcher { if (!update) { - if (!ctlHotkey.CurrentHotkeyAvailable) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("hotkeyIsNotUnavailable")); - return; - } - - if (_settings.CustomPluginHotkeys == null) - { - _settings.CustomPluginHotkeys = new ObservableCollection(); - } + _settings.CustomPluginHotkeys ??= new ObservableCollection(); var pluginHotkey = new CustomPluginHotkey { - Hotkey = ctlHotkey.CurrentHotkey.ToString(), - ActionKeyword = tbAction.Text + Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text }; _settings.CustomPluginHotkeys.Add(pluginHotkey); - SetHotkey(ctlHotkey.CurrentHotkey, delegate - { - App.API.ChangeQuery(pluginHotkey.ActionKeyword); - ShowMainWindow(); - }); + HotKeyMapper.SetCustomQueryHotkey(pluginHotkey); } else { - if (updateCustomHotkey.Hotkey != ctlHotkey.CurrentHotkey.ToString() && !ctlHotkey.CurrentHotkeyAvailable) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("hotkeyIsNotUnavailable")); - return; - } var oldHotkey = updateCustomHotkey.Hotkey; updateCustomHotkey.ActionKeyword = tbAction.Text; - updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString(); + updateCustomHotkey.Hotkey = HotkeyControl.CurrentHotkey.ToString(); //remove origin hotkey - RemoveHotkey(oldHotkey); - SetHotkey(new HotkeyModel(updateCustomHotkey.Hotkey), delegate - { - App.API.ChangeQuery(updateCustomHotkey.ActionKeyword); - ShowMainWindow(); - }); + HotKeyMapper.RemoveHotkey(oldHotkey); + HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey); } Close(); - - static void ShowMainWindow() - { - Window mainWindow = Application.Current.MainWindow; - mainWindow.Visibility = Visibility.Visible; - mainWindow.Focus(); - } } public void UpdateItem(CustomPluginHotkey item) { - updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o => o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey); + updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o => + o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey); if (updateCustomHotkey == null) { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey")); + App.API.ShowMsgBox(App.API.GetTranslation("invalidPluginHotkey")); Close(); return; } tbAction.Text = updateCustomHotkey.ActionKeyword; - ctlHotkey.SetHotkey(updateCustomHotkey.Hotkey, false); + HotkeyControl.SetHotkey(updateCustomHotkey.Hotkey, false); update = true; - lblAdd.Text = InternationalizationManager.Instance.GetTranslation("update"); + lblAdd.Text = App.API.GetTranslation("update"); } private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e) { App.API.ChangeQuery(tbAction.Text); - Application.Current.MainWindow.Visibility = Visibility.Visible; - } - - private void RemoveHotkey(string hotkeyStr) - { - if (!string.IsNullOrEmpty(hotkeyStr)) - { - HotkeyManager.Current.Remove(hotkeyStr); - } - } - - private void SetHotkey(HotkeyModel hotkey, EventHandler action) - { - string hotkeyStr = hotkey.ToString(); - try - { - HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action); - } - catch (Exception) - { - string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); - MessageBox.Show(errorMsg); - } + App.API.ShowMainWindow(); + Application.Current.MainWindow.Focus(); } private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e) { Close(); } + + private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */ + { + if (Keyboard.FocusedElement is not TextBox textBox) return; + + TraversalRequest tRequest = new TraversalRequest(FocusNavigationDirection.Next); + textBox.MoveFocus(tRequest); + } } } diff --git a/Flow.Launcher/CustomShortcutSetting.xaml b/Flow.Launcher/CustomShortcutSetting.xaml new file mode 100644 index 000000000..9256a2c52 --- /dev/null +++ b/Flow.Launcher/CustomShortcutSetting.xaml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher/CustomShortcutSetting.xaml.cs b/Flow.Launcher/CustomShortcutSetting.xaml.cs new file mode 100644 index 000000000..ecbdea606 --- /dev/null +++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs @@ -0,0 +1,70 @@ +using System; +using System.Windows; +using System.Windows.Input; +using Flow.Launcher.SettingPages.ViewModels; + +namespace Flow.Launcher +{ + public partial class CustomShortcutSetting : Window + { + private readonly SettingsPaneHotkeyViewModel _hotkeyVm; + public string Key { get; set; } = String.Empty; + public string Value { get; set; } = String.Empty; + private string originalKey { get; } = null; + private string originalValue { get; } = null; + private bool update { get; } = false; + + public CustomShortcutSetting(SettingsPaneHotkeyViewModel vm) + { + _hotkeyVm = vm; + InitializeComponent(); + } + + public CustomShortcutSetting(string key, string value, SettingsPaneHotkeyViewModel vm) + { + Key = key; + Value = value; + originalKey = key; + originalValue = value; + update = true; + _hotkeyVm = vm; + InitializeComponent(); + } + + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + { + DialogResult = false; + Close(); + } + + private void BtnAdd_OnClick(object sender, RoutedEventArgs e) + { + if (string.IsNullOrEmpty(Key) || string.IsNullOrEmpty(Value)) + { + App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut")); + return; + } + // Check if key is modified or adding a new one + if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key)) + { + App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut")); + return; + } + DialogResult = !update || originalKey != Key || originalValue != Value; + Close(); + } + + private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e) + { + DialogResult = false; + Close(); + } + + private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e) + { + App.API.ChangeQuery(tbExpand.Text); + App.API.ShowMainWindow(); + Application.Current.MainWindow.Focus(); + } + } +} diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index f3885ccfd..91cab9fa0 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -2,9 +2,9 @@ WinExe - net5.0-windows10.0.19041.0 + net7.0-windows10.0.19041.0 true - true + false Flow.Launcher.App Resources\app.ico app.manifest @@ -77,18 +77,31 @@ Designer PreserveNewest + + PreserveNewest + - - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + + + + + + + @@ -97,7 +110,29 @@ + + + Always + + + + + + PreserveNewest + + + - \ No newline at end of file + + + + + + + + + + + diff --git a/Flow.Launcher/Helper/AutoStartup.cs b/Flow.Launcher/Helper/AutoStartup.cs new file mode 100644 index 000000000..c5e20504b --- /dev/null +++ b/Flow.Launcher/Helper/AutoStartup.cs @@ -0,0 +1,190 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Principal; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.Logger; +using Microsoft.Win32; +using Microsoft.Win32.TaskScheduler; + +namespace Flow.Launcher.Helper; + +public class AutoStartup +{ + private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; + private const string LogonTaskName = $"{Constant.FlowLauncher} Startup"; + private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup"; + + public static bool IsEnabled + { + get + { + // Check if logon task is enabled + if (CheckLogonTask()) + { + return true; + } + + // Check if registry is enabled + try + { + using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true); + var path = key?.GetValue(Constant.FlowLauncher) as string; + return path == Constant.ExecutablePath; + } + catch (Exception e) + { + Log.Error("AutoStartup", $"Ignoring non-critical registry error (querying if enabled): {e}"); + } + + return false; + } + } + + private static bool CheckLogonTask() + { + using var taskService = new TaskService(); + var task = taskService.RootFolder.AllTasks.FirstOrDefault(t => t.Name == LogonTaskName); + if (task != null) + { + try + { + // Check if the action is the same as the current executable path + var action = task.Definition.Actions.FirstOrDefault()!.ToString().Trim(); + if (!Constant.ExecutablePath.Equals(action, StringComparison.OrdinalIgnoreCase) && !File.Exists(action)) + { + UnscheduleLogonTask(); + ScheduleLogonTask(); + } + + return true; + } + catch (Exception e) + { + Log.Error("AutoStartup", $"Failed to check logon task: {e}"); + } + } + + return false; + } + + public static void DisableViaLogonTaskAndRegistry() + { + Disable(true); + Disable(false); + } + + public static void EnableViaLogonTask() + { + Enable(true); + } + + public static void EnableViaRegistry() + { + Enable(false); + } + + public static void ChangeToViaLogonTask() + { + Disable(false); + Enable(true); + } + + public static void ChangeToViaRegistry() + { + Disable(true); + Enable(false); + } + + private static void Disable(bool logonTask) + { + try + { + if (logonTask) + { + UnscheduleLogonTask(); + } + else + { + using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true); + key?.DeleteValue(Constant.FlowLauncher, false); + } + } + catch (Exception e) + { + Log.Error("AutoStartup", $"Failed to disable auto-startup: {e}"); + throw; + } + } + + private static void Enable(bool logonTask) + { + try + { + if (logonTask) + { + ScheduleLogonTask(); + } + else + { + using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true); + key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\""); + } + } + catch (Exception e) + { + Log.Error("AutoStartup", $"Failed to enable auto-startup: {e}"); + throw; + } + } + + private static bool ScheduleLogonTask() + { + using var td = TaskService.Instance.NewTask(); + td.RegistrationInfo.Description = LogonTaskDesc; + td.Triggers.Add(new LogonTrigger { UserId = WindowsIdentity.GetCurrent().Name, Delay = TimeSpan.FromSeconds(2) }); + td.Actions.Add(Constant.ExecutablePath); + + if (IsCurrentUserIsAdmin()) + { + td.Principal.RunLevel = TaskRunLevel.Highest; + } + + td.Settings.StopIfGoingOnBatteries = false; + td.Settings.DisallowStartIfOnBatteries = false; + td.Settings.ExecutionTimeLimit = TimeSpan.Zero; + + try + { + TaskService.Instance.RootFolder.RegisterTaskDefinition(LogonTaskName, td); + return true; + } + catch (Exception e) + { + Log.Error("AutoStartup", $"Failed to schedule logon task: {e}"); + return false; + } + } + + private static bool UnscheduleLogonTask() + { + using var taskService = new TaskService(); + try + { + taskService.RootFolder.DeleteTask(LogonTaskName); + return true; + } + catch (Exception e) + { + Log.Error("AutoStartup", $"Failed to unschedule logon task: {e}"); + return false; + } + } + + private static bool IsCurrentUserIsAdmin() + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } +} diff --git a/Flow.Launcher/Helper/DWMDropShadow.cs b/Flow.Launcher/Helper/DWMDropShadow.cs deleted file mode 100644 index 3a1f82d6f..000000000 --- a/Flow.Launcher/Helper/DWMDropShadow.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Drawing.Printing; -using System.Runtime.InteropServices; -using System.Windows; -using System.Windows.Interop; - -namespace Flow.Launcher.Helper -{ - public class DwmDropShadow - { - - [DllImport("dwmapi.dll", PreserveSig = true)] - private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); - - [DllImport("dwmapi.dll")] - private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset); - - /// - /// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven). - /// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect, - /// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window). - /// - /// Window to which the shadow will be applied - public static void DropShadowToWindow(Window window) - { - if (!DropShadow(window)) - { - window.SourceInitialized += window_SourceInitialized; - } - } - - private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo - { - Window window = (Window)sender; - - DropShadow(window); - - window.SourceInitialized -= window_SourceInitialized; - } - - /// - /// The actual method that makes API calls to drop the shadow to the window - /// - /// Window to which the shadow will be applied - /// True if the method succeeded, false if not - private static bool DropShadow(Window window) - { - try - { - WindowInteropHelper helper = new WindowInteropHelper(window); - int val = 2; - int ret1 = DwmSetWindowAttribute(helper.Handle, 2, ref val, 4); - - if (ret1 == 0) - { - Margins m = new Margins { Bottom = 0, Left = 0, Right = 0, Top = 0 }; - int ret2 = DwmExtendFrameIntoClientArea(helper.Handle, ref m); - return ret2 == 0; - } - else - { - return false; - } - } - catch (Exception) - { - // Probably dwmapi.dll not found (incompatible OS) - return false; - } - } - - } -} \ No newline at end of file diff --git a/Flow.Launcher/Helper/DataWebRequestFactory.cs b/Flow.Launcher/Helper/DataWebRequestFactory.cs index b3b7e9c63..2e72ee240 100644 --- a/Flow.Launcher/Helper/DataWebRequestFactory.cs +++ b/Flow.Launcher/Helper/DataWebRequestFactory.cs @@ -2,68 +2,67 @@ using System.IO; using System.Net; -namespace Flow.Launcher.Helper +namespace Flow.Launcher.Helper; + +public class DataWebRequestFactory : IWebRequestCreate { - public class DataWebRequestFactory : IWebRequestCreate + class DataWebRequest : WebRequest { - class DataWebRequest : WebRequest + private readonly Uri _uri; + + public DataWebRequest(Uri uri) { - private readonly Uri m_uri; - - public DataWebRequest(Uri uri) - { - m_uri = uri; - } - - public override WebResponse GetResponse() - { - return new DataWebResponse(m_uri); - } + _uri = uri; } - class DataWebResponse : WebResponse + public override WebResponse GetResponse() { - private readonly string m_contentType; - private readonly byte[] m_data; - - public DataWebResponse(Uri uri) - { - string uriString = uri.AbsoluteUri; - - int commaIndex = uriString.IndexOf(','); - var headers = uriString.Substring(0, commaIndex).Split(';'); - m_contentType = headers[0]; - string dataString = uriString.Substring(commaIndex + 1); - m_data = Convert.FromBase64String(dataString); - } - - public override string ContentType - { - get { return m_contentType; } - set - { - throw new NotSupportedException(); - } - } - - public override long ContentLength - { - get { return m_data.Length; } - set - { - throw new NotSupportedException(); - } - } - - public override Stream GetResponseStream() - { - return new MemoryStream(m_data); - } - } - - public WebRequest Create(Uri uri) - { - return new DataWebRequest(uri); + return new DataWebResponse(_uri); } } + + class DataWebResponse : WebResponse + { + private readonly string _contentType; + private readonly byte[] _data; + + public DataWebResponse(Uri uri) + { + string uriString = uri.AbsoluteUri; + + int commaIndex = uriString.IndexOf(','); + var headers = uriString.Substring(0, commaIndex).Split(';'); + _contentType = headers[0]; + string dataString = uriString.Substring(commaIndex + 1); + _data = Convert.FromBase64String(dataString); + } + + public override string ContentType + { + get { return _contentType; } + set + { + throw new NotSupportedException(); + } + } + + public override long ContentLength + { + get { return _data.Length; } + set + { + throw new NotSupportedException(); + } + } + + public override Stream GetResponseStream() + { + return new MemoryStream(_data); + } + } + + public WebRequest Create(Uri uri) + { + return new DataWebRequest(uri); + } } diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index f3f590167..b1ddba717 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -1,50 +1,64 @@ using System; +using System.Threading.Tasks; +using System.Windows; using System.Windows.Threading; -using NLog; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Exception; -using NLog.Fluent; -using Log = Flow.Launcher.Infrastructure.Logger.Log; +using NLog; -namespace Flow.Launcher.Helper +namespace Flow.Launcher.Helper; + +public static class ErrorReporting { - public static class ErrorReporting + private static void Report(Exception e) { - private static void Report(Exception e) - { - var logger = LogManager.GetLogger("UnHandledException"); - logger.Fatal(ExceptionFormatter.FormatExcpetion(e)); - var reportWindow = new ReportWindow(e); - reportWindow.Show(); - } - - public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e) - { - //handle non-ui thread exceptions - Report((Exception)e.ExceptionObject); - } - - public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) - { - //handle ui thread exceptions - Report(e.Exception); - //prevent application exist, so the user can copy prompted error info - e.Handled = true; - } - - public static string RuntimeInfo() - { - var info = $"\nFlow Launcher version: {Constant.Version}" + - $"\nOS Version: {Environment.OSVersion.VersionString}" + - $"\nIntPtr Length: {IntPtr.Size}" + - $"\nx64: {Environment.Is64BitOperatingSystem}"; - return info; - } - - public static string DependenciesInfo() - { - var info = $"\nPython Path: {Constant.PythonPath}"; - return info; - } + var logger = LogManager.GetLogger("UnHandledException"); + logger.Fatal(ExceptionFormatter.FormatExcpetion(e)); + var reportWindow = new ReportWindow(e); + reportWindow.Show(); } -} \ No newline at end of file + + public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e) + { + //handle non-ui thread exceptions + Report((Exception)e.ExceptionObject); + } + + public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + //handle ui thread exceptions + Report(e.Exception); + //prevent application exist, so the user can copy prompted error info + e.Handled = true; + } + + public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + //handle unobserved task exceptions + Application.Current.Dispatcher.Invoke(() => Report(e.Exception)); + //prevent application exit, so the user can copy the prompted error info + } + + public static string RuntimeInfo() + { + var info = + $""" + + Flow Launcher version: {Constant.Version} + OS Version: {ExceptionFormatter.GetWindowsFullVersionFromRegistry()} + IntPtr Length: {IntPtr.Size} + x64: {Environment.Is64BitOperatingSystem} + """; + return info; + } + + public static string DependenciesInfo() + { + var info = $""" + + Python Path: {Constant.PythonPath} + Node Path: {Constant.NodePath} + """; + return info; + } +} diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs new file mode 100644 index 000000000..0771a6074 --- /dev/null +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -0,0 +1,162 @@ +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; +using System; +using NHotkey; +using NHotkey.Wpf; +using Flow.Launcher.ViewModel; +using ChefKeys; +using Flow.Launcher.Infrastructure.Logger; +using CommunityToolkit.Mvvm.DependencyInjection; + +namespace Flow.Launcher.Helper; + +internal static class HotKeyMapper +{ + private static Settings _settings; + private static MainViewModel _mainViewModel; + + internal static void Initialize() + { + _mainViewModel = Ioc.Default.GetRequiredService(); + _settings = Ioc.Default.GetService(); + + SetHotkey(_settings.Hotkey, OnToggleHotkey); + LoadCustomPluginHotkey(); + } + + internal static void OnToggleHotkey(object sender, HotkeyEventArgs args) + { + if (!_mainViewModel.ShouldIgnoreHotkeys()) + _mainViewModel.ToggleFlowLauncher(); + } + + internal static void OnToggleHotkeyWithChefKeys() + { + if (!_mainViewModel.ShouldIgnoreHotkeys()) + _mainViewModel.ToggleFlowLauncher(); + } + + private static void SetHotkey(string hotkeyStr, EventHandler action) + { + var hotkey = new HotkeyModel(hotkeyStr); + SetHotkey(hotkey, action); + } + + private static void SetWithChefKeys(string hotkeyStr) + { + try + { + ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, OnToggleHotkeyWithChefKeys); + ChefKeysManager.Start(); + } + catch (Exception e) + { + Log.Error( + string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}", + e.Message, + e.StackTrace)); + string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); + string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + App.API.ShowMsgBox(errorMsg, errorMsgTitle); + } + } + + internal static void SetHotkey(HotkeyModel hotkey, EventHandler action) + { + string hotkeyStr = hotkey.ToString(); + try + { + if (hotkeyStr == "LWin" || hotkeyStr == "RWin") + { + SetWithChefKeys(hotkeyStr); + return; + } + + HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action); + } + catch (Exception e) + { + Log.Error( + string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}", + e.Message, + e.StackTrace, + hotkeyStr)); + string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); + string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + App.API.ShowMsgBox(errorMsg, errorMsgTitle); + } + } + + internal static void RemoveHotkey(string hotkeyStr) + { + try + { + if (hotkeyStr == "LWin" || hotkeyStr == "RWin") + { + RemoveWithChefKeys(hotkeyStr); + return; + } + + if (!string.IsNullOrEmpty(hotkeyStr)) + HotkeyManager.Current.Remove(hotkeyStr); + } + catch (Exception e) + { + Log.Error( + string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}", + e.Message, + e.StackTrace)); + string errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr); + string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + App.API.ShowMsgBox(errorMsg, errorMsgTitle); + } + } + + private static void RemoveWithChefKeys(string hotkeyStr) + { + ChefKeysManager.UnregisterHotkey(hotkeyStr); + ChefKeysManager.Stop(); + } + + internal static void LoadCustomPluginHotkey() + { + if (_settings.CustomPluginHotkeys == null) + return; + + foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys) + { + SetCustomQueryHotkey(hotkey); + } + } + + internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey) + { + SetHotkey(hotkey.Hotkey, (s, e) => + { + if (_mainViewModel.ShouldIgnoreHotkeys()) + return; + + App.API.ShowMainWindow(); + App.API.ChangeQuery(hotkey.ActionKeyword, true); + }); + } + + internal static bool CheckAvailability(HotkeyModel currentHotkey) + { + try + { + HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { }); + + return true; + } + catch + { + } + finally + { + HotkeyManager.Current.Remove("HotkeyAvailabilityTest"); + } + + return false; + } +} diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index 1a1e6ec3c..de2579b62 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -1,193 +1,18 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Runtime.InteropServices; using System.IO.Pipes; -using System.Runtime.Serialization.Formatters; -using System.Security; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; -using System.Windows.Threading; // http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/ // modified to allow single instace restart -namespace Flow.Launcher.Helper +namespace Flow.Launcher.Helper { - internal enum WM + public interface ISingleInstanceApp { - NULL = 0x0000, - CREATE = 0x0001, - DESTROY = 0x0002, - MOVE = 0x0003, - SIZE = 0x0005, - ACTIVATE = 0x0006, - SETFOCUS = 0x0007, - KILLFOCUS = 0x0008, - ENABLE = 0x000A, - SETREDRAW = 0x000B, - SETTEXT = 0x000C, - GETTEXT = 0x000D, - GETTEXTLENGTH = 0x000E, - PAINT = 0x000F, - CLOSE = 0x0010, - QUERYENDSESSION = 0x0011, - QUIT = 0x0012, - QUERYOPEN = 0x0013, - ERASEBKGND = 0x0014, - SYSCOLORCHANGE = 0x0015, - SHOWWINDOW = 0x0018, - ACTIVATEAPP = 0x001C, - SETCURSOR = 0x0020, - MOUSEACTIVATE = 0x0021, - CHILDACTIVATE = 0x0022, - QUEUESYNC = 0x0023, - GETMINMAXINFO = 0x0024, - - WINDOWPOSCHANGING = 0x0046, - WINDOWPOSCHANGED = 0x0047, - - CONTEXTMENU = 0x007B, - STYLECHANGING = 0x007C, - STYLECHANGED = 0x007D, - DISPLAYCHANGE = 0x007E, - GETICON = 0x007F, - SETICON = 0x0080, - NCCREATE = 0x0081, - NCDESTROY = 0x0082, - NCCALCSIZE = 0x0083, - NCHITTEST = 0x0084, - NCPAINT = 0x0085, - NCACTIVATE = 0x0086, - GETDLGCODE = 0x0087, - SYNCPAINT = 0x0088, - NCMOUSEMOVE = 0x00A0, - NCLBUTTONDOWN = 0x00A1, - NCLBUTTONUP = 0x00A2, - NCLBUTTONDBLCLK = 0x00A3, - NCRBUTTONDOWN = 0x00A4, - NCRBUTTONUP = 0x00A5, - NCRBUTTONDBLCLK = 0x00A6, - NCMBUTTONDOWN = 0x00A7, - NCMBUTTONUP = 0x00A8, - NCMBUTTONDBLCLK = 0x00A9, - - SYSKEYDOWN = 0x0104, - SYSKEYUP = 0x0105, - SYSCHAR = 0x0106, - SYSDEADCHAR = 0x0107, - COMMAND = 0x0111, - SYSCOMMAND = 0x0112, - - MOUSEMOVE = 0x0200, - LBUTTONDOWN = 0x0201, - LBUTTONUP = 0x0202, - LBUTTONDBLCLK = 0x0203, - RBUTTONDOWN = 0x0204, - RBUTTONUP = 0x0205, - RBUTTONDBLCLK = 0x0206, - MBUTTONDOWN = 0x0207, - MBUTTONUP = 0x0208, - MBUTTONDBLCLK = 0x0209, - MOUSEWHEEL = 0x020A, - XBUTTONDOWN = 0x020B, - XBUTTONUP = 0x020C, - XBUTTONDBLCLK = 0x020D, - MOUSEHWHEEL = 0x020E, - - - CAPTURECHANGED = 0x0215, - - ENTERSIZEMOVE = 0x0231, - EXITSIZEMOVE = 0x0232, - - IME_SETCONTEXT = 0x0281, - IME_NOTIFY = 0x0282, - IME_CONTROL = 0x0283, - IME_COMPOSITIONFULL = 0x0284, - IME_SELECT = 0x0285, - IME_CHAR = 0x0286, - IME_REQUEST = 0x0288, - IME_KEYDOWN = 0x0290, - IME_KEYUP = 0x0291, - - NCMOUSELEAVE = 0x02A2, - - DWMCOMPOSITIONCHANGED = 0x031E, - DWMNCRENDERINGCHANGED = 0x031F, - DWMCOLORIZATIONCOLORCHANGED = 0x0320, - DWMWINDOWMAXIMIZEDCHANGE = 0x0321, - - #region Windows 7 - DWMSENDICONICTHUMBNAIL = 0x0323, - DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326, - #endregion - - USER = 0x0400, - - // This is the hard-coded message value used by WinForms for Shell_NotifyIcon. - // It's relatively safe to reuse. - TRAYMOUSEMESSAGE = 0x800, //WM_USER + 1024 - APP = 0x8000 + void OnSecondAppStarted(); } - [SuppressUnmanagedCodeSecurity] - internal static class NativeMethods - { - /// - /// Delegate declaration that matches WndProc signatures. - /// - public delegate IntPtr MessageHandler(WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled); - - [DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)] - private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs); - - - [DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)] - private static extern IntPtr _LocalFree(IntPtr hMem); - - - public static string[] CommandLineToArgvW(string cmdLine) - { - IntPtr argv = IntPtr.Zero; - try - { - int numArgs = 0; - - argv = _CommandLineToArgvW(cmdLine, out numArgs); - if (argv == IntPtr.Zero) - { - throw new Win32Exception(); - } - var result = new string[numArgs]; - - for (int i = 0; i < numArgs; i++) - { - IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr))); - result[i] = Marshal.PtrToStringUni(currArg); - } - - return result; - } - finally - { - - IntPtr p = _LocalFree(argv); - // Otherwise LocalFree failed. - // Assert.AreEqual(IntPtr.Zero, p); - } - } - - } - - public interface ISingleInstanceApp - { - void OnSecondAppStarted(); - } - /// /// This class checks to make sure that only one instance of /// this application is running at a time. @@ -199,9 +24,7 @@ namespace Flow.Launcher.Helper /// running as Administrator, can activate it with command line arguments. /// For most apps, this will not be much of an issue. /// - public static class SingleInstance - where TApplication: Application , ISingleInstanceApp - + public static class SingleInstance where TApplication : Application, ISingleInstanceApp { #region Private Fields @@ -214,15 +37,12 @@ namespace Flow.Launcher.Helper /// Suffix to the channel name. /// private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; /// /// Application mutex. /// - internal static Mutex singleInstanceMutex; - - #endregion - - #region Public Properties + internal static Mutex SingleInstanceMutex { get; set; } #endregion @@ -233,24 +53,23 @@ namespace Flow.Launcher.Helper /// If not, activates the first instance. /// /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance( string uniqueName ) + public static bool InitializeAsFirstInstance() { // Build unique application Id and the IPC channel name. - string applicationIdentifier = uniqueName + Environment.UserName; + string applicationIdentifier = InstanceMutexName + Environment.UserName; - string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); // Create mutex based on unique application Id to check if this is the first instance of the application. - bool firstInstance; - singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance); + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); if (firstInstance) { - _ = CreateRemoteService(channelName); + _ = CreateRemoteServiceAsync(channelName); return true; } else { - _ = SignalFirstInstance(channelName); + _ = SignalFirstInstanceAsync(channelName); return false; } } @@ -260,84 +79,31 @@ namespace Flow.Launcher.Helper /// public static void Cleanup() { - singleInstanceMutex?.ReleaseMutex(); + SingleInstanceMutex?.ReleaseMutex(); } #endregion #region Private Methods - /// - /// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved. - /// - /// List of command line arg strings. - private static IList GetCommandLineArgs( string uniqueApplicationName ) - { - string[] args = null; - - try - { - // The application was not clickonce deployed, get args from standard API's - args = Environment.GetCommandLineArgs(); - } - catch (NotSupportedException) - { - - // The application was clickonce deployed - // Clickonce deployed apps cannot recieve traditional commandline arguments - // As a workaround commandline arguments can be written to a shared location before - // the app is launched and the app can obtain its commandline arguments from the - // shared location - string appFolderPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName); - - string cmdLinePath = Path.Combine(appFolderPath, "cmdline.txt"); - if (File.Exists(cmdLinePath)) - { - try - { - using (TextReader reader = new StreamReader(cmdLinePath, Encoding.Unicode)) - { - args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd()); - } - - File.Delete(cmdLinePath); - } - catch (IOException) - { - } - } - } - - if (args == null) - { - args = new string[] { }; - } - - return new List(args); - } - /// /// Creates a remote server pipe for communication. /// Once receives signal from client, will activate first instance. /// /// Application's IPC channel name. - private static async Task CreateRemoteService(string channelName) + private static async Task CreateRemoteServiceAsync(string channelName) { - using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In)) + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) { - while(true) - { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - if (Application.Current != null) - { - // Do an asynchronous call to ActivateFirstInstance function - Application.Current.Dispatcher.Invoke(ActivateFirstInstance); - } - // Disconect client - pipeServer.Disconnect(); - } + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); + + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); } } @@ -348,25 +114,13 @@ namespace Flow.Launcher.Helper /// /// Command line arguments for the second instance, passed to the first instance to take appropriate action. /// - private static async Task SignalFirstInstance(string channelName) + private static async Task SignalFirstInstanceAsync(string channelName) { // Create a client pipe connected to server - using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) - { - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } - } + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); - /// - /// Callback for activating first instance of the application. - /// - /// Callback argument. - /// Always null. - private static object ActivateFirstInstanceCallback(object o) - { - ActivateFirstInstance(); - return null; + // Connect to the available pipe + await pipeClient.ConnectAsync(0); } /// @@ -385,27 +139,5 @@ namespace Flow.Launcher.Helper } #endregion - - #region Private Classes - - /// - /// Remoting service class which is exposed by the server i.e the first instance and called by the second instance - /// to pass on the command line arguments to the first instance and cause it to activate itself. - /// - private class IPCRemoteService : MarshalByRefObject - { - - /// - /// Remoting Object's ease expires after every 5 minutes by default. We need to override the InitializeLifetimeService class - /// to ensure that lease never expires. - /// - /// Always null. - public override object InitializeLifetimeService() - { - return null; - } - } - - #endregion } } diff --git a/Flow.Launcher/Helper/SingletonWindowOpener.cs b/Flow.Launcher/Helper/SingletonWindowOpener.cs index fdfaaa4fc..5282b61f9 100644 --- a/Flow.Launcher/Helper/SingletonWindowOpener.cs +++ b/Flow.Launcher/Helper/SingletonWindowOpener.cs @@ -2,28 +2,39 @@ using System.Linq; using System.Windows; -namespace Flow.Launcher.Helper -{ - public static class SingletonWindowOpener - { - public static T Open(params object[] args) where T : Window - { - var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.GetType() == typeof(T)) - ?? (T)Activator.CreateInstance(typeof(T), args); - Application.Current.MainWindow.Hide(); - - // Fix UI bug - // Add `window.WindowState = WindowState.Normal` - // If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar - // Not sure why this works tho - // Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`) - // https://stackoverflow.com/a/59719760/4230390 - window.WindowState = WindowState.Normal; - window.Show(); - - window.Focus(); +namespace Flow.Launcher.Helper; - return (T)window; +public static class SingletonWindowOpener +{ + public static T Open(params object[] args) where T : Window + { + var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.GetType() == typeof(T)) + ?? (T)Activator.CreateInstance(typeof(T), args); + + // Fix UI bug + // Add `window.WindowState = WindowState.Normal` + // If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar + // Not sure why this works tho + // Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`) + // https://stackoverflow.com/a/59719760/4230390 + // Ensure the window is not minimized before showing it + if (window.WindowState == WindowState.Minimized) + { + window.WindowState = WindowState.Normal; } + + // Ensure the window is visible + if (!window.IsVisible) + { + window.Show(); + } + else + { + window.Activate(); // Bring the window to the foreground if already open + } + + window.Focus(); + + return (T)window; } -} \ No newline at end of file +} diff --git a/Flow.Launcher/Helper/SyntaxSugars.cs b/Flow.Launcher/Helper/SyntaxSugars.cs index 7e0349422..41250dadf 100644 --- a/Flow.Launcher/Helper/SyntaxSugars.cs +++ b/Flow.Launcher/Helper/SyntaxSugars.cs @@ -1,24 +1,23 @@ using System; -namespace Flow.Launcher.Helper -{ - public static class SyntaxSugars - { - public static TResult CallOrRescueDefault(Func callback) - { - return CallOrRescueDefault(callback, default(TResult)); - } +namespace Flow.Launcher.Helper; - public static TResult CallOrRescueDefault(Func callback, TResult def) +public static class SyntaxSugars +{ + public static TResult CallOrRescueDefault(Func callback) + { + return CallOrRescueDefault(callback, default(TResult)); + } + + public static TResult CallOrRescueDefault(Func callback, TResult def) + { + try { - try - { - return callback(); - } - catch - { - return def; - } + return callback(); + } + catch + { + return def; } } } diff --git a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs index 9e5d77283..77bebe2d3 100644 --- a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs +++ b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs @@ -1,48 +1,122 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Runtime.InteropServices; -using System.Text; +using System.Windows; using System.Windows.Media; +using System.Windows.Media.Imaging; +using Flow.Launcher.Infrastructure; using Microsoft.Win32; -namespace Flow.Launcher.Helper +namespace Flow.Launcher.Helper; + +public static class WallpaperPathRetrieval { - public static class WallpaperPathRetrieval + private const int MaxCacheSize = 3; + private static readonly Dictionary<(string, DateTime), ImageBrush> WallpaperCache = new(); + private static readonly object CacheLock = new(); + + public static Brush GetWallpaperBrush() { - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern Int32 SystemParametersInfo(UInt32 action, - Int32 uParam, StringBuilder vParam, UInt32 winIni); - private static readonly UInt32 SPI_GETDESKWALLPAPER = 0x73; - private static int MAX_PATH = 260; - - public static string GetWallpaperPath() + // Invoke the method on the UI thread + if (!Application.Current.Dispatcher.CheckAccess()) { - var wallpaper = new StringBuilder(MAX_PATH); - SystemParametersInfo(SPI_GETDESKWALLPAPER, MAX_PATH, wallpaper, 0); - - var str = wallpaper.ToString(); - if (string.IsNullOrEmpty(str)) - return null; - - return str; + return Application.Current.Dispatcher.Invoke(GetWallpaperBrush); } - public static Color GetWallpaperColor() + try { - RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Colors", true); - var result = key.GetValue(@"Background", null); - if (result != null && result is string) + var wallpaperPath = Win32Helper.GetWallpaperPath(); + if (string.IsNullOrEmpty(wallpaperPath) || !File.Exists(wallpaperPath)) { - try - { - var parts = result.ToString().Trim().Split(new[] {' '}, 3).Select(byte.Parse).ToList(); - return Color.FromRgb(parts[0], parts[1], parts[2]); - } - catch + App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Wallpaper path is invalid: {wallpaperPath}"); + var wallpaperColor = GetWallpaperColor(); + return new SolidColorBrush(wallpaperColor); + } + + // Since the wallpaper file name can be the same (TranscodedWallpaper), + // we need to add the last modified date to differentiate them + var dateModified = File.GetLastWriteTime(wallpaperPath); + lock (CacheLock) + { + WallpaperCache.TryGetValue((wallpaperPath, dateModified), out var cachedWallpaper); + if (cachedWallpaper != null) { + return cachedWallpaper; } } - return Colors.Transparent; + + using var fileStream = File.OpenRead(wallpaperPath); + var decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None); + var frame = decoder.Frames[0]; + var originalWidth = frame.PixelWidth; + var originalHeight = frame.PixelHeight; + + if (originalWidth == 0 || originalHeight == 0) + { + App.API.LogInfo(nameof(WallpaperPathRetrieval), $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}"); + return new SolidColorBrush(Colors.Transparent); + } + + // Calculate the scaling factor to fit the image within 800x600 while preserving aspect ratio + var widthRatio = 800.0 / originalWidth; + var heightRatio = 600.0 / originalHeight; + var scaleFactor = Math.Min(widthRatio, heightRatio); + var decodedPixelWidth = (int)(originalWidth * scaleFactor); + var decodedPixelHeight = (int)(originalHeight * scaleFactor); + + // Set DecodePixelWidth and DecodePixelHeight to resize the image while preserving aspect ratio + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(wallpaperPath); + bitmap.DecodePixelWidth = decodedPixelWidth; + bitmap.DecodePixelHeight = decodedPixelHeight; + bitmap.EndInit(); + bitmap.Freeze(); // Make the bitmap thread-safe + var wallpaperBrush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill }; + wallpaperBrush.Freeze(); // Make the brush thread-safe + + // Manage cache size + lock (CacheLock) + { + if (WallpaperCache.Count >= MaxCacheSize) + { + // Remove the oldest wallpaper from the cache + var oldestCache = WallpaperCache.Keys.OrderBy(k => k.Item2).FirstOrDefault(); + if (oldestCache != default) + { + WallpaperCache.Remove(oldestCache); + } + } + + WallpaperCache.Add((wallpaperPath, dateModified), wallpaperBrush); + return wallpaperBrush; + } + } + catch (Exception ex) + { + App.API.LogException(nameof(WallpaperPathRetrieval), "Error retrieving wallpaper", ex); + return new SolidColorBrush(Colors.Transparent); } } + + private static Color GetWallpaperColor() + { + RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", false); + var result = key?.GetValue("Background", null); + if (result is string strResult) + { + try + { + var parts = strResult.Trim().Split(new[] { ' ' }, 3).Select(byte.Parse).ToList(); + return Color.FromRgb(parts[0], parts[1], parts[2]); + } + catch (Exception ex) + { + App.API.LogException(nameof(WallpaperPathRetrieval), "Error parsing wallpaper color", ex); + } + } + + return Colors.Transparent; + } } diff --git a/Flow.Launcher/Helper/WindowsInteropHelper.cs b/Flow.Launcher/Helper/WindowsInteropHelper.cs deleted file mode 100644 index f1e8b2099..000000000 --- a/Flow.Launcher/Helper/WindowsInteropHelper.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Text; -using System.Windows; -using System.Windows.Forms; -using System.Windows.Interop; -using System.Windows.Media; -using Point = System.Windows.Point; - -namespace Flow.Launcher.Helper -{ - public class WindowsInteropHelper - { - private const int GWL_STYLE = -16; //WPF's Message code for Title Bar's Style - private const int WS_SYSMENU = 0x80000; //WPF's Message code for System Menu - private static IntPtr _hwnd_shell; - private static IntPtr _hwnd_desktop; - - //Accessors for shell and desktop handlers - //Will set the variables once and then will return them - private static IntPtr HWND_SHELL - { - get - { - return _hwnd_shell != IntPtr.Zero ? _hwnd_shell : _hwnd_shell = GetShellWindow(); - } - } - private static IntPtr HWND_DESKTOP - { - get - { - return _hwnd_desktop != IntPtr.Zero ? _hwnd_desktop : _hwnd_desktop = GetDesktopWindow(); - } - } - - [DllImport("user32.dll", SetLastError = true)] - private static extern int GetWindowLong(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll")] - private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); - - [DllImport("user32.dll")] - private static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - private static extern IntPtr GetDesktopWindow(); - - [DllImport("user32.dll")] - private static extern IntPtr GetShellWindow(); - - [DllImport("user32.dll", SetLastError = true)] - private static extern int GetWindowRect(IntPtr hwnd, out RECT rc); - - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] - private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); - - [DllImport("user32.DLL")] - public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); - - - const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass"; - const string WINDOW_CLASS_WINTAB = "Flip3D"; - const string WINDOW_CLASS_PROGMAN = "Progman"; - const string WINDOW_CLASS_WORKERW = "WorkerW"; - - public static bool IsWindowFullscreen() - { - //get current active window - IntPtr hWnd = GetForegroundWindow(); - - if (hWnd != null && !hWnd.Equals(IntPtr.Zero)) - { - //if current active window is NOT desktop or shell - if (!(hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL))) - { - StringBuilder sb = new StringBuilder(256); - GetClassName(hWnd, sb, sb.Capacity); - string windowClass = sb.ToString(); - - //for Win+Tab (Flip3D) - if (windowClass == WINDOW_CLASS_WINTAB) - { - return false; - } - - RECT appBounds; - GetWindowRect(hWnd, out appBounds); - - //for console (ConsoleWindowClass), we have to check for negative dimensions - if (windowClass == WINDOW_CLASS_CONSOLE) - { - return appBounds.Top < 0 && appBounds.Bottom < 0; - } - - //for desktop (Progman or WorkerW, depends on the system), we have to check - if (windowClass == WINDOW_CLASS_PROGMAN || windowClass == WINDOW_CLASS_WORKERW) - { - IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null); - hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView"); - if (hWndDesktop != null && !hWndDesktop.Equals(IntPtr.Zero)) - { - return false; - } - } - - Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds; - if ((appBounds.Bottom - appBounds.Top) == screenBounds.Height && (appBounds.Right - appBounds.Left) == screenBounds.Width) - { - return true; - } - } - } - - return false; - } - - /// - /// disable windows toolbar's control box - /// this will also disable system menu with Alt+Space hotkey - /// - public static void DisableControlBox(Window win) - { - var hwnd = new WindowInteropHelper(win).Handle; - SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU); - } - - /// - /// Transforms pixels to Device Independent Pixels used by WPF - /// - /// current window, required to get presentation source - /// horizontal position in pixels - /// vertical position in pixels - /// point containing device independent pixels - public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY) - { - Matrix matrix; - var source = PresentationSource.FromVisual(visual); - if (source != null) - { - matrix = source.CompositionTarget.TransformFromDevice; - } - else - { - using (var src = new HwndSource(new HwndSourceParameters())) - { - matrix = src.CompositionTarget.TransformFromDevice; - } - } - return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY)); - } - - - [StructLayout(LayoutKind.Sequential)] - public struct RECT - { - public int Left; - public int Top; - public int Right; - public int Bottom; - } - } -} \ No newline at end of file diff --git a/Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs b/Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs new file mode 100644 index 000000000..9d2046972 --- /dev/null +++ b/Flow.Launcher/Helper/WindowsMediaPlayerHelper.cs @@ -0,0 +1,11 @@ +using Microsoft.Win32; + +namespace Flow.Launcher.Helper; +internal static class WindowsMediaPlayerHelper +{ + internal static bool IsWindowsMediaPlayerInstalled() + { + using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MediaPlayer"); + return key?.GetValue("Installation Directory") != null; + } +} diff --git a/Flow.Launcher/HotkeyControl.xaml b/Flow.Launcher/HotkeyControl.xaml index 0164d7dd7..02c625da5 100644 --- a/Flow.Launcher/HotkeyControl.xaml +++ b/Flow.Launcher/HotkeyControl.xaml @@ -1,19 +1,77 @@ - - - - - - - - - - \ No newline at end of file + + + diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 5c7141d1c..8762a934b 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -1,117 +1,321 @@ -using System; +#nullable enable + +using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Media; -using NHotkey.Wpf; -using Flow.Launcher.Core.Resource; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Plugin; +using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher { - public partial class HotkeyControl : UserControl + public partial class HotkeyControl { - public HotkeyModel CurrentHotkey { get; private set; } - public bool CurrentHotkeyAvailable { get; private set; } + public string WindowTitle { + get { return (string)GetValue(WindowTitleProperty); } + set { SetValue(WindowTitleProperty, value); } + } - public event EventHandler HotkeyChanged; + public static readonly DependencyProperty WindowTitleProperty = DependencyProperty.Register( + nameof(WindowTitle), + typeof(string), + typeof(HotkeyControl), + new PropertyMetadata(string.Empty) + ); - protected virtual void OnHotkeyChanged() + /// + /// Designed for Preview Hotkey and KeyGesture. + /// + public static readonly DependencyProperty ValidateKeyGestureProperty = DependencyProperty.Register( + nameof(ValidateKeyGesture), + typeof(bool), + typeof(HotkeyControl), + new PropertyMetadata(default(bool)) + ); + + public bool ValidateKeyGesture { - EventHandler handler = HotkeyChanged; - if (handler != null) handler(this, EventArgs.Empty); + get { return (bool)GetValue(ValidateKeyGestureProperty); } + set { SetValue(ValidateKeyGestureProperty, value); } + } + + public static readonly DependencyProperty DefaultHotkeyProperty = DependencyProperty.Register( + nameof(DefaultHotkey), + typeof(string), + typeof(HotkeyControl), + new PropertyMetadata(default(string)) + ); + + public string DefaultHotkey + { + get { return (string)GetValue(DefaultHotkeyProperty); } + set { SetValue(DefaultHotkeyProperty, value); } + } + + private static void OnHotkeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not HotkeyControl hotkeyControl) + { + return; + } + + hotkeyControl.RefreshHotkeyInterface(hotkeyControl.Hotkey); + } + + public static readonly DependencyProperty ChangeHotkeyProperty = DependencyProperty.Register( + nameof(ChangeHotkey), + typeof(ICommand), + typeof(HotkeyControl), + new PropertyMetadata(default(ICommand)) + ); + + public ICommand? ChangeHotkey + { + get { return (ICommand)GetValue(ChangeHotkeyProperty); } + set { SetValue(ChangeHotkeyProperty, value); } + } + + public static readonly DependencyProperty TypeProperty = DependencyProperty.Register( + nameof(Type), + typeof(HotkeyType), + typeof(HotkeyControl), + new FrameworkPropertyMetadata(HotkeyType.None, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged) + ); + + public HotkeyType Type + { + get { return (HotkeyType)GetValue(TypeProperty); } + set { SetValue(TypeProperty, value); } + } + + public enum HotkeyType + { + None, + // Custom query hotkeys + CustomQueryHotkey, + // Settings hotkeys + Hotkey, + PreviewHotkey, + OpenContextMenuHotkey, + SettingWindowHotkey, + CycleHistoryUpHotkey, + CycleHistoryDownHotkey, + SelectPrevPageHotkey, + SelectNextPageHotkey, + AutoCompleteHotkey, + AutoCompleteHotkey2, + SelectPrevItemHotkey, + SelectPrevItemHotkey2, + SelectNextItemHotkey, + SelectNextItemHotkey2 + } + + // We can initialize settings in static field because it has been constructed in App constuctor + // and it will not construct settings instances twice + private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + + private string hotkey = string.Empty; + public string Hotkey + { + get + { + return Type switch + { + // Custom query hotkeys + HotkeyType.CustomQueryHotkey => hotkey, + // Settings hotkeys + HotkeyType.Hotkey => _settings.Hotkey, + HotkeyType.PreviewHotkey => _settings.PreviewHotkey, + HotkeyType.OpenContextMenuHotkey => _settings.OpenContextMenuHotkey, + HotkeyType.SettingWindowHotkey => _settings.SettingWindowHotkey, + HotkeyType.CycleHistoryUpHotkey => _settings.CycleHistoryUpHotkey, + HotkeyType.CycleHistoryDownHotkey => _settings.CycleHistoryDownHotkey, + HotkeyType.SelectPrevPageHotkey => _settings.SelectPrevPageHotkey, + HotkeyType.SelectNextPageHotkey => _settings.SelectNextPageHotkey, + HotkeyType.AutoCompleteHotkey => _settings.AutoCompleteHotkey, + HotkeyType.AutoCompleteHotkey2 => _settings.AutoCompleteHotkey2, + HotkeyType.SelectPrevItemHotkey => _settings.SelectPrevItemHotkey, + HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2, + HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey, + HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2, + _ => throw new System.NotImplementedException("Hotkey type not set") + }; + } + set + { + switch (Type) + { + // Custom query hotkeys + case HotkeyType.CustomQueryHotkey: + // We just need to store it in a local field + // because we will save to settings in other place + hotkey = value; + break; + // Settings hotkeys + case HotkeyType.Hotkey: + _settings.Hotkey = value; + break; + case HotkeyType.PreviewHotkey: + _settings.PreviewHotkey = value; + break; + case HotkeyType.OpenContextMenuHotkey: + _settings.OpenContextMenuHotkey = value; + break; + case HotkeyType.SettingWindowHotkey: + _settings.SettingWindowHotkey = value; + break; + case HotkeyType.CycleHistoryUpHotkey: + _settings.CycleHistoryUpHotkey = value; + break; + case HotkeyType.CycleHistoryDownHotkey: + _settings.CycleHistoryDownHotkey = value; + break; + case HotkeyType.SelectPrevPageHotkey: + _settings.SelectPrevPageHotkey = value; + break; + case HotkeyType.SelectNextPageHotkey: + _settings.SelectNextPageHotkey = value; + break; + case HotkeyType.AutoCompleteHotkey: + _settings.AutoCompleteHotkey = value; + break; + case HotkeyType.AutoCompleteHotkey2: + _settings.AutoCompleteHotkey2 = value; + break; + case HotkeyType.SelectPrevItemHotkey: + _settings.SelectPrevItemHotkey = value; + break; + case HotkeyType.SelectNextItemHotkey: + _settings.SelectNextItemHotkey = value; + break; + case HotkeyType.SelectPrevItemHotkey2: + _settings.SelectPrevItemHotkey2 = value; + break; + case HotkeyType.SelectNextItemHotkey2: + _settings.SelectNextItemHotkey2 = value; + break; + default: + throw new System.NotImplementedException("Hotkey type not set"); + } + + // After setting the hotkey, we need to refresh the interface + RefreshHotkeyInterface(Hotkey); + } } public HotkeyControl() { InitializeComponent(); + + HotkeyList.ItemsSource = KeysToDisplay; + + // We should not call RefreshHotkeyInterface here because DependencyProperty is not set yet + // And it will be called in OnHotkeyChanged event or Hotkey setter later } - void TbHotkey_OnPreviewKeyDown(object sender, KeyEventArgs e) + private void RefreshHotkeyInterface(string hotkey) { - e.Handled = true; - tbMsg.Visibility = Visibility.Hidden; + SetKeysToDisplay(new HotkeyModel(hotkey)); + CurrentHotkey = new HotkeyModel(hotkey); + } - //when alt is pressed, the real key should be e.SystemKey - Key key = (e.Key == Key.System ? e.SystemKey : e.Key); + private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => + hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); - SpecialKeyState specialKeyState = GlobalHotkey.Instance.CheckModifiers(); + public string EmptyHotkey => App.API.GetTranslation("none"); - var hotkeyModel = new HotkeyModel( - specialKeyState.AltPressed, - specialKeyState.ShiftPressed, - specialKeyState.WinPressed, - specialKeyState.CtrlPressed, - key); + public ObservableCollection KeysToDisplay { get; set; } = new(); - var hotkeyString = hotkeyModel.ToString(); + public HotkeyModel CurrentHotkey { get; private set; } = new(false, false, false, false, Key.None); - if (hotkeyString == tbHotkey.Text) + public void GetNewHotkey(object sender, RoutedEventArgs e) + { + _ = OpenHotkeyDialogAsync(); + } + + private async Task OpenHotkeyDialogAsync() + { + if (!string.IsNullOrEmpty(Hotkey)) { - return; + HotKeyMapper.RemoveHotkey(Hotkey); } - Dispatcher.InvokeAsync(async () => + var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle); + await dialog.ShowAsync(); + switch (dialog.ResultType) { - await Task.Delay(500); - SetHotkey(hotkeyModel); - }); + case HotkeyControlDialog.EResultType.Cancel: + SetHotkey(Hotkey); + return; + case HotkeyControlDialog.EResultType.Save: + SetHotkey(dialog.ResultValue); + break; + case HotkeyControlDialog.EResultType.Delete: + Delete(); + break; + } } - public void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true) + private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true) { - CurrentHotkey = keyModel; - - tbHotkey.Text = CurrentHotkey.ToString(); - tbHotkey.Select(tbHotkey.Text.Length, 0); - if (triggerValidate) { - CurrentHotkeyAvailable = CheckHotkeyAvailability(); - if (!CurrentHotkeyAvailable) + bool hotkeyAvailable; + // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157 + if (keyModel.ToString() == "LWin" || keyModel.ToString() == "RWin") { - tbMsg.Foreground = new SolidColorBrush(Colors.Red); - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable"); + hotkeyAvailable = true; } else { - tbMsg.Foreground = new SolidColorBrush(Colors.Green); - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success"); + hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture); } - tbMsg.Visibility = Visibility.Visible; - OnHotkeyChanged(); + + if (!hotkeyAvailable) + { + return; + } + + Hotkey = keyModel.ToString(); + SetKeysToDisplay(CurrentHotkey); + ChangeHotkey?.Execute(keyModel); + } + else + { + Hotkey = keyModel.ToString(); + ChangeHotkey?.Execute(keyModel); } } - public void SetHotkey(string keyStr, bool triggerValidate = true) + public void Delete() + { + if (!string.IsNullOrEmpty(Hotkey)) + HotKeyMapper.RemoveHotkey(Hotkey); + Hotkey = ""; + SetKeysToDisplay(new HotkeyModel(false, false, false, false, Key.None)); + } + + private void SetKeysToDisplay(HotkeyModel? hotkey) + { + KeysToDisplay.Clear(); + + if (hotkey == null || hotkey == default(HotkeyModel)) + { + KeysToDisplay.Add(EmptyHotkey); + return; + } + + foreach (var key in hotkey.Value.EnumerateDisplayKeys()!) + { + KeysToDisplay.Add(key); + } + } + + public void SetHotkey(string? keyStr, bool triggerValidate = true) { SetHotkey(new HotkeyModel(keyStr), triggerValidate); } - - private bool CheckHotkeyAvailability() - { - try - { - HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", CurrentHotkey.CharKey, CurrentHotkey.ModifierKeys, (sender, e) => { }); - - return true; - } - catch - { - } - finally - { - HotkeyManager.Current.Remove("HotkeyAvailabilityTest"); - } - - return false; - } - - public new bool IsFocused - { - get { return tbHotkey.IsFocused; } - } } } diff --git a/Flow.Launcher/HotkeyControlDialog.xaml b/Flow.Launcher/HotkeyControlDialog.xaml new file mode 100644 index 000000000..9a5872f4d --- /dev/null +++ b/Flow.Launcher/HotkeyControlDialog.xaml @@ -0,0 +1,169 @@ + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - diff --git a/Flow.Launcher/PriorityChangeWindow.xaml.cs b/Flow.Launcher/PriorityChangeWindow.xaml.cs deleted file mode 100644 index 0adb1f080..000000000 --- a/Flow.Launcher/PriorityChangeWindow.xaml.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; -using Flow.Launcher.ViewModel; -using System; -using System.Collections.Generic; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; - -namespace Flow.Launcher -{ - /// - /// Interaction Logic of PriorityChangeWindow.xaml - /// - public partial class PriorityChangeWindow : Window - { - private readonly PluginPair plugin; - private Settings settings; - private readonly Internationalization translater = InternationalizationManager.Instance; - private readonly PluginViewModel pluginViewModel; - - public PriorityChangeWindow(string pluginId, Settings settings, PluginViewModel pluginViewModel) - { - InitializeComponent(); - plugin = PluginManager.GetPluginForId(pluginId); - this.settings = settings; - this.pluginViewModel = pluginViewModel; - if (plugin == null) - { - MessageBox.Show(translater.GetTranslation("cannotFindSpecifiedPlugin")); - Close(); - } - } - - private void BtnCancel_OnClick(object sender, RoutedEventArgs e) - { - Close(); - } - - private void btnDone_OnClick(object sender, RoutedEventArgs e) - { - if (int.TryParse(tbAction.Text.Trim(), out var newPriority)) - { - pluginViewModel.ChangePriority(newPriority); - Close(); - } - else - { - string msg = translater.GetTranslation("invalidPriority"); - MessageBox.Show(msg); - } - - } - - private void PriorityChangeWindow_Loaded(object sender, RoutedEventArgs e) - { - OldPriority.Text = pluginViewModel.Priority.ToString(); - tbAction.Focus(); - } - } -} \ No newline at end of file diff --git a/Flow.Launcher/ProgressBoxEx.xaml b/Flow.Launcher/ProgressBoxEx.xaml new file mode 100644 index 000000000..c17f8b61d --- /dev/null +++ b/Flow.Launcher/ProgressBoxEx.xaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/ReportWindow.xaml.cs b/Flow.Launcher/ReportWindow.xaml.cs index 7318fe4cd..6fe90783e 100644 --- a/Flow.Launcher/ReportWindow.xaml.cs +++ b/Flow.Launcher/ReportWindow.xaml.cs @@ -1,5 +1,5 @@ -using System; -using System.Diagnostics; +using Flow.Launcher.Core.ExternalPlugins; +using System; using System.Globalization; using System.IO; using System.Text; @@ -22,20 +22,42 @@ namespace Flow.Launcher SetException(exception); } + private static string GetIssuesUrl(string website) + { + if (!website.StartsWith("https://github.com")) + { + return website; + } + if(website.Contains("Flow-Launcher/Flow.Launcher")) + { + return Constant.IssuesUrl; + } + var treeIndex = website.IndexOf("tree", StringComparison.Ordinal); + return treeIndex == -1 ? $"{website}/issues" : $"{website[..treeIndex]}/issues"; + } + private void SetException(Exception exception) { string path = Log.CurrentLogDirectory; var directory = new DirectoryInfo(path); var log = directory.GetFiles().OrderByDescending(f => f.LastWriteTime).First(); - var paragraph = Hyperlink("Please open new issue in: ", Constant.Issue); - paragraph.Inlines.Add($"1. upload log file: {log.FullName}\n"); - paragraph.Inlines.Add($"2. copy below exception message"); + var websiteUrl = exception switch + { + FlowPluginException pluginException =>GetIssuesUrl(pluginException.Metadata.Website), + _ => Constant.IssuesUrl + }; + + var paragraph = Hyperlink(App.API.GetTranslation("reportWindow_please_open_issue"), websiteUrl); + paragraph.Inlines.Add(string.Format(App.API.GetTranslation("reportWindow_upload_log"), log.FullName)); + paragraph.Inlines.Add("\n"); + paragraph.Inlines.Add(App.API.GetTranslation("reportWindow_copy_below")); ErrorTextbox.Document.Blocks.Add(paragraph); StringBuilder content = new StringBuilder(); content.AppendLine(ErrorReporting.RuntimeInfo()); content.AppendLine(ErrorReporting.DependenciesInfo()); + content.AppendLine(); content.AppendLine($"Date: {DateTime.Now.ToString(CultureInfo.InvariantCulture)}"); content.AppendLine("Exception:"); content.AppendLine(exception.ToString()); @@ -44,22 +66,32 @@ namespace Flow.Launcher ErrorTextbox.Document.Blocks.Add(paragraph); } - private Paragraph Hyperlink(string textBeforeUrl, string url) + private static Paragraph Hyperlink(string textBeforeUrl, string url) { - var paragraph = new Paragraph(); - paragraph.Margin = new Thickness(0); + var paragraph = new Paragraph + { + Margin = new Thickness(0) + }; - var link = new Hyperlink { IsEnabled = true }; + var link = new Hyperlink + { + IsEnabled = true + }; link.Inlines.Add(url); link.NavigateUri = new Uri(url); - link.RequestNavigate += (s, e) => SearchWeb.NewTabInBrowser(e.Uri.ToString()); - link.Click += (s, e) => SearchWeb.NewTabInBrowser(url); + link.Click += (s, e) => SearchWeb.OpenInBrowserTab(url); paragraph.Inlines.Add(textBeforeUrl); + paragraph.Inlines.Add(" "); paragraph.Inlines.Add(link); paragraph.Inlines.Add("\n"); return paragraph; } + + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + { + Close(); + } } } diff --git a/Flow.Launcher/Resources/Controls/Card.xaml b/Flow.Launcher/Resources/Controls/Card.xaml new file mode 100644 index 000000000..33c1299a9 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/Card.xaml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/Card.xaml.cs b/Flow.Launcher/Resources/Controls/Card.xaml.cs new file mode 100644 index 000000000..c8f788aca --- /dev/null +++ b/Flow.Launcher/Resources/Controls/Card.xaml.cs @@ -0,0 +1,64 @@ +using System.Windows; +using UserControl = System.Windows.Controls.UserControl; + +namespace Flow.Launcher.Resources.Controls +{ + public partial class Card : UserControl + { + public enum CardType + { + Default, + Inside, + InsideFit + } + + public Card() + { + InitializeComponent(); + } + + public string Title + { + get { return (string)GetValue(TitleProperty); } + set { SetValue(TitleProperty, value); } + } + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register(nameof(Title), typeof(string), typeof(Card), new PropertyMetadata(string.Empty)); + + public string Sub + { + get { return (string)GetValue(SubProperty); } + set { SetValue(SubProperty, value); } + } + public static readonly DependencyProperty SubProperty = + DependencyProperty.Register(nameof(Sub), typeof(string), typeof(Card), new PropertyMetadata(string.Empty)); + + public string Icon + { + get { return (string)GetValue(IconProperty); } + set { SetValue(IconProperty, value); } + } + public static readonly DependencyProperty IconProperty = + DependencyProperty.Register(nameof(Icon), typeof(string), typeof(Card), new PropertyMetadata(string.Empty)); + + /// + /// Gets or sets additional content for the UserControl + /// + public object AdditionalContent + { + get { return (object)GetValue(AdditionalContentProperty); } + set { SetValue(AdditionalContentProperty, value); } + } + public static readonly DependencyProperty AdditionalContentProperty = + DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(Card), + new PropertyMetadata(null)); + public CardType Type + { + get { return (CardType)GetValue(TypeProperty); } + set { SetValue(TypeProperty, value); } + } + public static readonly DependencyProperty TypeProperty = + DependencyProperty.Register(nameof(Type), typeof(CardType), typeof(Card), + new PropertyMetadata(CardType.Default)); + } +} diff --git a/Flow.Launcher/Resources/Controls/CardGroup.xaml b/Flow.Launcher/Resources/Controls/CardGroup.xaml new file mode 100644 index 000000000..f48bf4b6c --- /dev/null +++ b/Flow.Launcher/Resources/Controls/CardGroup.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs b/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs new file mode 100644 index 000000000..b9588275c --- /dev/null +++ b/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; + +namespace Flow.Launcher.Resources.Controls; + +public partial class CardGroup : UserControl +{ + public enum CardGroupPosition + { + NotInGroup, + First, + Middle, + Last + } + + public new ObservableCollection Content + { + get { return (ObservableCollection)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + public static new readonly DependencyProperty ContentProperty = + DependencyProperty.Register(nameof(Content), typeof(ObservableCollection), typeof(CardGroup)); + + public static readonly DependencyProperty PositionProperty = DependencyProperty.RegisterAttached( + "Position", typeof(CardGroupPosition), typeof(CardGroup), + new FrameworkPropertyMetadata(CardGroupPosition.NotInGroup, FrameworkPropertyMetadataOptions.AffectsRender) + ); + + public static void SetPosition(UIElement element, CardGroupPosition value) + { + element.SetValue(PositionProperty, value); + } + + public static CardGroupPosition GetPosition(UIElement element) + { + return (CardGroupPosition)element.GetValue(PositionProperty); + } + + public CardGroup() + { + InitializeComponent(); + Content = new ObservableCollection(); + } +} diff --git a/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs b/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs new file mode 100644 index 000000000..605934e80 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs @@ -0,0 +1,21 @@ +using System.Windows; +using System.Windows.Controls; + +namespace Flow.Launcher.Resources.Controls; + +public class CardGroupCardStyleSelector : StyleSelector +{ + public Style FirstStyle { get; set; } + public Style MiddleStyle { get; set; } + public Style LastStyle { get; set; } + + public override Style SelectStyle(object item, DependencyObject container) + { + var itemsControl = ItemsControl.ItemsControlFromItemContainer(container); + var index = itemsControl.ItemContainerGenerator.IndexFromContainer(container); + + if (index == 0) return FirstStyle; + if (index == itemsControl.Items.Count - 1) return LastStyle; + return MiddleStyle; + } +} diff --git a/Flow.Launcher/Resources/Controls/ExCard.xaml b/Flow.Launcher/Resources/Controls/ExCard.xaml new file mode 100644 index 000000000..a70c0f4ea --- /dev/null +++ b/Flow.Launcher/Resources/Controls/ExCard.xaml @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/ExCard.xaml.cs b/Flow.Launcher/Resources/Controls/ExCard.xaml.cs new file mode 100644 index 000000000..f149951f0 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/ExCard.xaml.cs @@ -0,0 +1,57 @@ +using System.Windows; +using System.Windows.Controls; + +namespace Flow.Launcher.Resources.Controls +{ + public partial class ExCard : UserControl + { + public ExCard() + { + InitializeComponent(); + } + public string Title + { + get { return (string)GetValue(TitleProperty); } + set { SetValue(TitleProperty, value); } + } + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register(nameof(Title), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty)); + + public string Sub + { + get { return (string)GetValue(SubProperty); } + set { SetValue(SubProperty, value); } + } + public static readonly DependencyProperty SubProperty = + DependencyProperty.Register(nameof(Sub), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty)); + + public string Icon + { + get { return (string)GetValue(IconProperty); } + set { SetValue(IconProperty, value); } + } + public static readonly DependencyProperty IconProperty = + DependencyProperty.Register(nameof(Icon), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty)); + + /// + /// Gets or sets additional content for the UserControl + /// + public object AdditionalContent + { + get { return (object)GetValue(AdditionalContentProperty); } + set { SetValue(AdditionalContentProperty, value); } + } + public static readonly DependencyProperty AdditionalContentProperty = + DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(ExCard), + new PropertyMetadata(null)); + + public object SideContent + { + get { return (object)GetValue(SideContentProperty); } + set { SetValue(SideContentProperty, value); } + } + public static readonly DependencyProperty SideContentProperty = + DependencyProperty.Register(nameof(SideContent), typeof(object), typeof(ExCard), + new PropertyMetadata(null)); + } +} diff --git a/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml b/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml new file mode 100644 index 000000000..448efb9cf --- /dev/null +++ b/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml @@ -0,0 +1,74 @@ + + + + + diff --git a/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml.cs b/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml.cs new file mode 100644 index 000000000..9b19ffd86 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/HotkeyDisplay.xaml.cs @@ -0,0 +1,63 @@ +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; + +namespace Flow.Launcher.Resources.Controls +{ + public partial class HotkeyDisplay : UserControl + { + public enum DisplayType + { + Default, + Small + } + + public HotkeyDisplay() + { + InitializeComponent(); + //List stringList =e.NewValue.Split('+').ToList(); + Values = new ObservableCollection(); + KeysControl.ItemsSource = Values; + } + + public string Keys + { + get { return (string)GetValue(KeysProperty); } + set { SetValue(KeysProperty, value); } + } + + public static readonly DependencyProperty KeysProperty = + DependencyProperty.Register(nameof(Keys), typeof(string), typeof(HotkeyDisplay), + new PropertyMetadata(string.Empty, keyChanged)); + + public DisplayType Type + { + get { return (DisplayType)GetValue(TypeProperty); } + set { SetValue(TypeProperty, value); } + } + + public static readonly DependencyProperty TypeProperty = + DependencyProperty.Register(nameof(Type), typeof(DisplayType), typeof(HotkeyDisplay), + new PropertyMetadata(DisplayType.Default)); + + private static void keyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as UserControl; + if (null == control) return; // This should not be possible + + var newValue = e.NewValue as string; + if (null == newValue) return; + + if (d is not HotkeyDisplay hotkeyDisplay) + return; + + hotkeyDisplay.Values.Clear(); + foreach (var key in newValue.Split('+')) + { + hotkeyDisplay.Values.Add(key); + } + } + + public ObservableCollection Values { get; set; } + } +} diff --git a/Flow.Launcher/Resources/Controls/HyperLink.xaml b/Flow.Launcher/Resources/Controls/HyperLink.xaml new file mode 100644 index 000000000..9ea550afd --- /dev/null +++ b/Flow.Launcher/Resources/Controls/HyperLink.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs b/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs new file mode 100644 index 000000000..855cccdbd --- /dev/null +++ b/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs @@ -0,0 +1,39 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Navigation; + +namespace Flow.Launcher.Resources.Controls; + +public partial class HyperLink : UserControl +{ + public static readonly DependencyProperty UriProperty = DependencyProperty.Register( + nameof(Uri), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string)) + ); + + public string Uri + { + get => (string)GetValue(UriProperty); + set => SetValue(UriProperty, value); + } + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register( + nameof(Text), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string)) + ); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public HyperLink() + { + InitializeComponent(); + } + + private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + App.API.OpenUrl(e.Uri); + e.Handled = true; + } +} diff --git a/Flow.Launcher/Resources/Controls/InfoBar.xaml b/Flow.Launcher/Resources/Controls/InfoBar.xaml new file mode 100644 index 000000000..2ddcbdd0c --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InfoBar.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SelectFileManagerWindow.xaml.cs b/Flow.Launcher/SelectFileManagerWindow.xaml.cs new file mode 100644 index 000000000..b7bda4565 --- /dev/null +++ b/Flow.Launcher/SelectFileManagerWindow.xaml.cs @@ -0,0 +1,79 @@ +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.ViewModel; +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Windows; +using System.Windows.Controls; + +namespace Flow.Launcher +{ + public partial class SelectFileManagerWindow : Window, INotifyPropertyChanged + { + private int selectedCustomExplorerIndex; + + public event PropertyChangedEventHandler PropertyChanged; + + public Settings Settings { get; } + + public int SelectedCustomExplorerIndex + { + get => selectedCustomExplorerIndex; set + { + selectedCustomExplorerIndex = value; + PropertyChanged?.Invoke(this, new(nameof(CustomExplorer))); + } + } + public ObservableCollection CustomExplorers { get; set; } + + public CustomExplorerViewModel CustomExplorer => CustomExplorers[SelectedCustomExplorerIndex]; + public SelectFileManagerWindow(Settings settings) + { + Settings = settings; + CustomExplorers = new ObservableCollection(Settings.CustomExplorerList.Select(x => x.Copy())); + SelectedCustomExplorerIndex = Settings.CustomExplorerIndex; + InitializeComponent(); + } + + private void btnCancel_Click(object sender, RoutedEventArgs e) + { + Close(); + } + + private void btnDone_Click(object sender, RoutedEventArgs e) + { + Settings.CustomExplorerList = CustomExplorers.ToList(); + Settings.CustomExplorerIndex = SelectedCustomExplorerIndex; + Close(); + } + + private void btnAdd_Click(object sender, RoutedEventArgs e) + { + CustomExplorers.Add(new() + { + Name = "New Profile" + }); + SelectedCustomExplorerIndex = CustomExplorers.Count - 1; + } + + private void btnDelete_Click(object sender, RoutedEventArgs e) + { + CustomExplorers.RemoveAt(SelectedCustomExplorerIndex--); + } + + private void btnBrowseFile_Click(object sender, RoutedEventArgs e) + { + Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog(); + Nullable result = dlg.ShowDialog(); + + if (result == true) + { + TextBox path = (TextBox)(((FrameworkElement)sender).Parent as FrameworkElement).FindName("PathTextBox"); + path.Text = dlg.FileName; + path.Focus(); + ((Button)sender).Focus(); + } + } + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs b/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs new file mode 100644 index 000000000..c8c119e94 --- /dev/null +++ b/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.SettingPages.ViewModels; + +public class DropdownDataGeneric : BaseModel where TValue : Enum +{ + public string Display { get; set; } + public TValue Value { get; private init; } + public string LocalizationKey { get; set; } + + public static List GetValues(string keyPrefix) where TR : DropdownDataGeneric, new() + { + var data = new List(); + var enumValues = (TValue[])Enum.GetValues(typeof(TValue)); + + foreach (var value in enumValues) + { + var key = keyPrefix + value; + var display = App.API.GetTranslation(key); + data.Add(new TR { Display = display, Value = value, LocalizationKey = key }); + } + + return data; + } + + public static void UpdateLabels(List options) where TR : DropdownDataGeneric + { + foreach (var item in options) + { + item.Display = App.API.GetTranslation(item.LocalizationKey); + } + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs new file mode 100644 index 000000000..6cfb98306 --- /dev/null +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Core; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.SettingPages.ViewModels; + +public partial class SettingsPaneAboutViewModel : BaseModel +{ + private static readonly string ClassName = nameof(SettingsPaneAboutViewModel); + + private readonly Settings _settings; + private readonly Updater _updater; + + public string LogFolderSize + { + get + { + var size = GetLogFiles().Sum(file => file.Length); + return $"{App.API.GetTranslation("clearlogfolder")} ({BytesToReadableString(size)})"; + } + } + + public string CacheFolderSize + { + get + { + var size = GetCacheFiles().Sum(file => file.Length); + return $"{App.API.GetTranslation("clearcachefolder")} ({BytesToReadableString(size)})"; + } + } + + public string Website => Constant.Website; + public string SponsorPage => Constant.SponsorPage; + public string ReleaseNotes => _updater.GitHubRepository + "/releases/latest"; + public string Documentation => Constant.Documentation; + public string Docs => Constant.Docs; + public string Github => Constant.GitHub; + + public string Version => Constant.Version switch + { + "1.0.0" => Constant.Dev, + _ => Constant.Version + }; + + public string ActivatedTimes => string.Format( + App.API.GetTranslation("about_activate_times"), + _settings.ActivateTimes + ); + + public class LogLevelData : DropdownDataGeneric { } + + public List LogLevels { get; } = + DropdownDataGeneric.GetValues("LogLevel"); + + public LOGLEVEL LogLevel + { + get => _settings.LogLevel; + set + { + if (_settings.LogLevel != value) + { + _settings.LogLevel = value; + + Log.SetLogLevel(value); + } + } + } + + public SettingsPaneAboutViewModel(Settings settings, Updater updater) + { + _settings = settings; + _updater = updater; + UpdateEnumDropdownLocalizations(); + } + + private void UpdateEnumDropdownLocalizations() + { + DropdownDataGeneric.UpdateLabels(LogLevels); + } + + [RelayCommand] + private void OpenWelcomeWindow() + { + var window = new WelcomeWindow(); + window.ShowDialog(); + } + + [RelayCommand] + private void AskClearLogFolderConfirmation() + { + var confirmResult = App.API.ShowMsgBox( + App.API.GetTranslation("clearlogfolderMessage"), + App.API.GetTranslation("clearlogfolder"), + MessageBoxButton.YesNo + ); + + if (confirmResult == MessageBoxResult.Yes) + { + if (!ClearLogFolder()) + { + App.API.ShowMsgBox(App.API.GetTranslation("clearfolderfailMessage")); + } + } + } + + [RelayCommand] + private void AskClearCacheFolderConfirmation() + { + var confirmResult = App.API.ShowMsgBox( + App.API.GetTranslation("clearcachefolderMessage"), + App.API.GetTranslation("clearcachefolder"), + MessageBoxButton.YesNo + ); + + if (confirmResult == MessageBoxResult.Yes) + { + if (!ClearCacheFolder()) + { + App.API.ShowMsgBox(App.API.GetTranslation("clearfolderfailMessage")); + } + } + } + + [RelayCommand] + private void OpenSettingsFolder() + { + App.API.OpenDirectory(DataLocation.SettingsDirectory); + } + + [RelayCommand] + private void OpenParentOfSettingsFolder(object parameter) + { + string settingsFolderPath = Path.Combine(DataLocation.SettingsDirectory); + string parentFolderPath = Path.GetDirectoryName(settingsFolderPath); + App.API.OpenDirectory(parentFolderPath); + } + + [RelayCommand] + private void OpenLogsFolder() + { + App.API.OpenDirectory(GetLogDir(Constant.Version).FullName); + } + + [RelayCommand] + private Task UpdateAppAsync() => _updater.UpdateAppAsync(false); + + private bool ClearLogFolder() + { + var success = true; + var logDirectory = GetLogDir(); + var logFiles = GetLogFiles(); + + logFiles.ForEach(f => + { + try + { + f.Delete(); + } + catch (Exception e) + { + App.API.LogException(ClassName, $"Failed to delete log file: {f.Name}", e); + success = false; + } + }); + + logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly) + // Do not clean log files of current version + .Where(dir => !Constant.Version.Equals(dir.Name)) + .ToList() + .ForEach(dir => + { + try + { + dir.Delete(true); + } + catch (Exception e) + { + App.API.LogException(ClassName, $"Failed to delete log directory: {dir.Name}", e); + success = false; + } + }); + + OnPropertyChanged(nameof(LogFolderSize)); + + return success; + } + + private static DirectoryInfo GetLogDir(string version = "") + { + return new DirectoryInfo(Path.Combine(DataLocation.LogDirectory, version)); + } + + private static List GetLogFiles(string version = "") + { + return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList(); + } + + private bool ClearCacheFolder() + { + var success = true; + var cacheDirectory = GetCacheDir(); + var cacheFiles = GetCacheFiles(); + + cacheFiles.ForEach(f => + { + try + { + f.Delete(); + } + catch (Exception e) + { + App.API.LogException(ClassName, $"Failed to delete cache file: {f.Name}", e); + success = false; + } + }); + + cacheDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly) + .ToList() + .ForEach(dir => + { + try + { + dir.Delete(true); + } + catch (Exception e) + { + App.API.LogException(ClassName, $"Failed to delete cache directory: {dir.Name}", e); + success = false; + } + }); + + OnPropertyChanged(nameof(CacheFolderSize)); + + return success; + } + + private static DirectoryInfo GetCacheDir() + { + return new DirectoryInfo(DataLocation.CacheDirectory); + } + + private static List GetCacheFiles() + { + return GetCacheDir().EnumerateFiles("*", SearchOption.AllDirectories).ToList(); + } + + private static string BytesToReadableString(long bytes) + { + const int scale = 1024; + string[] orders = { "GB", "MB", "KB", "B" }; + long max = (long)Math.Pow(scale, orders.Length - 1); + + foreach (string order in orders) + { + if (bytes > max) return $"{decimal.Divide(bytes, max):##.##} {order}"; + + max /= scale; + } + + return "0 B"; + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs new file mode 100644 index 000000000..021c9d7fe --- /dev/null +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Core; +using Flow.Launcher.Core.Configuration; +using Flow.Launcher.Core.Resource; +using Flow.Launcher.Helper; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; + +namespace Flow.Launcher.SettingPages.ViewModels; + +public partial class SettingsPaneGeneralViewModel : BaseModel +{ + public Settings Settings { get; } + private readonly Updater _updater; + private readonly IPortable _portable; + private readonly Internationalization _translater; + + public SettingsPaneGeneralViewModel(Settings settings, Updater updater, IPortable portable, Internationalization translater) + { + Settings = settings; + _updater = updater; + _portable = portable; + _translater = translater; + UpdateEnumDropdownLocalizations(); + } + + public class SearchWindowScreenData : DropdownDataGeneric { } + public class SearchWindowAlignData : DropdownDataGeneric { } + public class SearchPrecisionData : DropdownDataGeneric { } + public class LastQueryModeData : DropdownDataGeneric { } + + public bool StartFlowLauncherOnSystemStartup + { + get => Settings.StartFlowLauncherOnSystemStartup; + set + { + Settings.StartFlowLauncherOnSystemStartup = value; + + try + { + if (value) + { + if (UseLogonTaskForStartup) + { + AutoStartup.EnableViaLogonTask(); + } + else + { + AutoStartup.EnableViaRegistry(); + } + } + else + { + AutoStartup.DisableViaLogonTaskAndRegistry(); + } + } + catch (Exception e) + { + App.API.ShowMsg(App.API.GetTranslation("setAutoStartFailed"), e.Message); + } + } + } + + public bool UseLogonTaskForStartup + { + get => Settings.UseLogonTaskForStartup; + set + { + Settings.UseLogonTaskForStartup = value; + + if (StartFlowLauncherOnSystemStartup) + { + try + { + if (UseLogonTaskForStartup) + { + AutoStartup.ChangeToViaLogonTask(); + } + else + { + AutoStartup.ChangeToViaRegistry(); + } + } + catch (Exception e) + { + App.API.ShowMsg(App.API.GetTranslation("setAutoStartFailed"), e.Message); + } + } + } + } + + public List SearchWindowScreens { get; } = + DropdownDataGeneric.GetValues("SearchWindowScreen"); + + public List SearchWindowAligns { get; } = + DropdownDataGeneric.GetValues("SearchWindowAlign"); + + public List SearchPrecisionScores { get; } = + DropdownDataGeneric.GetValues("SearchPrecision"); + + public List ScreenNumbers + { + get + { + var screens = Screen.AllScreens; + var screenNumbers = new List(); + for (int i = 1; i <= screens.Length; i++) + { + screenNumbers.Add(i); + } + + return screenNumbers; + } + } + + // This is only required to set at startup. When portable mode enabled/disabled a restart is always required + private static bool _portableMode = DataLocation.PortableDataLocationInUse(); + + public bool PortableMode + { + get => _portableMode; + set + { + if (!_portable.CanUpdatePortability()) + return; + + if (DataLocation.PortableDataLocationInUse()) + { + _portable.DisablePortableMode(); + } + else + { + _portable.EnablePortableMode(); + } + } + } + + public List LastQueryModes { get; } = + DropdownDataGeneric.GetValues("LastQuery"); + + public int SearchDelayTimeValue + { + get => Settings.SearchDelayTime; + set + { + if (Settings.SearchDelayTime != value) + { + Settings.SearchDelayTime = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(SearchDelayTimeDisplay)); + } + } + } + public string SearchDelayTimeDisplay => $"{SearchDelayTimeValue}ms"; + + private void UpdateEnumDropdownLocalizations() + { + DropdownDataGeneric.UpdateLabels(SearchWindowScreens); + DropdownDataGeneric.UpdateLabels(SearchWindowAligns); + DropdownDataGeneric.UpdateLabels(SearchPrecisionScores); + DropdownDataGeneric.UpdateLabels(LastQueryModes); + } + + public string Language + { + get => Settings.Language; + set + { + _translater.ChangeLanguage(value); + + if (_translater.PromptShouldUsePinyin(value)) + ShouldUsePinyin = true; + + UpdateEnumDropdownLocalizations(); + } + } + + #region Korean IME + + // The new Korean IME used in Windows 11 has compatibility issues with WPF. This issue is difficult to resolve within + // WPF itself, but it can be avoided by having the user switch to the legacy IME at the system level. Therefore, + // we provide guidance and a direct button for users to make this change themselves. If the relevant registry key does + // not exist (i.e., the Korean IME is not installed), this setting will not be shown at all. + + public bool LegacyKoreanIMEEnabled + { + get => Win32Helper.IsLegacyKoreanIMEEnabled(); + set + { + if (Win32Helper.SetLegacyKoreanIMEEnabled(value)) + { + OnPropertyChanged(); + OnPropertyChanged(nameof(KoreanIMERegistryValueIsZero)); + } + else + { + //Since this is rarely seen text, language support is not provided. + App.API.ShowMsg("Failed to change Korean IME setting", "Please check your system registry access or contact support."); + } + } + } + + public bool KoreanIMERegistryKeyExists + { + get + { + var registryKeyExists = Win32Helper.IsKoreanIMEExist(); + var koreanLanguageInstalled = InputLanguage.InstalledInputLanguages.Cast().Any(lang => lang.Culture.Name.StartsWith("ko")); + var isWindows11 = Win32Helper.IsWindows11(); + + // Return true if Windows 11 with Korean IME installed, or if the registry key exists + return (isWindows11 && koreanLanguageInstalled) || registryKeyExists; + } + } + + public bool KoreanIMERegistryValueIsZero + { + get + { + var value = Win32Helper.GetLegacyKoreanIMERegistryValue(); + if (value is int intValue) + { + return intValue == 0; + } + else if (value != null && int.TryParse(value.ToString(), out var parsedValue)) + { + return parsedValue == 0; + } + + return false; + } + } + + [RelayCommand] + private void OpenImeSettings() + { + Win32Helper.OpenImeSettings(); + } + + #endregion + + public bool ShouldUsePinyin + { + get => Settings.ShouldUsePinyin; + set => Settings.ShouldUsePinyin = value; + } + + public List Languages => _translater.LoadAvailableLanguages(); + + public string AlwaysPreviewToolTip => string.Format( + App.API.GetTranslation("AlwaysPreviewToolTip"), + Settings.PreviewHotkey + ); + + private static 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 + }; + + return dlg.ShowDialog() switch + { + DialogResult.OK => dlg.FileName, + _ => string.Empty + }; + } + + private void UpdateApp() + { + _ = _updater.UpdateAppAsync(false); + } + + public bool AutoUpdates + { + get => Settings.AutoUpdates; + set + { + Settings.AutoUpdates = value; + + if (value) + { + UpdateApp(); + } + } + } + + [RelayCommand] + private void SelectPython() + { + var selectedFile = GetFileFromDialog( + App.API.GetTranslation("selectPythonExecutable"), + "Python|pythonw.exe" + ); + + if (!string.IsNullOrEmpty(selectedFile)) + Settings.PluginSettings.PythonExecutablePath = selectedFile; + } + + [RelayCommand] + private void SelectNode() + { + var selectedFile = GetFileFromDialog( + App.API.GetTranslation("selectNodeExecutable"), + "node|*.exe" + ); + + if (!string.IsNullOrEmpty(selectedFile)) + Settings.PluginSettings.NodeExecutablePath = selectedFile; + } + + [RelayCommand] + private void SelectFileManager() + { + var fileManagerChangeWindow = new SelectFileManagerWindow(Settings); + fileManagerChangeWindow.ShowDialog(); + } + + [RelayCommand] + private void SelectBrowser() + { + var browserWindow = new SelectBrowserWindow(Settings); + browserWindow.ShowDialog(); + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs new file mode 100644 index 000000000..7a7c19dd3 --- /dev/null +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs @@ -0,0 +1,140 @@ +using System.Linq; +using System.Windows; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Helper; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.SettingPages.ViewModels; + +public partial class SettingsPaneHotkeyViewModel : BaseModel +{ + public Settings Settings { get; } + + public CustomPluginHotkey SelectedCustomPluginHotkey { get; set; } + public CustomShortcutModel SelectedCustomShortcut { get; set; } + + public string[] OpenResultModifiersList => new[] + { + KeyConstant.Alt, + KeyConstant.Ctrl, + $"{KeyConstant.Ctrl}+{KeyConstant.Alt}" + }; + + public SettingsPaneHotkeyViewModel(Settings settings) + { + Settings = settings; + } + + [RelayCommand] + private void SetTogglingHotkey(HotkeyModel hotkey) + { + HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey); + } + + [RelayCommand] + private void CustomHotkeyDelete() + { + var item = SelectedCustomPluginHotkey; + if (item is null) + { + App.API.ShowMsgBox(App.API.GetTranslation("pleaseSelectAnItem")); + return; + } + + var result = App.API.ShowMsgBox( + string.Format( + App.API.GetTranslation("deleteCustomHotkeyWarning"), item.Hotkey + ), + App.API.GetTranslation("delete"), + MessageBoxButton.YesNo + ); + + if (result is MessageBoxResult.Yes) + { + Settings.CustomPluginHotkeys.Remove(item); + HotKeyMapper.RemoveHotkey(item.Hotkey); + } + } + + [RelayCommand] + private void CustomHotkeyEdit() + { + var item = SelectedCustomPluginHotkey; + if (item is null) + { + App.API.ShowMsgBox(App.API.GetTranslation("pleaseSelectAnItem")); + return; + } + + var window = new CustomQueryHotkeySetting(Settings); + window.UpdateItem(item); + window.ShowDialog(); + } + + [RelayCommand] + private void CustomHotkeyAdd() + { + new CustomQueryHotkeySetting(Settings).ShowDialog(); + } + + [RelayCommand] + private void CustomShortcutDelete() + { + var item = SelectedCustomShortcut; + if (item is null) + { + App.API.ShowMsgBox(App.API.GetTranslation("pleaseSelectAnItem")); + return; + } + + var result = App.API.ShowMsgBox( + string.Format( + App.API.GetTranslation("deleteCustomShortcutWarning"), item.Key, item.Value + ), + App.API.GetTranslation("delete"), + MessageBoxButton.YesNo + ); + + if (result is MessageBoxResult.Yes) + { + Settings.CustomShortcuts.Remove(item); + } + } + + [RelayCommand] + private void CustomShortcutEdit() + { + var item = SelectedCustomShortcut; + if (item is null) + { + App.API.ShowMsgBox(App.API.GetTranslation("pleaseSelectAnItem")); + return; + } + + var window = new CustomShortcutSetting(item.Key, item.Value, this); + if (window.ShowDialog() is not true) return; + + var index = Settings.CustomShortcuts.IndexOf(item); + Settings.CustomShortcuts[index] = new CustomShortcutModel(window.Key, window.Value); + } + + [RelayCommand] + private void CustomShortcutAdd() + { + var window = new CustomShortcutSetting(this); + if (window.ShowDialog() is true) + { + var shortcut = new CustomShortcutModel(window.Key, window.Value); + Settings.CustomShortcuts.Add(shortcut); + } + } + + internal bool DoesShortcutExist(string key) + { + return Settings.CustomShortcuts.Any(v => v.Key == key) || + Settings.BuiltinShortcuts.Any(v => v.Key == key); + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs new file mode 100644 index 000000000..fd2c8e09f --- /dev/null +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin; +using Flow.Launcher.ViewModel; + +namespace Flow.Launcher.SettingPages.ViewModels; + +public partial class SettingsPanePluginStoreViewModel : BaseModel +{ + public string FilterText { get; set; } = string.Empty; + + public IList ExternalPlugins => + App.API.GetPluginManifest()?.Select(p => new PluginStoreItemViewModel(p)) + .OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease) + .ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated) + .ThenByDescending(p => p.Category == PluginStoreItemViewModel.None) + .ThenByDescending(p => p.Category == PluginStoreItemViewModel.Installed) + .ToList(); + + [RelayCommand] + private async Task RefreshExternalPluginsAsync() + { + if (await App.API.UpdatePluginManifestAsync()) + { + OnPropertyChanged(nameof(ExternalPlugins)); + } + } + + public bool SatisfiesFilter(PluginStoreItemViewModel plugin) + { + return string.IsNullOrEmpty(FilterText) || + App.API.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() || + App.API.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet(); + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs new file mode 100644 index 000000000..b89e970e9 --- /dev/null +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs @@ -0,0 +1,191 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.ViewModel; +using ModernWpf.Controls; + +#nullable enable + +namespace Flow.Launcher.SettingPages.ViewModels; + +public partial class SettingsPanePluginsViewModel : BaseModel +{ + private readonly Settings _settings; + + public class DisplayModeData : DropdownDataGeneric { } + + public List DisplayModes { get; } = + DropdownDataGeneric.GetValues("DisplayMode"); + + private DisplayMode _selectedDisplayMode = DisplayMode.OnOff; + public DisplayMode SelectedDisplayMode + { + get => _selectedDisplayMode; + set + { + if (_selectedDisplayMode != value) + { + _selectedDisplayMode = value; + OnPropertyChanged(); + UpdateDisplayModeFromSelection(); + } + } + } + + private bool _isOnOffSelected = true; + public bool IsOnOffSelected + { + get => _isOnOffSelected; + set + { + if (_isOnOffSelected != value) + { + _isOnOffSelected = value; + OnPropertyChanged(); + } + } + } + + private bool _isPrioritySelected; + public bool IsPrioritySelected + { + get => _isPrioritySelected; + set + { + if (_isPrioritySelected != value) + { + _isPrioritySelected = value; + OnPropertyChanged(); + } + } + } + + private bool _isSearchDelaySelected; + public bool IsSearchDelaySelected + { + get => _isSearchDelaySelected; + set + { + if (_isSearchDelaySelected != value) + { + _isSearchDelaySelected = value; + OnPropertyChanged(); + } + } + } + + public SettingsPanePluginsViewModel(Settings settings) + { + _settings = settings; + UpdateEnumDropdownLocalizations(); + } + + public string FilterText { get; set; } = string.Empty; + + public PluginViewModel? SelectedPlugin { get; set; } + + private IEnumerable? _pluginViewModels; + private IEnumerable PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins + .OrderBy(plugin => plugin.Metadata.Disabled) + .ThenBy(plugin => plugin.Metadata.Name) + .Select(plugin => new PluginViewModel + { + PluginPair = plugin, + PluginSettingsObject = _settings.PluginSettings.GetPluginSettings(plugin.Metadata.ID) + }) + .Where(plugin => plugin.PluginSettingsObject != null) + .ToList(); + + public List FilteredPluginViewModels => PluginViewModels + .Where(v => + string.IsNullOrEmpty(FilterText) || + App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() || + App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet() + ) + .ToList(); + + [RelayCommand] + private async Task OpenHelperAsync() + { + var helpDialog = new ContentDialog() + { + Content = new StackPanel + { + Children = + { + new TextBlock + { + Text = (string)Application.Current.Resources["priority"], + FontSize = 18, + Margin = new Thickness(0, 0, 0, 10), + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = (string)Application.Current.Resources["priority_tips"], + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = (string)Application.Current.Resources["searchDelay"], + FontSize = 18, + Margin = new Thickness(0, 24, 0, 10), + TextWrapping = TextWrapping.Wrap + }, + new TextBlock + { + Text = (string)Application.Current.Resources["searchDelayTimeTips"], + TextWrapping = TextWrapping.Wrap + } + } + }, + + PrimaryButtonText = (string)Application.Current.Resources["commonOK"], + CornerRadius = new CornerRadius(8), + Style = (Style)Application.Current.Resources["ContentDialog"] + }; + + await helpDialog.ShowAsync(); + } + + private void UpdateEnumDropdownLocalizations() + { + DropdownDataGeneric.UpdateLabels(DisplayModes); + } + + private void UpdateDisplayModeFromSelection() + { + switch (SelectedDisplayMode) + { + case DisplayMode.Priority: + IsOnOffSelected = false; + IsPrioritySelected = true; + IsSearchDelaySelected = false; + break; + case DisplayMode.SearchDelay: + IsOnOffSelected = false; + IsPrioritySelected = false; + IsSearchDelaySelected = true; + break; + default: + IsOnOffSelected = true; + IsPrioritySelected = false; + IsSearchDelaySelected = false; + break; + } + } +} + +public enum DisplayMode +{ + OnOff, + Priority, + SearchDelay +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs new file mode 100644 index 000000000..38a6ef31e --- /dev/null +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs @@ -0,0 +1,60 @@ +using System.Net; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Core; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.SettingPages.ViewModels; + +public partial class SettingsPaneProxyViewModel : BaseModel +{ + private readonly Updater _updater; + public Settings Settings { get; } + + public SettingsPaneProxyViewModel(Settings settings, Updater updater) + { + _updater = updater; + Settings = settings; + } + + [RelayCommand] + private void OnTestProxyClicked() + { + var message = TestProxy(); + App.API.ShowMsgBox(App.API.GetTranslation(message)); + } + + private string TestProxy() + { + if (string.IsNullOrEmpty(Settings.Proxy.Server)) return "serverCantBeEmpty"; + if (Settings.Proxy.Port <= 0) return "portCantBeEmpty"; + + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository); + + if (string.IsNullOrEmpty(Settings.Proxy.UserName) || string.IsNullOrEmpty(Settings.Proxy.Password)) + { + request.Proxy = new WebProxy(Settings.Proxy.Server, Settings.Proxy.Port); + } + else + { + request.Proxy = new WebProxy(Settings.Proxy.Server, Settings.Proxy.Port) + { + Credentials = new NetworkCredential(Settings.Proxy.UserName, Settings.Proxy.Password) + }; + } + + try + { + var response = (HttpWebResponse)request.GetResponse(); + return response.StatusCode switch + { + HttpStatusCode.OK => "proxyIsCorrect", + _ => "proxyConnectFailed" + }; + } + catch + { + return "proxyConnectFailed"; + } + } +} diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs new file mode 100644 index 000000000..f78704ef2 --- /dev/null +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs @@ -0,0 +1,510 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Windows.Media; +using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Core.Resource; +using Flow.Launcher.Helper; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; +using Flow.Launcher.ViewModel; +using ModernWpf; +using ThemeManagerForColorSchemeSwitch = ModernWpf.ThemeManager; + +namespace Flow.Launcher.SettingPages.ViewModels; + +public partial class SettingsPaneThemeViewModel : BaseModel +{ + private const string DefaultFont = "Segoe UI"; + public string BackdropSubText => !Win32Helper.IsBackdropSupported() ? App.API.GetTranslation("BackdropTypeDisabledToolTip") : ""; + public Settings Settings { get; } + private readonly Theme _theme = Ioc.Default.GetRequiredService(); + + public static string LinkHowToCreateTheme => @"https://www.flowlauncher.com/theme-builder/"; + public static string LinkThemeGallery => "https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438"; + + private List _themes; + public List Themes => _themes ??= App.API.GetAvailableThemes(); + + private ThemeData _selectedTheme; + public ThemeData SelectedTheme + { + get => _selectedTheme ??= Themes.Find(v => v == App.API.GetCurrentTheme()); + set + { + _selectedTheme = value; + App.API.SetCurrentTheme(value); + + // Update UI state + OnPropertyChanged(nameof(BackdropType)); + OnPropertyChanged(nameof(IsBackdropEnabled)); + OnPropertyChanged(nameof(IsDropShadowEnabled)); + OnPropertyChanged(nameof(DropShadowEffect)); + } + } + + public bool IsBackdropEnabled + { + get + { + if (!Win32Helper.IsBackdropSupported()) return false; + return SelectedTheme?.HasBlur ?? false; + } + } + + public bool IsDropShadowEnabled => !_theme.BlurEnabled; + + public bool DropShadowEffect + { + get => Settings.UseDropShadowEffect; + set + { + if (_theme.BlurEnabled) + { + // Always DropShadowEffect = true with blur theme + Settings.UseDropShadowEffect = true; + return; + } + + // User can change shadow with non-blur theme. + if (value) + { + _theme.AddDropShadowEffectToCurrentTheme(); + } + else + { + _theme.RemoveDropShadowEffectFromCurrentTheme(); + } + + Settings.UseDropShadowEffect = value; + OnPropertyChanged(nameof(DropShadowEffect)); + } + } + + public double WindowHeightSize + { + get => Settings.WindowHeightSize; + set => Settings.WindowHeightSize = value; + } + + public double ItemHeightSize + { + get => Settings.ItemHeightSize; + set => Settings.ItemHeightSize = value; + } + + public double QueryBoxFontSize + { + get => Settings.QueryBoxFontSize; + set => Settings.QueryBoxFontSize = value; + } + + public double ResultItemFontSize + { + get => Settings.ResultItemFontSize; + set => Settings.ResultItemFontSize = value; + } + + public double ResultSubItemFontSize + { + get => Settings.ResultSubItemFontSize; + set => Settings.ResultSubItemFontSize = value; + } + + public class ColorSchemeData : DropdownDataGeneric { } + + public List ColorSchemes { get; } = DropdownDataGeneric.GetValues("ColorScheme"); + public string ColorScheme + { + get => Settings.ColorScheme; + set + { + ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme = value switch + { + Constant.Light => ApplicationTheme.Light, + Constant.Dark => ApplicationTheme.Dark, + Constant.System => null, + _ => ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme + }; + Settings.ColorScheme = value; + _ = _theme.RefreshFrameAsync(); + } + } + + public List TimeFormatList { get; } = new() + { + "h:mm", + "hh:mm", + "H:mm", + "HH:mm", + "tt h:mm", + "tt hh:mm", + "h:mm tt", + "hh:mm tt", + "hh:mm:ss tt", + "HH:mm:ss" + }; + + public List DateFormatList { get; } = new() + { + "MM'/'dd dddd", + "MM'/'dd ddd", + "MM'/'dd", + "MM'-'dd", + "MMMM', 'dd", + "dd'/'MM", + "dd'-'MM", + "ddd MM'/'dd", + "dddd MM'/'dd", + "dddd", + "ddd dd'/'MM", + "dddd dd'/'MM", + "dddd dd', 'MMMM", + "dd', 'MMMM", + "dd.MM.yy", + "dd.MM.yyyy" + }; + + public string TimeFormat + { + get => Settings.TimeFormat; + set => Settings.TimeFormat = value; + } + + public string DateFormat + { + get => Settings.DateFormat; + set => Settings.DateFormat = value; + } + + public IEnumerable MaxResultsRange => Enumerable.Range(2, 16); + + public bool KeepMaxResults + { + get => Settings.KeepMaxResults; + set => Settings.KeepMaxResults = value; + } + + public string ClockText => DateTime.Now.ToString(TimeFormat, CultureInfo.CurrentUICulture); + + public string DateText => DateTime.Now.ToString(DateFormat, CultureInfo.CurrentUICulture); + + public bool UseGlyphIcons + { + get => Settings.UseGlyphIcons; + set => Settings.UseGlyphIcons = value; + } + + public bool UseAnimation + { + get => Settings.UseAnimation; + set => Settings.UseAnimation = value; + } + + public class AnimationSpeedData : DropdownDataGeneric { } + public List AnimationSpeeds { get; } = DropdownDataGeneric.GetValues("AnimationSpeed"); + + public class BackdropTypeData : DropdownDataGeneric { } + + public List BackdropTypesList { get; } = + DropdownDataGeneric.GetValues("BackdropTypes"); + + public BackdropTypes BackdropType + { + get => Enum.IsDefined(typeof(BackdropTypes), Settings.BackdropType) + ? Settings.BackdropType + : BackdropTypes.None; + set + { + if (!Enum.IsDefined(typeof(BackdropTypes), value)) + { + value = BackdropTypes.None; + } + + Settings.BackdropType = value; + + // Can only apply blur because drop shadow effect is not supported with backdrop + // So drop shadow effect has been disabled + _ = _theme.SetBlurForWindowAsync(); + + OnPropertyChanged(nameof(IsDropShadowEnabled)); + } + } + + public bool UseSound + { + get => Settings.UseSound; + set => Settings.UseSound = value; + } + + public bool ShowWMPWarning + { + get => !Settings.WMPInstalled && UseSound; + } + + public bool EnableVolumeAdjustment + { + get => Settings.WMPInstalled; + } + + public double SoundEffectVolume + { + get => Settings.SoundVolume; + set => Settings.SoundVolume = value; + } + + public bool ShowPlaceholder + { + get => Settings.ShowPlaceholder; + set => Settings.ShowPlaceholder = value; + } + + public string PlaceholderTextTip + { + get => string.Format(App.API.GetTranslation("PlaceholderTextTip"), App.API.GetTranslation("queryTextBoxPlaceholder")); + } + + public string PlaceholderText + { + get => Settings.PlaceholderText; + set => Settings.PlaceholderText = value; + } + + public bool UseClock + { + get => Settings.UseClock; + set => Settings.UseClock = value; + } + + public bool UseDate + { + get => Settings.UseDate; + set => Settings.UseDate = value; + } + + public Brush PreviewBackground + { + get => WallpaperPathRetrieval.GetWallpaperBrush(); + } + + public ResultsViewModel PreviewResults + { + get + { + var results = new List + { + new() + { + Title = App.API.GetTranslation("SampleTitleExplorer"), + SubTitle = App.API.GetTranslation("SampleSubTitleExplorer"), + IcoPath = Path.Combine( + Constant.ProgramDirectory, + @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png" + ) + }, + new() + { + Title = App.API.GetTranslation("SampleTitleWebSearch"), + SubTitle = App.API.GetTranslation("SampleSubTitleWebSearch"), + IcoPath = Path.Combine( + Constant.ProgramDirectory, + @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png" + ) + }, + new() + { + Title = App.API.GetTranslation("SampleTitleProgram"), + SubTitle = App.API.GetTranslation("SampleSubTitleProgram"), + IcoPath = Path.Combine( + Constant.ProgramDirectory, + @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png" + ) + }, + new() + { + Title = App.API.GetTranslation("SampleTitleProcessKiller"), + SubTitle = App.API.GetTranslation("SampleSubTitleProcessKiller"), + IcoPath = Path.Combine( + Constant.ProgramDirectory, + @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png" + ) + } + }; + var vm = new ResultsViewModel(Settings); + vm.AddResults(results, "PREVIEW"); + return vm; + } + } + + public FontFamily SelectedQueryBoxFont + { + get + { + var fontExists = Fonts.SystemFontFamilies.Any( + fontFamily => + fontFamily.FamilyNames.Values != null && + fontFamily.FamilyNames.Values.Contains(Settings.QueryBoxFont) + ); + + return fontExists switch + { + true => new FontFamily(Settings.QueryBoxFont), + _ => new FontFamily(DefaultFont) + }; + } + set + { + Settings.QueryBoxFont = value.ToString(); + _theme.UpdateFonts(); + } + } + + public FamilyTypeface SelectedQueryBoxFontFaces + { + get + { + var typeface = SyntaxSugars.CallOrRescueDefault( + () => SelectedQueryBoxFont.ConvertFromInvariantStringsOrNormal( + Settings.QueryBoxFontStyle, + Settings.QueryBoxFontWeight, + Settings.QueryBoxFontStretch + ) + ); + return typeface; + } + set + { + Settings.QueryBoxFontStretch = value.Stretch.ToString(); + Settings.QueryBoxFontWeight = value.Weight.ToString(); + Settings.QueryBoxFontStyle = value.Style.ToString(); + _theme.UpdateFonts(); + } + } + + public FontFamily SelectedResultFont + { + get + { + var fontExists = Fonts.SystemFontFamilies.Any( + fontFamily => + fontFamily.FamilyNames.Values != null && + fontFamily.FamilyNames.Values.Contains(Settings.ResultFont) + ); + return fontExists switch + { + true => new FontFamily(Settings.ResultFont), + _ => new FontFamily(DefaultFont) + }; + } + set + { + Settings.ResultFont = value.ToString(); + _theme.UpdateFonts(); + } + } + + public FamilyTypeface SelectedResultFontFaces + { + get + { + var typeface = SyntaxSugars.CallOrRescueDefault( + () => SelectedResultFont.ConvertFromInvariantStringsOrNormal( + Settings.ResultFontStyle, + Settings.ResultFontWeight, + Settings.ResultFontStretch + ) + ); + return typeface; + } + set + { + Settings.ResultFontStretch = value.Stretch.ToString(); + Settings.ResultFontWeight = value.Weight.ToString(); + Settings.ResultFontStyle = value.Style.ToString(); + _theme.UpdateFonts(); + } + } + + public FontFamily SelectedResultSubFont + { + get + { + if (Fonts.SystemFontFamilies.Any(o => + o.FamilyNames.Values != null && + o.FamilyNames.Values.Contains(Settings.ResultSubFont))) + { + var font = new FontFamily(Settings.ResultSubFont); + return font; + } + else + { + var font = new FontFamily(DefaultFont); + return font; + } + } + set + { + Settings.ResultSubFont = value.ToString(); + _theme.UpdateFonts(); + } + } + + public FamilyTypeface SelectedResultSubFontFaces + { + get + { + var typeface = SyntaxSugars.CallOrRescueDefault( + () => SelectedResultSubFont.ConvertFromInvariantStringsOrNormal( + Settings.ResultSubFontStyle, + Settings.ResultSubFontWeight, + Settings.ResultSubFontStretch + )); + return typeface; + } + set + { + Settings.ResultSubFontStretch = value.Stretch.ToString(); + Settings.ResultSubFontWeight = value.Weight.ToString(); + Settings.ResultSubFontStyle = value.Style.ToString(); + _theme.UpdateFonts(); + } + } + + public string ThemeImage => Constant.QueryTextBoxIconImagePath; + + public SettingsPaneThemeViewModel(Settings settings) + { + Settings = settings; + } + + [RelayCommand] + private void OpenThemesFolder() + { + App.API.OpenDirectory(DataLocation.ThemesDirectory); + } + + [RelayCommand] + public void Reset() + { + SelectedQueryBoxFont = new FontFamily(DefaultFont); + SelectedQueryBoxFontFaces = new FamilyTypeface { Stretch = FontStretches.Normal, Weight = FontWeights.Normal, Style = FontStyles.Normal }; + QueryBoxFontSize = 20; + + SelectedResultFont = new FontFamily(DefaultFont); + SelectedResultFontFaces = new FamilyTypeface { Stretch = FontStretches.Normal, Weight = FontWeights.Normal, Style = FontStyles.Normal }; + ResultItemFontSize = 16; + + SelectedResultSubFont = new FontFamily(DefaultFont); + SelectedResultSubFontFaces = new FamilyTypeface { Stretch = FontStretches.Normal, Weight = FontWeights.Normal, Style = FontStyles.Normal }; + ResultSubItemFontSize = 13; + + WindowHeightSize = 42; + ItemHeightSize = 58; + } +} diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml new file mode 100644 index 000000000..f7deee7cf --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs new file mode 100644 index 000000000..1ecc02aa6 --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs @@ -0,0 +1,31 @@ +using System.Windows.Navigation; +using Flow.Launcher.Core; +using Flow.Launcher.SettingPages.ViewModels; +using Flow.Launcher.Infrastructure.UserSettings; +using CommunityToolkit.Mvvm.DependencyInjection; + +namespace Flow.Launcher.SettingPages.Views; + +public partial class SettingsPaneAbout +{ + private SettingsPaneAboutViewModel _viewModel = null!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (!IsInitialized) + { + var settings = Ioc.Default.GetRequiredService(); + var updater = Ioc.Default.GetRequiredService(); + _viewModel = new SettingsPaneAboutViewModel(settings, updater); + DataContext = _viewModel; + InitializeComponent(); + } + base.OnNavigatedTo(e); + } + + private void OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + App.API.OpenUrl(e.Uri.AbsoluteUri); + e.Handled = true; + } +} diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml new file mode 100644 index 000000000..4782d356e --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs new file mode 100644 index 000000000..3bd24bc13 --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs @@ -0,0 +1,65 @@ +using System.ComponentModel; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Navigation; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.SettingPages.ViewModels; +using Flow.Launcher.ViewModel; +using Flow.Launcher.Infrastructure.UserSettings; + +namespace Flow.Launcher.SettingPages.Views; + +public partial class SettingsPanePluginStore +{ + private SettingsPanePluginStoreViewModel _viewModel = null!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (!IsInitialized) + { + var settings = Ioc.Default.GetRequiredService(); + _viewModel = new SettingsPanePluginStoreViewModel(); + DataContext = _viewModel; + InitializeComponent(); + } + _viewModel.PropertyChanged += ViewModel_PropertyChanged; + base.OnNavigatedTo(e); + } + + private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SettingsPanePluginStoreViewModel.FilterText)) + { + ((CollectionViewSource)FindResource("PluginStoreCollectionView")).View.Refresh(); + } + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + _viewModel.PropertyChanged -= ViewModel_PropertyChanged; + base.OnNavigatingFrom(e); + } + + private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return; + PluginStoreFilterTextbox.Focus(); + } + + private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + App.API.OpenUrl(e.Uri.AbsoluteUri); + e.Handled = true; + } + + private void PluginStoreCollectionView_OnFilter(object sender, FilterEventArgs e) + { + if (e.Item is not PluginStoreItemViewModel plugin) + { + e.Accepted = false; + return; + } + + e.Accepted = _viewModel.SatisfiesFilter(plugin); + } +} diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml new file mode 100644 index 000000000..f9f708314 --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - \ No newline at end of file + + + + + + diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs index 2823e4ddd..28140f024 100644 --- a/Flow.Launcher/SettingWindow.xaml.cs +++ b/Flow.Launcher/SettingWindow.xaml.cs @@ -1,304 +1,207 @@ -using System; -using System.IO; +using System; using System.Windows; +using System.Windows.Forms; using System.Windows.Input; using System.Windows.Interop; -using System.Windows.Navigation; -using Microsoft.Win32; -using NHotkey; -using NHotkey.Wpf; -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.SettingPages.Views; using Flow.Launcher.ViewModel; +using ModernWpf.Controls; +using TextBox = System.Windows.Controls.TextBox; -namespace Flow.Launcher +namespace Flow.Launcher; + +public partial class SettingWindow { - public partial class SettingWindow + private readonly IPublicAPI _api; + private readonly Settings _settings; + private readonly SettingWindowViewModel _viewModel; + + public SettingWindow() { - private const string StartupPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"; + var viewModel = Ioc.Default.GetRequiredService(); + _settings = Ioc.Default.GetRequiredService(); + DataContext = viewModel; + _viewModel = viewModel; + _api = Ioc.Default.GetRequiredService(); + InitializePosition(); + InitializeComponent(); + } - public readonly IPublicAPI API; - private Settings settings; - private SettingWindowViewModel viewModel; + private void OnLoaded(object sender, RoutedEventArgs e) + { + RefreshMaximizeRestoreButton(); + // Fix (workaround) for the window freezes after lock screen (Win+L) or sleep + // https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf + HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource; + HwndTarget hwndTarget = hwndSource.CompositionTarget; + hwndTarget.RenderMode = RenderMode.SoftwareOnly; // Must use software only render mode here - public SettingWindow(IPublicAPI api, SettingWindowViewModel viewModel) + InitializePosition(); + } + + private void OnClosed(object sender, EventArgs e) + { + _settings.SettingWindowState = WindowState; + _settings.SettingWindowTop = Top; + _settings.SettingWindowLeft = Left; + _viewModel.Save(); + _api.SavePluginSettings(); + } + + private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e) + { + Close(); + } + + private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */ + { + if (Keyboard.FocusedElement is not TextBox textBox) { - InitializeComponent(); - settings = viewModel.Settings; - DataContext = viewModel; - this.viewModel = viewModel; - API = api; + return; } + var tRequest = new TraversalRequest(FocusNavigationDirection.Next); + textBox.MoveFocus(tRequest); + } - #region General - private void OnLoaded(object sender, RoutedEventArgs e) + /* Custom TitleBar */ + + private void OnMinimizeButtonClick(object sender, RoutedEventArgs e) + { + WindowState = WindowState.Minimized; + } + + private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e) + { + WindowState = WindowState switch { - // Fix (workaround) for the window freezes after lock screen (Win+L) - // https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf - HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource; - HwndTarget hwndTarget = hwndSource.CompositionTarget; - hwndTarget.RenderMode = RenderMode.SoftwareOnly; + WindowState.Maximized => WindowState.Normal, + _ => WindowState.Maximized + }; + } + + private void OnCloseButtonClick(object sender, RoutedEventArgs e) + { + Close(); + } + + private void RefreshMaximizeRestoreButton() + { + if (WindowState == WindowState.Maximized) + { + MaximizeButton.Visibility = Visibility.Hidden; + RestoreButton.Visibility = Visibility.Visible; } - - private void OnAutoStartupChecked(object sender, RoutedEventArgs e) + else { - SetStartup(); + MaximizeButton.Visibility = Visibility.Visible; + RestoreButton.Visibility = Visibility.Hidden; } + } - private void OnAutoStartupUncheck(object sender, RoutedEventArgs e) + private void Window_StateChanged(object sender, EventArgs e) + { + RefreshMaximizeRestoreButton(); + } + + public void InitializePosition() + { + var previousTop = _settings.SettingWindowTop; + var previousLeft = _settings.SettingWindowLeft; + + if (previousTop == null || previousLeft == null || !IsPositionValid(previousTop.Value, previousLeft.Value)) { - RemoveStartup(); + Top = WindowTop(); + Left = WindowLeft(); } - - public static void SetStartup() + else { - using (var key = Registry.CurrentUser.OpenSubKey(StartupPath, true)) + Top = previousTop.Value; + Left = previousLeft.Value; + } + WindowState = _settings.SettingWindowState; + } + + private static bool IsPositionValid(double top, double left) + { + foreach (var screen in Screen.AllScreens) + { + var workingArea = screen.WorkingArea; + + if (left >= workingArea.Left && left < workingArea.Right && + top >= workingArea.Top && top < workingArea.Bottom) { - key?.SetValue(Infrastructure.Constant.FlowLauncher, Infrastructure.Constant.ExecutablePath); + return true; } } + return false; + } - private void RemoveStartup() + private double WindowLeft() + { + var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var left = (dip2.X - ActualWidth) / 2 + dip1.X; + return left; + } + + private double WindowTop() + { + var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); + var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); + var top = (dip2.Y - ActualHeight) / 2 + dip1.Y - 20; + return top; + } + + private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) + { + if (args.IsSettingsSelected) { - using (var key = Registry.CurrentUser.OpenSubKey(StartupPath, true)) - { - key?.DeleteValue(Infrastructure.Constant.FlowLauncher, false); - } + ContentFrame.Navigate(typeof(SettingsPaneGeneral)); } - - public static bool StartupSet() + else { - using (var key = Registry.CurrentUser.OpenSubKey(StartupPath, true)) + var selectedItem = (NavigationViewItem)args.SelectedItem; + if (selectedItem == null) { - var path = key?.GetValue(Infrastructure.Constant.FlowLauncher) as string; - if (path != null) - { - return path == Infrastructure.Constant.ExecutablePath; - } - else - { - return false; - } - } - } - - private void OnSelectPythonDirectoryClick(object sender, RoutedEventArgs e) - { - var dlg = new System.Windows.Forms.FolderBrowserDialog - { - SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) - }; - - var result = dlg.ShowDialog(); - if (result == System.Windows.Forms.DialogResult.OK) - { - string pythonDirectory = dlg.SelectedPath; - if (!string.IsNullOrEmpty(pythonDirectory)) - { - var pythonPath = Path.Combine(pythonDirectory, PluginsLoader.PythonExecutable); - if (File.Exists(pythonPath)) - { - settings.PluginSettings.PythonDirectory = pythonDirectory; - MessageBox.Show("Remember to restart Flow Launcher use new Python path"); - } - else - { - MessageBox.Show("Can't find python in given directory"); - } - } - } - } - - #endregion - - #region Hotkey - - private void OnHotkeyControlLoaded(object sender, RoutedEventArgs e) - { - HotkeyControl.SetHotkey(viewModel.Settings.Hotkey, false); - } - - void OnHotkeyChanged(object sender, EventArgs e) - { - if (HotkeyControl.CurrentHotkeyAvailable) - { - SetHotkey(HotkeyControl.CurrentHotkey, (o, args) => - { - if (!Application.Current.MainWindow.IsVisible) - { - Application.Current.MainWindow.Visibility = Visibility.Visible; - } - else - { - Application.Current.MainWindow.Visibility = Visibility.Hidden; - } - }); - RemoveHotkey(settings.Hotkey); - settings.Hotkey = HotkeyControl.CurrentHotkey.ToString(); - } - } - - void SetHotkey(HotkeyModel hotkey, EventHandler action) - { - string hotkeyStr = hotkey.ToString(); - try - { - HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action); - } - catch (Exception) - { - string errorMsg = - string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); - MessageBox.Show(errorMsg); - } - } - - void RemoveHotkey(string hotkeyStr) - { - if (!string.IsNullOrEmpty(hotkeyStr)) - { - HotkeyManager.Current.Remove(hotkeyStr); - } - } - - private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e) - { - var item = viewModel.SelectedCustomPluginHotkey; - if (item == null) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); + NavView_Loaded(sender, null); /* Reset First Page */ return; } - string deleteWarning = - string.Format(InternationalizationManager.Instance.GetTranslation("deleteCustomHotkeyWarning"), - item.Hotkey); - if ( - MessageBox.Show(deleteWarning, InternationalizationManager.Instance.GetTranslation("delete"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) + var pageType = selectedItem.Name switch { - settings.CustomPluginHotkeys.Remove(item); - RemoveHotkey(item.Hotkey); - } + nameof(General) => typeof(SettingsPaneGeneral), + nameof(Plugins) => typeof(SettingsPanePlugins), + nameof(PluginStore) => typeof(SettingsPanePluginStore), + nameof(Theme) => typeof(SettingsPaneTheme), + nameof(Hotkey) => typeof(SettingsPaneHotkey), + nameof(Proxy) => typeof(SettingsPaneProxy), + nameof(About) => typeof(SettingsPaneAbout), + _ => typeof(SettingsPaneGeneral) + }; + ContentFrame.Navigate(pageType); } - - private void OnnEditCustomHotkeyClick(object sender, RoutedEventArgs e) - { - var item = viewModel.SelectedCustomPluginHotkey; - if (item != null) - { - CustomQueryHotkeySetting window = new CustomQueryHotkeySetting(this, settings); - window.UpdateItem(item); - window.ShowDialog(); - } - else - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); - } - } - - private void OnAddCustomeHotkeyClick(object sender, RoutedEventArgs e) - { - new CustomQueryHotkeySetting(this, settings).ShowDialog(); - } - - #endregion - - #region Plugin - - private void OnPluginToggled(object sender, RoutedEventArgs e) - { - var id = viewModel.SelectedPlugin.PluginPair.Metadata.ID; - // used to sync the current status from the plugin manager into the setting to keep consistency after save - settings.PluginSettings.Plugins[id].Disabled = viewModel.SelectedPlugin.PluginPair.Metadata.Disabled; - } - - private void OnPluginPriorityClick(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) - { - PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(viewModel.SelectedPlugin.PluginPair.Metadata.ID, settings, viewModel.SelectedPlugin); - priorityChangeWindow.ShowDialog(); - } - } - - private void OnPluginActionKeywordsClick(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) - { - var id = viewModel.SelectedPlugin.PluginPair.Metadata.ID; - ActionKeywords changeKeywordsWindow = new ActionKeywords(id, settings, viewModel.SelectedPlugin); - changeKeywordsWindow.ShowDialog(); - } - } - - private void OnPluginNameClick(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) - { - var website = viewModel.SelectedPlugin.PluginPair.Metadata.Website; - if (!string.IsNullOrEmpty(website)) - { - var uri = new Uri(website); - if (Uri.CheckSchemeName(uri.Scheme)) - { - SearchWeb.NewTabInBrowser(website); - } - } - } - } - - private void OnPluginDirecotyClick(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) - { - var directory = viewModel.SelectedPlugin.PluginPair.Metadata.PluginDirectory; - if (!string.IsNullOrEmpty(directory)) - FilesFolders.OpenPath(directory); - } - } - #endregion - - #region Proxy - - private void OnTestProxyClick(object sender, RoutedEventArgs e) - { // TODO: change to command - var msg = viewModel.TestProxy(); - MessageBox.Show(msg); // TODO: add message box service - } - - #endregion - - private async void OnCheckUpdates(object sender, RoutedEventArgs e) - { - viewModel.UpdateApp(); // TODO: change to command - } - - private void OnRequestNavigate(object sender, RequestNavigateEventArgs e) - { - SearchWeb.NewTabInBrowser(e.Uri.AbsoluteUri); - e.Handled = true; - } - - private void OnClosed(object sender, EventArgs e) - { - viewModel.Save(); - } - - private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e) - { - Close(); - } - - private void OpenPluginFolder(object sender, RoutedEventArgs e) - { - FilesFolders.OpenPath(Path.Combine(DataLocation.DataDirectory(), Constant.Themes)); - } - } -} \ No newline at end of file + + private void NavView_Loaded(object sender, RoutedEventArgs e) + { + if (ContentFrame.IsLoaded) + { + ContentFrame_Loaded(sender, e); + } + else + { + ContentFrame.Loaded += ContentFrame_Loaded; + } + } + + private void ContentFrame_Loaded(object sender, RoutedEventArgs e) + { + NavView.SelectedItem ??= NavView.MenuItems[0]; /* Set First Page */ + } +} diff --git a/Flow.Launcher/Settings.cs b/Flow.Launcher/Settings.cs index c5294b372..8f53c061f 100644 --- a/Flow.Launcher/Settings.cs +++ b/Flow.Launcher/Settings.cs @@ -1,6 +1,7 @@ -namespace Flow.Launcher.Properties { - - +namespace Flow.Launcher.Properties +{ + + // This class allows you to handle specific events on the settings class: // The SettingChanging event is raised before a setting's value is changed. // The PropertyChanged event is raised after a setting's value is changed. diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 052c296cd..7f35904a5 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -1,61 +1,81 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Text.Json.Serialization; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage { - // todo this class is not thread safe.... but used from multiple threads. public class TopMostRecord { [JsonInclude] - public Dictionary records { get; private set; } = new Dictionary(); + public ConcurrentDictionary records { get; private set; } = new ConcurrentDictionary(); internal bool IsTopMost(Result result) { - if (records.Count == 0 || !records.ContainsKey(result.OriginQuery.RawQuery)) + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to check if the result is top most + if (records.IsEmpty || result.OriginQuery == null || + !records.TryGetValue(result.OriginQuery.RawQuery, out var value)) { return false; } // since this dictionary should be very small (or empty) going over it should be pretty fast. - return records[result.OriginQuery.RawQuery].Equals(result); + return value.Equals(result); } internal void Remove(Result result) { - records.Remove(result.OriginQuery.RawQuery); + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to remove the record + if (result.OriginQuery == null) + { + return; + } + + records.Remove(result.OriginQuery.RawQuery, out _); } internal void AddOrUpdate(Result result) { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to add or update the record + if (result.OriginQuery == null) + { + return; + } + var record = new Record { PluginID = result.PluginID, Title = result.Title, - SubTitle = result.SubTitle + SubTitle = result.SubTitle, + RecordKey = result.RecordKey }; - records[result.OriginQuery.RawQuery] = record; - - } - - public void Load(Dictionary dictionary) - { - records = dictionary; + records.AddOrUpdate(result.OriginQuery.RawQuery, record, (key, oldValue) => record); } } - public class Record { public string Title { get; set; } public string SubTitle { get; set; } public string PluginID { get; set; } + public string RecordKey { get; set; } public bool Equals(Result r) { - return Title == r.Title - && SubTitle == r.SubTitle - && PluginID == r.PluginID; + if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey)) + { + return Title == r.Title + && SubTitle == r.SubTitle + && PluginID == r.PluginID; + } + else + { + return RecordKey == r.RecordKey + && PluginID == r.PluginID; + } } } } diff --git a/Flow.Launcher/Storage/UserSelectedRecord.cs b/Flow.Launcher/Storage/UserSelectedRecord.cs index 23d8c4faf..d30dccd26 100644 --- a/Flow.Launcher/Storage/UserSelectedRecord.cs +++ b/Flow.Launcher/Storage/UserSelectedRecord.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using System.Text.Json.Serialization; -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; -using Flow.Launcher.ViewModel; namespace Flow.Launcher.Storage { @@ -20,7 +15,6 @@ namespace Flow.Launcher.Storage [JsonInclude, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary records { get; private set; } - public UserSelectedRecord() { recordsWithQuery = new Dictionary(); @@ -50,16 +44,38 @@ namespace Flow.Launcher.Storage private static int GenerateResultHashCode(Result result) { - int hashcode = GenerateStaticHashCode(result.Title); - return GenerateStaticHashCode(result.SubTitle, hashcode); + if (string.IsNullOrEmpty(result.RecordKey)) + { + int hashcode = GenerateStaticHashCode(result.Title); + return GenerateStaticHashCode(result.SubTitle, hashcode); + } + else + { + return GenerateStaticHashCode(result.RecordKey); + } } private static int GenerateQueryAndResultHashCode(Query query, Result result) { + // query is null when user select the context menu item directly of one item from query list + // so we only need to consider the result + if (query == null) + { + return GenerateResultHashCode(result); + } + int hashcode = GenerateStaticHashCode(query.ActionKeyword); hashcode = GenerateStaticHashCode(query.Search, hashcode); - hashcode = GenerateStaticHashCode(result.Title, hashcode); - hashcode = GenerateStaticHashCode(result.SubTitle, hashcode); + + if (string.IsNullOrEmpty(result.RecordKey)) + { + hashcode = GenerateStaticHashCode(result.Title, hashcode); + hashcode = GenerateStaticHashCode(result.SubTitle, hashcode); + } + else + { + hashcode = GenerateStaticHashCode(result.RecordKey, hashcode); + } return hashcode; } @@ -106,4 +122,4 @@ namespace Flow.Launcher.Storage return selectedCount; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml index df0304bc2..35d1f9a41 100644 --- a/Flow.Launcher/Themes/Base.xaml +++ b/Flow.Launcher/Themes/Base.xaml @@ -1,28 +1,61 @@ - + + 0 + 0 + 0 + + + 52 + + - - - - + + + + + + + + + + + + + + + - - - + + - + - + + + + + + - + @@ -112,44 +273,50 @@ - - - - - + + + + - + + + + + + + + + + + F1 M12000,12000z M0,0z M10354,10962C10326,10951 10279,10927 10249,10907 10216,10886 9476,10153 8370,9046 7366,8042 6541,7220 6536,7220 6532,7220 6498,7242 6461,7268 6213,7447 5883,7619 5592,7721 5194,7860 4802,7919 4360,7906 3612,7886 2953,7647 2340,7174 2131,7013 1832,6699 1664,6465 1394,6088 1188,5618 1097,5170 1044,4909 1030,4764 1030,4470 1030,4130 1056,3914 1135,3609 1263,3110 1511,2633 1850,2235 1936,2134 2162,1911 2260,1829 2781,1395 3422,1120 4090,1045 4271,1025 4667,1025 4848,1045 5505,1120 6100,1368 6630,1789 6774,1903 7081,2215 7186,2355 7362,2588 7467,2759 7579,2990 7802,3455 7911,3937 7911,4460 7911,4854 7861,5165 7737,5542 7684,5702 7675,5724 7602,5885 7517,6071 7390,6292 7270,6460 7242,6499 7220,6533 7220,6538 7220,6542 8046,7371 9055,8380 10441,9766 10898,10229 10924,10274 10945,10308 10966,10364 10976,10408 10990,10472 10991,10493 10980,10554 10952,10717 10840,10865 10690,10937 10621,10971 10607,10974 10510,10977 10425,10980 10395,10977 10354,10962z M4685,7050C5214,7001 5694,6809 6100,6484 6209,6396 6396,6209 6484,6100 7151,5267 7246,4110 6721,3190 6369,2571 5798,2137 5100,1956 4706,1855 4222,1855 3830,1957 3448,2056 3140,2210 2838,2453 2337,2855 2010,3427 1908,4080 1877,4274 1877,4656 1908,4850 1948,5105 2028,5370 2133,5590 2459,6272 3077,6782 3810,6973 3967,7014 4085,7034 4290,7053 4371,7061 4583,7059 4685,7050z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Themes/BlackAndWhite.xaml b/Flow.Launcher/Themes/BlackAndWhite.xaml deleted file mode 100644 index 01e7e0bc1..000000000 --- a/Flow.Launcher/Themes/BlackAndWhite.xaml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - #4F6180 - + + - - + + + - + + + + + + + #19ffffff + + + - - - - - - - - #356ef3 - - - - - + + + diff --git a/Flow.Launcher/Themes/BlurBlack.xaml b/Flow.Launcher/Themes/BlurBlack.xaml index c03285af0..5ce3932e1 100644 --- a/Flow.Launcher/Themes/BlurBlack.xaml +++ b/Flow.Launcher/Themes/BlurBlack.xaml @@ -1,62 +1,184 @@ - + + - True + Dark + #B0000000 + #B6000000 + 0 0 0 8 - + + - - - + + - - - - - - - #356ef3 + #19c9c9c9 - - - + + + + + + + diff --git a/Flow.Launcher/Themes/BlurWhite.xaml b/Flow.Launcher/Themes/BlurWhite.xaml index d88c033c9..25cbfe9c9 100644 --- a/Flow.Launcher/Themes/BlurWhite.xaml +++ b/Flow.Launcher/Themes/BlurWhite.xaml @@ -1,60 +1,205 @@ - + + - True - - + + + - - + + - + + + + + + + + - - - - - - - - - #356ef3 - - - - - + + + diff --git a/Flow.Launcher/Themes/Circle System.xaml b/Flow.Launcher/Themes/Circle System.xaml new file mode 100644 index 000000000..24c7bd65b --- /dev/null +++ b/Flow.Launcher/Themes/Circle System.xaml @@ -0,0 +1,200 @@ + + + + + + + True + Auto + #E5E6E6E6 + #DF1F1F1F + + + + + + + + + + + + + + + + + + + + + + 8 + 10 0 10 0 + 0 0 0 10 + + + + + + + + diff --git a/Flow.Launcher/Themes/Cyan Dark.xaml b/Flow.Launcher/Themes/Cyan Dark.xaml new file mode 100644 index 000000000..59ebad0f6 --- /dev/null +++ b/Flow.Launcher/Themes/Cyan Dark.xaml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + #1e292f + + + + + + + + + + 0 + 0 + 0 0 0 4 + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher/Themes/Darker Glass.xaml b/Flow.Launcher/Themes/Darker Glass.xaml new file mode 100644 index 000000000..9ffaaf566 --- /dev/null +++ b/Flow.Launcher/Themes/Darker Glass.xaml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + #2e436e + + + + + + + + + + 8 + 10 0 10 0 + 0 0 0 10 + + + + + + + + diff --git a/Flow.Launcher/Themes/Darker.xaml b/Flow.Launcher/Themes/Darker.xaml index ae9ba899a..7d2614be8 100644 --- a/Flow.Launcher/Themes/Darker.xaml +++ b/Flow.Launcher/Themes/Darker.xaml @@ -1,46 +1,102 @@ - + - - + - + - - - + - - - + #4d4d4d + - - - - + + + diff --git a/Flow.Launcher/Themes/Discord Dark.xaml b/Flow.Launcher/Themes/Discord Dark.xaml new file mode 100644 index 000000000..c1336309b --- /dev/null +++ b/Flow.Launcher/Themes/Discord Dark.xaml @@ -0,0 +1,194 @@ + + + + + 0 0 0 6 + + + + + + + + + + + + + + #36363d + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher/Themes/Dracula.xaml b/Flow.Launcher/Themes/Dracula.xaml new file mode 100644 index 000000000..6e3510f79 --- /dev/null +++ b/Flow.Launcher/Themes/Dracula.xaml @@ -0,0 +1,193 @@ + + + + + 0 0 0 6 + + + + + + + + + + + + #44475a + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Themes/Gray.xaml b/Flow.Launcher/Themes/Gray.xaml index 1fbaa959a..84570e9d6 100644 --- a/Flow.Launcher/Themes/Gray.xaml +++ b/Flow.Launcher/Themes/Gray.xaml @@ -1,54 +1,194 @@ - - + + - + - - - - - - - - - #787878 - + + - + + + 0 + 0 + 0 0 0 0 + + + + + + + + diff --git a/Flow.Launcher/Themes/League.xaml b/Flow.Launcher/Themes/League.xaml new file mode 100644 index 000000000..89c6691f8 --- /dev/null +++ b/Flow.Launcher/Themes/League.xaml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + #192026 + + + + + + F1 M12000,12000z M0,0z M10354,10962C10326,10951 10279,10927 10249,10907 10216,10886 9476,10153 8370,9046 7366,8042 6541,7220 6536,7220 6532,7220 6498,7242 6461,7268 6213,7447 5883,7619 5592,7721 5194,7860 4802,7919 4360,7906 3612,7886 2953,7647 2340,7174 2131,7013 1832,6699 1664,6465 1394,6088 1188,5618 1097,5170 1044,4909 1030,4764 1030,4470 1030,4130 1056,3914 1135,3609 1263,3110 1511,2633 1850,2235 1936,2134 2162,1911 2260,1829 2781,1395 3422,1120 4090,1045 4271,1025 4667,1025 4848,1045 5505,1120 6100,1368 6630,1789 6774,1903 7081,2215 7186,2355 7362,2588 7467,2759 7579,2990 7802,3455 7911,3937 7911,4460 7911,4854 7861,5165 7737,5542 7684,5702 7675,5724 7602,5885 7517,6071 7390,6292 7270,6460 7242,6499 7220,6533 7220,6538 7220,6542 8046,7371 9055,8380 10441,9766 10898,10229 10924,10274 10945,10308 10966,10364 10976,10408 10990,10472 10991,10493 10980,10554 10952,10717 10840,10865 10690,10937 10621,10971 10607,10974 10510,10977 10425,10980 10395,10977 10354,10962z M4685,7050C5214,7001 5694,6809 6100,6484 6209,6396 6396,6209 6484,6100 7151,5267 7246,4110 6721,3190 6369,2571 5798,2137 5100,1956 4706,1855 4222,1855 3830,1957 3448,2056 3140,2210 2838,2453 2337,2855 2010,3427 1908,4080 1877,4274 1877,4656 1908,4850 1948,5105 2028,5370 2133,5590 2459,6272 3077,6782 3810,6973 3967,7014 4085,7034 4290,7053 4371,7061 4583,7059 4685,7050z + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher/Themes/Light.xaml b/Flow.Launcher/Themes/Light.xaml deleted file mode 100644 index 2380f424e..000000000 --- a/Flow.Launcher/Themes/Light.xaml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - #909090 - - - - - - \ No newline at end of file diff --git a/Flow.Launcher/Themes/Metro Server.xaml b/Flow.Launcher/Themes/Metro Server.xaml deleted file mode 100644 index 9d74252c5..000000000 --- a/Flow.Launcher/Themes/Metro Server.xaml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - #006ac1 - - - \ No newline at end of file diff --git a/Flow.Launcher/Themes/Midnight.xaml b/Flow.Launcher/Themes/Midnight.xaml new file mode 100644 index 000000000..134b006d9 --- /dev/null +++ b/Flow.Launcher/Themes/Midnight.xaml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + #202938 + + + + + + + + + + 8 + 10 0 10 0 + 0 0 0 10 + + + + + + + + diff --git a/Flow.Launcher/Themes/Nord Darker.xaml b/Flow.Launcher/Themes/Nord Darker.xaml index 13f4bf230..7bbc737d1 100644 --- a/Flow.Launcher/Themes/Nord Darker.xaml +++ b/Flow.Launcher/Themes/Nord Darker.xaml @@ -1,56 +1,167 @@ - + - + - - - - + - - - - - #5e81ac - - + + + + + + + + + diff --git a/Flow.Launcher/Themes/Nord.xaml b/Flow.Launcher/Themes/Nord.xaml deleted file mode 100644 index 2253b3410..000000000 --- a/Flow.Launcher/Themes/Nord.xaml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - #5e81ac - - - - diff --git a/Flow.Launcher/Themes/Pink.xaml b/Flow.Launcher/Themes/Pink.xaml index d3130584b..619e10bd2 100644 --- a/Flow.Launcher/Themes/Pink.xaml +++ b/Flow.Launcher/Themes/Pink.xaml @@ -1,36 +1,164 @@ - + - + - + - - - - - - - - + + #cc1081 - - + + + + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher/Themes/SlimLight.xaml b/Flow.Launcher/Themes/SlimLight.xaml new file mode 100644 index 000000000..b59cb0978 --- /dev/null +++ b/Flow.Launcher/Themes/SlimLight.xaml @@ -0,0 +1,230 @@ + + + + + + 38 + + + + + + + + + + + + + + #d6d4d7 + + + + + + + + + + + 6 + 4 0 4 0 + 0 0 0 4 + + + + + + + + diff --git a/Flow.Launcher/Themes/Sublime.xaml b/Flow.Launcher/Themes/Sublime.xaml new file mode 100644 index 000000000..bde085132 --- /dev/null +++ b/Flow.Launcher/Themes/Sublime.xaml @@ -0,0 +1,190 @@ + + + + + 0 0 0 8 + + + + + + + + + + + + + + + #3c454e + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Themes/Ubuntu.xaml b/Flow.Launcher/Themes/Ubuntu.xaml new file mode 100644 index 000000000..330d57b29 --- /dev/null +++ b/Flow.Launcher/Themes/Ubuntu.xaml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + #4d4d4d + + + + + + + + + + 0 + 0 0 0 0 + 0 0 0 8 + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher/Themes/Win10System.xaml b/Flow.Launcher/Themes/Win10System.xaml new file mode 100644 index 000000000..e40e3e7cd --- /dev/null +++ b/Flow.Launcher/Themes/Win10System.xaml @@ -0,0 +1,218 @@ + + + + + + False + Auto + #FFFAFAFA + #FF202020 + 0 0 0 8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Themes/Win11Light.xaml b/Flow.Launcher/Themes/Win11Light.xaml new file mode 100644 index 000000000..a08b8eef9 --- /dev/null +++ b/Flow.Launcher/Themes/Win11Light.xaml @@ -0,0 +1,235 @@ + + + + + + + True + Auto + #BFFAFAFA + #DD202020 + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + 10 0 10 0 + 0 10 0 10 + + + + + + + + diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2fd00224c..4a6c1d639 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,37 +1,35 @@ -using System; +using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; using System.Linq; +using System.Text; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; using System.Windows; using System.Windows.Input; -using NHotkey; -using NHotkey.Wpf; +using System.Windows.Media; +using System.Windows.Threading; +using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; -using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; -using Flow.Launcher.Infrastructure.Logger; using Microsoft.VisualStudio.Threading; -using System.Threading.Channels; -using ISavable = Flow.Launcher.Plugin.ISavable; -using System.Windows.Threading; namespace Flow.Launcher.ViewModel { - public class MainViewModel : BaseModel, ISavable + public partial class MainViewModel : BaseModel, ISavable, IDisposable { #region Private Fields - private const string DefaultOpenResultModifiers = "Alt"; - private bool _isQueryRunning; private Query _lastQuery; private string _queryTextBeforeLeaveResults; @@ -39,30 +37,99 @@ namespace Flow.Launcher.ViewModel private readonly FlowLauncherJsonStorage _historyItemsStorage; private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; private readonly FlowLauncherJsonStorage _topMostRecordStorage; - private readonly Settings _settings; private readonly History _history; + private int lastHistoryIndex = 1; private readonly UserSelectedRecord _userSelectedRecord; private readonly TopMostRecord _topMostRecord; private CancellationTokenSource _updateSource; private CancellationToken _updateToken; - private readonly Internationalization _translator = InternationalizationManager.Instance; - private ChannelWriter _resultsUpdateChannelWriter; private Task _resultsViewUpdateTask; + private readonly IReadOnlyList _emptyResult = new List(); + #endregion #region Constructor - public MainViewModel(Settings settings) + public MainViewModel() { _queryTextBeforeLeaveResults = ""; _queryText = ""; _lastQuery = new Query(); - _settings = settings; + Settings = Ioc.Default.GetRequiredService(); + Settings.PropertyChanged += (_, args) => + { + switch (args.PropertyName) + { + case nameof(Settings.WindowSize): + OnPropertyChanged(nameof(MainWindowWidth)); + break; + case nameof(Settings.WindowHeightSize): + OnPropertyChanged(nameof(MainWindowHeight)); + break; + case nameof(Settings.QueryBoxFontSize): + OnPropertyChanged(nameof(QueryBoxFontSize)); + break; + case nameof(Settings.ItemHeightSize): + OnPropertyChanged(nameof(ItemHeightSize)); + break; + case nameof(Settings.ResultItemFontSize): + OnPropertyChanged(nameof(ResultItemFontSize)); + break; + case nameof(Settings.ResultSubItemFontSize): + OnPropertyChanged(nameof(ResultSubItemFontSize)); + break; + case nameof(Settings.AlwaysStartEn): + OnPropertyChanged(nameof(StartWithEnglishMode)); + break; + case nameof(Settings.OpenResultModifiers): + OnPropertyChanged(nameof(OpenResultCommandModifiers)); + break; + case nameof(Settings.PreviewHotkey): + OnPropertyChanged(nameof(PreviewHotkey)); + break; + case nameof(Settings.AutoCompleteHotkey): + OnPropertyChanged(nameof(AutoCompleteHotkey)); + break; + case nameof(Settings.CycleHistoryUpHotkey): + OnPropertyChanged(nameof(CycleHistoryUpHotkey)); + break; + case nameof(Settings.CycleHistoryDownHotkey): + OnPropertyChanged(nameof(CycleHistoryDownHotkey)); + break; + case nameof(Settings.AutoCompleteHotkey2): + OnPropertyChanged(nameof(AutoCompleteHotkey2)); + break; + case nameof(Settings.SelectNextItemHotkey): + OnPropertyChanged(nameof(SelectNextItemHotkey)); + break; + case nameof(Settings.SelectNextItemHotkey2): + OnPropertyChanged(nameof(SelectNextItemHotkey2)); + break; + case nameof(Settings.SelectPrevItemHotkey): + OnPropertyChanged(nameof(SelectPrevItemHotkey)); + break; + case nameof(Settings.SelectPrevItemHotkey2): + OnPropertyChanged(nameof(SelectPrevItemHotkey2)); + break; + case nameof(Settings.SelectNextPageHotkey): + OnPropertyChanged(nameof(SelectNextPageHotkey)); + break; + case nameof(Settings.SelectPrevPageHotkey): + OnPropertyChanged(nameof(SelectPrevPageHotkey)); + break; + case nameof(Settings.OpenContextMenuHotkey): + OnPropertyChanged(nameof(OpenContextMenuHotkey)); + break; + case nameof(Settings.SettingWindowHotkey): + OnPropertyChanged(nameof(SettingWindowHotkey)); + break; + } + }; _historyItemsStorage = new FlowLauncherJsonStorage(); _userSelectedRecordStorage = new FlowLauncherJsonStorage(); @@ -71,18 +138,52 @@ namespace Flow.Launcher.ViewModel _userSelectedRecord = _userSelectedRecordStorage.Load(); _topMostRecord = _topMostRecordStorage.Load(); - ContextMenu = new ResultsViewModel(_settings); - Results = new ResultsViewModel(_settings); - History = new ResultsViewModel(_settings); + ContextMenu = new ResultsViewModel(Settings) + { + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview + }; + Results = new ResultsViewModel(Settings) + { + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview + }; + History = new ResultsViewModel(Settings) + { + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview + }; _selectedResults = Results; - InitializeKeyCommands(); - RegisterViewUpdate(); - RegisterResultsUpdatedEvent(); + Results.PropertyChanged += (o, args) => + { + switch (args.PropertyName) + { + case nameof(Results.SelectedItem): + _selectedItemFromQueryResults = true; + PreviewSelectedItem = Results.SelectedItem; + _ = UpdatePreviewAsync(); + break; + } + }; - SetHotkey(_settings.Hotkey, OnHotkey); - SetCustomPluginHotkey(); - SetOpenResultModifiers(); + History.PropertyChanged += (o, args) => + { + switch (args.PropertyName) + { + case nameof(History.SelectedItem): + _selectedItemFromQueryResults = false; + PreviewSelectedItem = History.SelectedItem; + _ = UpdatePreviewAsync(); + break; + } + }; + + RegisterViewUpdate(); + _ = RegisterClockAndDateUpdateAsync(); } private void RegisterViewUpdate() @@ -90,9 +191,9 @@ namespace Flow.Launcher.ViewModel var resultUpdateChannel = Channel.CreateUnbounded(); _resultsUpdateChannelWriter = resultUpdateChannel.Writer; _resultsViewUpdateTask = - Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); + Task.Run(UpdateActionAsync).ContinueWith(continueAction, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); - async Task updateAction() + async Task UpdateActionAsync() { var queue = new Dictionary(); var channelReader = resultUpdateChannel.Reader; @@ -111,11 +212,10 @@ namespace Flow.Launcher.ViewModel queue.Clear(); } - Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); + if (!_disposed) + Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); } - ; - void continueAction(Task t) { #if DEBUG @@ -123,12 +223,12 @@ namespace Flow.Launcher.ViewModel #else Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}"); _resultsViewUpdateTask = - Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); + Task.Run(UpdateActionAsync).ContinueWith(continueAction, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); #endif } } - private void RegisterResultsUpdatedEvent() + public void RegisterResultsUpdatedEvent() { foreach (var pair in PluginManager.GetPluginsForInterface()) { @@ -142,8 +242,12 @@ namespace Flow.Launcher.ViewModel var token = e.Token == default ? _updateToken : e.Token; - PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, token))) + // make a clone to avoid possible issue that plugin will also change the list and items when updating view model + var resultsCopy = DeepCloneResults(e.Results, token); + + PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, + token))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); } @@ -151,220 +255,800 @@ namespace Flow.Launcher.ViewModel } } - - private void InitializeKeyCommands() + private async Task RegisterClockAndDateUpdateAsync() { - EscCommand = new RelayCommand(_ => + var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + // ReSharper disable once MethodSupportsCancellation + while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { - if (!SelectedIsFromQueryResults()) - { - SelectedResults = Results; - } - else - { - MainWindowVisibility = Visibility.Collapsed; - } - }); + if (Settings.UseClock) + ClockText = DateTime.Now.ToString(Settings.TimeFormat, CultureInfo.CurrentCulture); + if (Settings.UseDate) + DateText = DateTime.Now.ToString(Settings.DateFormat, CultureInfo.CurrentCulture); + } + } - SelectNextItemCommand = new RelayCommand(_ => { SelectedResults.SelectNextResult(); }); + [RelayCommand] + private async Task ReloadPluginDataAsync() + { + Hide(); - SelectPrevItemCommand = new RelayCommand(_ => { SelectedResults.SelectPrevResult(); }); + await PluginManager.ReloadDataAsync().ConfigureAwait(false); + App.API.ShowMsg(App.API.GetTranslation("success"), + App.API.GetTranslation("completedSuccessfully")); + } - SelectNextPageCommand = new RelayCommand(_ => { SelectedResults.SelectNextPage(); }); - - SelectPrevPageCommand = new RelayCommand(_ => { SelectedResults.SelectPrevPage(); }); - - SelectFirstResultCommand = new RelayCommand(_ => SelectedResults.SelectFirstResult()); - - StartHelpCommand = new RelayCommand(_ => + [RelayCommand] + private void LoadHistory() + { + if (QueryResultsSelected()) { - SearchWeb.NewTabInBrowser("https://github.com/Flow-Launcher/Flow.Launcher/wiki/Flow-Launcher/"); - }); - - OpenResultCommand = new RelayCommand(index => + SelectedResults = History; + History.SelectedIndex = _history.Items.Count - 1; + } + else { - var results = SelectedResults; + SelectedResults = Results; + } + } - if (index != null) - { - results.SelectedIndex = int.Parse(index.ToString()); - } - - var result = results.SelectedItem?.Result; - if (result != null) // SelectedItem returns null if selection is empty. - { - bool hideWindow = result.Action != null && result.Action(new ActionContext - { - SpecialKeyState = GlobalHotkey.Instance.CheckModifiers() - }); - - if (hideWindow) - { - MainWindowVisibility = Visibility.Collapsed; - } - - if (SelectedIsFromQueryResults()) - { - _userSelectedRecord.Add(result); - _history.Add(result.OriginQuery.RawQuery); - } - else - { - SelectedResults = Results; - } - } - }); - - LoadContextMenuCommand = new RelayCommand(_ => + [RelayCommand] + public void ReQuery() + { + if (QueryResultsSelected()) { - if (SelectedIsFromQueryResults()) - { - // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing - // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing - if (SelectedResults.SelectedItem != null) - SelectedResults = ContextMenu; - } - else - { - SelectedResults = Results; - } - }); + // When we are re-querying, we should not delay the query + _ = QueryResultsAsync(false, isReQuery: true); + } + } - LoadHistoryCommand = new RelayCommand(_ => + public void ReQuery(bool reselect) + { + BackToQueryResults(); + // When we are re-querying, we should not delay the query + _ = QueryResultsAsync(false, isReQuery: true, reSelect: reselect); + } + + [RelayCommand] + public void ReverseHistory() + { + if (_history.Items.Count > 0) { - if (SelectedIsFromQueryResults()) + ChangeQueryText(_history.Items[^lastHistoryIndex].Query); + if (lastHistoryIndex < _history.Items.Count) { - SelectedResults = History; - History.SelectedIndex = _history.Items.Count - 1; + lastHistoryIndex++; } - else - { - SelectedResults = Results; - } - }); + } + } - ReloadPluginDataCommand = new RelayCommand(_ => + [RelayCommand] + public void ForwardHistory() + { + if (_history.Items.Count > 0) { - var msg = new Msg + ChangeQueryText(_history.Items[^lastHistoryIndex].Query); + if (lastHistoryIndex > 1) { - Owner = Application.Current.MainWindow - }; + lastHistoryIndex--; + } + } + } - MainWindowVisibility = Visibility.Collapsed; + [RelayCommand] + private void LoadContextMenu() + { + if (QueryResultsSelected()) + { + // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing + // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing + if (SelectedResults.SelectedItem != null) + SelectedResults = ContextMenu; + } + else + { + SelectedResults = Results; + } + } - PluginManager - .ReloadData() - .ContinueWith(_ => - Application.Current.Dispatcher.Invoke(() => - { - msg.Show( - InternationalizationManager.Instance.GetTranslation("success"), - InternationalizationManager.Instance.GetTranslation("completedSuccessfully"), - ""); - })) - .ConfigureAwait(false); - }); + [RelayCommand] + private void Backspace(object index) + { + var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); + + // GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string + var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\')); + + var actionKeyword = string.IsNullOrEmpty(query.ActionKeyword) ? string.Empty : $"{query.ActionKeyword} "; + + ChangeQueryText($"{actionKeyword}{path}"); + } + + [RelayCommand] + private void AutocompleteQuery() + { + var result = SelectedResults.SelectedItem?.Result; + if (result != null && QueryResultsSelected()) // SelectedItem returns null if selection is empty. + { + var autoCompleteText = result.Title; + + if (!string.IsNullOrEmpty(result.AutoCompleteText)) + { + autoCompleteText = result.AutoCompleteText; + } + else if (!string.IsNullOrEmpty(SelectedResults.SelectedItem?.QuerySuggestionText)) + { + //var defaultSuggestion = SelectedResults.SelectedItem.QuerySuggestionText; + //// check if result.actionkeywordassigned is empty + //if (!string.IsNullOrEmpty(result.ActionKeywordAssigned)) + //{ + // autoCompleteText = $"{result.ActionKeywordAssigned} {defaultSuggestion}"; + //} + + autoCompleteText = SelectedResults.SelectedItem.QuerySuggestionText; + } + + var specialKeyState = GlobalHotkey.CheckModifiers(); + if (specialKeyState.ShiftPressed) + { + autoCompleteText = result.SubTitle; + } + + ChangeQueryText(autoCompleteText); + } + } + + [RelayCommand] + private async Task OpenResultAsync(string index) + { + var results = SelectedResults; + if (index is not null) + { + results.SelectedIndex = int.Parse(index); + } + + var result = results.SelectedItem?.Result; + if (result == null) + { + return; + } + + var hideWindow = await result.ExecuteAsync(new ActionContext + { + // not null means pressing modifier key + number, should ignore the modifier key + SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() + }) + .ConfigureAwait(false); + + if (QueryResultsSelected()) + { + _userSelectedRecord.Add(result); + // origin query is null when user select the context menu item directly of one item from query list + // so we don't want to add it to history + if (result.OriginQuery != null) + { + _history.Add(result.OriginQuery.RawQuery); + } + lastHistoryIndex = 1; + } + + if (hideWindow) + { + Hide(); + } + } + + private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) + { + var resultsCopy = new List(); + foreach (var result in results.ToList()) + { + if (token.IsCancellationRequested) + { + break; + } + + var resultCopy = result.Clone(); + resultsCopy.Add(resultCopy); + } + return resultsCopy; + } + + #endregion + + #region BasicCommands + + [RelayCommand] + private void OpenSetting() + { + App.API.OpenSettingDialog(); + } + + [RelayCommand] + private void SelectHelp() + { + App.API.OpenUrl("https://www.flowlauncher.com/docs/#/usage-tips"); + } + + [RelayCommand] + private void SelectFirstResult() + { + SelectedResults.SelectFirstResult(); + } + + [RelayCommand] + private void SelectLastResult() + { + SelectedResults.SelectLastResult(); + } + + [RelayCommand] + private void SelectPrevPage() + { + SelectedResults.SelectPrevPage(); + } + + [RelayCommand] + private void SelectNextPage() + { + SelectedResults.SelectNextPage(); + } + + [RelayCommand] + private void SelectPrevItem() + { + if (_history.Items.Count > 0 + && QueryText == string.Empty + && QueryResultsSelected()) + { + lastHistoryIndex = 1; + ReverseHistory(); + } + else + { + SelectedResults.SelectPrevResult(); + } + } + + [RelayCommand] + private void SelectNextItem() + { + SelectedResults.SelectNextResult(); + } + + [RelayCommand] + private void Esc() + { + if (!QueryResultsSelected()) + { + SelectedResults = Results; + } + else + { + Hide(); + } + } + + public void BackToQueryResults() + { + if (!QueryResultsSelected()) + { + SelectedResults = Results; + } + } + + [RelayCommand] + public void ToggleGameMode() + { + GameModeStatus = !GameModeStatus; + } + + [RelayCommand] + public void CopyAlternative() + { + var result = Results.SelectedItem?.Result?.CopyText; + + if (result != null) + { + App.API.CopyToClipboard(result, directCopy: false); + } } #endregion #region ViewModel Properties + public Settings Settings { get; } + public string ClockText { get; private set; } + public string DateText { get; private set; } + public ResultsViewModel Results { get; private set; } + public ResultsViewModel ContextMenu { get; private set; } + public ResultsViewModel History { get; private set; } - private string _queryText; + public bool GameModeStatus { get; set; } = false; + private string _queryText; public string QueryText { get => _queryText; set { _queryText = value; - Query(); + OnPropertyChanged(); } } + [RelayCommand] + private void IncreaseWidth() + { + MainWindowWidth += 100; + Settings.WindowLeft -= 50; + OnPropertyChanged(nameof(MainWindowWidth)); + } + + [RelayCommand] + private void DecreaseWidth() + { + if (MainWindowWidth - 100 < 400 || MainWindowWidth == 400) + { + MainWindowWidth = 400; + } + else + { + MainWindowWidth -= 100; + Settings.WindowLeft += 50; + } + + OnPropertyChanged(nameof(MainWindowWidth)); + } + + [RelayCommand] + private void IncreaseMaxResult() + { + if (Settings.MaxResultsToShow == 17) + return; + + Settings.MaxResultsToShow += 1; + } + + [RelayCommand] + private void DecreaseMaxResult() + { + if (Settings.MaxResultsToShow == 2) + return; + + Settings.MaxResultsToShow -= 1; + } + /// /// we need move cursor to end when we manually changed query /// but we don't want to move cursor to end when query is updated from TextBox /// /// - public void ChangeQueryText(string queryText) + /// Force query even when Query Text doesn't change + public void ChangeQueryText(string queryText, bool isReQuery = false) { + _ = ChangeQueryTextAsync(queryText, isReQuery); + } + + /// + /// Async version of + /// + private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false) + { + // Must check access so that we will not block the UI thread which cause window visibility issue + if (!Application.Current.Dispatcher.CheckAccess()) + { + await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryText(queryText, isReQuery)); + return; + } + + if (QueryText != queryText) + { + // Change query text first + QueryText = queryText; + // When we are changing query from codes, we should not delay the query + await QueryAsync(false, isReQuery: false); + + // set to false so the subsequent set true triggers + // PropertyChanged and MoveQueryTextToEnd is called + QueryTextCursorMovedToEnd = false; + } + else if (isReQuery) + { + // When we are re-querying, we should not delay the query + await QueryAsync(false, isReQuery: true); + } + QueryTextCursorMovedToEnd = true; - QueryText = queryText; } public bool LastQuerySelected { get; set; } + + // This is not a reliable indicator of the cursor's position, it is manually set for a specific purpose. public bool QueryTextCursorMovedToEnd { get; set; } private ResultsViewModel _selectedResults; private ResultsViewModel SelectedResults { - get { return _selectedResults; } + get => _selectedResults; set { + var isReturningFromQueryResults = QueryResultsSelected(); + var isReturningFromContextMenu = ContextMenuSelected(); + var isReturningFromHistory = HistorySelected(); _selectedResults = value; - if (SelectedIsFromQueryResults()) + if (QueryResultsSelected()) { - ContextMenu.Visbility = Visibility.Collapsed; - History.Visbility = Visibility.Collapsed; - ChangeQueryText(_queryTextBeforeLeaveResults); + Results.Visibility = Visibility.Visible; + ContextMenu.Visibility = Visibility.Collapsed; + History.Visibility = Visibility.Collapsed; + + // QueryText setter (used in ChangeQueryText) runs the query again, resetting the selected + // result from the one that was selected before going into the context menu to the first result. + // The code below correctly restores QueryText and puts the text caret at the end without + // running the query again when returning from the context menu. + if (isReturningFromContextMenu) + { + _queryText = _queryTextBeforeLeaveResults; + OnPropertyChanged(nameof(QueryText)); + QueryTextCursorMovedToEnd = true; + } + else + { + ChangeQueryText(_queryTextBeforeLeaveResults); + } + + // If we are returning from history and we have not set select item yet, + // we need to clear the preview selected item + if (isReturningFromHistory && _selectedItemFromQueryResults.HasValue && (!_selectedItemFromQueryResults.Value)) + { + PreviewSelectedItem = null; + } } else { - Results.Visbility = Visibility.Collapsed; + Results.Visibility = Visibility.Collapsed; + if (HistorySelected()) + { + ContextMenu.Visibility = Visibility.Collapsed; + History.Visibility = Visibility.Visible; + } + else + { + ContextMenu.Visibility = Visibility.Visible; + History.Visibility = Visibility.Collapsed; + } _queryTextBeforeLeaveResults = QueryText; - // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() // http://stackoverflow.com/posts/25895769/revisions - if (string.IsNullOrEmpty(QueryText)) + QueryText = string.Empty; + // When we are changing query because selected results are changed to history or context menu, + // we should not delay the query + Query(false); + + if (HistorySelected()) { - Query(); - } - else - { - QueryText = string.Empty; + // If we are returning from query results and we have not set select item yet, + // we need to clear the preview selected item + if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value) + { + PreviewSelectedItem = null; + } } } - _selectedResults.Visbility = Visibility.Visible; + _selectedResults.Visibility = Visibility.Visible; } } public Visibility ProgressBarVisibility { get; set; } - public Visibility MainWindowVisibility { get; set; } + + // This is to be used for determining the visibility status of the main window instead of MainWindowVisibility + // because it is more accurate and reliable representation than using Visibility as a condition check + public bool MainWindowVisibilityStatus { get; set; } = true; - public ICommand EscCommand { get; set; } - public ICommand SelectNextItemCommand { get; set; } - public ICommand SelectPrevItemCommand { get; set; } - public ICommand SelectNextPageCommand { get; set; } - public ICommand SelectPrevPageCommand { get; set; } - public ICommand SelectFirstResultCommand { get; set; } - public ICommand StartHelpCommand { get; set; } - public ICommand LoadContextMenuCommand { get; set; } - public ICommand LoadHistoryCommand { get; set; } - public ICommand OpenResultCommand { get; set; } - public ICommand ReloadPluginDataCommand { get; set; } + public event VisibilityChangedEventHandler VisibilityChanged; - public string OpenResultCommandModifiers { get; private set; } + public Visibility ClockPanelVisibility { get; set; } + public Visibility SearchIconVisibility { get; set; } + public double ClockPanelOpacity { get; set; } = 1; + public double SearchIconOpacity { get; set; } = 1; - public string Image => Constant.QueryTextBoxIconImagePath; + private string _placeholderText; + public string PlaceholderText + { + get => string.IsNullOrEmpty(_placeholderText) ? App.API.GetTranslation("queryTextBoxPlaceholder") : _placeholderText; + set + { + _placeholderText = value; + OnPropertyChanged(); + } + } + + public double MainWindowWidth + { + get => Settings.WindowSize; + set + { + if (!MainWindowVisibilityStatus) return; + Settings.WindowSize = value; + } + } + + public double MainWindowHeight + { + get => Settings.WindowHeightSize; + set => Settings.WindowHeightSize = value; + } + + public double QueryBoxFontSize + { + get => Settings.QueryBoxFontSize; + set => Settings.QueryBoxFontSize = value; + } + + public double ItemHeightSize + { + get => Settings.ItemHeightSize; + set => Settings.ItemHeightSize = value; + } + + public double ResultItemFontSize + { + get => Settings.ResultItemFontSize; + set => Settings.ResultItemFontSize = value; + } + + public double ResultSubItemFontSize + { + get => Settings.ResultSubItemFontSize; + set => Settings.ResultSubItemFontSize = value; + } + + public ImageSource PluginIconSource { get; private set; } = null; + + public string PluginIconPath { get; set; } = null; + + public string OpenResultCommandModifiers => Settings.OpenResultModifiers; + + private static string VerifyOrSetDefaultHotkey(string hotkey, string defaultHotkey) + { + try + { + var converter = new KeyGestureConverter(); + var key = (KeyGesture)converter.ConvertFromString(hotkey); + } + catch (Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException) + { + return defaultHotkey; + } + + return hotkey; + } + + public string PreviewHotkey => VerifyOrSetDefaultHotkey(Settings.PreviewHotkey, "F1"); + public string AutoCompleteHotkey => VerifyOrSetDefaultHotkey(Settings.AutoCompleteHotkey, "Ctrl+Tab"); + public string AutoCompleteHotkey2 => VerifyOrSetDefaultHotkey(Settings.AutoCompleteHotkey2, ""); + public string SelectNextItemHotkey => VerifyOrSetDefaultHotkey(Settings.SelectNextItemHotkey, "Tab"); + public string SelectNextItemHotkey2 => VerifyOrSetDefaultHotkey(Settings.SelectNextItemHotkey2, ""); + public string SelectPrevItemHotkey => VerifyOrSetDefaultHotkey(Settings.SelectPrevItemHotkey, "Shift+Tab"); + public string SelectPrevItemHotkey2 => VerifyOrSetDefaultHotkey(Settings.SelectPrevItemHotkey2, ""); + public string SelectNextPageHotkey => VerifyOrSetDefaultHotkey(Settings.SelectNextPageHotkey, ""); + public string SelectPrevPageHotkey => VerifyOrSetDefaultHotkey(Settings.SelectPrevPageHotkey, ""); + public string OpenContextMenuHotkey => VerifyOrSetDefaultHotkey(Settings.OpenContextMenuHotkey, "Ctrl+O"); + public string SettingWindowHotkey => VerifyOrSetDefaultHotkey(Settings.SettingWindowHotkey, "Ctrl+I"); + public string CycleHistoryUpHotkey => VerifyOrSetDefaultHotkey(Settings.CycleHistoryUpHotkey, "Alt+Up"); + public string CycleHistoryDownHotkey => VerifyOrSetDefaultHotkey(Settings.CycleHistoryDownHotkey, "Alt+Down"); + + public bool StartWithEnglishMode => Settings.AlwaysStartEn; #endregion - public void Query() + #region Preview + + private static readonly int ResultAreaColumnPreviewShown = 1; + private static readonly int ResultAreaColumnPreviewHidden = 3; + + private bool? _selectedItemFromQueryResults; + + private ResultViewModel _previewSelectedItem; + public ResultViewModel PreviewSelectedItem { - if (SelectedIsFromQueryResults()) + get => _previewSelectedItem; + set { - QueryResults(); + _previewSelectedItem = value; + OnPropertyChanged(); + } + } + + public bool InternalPreviewVisible + { + get + { + if (ResultAreaColumn == ResultAreaColumnPreviewShown) + return true; + + if (ResultAreaColumn == ResultAreaColumnPreviewHidden) + return false; +#if DEBUG + throw new NotImplementedException("ResultAreaColumn should match ResultAreaColumnPreviewShown/ResultAreaColumnPreviewHidden value"); +#else + Log.Error("MainViewModel", "ResultAreaColumnPreviewHidden/ResultAreaColumnPreviewShown int value not implemented", "InternalPreviewVisible"); + return false; +#endif + } + } + + public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown; + + // This is not a reliable indicator of whether external preview is visible due to the + // ability of manually closing/exiting the external preview program which, does not inform flow that + // preview is no longer available. + public bool ExternalPreviewVisible { get; private set; } + + private async Task ShowPreviewAsync() + { + var useExternalPreview = PluginManager.UseExternalPreview(); + + switch (useExternalPreview) + { + case true + when CanExternalPreviewSelectedResult(out var path): + // Internal preview may still be on when user switches to external + if (InternalPreviewVisible) + HideInternalPreview(); + + _ = OpenExternalPreviewAsync(path); + break; + + case true + when !CanExternalPreviewSelectedResult(out var _): + if (ExternalPreviewVisible) + await CloseExternalPreviewAsync(); + + ShowInternalPreview(); + break; + + case false: + ShowInternalPreview(); + break; + } + } + + private void HidePreview() + { + if (PluginManager.UseExternalPreview()) + _ = CloseExternalPreviewAsync(); + + if (InternalPreviewVisible) + HideInternalPreview(); + } + + [RelayCommand] + private void TogglePreview() + { + if (InternalPreviewVisible || ExternalPreviewVisible) + { + HidePreview(); + } + else + { + _ = ShowPreviewAsync(); + } + } + + private async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) + { + await PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false); + ExternalPreviewVisible = true; + } + + private async Task CloseExternalPreviewAsync() + { + await PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false); + ExternalPreviewVisible = false; + } + + private static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) + { + await PluginManager.SwitchExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false); + } + + private void ShowInternalPreview() + { + ResultAreaColumn = ResultAreaColumnPreviewShown; + PreviewSelectedItem?.LoadPreviewImage(); + } + + private void HideInternalPreview() + { + ResultAreaColumn = ResultAreaColumnPreviewHidden; + } + + public void ResetPreview() + { + switch (Settings.AlwaysPreview) + { + case true + when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path): + _ = OpenExternalPreviewAsync(path); + break; + case true: + ShowInternalPreview(); + break; + case false: + HidePreview(); + break; + } + } + + private async Task UpdatePreviewAsync() + { + switch (PluginManager.UseExternalPreview()) + { + case true + when CanExternalPreviewSelectedResult(out var path): + if (ExternalPreviewVisible) + { + _ = SwitchExternalPreviewAsync(path, false); + } + else if (InternalPreviewVisible) + { + HideInternalPreview(); + _ = OpenExternalPreviewAsync(path); + } + break; + case true + when !CanExternalPreviewSelectedResult(out var _): + if (ExternalPreviewVisible) + { + await CloseExternalPreviewAsync(); + ShowInternalPreview(); + } + break; + case false + when InternalPreviewVisible: + PreviewSelectedItem?.LoadPreviewImage(); + break; + } + } + + private bool CanExternalPreviewSelectedResult(out string path) + { + path = QueryResultsPreviewed() ? Results.SelectedItem?.Result?.Preview.FilePath : string.Empty; + return !string.IsNullOrEmpty(path); + } + + private bool QueryResultsPreviewed() + { + var previewed = PreviewSelectedItem == Results.SelectedItem; + return previewed; + } + + #endregion + + #region Query + + public void Query(bool searchDelay, bool isReQuery = false) + { + _ = QueryAsync(searchDelay, isReQuery); + } + + private async Task QueryAsync(bool searchDelay, bool isReQuery = false) + { + if (QueryResultsSelected()) + { + await QueryResultsAsync(searchDelay, isReQuery); } else if (ContextMenuSelected()) { @@ -392,21 +1076,20 @@ namespace Flow.Launcher.ViewModel if (!string.IsNullOrEmpty(query)) { - var filtered = results.Where + var filtered = results.Select(x => x.Clone()).Where ( r => { - var match = StringMatcher.FuzzySearch(query, r.Title); + var match = App.API.FuzzySearch(query, r.Title); if (!match.IsSearchPrecisionScoreMet()) { - match = StringMatcher.FuzzySearch(query, r.SubTitle); + match = App.API.FuzzySearch(query, r.SubTitle); } if (!match.IsSearchPrecisionScoreMet()) return false; r.Score = match.Score; return true; - }).ToList(); ContextMenu.AddResults(filtered, id); } @@ -426,21 +1109,23 @@ namespace Flow.Launcher.ViewModel var results = new List(); foreach (var h in _history.Items) { - var title = _translator.GetTranslation("executeQuery"); - var time = _translator.GetTranslation("lastExecuteTime"); + var title = App.API.GetTranslation("executeQuery"); + var time = App.API.GetTranslation("lastExecuteTime"); var result = new Result { Title = string.Format(title, h.Query), SubTitle = string.Format(time, h.ExecutedDateTime), IcoPath = "Images\\history.png", - OriginQuery = new Query + Preview = new Result.PreviewInfo { - RawQuery = h.Query + PreviewImagePath = Constant.HistoryIcon, + Description = string.Format(time, h.ExecutedDateTime) }, + OriginQuery = new Query { RawQuery = h.Query }, Action = _ => { SelectedResults = Results; - ChangeQueryText(h.Query); + App.API.ChangeQuery(h.Query); return false; } }; @@ -451,8 +1136,8 @@ namespace Flow.Launcher.ViewModel { var filtered = results.Where ( - r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || - StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() + r => App.API.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || + App.API.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() ).ToList(); History.AddResults(filtered, id); } @@ -462,25 +1147,41 @@ namespace Flow.Launcher.ViewModel } } - private readonly IReadOnlyList _emptyResult = new List(); - - private async void QueryResults() + private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { _updateSource?.Cancel(); - if (string.IsNullOrWhiteSpace(QueryText)) + var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); + + var plugins = PluginManager.ValidPluginsForQuery(query); + + if (query == null || plugins.Count == 0) // shortcut expanded { Results.Clear(); - Results.Visbility = Visibility.Collapsed; + Results.Visibility = Visibility.Collapsed; + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; return; } + else if (plugins.Count == 1) + { + PluginIconPath = plugins.Single().Metadata.IcoPath; + PluginIconSource = await App.API.LoadImageAsync(PluginIconPath); + SearchIconVisibility = Visibility.Hidden; + } + else + { + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; + } _updateSource?.Dispose(); var currentUpdateSource = new CancellationTokenSource(); _updateSource = currentUpdateSource; - var currentCancellationToken = _updateSource.Token; - _updateToken = currentCancellationToken; + _updateToken = _updateSource.Token; ProgressBarVisibility = Visibility.Hidden; _isQueryRunning = true; @@ -488,45 +1189,46 @@ namespace Flow.Launcher.ViewModel // Switch to ThreadPool thread await TaskScheduler.Default; - if (currentCancellationToken.IsCancellationRequested) + if (_updateSource.Token.IsCancellationRequested) return; - var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); + // Update the query's IsReQuery property to true if this is a re-query + query.IsReQuery = isReQuery; // handle the exclusiveness of plugin using action keyword RemoveOldQueryResults(query); _lastQuery = query; - var plugins = PluginManager.ValidPluginsForQuery(query); - if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign) { // Wait 45 millisecond for query change in global query // if query changes, return so that it won't be calculated - await Task.Delay(45, currentCancellationToken); - if (currentCancellationToken.IsCancellationRequested) + await Task.Delay(45, _updateSource.Token); + if (_updateSource.Token.IsCancellationRequested) return; } - _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => - { - // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet - if (!currentCancellationToken.IsCancellationRequested && _isQueryRunning) + _ = Task.Delay(200, _updateSource.Token).ContinueWith(_ => { - ProgressBarVisibility = Visibility.Visible; - } - }, currentCancellationToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); + // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet + if (!_updateSource.Token.IsCancellationRequested && _isQueryRunning) + { + ProgressBarVisibility = Visibility.Visible; + } + }, + _updateSource.Token, + TaskContinuationOptions.NotOnCanceled, + TaskScheduler.Default); - // plugins is ICollection, meaning LINQ will get the Count and preallocate Array + // plugins are ICollection, meaning LINQ will get the Count and preallocate Array var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTask(plugin), + false => QueryTaskAsync(plugin, _updateSource.Token), true => Task.CompletedTask }).ToArray(); - try { // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first @@ -537,42 +1239,117 @@ namespace Flow.Launcher.ViewModel // nothing to do here } - if (currentCancellationToken.IsCancellationRequested) + if (_updateSource.Token.IsCancellationRequested) return; // this should happen once after all queries are done so progress bar should continue // until the end of all querying _isQueryRunning = false; - if (!currentCancellationToken.IsCancellationRequested) + if (!_updateSource.Token.IsCancellationRequested) { // update to hidden if this is still the current query ProgressBarVisibility = Visibility.Hidden; } // Local function - async Task QueryTask(PluginPair plugin) + async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) { + if (searchDelay) + { + var searchDelayTime = plugin.Metadata.SearchDelayTime ?? Settings.SearchDelayTime; + + await Task.Delay(searchDelayTime, token); + + if (token.IsCancellationRequested) + return; + } + // Since it is wrapped within a ThreadPool Thread, the synchronous context is null // Task.Yield will force it to run in ThreadPool await Task.Yield(); - IReadOnlyList results = await PluginManager.QueryForPluginAsync(plugin, query, currentCancellationToken); + IReadOnlyList results = + await PluginManager.QueryForPluginAsync(plugin, query, token); - currentCancellationToken.ThrowIfCancellationRequested(); + if (token.IsCancellationRequested) + return; - results ??= _emptyResult; + IReadOnlyList resultsCopy; + if (results == null) + { + resultsCopy = _emptyResult; + } + else + { + // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. + resultsCopy = DeepCloneResults(results, token); + } - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken))) + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, + token, reSelect))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); } } } + private Query ConstructQuery(string queryText, IEnumerable customShortcuts, + IEnumerable builtInShortcuts) + { + if (string.IsNullOrWhiteSpace(queryText)) + { + return null; + } + + StringBuilder queryBuilder = new(queryText); + StringBuilder queryBuilderTmp = new(queryText); + + // Sorting order is important here, the reason is for matching longest shortcut by default + foreach (var shortcut in customShortcuts.OrderByDescending(x => x.Key.Length)) + { + if (queryBuilder.Equals(shortcut.Key)) + { + queryBuilder.Replace(shortcut.Key, shortcut.Expand()); + } + + queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand()); + } + + string customExpanded = queryBuilder.ToString(); + + Application.Current.Dispatcher.Invoke(() => + { + foreach (var shortcut in builtInShortcuts) + { + try + { + if (customExpanded.Contains(shortcut.Key)) + { + var expansion = shortcut.Expand(); + queryBuilder.Replace(shortcut.Key, expansion); + queryBuilderTmp.Replace(shortcut.Key, expansion); + } + } + catch (Exception e) + { + Log.Exception( + $"{nameof(MainViewModel)}.{nameof(ConstructQuery)}|Error when expanding shortcut {shortcut.Key}", + e); + } + } + }); + + // show expanded builtin shortcuts + // use private field to avoid infinite recursion + _queryText = queryBuilderTmp.ToString(); + + var query = QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); + return query; + } private void RemoveOldQueryResults(Query query) { - if (_lastQuery.ActionKeyword != query.ActionKeyword) + if (_lastQuery?.ActionKeyword != query?.ActionKeyword) { Results.Clear(); } @@ -585,13 +1362,14 @@ namespace Flow.Launcher.ViewModel { menu = new Result { - Title = InternationalizationManager.Instance.GetTranslation("cancelTopMostInThisQuery"), + Title = App.API.GetTranslation("cancelTopMostInThisQuery"), IcoPath = "Images\\down.png", PluginDirectory = Constant.ProgramDirectory, Action = _ => { _topMostRecord.Remove(result); - App.API.ShowMsg("Success"); + App.API.ShowMsg(App.API.GetTranslation("success")); + App.API.ReQuery(); return false; } }; @@ -600,13 +1378,15 @@ namespace Flow.Launcher.ViewModel { menu = new Result { - Title = InternationalizationManager.Instance.GetTranslation("setAsTopMostInThisQuery"), + Title = App.API.GetTranslation("setAsTopMostInThisQuery"), IcoPath = "Images\\up.png", + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xeac2"), PluginDirectory = Constant.ProgramDirectory, Action = _ => { _topMostRecord.AddOrUpdate(result); - App.API.ShowMsg("Success"); + App.API.ShowMsg(App.API.GetTranslation("success")); + App.API.ReQuery(); return false; } }; @@ -615,10 +1395,10 @@ namespace Flow.Launcher.ViewModel return menu; } - private Result ContextMenuPluginInfo(string id) + private static Result ContextMenuPluginInfo(string id) { var metadata = PluginManager.GetPluginForId(id).Metadata; - var translator = InternationalizationManager.Instance; + var translator = App.API; var author = translator.GetTranslation("author"); var website = translator.GetTranslation("website"); @@ -626,7 +1406,7 @@ namespace Flow.Launcher.ViewModel var plugin = translator.GetTranslation("plugin"); var title = $"{plugin}: {metadata.Name}"; var icon = metadata.IcoPath; - var subtitle = $"{author}: {metadata.Author}, {website}: {metadata.Website} {version}: {metadata.Version}"; + var subtitle = $"{author} {metadata.Author}"; var menu = new Result { @@ -634,12 +1414,16 @@ namespace Flow.Launcher.ViewModel IcoPath = icon, SubTitle = subtitle, PluginDirectory = metadata.PluginDirectory, - Action = _ => false + Action = _ => + { + App.API.OpenUrl(metadata.Website); + return true; + } }; return menu; } - private bool SelectedIsFromQueryResults() + internal bool QueryResultsSelected() { var selected = SelectedResults == Results; return selected; @@ -657,113 +1441,157 @@ namespace Flow.Launcher.ViewModel return selected; } + #endregion + #region Hotkey - private void SetHotkey(string hotkeyStr, EventHandler action) + public void ToggleFlowLauncher() { - var hotkey = new HotkeyModel(hotkeyStr); - SetHotkey(hotkey, action); - } - - private void SetHotkey(HotkeyModel hotkey, EventHandler action) - { - string hotkeyStr = hotkey.ToString(); - try + if (!MainWindowVisibilityStatus) { - HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action); + Show(); } - catch (Exception) + else { - string errorMsg = - string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), - hotkeyStr); - MessageBox.Show(errorMsg); - } - } - - public void RemoveHotkey(string hotkeyStr) - { - if (!string.IsNullOrEmpty(hotkeyStr)) - { - HotkeyManager.Current.Remove(hotkeyStr); + Hide(); } } /// /// Checks if Flow Launcher should ignore any hotkeys /// - /// - private bool ShouldIgnoreHotkeys() + public bool ShouldIgnoreHotkeys() { - //double if to omit calling win32 function - if (_settings.IgnoreHotkeysOnFullscreen) - if (WindowsInteropHelper.IsWindowFullscreen()) - return true; - - return false; - } - - private void SetCustomPluginHotkey() - { - if (_settings.CustomPluginHotkeys == null) return; - foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys) - { - SetHotkey(hotkey.Hotkey, (s, e) => - { - if (ShouldIgnoreHotkeys()) return; - MainWindowVisibility = Visibility.Visible; - ChangeQueryText(hotkey.ActionKeyword); - }); - } - } - - private void SetOpenResultModifiers() - { - OpenResultCommandModifiers = _settings.OpenResultModifiers ?? DefaultOpenResultModifiers; - } - - private void OnHotkey(object sender, HotkeyEventArgs e) - { - if (!ShouldIgnoreHotkeys()) - { - if (_settings.LastQueryMode == LastQueryMode.Empty) - { - ChangeQueryText(string.Empty); - } - else if (_settings.LastQueryMode == LastQueryMode.Preserved) - { - LastQuerySelected = true; - } - else if (_settings.LastQueryMode == LastQueryMode.Selected) - { - LastQuerySelected = false; - } - else - { - throw new ArgumentException($"wrong LastQueryMode: <{_settings.LastQueryMode}>"); - } - - ToggleFlowLauncher(); - e.Handled = true; - } - } - - private void ToggleFlowLauncher() - { - if (MainWindowVisibility != Visibility.Visible) - { - MainWindowVisibility = Visibility.Visible; - } - else - { - MainWindowVisibility = Visibility.Collapsed; - } + return Settings.IgnoreHotkeysOnFullscreen && Win32Helper.IsForegroundWindowFullscreen() || GameModeStatus; } #endregion #region Public Methods +#pragma warning disable VSTHRD100 // Avoid async void methods + + public void Show() + { + // When application is exiting, the Application.Current will be null + Application.Current?.Dispatcher.Invoke(() => + { + // When application is exiting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + // 📌 Remove DWM Cloak (Make the window visible normally) + Win32Helper.DWMSetCloakForWindow(mainWindow, false); + + // Set clock and search icon opacity + var opacity = Settings.UseAnimation ? 0.0 : 1.0; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + + // Set clock and search icon visibility + ClockPanelVisibility = string.IsNullOrEmpty(QueryText) ? Visibility.Visible : Visibility.Collapsed; + if (PluginIconSource != null) + { + SearchIconOpacity = 0.0; + } + else + { + SearchIconVisibility = Visibility.Visible; + } + } + }, DispatcherPriority.Render); + + // Update WPF properties + MainWindowVisibility = Visibility.Visible; + MainWindowVisibilityStatus = true; + VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); + + // Switch keyboard layout + if (StartWithEnglishMode) + { + Win32Helper.SwitchToEnglishKeyboardLayout(true); + } + } + + public async void Hide() + { + lastHistoryIndex = 1; + + if (ExternalPreviewVisible) + { + await CloseExternalPreviewAsync(); + } + + if (!QueryResultsSelected()) + { + SelectedResults = Results; + } + + switch (Settings.LastQueryMode) + { + case LastQueryMode.Empty: + await ChangeQueryTextAsync(string.Empty); + break; + case LastQueryMode.Preserved: + case LastQueryMode.Selected: + LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved; + break; + case LastQueryMode.ActionKeywordPreserved: + case LastQueryMode.ActionKeywordSelected: + var newQuery = _lastQuery.ActionKeyword; + + if (!string.IsNullOrEmpty(newQuery)) + newQuery += " "; + await ChangeQueryTextAsync(newQuery); + + if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected) + LastQuerySelected = false; + break; + } + + // When application is exiting, the Application.Current will be null + Application.Current?.Dispatcher.Invoke(() => + { + // When application is exiting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + // Set clock and search icon opacity + var opacity = Settings.UseAnimation ? 0.0 : 1.0; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + + // Set clock and search icon visibility + ClockPanelVisibility = Visibility.Hidden; + SearchIconVisibility = Visibility.Hidden; + + // Force UI update + mainWindow.ClockPanel.UpdateLayout(); + mainWindow.SearchIcon.UpdateLayout(); + + // 📌 Apply DWM Cloak (Completely hide the window) + Win32Helper.DWMSetCloakForWindow(mainWindow, true); + } + }, DispatcherPriority.Render); + + // Switch keyboard layout + if (StartWithEnglishMode) + { + Win32Helper.RestorePreviousKeyboardLayout(); + } + + // Delay for a while to make sure clock will not flicker + await Task.Delay(50); + + // Update WPF properties + MainWindowVisibilityStatus = false; + MainWindowVisibility = Visibility.Collapsed; + VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = false }); + } + +#pragma warning restore VSTHRD100 // Avoid async void methods + + /// + /// Save history, user selected records and top most records + /// public void Save() { _historyItemsStorage.Save(); @@ -772,9 +1600,9 @@ namespace Flow.Launcher.ViewModel } /// - /// To avoid deadlock, this method should not called from main thread + /// To avoid deadlock, this method should not be called from main thread /// - public void UpdateResultView(IEnumerable resultsForUpdates) + public void UpdateResultView(ICollection resultsForUpdates) { if (!resultsForUpdates.Any()) return; @@ -797,26 +1625,79 @@ namespace Flow.Launcher.ViewModel } #endif - foreach (var metaResults in resultsForUpdates) { foreach (var result in metaResults.Results) { if (_topMostRecord.IsTopMost(result)) { - result.Score = int.MaxValue; + result.Score = Result.MaxScore; } else { var priorityScore = metaResults.Metadata.Priority * 150; - result.Score += _userSelectedRecord.GetSelectedCount(result) + priorityScore; + if (result.AddSelectedCount) + { + if ((long)result.Score + _userSelectedRecord.GetSelectedCount(result) + priorityScore > Result.MaxScore) + { + result.Score = Result.MaxScore; + } + else + { + result.Score += _userSelectedRecord.GetSelectedCount(result) + priorityScore; + } + } + else + { + if ((long)result.Score + priorityScore > Result.MaxScore) + { + result.Score = Result.MaxScore; + } + else + { + result.Score += priorityScore; + } + } } } } - Results.AddResults(resultsForUpdates, token); + // it should be the same for all results + bool reSelect = resultsForUpdates.First().ReSelectFirstResult; + + Results.AddResults(resultsForUpdates, token, reSelect); + } + + #endregion + + #region IDisposable + + private bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _updateSource?.Dispose(); + _resultsUpdateChannelWriter?.Complete(); + if (_resultsViewUpdateTask?.IsCompleted == true) + { + _resultsViewUpdateTask.Dispose(); + } + _disposed = true; + } + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } #endregion } -} \ No newline at end of file +} diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs new file mode 100644 index 000000000..d1cf74501 --- /dev/null +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Plugin; +using Version = SemanticVersioning.Version; + +namespace Flow.Launcher.ViewModel +{ + public partial class PluginStoreItemViewModel : BaseModel + { + private PluginPair PluginManagerData => PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"); + public PluginStoreItemViewModel(UserPlugin plugin) + { + _plugin = plugin; + } + + private UserPlugin _plugin; + + public string ID => _plugin.ID; + public string Name => _plugin.Name; + public string Description => _plugin.Description; + public string Author => _plugin.Author; + public string Version => _plugin.Version; + public string Language => _plugin.Language; + public string Website => _plugin.Website; + public string UrlDownload => _plugin.UrlDownload; + public string UrlSourceCode => _plugin.UrlSourceCode; + public string IcoPath => _plugin.IcoPath; + + public bool LabelInstalled => PluginManager.GetPluginForId(_plugin.ID) != null; + public bool LabelUpdate => LabelInstalled && new Version(_plugin.Version) > new Version(PluginManager.GetPluginForId(_plugin.ID).Metadata.Version); + + internal const string None = "None"; + internal const string RecentlyUpdated = "RecentlyUpdated"; + internal const string NewRelease = "NewRelease"; + internal const string Installed = "Installed"; + + public string Category + { + get + { + string category = None; + if (DateTime.Now - _plugin.LatestReleaseDate < TimeSpan.FromDays(7)) + { + category = RecentlyUpdated; + } + if (DateTime.Now - _plugin.DateAdded < TimeSpan.FromDays(7)) + { + category = NewRelease; + } + if (PluginManager.GetPluginForId(_plugin.ID) != null) + { + category = Installed; + } + + return category; + } + } + + [RelayCommand] + private void ShowCommandQuery(string action) + { + var actionKeyword = PluginManagerData.Metadata.ActionKeywords.Any() ? PluginManagerData.Metadata.ActionKeywords[0] + " " : String.Empty; + App.API.ChangeQuery($"{actionKeyword}{action} {_plugin.Name}"); + App.API.ShowMainWindow(); + } + } +} diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 1ac320cf0..8312806fb 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -1,43 +1,196 @@ +using System.Linq; +using System.Threading.Tasks; using System.Windows; +using System.Windows.Controls; using System.Windows.Media; -using Flow.Launcher.Plugin; -using Flow.Launcher.Infrastructure.Image; +using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.Image; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.Resources.Controls; namespace Flow.Launcher.ViewModel { - public class PluginViewModel : BaseModel + public partial class PluginViewModel : BaseModel { - public PluginPair PluginPair { get; set; } + private static readonly Settings Settings = Ioc.Default.GetRequiredService(); - public ImageSource Image => ImageLoader.Load(PluginPair.Metadata.IcoPath); - public bool PluginState + private readonly PluginPair _pluginPair; + public PluginPair PluginPair { - get { return !PluginPair.Metadata.Disabled; } - set + get => _pluginPair; + init { - PluginPair.Metadata.Disabled = !value; + _pluginPair = value; + value.Metadata.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(PluginPair.Metadata.AvgQueryTime)) + OnPropertyChanged(nameof(QueryTime)); + }; } } - public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed; - public string InitilizaTime => PluginPair.Metadata.InitTime.ToString() + "ms"; - public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms"; - public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords); - public int Priority => PluginPair.Metadata.Priority; - public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword) + private static string PluginManagerActionKeyword + { + get + { + var keyword = PluginManager + .GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7") + .Metadata.ActionKeywords.FirstOrDefault(); + return keyword switch + { + null or "*" => string.Empty, + _ => keyword + }; + } + } + + private async Task LoadIconAsync() + { + Image = await App.API.LoadImageAsync(PluginPair.Metadata.IcoPath); + OnPropertyChanged(nameof(Image)); + } + + public ImageSource Image + { + get + { + if (_image == ImageLoader.MissingImage) + _ = LoadIconAsync(); + + return _image; + } + set => _image = value; + } + + public bool PluginState + { + get => !PluginPair.Metadata.Disabled; + set + { + PluginPair.Metadata.Disabled = !value; + PluginSettingsObject.Disabled = !value; + } + } + + public bool IsExpanded + { + get => _isExpanded; + set + { + _isExpanded = value; + + OnPropertyChanged(); + OnPropertyChanged(nameof(SettingControl)); + } + } + + public int Priority + { + get => PluginPair.Metadata.Priority; + set + { + PluginPair.Metadata.Priority = value; + PluginSettingsObject.Priority = value; + } + } + + public double PluginSearchDelayTime + { + get => PluginPair.Metadata.SearchDelayTime == null ? + double.NaN : + PluginPair.Metadata.SearchDelayTime.Value; + set + { + if (double.IsNaN(value)) + { + PluginPair.Metadata.SearchDelayTime = null; + PluginSettingsObject.SearchDelayTime = null; + } + else + { + PluginPair.Metadata.SearchDelayTime = (int)value; + PluginSettingsObject.SearchDelayTime = (int)value; + } + } + } + + private Control _settingControl; + private bool _isExpanded; + + private Control _bottomPart1; + public Control BottomPart1 => IsExpanded ? _bottomPart1 ??= new InstalledPluginDisplayKeyword() : null; + + private Control _bottomPart2; + public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; + + public bool HasSettingControl => PluginPair.Plugin is ISettingProvider && + (PluginPair.Plugin is not JsonRPCPluginBase jsonRPCPluginBase || jsonRPCPluginBase.NeedCreateSettingPanel()); + public Control SettingControl + => IsExpanded + ? _settingControl + ??= HasSettingControl + ? ((ISettingProvider)PluginPair.Plugin).CreateSettingPanel() + : null + : null; + private ImageSource _image = ImageLoader.MissingImage; + + public Visibility ActionKeywordsVisibility => PluginPair.Metadata.HideActionKeywordPanel ? + Visibility.Collapsed : Visibility.Visible; + public string InitializeTime => PluginPair.Metadata.InitTime + "ms"; + public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms"; + public string Version => App.API.GetTranslation("plugin_query_version") + " " + PluginPair.Metadata.Version; + public string InitAndQueryTime => + App.API.GetTranslation("plugin_init_time") + " " + + PluginPair.Metadata.InitTime + "ms, " + + App.API.GetTranslation("plugin_query_time") + " " + + PluginPair.Metadata.AvgQueryTime + "ms"; + public string ActionKeywordsText => string.Join(Query.ActionKeywordSeparator, PluginPair.Metadata.ActionKeywords); + public string SearchDelayTimeText => PluginPair.Metadata.SearchDelayTime == null ? + App.API.GetTranslation("default") : + App.API.GetTranslation($"SearchDelayTime{PluginPair.Metadata.SearchDelayTime}"); + public Infrastructure.UserSettings.Plugin PluginSettingsObject{ get; init; } + public bool SearchDelayEnabled => Settings.SearchQueryResultsWithDelay; + public string DefaultSearchDelay => Settings.SearchDelayTime.ToString(); + + public void OnActionKeywordsChanged() { - PluginManager.ReplaceActionKeyword(PluginPair.Metadata.ID, oldActionKeyword, newActionKeyword); - OnPropertyChanged(nameof(ActionKeywordsText)); } - public void ChangePriority(int newPriority) + public void OnSearchDelayTimeChanged() { - PluginPair.Metadata.Priority = newPriority; - OnPropertyChanged(nameof(Priority)); + OnPropertyChanged(nameof(SearchDelayTimeText)); } - public bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword); + [RelayCommand] + private void OpenPluginDirectory() + { + var directory = PluginPair.Metadata.PluginDirectory; + if (!string.IsNullOrEmpty(directory)) + App.API.OpenDirectory(directory); + } + + [RelayCommand] + private void OpenSourceCodeLink() + { + App.API.OpenUrl(PluginPair.Metadata.Website); + } + + [RelayCommand] + private void OpenDeletePluginWindow() + { + App.API.ChangeQuery($"{PluginManagerActionKeyword} uninstall {PluginPair.Metadata.Name}".Trim(), true); + App.API.ShowMainWindow(); + } + + [RelayCommand] + private void SetActionKeywords() + { + var changeKeywordsWindow = new ActionKeywords(this); + changeKeywordsWindow.ShowDialog(); + } } } diff --git a/Flow.Launcher/ViewModel/RelayCommand.cs b/Flow.Launcher/ViewModel/RelayCommand.cs index ee4140ed5..e8d4af8b5 100644 --- a/Flow.Launcher/ViewModel/RelayCommand.cs +++ b/Flow.Launcher/ViewModel/RelayCommand.cs @@ -5,7 +5,7 @@ namespace Flow.Launcher.ViewModel { public class RelayCommand : ICommand { - private Action _action; + private readonly Action _action; public RelayCommand(Action action) { @@ -17,7 +17,9 @@ namespace Flow.Launcher.ViewModel return true; } +#pragma warning disable CS0067 // the event is never used public event EventHandler CanExecuteChanged; +#pragma warning restore CS0067 public virtual void Execute(object parameter) { diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index c91bbb107..9aab71a32 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Drawing.Text; +using System.IO; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; @@ -11,120 +14,254 @@ namespace Flow.Launcher.ViewModel { public class ResultViewModel : BaseModel { - public class LazyAsync : Lazy> - { - private readonly T defaultValue; - - private readonly Action _updateCallback; - public new T Value - { - get - { - if (!IsValueCreated) - { - _ = Exercute(); // manually use callback strategy - - return defaultValue; - } - - if (!base.Value.IsCompletedSuccessfully) - return defaultValue; - - return base.Value.Result; - - // If none of the variables captured by the local function are captured by other lambdas, - // the compiler can avoid heap allocations. - async ValueTask Exercute() - { - await base.Value.ConfigureAwait(false); - _updateCallback(); - } - - } - } - public LazyAsync(Func> factory, T defaultValue, Action updateCallback) : base(factory) - { - if (defaultValue != null) - { - this.defaultValue = defaultValue; - } - - _updateCallback = updateCallback; - } - } + private static readonly PrivateFontCollection FontCollection = new(); + private static readonly Dictionary Fonts = new(); public ResultViewModel(Result result, Settings settings) { - if (result != null) - { - Result = result; - - Image = new LazyAsync( - SetImage, - ImageLoader.DefaultImage, - () => - { - OnPropertyChanged(nameof(Image)); - }); - } - Settings = settings; + + if (result == null) return; + + Result = result; + + if (Result.Glyph is { FontFamily: not null } glyph) + { + // Checks if it's a system installed font, which does not require path to be provided. + if (glyph.FontFamily.EndsWith(".ttf") || glyph.FontFamily.EndsWith(".otf")) + { + string fontFamilyPath = glyph.FontFamily; + + if (!Path.IsPathRooted(fontFamilyPath)) + { + fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath); + } + + if (Fonts.TryGetValue(fontFamilyPath, out var value)) + { + Glyph = glyph with + { + FontFamily = value + }; + } + else + { + FontCollection.AddFontFile(fontFamilyPath); + Fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{FontCollection.Families[^1].Name}"; + Glyph = glyph with + { + FontFamily = Fonts[fontFamilyPath] + }; + } + } + else + { + Glyph = glyph; + } + } } - public Settings Settings { get; private set; } + public Settings Settings { get; } - public Visibility ShowOpenResultHotkey => Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Hidden; + public Visibility ShowOpenResultHotkey => + Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Collapsed; + + public Visibility ShowDefaultPreview => Result.PreviewPanel == null ? Visibility.Visible : Visibility.Collapsed; + + public Visibility ShowCustomizedPreview => Result.PreviewPanel == null ? Visibility.Collapsed : Visibility.Visible; + + public Visibility ShowIcon + { + get + { + // If both glyph and image icons are not available, it will then be the default icon + if (!ImgIconAvailable && !GlyphAvailable) + return Visibility.Visible; + + // Although user can choose to use glyph icons, plugins may choose to supply only image icons. + // In this case we ignore the setting because otherwise icons will not display as intended + if (Settings.UseGlyphIcons && !GlyphAvailable && ImgIconAvailable) + return Visibility.Visible; + + return !Settings.UseGlyphIcons && ImgIconAvailable ? Visibility.Visible : Visibility.Hidden; + } + } + + public Visibility ShowPreviewImage + { + get + { + if (PreviewImageAvailable) + return Visibility.Visible; + + // Fall back to icon + return ShowIcon; + } + } + + public double IconRadius + { + get + { + if (Result.RoundedIcon) + return IconXY / 2; + + return IconXY; + } + + } + + public Visibility ShowGlyph + { + get + { + // Although user can choose to not use glyph icons, plugins may choose to supply only glyph icons. + // In this case we ignore the setting because otherwise icons will not display as intended + if (!Settings.UseGlyphIcons && !ImgIconAvailable && GlyphAvailable) + return Visibility.Visible; + + return Settings.UseGlyphIcons && GlyphAvailable ? Visibility.Visible : Visibility.Collapsed; + } + } + + private bool GlyphAvailable => Glyph is not null; + + private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPath) || Result.Icon is not null; + + private bool PreviewImageAvailable => !string.IsNullOrEmpty(Result.Preview.PreviewImagePath) || Result.Preview.PreviewDelegate != null; public string OpenResultModifiers => Settings.OpenResultModifiers; public string ShowTitleToolTip => string.IsNullOrEmpty(Result.TitleToolTip) - ? Result.Title - : Result.TitleToolTip; + ? Result.Title + : Result.TitleToolTip; public string ShowSubTitleToolTip => string.IsNullOrEmpty(Result.SubTitleToolTip) - ? Result.SubTitle - : Result.SubTitleToolTip; + ? Result.SubTitle + : Result.SubTitleToolTip; - public LazyAsync Image { get; set; } + private volatile bool _imageLoaded; + private volatile bool _previewImageLoaded; - private async ValueTask SetImage() + private ImageSource _image = ImageLoader.LoadingImage; + private ImageSource _previewImage = ImageLoader.LoadingImage; + + public ImageSource Image { - var imagePath = Result.IcoPath; - if (string.IsNullOrEmpty(imagePath) && Result.Icon != null) + get + { + if (!_imageLoaded) + { + _imageLoaded = true; + _ = LoadImageAsync(); + } + + return _image; + } + private set => _image = value; + } + + public ImageSource PreviewImage + { + get + { + if (!_previewImageLoaded) + { + _previewImageLoaded = true; + _ = LoadPreviewImageAsync(); + } + + return _previewImage; + } + private set => _previewImage = value; + } + + /// + /// Determines if to use the full width of the preview panel + /// + public bool UseBigThumbnail => Result.Preview.IsMedia; + + public GlyphInfo Glyph { get; set; } + + private async Task LoadImageInternalAsync(string imagePath, Result.IconDelegate icon, bool loadFullImage) + { + if (string.IsNullOrEmpty(imagePath) && icon != null) { try { - return Result.Icon(); + return icon(); } catch (Exception e) { - Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e); - return ImageLoader.DefaultImage; + Log.Exception( + $"|ResultViewModel.LoadImageInternalAsync|IcoPath is empty and exception when calling IconDelegate for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", + e); } } - if (ImageLoader.CacheContainImage(imagePath)) - // will get here either when icoPath has value\icon delegate is null\when had exception in delegate - return ImageLoader.Load(imagePath); - - return await Task.Run(() => ImageLoader.Load(imagePath)); + return await App.API.LoadImageAsync(imagePath, loadFullImage).ConfigureAwait(false); } - public Result Result { get; } - - public override bool Equals(object obj) + private async Task LoadImageAsync() { - var r = obj as ResultViewModel; - if (r != null) + var imagePath = Result.IcoPath; + var iconDelegate = Result.Icon; + if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img)) { - return Result.Equals(r.Result); + _image = img; } else { - return false; + // We need to modify the property not field here to trigger the OnPropertyChanged event + Image = await LoadImageInternalAsync(imagePath, iconDelegate, false).ConfigureAwait(false); } } + private async Task LoadPreviewImageAsync() + { + var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPath; + var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon; + if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img)) + { + _previewImage = img; + } + else + { + // We need to modify the property not field here to trigger the OnPropertyChanged event + PreviewImage = await LoadImageInternalAsync(imagePath, iconDelegate, true).ConfigureAwait(false); + } + } + + public void LoadPreviewImage() + { + if (ShowDefaultPreview == Visibility.Visible && !_previewImageLoaded && ShowPreviewImage == Visibility.Visible) + { + _previewImageLoaded = true; + _ = LoadPreviewImageAsync(); + } + } + + public Result Result { get; } + public int ResultProgress + { + get + { + if (Result.ProgressBar == null) + return 0; + + return Result.ProgressBar.Value; + } + } + + public string QuerySuggestionText { get; set; } + + public double IconXY { get; set; } = 32; + + public override bool Equals(object obj) + { + return obj is ResultViewModel r && Result.Equals(r.Result); + } + public override int GetHashCode() { return Result.GetHashCode(); diff --git a/Flow.Launcher/ViewModel/ResultsForUpdate.cs b/Flow.Launcher/ViewModel/ResultsForUpdate.cs index 94c6a923a..bc0be0de8 100644 --- a/Flow.Launcher/ViewModel/ResultsForUpdate.cs +++ b/Flow.Launcher/ViewModel/ResultsForUpdate.cs @@ -1,28 +1,16 @@ using Flow.Launcher.Plugin; -using System; using System.Collections.Generic; -using System.Text; using System.Threading; namespace Flow.Launcher.ViewModel { - public struct ResultsForUpdate + public record struct ResultsForUpdate( + IReadOnlyList Results, + PluginMetadata Metadata, + Query Query, + CancellationToken Token, + bool ReSelectFirstResult = true) { - public IReadOnlyList Results { get; } - - public PluginMetadata Metadata { get; } - public string ID { get; } - - public Query Query { get; } - public CancellationToken Token { get; } - - public ResultsForUpdate(IReadOnlyList results, PluginMetadata metadata, Query query, CancellationToken token) - { - Results = results; - Metadata = metadata; - Query = query; - Token = token; - ID = metadata.ID; - } + public string ID { get; } = Metadata.ID; } } diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 0766c7bbc..61566b415 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,5 +1,4 @@ -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -8,6 +7,9 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; +using System.Windows.Input; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; namespace Flow.Launcher.ViewModel { @@ -26,14 +28,21 @@ namespace Flow.Launcher.ViewModel Results = new ResultCollection(); BindingOperations.EnableCollectionSynchronization(Results, _collectionLock); } + public ResultsViewModel(Settings settings) : this() { _settings = settings; _settings.PropertyChanged += (s, e) => { - if (e.PropertyName == nameof(_settings.MaxResultsToShow)) + switch (e.PropertyName) { - OnPropertyChanged(nameof(MaxHeight)); + case nameof(_settings.MaxResultsToShow): + OnPropertyChanged(nameof(MaxHeight)); + break; + case nameof(_settings.ItemHeightSize): + OnPropertyChanged(nameof(ItemHeightSize)); + OnPropertyChanged(nameof(MaxHeight)); + break; } }; } @@ -42,13 +51,39 @@ namespace Flow.Launcher.ViewModel #region Properties - public int MaxHeight => MaxResults * 50; + public bool IsPreviewOn { get; set; } + + public double MaxHeight + { + get + { + var newResultsCount = MaxResults; + if (IsPreviewOn) + { + newResultsCount = (int)Math.Ceiling(380 / _settings.ItemHeightSize); + if (newResultsCount < MaxResults) + { + newResultsCount = MaxResults; + } + } + return newResultsCount * _settings.ItemHeightSize; + } + } + + public double ItemHeightSize + { + get => _settings.ItemHeightSize; + set => _settings.ItemHeightSize = value; + } public int SelectedIndex { get; set; } public ResultViewModel SelectedItem { get; set; } public Thickness Margin { get; set; } - public Visibility Visbility { get; set; } = Visibility.Collapsed; + public Visibility Visibility { get; set; } = Visibility.Collapsed; + + public ICommand RightClickResultCommand { get; init; } + public ICommand LeftClickResultCommand { get; init; } #endregion @@ -113,6 +148,11 @@ namespace Flow.Launcher.ViewModel SelectedIndex = NewIndex(0); } + public void SelectLastResult() + { + SelectedIndex = NewIndex(Results.Count - 1); + } + public void Clear() { lock (_collectionLock) @@ -143,36 +183,34 @@ namespace Flow.Launcher.ViewModel /// /// To avoid deadlock, this method should not called from main thread /// - public void AddResults(IEnumerable resultsForUpdates, CancellationToken token) + public void AddResults(ICollection resultsForUpdates, CancellationToken token, bool reselect = true) { var newResults = NewResults(resultsForUpdates); if (token.IsCancellationRequested) return; - UpdateResults(newResults, token); + UpdateResults(newResults, token, reselect); } - private void UpdateResults(List newResults, CancellationToken token = default) + private void UpdateResults(List newResults, CancellationToken token = default, bool reselect = true) { lock (_collectionLock) { // update UI in one run, so it can avoid UI flickering Results.Update(newResults, token); - if (Results.Any()) + if (reselect && Results.Any()) SelectedItem = Results[0]; } - switch (Visbility) + switch (Visibility) { case Visibility.Collapsed when Results.Count > 0: - Margin = new Thickness { Top = 8 }; SelectedIndex = 0; - Visbility = Visibility.Visible; + Visibility = Visibility.Visible; break; case Visibility.Visible when Results.Count == 0: - Margin = new Thickness { Top = 0 }; - Visbility = Visibility.Collapsed; + Visibility = Visibility.Collapsed; break; } } @@ -182,7 +220,6 @@ namespace Flow.Launcher.ViewModel if (newRawResults.Count == 0) return Results; - var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); return Results.Where(r => r.Result.PluginID != resultId) @@ -191,12 +228,12 @@ namespace Flow.Launcher.ViewModel .ToList(); } - private List NewResults(IEnumerable resultsForUpdates) + private List NewResults(ICollection resultsForUpdates) { if (!resultsForUpdates.Any()) return Results; - return Results.Where(r => r != null && !resultsForUpdates.Any(u => u.ID == r.Result.PluginID)) + return Results.Where(r => r?.Result != null && resultsForUpdates.All(u => u.ID != r.Result.PluginID)) .Concat(resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) .OrderByDescending(rv => rv.Result.Score) .ToList(); @@ -204,6 +241,7 @@ namespace Flow.Launcher.ViewModel #endregion #region FormattedText Dependency Property + public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached( "FormattedText", typeof(Inline), @@ -222,8 +260,7 @@ namespace Flow.Launcher.ViewModel private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var textBlock = d as TextBlock; - if (textBlock == null) return; + if (d is not TextBlock textBlock) return; var inline = (Inline)e.NewValue; @@ -232,6 +269,7 @@ namespace Flow.Launcher.ViewModel textBlock.Inlines.Add(inline); } + #endregion public class ResultCollection : List, INotifyCollectionChanged @@ -242,7 +280,6 @@ namespace Flow.Launcher.ViewModel public event NotifyCollectionChangedEventHandler CollectionChanged; - protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke(this, e); @@ -257,9 +294,10 @@ namespace Flow.Launcher.ViewModel return; // manually update event - // wpf use directx / double buffered already, so just reset all won't cause ui flickering + // wpf use DirectX / double buffered already, so just reset all won't cause ui flickering OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + private void AddAll(List Items) { for (int i = 0; i < Items.Count; i++) @@ -271,6 +309,7 @@ namespace Flow.Launcher.ViewModel OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); } } + public void RemoveAll(int Capacity = 512) { Clear(); @@ -310,4 +349,4 @@ namespace Flow.Launcher.ViewModel } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index 53789d2d9..51afe533d 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -1,477 +1,46 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using Flow.Launcher.Core; -using Flow.Launcher.Core.Configuration; -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; -using Flow.Launcher.Helper; -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Image; -using Flow.Launcher.Infrastructure.Storage; -using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using Flow.Launcher.Plugin.SharedModels; -namespace Flow.Launcher.ViewModel +namespace Flow.Launcher.ViewModel; + +public partial class SettingWindowViewModel : BaseModel { - public class SettingWindowViewModel : BaseModel + private readonly Settings _settings; + + public SettingWindowViewModel(Settings settings) { - private readonly Updater _updater; - private readonly IPortable _portable; - private readonly FlowLauncherJsonStorage _storage; + _settings = settings; + } - public SettingWindowViewModel(Updater updater, IPortable portable) - { - _updater = updater; - _portable = portable; - _storage = new FlowLauncherJsonStorage(); - Settings = _storage.Load(); - Settings.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(Settings.ActivateTimes)) - { - OnPropertyChanged(nameof(ActivatedTimes)); - } - }; - } + /// + /// Save Flow settings. Plugins settings are not included. + /// + public void Save() + { + _settings.Save(); + } - public Settings Settings { get; set; } + public double SettingWindowWidth + { + get => _settings.SettingWindowWidth; + set => _settings.SettingWindowWidth = value; + } - public async void UpdateApp() - { - await _updater.UpdateApp(App.API, false); - } + public double SettingWindowHeight + { + get => _settings.SettingWindowHeight; + set => _settings.SettingWindowHeight = value; + } - public bool AutoUpdates - { - get { return Settings.AutoUpdates; } - set - { - Settings.AutoUpdates = value; + public double? SettingWindowTop + { + get => _settings.SettingWindowTop; + set => _settings.SettingWindowTop = value; + } - if (value) - UpdateApp(); - } - } - - public bool AutoHideScrollBar - { - get => Settings.AutoHideScrollBar; - set => Settings.AutoHideScrollBar = value; - } - - // This is only required to set at startup. When portable mode enabled/disabled a restart is always required - private bool _portableMode = DataLocation.PortableDataLocationInUse(); - public bool PortableMode - { - get { return _portableMode; } - set - { - if (!_portable.CanUpdatePortability()) - return; - - if (DataLocation.PortableDataLocationInUse()) - { - _portable.DisablePortableMode(); - } - else - { - _portable.EnablePortableMode(); - } - } - } - - public void Save() - { - foreach (var vm in PluginViewModels) - { - var id = vm.PluginPair.Metadata.ID; - - Settings.PluginSettings.Plugins[id].Disabled = vm.PluginPair.Metadata.Disabled; - Settings.PluginSettings.Plugins[id].Priority = vm.Priority; - } - - PluginManager.Save(); - _storage.Save(); - } - - #region general - - // todo a better name? - public class LastQueryMode - { - public string Display { get; set; } - public Infrastructure.UserSettings.LastQueryMode Value { get; set; } - } - public List LastQueryModes - { - get - { - List modes = new List(); - var enums = (Infrastructure.UserSettings.LastQueryMode[])Enum.GetValues(typeof(Infrastructure.UserSettings.LastQueryMode)); - foreach (var e in enums) - { - var key = $"LastQuery{e}"; - var display = _translater.GetTranslation(key); - var m = new LastQueryMode { Display = display, Value = e, }; - modes.Add(m); - } - return modes; - } - } - - public string Language - { - get - { - return Settings.Language; - } - set - { - InternationalizationManager.Instance.ChangeLanguage(value); - - if (InternationalizationManager.Instance.PromptShouldUsePinyin(value)) - ShouldUsePinyin = true; - } - } - - public bool ShouldUsePinyin - { - get - { - return Settings.ShouldUsePinyin; - } - set - { - Settings.ShouldUsePinyin = value; - } - } - - public List QuerySearchPrecisionStrings - { - get - { - var precisionStrings = new List(); - - var enumList = Enum.GetValues(typeof(SearchPrecisionScore)).Cast().ToList(); - - enumList.ForEach(x => precisionStrings.Add(x.ToString())); - - return precisionStrings; - } - } - - public List OpenResultModifiersList => new List { KeyConstant.Alt, KeyConstant.Ctrl, $"{KeyConstant.Ctrl}+{KeyConstant.Alt}" }; - private Internationalization _translater => InternationalizationManager.Instance; - public List Languages => _translater.LoadAvailableLanguages(); - public IEnumerable MaxResultsRange => Enumerable.Range(2, 16); - - public string TestProxy() - { - var proxyServer = Settings.Proxy.Server; - var proxyUserName = Settings.Proxy.UserName; - if (string.IsNullOrEmpty(proxyServer)) - { - return InternationalizationManager.Instance.GetTranslation("serverCantBeEmpty"); - } - if (Settings.Proxy.Port <= 0) - { - return InternationalizationManager.Instance.GetTranslation("portCantBeEmpty"); - } - - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository); - - if (string.IsNullOrEmpty(proxyUserName) || string.IsNullOrEmpty(Settings.Proxy.Password)) - { - request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port); - } - else - { - request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port) - { - Credentials = new NetworkCredential(proxyUserName, Settings.Proxy.Password) - }; - } - try - { - var response = (HttpWebResponse)request.GetResponse(); - if (response.StatusCode == HttpStatusCode.OK) - { - return InternationalizationManager.Instance.GetTranslation("proxyIsCorrect"); - } - else - { - return InternationalizationManager.Instance.GetTranslation("proxyConnectFailed"); - } - } - catch - { - return InternationalizationManager.Instance.GetTranslation("proxyConnectFailed"); - } - } - - #endregion - - #region plugin - - public static string Plugin => @"https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest"; - public PluginViewModel SelectedPlugin { get; set; } - - public IList PluginViewModels - { - get - { - var metadatas = PluginManager.AllPlugins - .OrderBy(x => x.Metadata.Disabled) - .ThenBy(y => y.Metadata.Name) - .Select(p => new PluginViewModel { PluginPair = p }) - .ToList(); - return metadatas; - } - } - - public Control SettingProvider - { - get - { - var settingProvider = SelectedPlugin.PluginPair.Plugin as ISettingProvider; - if (settingProvider != null) - { - var control = settingProvider.CreateSettingPanel(); - control.HorizontalAlignment = HorizontalAlignment.Stretch; - control.VerticalAlignment = VerticalAlignment.Stretch; - return control; - } - else - { - return new Control(); - } - } - } - - - - #endregion - - #region theme - - public static string Theme => @"http://www.wox.one/theme/builder"; - - public string SelectedTheme - { - get { return Settings.Theme; } - set - { - Settings.Theme = value; - ThemeManager.Instance.ChangeTheme(value); - - if (ThemeManager.Instance.BlurEnabled && Settings.UseDropShadowEffect) - DropShadowEffect = false; - } - } - - public List Themes - => ThemeManager.Instance.LoadAvailableThemes().Select(Path.GetFileNameWithoutExtension).ToList(); - - public bool DropShadowEffect - { - get { return Settings.UseDropShadowEffect; } - set - { - if (ThemeManager.Instance.BlurEnabled && value) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed")); - return; - } - - if (value) - { - ThemeManager.Instance.AddDropShadowEffectToCurrentTheme(); - } - else - { - ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme(); - } - - Settings.UseDropShadowEffect = value; - } - } - - public Brush PreviewBackground - { - get - { - var wallpaper = WallpaperPathRetrieval.GetWallpaperPath(); - if (wallpaper != null && File.Exists(wallpaper)) - { - var memStream = new MemoryStream(File.ReadAllBytes(wallpaper)); - var bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.StreamSource = memStream; - bitmap.EndInit(); - var brush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill }; - return brush; - } - else - { - var wallpaperColor = WallpaperPathRetrieval.GetWallpaperColor(); - var brush = new SolidColorBrush(wallpaperColor); - return brush; - } - } - } - - public ResultsViewModel PreviewResults - { - get - { - var results = new List - { - new Result - { - Title = "Explorer", - SubTitle = "Search for files, folders and file contents", - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png") - }, - new Result - { - Title = "WebSearch", - SubTitle = "Search the web with different search engine support", - IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png") - }, - new Result - { - Title = "Program", - SubTitle = "Launch programs as admin or a different user", - IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png") - }, - new Result - { - Title = "ProcessKiller", - SubTitle = "Terminate unwanted processes", - IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png") - } - }; - var vm = new ResultsViewModel(Settings); - vm.AddResults(results, "PREVIEW"); - return vm; - } - } - - public FontFamily SelectedQueryBoxFont - { - get - { - if (Fonts.SystemFontFamilies.Count(o => - o.FamilyNames.Values != null && - o.FamilyNames.Values.Contains(Settings.QueryBoxFont)) > 0) - { - var font = new FontFamily(Settings.QueryBoxFont); - return font; - } - else - { - var font = new FontFamily("Segoe UI"); - return font; - } - } - set - { - Settings.QueryBoxFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); - } - } - - public FamilyTypeface SelectedQueryBoxFontFaces - { - get - { - var typeface = SyntaxSugars.CallOrRescueDefault( - () => SelectedQueryBoxFont.ConvertFromInvariantStringsOrNormal( - Settings.QueryBoxFontStyle, - Settings.QueryBoxFontWeight, - Settings.QueryBoxFontStretch - )); - return typeface; - } - set - { - Settings.QueryBoxFontStretch = value.Stretch.ToString(); - Settings.QueryBoxFontWeight = value.Weight.ToString(); - Settings.QueryBoxFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); - } - } - - public FontFamily SelectedResultFont - { - get - { - if (Fonts.SystemFontFamilies.Count(o => - o.FamilyNames.Values != null && - o.FamilyNames.Values.Contains(Settings.ResultFont)) > 0) - { - var font = new FontFamily(Settings.ResultFont); - return font; - } - else - { - var font = new FontFamily("Segoe UI"); - return font; - } - } - set - { - Settings.ResultFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); - } - } - - public FamilyTypeface SelectedResultFontFaces - { - get - { - var typeface = SyntaxSugars.CallOrRescueDefault( - () => SelectedResultFont.ConvertFromInvariantStringsOrNormal( - Settings.ResultFontStyle, - Settings.ResultFontWeight, - Settings.ResultFontStretch - )); - return typeface; - } - set - { - Settings.ResultFontStretch = value.Stretch.ToString(); - Settings.ResultFontWeight = value.Weight.ToString(); - Settings.ResultFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); - } - } - - public string ThemeImage => Constant.QueryTextBoxIconImagePath; - - #endregion - - #region hotkey - - public CustomPluginHotkey SelectedCustomPluginHotkey { get; set; } - - #endregion - - #region about - - public string Website => Constant.Website; - public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest"; - public string Documentation => Constant.Documentation; - public static string Version => Constant.Version; - public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); - #endregion + public double? SettingWindowLeft + { + get => _settings.SettingWindowLeft; + set => _settings.SettingWindowLeft = value; } } diff --git a/Flow.Launcher/ViewModel/WelcomeViewModel.cs b/Flow.Launcher/ViewModel/WelcomeViewModel.cs new file mode 100644 index 000000000..5eecabfde --- /dev/null +++ b/Flow.Launcher/ViewModel/WelcomeViewModel.cs @@ -0,0 +1,68 @@ +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.ViewModel +{ + public partial class WelcomeViewModel : BaseModel + { + public const int MaxPageNum = 5; + + public string PageDisplay => $"{PageNum}/5"; + + private int _pageNum = 1; + public int PageNum + { + get => _pageNum; + set + { + if (_pageNum != value) + { + _pageNum = value; + OnPropertyChanged(); + UpdateView(); + } + } + } + + private bool _backEnabled = false; + public bool BackEnabled + { + get => _backEnabled; + set + { + _backEnabled = value; + OnPropertyChanged(); + } + } + + private bool _nextEnabled = true; + public bool NextEnabled + { + get => _nextEnabled; + set + { + _nextEnabled = value; + OnPropertyChanged(); + } + } + + private void UpdateView() + { + OnPropertyChanged(nameof(PageDisplay)); + if (PageNum == 1) + { + BackEnabled = false; + NextEnabled = true; + } + else if (PageNum == MaxPageNum) + { + BackEnabled = true; + NextEnabled = false; + } + else + { + BackEnabled = true; + NextEnabled = true; + } + } + } +} diff --git a/Flow.Launcher/WelcomeWindow.xaml b/Flow.Launcher/WelcomeWindow.xaml new file mode 100644 index 000000000..e7c483f89 --- /dev/null +++ b/Flow.Launcher/WelcomeWindow.xaml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs new file mode 100644 index 000000000..bff2967d6 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs @@ -0,0 +1,56 @@ +using Flow.Launcher.Plugin.BrowserBookmark.Models; +using System.Windows; +using System.Windows.Input; +using System.Windows.Forms; + +namespace Flow.Launcher.Plugin.BrowserBookmark.Views; + +/// +/// Interaction logic for CustomBrowserSetting.xaml +/// +public partial class CustomBrowserSettingWindow : Window +{ + private CustomBrowser _currentCustomBrowser; + public CustomBrowserSettingWindow(CustomBrowser browser) + { + InitializeComponent(); + _currentCustomBrowser = browser; + DataContext = new CustomBrowser + { + Name = browser.Name, + DataDirectoryPath = browser.DataDirectoryPath, + BrowserType = browser.BrowserType, + }; + } + + private void ConfirmEditCustomBrowser(object sender, RoutedEventArgs e) + { + CustomBrowser editBrowser = (CustomBrowser)DataContext; + _currentCustomBrowser.Name = editBrowser.Name; + _currentCustomBrowser.DataDirectoryPath = editBrowser.DataDirectoryPath; + _currentCustomBrowser.BrowserType = editBrowser.BrowserType; + DialogResult = true; + Close(); + } + + private void CancelEditCustomBrowser(object sender, RoutedEventArgs e) + { + Close(); + } + + private void WindowKeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + ConfirmEditCustomBrowser(sender, e); + } + } + + private void OnSelectPathClick(object sender, RoutedEventArgs e) + { + var dialog = new FolderBrowserDialog(); + dialog.ShowDialog(); + CustomBrowser editBrowser = (CustomBrowser)DataContext; + editBrowser.DataDirectoryPath = dialog.SelectedPath; + } +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml index 8457f52f7..30424f4e8 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml @@ -1,39 +1,95 @@ - - + + - - + + - - - - - - - - + + + + + + +