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 index 11a0bcdf6..e1b8ed79e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -58,7 +58,7 @@ dotnet_style_prefer_conditional_expression_over_return = true:silent ############################### # Style Definitions dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# Use PascalCase for constant fields +# Use PascalCase for constant fields dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style @@ -134,7 +134,7 @@ csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion -csharp_style_namespace_declarations = block_scoped:silent +csharp_style_namespace_declarations = file_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_expression_bodied_lambdas = true:silent csharp_style_expression_bodied_local_functions = false:silent diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 00cc67ea0..a36a6af3e 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -3,3 +3,6 @@ https ssh ubuntu runcount +Firefox +Português +Português (Brasil) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 0df5228ba..6ba41e410 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -19,7 +19,6 @@ Prioritise Segoe Google Customise -UWP uwp Bokmal Bokm @@ -59,7 +58,6 @@ popup ptr pluginindicator TobiasSekan -Img img resx bak @@ -76,6 +74,7 @@ WCA_ACCENT_POLICY HGlobal dopusrt firefox +Firefox msedge svgc ime @@ -84,7 +83,30 @@ txb btn otf searchplugin -Noresult 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/patterns.txt b/.github/actions/spelling/patterns.txt index 2f4ff418d..f29f57ad5 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -115,3 +115,9 @@ #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/dependabot.yml b/.github/dependabot.yml index d9b39eb89..da4231f74 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,8 @@ updates: - package-ecosystem: "nuget" # See documentation for possible values directory: "/" # Location of package manifests schedule: - interval: "weekly" + interval: "daily" + open-pull-requests-limit: 3 ignore: - dependency-name: "squirrel-windows" reviewers: 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 index df962a675..7aaa9296a 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -73,7 +73,7 @@ jobs: steps: - name: check-spelling id: spelling - uses: check-spelling/check-spelling@main + uses: check-spelling/check-spelling@prerelease with: suppress_push_for_open_pull_request: 1 checkout: true @@ -89,12 +89,12 @@ jobs: check_extra_dictionaries: '' quit_without_error: true extra_dictionaries: - cspell:software-terms/src/software-terms.txt + cspell:software-terms/dict/softwareTerms.txt cspell:win32/src/win32.txt - cspell:php/php.txt cspell:filetypes/filetypes.txt cspell:csharp/csharp.txt - cspell:dotnet/dotnet.txt + cspell:dotnet/dict/dotnet.txt + cspell:python/src/common/extra.txt cspell:python/src/python/python-lib.txt cspell:aws/aws.txt cspell:companies/src/companies.txt @@ -113,7 +113,7 @@ jobs: # if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push' # steps: # - name: comment -# uses: check-spelling/check-spelling@main +# uses: check-spelling/check-spelling@@v0.0.22 # with: # checkout: true # spell_check_this: check-spelling/spell-check-this@main @@ -129,7 +129,7 @@ jobs: if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request') steps: - name: comment - uses: check-spelling/check-spelling@main + uses: check-spelling/check-spelling@prerelease with: checkout: true spell_check_this: check-spelling/spell-check-this@main @@ -153,7 +153,7 @@ jobs: # cancel-in-progress: false # steps: # - name: apply spelling updates - # uses: check-spelling/check-spelling@main + # uses: check-spelling/check-spelling@v0.0.22 # with: # experimental_apply_changes_via_bot: 1 # checkout: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index caac10c93..719f2556e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,6 +6,11 @@ on: schedule: - cron: '30 1 * * *' +env: + days-before-stale: 60 + days-before-close: 7 + exempt-issue-labels: 'keep-fresh' + jobs: stale: runs-on: ubuntu-latest @@ -13,14 +18,14 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: - stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 5 days.' - days-before-stale: 45 - days-before-close: 7 + stale-issue-message: 'This issue is stale because it has been open ${{ env.days-before-stale }} days with no activity. Remove stale label or comment or this will be closed in ${{ env.days-before-stale }} days.\n\nAlternatively this issue can be kept open by adding one of the following labels:\n${{ env.exempt-issue-labels }}' + days-before-stale: ${{ env.days-before-stale }} + days-before-close: ${{ env.days-before-close }} days-before-pr-close: -1 exempt-all-milestones: true close-issue-message: 'This issue was closed because it has been stale for 7 days with no activity. If you feel this issue still needs attention please feel free to reopen.' stale-pr-label: 'no-pr-activity' - exempt-issue-labels: 'keep-fresh' + exempt-issue-labels: ${{ env.exempt-issue-labels }} exempt-pr-labels: 'keep-fresh,awaiting-approval,work-in-progress' 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 index b1d289091..7040ee606 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -1,13 +1,11 @@ name: Publish to Winget on: - release: - types: [released] + workflow_dispatch: jobs: publish: - # Action can only be run on windows - runs-on: windows-latest + runs-on: ubuntu-latest steps: - uses: vedantmgoyal2009/winget-releaser@v2 with: 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 b58154dcb..069154364 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -9,11 +9,15 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; using System.Linq; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Configuration { public class Portable : IPortable { + private readonly IPublicAPI API = Ioc.Default.GetRequiredService(); + /// /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish /// @@ -40,7 +44,7 @@ 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); @@ -64,7 +68,7 @@ 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); @@ -95,13 +99,13 @@ namespace Flow.Launcher.Core.Configuration public void MoveUserDataFolder(string fromLocation, string toLocation) { - FilesFolders.CopyAll(fromLocation, toLocation); + FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s)); VerifyUserDataAfterMove(fromLocation, toLocation); } public void VerifyUserDataAfterMove(string fromLocation, string toLocation) { - FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation); + FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s)); } public void CreateShortcuts() @@ -157,13 +161,13 @@ namespace Flow.Launcher.Core.Configuration // delete it and prompt the user to pick the portable data location if (File.Exists(roamingDataDeleteFilePath)) { - FilesFolders.RemoveFolderIfExists(roamingDataDir); + FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s)); - if (MessageBox.Show("Flow Launcher has detected you enabled portable mode, " + + if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " + "would you like to move it to a different location?", string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) { - FilesFolders.OpenPath(Constant.RootDirectory); + FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s)); Environment.Exit(0); } @@ -172,9 +176,9 @@ namespace Flow.Launcher.Core.Configuration // delete it and notify the user about it. else if (File.Exists(portableDataDeleteFilePath)) { - FilesFolders.RemoveFolderIfExists(portableDataDir); + FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s)); - MessageBox.Show("Flow Launcher has detected you disabled portable mode, " + + API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " + "the relevant shortcuts and uninstaller entry have been created"); } } @@ -186,7 +190,7 @@ namespace Flow.Launcher.Core.Configuration if (roamingLocationExists && portableLocationExists) { - MessageBox.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " + + API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " + "{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.", DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs new file mode 100644 index 000000000..68be746f2 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs @@ -0,0 +1,68 @@ +using Flow.Launcher.Infrastructure.Http; +using Flow.Launcher.Infrastructure.Logger; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Core.ExternalPlugins +{ + public record CommunityPluginSource(string ManifestFileUrl) + { + private string latestEtag = ""; + + private List 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..affd7c312 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +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 index 9ebacc942..451df6147 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -4,14 +4,18 @@ using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using System; using System.Collections.Generic; -using System.IO; using System.Linq; +using System.Windows; using System.Windows.Forms; +using Flow.Launcher.Core.Resource; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Core.ExternalPlugins.Environments { public abstract class AbstractPluginEnvironment { + protected readonly IPublicAPI API = Ioc.Default.GetRequiredService(); + internal abstract string Language { get; } internal abstract string EnvName { get; } @@ -24,7 +28,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal virtual string FileDialogFilter => string.Empty; - internal abstract string PluginsSettingsFilePath { get; set; } + internal abstract string PluginsSettingsFilePath { get; set; } internal List PluginMetadataList; @@ -41,18 +45,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase))) return new List(); - // TODO: Remove. This is backwards compatibility for 1.10.0 release- changed PythonEmbeded to Environments/Python - if (Language.Equals(AllowedLanguage.Python, StringComparison.OrdinalIgnoreCase)) - { - FilesFolders.RemoveFolderIfExists(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable")); - - if (!string.IsNullOrEmpty(PluginSettings.PythonDirectory) && PluginSettings.PythonDirectory.StartsWith(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable"))) - { - InstallEnvironment(); - PluginSettings.PythonDirectory = string.Empty; - } - } - if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath)) { // Ensure latest only if user is using Flow's environment setup. @@ -62,15 +54,16 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments return SetPathForPluginPairs(PluginsSettingsFilePath, Language); } - if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " + - $"will require {EnvName} to run. Would you like to download {EnvName}? " + - Environment.NewLine + Environment.NewLine + - "Click no if it's already installed, " + - $"and you will be prompted to select the folder that contains the {EnvName} executable", - string.Empty, MessageBoxButtons.YesNo) == DialogResult.No) + var noRuntimeMessage = string.Format( + InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"), + Language, + EnvName, + Environment.NewLine + ); + if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) { - var msg = $"Please select the {EnvName} executable"; - var selectedFile = string.Empty; + var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName); + string selectedFile; selectedFile = GetFileFromDialog(msg, FileDialogFilter); @@ -92,8 +85,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments } else { - MessageBox.Show( - $"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom)."); + API.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language)); Log.Error("PluginsLoader", $"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.", $"{Language}Environment"); @@ -109,7 +101,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments if (expectedPath == currentPath) return; - FilesFolders.RemoveFolderIfExists(installedDirPath); + FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s)); InstallEnvironment(); @@ -143,14 +135,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments }; var result = dlg.ShowDialog(); - if (result == DialogResult.OK) - { - return dlg.FileName; - } - else - { - return string.Empty; - } + return result == DialogResult.OK ? dlg.FileName : string.Empty; + } /// 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 index 55b3b60bd..607c19062 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -16,7 +16,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName); - internal override string InstallPath => Path.Combine(EnvPath, "PythonEmbeddable-v3.8.9"); + internal override string InstallPath => Path.Combine(EnvPath, "PythonEmbeddable-v3.11.4"); internal override string ExecutablePath => Path.Combine(InstallPath, "pythonw.exe"); @@ -28,10 +28,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal override void InstallEnvironment() { - FilesFolders.RemoveFolderIfExists(InstallPath); + FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); - // Python 3.8.9 is used for Windows 7 compatibility - DroplexPackage.Drop(App.python_3_8_9_embeddable, InstallPath).Wait(); + // 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; } 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 index 70341f711..399f7cc03 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal override void InstallEnvironment() { - FilesFolders.RemoveFolderIfExists(InstallPath); + FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait(); diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs new file mode 100644 index 000000000..e8cb72e11 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Droplex; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin; +using System.IO; +using Flow.Launcher.Core.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + internal class TypeScriptV2Environment : AbstractPluginEnvironment + { + internal override string Language => AllowedLanguage.TypeScriptV2; + + internal override string EnvName => DataLocation.NodeEnvironmentName; + + internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName); + + internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); + internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); + + internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } + + internal TypeScriptV2Environment(List 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/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index e3f0e2a2f..ac8abcdcc 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -1,10 +1,6 @@ -using Flow.Launcher.Infrastructure.Http; -using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Logger; using System; using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -12,38 +8,37 @@ namespace Flow.Launcher.Core.ExternalPlugins { public static class PluginsManifest { - private const string manifestFileUrl = "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json"; + private static readonly CommunityPluginStore mainPluginStore = + new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json", + "https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json", + "https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json", + "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json"); private static readonly SemaphoreSlim manifestUpdateLock = new(1); - private static string latestEtag = ""; + private static DateTime lastFetchedAt = DateTime.MinValue; + private static TimeSpan fetchTimeout = TimeSpan.FromMinutes(2); - public static List UserPlugins { get; private set; } = new List(); + public static List UserPlugins { get; private set; } - public static async Task UpdateManifestAsync(CancellationToken token = default) + public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false) { try { await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false); - var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl); - request.Headers.Add("If-None-Match", latestEtag); - - using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false); - - if (response.StatusCode == HttpStatusCode.OK) + if (UserPlugins == null || usePrimaryUrlOnly || DateTime.Now.Subtract(lastFetchedAt) >= fetchTimeout) { - Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo"); + var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false); - await using var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false); + // If the results are empty, we shouldn't update the manifest because the results are invalid. + if (results.Count != 0) + { + UserPlugins = results; + lastFetchedAt = DateTime.Now; - UserPlugins = await JsonSerializer.DeserializeAsync>(json, cancellationToken: token).ConfigureAwait(false); - - latestEtag = response.Headers.ETag.Tag; - } - else if (response.StatusCode != HttpStatusCode.NotModified) - { - Log.Warn($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http response for manifest file was {response.StatusCode}"); + return true; + } } } catch (Exception e) @@ -54,6 +49,8 @@ namespace Flow.Launcher.Core.ExternalPlugins { manifestUpdateLock.Release(); } + + return false; } } } diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs index bb1279b2c..79d6d7605 100644 --- a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs +++ b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Flow.Launcher.Core.ExternalPlugins { @@ -13,9 +13,11 @@ namespace Flow.Launcher.Core.ExternalPlugins public string Website { get; set; } public string UrlDownload { get; set; } public string UrlSourceCode { get; set; } + public string LocalInstallPath { get; set; } public string IcoPath { get; set; } - public DateTime LatestReleaseDate { get; set; } - public DateTime DateAdded { get; set; } + public DateTime? LatestReleaseDate { get; set; } + public DateTime? DateAdded { get; set; } + public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath); } } diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 4077320bc..e9f199d00 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -1,4 +1,4 @@ - + net7.0-windows @@ -53,10 +53,12 @@ - - - + + + + + diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index 6b55bb3e3..857122aa6 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -1,8 +1,8 @@ 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 { @@ -28,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[0] = request.ToString(); + _startInfo.ArgumentList[0] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[0] = rpcRequest.ToString(); + _startInfo.ArgumentList[0] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); return Execute(_startInfo); } } 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 e937779a1..5b2a9f6cb 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -12,76 +12,42 @@ * */ -using Flow.Launcher.Core.Resource; using System.Collections.Generic; -using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Plugin; using System.Text.Json; namespace Flow.Launcher.Core.Plugin { - public class JsonRPCErrorModel - { - public int Code { get; set; } + public record JsonRPCBase(int Id, JsonRPCErrorModel Error = default); + public record JsonRPCErrorModel(int Code, string Message, string Data); - public string Message { get; set; } + public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error); + public record JsonRPCQueryResponseModel(int Id, + [property: JsonPropertyName("result")] List 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 Dictionary SettingsChange { get; set; } - - public string DebugMessage { get; set; } - } - - public class JsonRPCRequestModel - { - public string Method { get; set; } - - public object[] Parameters { get; set; } - - public Dictionary Settings { 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 diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index f3b2eed87..97c3c8981 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -23,9 +23,6 @@ using Orientation = System.Windows.Controls.Orientation; using TextBox = System.Windows.Controls.TextBox; using UserControl = System.Windows.Controls.UserControl; using System.Windows.Documents; -using static System.Windows.Forms.LinkLabel; -using Droplex; -using System.Windows.Forms; namespace Flow.Launcher.Core.Plugin { @@ -33,9 +30,8 @@ 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, ISettingProvider, ISavable + internal abstract class JsonRPCPlugin : JsonRPCPluginBase { - protected PluginInitContext context; public const string JsonRPC = "JsonRPC"; protected abstract Task RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default); @@ -43,19 +39,16 @@ namespace Flow.Launcher.Core.Plugin private static readonly RecyclableMemoryStreamManager BufferManager = new(); - private string SettingConfigurationPath => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name, "Settings.json"); + private int RequestId { get; set; } - public List LoadContextMenus(Result selectedResult) + private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json"); + + public override List LoadContextMenus(Result selectedResult) { - var request = new JsonRPCRequestModel - { - Method = "context_menu", - Parameters = new[] - { - selectedResult.ContextData - } - }; + var request = new JsonRPCRequestModel(RequestId++, + "context_menu", + new[] { selectedResult.ContextData }); var output = Request(request); return DeserializedResult(output); } @@ -80,7 +73,6 @@ namespace Flow.Launcher.Core.Plugin { WriteIndented = true }; - private Dictionary Settings { get; set; } private readonly Dictionary _settingControls = new(); @@ -106,85 +98,42 @@ namespace Flow.Launcher.Core.Plugin return ParseResults(queryResponseModel); } - - private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) + protected override async Task ExecuteResultAsync(JsonRPCResult result) { - if (queryResponseModel.Result == null) return null; + if (result.JsonRPCAction == null) return false; - if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) + if (string.IsNullOrEmpty(result.JsonRPCAction.Method)) { - context.API.ShowMsg(queryResponseModel.DebugMessage); + return !result.JsonRPCAction.DontHideAfterAction; } - foreach (var result in queryResponseModel.Result) + if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) { - result.AsyncAction = async c => + ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], + result.JsonRPCAction.Parameters); + } + else + { + await using var actionResponse = await RequestAsync(result.JsonRPCAction); + + if (actionResponse.Length == 0) { - UpdateSettings(result.SettingsChange); - - if (result.JsonRPCAction == null) return false; - - if (string.IsNullOrEmpty(result.JsonRPCAction.Method)) - { - return !result.JsonRPCAction.DontHideAfterAction; - } - - if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) - { - ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], - result.JsonRPCAction.Parameters); - } - else - { - await using var actionResponse = await RequestAsync(result.JsonRPCAction); - - if (actionResponse.Length == 0) - { - return !result.JsonRPCAction.DontHideAfterAction; - } - - var jsonRpcRequestModel = await - JsonSerializer.DeserializeAsync(actionResponse, options); - - if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) - { - ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], - jsonRpcRequestModel.Parameters); - } - } - return !result.JsonRPCAction.DontHideAfterAction; - }; + } + + var jsonRpcRequestModel = await + JsonSerializer.DeserializeAsync(actionResponse, options); + + if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) + { + ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], + jsonRpcRequestModel.Parameters); + } } - var results = new List(); - - results.AddRange(queryResponseModel.Result); - - UpdateSettings(queryResponseModel.SettingsChange); - - return results; + return !result.JsonRPCAction.DontHideAfterAction; } - private void ExecuteFlowLauncherAPI(string method, object[] parameters) - { - var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray(); - var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray); - if (methodInfo == null) - { - return; - } - try - { - methodInfo.Invoke(PluginManager.API, parameters); - } - catch (Exception) - { -#if (DEBUG) - throw; -#endif - } - } /// /// Execute external program and return the output @@ -299,379 +248,19 @@ namespace Flow.Launcher.Core.Plugin return sourceBuffer; } - - public async Task> QueryAsync(Query query, CancellationToken token) + public override async Task> QueryAsync(Query query, CancellationToken token) { - var request = new JsonRPCRequestModel - { - Method = "query", - Parameters = new object[] + var request = new JsonRPCRequestModel(RequestId++, + "query", + new object[] { query.Search }, - Settings = Settings - }; + Settings?.Inner); + var output = await RequestAsync(request, token); + return await DeserializedResultAsync(output); } - - public async Task InitSettingAsync() - { - if (!File.Exists(SettingConfigurationPath)) - return; - - if (File.Exists(SettingPath)) - { - await using var fileStream = File.OpenRead(SettingPath); - Settings = await JsonSerializer.DeserializeAsync>(fileStream, options); - } - - var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build(); - _settingsTemplate = deserializer.Deserialize(await File.ReadAllTextAsync(SettingConfigurationPath)); - - Settings ??= new Dictionary(); - - foreach (var (type, attribute) in _settingsTemplate.Body) - { - if (type == "textBlock") - continue; - if (!Settings.ContainsKey(attribute.Name)) - { - Settings[attribute.Name] = attribute.DefaultValue; - } - } - } - - public virtual async Task InitAsync(PluginInitContext context) - { - this.context = context; - await InitSettingAsync(); - } - private static readonly Thickness settingControlMargin = new(0, 9, 18, 9); - private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9); - private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingDescMargin = new(0, 2, 0, 0); - private static readonly Thickness settingSepMargin = new(0, 0, 0, 2); - private JsonRpcConfigurationModel _settingsTemplate; - - public Control CreateSettingPanel() - { - if (Settings == null) - return new(); - var settingWindow = new UserControl(); - var mainPanel = new Grid - { - Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center - }; - ColumnDefinition gridCol1 = new ColumnDefinition(); - ColumnDefinition gridCol2 = new ColumnDefinition(); - - gridCol1.Width = new GridLength(70, GridUnitType.Star); - gridCol2.Width = new GridLength(30, GridUnitType.Star); - mainPanel.ColumnDefinitions.Add(gridCol1); - mainPanel.ColumnDefinitions.Add(gridCol2); - settingWindow.Content = mainPanel; - int rowCount = 0; - foreach (var (type, attribute) in _settingsTemplate.Body) - { - Separator sep = new Separator(); - sep.VerticalAlignment = VerticalAlignment.Top; - sep.Margin = settingSepMargin; - sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ - var panel = new StackPanel - { - Orientation = Orientation.Vertical, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelPanelMargin - }; - RowDefinition gridRow = new RowDefinition(); - mainPanel.RowDefinitions.Add(gridRow); - var name = new TextBlock() - { - Text = attribute.Label, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - var desc = new TextBlock() - { - Text = attribute.Description, - FontSize = 12, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingDescMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); - - if (attribute.Description == null) /* if no description, hide */ - desc.Visibility = Visibility.Collapsed; - - - if (type != "textBlock") /* if textBlock, hide desc */ - { - panel.Children.Add(name); - panel.Children.Add(desc); - } - - - Grid.SetColumn(panel, 0); - Grid.SetRow(panel, rowCount); - - FrameworkElement contentControl; - - switch (type) - { - case "textBlock": - { - contentControl = new TextBlock - { - Text = attribute.Description.Replace("\\r\\n", "\r\n"), - Margin = settingTextBlockMargin, - Padding = new Thickness(0, 0, 0, 0), - HorizontalAlignment = System.Windows.HorizontalAlignment.Left, - TextAlignment = TextAlignment.Left, - TextWrapping = TextWrapping.Wrap - }; - Grid.SetColumn(contentControl, 0); - Grid.SetColumnSpan(contentControl, 2); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "input": - { - var textBox = new TextBox() - { - Text = Settings[attribute.Name] as string ?? string.Empty, - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "inputWithFileBtn": - { - var textBox = new TextBox() - { - Margin = new Thickness(10, 0, 0, 0), - Text = Settings[attribute.Name] as string ?? string.Empty, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - var Btn = new System.Windows.Controls.Button() - { - Margin = new Thickness(10, 0, 0, 0), Content = "Browse" - }; - var dockPanel = new DockPanel() - { - Margin = settingControlMargin - }; - DockPanel.SetDock(Btn, Dock.Right); - dockPanel.Children.Add(Btn); - dockPanel.Children.Add(textBox); - contentControl = dockPanel; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "textarea": - { - var textBox = new TextBox() - { - Height = 120, - Margin = settingControlMargin, - VerticalAlignment = VerticalAlignment.Center, - TextWrapping = TextWrapping.WrapWithOverflow, - AcceptsReturn = true, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - Text = Settings[attribute.Name] as string ?? string.Empty, - ToolTip = attribute.Description - }; - textBox.TextChanged += (sender, _) => - { - Settings[attribute.Name] = ((TextBox)sender).Text; - }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "passwordBox": - { - var passwordBox = new PasswordBox() - { - Margin = settingControlMargin, - Password = Settings[attribute.Name] as string ?? string.Empty, - PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - passwordBox.PasswordChanged += (sender, _) => - { - Settings[attribute.Name] = ((PasswordBox)sender).Password; - }; - contentControl = passwordBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "dropdown": - { - var comboBox = new System.Windows.Controls.ComboBox() - { - ItemsSource = attribute.Options, - SelectedItem = Settings[attribute.Name], - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; - comboBox.SelectionChanged += (sender, _) => - { - Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; - }; - contentControl = comboBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "checkbox": - var checkBox = new CheckBox - { - IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue), - Margin = settingCheckboxMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; - checkBox.Click += (sender, _) => - { - Settings[attribute.Name] = ((CheckBox)sender).IsChecked; - }; - contentControl = checkBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - case "hyperlink": - var hyperlink = new Hyperlink - { - ToolTip = attribute.Description, NavigateUri = attribute.url - }; - var linkbtn = new System.Windows.Controls.Button - { - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin - }; - linkbtn.Content = attribute.urlLabel; - - contentControl = linkbtn; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - default: - continue; - } - if (type != "textBlock") - _settingControls[attribute.Name] = contentControl; - mainPanel.Children.Add(panel); - mainPanel.Children.Add(contentControl); - rowCount++; - - } - return settingWindow; - } - - public void Save() - { - if (Settings != null) - { - Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name)); - File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption)); - } - } - - public void UpdateSettings(Dictionary settings) - { - if (settings == null || settings.Count == 0) - return; - - foreach (var (key, value) in settings) - { - if (Settings.ContainsKey(key)) - { - Settings[key] = value; - } - if (_settingControls.ContainsKey(key)) - { - - switch (_settingControls[key]) - { - case TextBox textBox: - textBox.Dispatcher.Invoke(() => textBox.Text = value as string); - break; - case PasswordBox passwordBox: - passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string); - break; - case System.Windows.Controls.ComboBox comboBox: - comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); - break; - case CheckBox checkBox: - checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string)); - break; - } - } - } - } } - } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs new file mode 100644 index 000000000..7248c6259 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -0,0 +1,178 @@ +using Flow.Launcher.Core.Resource; +using Flow.Launcher.Infrastructure; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Microsoft.IO; +using System.Windows; +using System.Windows.Controls; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using CheckBox = System.Windows.Controls.CheckBox; +using Control = System.Windows.Controls.Control; +using Orientation = System.Windows.Controls.Orientation; +using TextBox = System.Windows.Controls.TextBox; +using UserControl = System.Windows.Controls.UserControl; +using System.Windows.Documents; +using static System.Windows.Forms.LinkLabel; +using Droplex; +using System.Windows.Forms; +using Microsoft.VisualStudio.Threading; + +namespace Flow.Launcher.Core.Plugin +{ + /// + /// Represent the plugin that using JsonPRC + /// every JsonRPC plugin should has its own plugin instance + /// + public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + { + protected PluginInitContext Context; + public const string JsonRPC = "JsonRPC"; + + private int RequestId { get; set; } + + private string SettingConfigurationPath => + Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + + private string SettingDirectory => Path.Combine(DataLocation.PluginSettingsDirectory, + Context.CurrentPluginMetadata.Name); + + private string SettingPath => Path.Combine(SettingDirectory, "Settings.json"); + + public abstract List 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) + { + this.Context = context; + await InitSettingAsync(); + } + + public void Save() + { + Settings?.Save(); + } + + public bool NeedCreateSettingPanel() + { + return Settings.NeedCreateSettingPanel(); + } + + public Control CreateSettingPanel() + { + return Settings.CreateSettingPanel(); + } + + public void DeletePluginSettingsDirectory() + { + if (Directory.Exists(SettingDirectory)) + { + Directory.Delete(SettingDirectory, true); + } + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs new file mode 100644 index 000000000..8412ba7e8 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -0,0 +1,446 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Forms; +using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Plugin; +using CheckBox = System.Windows.Controls.CheckBox; +using ComboBox = System.Windows.Controls.ComboBox; +using Control = System.Windows.Controls.Control; +using Orientation = System.Windows.Controls.Orientation; +using TextBox = System.Windows.Controls.TextBox; +using UserControl = System.Windows.Controls.UserControl; + +namespace Flow.Launcher.Core.Plugin +{ + public class JsonRPCPluginSettings + { + public required JsonRpcConfigurationModel? Configuration { get; init; } + + public required string SettingPath { get; init; } + public Dictionary SettingControls { get; } = new(); + + public IReadOnlyDictionary Inner => Settings; + protected ConcurrentDictionary Settings { get; set; } + public required IPublicAPI API { get; init; } + + private JsonStorage> _storage; + + // maybe move to resource? + private static readonly Thickness settingControlMargin = new(0, 9, 18, 9); + private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9); + private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0); + private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9); + private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9); + private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0); + private static readonly Thickness settingDescMargin = new(0, 2, 0, 0); + private static readonly Thickness settingSepMargin = new(0, 0, 0, 2); + + public async Task InitializeAsync() + { + _storage = new JsonStorage>(SettingPath); + Settings = await _storage.LoadAsync(); + + if (Configuration == null) + { + return; + } + + foreach (var (type, attributes) in Configuration.Body) + { + if (attributes.Name == null) + { + continue; + } + + if (!Settings.ContainsKey(attributes.Name)) + { + Settings[attributes.Name] = attributes.DefaultValue; + } + } + } + + + public void UpdateSettings(IReadOnlyDictionary settings) + { + if (settings == null || settings.Count == 0) + return; + + foreach (var (key, value) in settings) + { + Settings[key] = value; + + if (SettingControls.TryGetValue(key, out var control)) + { + switch (control) + { + case TextBox textBox: + textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty); + break; + case PasswordBox passwordBox: + passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty); + break; + case ComboBox comboBox: + comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); + break; + case CheckBox checkBox: + checkBox.Dispatcher.Invoke(() => + checkBox.IsChecked = value is bool isChecked + ? isChecked + : bool.Parse(value as string ?? string.Empty)); + break; + } + } + } + + Save(); + } + + public async Task SaveAsync() + { + await _storage.SaveAsync(); + } + + public void Save() + { + _storage.Save(); + } + + public bool NeedCreateSettingPanel() + { + // If there are no settings or the settings configuration is empty, return null + return Settings != null && Configuration != null && Configuration.Body.Count != 0; + } + + public Control CreateSettingPanel() + { + // No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true + + var settingWindow = new UserControl(); + var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; + + ColumnDefinition gridCol1 = new ColumnDefinition(); + ColumnDefinition gridCol2 = new ColumnDefinition(); + + gridCol1.Width = new GridLength(70, GridUnitType.Star); + gridCol2.Width = new GridLength(30, GridUnitType.Star); + mainPanel.ColumnDefinitions.Add(gridCol1); + mainPanel.ColumnDefinitions.Add(gridCol2); + settingWindow.Content = mainPanel; + int rowCount = 0; + + foreach (var (type, attribute) in Configuration.Body) + { + Separator sep = new Separator(); + sep.VerticalAlignment = VerticalAlignment.Top; + sep.Margin = settingSepMargin; + sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ + var panel = new StackPanel + { + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingLabelPanelMargin + }; + + RowDefinition gridRow = new RowDefinition(); + mainPanel.RowDefinitions.Add(gridRow); + var name = new TextBlock() + { + Text = attribute.Label, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingLabelMargin, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + var desc = new TextBlock() + { + Text = attribute.Description, + FontSize = 12, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingDescMargin, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); + + if (attribute.Description == null) /* if no description, hide */ + desc.Visibility = Visibility.Collapsed; + + + if (type != "textBlock") /* if textBlock, hide desc */ + { + panel.Children.Add(name); + panel.Children.Add(desc); + } + + + Grid.SetColumn(panel, 0); + Grid.SetRow(panel, rowCount); + + FrameworkElement contentControl; + + switch (type) + { + case "textBlock": + { + contentControl = new TextBlock + { + Text = attribute.Description.Replace("\\r\\n", "\r\n"), + Margin = settingTextBlockMargin, + Padding = new Thickness(0, 0, 0, 0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; + + Grid.SetColumn(contentControl, 0); + Grid.SetColumnSpan(contentControl, 2); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "input": + { + var textBox = new TextBox() + { + Text = Settings[attribute.Name] as string ?? string.Empty, + Margin = settingControlMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + + contentControl = textBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "inputWithFileBtn": + case "inputWithFolderBtn": + { + var textBox = new TextBox() + { + Margin = new Thickness(10, 0, 0, 0), + Text = Settings[attribute.Name] as string ?? string.Empty, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + + var Btn = new System.Windows.Controls.Button() + { + Margin = new Thickness(10, 0, 0, 0), Content = "Browse" + }; + + Btn.Click += (_, _) => + { + using CommonDialog dialog = type switch + { + "inputWithFolderBtn" => new FolderBrowserDialog(), + _ => new OpenFileDialog(), + }; + if (dialog.ShowDialog() != DialogResult.OK) return; + + var path = dialog switch + { + FolderBrowserDialog folderDialog => folderDialog.SelectedPath, + OpenFileDialog fileDialog => fileDialog.FileName, + }; + textBox.Text = path; + Settings[attribute.Name] = path; + }; + + var dockPanel = new DockPanel() { Margin = settingControlMargin }; + + DockPanel.SetDock(Btn, Dock.Right); + dockPanel.Children.Add(Btn); + dockPanel.Children.Add(textBox); + contentControl = dockPanel; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "textarea": + { + var textBox = new TextBox() + { + Height = 120, + Margin = settingControlMargin, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow, + AcceptsReturn = true, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + Text = Settings[attribute.Name] as string ?? string.Empty, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (sender, _) => + { + Settings[attribute.Name] = ((TextBox)sender).Text; + }; + + contentControl = textBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "passwordBox": + { + var passwordBox = new PasswordBox() + { + Margin = settingControlMargin, + Password = Settings[attribute.Name] as string ?? string.Empty, + PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + passwordBox.PasswordChanged += (sender, _) => + { + Settings[attribute.Name] = ((PasswordBox)sender).Password; + }; + + contentControl = passwordBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "dropdown": + { + var comboBox = new System.Windows.Controls.ComboBox() + { + ItemsSource = attribute.Options, + SelectedItem = Settings[attribute.Name], + Margin = settingControlMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + ToolTip = attribute.Description + }; + + comboBox.SelectionChanged += (sender, _) => + { + Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; + }; + + contentControl = comboBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "checkbox": + var checkBox = new CheckBox + { + IsChecked = + Settings[attribute.Name] is bool isChecked + ? isChecked + : bool.Parse(attribute.DefaultValue), + Margin = settingCheckboxMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + ToolTip = attribute.Description + }; + + checkBox.Click += (sender, _) => + { + Settings[attribute.Name] = ((CheckBox)sender).IsChecked; + }; + + contentControl = checkBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + case "hyperlink": + var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url }; + + var linkbtn = new System.Windows.Controls.Button + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + Margin = settingControlMargin + }; + + linkbtn.Content = attribute.urlLabel; + + contentControl = linkbtn; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + default: + continue; + } + + if (type != "textBlock") + SettingControls[attribute.Name] = contentControl; + + mainPanel.Children.Add(panel); + mainPanel.Children.Add(contentControl); + rowCount++; + } + + return settingWindow; + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs new file mode 100644 index 000000000..abe563c14 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Core.Plugin.JsonRPCV2Models; +using Flow.Launcher.Plugin; +using Microsoft.VisualStudio.Threading; +using StreamJsonRpc; +using IAsyncDisposable = System.IAsyncDisposable; + + +namespace Flow.Launcher.Core.Plugin +{ + internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated + { + public const string JsonRpc = "JsonRPC"; + + protected abstract IDuplexPipe ClientPipe { get; set; } + + protected StreamReader ErrorStream { get; set; } + + private JsonRpc RPC { get; set; } + + + protected override async Task 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 e) + { + } + } + + public virtual async ValueTask DisposeAsync() + { + try + { + await RPC.InvokeAsync("close"); + } + catch (RemoteMethodNotFoundException 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..8df2ce9ed --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; + +namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models +{ + public class JsonRPCPublicAPI + { + private IPublicAPI _api; + + public JsonRPCPublicAPI(IPublicAPI api) + { + _api = api; + } + + public void ChangeQuery(string query, bool requery = false) + { + _api.ChangeQuery(query, requery); + } + + public void RestartApp() + { + _api.RestartApp(); + } + + public void ShellRun(string cmd, string filename = "cmd.exe") + { + _api.ShellRun(cmd, filename); + } + + public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true) + { + _api.CopyToClipboard(text, directCopy, showDefaultNotification); + } + + public void SaveAppAllSettings() + { + _api.SaveAppAllSettings(); + } + + public void SavePluginSettings() + { + _api.SavePluginSettings(); + } + + public Task ReloadAllPluginDataAsync() + { + return _api.ReloadAllPluginData(); + } + + public void CheckForNewUpdate() + { + _api.CheckForNewUpdate(); + } + + public void ShowMsgError(string title, string subTitle = "") + { + _api.ShowMsgError(title, subTitle); + } + + public void ShowMainWindow() + { + _api.ShowMainWindow(); + } + + public void HideMainWindow() + { + _api.HideMainWindow(); + } + + public bool IsMainWindowVisible() + { + return _api.IsMainWindowVisible(); + } + + public void ShowMsg(string title, string subTitle = "", string iconPath = "") + { + _api.ShowMsg(title, subTitle, iconPath); + } + + public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) + { + _api.ShowMsg(title, subTitle, iconPath, useMainWindowAsOwner); + } + + public void OpenSettingDialog() + { + _api.OpenSettingDialog(); + } + + public string GetTranslation(string key) + { + return _api.GetTranslation(key); + } + + public List 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 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(); + } + } +} 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 index 1247143fa..aeac18ca9 100644 --- a/Flow.Launcher.Core/Plugin/NodePlugin.cs +++ b/Flow.Launcher.Core/Plugin/NodePlugin.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Plugin; @@ -31,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - _startInfo.ArgumentList[1] = request.ToString(); + _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] = rpcRequest.ToString(); + _startInfo.ArgumentList[1] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); return Execute(_startInfo); } @@ -46,8 +43,8 @@ namespace Flow.Launcher.Core.Plugin { _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); _startInfo.ArgumentList.Add(string.Empty); - await base.InitAsync(context); _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 9d76b6be0..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; @@ -37,6 +34,17 @@ namespace Flow.Launcher.Core.Plugin return existAssembly ?? (assemblyPath == null ? null : LoadFromAssemblyPath(assemblyPath)); } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var path = dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (!string.IsNullOrEmpty(path)) + { + return LoadUnmanagedDllFromPath(path); + } + + return IntPtr.Zero; + } internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type) { @@ -44,4 +52,4 @@ namespace Flow.Launcher.Core.Plugin return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type)); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 3b4a6e445..09711051e 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -11,6 +11,10 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using ISavable = Flow.Launcher.Plugin.ISavable; +using Flow.Launcher.Plugin.SharedCommands; +using System.Text.Json; +using Flow.Launcher.Core.Resource; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Core.Plugin { @@ -25,11 +29,13 @@ namespace Flow.Launcher.Core.Plugin public static readonly HashSet 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 List _modifiedPlugins = new List(); /// /// Directories that will hold Flow Launcher plugin directory @@ -48,6 +54,9 @@ namespace Flow.Launcher.Core.Plugin } } + /// + /// Save json and ISavable + /// public static void Save() { foreach (var plugin in AllPlugins) @@ -85,6 +94,48 @@ namespace Flow.Launcher.Core.Plugin }).ToArray()); } + public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static async Task CloseExternalPreviewAsync() + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.ClosePreviewAsync(), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static bool UseExternalPreview() + { + return GetPluginsForInterface().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 @@ -110,9 +161,8 @@ namespace Flow.Launcher.Core.Plugin /// Call initialize for all plugins /// /// return the list of failed to init plugins or null for none - public static async Task InitializePluginsAsync(IPublicAPI api) + public static async Task InitializePluginsAsync() { - API = api; var failedPlugins = new ConcurrentQueue(); var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate @@ -155,12 +205,21 @@ namespace Flow.Launcher.Core.Plugin } } + InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface()); + InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService().Language); + if (failedPlugins.Any()) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); - API.ShowMsg($"Fail to Init Plugins", - $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", - "", false); + API.ShowMsg( + API.GetTranslation("failedToInitializePluginsTitle"), + string.Format( + API.GetTranslation("failedToInitializePluginsMessage"), + failed + ), + "", + false + ); } } @@ -168,11 +227,11 @@ namespace Flow.Launcher.Core.Plugin { if (query is null) return Array.Empty(); - + if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword)) return GlobalPlugins; - - + + var plugin = NonGlobalPlugins[query.ActionKeyword]; return new List { @@ -207,12 +266,24 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - throw new FlowPluginException(metadata, e); + Result r = new() + { + Title = $"{metadata.Name}: Failed to respond!", + SubTitle = "Select this result for more info", + IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon, + PluginDirectory = metadata.PluginDirectory, + ActionKeywordAssigned = query.ActionKeyword, + PluginID = metadata.ID, + OriginQuery = query, + Action = _ => { throw new FlowPluginException(metadata, e);}, + Score = -100 + }; + results.Add(r); } return results; } - public static void UpdatePluginMetadata(List results, PluginMetadata metadata, Query query) + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) { @@ -220,8 +291,8 @@ namespace Flow.Launcher.Core.Plugin r.PluginID = metadata.ID; r.OriginQuery = query; - // ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions - // Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level + // ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions + // Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level if (metadata.ActionKeywords.Count == 1) r.ActionKeywordAssigned = query.ActionKeyword; } @@ -239,7 +310,8 @@ namespace Flow.Launcher.Core.Plugin public static IEnumerable 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) @@ -328,5 +400,215 @@ namespace Flow.Launcher.Core.Plugin RemoveActionKeyword(id, oldActionKeyword); } } + + private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath) + { + var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length; + var unzippedFilesCount = Directory.GetFiles(unzippedParentFolderPath).Length; + + // adjust path depending on how the plugin is zipped up + // the recommended should be to zip up the folder not the contents + if (unzippedFolderCount == 1 && unzippedFilesCount == 0) + // folder is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/pluginFolderName/ + return Directory.GetDirectories(unzippedParentFolderPath)[0]; + + if (unzippedFilesCount > 1) + // content is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/ + return unzippedParentFolderPath; + + return string.Empty; + } + + private static bool SameOrLesserPluginVersionExists(string metadataPath) + { + var newMetadata = JsonSerializer.Deserialize(File.ReadAllText(metadataPath)); + return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID + && newMetadata.Version.CompareTo(x.Metadata.Version) <= 0); + } + + #region Public functions + + public static bool PluginModified(string uuid) + { + return _modifiedPlugins.Contains(uuid); + } + + + /// + /// 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 + /// + public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) + { + InstallPlugin(newVersion, zipFilePath, checkModified:false); + UninstallPlugin(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false); + _modifiedPlugins.Add(existingVersion.ID); + } + + /// + /// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation + /// + public static void InstallPlugin(UserPlugin plugin, string zipFilePath) + { + InstallPlugin(plugin, zipFilePath, checkModified: true); + } + + /// + /// Uninstall a plugin. + /// + public static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false) + { + UninstallPlugin(plugin, removePluginFromSettings, removePluginSettings, true); + } + + #endregion + + #region Internal functions + + internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified) + { + if (checkModified && PluginModified(plugin.ID)) + { + // Distinguish exception from installing same or less version + throw new ArgumentException($"Plugin {plugin.Name} {plugin.ID} has been modified.", nameof(plugin)); + } + + // Unzip plugin files to temp folder + var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath); + + if(!plugin.IsFromLocalInstallPath) + File.Delete(zipFilePath); + + var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath); + + var metadataJsonFilePath = string.Empty; + if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName))) + metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName); + + if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath)) + { + throw new FileNotFoundException($"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist"); + } + + if (SameOrLesserPluginVersionExists(metadataJsonFilePath)) + { + throw new InvalidOperationException($"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}"); + } + + var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}"; + + var defaultPluginIDs = new List + { + "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark + "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator + "572be03c74c642baae319fc283e561a8", // Explorer + "6A122269676E40EB86EB543B945932B9", // PluginIndicator + "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager + "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller + "791FC278BA414111B8D1886DFE447410", // Program + "D409510CD0D2481F853690A07E6DC426", // Shell + "CEA08895D2544B019B2E9C5009600DF4", // Sys + "0308FD86DE0A4DEE8D62B9B535370992", // URL + "565B73353DBF4806919830B9202EE3BF", // WebSearch + "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings + }; + + // Treat default plugin differently, it needs to be removable along with each flow release + var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID) + ? DataLocation.PluginsDirectory + : Constant.PreinstalledDirectory; + + var newPluginPath = Path.Combine(installDirectory, folderName); + + FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s)); + + try + { + if (Directory.Exists(tempFolderPluginPath)) + Directory.Delete(tempFolderPluginPath, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.InstallPlugin|Failed to delete temp folder {tempFolderPluginPath}", e); + } + + if (checkModified) + { + _modifiedPlugins.Add(plugin.ID); + } + } + + internal static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) + { + if (checkModified && PluginModified(plugin.ID)) + { + throw new ArgumentException($"Plugin {plugin.Name} has been modified"); + } + + if (removePluginSettings) + { + if (AllowedLanguage.IsDotNet(plugin.Language)) // for the plugin in .NET, we can use assembly loader + { + var assemblyLoader = new PluginAssemblyLoader(plugin.ExecuteFilePath); + var assembly = assemblyLoader.LoadAssemblyAndDependencies(); + var assemblyName = assembly.GetName().Name; + + // if user want to remove the plugin settings, we cannot call save method for the plugin json storage instance of this plugin + // so we need to remove it from the api instance + var method = API.GetType().GetMethod("RemovePluginSettings"); + var pluginJsonStorage = method?.Invoke(API, new object[] { assemblyName }); + + // if there exists a json storage for current plugin, we need to delete the directory path + if (pluginJsonStorage != null) + { + var deleteMethod = pluginJsonStorage.GetType().GetMethod("DeleteDirectory"); + try + { + deleteMethod?.Invoke(pluginJsonStorage, null); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), + string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); + } + } + } + else // the plugin with json prc interface + { + var pluginPair = AllPlugins.FirstOrDefault(p => p.Metadata.ID == plugin.ID); + if (pluginPair != null && pluginPair.Plugin is JsonRPCPlugin jsonRpcPlugin) + { + try + { + jsonRpcPlugin.DeletePluginSettingsDirectory(); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), + string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); + } + } + } + } + + if (removePluginFromSettings) + { + Settings.Plugins.Remove(plugin.ID); + AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); + } + + // Marked for deletion. Will be deleted on next start up + using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt")); + + if (checkModified) + { + _modifiedPlugins.Add(plugin.ID); + } + } + + #endregion } } diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index e6329aba1..4827cf69d 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -3,9 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using System.Windows.Forms; +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 Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; @@ -17,22 +20,33 @@ namespace Flow.Launcher.Core.Plugin public static List Plugins(List metadatas, PluginsSettings settings) { var dotnetPlugins = DotNetPlugins(metadatas); - + 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 executableV2Plugins = ExecutableV2Plugins(metadatas); + var plugins = dotnetPlugins - .Concat(pythonPlugins) - .Concat(tsPlugins) - .Concat(jsPlugins) - .Concat(executablePlugins) - .ToList(); + .Concat(pythonPlugins) + .Concat(pythonV2Plugins) + .Concat(tsPlugins) + .Concat(jsPlugins) + .Concat(tsV2Plugins) + .Concat(jsV2Plugins) + .Concat(executablePlugins) + .Concat(executableV2Plugins) + .ToList(); return plugins; } @@ -91,7 +105,7 @@ namespace Flow.Launcher.Core.Plugin return; } - plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata}); + plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); }); metadata.InitTime += milliseconds; } @@ -106,17 +120,17 @@ namespace Flow.Launcher.Core.Plugin _ = 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 ExecutablePlugins(IEnumerable source) + public static IEnumerable ExecutablePlugins(IEnumerable source) { return source .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) @@ -125,5 +139,15 @@ namespace Flow.Launcher.Core.Plugin Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata }); } + + public static IEnumerable ExecutableV2Plugins(IEnumerable source) + { + return source + .Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase)) + .Select(metadata => new PluginPair + { + Plugin = new ExecutablePluginV2(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 2bbf6d110..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; @@ -23,22 +24,20 @@ namespace Flow.Launcher.Core.Plugin RedirectStandardError = true, }; - // temp fix for issue #667 var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); _startInfo.EnvironmentVariables["PYTHONPATH"] = path; + // Prevent Python from writing .py[co] files. + // Because .pyc contains location infos which will prevent python portable. + _startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1"; _startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; _startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory; _startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory; - - - //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable - _startInfo.ArgumentList.Add("-B"); } protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - _startInfo.ArgumentList[2] = request.ToString(); + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } @@ -46,15 +45,58 @@ namespace Flow.Launcher.Core.Plugin 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[2] = rpcRequest.ToString(); - _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); + _startInfo.WorkingDirectory = Context.CurrentPluginMetadata.PluginDirectory; // TODO: Async Action return Execute(_startInfo); } + public override async Task InitAsync(PluginInitContext context) { - _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); - _startInfo.ArgumentList.Add(""); + // Run .py files via `-c ` + if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) + { + var rootDirectory = context.CurrentPluginMetadata.PluginDirectory; + var libDirectory = Path.Combine(rootDirectory, "lib"); + var libPyWin32Directory = Path.Combine(libDirectory, "win32"); + var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib"); + var pluginDirectory = Path.Combine(rootDirectory, "plugin"); + + // This makes it easier for plugin authors to import their own modules. + // They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually. + // Instead of running the .py file directly, we pass the code we want to run as a CLI argument. + // This code sets sys.path for the plugin author and then runs the .py file via runpy. + _startInfo.ArgumentList.Add("-c"); + _startInfo.ArgumentList.Add( + $""" + import sys + sys.path.append(r'{rootDirectory}') + sys.path.append(r'{libDirectory}') + sys.path.append(r'{libPyWin32LibDirectory}') + sys.path.append(r'{libPyWin32Directory}') + sys.path.append(r'{pluginDirectory}') + + import runpy + runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__') + """ + ); + // Plugins always expect the JSON data to be in the third argument + // (we're always setting it as _startInfo.ArgumentList[2] = ...). + _startInfo.ArgumentList.Add(""); + } + // Run .pyz files as is + else + { + // No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now, + // but the plugins still expect data to be sent as the third argument, so we're keeping + // the flag here, even though it's not necessary anymore. + _startInfo.ArgumentList.Add("-B"); + _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + // Plugins always expect the JSON data to be in the third argument + // (we're always setting it as _startInfo.ArgumentList[2] = ...). + _startInfo.ArgumentList.Add(""); + } + await base.InitAsync(context); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; } 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 a819f94b7..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 @@ -34,9 +33,13 @@ namespace Flow.Launcher.Core.Plugin searchTerms = terms; } - var query = new Query(rawQuery, search,terms, searchTerms, actionKeyword); - - return query; + return new Query () + { + Search = search, + RawQuery = rawQuery, + SearchTerms = searchTerms, + ActionKeyword = actionKeyword + }; } } } \ No newline at end of file diff --git a/Flow.Launcher.Core/Resource/AvailableLanguages.cs b/Flow.Launcher.Core/Resource/AvailableLanguages.cs index f541d3f35..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 { @@ -23,8 +23,12 @@ namespace Flow.Launcher.Core.Resource public static Language Spanish_LatinAmerica = new Language("es-419", "Spanish (Latin America)"); public static Language Italian = new Language("it", "Italiano"); public static Language Norwegian_Bokmal = new Language("nb-NO", "Norsk Bokmål"); - public static Language Slovak = new Language("sk", "Slovenský"); + public static Language Slovak = new Language("sk", "Slovenčina"); public static Language Turkish = new Language("tr", "Türkçe"); + public static Language Czech = new Language("cs", "čeština"); + public static Language Arabic = new Language("ar", "اللغة العربية"); + public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt"); + public static Language Hebrew = new Language("he", "עברית"); public static List GetAvailableLanguages() { @@ -50,9 +54,46 @@ namespace Flow.Launcher.Core.Resource Italian, Norwegian_Bokmal, Slovak, - Turkish + Turkish, + Czech, + Arabic, + Vietnamese, + Hebrew }; return languages; } + + public static string GetSystemTranslation(string languageCode) + { + return languageCode switch + { + "en" => "System", + "zh-cn" => "系统", + "zh-tw" => "系統", + "uk-UA" => "Система", + "ru" => "Система", + "fr" => "Système", + "ja" => "システム", + "nl" => "Systeem", + "pl" => "System", + "da" => "System", + "de" => "System", + "ko" => "시스템", + "sr" => "Систем", + "pt-pt" => "Sistema", + "pt-br" => "Sistema", + "es" => "Sistema", + "es-419" => "Sistema", + "it" => "Sistema", + "nb-NO" => "System", + "sk" => "Systém", + "tr" => "Sistem", + "cs" => "Systém", + "ar" => "النظام", + "vi-vn" => "Hệ thống", + "he" => "מערכת", + _ => "System", + }; + } } } diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index acc693ed5..e2a66656a 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -11,38 +11,65 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using System.Globalization; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Core.Resource { public class Internationalization { - public Settings Settings { get; set; } private const string Folder = "Languages"; + private const string DefaultLanguageCode = "en"; private const string DefaultFile = "en.xaml"; private const string Extension = ".xaml"; + private readonly Settings _settings; private readonly List _languageDirectories = new List(); private readonly List _oldResources = new List(); + private readonly string SystemLanguageCode; - public Internationalization() + public Internationalization(Settings settings) { - AddPluginLanguageDirectories(); - LoadDefaultLanguage(); - // we don't want to load /Languages/en.xaml twice - // so add flowlauncher language directory after load plugin language files + _settings = settings; AddFlowLauncherLanguageDirectory(); + SystemLanguageCode = GetSystemLanguageCodeAtStartup(); } - private void AddFlowLauncherLanguageDirectory() { var directory = Path.Combine(Constant.ProgramDirectory, Folder); _languageDirectories.Add(directory); } - - private void AddPluginLanguageDirectories() + private static string GetSystemLanguageCodeAtStartup() { - foreach (var plugin in PluginManager.GetPluginsForInterface()) + var availableLanguages = AvailableLanguages.GetAvailableLanguages(); + + // Retrieve the language identifiers for the current culture. + // ChangeLanguage method overrides the CultureInfo.CurrentCulture, so this needs to + // be called at startup in order to get the correct lang code of system. + var currentCulture = CultureInfo.CurrentCulture; + var twoLetterCode = currentCulture.TwoLetterISOLanguageName; + var threeLetterCode = currentCulture.ThreeLetterISOLanguageName; + var fullName = currentCulture.Name; + + // Try to find a match in the available languages list + foreach (var language in availableLanguages) + { + var languageCode = language.LanguageCode; + + if (string.Equals(languageCode, twoLetterCode, StringComparison.OrdinalIgnoreCase) || + string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) || + string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase)) + { + return languageCode; + } + } + + return DefaultLanguageCode; + } + + internal void AddPluginLanguageDirectories(IEnumerable plugins) + { + foreach (var plugin in plugins) { var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location; var dir = Path.GetDirectoryName(location); @@ -56,10 +83,15 @@ namespace Flow.Launcher.Core.Resource Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>"); } } + + LoadDefaultLanguage(); } private void LoadDefaultLanguage() { + // Removes language files loaded before any plugins were loaded. + // Prevents the language Flow started in from overwriting English if the user switches back to English + RemoveOldLanguageFiles(); LoadLanguage(AvailableLanguages.English); _oldResources.Clear(); } @@ -67,8 +99,18 @@ namespace Flow.Launcher.Core.Resource public void ChangeLanguage(string languageCode) { languageCode = languageCode.NonNull(); - Language language = GetLanguageByLanguageCode(languageCode); - ChangeLanguage(language); + + // Get actual language if language code is system + var isSystem = false; + if (languageCode == Constant.SystemLanguageCode) + { + languageCode = SystemLanguageCode; + isSystem = true; + } + + // Get language by language code and change language + var language = GetLanguageByLanguageCode(languageCode); + ChangeLanguage(language, isSystem); } private Language GetLanguageByLanguageCode(string languageCode) @@ -86,26 +128,22 @@ namespace Flow.Launcher.Core.Resource } } - public void ChangeLanguage(Language language) + private void ChangeLanguage(Language language, bool isSystem) { language = language.NonNull(); - RemoveOldLanguageFiles(); if (language != AvailableLanguages.English) { LoadLanguage(language); } - // Culture of this thread - // Use CreateSpecificCulture to preserve possible user-override settings in Windows + // 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; - // App domain - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode); - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.DefaultThreadCurrentCulture; // Raise event after culture is set - Settings.Language = language.LanguageCode; + _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; _ = Task.Run(() => { UpdatePluginMetadataTranslations(); @@ -116,7 +154,7 @@ namespace Flow.Launcher.Core.Resource { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); - if (Settings.ShouldUsePinyin) + if (_settings.ShouldUsePinyin) return false; if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW) @@ -126,7 +164,7 @@ namespace Flow.Launcher.Core.Resource // "Do you want to search with pinyin?" string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ; - if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) + if (Ioc.Default.GetRequiredService().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) return false; return true; @@ -143,11 +181,14 @@ namespace Flow.Launcher.Core.Resource private void LoadLanguage(Language language) { + var flowEnglishFile = Path.Combine(Constant.ProgramDirectory, Folder, DefaultFile); var dicts = Application.Current.Resources.MergedDictionaries; var filename = $"{language.LanguageCode}{Extension}"; var files = _languageDirectories .Select(d => LanguageFile(d, filename)) - .Where(f => !string.IsNullOrEmpty(f)) + // Exclude Flow's English language file since it's built into the binary, and there's no need to load + // it again from the file system. + .Where(f => !string.IsNullOrEmpty(f) && f != flowEnglishFile) .ToArray(); if (files.Length > 0) @@ -166,7 +207,9 @@ 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) @@ -193,7 +236,7 @@ namespace Flow.Launcher.Core.Resource { p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); - pluginI18N.OnCultureInfoChanged(CultureInfo.DefaultThreadCurrentCulture); + pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); } catch (Exception e) { diff --git a/Flow.Launcher.Core/Resource/InternationalizationManager.cs b/Flow.Launcher.Core/Resource/InternationalizationManager.cs index 3d87626e6..5d718466c 100644 --- a/Flow.Launcher.Core/Resource/InternationalizationManager.cs +++ b/Flow.Launcher.Core/Resource/InternationalizationManager.cs @@ -1,26 +1,12 @@ -namespace Flow.Launcher.Core.Resource +using System; +using CommunityToolkit.Mvvm.DependencyInjection; + +namespace Flow.Launcher.Core.Resource { + [Obsolete("InternationalizationManager.Instance is obsolete. Use Ioc.Default.GetRequiredService() instead.")] public static class InternationalizationManager { - private static Internationalization instance; - private static object syncObject = new object(); - public static Internationalization Instance - { - get - { - if (instance == null) - { - lock (syncObject) - { - if (instance == null) - { - instance = new Internationalization(); - } - } - } - return instance; - } - } + => Ioc.Default.GetRequiredService(); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 96338cf6a..4deea1f66 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -2,27 +2,34 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; +using System.Xml; using System.Windows; using System.Windows.Controls; -using System.Windows.Interop; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Media.Effects; +using System.Windows.Shell; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Resource { public class Theme { + private const string ThemeMetadataNamePrefix = "Name:"; + private const string ThemeMetadataIsDarkPrefix = "IsDark:"; + private const string ThemeMetadataHasBlurPrefix = "HasBlur:"; + private const int ShadowExtraMargin = 32; - private readonly List _themeDirectories = new List(); + 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); @@ -32,8 +39,11 @@ namespace Flow.Launcher.Core.Resource private double mainWindowWidth; - public Theme() + public Theme(IPublicAPI publicAPI, Settings settings) { + _api = publicAPI; + _settings = settings; + _themeDirectories.Add(DirectoryPath); _themeDirectories.Add(UserDirectoryPath); MakeSureThemeDirectoriesExist(); @@ -79,33 +89,33 @@ namespace Flow.Launcher.Core.Resource { if (string.IsNullOrEmpty(path)) throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); - + // reload all resources even if the theme itself hasn't changed in order to pickup changes // to things like fonts UpdateResourceDictionary(GetResourceDictionary(theme)); - - Settings.Theme = theme; - + _settings.Theme = theme; + + //always allow re-loading default theme, in case of failure of switching to a new theme from default theme if (_oldTheme != theme || theme == defaultTheme) { _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); } - BlurEnabled = IsBlurTheme(); + BlurEnabled = Win32Helper.IsBlurTheme(); - if (Settings.UseDropShadowEffect && !BlurEnabled) + if (_settings.UseDropShadowEffect && !BlurEnabled) AddDropShadowEffectToCurrentTheme(); - SetBlurForWindow(); + Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled); } catch (DirectoryNotFoundException) { Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); if (theme != defaultTheme) { - MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme)); + _api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme)); ChangeTheme(defaultTheme); } return false; @@ -115,7 +125,7 @@ namespace Flow.Launcher.Core.Resource Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); if (theme != defaultTheme) { - MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme)); + _api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme)); ChangeTheme(defaultTheme); } return false; @@ -143,19 +153,19 @@ namespace Flow.Launcher.Core.Resource return dict; } - private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(Settings.Theme); + private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(_settings.Theme); public ResourceDictionary GetResourceDictionary(string theme) { var dict = GetThemeResourceDictionary(theme); - + if (dict["QueryBoxStyle"] is Style queryBoxStyle && dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) { - var fontFamily = new FontFamily(Settings.QueryBoxFont); - var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle); - var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.QueryBoxFontWeight); - var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.QueryBoxFontStretch); + var fontFamily = new FontFamily(_settings.QueryBoxFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); @@ -176,25 +186,39 @@ namespace Flow.Launcher.Core.Resource } if (dict["ItemTitleStyle"] is Style resultItemStyle && - dict["ItemSubTitleStyle"] is Style resultSubItemStyle && - dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle && dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) { - Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultFont)); - Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultFontStyle)); - Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultFontWeight)); - Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultFontStretch)); + Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultFont)); + Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle)); + Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight)); + Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch)); Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; Array.ForEach( - new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o + new[] { resultItemStyle, resultItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); } + + if ( + dict["ItemSubTitleStyle"] is Style resultSubItemStyle && + dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) + { + Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultSubFont)); + Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle)); + Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight)); + Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch)); + + Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; + Array.ForEach( + new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o + => Array.ForEach(setters, p => o.Setters.Add(p))); + } + /* Ignore Theme Window Width and use setting */ var windowStyle = dict["WindowStyle"] as Style; - var width = Settings.WindowSize; + var width = _settings.WindowSize; windowStyle.Setters.Add(new Setter(Window.WidthProperty, width)); mainWindowWidth = (double)width; return dict; @@ -202,20 +226,56 @@ namespace Flow.Launcher.Core.Resource private ResourceDictionary GetCurrentResourceDictionary( ) { - return GetResourceDictionary(Settings.Theme); + return GetResourceDictionary(_settings.Theme); } - public List LoadAvailableThemes() + public List LoadAvailableThemes() { - List themes = new List(); + List themes = new List(); foreach (var themeDirectory in _themeDirectories) { - themes.AddRange( - Directory.GetFiles(themeDirectory) - .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) - .ToList()); + var filePaths = Directory + .GetFiles(themeDirectory) + .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) + .Select(GetThemeDataFromPath); + themes.AddRange(filePaths); } - return themes.OrderBy(o => o).ToList(); + + return themes.OrderBy(o => o.Name).ToList(); + } + + private ThemeData GetThemeDataFromPath(string path) + { + using var reader = XmlReader.Create(path); + reader.Read(); + + var extensionlessName = Path.GetFileNameWithoutExtension(path); + + if (reader.NodeType is not XmlNodeType.Comment) + return new ThemeData(extensionlessName, extensionlessName); + + var commentLines = reader.Value.Trim().Split('\n').Select(v => v.Trim()); + + var name = extensionlessName; + bool? isDark = null; + bool? hasBlur = null; + foreach (var line in commentLines) + { + if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim(); + } + else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase)) + { + isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim()); + } + else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase)) + { + hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim()); + } + } + + return new ThemeData(extensionlessName, name, isDark, hasBlur); } private string GetThemePath(string themeName) @@ -253,12 +313,15 @@ namespace Flow.Launcher.Core.Resource var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; if (marginSetter == null) { + var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin); marginSetter = new Setter() { Property = Border.MarginProperty, - Value = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin), + Value = margin, }; windowBorderStyle.Setters.Add(marginSetter); + + SetResizeBoarderThickness(margin); } else { @@ -269,6 +332,8 @@ namespace Flow.Launcher.Core.Resource baseMargin.Right + ShadowExtraMargin, baseMargin.Bottom + ShadowExtraMargin); marginSetter.Value = newMargin; + + SetResizeBoarderThickness(newMargin); } windowBorderStyle.Setters.Add(effectSetter); @@ -299,99 +364,36 @@ namespace Flow.Launcher.Core.Resource marginSetter.Value = newMargin; } + SetResizeBoarderThickness(null); + UpdateResourceDictionary(dict); } - #region Blur Handling - /* - Found on https://github.com/riverar/sample-win10-aeroglass - */ - private enum AccentState + // because adding drop shadow effect will change the margin of the window, + // we need to update the window chrome thickness to correct set the resize border + private static void SetResizeBoarderThickness(Thickness? effectMargin) { - ACCENT_DISABLED = 0, - ACCENT_ENABLE_GRADIENT = 1, - ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, - ACCENT_ENABLE_BLURBEHIND = 3, - ACCENT_INVALID_STATE = 4 - } - - [StructLayout(LayoutKind.Sequential)] - private struct AccentPolicy - { - public AccentState AccentState; - public int AccentFlags; - public int GradientColor; - public int AnimationId; - } - - [StructLayout(LayoutKind.Sequential)] - private struct WindowCompositionAttributeData - { - public WindowCompositionAttribute Attribute; - public IntPtr Data; - public int SizeOfData; - } - - private enum WindowCompositionAttribute - { - WCA_ACCENT_POLICY = 19 - } - [DllImport("user32.dll")] - private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); - - /// - /// Sets the blur for a window via SetWindowCompositionAttribute - /// - public void SetBlurForWindow() - { - if (BlurEnabled) + var window = Application.Current.MainWindow; + if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome) { - SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND); - } - else - { - SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED); + Thickness thickness; + if (effectMargin == null) + { + thickness = SystemParameters.WindowResizeBorderThickness; + } + else + { + thickness = new Thickness( + effectMargin.Value.Left + SystemParameters.WindowResizeBorderThickness.Left, + effectMargin.Value.Top + SystemParameters.WindowResizeBorderThickness.Top, + effectMargin.Value.Right + SystemParameters.WindowResizeBorderThickness.Right, + effectMargin.Value.Bottom + SystemParameters.WindowResizeBorderThickness.Bottom); + } + + windowChrome.ResizeBorderThickness = thickness; } } - private bool IsBlurTheme() - { - if (Environment.OSVersion.Version >= new Version(6, 2)) - { - var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); - - if (resource is bool) - return (bool)resource; - - return false; - } - - return false; - } - - private void SetWindowAccent(Window w, AccentState state) - { - var windowHelper = new WindowInteropHelper(w); - - windowHelper.EnsureHandle(); - - var accent = new AccentPolicy { AccentState = state }; - var accentStructSize = Marshal.SizeOf(accent); - - var accentPtr = Marshal.AllocHGlobal(accentStructSize); - Marshal.StructureToPtr(accent, accentPtr, false); - - var data = new WindowCompositionAttributeData - { - Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY, - SizeOfData = accentStructSize, - Data = accentPtr - }; - - SetWindowCompositionAttribute(windowHelper.Handle, ref data); - - Marshal.FreeHGlobal(accentPtr); - } - #endregion + public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null); } } diff --git a/Flow.Launcher.Core/Resource/ThemeManager.cs b/Flow.Launcher.Core/Resource/ThemeManager.cs index 71f9acaa5..3cbe8319a 100644 --- a/Flow.Launcher.Core/Resource/ThemeManager.cs +++ b/Flow.Launcher.Core/Resource/ThemeManager.cs @@ -1,26 +1,12 @@ -namespace Flow.Launcher.Core.Resource +using System; +using CommunityToolkit.Mvvm.DependencyInjection; + +namespace Flow.Launcher.Core.Resource { + [Obsolete("ThemeManager.Instance is obsolete. Use Ioc.Default.GetRequiredService() instead.")] public class ThemeManager { - private static Theme instance; - private static object syncObject = new object(); - public static Theme Instance - { - get - { - if (instance == null) - { - lock (syncObject) - { - if (instance == null) - { - instance = new Theme(); - } - } - } - return instance; - } - } + => Ioc.Default.GetRequiredService(); } } diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 28232dc65..ec0b8bd57 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -23,24 +23,33 @@ namespace Flow.Launcher.Core public class Updater { public bool UpdateToPrerelease { get; set; } - + + // TODO public const string ReleaseRepository = "https://github.com/Flow-Launcher/Flow.Launcher"; public const string PrereleaseRepository = "https://github.com/Flow-Launcher/Prereleases"; - + + // TODO public string GitHubRepository => UpdateToPrerelease ? PrereleaseRepository : ReleaseRepository; + private readonly IPublicAPI _api; + + public Updater(IPublicAPI publicAPI, string gitHubRepository) + { + _api = publicAPI; + GitHubRepository = gitHubRepository; + } private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1); - public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) + public async Task UpdateAppAsync(bool silentUpdate = true) { await UpdateLock.WaitAsync().ConfigureAwait(false); try { if (!silentUpdate) - api.ShowMsg(api.GetTranslation("pleaseWait"), - api.GetTranslation("update_flowlauncher_update_check")); + _api.ShowMsg(_api.GetTranslation("pleaseWait"), + _api.GetTranslation("update_flowlauncher_update_check")); using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false); @@ -55,14 +64,13 @@ namespace Flow.Launcher.Core if (newReleaseVersion <= currentVersion) { if (!silentUpdate) - MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest")); - + _api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest")); return; } if (!silentUpdate) - api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), - api.GetTranslation("update_flowlauncher_updating")); + _api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"), + _api.GetTranslation("update_flowlauncher_updating")); await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); @@ -71,9 +79,9 @@ namespace Flow.Launcher.Core if (DataLocation.PortableDataLocationInUse()) { var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; - FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); - if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) - MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), + FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)); + if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s))) + _api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), DataLocation.PortableDataPath, targetDestination)); } @@ -86,7 +94,7 @@ namespace Flow.Launcher.Core 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); } @@ -99,8 +107,8 @@ namespace Flow.Launcher.Core Log.Exception($"|Updater.UpdateApp|Error Occurred", e); if (!silentUpdate) - api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), - api.GetTranslation("update_flowlauncher_check_connection")); + _api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"), + _api.GetTranslation("update_flowlauncher_check_connection")); } finally { 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 e23d2137e..c86ed4324 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -7,6 +7,7 @@ namespace Flow.Launcher.Infrastructure public static class Constant { public const string FlowLauncher = "Flow.Launcher"; + public const string FlowLauncherFullName = "Flow Launcher"; public const string Plugins = "Plugins"; public const string PluginMetadataFileName = "plugin.json"; @@ -19,7 +20,7 @@ namespace Flow.Launcher.Infrastructure public static readonly string RootDirectory = Directory.GetParent(ApplicationDirectory).ToString(); public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins); - public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new/choose"; + public const string IssuesUrl = "https://github.com/Flow-Launcher/Flow.Launcher/issues"; public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion; public static readonly string Dev = "Dev"; public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips"; @@ -30,6 +31,7 @@ namespace Flow.Launcher.Infrastructure public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png"); public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png"); public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png"); + public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png"); public static string PythonPath; public static string NodePath; @@ -50,5 +52,7 @@ namespace Flow.Launcher.Infrastructure 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 54c19c048..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,7 +62,7 @@ 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}"); @@ -173,5 +172,35 @@ namespace Flow.Launcher.Infrastructure.Exception } } + + public static string GetWindowsFullVersionFromRegistry() + { + try + { + var buildRevision = GetWindowsRevisionFromRegistry(); + var currentBuild = Environment.OSVersion.Version.Build; + return currentBuild.ToString() + "." + buildRevision; + } + catch + { + return Environment.OSVersion.VersionString; + } + } + + public static string GetWindowsRevisionFromRegistry() + { + try + { + using (RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\")) + { + var buildRevision = registryKey.GetValue("UBR").ToString(); + return buildRevision; + } + } + catch + { + return "0"; + } + } } } diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index 76695a4e3..b97c096c3 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; +using Windows.Win32; namespace Flow.Launcher.Infrastructure { @@ -15,7 +15,20 @@ namespace Flow.Launcher.Infrastructure { var explorerWindow = GetActiveExplorer(); string locationUrl = explorerWindow?.LocationURL; - return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null; + 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; } /// @@ -54,10 +67,6 @@ namespace Flow.Launcher.Infrastructure return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First; } - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); - private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); /// @@ -70,9 +79,9 @@ namespace Flow.Launcher.Infrastructure var index = 0; var numRemaining = hWnds.Count; - EnumWindows((wnd, _) => + PInvoke.EnumWindows((wnd, _) => { - var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32()); + var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd); if (searchIndex != -1) { z[searchIndex] = index; diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index f4d7511c6..b91da7114 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -1,4 +1,4 @@ - + net7.0-windows @@ -35,6 +35,10 @@ false + + + + @@ -49,14 +53,19 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - diff --git a/Flow.Launcher.Infrastructure/Helper.cs b/Flow.Launcher.Infrastructure/Helper.cs index db575de90..864d796c7 100644 --- a/Flow.Launcher.Infrastructure/Helper.cs +++ b/Flow.Launcher.Infrastructure/Helper.cs @@ -2,7 +2,6 @@ using System; using System.IO; -using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs b/Flow.Launcher.Infrastructure/Hotkey/GlobalHotkey.cs index a09185696..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 { @@ -11,44 +15,45 @@ namespace Flow.Launcher.Infrastructure.Hotkey /// public unsafe class GlobalHotkey : IDisposable { - private static readonly IntPtr hookId; - - - + private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback; + private static readonly UnhookWindowsHookExSafeHandle hookId; + public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state); internal static Func hookedKeyboardCallback; - //Modifier key constants - private const int VK_SHIFT = 0x10; - private const int VK_CONTROL = 0x11; - private const int VK_ALT = 0x12; - private const int VK_WIN = 91; - static GlobalHotkey() { // Set the hook - hookId = InterceptKeys.SetHook(& LowLevelKeyboardProc); + hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL); + } + + private static UnhookWindowsHookExSafeHandle SetHook(HOOKPROC proc, WINDOWS_HOOK_ID hookId) + { + using var curProcess = Process.GetCurrentProcess(); + using var curModule = curProcess.MainModule; + return PInvoke.SetWindowsHookEx(hookId, proc, PInvoke.GetModuleHandle(curModule.ModuleName), 0); } public static SpecialKeyState CheckModifiers() { SpecialKeyState state = new SpecialKeyState(); - if ((InterceptKeys.GetKeyState(VK_SHIFT) & 0x8000) != 0) + if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) & 0x8000) != 0) { //SHIFT is pressed state.ShiftPressed = true; } - if ((InterceptKeys.GetKeyState(VK_CONTROL) & 0x8000) != 0) + if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) & 0x8000) != 0) { //CONTROL is pressed state.CtrlPressed = true; } - if ((InterceptKeys.GetKeyState(VK_ALT) & 0x8000) != 0) + if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) & 0x8000) != 0) { //ALT is pressed state.AltPressed = true; } - if ((InterceptKeys.GetKeyState(VK_WIN) & 0x8000) != 0) + if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_LWIN) & 0x8000) != 0 || + (PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_RWIN) & 0x8000) != 0) { //WIN is pressed state.WinPressed = true; @@ -57,33 +62,33 @@ namespace Flow.Launcher.Infrastructure.Hotkey return state; } - [UnmanagedCallersOnly] - private static IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam) + private static LRESULT HookKeyboardCallback(int nCode, WPARAM wParam, LPARAM lParam) { bool continues = true; if (nCode >= 0) { - if (wParam.ToUInt32() == (int)KeyEvent.WM_KEYDOWN || - wParam.ToUInt32() == (int)KeyEvent.WM_KEYUP || - wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYDOWN || - wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYUP) + if (wParam.Value == (int)KeyEvent.WM_KEYDOWN || + wParam.Value == (int)KeyEvent.WM_KEYUP || + wParam.Value == (int)KeyEvent.WM_SYSKEYDOWN || + wParam.Value == (int)KeyEvent.WM_SYSKEYUP) { if (hookedKeyboardCallback != null) - continues = hookedKeyboardCallback((KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), CheckModifiers()); + continues = hookedKeyboardCallback((KeyEvent)wParam.Value, Marshal.ReadInt32(lParam), CheckModifiers()); } } if (continues) { - return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam); + return PInvoke.CallNextHookEx(hookId, nCode, wParam, lParam); } - return (IntPtr)(-1); + + return new LRESULT(1); } public void Dispose() { - InterceptKeys.UnhookWindowsHookEx(hookId); + hookId.Dispose(); } ~GlobalHotkey() @@ -91,4 +96,4 @@ namespace Flow.Launcher.Infrastructure.Hotkey Dispose(); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs index 937059436..25bc75a56 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs @@ -6,7 +6,7 @@ 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; } @@ -17,8 +17,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey private static readonly Dictionary specialSymbolDictionary = new Dictionary { - {Key.Space, "Space"}, - {Key.Oem3, "~"} + { Key.Space, "Space" }, { Key.Oem3, "~" } }; public ModifierKeys ModifierKeys @@ -30,18 +29,22 @@ namespace Flow.Launcher.Infrastructure.Hotkey { modifierKeys |= ModifierKeys.Alt; } + if (Shift) { modifierKeys |= ModifierKeys.Shift; } + if (Win) { modifierKeys |= ModifierKeys.Windows; } + if (Ctrl) { modifierKeys |= ModifierKeys.Control; } + return modifierKeys; } } @@ -66,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 == 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; @@ -103,7 +112,6 @@ namespace Flow.Launcher.Infrastructure.Hotkey } catch (ArgumentException) { - } } } @@ -111,33 +119,39 @@ namespace Flow.Launcher.Infrastructure.Hotkey public override string ToString() { - List keys = new List(); - if (Ctrl) + return string.Join(" + ", EnumerateDisplayKeys()); + } + + public IEnumerable EnumerateDisplayKeys() + { + if (Ctrl && CharKey is not (Key.LeftCtrl or Key.RightCtrl)) { - keys.Add("Ctrl"); + yield return "Ctrl"; } - if (Alt) + + if (Alt && CharKey is not (Key.LeftAlt or Key.RightAlt)) { - keys.Add("Alt"); + yield return "Alt"; } - if (Shift) + + if (Shift && CharKey is not (Key.LeftShift or Key.RightShift)) { - keys.Add("Shift"); + yield return "Shift"; } - if (Win) + + if (Win && CharKey is not (Key.LWin or Key.RWin)) { - keys.Add("Win"); + yield return "Win"; } if (CharKey != Key.None) { - keys.Add(specialSymbolDictionary.ContainsKey(CharKey) - ? specialSymbolDictionary[CharKey] - : CharKey.ToString()); + yield return specialSymbolDictionary.TryGetValue(CharKey, out var value) + ? value + : CharKey.ToString(); } - return string.Join(" + ", keys); } - + /// /// Validate hotkey /// @@ -164,11 +178,13 @@ namespace Flow.Launcher.Infrastructure.Hotkey { KeyGesture keyGesture = new KeyGesture(CharKey, ModifierKeys); } - catch (System.Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException) + catch (System.Exception e) when + (e is NotSupportedException || e is InvalidEnumArgumentException) { return false; } } + if (ModifierKeys == ModifierKeys.None) { return !IsPrintableCharacter(CharKey); @@ -206,18 +222,6 @@ namespace Flow.Launcher.Infrastructure.Hotkey key == Key.Decimal; } - public override bool Equals(object obj) - { - if (obj is HotkeyModel other) - { - return ModifierKeys == other.ModifierKeys && CharKey == other.CharKey; - } - else - { - return false; - } - } - public override int GetHashCode() { 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 d33bac34c..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 unsafe class InterceptKeys - { - public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam); - - private const int WH_KEYBOARD_LL = 13; - - public static IntPtr SetHook(delegate* unmanaged proc) - { - using (Process curProcess = Process.GetCurrentProcess()) - using (ProcessModule curModule = curProcess.MainModule) - { - return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); - } - } - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - public static extern IntPtr SetWindowsHookEx(int idHook, delegate* unmanaged 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 index 15e306883..95bb25837 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/KeyEvent.cs @@ -1,3 +1,5 @@ +using Windows.Win32; + namespace Flow.Launcher.Infrastructure.Hotkey { public enum KeyEvent @@ -5,21 +7,21 @@ namespace Flow.Launcher.Infrastructure.Hotkey /// /// Key down /// - WM_KEYDOWN = 256, + WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN, /// /// Key up /// - WM_KEYUP = 257, + WM_KEYUP = (int)PInvoke.WM_KEYUP, /// /// System key up /// - WM_SYSKEYUP = 261, + WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP, /// /// System key down /// - WM_SYSKEYDOWN = 260 + WM_SYSKEYDOWN = (int)PInvoke.WM_SYSKEYDOWN } -} \ 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 e5be0701f..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 @@ -81,20 +77,55 @@ namespace Flow.Launcher.Infrastructure.Http } 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, token); + 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 { diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index e7b1f46f7..ddbab4ef0 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -3,36 +3,23 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using System.Windows.Media; +using BitFaster.Caching.Lfu; namespace Flow.Launcher.Infrastructure.Image { - [Serializable] - public class ImageUsage - { - - public int usage; - public ImageSource imageSource; - - public ImageUsage(int usage, ImageSource image) - { - this.usage = usage; - imageSource = image; - } - } - public class ImageCache { - private const int MaxCached = 50; - public ConcurrentDictionary<(string, bool), ImageUsage> Data { get; } = new(); - private const int permissibleFactor = 2; - private SemaphoreSlim semaphore = new(1, 1); + private const int MaxCached = 150; - public void Initialization(Dictionary<(string, bool), int> usage) + private ConcurrentLfu<(string, bool), ImageSource> CacheManager { get; set; } = new(MaxCached); + + public void Initialize(IEnumerable<(string, bool)> usage) { - foreach (var key in usage.Keys) + foreach (var key in usage) { - Data[key] = new ImageUsage(usage[key], null); + CacheManager.AddOrUpdate(key, null); } } @@ -40,70 +27,42 @@ namespace Flow.Launcher.Infrastructure.Image { get { - if (!Data.TryGetValue((path, isFullImage), out var value)) - { - return null; - } - value.usage++; - return value.imageSource; - + return CacheManager.TryGet((path, isFullImage), out var value) ? value : null; } set { - Data.AddOrUpdate( - (path, isFullImage), - new ImageUsage(0, value), - (k, v) => - { - v.imageSource = value; - v.usage++; - return v; - } - ); - - SliceExtra(); - - async void SliceExtra() - { - // To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size - // This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time - if (Data.Count > permissibleFactor * MaxCached) - { - await semaphore.WaitAsync().ConfigureAwait(false); - // To delete the images from the data dictionary based on the resizing of the Usage Dictionary - // Double Check to avoid concurrent remove - if (Data.Count > permissibleFactor * MaxCached) - foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key)) - Data.TryRemove(key, out _); - semaphore.Release(); - } - } + CacheManager.AddOrUpdate((path, isFullImage), value); } } + public async ValueTask GetOrAddAsync(string key, + Func<(string, bool), Task> valueFactory, + bool isFullImage = false) + { + return await CacheManager.GetOrAddAsync((key, isFullImage), valueFactory); + } + public bool ContainsKey(string key, bool isFullImage) { - return key is not null && Data.ContainsKey((key, isFullImage)) && Data[(key, isFullImage)].imageSource != null; + return CacheManager.TryGet((key, isFullImage), out _); } public bool TryGetValue(string key, bool isFullImage, out ImageSource image) { - if (key is not null) + if (CacheManager.TryGet((key, isFullImage), out var value)) { - bool hasKey = Data.TryGetValue((key, isFullImage), out var imageUsage); - image = hasKey ? imageUsage.imageSource : null; - return hasKey; - } - else - { - image = null; - return false; + image = value; + return image != null; } + + + image = null; + return false; } public int CacheSize() { - return Data.Count; + return CacheManager.Count; } /// @@ -111,7 +70,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; } } } diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 62524f03a..6f7b1cd90 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Net; -using System.Net.Http; +using System.Threading; using System.Threading.Tasks; +using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.Imaging; using Flow.Launcher.Infrastructure.Logger; @@ -18,32 +17,30 @@ namespace Flow.Launcher.Infrastructure.Image public static class ImageLoader { private static readonly ImageCache ImageCache = new(); - private static BinaryStorage> _storage; + 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 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; - private static readonly string[] ImageExtensions = - { - ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" - }; + private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" }; - public static void Initialize() + public static async Task InitializeAsync() { - _storage = new BinaryStorage>("Image"); + _storage = new BinaryStorage>("Image"); _hashGenerator = new ImageHashGenerator(); - var usage = LoadStorageToConcurrentDictionary(); + var usage = await LoadStorageToConcurrentDictionaryAsync(); - foreach (var icon in new[] - { - Constant.DefaultIcon, Constant.MissingImgIcon - }) + ImageCache.Initialize(usage); + + foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon }) { ImageSource img = new BitmapImage(new Uri(icon)); img.Freeze(); @@ -54,33 +51,42 @@ namespace Flow.Launcher.Infrastructure.Image { await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () => { - foreach (var ((path, isFullImage), _) in ImageCache.Data) + foreach (var (path, isFullImage) in usage) { await LoadAsync(path, isFullImage); } }); - Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); + Log.Info( + $"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); }); } - public static void Save() + public static async Task Save() { - lock (_storage) + await storageLock.WaitAsync(); + + try { - _storage.Save(ImageCache.Data - .ToDictionary( - x => x.Key, - x => x.Value.usage)); + await _storage.SaveAsync(ImageCache.EnumerateEntries() + .Select(x => x.Key) + .ToList()); + } + finally + { + storageLock.Release(); } } - private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary() + private static async Task> LoadStorageToConcurrentDictionaryAsync() { - lock (_storage) + await storageLock.WaitAsync(); + try { - var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>()); - - return new ConcurrentDictionary<(string, bool), int>(loaded); + return await _storage.TryLoadAsync(new List<(string, bool)>()); + } + finally + { + storageLock.Release(); } } @@ -118,9 +124,12 @@ namespace Flow.Launcher.Infrastructure.Image return new ImageResult(MissingImage, ImageType.Error); } - if (ImageCache.ContainsKey(path, loadFullImage)) + // extra scope for use of same variable name { - return new ImageResult(ImageCache[path, loadFullImage], ImageType.Cache); + if (ImageCache.TryGetValue(path, loadFullImage, out var imageSource)) + { + return new ImageResult(imageSource, ImageType.Cache); + } } if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult) @@ -130,7 +139,8 @@ namespace Flow.Launcher.Infrastructure.Image ImageCache[path, loadFullImage] = image; return new ImageResult(image, ImageType.ImageFile); } - if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + + if (path.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { var imageSource = new BitmapImage(new Uri(path)); imageSource.Freeze(); @@ -159,6 +169,7 @@ namespace Flow.Launcher.Infrastructure.Image return imageResult; } + private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult) { // Download image from url @@ -174,6 +185,7 @@ namespace Flow.Launcher.Infrastructure.Image image.DecodePixelHeight = SmallIconSize; image.DecodePixelWidth = SmallIconSize; } + image.StreamSource = buffer; image.EndInit(); image.StreamSource = null; @@ -189,8 +201,8 @@ namespace Flow.Launcher.Infrastructure.Image if (Directory.Exists(path)) { /* Directories can also have thumbnails instead of shell icons. - * Generating thumbnails for a bunch of folder results while scrolling - * could have a big impact on performance and Flow.Launcher responsibility. + * Generating thumbnails for a bunch of folder results while scrolling + * could have a big impact on performance and Flow.Launcher responsibility. * - Solution: just load the icon */ type = ImageType.Folder; @@ -204,14 +216,22 @@ namespace Flow.Launcher.Infrastructure.Image type = ImageType.ImageFile; if (loadFullImage) { - image = LoadFullImage(path); - type = ImageType.FullImageFile; + try + { + image = LoadFullImage(path); + type = ImageType.FullImageFile; + } + catch (NotSupportedException) + { + image = Image; + type = ImageType.Error; + } } else { - /* Although the documentation for GetImage on MSDN indicates that + /* Although the documentation for GetImage on MSDN indicates that * if a thumbnail is available it will return one, this has proved to not - * be the case in many situations while testing. + * be the case in many situations while testing. * - Solution: explicitly pass the ThumbnailOnly flag */ image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly); @@ -237,7 +257,8 @@ namespace Flow.Launcher.Infrastructure.Image return new ImageResult(image, type); } - private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, int size = SmallIconSize) + private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, + int size = SmallIconSize) { return WindowsThumbnailProvider.GetThumbnail( path, @@ -262,17 +283,19 @@ namespace Flow.Launcher.Infrastructure.Image 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, false] ?? img; + { + // image already exists + img = ImageCache[key, loadFullImage] ?? img; } else - { // new guid + { + // new guid GuidToKey[hash] = path; } @@ -290,7 +313,7 @@ namespace Flow.Launcher.Infrastructure.Image BitmapImage image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; - image.UriSource = new Uri(path); + image.UriSource = new Uri(path); image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; image.EndInit(); @@ -315,8 +338,10 @@ namespace Flow.Launcher.Infrastructure.Image resizedHeight.EndInit(); return resizedHeight; } + return resizedWidth; } + return image; } } 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 b8f1408e7..7f847e287 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -5,11 +5,8 @@ using NLog; using NLog.Config; using NLog.Targets; using Flow.Launcher.Infrastructure.UserSettings; -using JetBrains.Annotations; -using NLog.Fluent; using NLog.Targets.Wrappers; using System.Runtime.ExceptionServices; -using System.Text; namespace Flow.Launcher.Infrastructure.Logger { @@ -51,17 +48,45 @@ namespace Flow.Launcher.Infrastructure.Logger configuration.AddTarget("file", fileTargetASyncWrapper); configuration.AddTarget("debug", debugTarget); + var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper) + { + RuleName = "file" + }; #if DEBUG - var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper); - var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget); + var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget) + { + RuleName = "debug" + }; configuration.LoggingRules.Add(debugRule); -#else - var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper); #endif configuration.LoggingRules.Add(fileRule); LogManager.Configuration = configuration; } + public static void SetLogLevel(LOGLEVEL level) + { + switch (level) + { + case LOGLEVEL.DEBUG: + UseDebugLogLevel(); + break; + default: + UseInfoLogLevel(); + break; + } + Info(nameof(Logger), $"Using log level: {level}."); + } + + private static void UseDebugLogLevel() + { + LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal); + } + + private static void UseInfoLogLevel() + { + LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal); + } + private static void LogFaultyFormat(string message) { var logger = LogManager.GetLogger("FaultyLogger"); @@ -76,7 +101,6 @@ namespace Flow.Launcher.Infrastructure.Logger return valid; } - public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") { exception = exception.Demystify(); @@ -115,8 +139,6 @@ namespace Flow.Launcher.Infrastructure.Logger { var logger = LogManager.GetLogger(classAndMethod); - var messageBuilder = new StringBuilder(); - logger.Error(e, message); } @@ -136,7 +158,8 @@ namespace Flow.Launcher.Infrastructure.Logger } } - /// 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); } } + + public enum LOGLEVEL + { + DEBUG, + INFO + } } diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt new file mode 100644 index 000000000..f117534a1 --- /dev/null +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -0,0 +1,19 @@ +SHCreateItemFromParsingName +DeleteObject +IShellItem +IShellItemImageFactory +S_OK + +SetWindowsHookEx +UnhookWindowsHookEx +CallNextHookEx +GetModuleHandle +GetKeyState +VIRTUAL_KEY + +WM_KEYDOWN +WM_KEYUP +WM_SYSKEYDOWN +WM_SYSKEYUP + +EnumWindows \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 7d7235968..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 { @@ -129,7 +130,12 @@ 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)); } diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index ea2d42773..2a439b8cc 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -4,115 +4,78 @@ using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters; using System.Runtime.Serialization.Formatters.Binary; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; +using MemoryPack; namespace Flow.Launcher.Infrastructure.Storage { -#pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete. /// /// Stroage object using binary data /// Normally, it has better performance, but not readable /// + /// + /// It utilize MemoryPack, which means the object must be MemoryPackSerializable + /// https://github.com/Cysharp/MemoryPack + /// public class BinaryStorage { + const string DirectoryName = "Cache"; + + const string FileSuffix = ".cache"; + public BinaryStorage(string filename) { - const string directoryName = "Cache"; - var directoryPath = Path.Combine(DataLocation.DataDirectory(), directoryName); + var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); Helper.ValidateDirectory(directoryPath); - const string fileSuffix = ".cache"; - FilePath = Path.Combine(directoryPath, $"{filename}{fileSuffix}"); + FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); } public string FilePath { get; } - public T TryLoad(T defaultData) + public async ValueTask TryLoadAsync(T defaultData) { if (File.Exists(FilePath)) { if (new FileInfo(FilePath).Length == 0) { Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>"); - Save(defaultData); + await SaveAsync(defaultData); return defaultData; } - using (var stream = new FileStream(FilePath, FileMode.Open)) - { - var d = Deserialize(stream, defaultData); - return d; - } + await using var stream = new FileStream(FilePath, FileMode.Open); + var d = await DeserializeAsync(stream, defaultData); + return d; } else { Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data"); - Save(defaultData); + await SaveAsync(defaultData); return defaultData; } } - private T Deserialize(FileStream stream, T defaultData) + private async ValueTask DeserializeAsync(Stream stream, T defaultData) { - //http://stackoverflow.com/questions/2120055/binaryformatter-deserialize-gives-serializationexception - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; - BinaryFormatter binaryFormatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple - }; - try { - var t = ((T)binaryFormatter.Deserialize(stream)).NonNull(); + var t = await MemoryPackSerializer.DeserializeAsync(stream); return t; } catch (System.Exception e) { - Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); + // Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); return defaultData; } - finally - { - AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve; - } } - private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + public async ValueTask SaveAsync(T data) { - Assembly ayResult = null; - string sShortAssemblyName = args.Name.Split(',')[0]; - Assembly[] ayAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (Assembly ayAssembly in ayAssemblies) - { - if (sShortAssemblyName == ayAssembly.FullName.Split(',')[0]) - { - ayResult = ayAssembly; - break; - } - } - return ayResult; - } - - public void Save(T data) - { - using (var stream = new FileStream(FilePath, FileMode.Create)) - { - BinaryFormatter binaryFormatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple - }; - - try - { - binaryFormatter.Serialize(stream, data); - } - catch (SerializationException e) - { - Log.Exception($"|BinaryStorage.Save|serialize error for file <{FilePath}>", e); - } - } + await using var stream = new FileStream(FilePath, FileMode.Create); + await MemoryPackSerializer.SerializeAsync(stream, data); } } -#pragma warning restore SYSLIB0011 } diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs index ec7a38804..865041fb3 100644 --- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.IO; using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher.Infrastructure.Storage 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 45456ddeb..507838d94 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; using System.IO; using System.Text.Json; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; namespace Flow.Launcher.Infrastructure.Storage @@ -26,6 +27,87 @@ namespace Flow.Launcher.Infrastructure.Storage protected string DirectoryPath { get; init; } = null!; + // Let the derived class to set the file path + protected JsonStorage() + { + } + + public JsonStorage(string filePath) + { + FilePath = filePath; + DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path"); + + Helper.ValidateDirectory(DirectoryPath); + } + + public async Task LoadAsync() + { + if (Data != null) + return Data; + + string? serialized = null; + + if (File.Exists(FilePath)) + { + serialized = await File.ReadAllTextAsync(FilePath); + } + + if (!string.IsNullOrEmpty(serialized)) + { + try + { + Data = JsonSerializer.Deserialize(serialized) ?? await LoadBackupOrDefaultAsync(); + } + catch (JsonException) + { + Data = await LoadBackupOrDefaultAsync(); + } + } + else + { + Data = await LoadBackupOrDefaultAsync(); + } + + return Data.NonNull(); + } + + private async ValueTask LoadBackupOrDefaultAsync() + { + var backup = await TryLoadBackupAsync(); + + return backup ?? LoadDefault(); + } + + private async ValueTask TryLoadBackupAsync() + { + if (!File.Exists(BackupFilePath)) + return default; + + try + { + await using var source = File.OpenRead(BackupFilePath); + var data = await JsonSerializer.DeserializeAsync(source) ?? default; + + if (data != null) + RestoreBackup(); + + return data; + } + catch (JsonException) + { + return default; + } + } + + private void RestoreBackup() + { + Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully"); + + if (File.Exists(FilePath)) + File.Replace(BackupFilePath, FilePath, null); + else + File.Move(BackupFilePath, FilePath); + } public T Load() { @@ -75,18 +157,9 @@ namespace Flow.Launcher.Infrastructure.Storage var data = JsonSerializer.Deserialize(File.ReadAllText(BackupFilePath)); if (data != null) - { - 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); + RestoreBackup(); - return data; - } - - return default; + return data; } catch (JsonException) { @@ -108,20 +181,31 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { string serialized = JsonSerializer.Serialize(Data, - new JsonSerializerOptions - { - WriteIndented = true - }); + new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(TempFilePath, serialized); + AtomicWriteSetting(); + } + + public async Task SaveAsync() + { + await using var tempOutput = File.OpenWrite(TempFilePath); + await JsonSerializer.SerializeAsync(tempOutput, Data, + new JsonSerializerOptions { WriteIndented = true }); + AtomicWriteSetting(); + } + + private void AtomicWriteSetting() + { if (!File.Exists(FilePath)) { File.Move(TempFilePath, FilePath); } else { - File.Replace(TempFilePath, FilePath, BackupFilePath); + var finalFilePath = new FileInfo(FilePath).LinkTarget ?? FilePath; + File.Replace(TempFilePath, finalFilePath, BackupFilePath); } } } diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index abe3f55b5..bc3900da8 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -3,14 +3,17 @@ using Flow.Launcher.Infrastructure.UserSettings; 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; + public PluginJsonStorage() { // C# related, add python related below var dataType = typeof(T); - var assemblyName = dataType.Assembly.GetName().Name; - DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName); + AssemblyName = dataType.Assembly.GetName().Name; + DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName); Helper.ValidateDirectory(DirectoryPath); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); @@ -20,6 +23,13 @@ namespace Flow.Launcher.Infrastructure.Storage { Data = data; } + + public void DeleteDirectory() + { + if (Directory.Exists(DirectoryPath)) + { + Directory.Delete(DirectoryPath, true); + } + } } } - diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index bd5dbdda9..e85c5d6f4 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -1,28 +1,35 @@ -using Flow.Launcher.Plugin.SharedModels; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin.SharedModels; using System; using System.Collections.Generic; using System.Linq; +using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher.Infrastructure { public class StringMatcher { - private readonly MatchOption _defaultMatchOption = new MatchOption(); + private readonly MatchOption _defaultMatchOption = new(); public SearchPrecisionScore UserSettingSearchPrecision { get; set; } private readonly IAlphabet _alphabet; - public StringMatcher(IAlphabet alphabet = null) + public StringMatcher(IAlphabet alphabet, Settings settings) + { + _alphabet = alphabet; + UserSettingSearchPrecision = settings.QuerySearchPrecision; + } + + // This is a workaround to allow unit tests to set the instance + public StringMatcher(IAlphabet alphabet) { _alphabet = alphabet; } - public static StringMatcher Instance { get; internal set; } - public static MatchResult FuzzySearch(string query, string stringToCompare) { - return Instance.FuzzyMatch(query, stringToCompare); + return Ioc.Default.GetRequiredService().FuzzyMatch(query, stringToCompare); } public MatchResult FuzzyMatch(string query, string stringToCompare) @@ -241,16 +248,16 @@ namespace Flow.Launcher.Infrastructure return false; } - private bool IsAcronymChar(string stringToCompare, int compareStringIndex) + private static bool IsAcronymChar(string stringToCompare, int compareStringIndex) => char.IsUpper(stringToCompare[compareStringIndex]) || compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]); - private bool IsAcronymNumber(string stringToCompare, int compareStringIndex) + private static bool IsAcronymNumber(string stringToCompare, int compareStringIndex) => stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9; // To get the index of the closest space which preceeds the first matching index - private int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) + private static int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) { var closestSpaceIndex = -1; diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs index 7806debe1..c54c30478 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomExplorerViewModel.cs @@ -1,9 +1,4 @@ using Flow.Launcher.Plugin; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Flow.Launcher.ViewModel { 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 130e25d7b..98f4dccda 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -26,9 +26,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } - // TODO: Remove. This is backwards compatibility for 1.10.0 release. - public string PythonDirectory { get; set; } - public Dictionary Plugins { get; set; } = new Dictionary(); public void UpdatePluginSettings(List metadatas) @@ -38,25 +35,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings if (Plugins.ContainsKey(metadata.ID)) { var settings = Plugins[metadata.ID]; - - if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count > settings.ActionKeywords.Count) - { - // TODO: Remove. This is backwards compatibility for Explorer 1.8.0 release. - // Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder. - if (settings.Version.CompareTo("1.8.0") < 0) - { - settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search - settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search - settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for quick access action keyword - } - - // TODO: Remove. This is backwards compatibility for Explorer 1.9.0 release. - // Introduced a new action keywords in Explorer since 1.8.0, so need to update plugin setting in the UserData folder. - if (settings.Version.CompareTo("1.8.0") > 0) - { - settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for quick access action keyword - } - } if (string.IsNullOrEmpty(settings.Version)) settings.Version = metadata.Version; diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 85c1c3c86..a44570ccb 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -1,18 +1,39 @@ -using System; -using System.Collections.Generic; +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; @@ -20,6 +41,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings 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 { @@ -42,7 +75,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings OnPropertyChanged(nameof(MaxResultsToShow)); } } - public bool UseDropShadowEffect { get; set; } = false; + public bool UseDropShadowEffect { get; set; } = true; + + /* Appearance Settings. It should be separated from the setting later.*/ + public double WindowHeightSize { get; set; } = 42; + public double ItemHeightSize { get; set; } = 58; + public double QueryBoxFontSize { get; set; } = 20; + public double ResultItemFontSize { get; set; } = 16; + public double ResultSubItemFontSize { get; set; } = 13; public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name; public string QueryBoxFontStyle { get; set; } public string QueryBoxFontWeight { get; set; } @@ -51,9 +91,15 @@ 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"; @@ -62,9 +108,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings public double SettingWindowWidth { get; set; } = 1000; public double SettingWindowHeight { get; set; } = 700; - public double SettingWindowTop { get; set; } - public double SettingWindowLeft { get; set; } - public WindowState SettingWindowState { get; set; } = WindowState.Normal; + public double? SettingWindowTop { get; set; } = null; + public double? SettingWindowLeft { get; set; } = null; + public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal; public bool PrereleaseUpdateSource { get; set; } @@ -156,40 +202,28 @@ namespace Flow.Launcher.Infrastructure.UserSettings } }; + [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; + public bool AlwaysPreview { get; set; } = false; + public bool AlwaysStartEn { get; set; } = false; + private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular; [JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))] - public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular; - - [JsonIgnore] - public string QuerySearchPrecisionString + 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; } } @@ -197,17 +231,18 @@ 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; - + + public bool KeepMaxResults { get; set; } = false; public int MaxResultsToShow { get; set; } = 5; public int ActivateTimes { get; set; } @@ -219,7 +254,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings [JsonIgnore] public ObservableCollection BuiltinShortcuts { get; set; } = new() { - new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), + new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) }; @@ -227,6 +262,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool EnableUpdateLog { 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 @@ -242,8 +278,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool HideWhenDeactivated { get; set; } = true; [JsonConverter(typeof(JsonStringEnumConverter))] - public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.RememberLastLaunchLocation; - + public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor; + [JsonConverter(typeof(JsonStringEnumConverter))] public SearchWindowAligns SearchWindowAlign { get; set; } = SearchWindowAligns.Center; @@ -255,17 +291,112 @@ namespace Flow.Launcher.Infrastructure.UserSettings [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 } public enum ColorSchemes @@ -274,7 +405,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings Light, Dark } - + public enum SearchWindowScreens { RememberLastLaunchLocation, @@ -283,7 +414,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings Primary, Custom } - + public enum SearchWindowAligns { Center, @@ -292,4 +423,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings RightTop, Custom } + + public enum AnimationSpeeds + { + Slow, + Medium, + Fast, + Custom + } } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs new file mode 100644 index 000000000..867fef4f5 --- /dev/null +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -0,0 +1,101 @@ +using System; +using System.Runtime.InteropServices; +using System.Windows.Interop; +using System.Windows; + +namespace Flow.Launcher.Infrastructure +{ + public static class Win32Helper + { + #region Blur Handling + + /* + Found on https://github.com/riverar/sample-win10-aeroglass + */ + + private enum AccentState + { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_GRADIENT = 1, + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_INVALID_STATE = 4 + } + + [StructLayout(LayoutKind.Sequential)] + private struct AccentPolicy + { + public AccentState AccentState; + public int AccentFlags; + public int GradientColor; + public int AnimationId; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WindowCompositionAttributeData + { + public WindowCompositionAttribute Attribute; + public IntPtr Data; + public int SizeOfData; + } + + private enum WindowCompositionAttribute + { + WCA_ACCENT_POLICY = 19 + } + + [DllImport("user32.dll")] + private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); + + /// + /// Checks if the blur theme is enabled + /// + public static bool IsBlurTheme() + { + if (Environment.OSVersion.Version >= new Version(6, 2)) + { + var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); + + if (resource is bool b) + return b; + + return false; + } + + return false; + } + + /// + /// Sets the blur for a window via SetWindowCompositionAttribute + /// + public static void SetBlurForWindow(Window w, bool blur) + { + SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED); + } + + private static void SetWindowAccent(Window w, AccentState state) + { + var windowHelper = new WindowInteropHelper(w); + + windowHelper.EnsureHandle(); + + var accent = new AccentPolicy { AccentState = state }; + var accentStructSize = Marshal.SizeOf(accent); + + var accentPtr = Marshal.AllocHGlobal(accentStructSize); + Marshal.StructureToPtr(accent, accentPtr, false); + + var data = new WindowCompositionAttributeData + { + Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY, + SizeOfData = accentStructSize, + Data = accentPtr + }; + + SetWindowCompositionAttribute(windowHelper.Handle, ref data); + + Marshal.FreeHGlobal(accentPtr); + } + #endregion + } +} diff --git a/Flow.Launcher.Plugin/ActionContext.cs b/Flow.Launcher.Plugin/ActionContext.cs index d898972da..e31c8e31d 100644 --- a/Flow.Launcher.Plugin/ActionContext.cs +++ b/Flow.Launcher.Plugin/ActionContext.cs @@ -2,18 +2,47 @@ 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) | @@ -21,5 +50,12 @@ namespace Flow.Launcher.Plugin (AltPressed ? ModifierKeys.Alt : ModifierKeys.None) | (WinPressed ? ModifierKeys.Windows : ModifierKeys.None); } + + public static readonly SpecialKeyState Default = new () { + CtrlPressed = false, + ShiftPressed = false, + AltPressed = false, + WinPressed = false + }; } } diff --git a/Flow.Launcher.Plugin/AllowedLanguage.cs b/Flow.Launcher.Plugin/AllowedLanguage.cs index d5eea4fa5..619a94deb 100644 --- a/Flow.Launcher.Plugin/AllowedLanguage.cs +++ b/Flow.Launcher.Plugin/AllowedLanguage.cs @@ -12,6 +12,11 @@ namespace Flow.Launcher.Plugin /// public const string Python = "Python"; + /// + /// Python V2 + /// + public const string PythonV2 = "Python_v2"; + /// /// C# /// @@ -27,16 +32,31 @@ namespace Flow.Launcher.Plugin /// 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 /// @@ -45,7 +65,7 @@ namespace Flow.Launcher.Plugin public static bool IsDotNet(string language) { return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase) - || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); + || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); } /// @@ -56,10 +76,15 @@ namespace Flow.Launcher.Plugin public static bool IsAllowed(string language) { return IsDotNet(language) - || language.Equals(Python, StringComparison.OrdinalIgnoreCase) - || language.Equals(Executable, StringComparison.OrdinalIgnoreCase) - || language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase) - || language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase); + || 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); + ; } } } diff --git a/Flow.Launcher.Plugin/EventHandler.cs b/Flow.Launcher.Plugin/EventHandler.cs index 009e1721c..893b0ba80 100644 --- a/Flow.Launcher.Plugin/EventHandler.cs +++ b/Flow.Launcher.Plugin/EventHandler.cs @@ -1,4 +1,5 @@ -using System.Windows; +using System; +using System.Windows; using System.Windows.Input; namespace Flow.Launcher.Plugin @@ -32,6 +33,24 @@ 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 /// 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 f3fc31ed8..2feb21b12 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -14,10 +14,10 @@ - 4.0.0 - 4.0.0 - 4.0.0 - 4.0.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 @@ -56,7 +57,11 @@ - + + + + + @@ -66,7 +71,11 @@ 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 index 730046e1d..45b90b09e 100644 --- a/Flow.Launcher.Plugin/GlyphInfo.cs +++ b/Flow.Launcher.Plugin/GlyphInfo.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Media; - -namespace Flow.Launcher.Plugin +namespace Flow.Launcher.Plugin { /// /// Text with FontFamily specified 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/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 61662b671..bbc998fcb 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; namespace Flow.Launcher.Plugin { @@ -7,8 +7,14 @@ namespace Flow.Launcher.Plugin /// 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(); /// diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index d9cf68469..4c7af4cd4 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using System.Windows; namespace Flow.Launcher.Plugin { @@ -16,12 +17,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); @@ -38,12 +40,18 @@ namespace Flow.Launcher.Plugin /// 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"); - + /// - /// Copy Text to clipboard + /// Copies the passed in text and shows a message indicating whether the operation was completed successfully. + /// When directCopy is set to true and passed in text is the path to a file or directory, + /// the actual file/directory will be copied to clipboard. Otherwise the text itself will still be copied to clipboard. /// /// Text to save on clipboard - public void CopyToClipboard(string text); + /// 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 @@ -80,6 +88,22 @@ namespace Flow.Launcher.Plugin /// 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 /// @@ -116,13 +140,6 @@ 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 - /// - [Obsolete("Unable to Retrieve correct return value")] - event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent; - /// /// Register a callback for Global Keyboard Event /// @@ -165,9 +182,13 @@ namespace Flow.Launcher.Plugin /// /// 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 @@ -259,5 +280,69 @@ namespace Flow.Launcher.Plugin /// 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(); } } diff --git a/Flow.Launcher.Plugin/Interfaces/ISavable.cs b/Flow.Launcher.Plugin/Interfaces/ISavable.cs index 2d13eaa6e..77bd304e4 100644 --- a/Flow.Launcher.Plugin/Interfaces/ISavable.cs +++ b/Flow.Launcher.Plugin/Interfaces/ISavable.cs @@ -1,12 +1,18 @@ -namespace Flow.Launcher.Plugin +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 additional data e.g. cache needs to be saved. /// + /// + /// For storing plugin settings, prefer + /// or . + /// Once called, your settings will be automatically saved by Flow. + /// public interface ISavable : IFeatures { + /// + /// Save additional plugin data, such as cache. + /// void Save(); } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin/NativeMethods.txt b/Flow.Launcher.Plugin/NativeMethods.txt new file mode 100644 index 000000000..e3e2b705e --- /dev/null +++ b/Flow.Launcher.Plugin/NativeMethods.txt @@ -0,0 +1,3 @@ +EnumThreadWindows +GetWindowText +GetWindowTextLength \ No newline at end of file diff --git a/Flow.Launcher.Plugin/PluginInitContext.cs b/Flow.Launcher.Plugin/PluginInitContext.cs index 04f20e984..f040752bd 100644 --- a/Flow.Launcher.Plugin/PluginInitContext.cs +++ b/Flow.Launcher.Plugin/PluginInitContext.cs @@ -1,7 +1,8 @@ -using System; - -namespace Flow.Launcher.Plugin +namespace Flow.Launcher.Plugin { + /// + /// Carries data passed to a plugin when it gets initialized. + /// public class PluginInitContext { public PluginInitContext() @@ -14,6 +15,9 @@ namespace Flow.Launcher.Plugin 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..b4e06913e 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text.Json.Serialization; diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 95547d273..e182491c2 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -1,7 +1,4 @@ -using JetBrains.Annotations; -using System; -using System.Collections.Generic; -using System.Linq; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { @@ -9,88 +6,89 @@ namespace Flow.Launcher.Plugin { public Query() { } - /// - /// to allow unit tests for plug ins - /// - public Query(string rawQuery, string search, string[] terms, string[] searchTerms, string actionKeyword = "") - { - Search = search; - RawQuery = rawQuery; -#pragma warning disable CS0618 - Terms = terms; -#pragma warning restore CS0618 - SearchTerms = searchTerms; - 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 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 init; } /// /// The search string split into a string array. + /// Does not include the . /// public string[] SearchTerms { get; init; } - /// - /// The raw query split into a string array - /// - [Obsolete("It may or may not include action keyword, which can be confusing. Use SearchTerms instead")] - public string[] Terms { get; init; } - /// /// Query can be splited into multiple terms by whitespace /// public const string TermSeparator = " "; - [Obsolete("Typo")] - public const string TermSeperater = TermSeparator; /// /// User can set multiple action keywords seperated by ';' /// public const string ActionKeywordSeparator = ";"; - [Obsolete("Typo")] - public const string ActionKeywordSeperater = ActionKeywordSeparator; - /// - /// '*' is used for System Plugin + /// Wildcard action keyword. Plugins using this value will be queried on every search. /// public const string GlobalPluginWildcardSign = "*"; + /// + /// The action keyword part of this query. + /// For global plugins this value will be empty. + /// public string ActionKeyword { get; init; } + [JsonIgnore] /// - /// 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. + /// public string FirstSearch => SplitSearch(0); - + + [JsonIgnore] private string _secondToEndSearch; - + /// /// strings from second search (including) to last search /// + [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) @@ -98,6 +96,7 @@ namespace Flow.Launcher.Plugin 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 1c4467762..9b16cc1cb 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -8,7 +9,7 @@ using System.Windows.Media; namespace Flow.Launcher.Plugin { /// - /// Describes the result of a plugin + /// Describes a result of a executed by a plugin /// public class Result { @@ -17,6 +18,8 @@ namespace Flow.Launcher.Plugin private string _icoPath; + private string _copyText = string.Empty; + /// /// The title of the result. This is always required. /// @@ -28,13 +31,13 @@ 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 + /// 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. /// @@ -46,16 +49,17 @@ namespace Flow.Launcher.Plugin /// /// 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 + /// 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; } /// - /// Image Displayed on the result - /// Relative Path to the Image File - /// GlyphInfo is prioritized if not null + /// 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; } @@ -66,7 +70,8 @@ namespace Flow.Launcher.Plugin && !string.IsNullOrEmpty(PluginDirectory) && !Path.IsPathRooted(value) && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) - && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { _icoPath = Path.Combine(PluginDirectory, value); } @@ -76,22 +81,22 @@ namespace Flow.Launcher.Plugin } } } + /// /// Determines if Icon has a border radius /// public bool RoundedIcon { get; set; } = false; /// - /// Delegate function, see + /// Delegate function that produces an /// /// public delegate ImageSource IconDelegate(); /// - /// Delegate to Get Image Source + /// Delegate to load an icon for this result. /// public IconDelegate Icon; - private string _copyText = string.Empty; /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) @@ -100,25 +105,29 @@ namespace Flow.Launcher.Plugin /// - /// Delegate. An action to take in the form of a function call when the result has been selected - /// - /// 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; } /// - /// Delegate. An Async action to take in the form of a function call when the result has been selected - /// - /// true to hide flowlauncher after select result - /// + /// 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 /// + /// default: 0 public int Score { get; set; } /// @@ -126,12 +135,6 @@ namespace Flow.Launcher.Plugin /// public IList TitleHighlightData { get; set; } - /// - /// Deprecated as of Flow Launcher v1.9.1. Subtitle highlighting is no longer offered - /// - [Obsolete("Deprecated as of Flow Launcher v1.9.1. Subtitle highlighting is no longer offered")] - public IList SubTitleHighlightData { get; set; } - /// /// Query information associated with the result /// @@ -154,39 +157,53 @@ namespace Flow.Launcher.Plugin } } - /// - 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; - - return equality; - } - - /// - public override int GetHashCode() - { - var hashcode = (Title?.GetHashCode() ?? 0) ^ - (SubTitle?.GetHashCode() ?? 0); - return hashcode; - } - /// public override string ToString() { return Title + SubTitle + Score; } + /// + /// 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 associated with this result + /// /// /// As external information for ContextMenu /// - /// public object ContextData { get; set; } /// @@ -230,30 +247,72 @@ namespace Flow.Launcher.Plugin /// #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; /// - /// Info of the preview image. + /// 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; } + 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; } - public string Description { get; set; } - public IconDelegate PreviewDelegate { get; set; } + 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, }; } } diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index dd4dcc7a4..5f003e351 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; #pragma warning disable IDE0005 using System.Windows; #pragma warning restore IDE0005 @@ -20,7 +21,8 @@ namespace Flow.Launcher.Plugin.SharedCommands /// /// /// - 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); @@ -53,7 +55,7 @@ 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) @@ -61,8 +63,9 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG 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 } @@ -74,8 +77,9 @@ namespace Flow.Launcher.Plugin.SharedCommands /// /// /// + /// /// - public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath) + public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath, Func messageBoxExShow = null) { try { @@ -95,7 +99,8 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG 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 } @@ -106,7 +111,8 @@ namespace Flow.Launcher.Plugin.SharedCommands /// Deletes a folder if it exists /// /// - public static void RemoveFolderIfExists(this string path) + /// + public static void RemoveFolderIfExists(this string path, Func messageBoxExShow = null) { try { @@ -118,7 +124,8 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG 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 } } @@ -147,7 +154,8 @@ namespace Flow.Launcher.Plugin.SharedCommands /// Open a directory window (using the OS's default handler, usually explorer) /// /// - public static void OpenPath(string fileOrFolderPath) + /// + public static void OpenPath(string fileOrFolderPath, Func messageBoxExShow = null) { var psi = new ProcessStartInfo { @@ -165,7 +173,8 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG 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 } } @@ -176,7 +185,8 @@ namespace Flow.Launcher.Plugin.SharedCommands /// File path /// Working directory /// Open as Administrator - public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false) + /// + public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false, Func messageBoxExShow = null) { var psi = new ProcessStartInfo { @@ -195,11 +205,30 @@ namespace Flow.Launcher.Plugin.SharedCommands #if DEBUG throw; #else - MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", filePath)); + 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; + } + /// /// This checks whether a given string is a directory path or network location string. /// It does not check if location actually exists. @@ -263,7 +292,7 @@ namespace Flow.Launcher.Plugin.SharedCommands } /// - /// Returns if contains . + /// Returns if contains . Equal paths are not considered to be contained by default. /// From https://stackoverflow.com/a/66877016 /// /// Parent path diff --git a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs index 6588132b9..a7744ffac 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; @@ -71,12 +71,6 @@ namespace Flow.Launcher.Plugin.SharedCommands } } - [Obsolete("This is provided for backwards compatibility after 1.9.0 release, e.g. GitHub plugin. Use the new method instead")] - public static void NewBrowserWindow(this string url, string browserPath = "") - { - OpenInBrowserWindow(url, browserPath); - } - /// /// Opens search as a tab in the default browser chosen in Windows settings. /// @@ -111,11 +105,5 @@ namespace Flow.Launcher.Plugin.SharedCommands }); } } - - [Obsolete("This is provided for backwards compatibility after 1.9.0 release, e.g. GitHub plugin. Use the new method instead")] - public static void NewTabInBrowser(this string url, string browserPath = "") - { - OpenInBrowserTab(url, browserPath); - } } } \ No newline at end of file diff --git a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs index c18f8b90c..a0440e30d 100644 --- a/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs +++ b/Flow.Launcher.Plugin/SharedCommands/ShellCommand.cs @@ -1,22 +1,16 @@ using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using System.Threading; -using System.Threading.Tasks; +using Windows.Win32; +using Windows.Win32.Foundation; namespace Flow.Launcher.Plugin.SharedCommands { public static class ShellCommand { 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; @@ -31,6 +25,7 @@ namespace Flow.Launcher.Plugin.SharedCommands CheckSecurityWindow(); Thread.Sleep(25); } + while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open { containsSecurityWindow = false; @@ -45,24 +40,33 @@ 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 = "", bool createNoWindow = false) + public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", + string arguments = "", string verb = "", bool createNoWindow = false) { var info = new ProcessStartInfo { diff --git a/Flow.Launcher.Test/FilesFoldersTest.cs b/Flow.Launcher.Test/FilesFoldersTest.cs index d16826053..2621fc2da 100644 --- a/Flow.Launcher.Test/FilesFoldersTest.cs +++ b/Flow.Launcher.Test/FilesFoldersTest.cs @@ -1,5 +1,6 @@ -using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; +using NUnit.Framework.Legacy; namespace Flow.Launcher.Test { @@ -33,21 +34,21 @@ namespace Flow.Launcher.Test [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)] - public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) - { - Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); - } - [TestCase(@"c:\foo", @"c:\foo", true)] [TestCase(@"c:\foo\", @"c:\foo", true)] [TestCase(@"c:\foo", @"c:\foo\", true)] - public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeTrue(string parentPath, string path, bool expectedResult) + public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) { - Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, true)); + 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 039947a9f..0241a374e 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -1,4 +1,4 @@ - + net7.0-windows10.0.19041.0 @@ -49,12 +49,12 @@ - - + + 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 d7f143218..090719642 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -3,6 +3,7 @@ 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,7 +128,7 @@ namespace Flow.Launcher.Test Debug.WriteLine("###############################################"); Debug.WriteLine(""); - Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); + ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); } } @@ -147,11 +150,11 @@ namespace Flow.Launcher.Test 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}"); } @@ -181,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); @@ -190,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}" + @@ -232,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); @@ -241,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}" + @@ -260,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); @@ -277,7 +280,7 @@ 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}" + @@ -293,7 +296,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); @@ -310,7 +313,7 @@ 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}" + @@ -323,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; @@ -336,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}" + @@ -358,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 index d6ba48f19..2cc05f95a 100644 --- a/Flow.Launcher.Test/PluginLoadTest.cs +++ b/Flow.Launcher.Test/PluginLoadTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; using System.Collections.Generic; @@ -15,37 +16,37 @@ namespace Flow.Launcher.Test // Given var duplicateList = new List { - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.1" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.2" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "ABC0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "ABC0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" @@ -56,11 +57,11 @@ namespace Flow.Launcher.Test (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); // Then - Assert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); - Assert.True(unique.Count() == 1); + ClassicAssert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); + ClassicAssert.True(unique.Count == 1); - Assert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); - Assert.True(duplicates.Count() == 6); + ClassicAssert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); + ClassicAssert.True(duplicates.Count == 6); } [Test] @@ -69,12 +70,12 @@ namespace Flow.Launcher.Test // Given var duplicateList = new List { - new PluginMetadata + new() { ID = "CEA0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" @@ -85,8 +86,8 @@ namespace Flow.Launcher.Test (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); // Then - Assert.True(unique.Count() == 0); - Assert.True(duplicates.Count() == 2); + 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 e9d37433f..420da266d 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -5,12 +5,10 @@ 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.IO; using System.Runtime.Versioning; -using System.Threading; -using System.Threading.Tasks; using static Flow.Launcher.Plugin.Explorer.Search.SearchManager; namespace Flow.Launcher.Test.Plugins @@ -22,28 +20,6 @@ namespace Flow.Launcher.Test.Plugins [TestFixture] public class ExplorerTest { -#pragma warning disable CS1998 // async method with no await (more readable to leave it async to match the tested signature) - private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken) - { - return new List(); - } -#pragma warning restore CS1998 - - 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; @@ -57,7 +33,7 @@ namespace Flow.Launcher.Test.Plugins var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath); // Then - Assert.IsTrue(result == expectedString, + ClassicAssert.IsTrue(result == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual: {result}{Environment.NewLine}"); } @@ -74,7 +50,7 @@ namespace Flow.Launcher.Test.Plugins var queryString = queryConstructor.Directory(folderPath); // Then - Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), + ClassicAssert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), $"Expected string: {expectedString}{Environment.NewLine} " + $"Actual string was: {queryString}{Environment.NewLine}"); } @@ -94,7 +70,7 @@ namespace Flow.Launcher.Test.Plugins var queryString = queryConstructor.Directory(folderPath, userSearchString); // Then - Assert.AreEqual(expectedString, queryString); + ClassicAssert.AreEqual(expectedString, queryString); } [SupportedOSPlatform("windows7.0")] @@ -105,7 +81,7 @@ namespace Flow.Launcher.Test.Plugins const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch; // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -128,7 +104,7 @@ namespace Flow.Launcher.Test.Plugins var resultString = queryConstructor.FilesAndFolders(userSearchString); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } @@ -138,13 +114,13 @@ namespace Flow.Launcher.Test.Plugins string querySearchString, string expectedString) { // Given - var queryConstructor = new QueryConstructor(new Settings()); + _ = new QueryConstructor(new Settings()); //When 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}"); } @@ -162,12 +138,12 @@ namespace Flow.Launcher.Test.Plugins 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 @@ -181,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}"); } @@ -191,18 +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}"); @@ -229,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}"); } @@ -242,7 +222,7 @@ 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}"); } @@ -256,7 +236,7 @@ namespace Flow.Launcher.Test.Plugins var resultString = QueryConstructor.RecursiveDirectoryConstraint(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -270,7 +250,7 @@ namespace Flow.Launcher.Test.Plugins var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")] @@ -301,7 +281,7 @@ namespace Flow.Launcher.Test.Plugins var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")] @@ -330,7 +310,7 @@ namespace Flow.Launcher.Test.Plugins var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")] @@ -362,7 +342,7 @@ namespace Flow.Launcher.Test.Plugins var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")] @@ -394,7 +374,7 @@ namespace Flow.Launcher.Test.Plugins var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase(@"c:\foo", @"c:\foo", true)] @@ -416,7 +396,7 @@ namespace Flow.Launcher.Test.Plugins }; // When, Then - Assert.AreEqual(expectedResult, comparator.Equals(result1, result2)); + ClassicAssert.AreEqual(expectedResult, comparator.Equals(result1, result2)); } [TestCase(@"c:\foo\", @"c:\foo\")] @@ -440,7 +420,7 @@ namespace Flow.Launcher.Test.Plugins var hash2 = comparator.GetHashCode(result2); // When, Then - Assert.IsTrue(hash1 == hash2); + ClassicAssert.IsTrue(hash1 == hash2); } [TestCase(@"%appdata%", true)] @@ -457,7 +437,7 @@ namespace Flow.Launcher.Test.Plugins var result = EnvironmentVariables.HasEnvironmentVar(path); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } } } diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index 765280e08..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 @@ -41,58 +39,27 @@ namespace Flow.Launcher.Test.Plugins Search = resultText }, default); - Assert.IsNotNull(results); + ClassicAssert.IsNotNull(results); foreach (var result in results) { - Assert.IsNotNull(result); - Assert.IsNotNull(result.AsyncAction); - 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 JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - - var pascalText = JsonSerializer.Serialize(reference); - - var results1 = await QueryAsync(new Query { Search = camelText }, default); - var results2 = await QueryAsync(new Query { Search = 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.AsyncAction); - } - } - } -} \ No newline at end of file +} 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 45ff8fc9e..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 1d403c5a1..e44b23232 100644 --- a/Flow.Launcher.sln +++ b/Flow.Launcher.sln @@ -2,15 +2,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # 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 {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} = {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} @@ -27,6 +18,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher", "Flow.Launc {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}" @@ -47,12 +47,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .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}" diff --git a/Flow.Launcher/ActionKeywords.xaml.cs b/Flow.Launcher/ActionKeywords.xaml.cs index 012c9ff4e..c3966e618 100644 --- a/Flow.Launcher/ActionKeywords.xaml.cs +++ b/Flow.Launcher/ActionKeywords.xaml.cs @@ -1,10 +1,8 @@ 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 Flow.Launcher.Core; namespace Flow.Launcher { @@ -37,6 +35,7 @@ namespace Flow.Launcher var oldActionKeyword = plugin.Metadata.ActionKeywords[0]; var newActionKeyword = tbAction.Text.Trim(); newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*"; + if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword)) { pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword); @@ -45,7 +44,7 @@ namespace Flow.Launcher else { string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned"); - MessageBox.Show(msg); + App.API.ShowMsgBox(msg); } } } diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index b8e2a1cfe..17c0ae0d5 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -20,11 +20,17 @@ + + + + + - + + diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 838eead1c..fa09f4fa7 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -3,8 +3,8 @@ 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; @@ -15,24 +15,85 @@ 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 Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher { public partial class App : IDisposable, ISingleInstanceApp { - public static PublicAPIInstance API { get; private set; } + public static IPublicAPI API { get; private set; } private const string Unique = "Flow.Launcher_Unique_Application_Mutex"; private static bool _disposed; - private Settings _settings; - private MainViewModel _mainVM; - private SettingWindowViewModel _settingsVM; - private readonly Updater _updater = new(); - private readonly Portable _portable = new Portable(); - private readonly PinyinAlphabet _alphabet = new PinyinAlphabet(); - private StringMatcher _stringMatcher; + private readonly Settings _settings; + + 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() + ).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(); + } + catch (Exception e) + { + ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); + return; + } + } + + private 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); + } [STAThread] public static void Main() @@ -51,49 +112,42 @@ namespace Flow.Launcher { await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => { - _portable.PreStartCleanUpAfterPortabilityUpdate(); + 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(); - - ImageLoader.Initialize(); - _settingsVM = new SettingWindowViewModel(_updater, _portable); - _settings = _settingsVM.Settings; + var imageLoadertask = ImageLoader.InitializeAsync(); AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); - _alphabet.Initialize(_settings); - _stringMatcher = new StringMatcher(_alphabet); - StringMatcher.Instance = _stringMatcher; - _stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision; + // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future + InternationalizationManager.Instance.ChangeLanguage(_settings.Language); PluginManager.LoadPlugins(_settings.PluginSettings); - _mainVM = new MainViewModel(_settings); - API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet); - - Http.API = API; Http.Proxy = _settings.Proxy; - await PluginManager.InitializePluginsAsync(API); - var window = new MainWindow(_settings, _mainVM); + await PluginManager.InitializePluginsAsync(); + await imageLoadertask; + + var mainVM = Ioc.Default.GetRequiredService(); + var window = new MainWindow(_settings, mainVM); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); Current.MainWindow = window; Current.MainWindow.Title = Constant.FlowLauncher; - HotKeyMapper.Initialize(_mainVM); + HotKeyMapper.Initialize(); - // 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 settings - ThemeManager.Instance.Settings = _settings; + // TODO: Clean ThemeManager.Instance in future ThemeManager.Instance.ChangeTheme(_settings.Theme); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -104,7 +158,8 @@ namespace Flow.Launcher AutoUpdates(); API.SaveAppAllSettings(); - Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); + Log.Info( + "|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); }); } @@ -116,14 +171,22 @@ namespace Flow.Launcher { try { - Helper.AutoStartup.Enable(); + 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; - Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message); + Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), + e.Message); } } } @@ -137,11 +200,11 @@ namespace Flow.Launcher { // check update every 5 hours var timer = new PeriodicTimer(TimeSpan.FromHours(5)); - await _updater.UpdateAppAsync(API); + await Ioc.Default.GetRequiredService().UpdateAppAsync(); while (await timer.WaitForNextTickAsync()) // check updates on startup - await _updater.UpdateAppAsync(API); + await Ioc.Default.GetRequiredService().UpdateAppAsync(); } }); } @@ -184,7 +247,7 @@ namespace Flow.Launcher public void OnSecondAppStarted() { - _mainVM.Show(); + Ioc.Default.GetRequiredService().Show(); } } } diff --git a/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs b/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs index 0bff23fe1..41e879913 100644 --- a/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs +++ b/Flow.Launcher/Converters/BoolToIMEConversionModeConverter.cs @@ -3,53 +3,38 @@ 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) - { - if (value is bool v) - { - if (v) - { - return ImeConversionModeValues.Alphanumeric; - } - else - { - return ImeConversionModeValues.DoNotCare; - } - } - return ImeConversionModeValues.DoNotCare; - } +namespace Flow.Launcher.Converters; - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) +internal class BoolToIMEConversionModeConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value switch { - throw new NotImplementedException(); - } + true => ImeConversionModeValues.Alphanumeric, + _ => ImeConversionModeValues.DoNotCare + }; } - internal class BoolToIMEStateConverter : IValueConverter + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value is bool v) - { - if (v) - { - return InputMethodState.Off; - } - else - { - return InputMethodState.DoNotCare; - } - } - return InputMethodState.DoNotCare; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } + 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 index ad474d693..c0fe6ab55 100644 --- a/Flow.Launcher/Converters/BoolToVisibilityConverter.cs +++ b/Flow.Launcher/Converters/BoolToVisibilityConverter.cs @@ -1,43 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Globalization; using System.Windows; -using System.Windows.Controls; using System.Windows.Data; -namespace Flow.Launcher.Converters +namespace Flow.Launcher.Converters; + +public class BoolToVisibilityConverter : IValueConverter { - public class BoolToVisibilityConverter : IValueConverter + public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture) { - public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture) + return (value, parameter) switch { - if (parameter != null) - { - if (value is true) - { - return Visibility.Collapsed; - } + (true, not null) => Visibility.Collapsed, + (_, not null) => Visibility.Visible, - else - { - return Visibility.Visible; - } - } - else { - if (value is true) - { - return Visibility.Visible; - } - - else { - return Visibility.Collapsed; - } - } - } - - public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); + (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 index 83e83f1de..9b0579c33 100644 --- a/Flow.Launcher/Converters/BorderClipConverter.cs +++ b/Flow.Launcher/Converters/BorderClipConverter.cs @@ -1,57 +1,47 @@ 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.Media; -using System.Windows.Documents; using System.Windows.Shapes; // For Clipping inside listbox item -namespace Flow.Launcher.Converters +namespace Flow.Launcher.Converters; + +public class BorderClipConverter : IMultiValueConverter { - public class BorderClipConverter : IMultiValueConverter + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + if (values is not [double width, double height, CornerRadius radius]) { - if (values.Length == 3 && values[0] is double && values[1] is double && values[2] is CornerRadius) - { - var width = (double)values[0]; - var height = (double)values[1]; - Path myPath = new Path(); - if (width < Double.Epsilon || height < Double.Epsilon) - { - return Geometry.Empty; - } - var radius = (CornerRadius)values[2]; - var radiusHeight = radius.TopLeft; - - // Drawing Round box for bottom round, and rect for top area of listbox. - var corner = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft); - var box = new RectangleGeometry(new Rect(0, 0, width, radiusHeight), 0, 0); - - GeometryGroup myGeometryGroup = new GeometryGroup(); - myGeometryGroup.Children.Add(corner); - myGeometryGroup.Children.Add(box); - - CombinedGeometry c1 = new CombinedGeometry(GeometryCombineMode.Union, corner, box); - myPath.Data = c1; - - myPath.Data.Freeze(); - return myPath.Data; - } - return DependencyProperty.UnsetValue; } - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + Path myPath = new Path(); + if (width < Double.Epsilon || height < Double.Epsilon) { - throw new NotSupportedException(); + return Geometry.Empty; } + var radiusHeight = radius.TopLeft; + + // Drawing Round box for bottom round, and rect for top area of listbox. + var corner = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft); + var box = new RectangleGeometry(new Rect(0, 0, width, radiusHeight), 0, 0); + + GeometryGroup myGeometryGroup = new GeometryGroup(); + myGeometryGroup.Children.Add(corner); + myGeometryGroup.Children.Add(box); + + CombinedGeometry c1 = new CombinedGeometry(GeometryCombineMode.Union, corner, box); + myPath.Data = c1; + + myPath.Data.Freeze(); + return myPath.Data; } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } } diff --git a/Flow.Launcher/Converters/DateTimeFormatToNowConverter.cs b/Flow.Launcher/Converters/DateTimeFormatToNowConverter.cs index 3c46fd01a..7d75ffd5c 100644 --- a/Flow.Launcher/Converters/DateTimeFormatToNowConverter.cs +++ b/Flow.Launcher/Converters/DateTimeFormatToNowConverter.cs @@ -2,18 +2,17 @@ using System.Globalization; using System.Windows.Data; -namespace Flow.Launcher.Converters -{ - public class DateTimeFormatToNowConverter : IValueConverter - { +namespace Flow.Launcher.Converters; - 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(); - } +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 index e81bb2507..24a316603 100644 --- a/Flow.Launcher/Converters/DiameterToCenterPointConverter.cs +++ b/Flow.Launcher/Converters/DiameterToCenterPointConverter.cs @@ -3,23 +3,22 @@ using System.Globalization; using System.Windows; using System.Windows.Data; -namespace Flow.Launcher.Converters +namespace Flow.Launcher.Converters; + +public class DiameterToCenterPointConverter : IValueConverter { - public class DiameterToCenterPointConverter : IValueConverter + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + if (value is double d) { - if (value is double d) - { - return new Point(d / 2, d / 2); - } - - return new Point(0, 0); + return new Point(d / 2, d / 2); } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } + 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 972dd1bc8..436f5fed5 100644 --- a/Flow.Launcher/Converters/HighlightTextConverter.cs +++ b/Flow.Launcher/Converters/HighlightTextConverter.cs @@ -1,56 +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.Media; 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 Run(currentCharacter) { Style = (Style)Application.Current.FindResource("HighlightStyle") }); - - } - 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 index 51129cfb8..d26b39d6b 100644 --- a/Flow.Launcher/Converters/IconRadiusConverter.cs +++ b/Flow.Launcher/Converters/IconRadiusConverter.cs @@ -1,27 +1,20 @@ using System; using System.Globalization; using System.Windows.Data; -using Windows.Devices.PointOfService; -namespace Flow.Launcher.Converters +namespace Flow.Launcher.Converters; + +public class IconRadiusConverter : IMultiValueConverter { - public class IconRadiusConverter : IMultiValueConverter + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) - { - if (values.Length != 2) - throw new ArgumentException("IconRadiusConverter must have 2 parameters"); + if (values is not [double size, bool isIconCircular]) + throw new ArgumentException("IconRadiusConverter must have 2 parameters: [double, bool]"); - return values[1] switch - { - true => (double)values[0] / 2, - false => (double)values[0], - _ => throw new ArgumentException("The second argument should be boolean", nameof(values)) - }; - } - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } + 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 7586d1fcf..7ab13190a 100644 --- a/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs +++ b/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs @@ -1,29 +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 = 10; + var number = int.MaxValue; - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - var number = int.MaxValue; + if (value is ListBoxItem listBoxItem + && ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox) + number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; - if (value is ListBoxItem listBoxItem - && ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox) - number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; - - return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); + 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 02b9bdbde..9aed2e07b 100644 --- a/Flow.Launcher/Converters/OrdinalConverter.cs +++ b/Flow.Launcher/Converters/OrdinalConverter.cs @@ -1,23 +1,24 @@ +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 - && ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox) - { - var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; - return res == 10 ? 0 : res; // 10th item => HOTKEY+0 - } +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 1e39473e0..ed94771f0 100644 --- a/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs +++ b/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs @@ -6,74 +6,62 @@ 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 != 3) - { - return string.Empty; - } - var QueryTextBox = values[0] as TextBox; + var selectedResult = selectedItem.Result; + var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " "; + var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title; - var queryText = (string)values[2]; - - 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); + // TODO: Obsolete warning? + var ft = new FormattedText(queryTextBox.Text, CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black); + + var offset = queryTextBox.Padding.Right; + + if (ft.Width + offset > queryTextBox.ActualWidth || queryTextBox.HorizontalOffset != 0) return string.Empty; - } - 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; - - - // For AutocompleteQueryCommand. - // When user typed lower case and result title is uppercase, we still want to display suggestion - selectedItem.QuerySuggestionText = queryText + selectedResultPossibleSuggestion.Substring(queryText.Length); - - // Check if Text will be larger then our QueryTextBox - System.Windows.Media.Typeface typeface = new Typeface(QueryTextBox.FontFamily, QueryTextBox.FontStyle, QueryTextBox.FontWeight, QueryTextBox.FontStretch); - // TODO: Obsolete warning? - System.Windows.Media.FormattedText ft = new FormattedText(QueryTextBox.Text, System.Globalization.CultureInfo.DefaultThreadCurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, QueryTextBox.FontSize, Brushes.Black); - - var offset = QueryTextBox.Padding.Right; - - if ((ft.Width + offset) > QueryTextBox.ActualWidth || QueryTextBox.HorizontalOffset != 0) - { - return string.Empty; - }; - - return selectedItem.QuerySuggestionText; - } - catch (Exception e) - { - Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e); - return string.Empty; - } + 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; } } + + 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 index 3675f06fc..21bf584e7 100644 --- a/Flow.Launcher/Converters/StringToKeyBindingConverter.cs +++ b/Flow.Launcher/Converters/StringToKeyBindingConverter.cs @@ -3,30 +3,27 @@ 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) - { - var mode = parameter as string; - var hotkeyStr = value as string; - var converter = new KeyGestureConverter(); - var key = (KeyGesture)converter.ConvertFromString(hotkeyStr); - if (mode == "key") - { - return key.Key; - } - else if (mode == "modifiers") - { - return key.Modifiers; - } - return null; - } +namespace Flow.Launcher.Converters; - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) +class StringToKeyBindingConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is not string mode || value is not string hotkeyStr) + return null; + + var converter = new KeyGestureConverter(); + var key = (KeyGesture)converter.ConvertFromString(hotkeyStr); + return mode switch { - throw new NotImplementedException(); - } + "key" => key?.Key, + "modifiers" => key?.Modifiers, + _ => null + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); } } diff --git a/Flow.Launcher/Converters/TextConverter.cs b/Flow.Launcher/Converters/TextConverter.cs index 90d445776..5f0e1ea82 100644 --- a/Flow.Launcher/Converters/TextConverter.cs +++ b/Flow.Launcher/Converters/TextConverter.cs @@ -4,29 +4,27 @@ using System.Windows.Data; using Flow.Launcher.Core.Resource; using Flow.Launcher.ViewModel; -namespace Flow.Launcher.Converters -{ - public class TextConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - var ID = value.ToString(); - switch(ID) - { - case PluginStoreItemViewModel.NewRelease: - return InternationalizationManager.Instance.GetTranslation("pluginStore_NewRelease"); - case PluginStoreItemViewModel.RecentlyUpdated: - return InternationalizationManager.Instance.GetTranslation("pluginStore_RecentlyUpdated"); - case PluginStoreItemViewModel.None: - return InternationalizationManager.Instance.GetTranslation("pluginStore_None"); - case PluginStoreItemViewModel.Installed: - return InternationalizationManager.Instance.GetTranslation("pluginStore_Installed"); - default: - return ID; - } - - } +namespace Flow.Launcher.Converters; - public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); +public class TextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var id = value?.ToString(); + var translationKey = id switch + { + PluginStoreItemViewModel.NewRelease => "pluginStore_NewRelease", + PluginStoreItemViewModel.RecentlyUpdated => "pluginStore_RecentlyUpdated", + PluginStoreItemViewModel.None => "pluginStore_None", + PluginStoreItemViewModel.Installed => "pluginStore_Installed", + _ => null + }; + + if (translationKey is null) + return id; + + return InternationalizationManager.Instance.GetTranslation(translationKey); } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); } diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml index 1113cb24d..70ebb404b 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml @@ -2,10 +2,12 @@ x:Class="Flow.Launcher.CustomQueryHotkeySetting" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:flowlauncher="clr-namespace:Flow.Launcher" Title="{DynamicResource customeQueryHotkeyTitle}" Width="530" Background="{DynamicResource PopuBGColor}" + DataContext="{Binding RelativeSource={RelativeSource Self}}" Foreground="{DynamicResource PopupTextColor}" Icon="Images\app.png" MouseDown="window_MouseDown" @@ -30,14 +32,11 @@ - - - + diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 3da66caeb..e1dfc1108 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -1,141 +1,213 @@ -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 Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Plugin; -using System.Threading; 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 IHotkeySettings HotkeySettings { + get { return (IHotkeySettings)GetValue(HotkeySettingsProperty); } + set { SetValue(HotkeySettingsProperty, value); } + } - public event EventHandler HotkeyChanged; + public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register( + nameof(HotkeySettings), + typeof(IHotkeySettings), + typeof(HotkeyControl), + new PropertyMetadata() + ); + public string WindowTitle { + get { return (string)GetValue(WindowTitleProperty); } + set { SetValue(WindowTitleProperty, value); } + } + + public static readonly DependencyProperty WindowTitleProperty = DependencyProperty.Register( + nameof(WindowTitle), + typeof(string), + typeof(HotkeyControl), + new PropertyMetadata(string.Empty) + ); /// /// Designed for Preview Hotkey and KeyGesture. /// - public bool ValidateKeyGesture { get; set; } = false; + public static readonly DependencyProperty ValidateKeyGestureProperty = DependencyProperty.Register( + nameof(ValidateKeyGesture), + typeof(bool), + typeof(HotkeyControl), + new PropertyMetadata(default(bool)) + ); - protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty); - - public HotkeyControl() + public bool ValidateKeyGesture { - InitializeComponent(); + get { return (bool)GetValue(ValidateKeyGestureProperty); } + set { SetValue(ValidateKeyGestureProperty, value); } } - private CancellationTokenSource hotkeyUpdateSource; + public static readonly DependencyProperty DefaultHotkeyProperty = DependencyProperty.Register( + nameof(DefaultHotkey), + typeof(string), + typeof(HotkeyControl), + new PropertyMetadata(default(string)) + ); - private void TbHotkey_OnPreviewKeyDown(object sender, KeyEventArgs e) + public string DefaultHotkey { - hotkeyUpdateSource?.Cancel(); - hotkeyUpdateSource?.Dispose(); - hotkeyUpdateSource = new(); - var token = hotkeyUpdateSource.Token; - e.Handled = true; + get { return (string)GetValue(DefaultHotkeyProperty); } + set { SetValue(DefaultHotkeyProperty, value); } + } - //when alt is pressed, the real key should be e.SystemKey - Key key = e.Key == Key.System ? e.SystemKey : e.Key; - - SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers(); - - var hotkeyModel = new HotkeyModel( - specialKeyState.AltPressed, - specialKeyState.ShiftPressed, - specialKeyState.WinPressed, - specialKeyState.CtrlPressed, - key); - - if (hotkeyModel.Equals(CurrentHotkey)) + private static void OnHotkeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not HotkeyControl hotkeyControl) { return; } - _ = Dispatcher.InvokeAsync(async () => - { - await Task.Delay(500, token); - if (!token.IsCancellationRequested) - await SetHotkeyAsync(hotkeyModel); - }); + hotkeyControl.SetKeysToDisplay(new HotkeyModel(hotkeyControl.Hotkey)); + hotkeyControl.CurrentHotkey = new HotkeyModel(hotkeyControl.Hotkey); } - public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true) - { - tbHotkey.Text = keyModel.ToString(); - tbHotkey.Select(tbHotkey.Text.Length, 0); + public static readonly DependencyProperty ChangeHotkeyProperty = DependencyProperty.Register( + nameof(ChangeHotkey), + typeof(ICommand), + typeof(HotkeyControl), + new PropertyMetadata(default(ICommand)) + ); + + public ICommand? ChangeHotkey + { + get { return (ICommand)GetValue(ChangeHotkeyProperty); } + set { SetValue(ChangeHotkeyProperty, value); } + } + + + public static readonly DependencyProperty HotkeyProperty = DependencyProperty.Register( + nameof(Hotkey), + typeof(string), + typeof(HotkeyControl), + new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHotkeyChanged) + ); + + public string Hotkey + { + get { return (string)GetValue(HotkeyProperty); } + set { SetValue(HotkeyProperty, value); } + } + + public HotkeyControl() + { + InitializeComponent(); + + HotkeyList.ItemsSource = KeysToDisplay; + SetKeysToDisplay(CurrentHotkey); + } + + private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => + hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); + + public string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none"); + + public ObservableCollection KeysToDisplay { get; set; } = new(); + + public HotkeyModel CurrentHotkey { get; private set; } = new(false, false, false, false, Key.None); + + + public void GetNewHotkey(object sender, RoutedEventArgs e) + { + OpenHotkeyDialog(); + } + + private async Task OpenHotkeyDialog() + { + if (!string.IsNullOrEmpty(Hotkey)) + { + HotKeyMapper.RemoveHotkey(Hotkey); + } + + var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, HotkeySettings, WindowTitle); + await dialog.ShowAsync(); + switch (dialog.ResultType) + { + case HotkeyControlDialog.EResultType.Cancel: + SetHotkey(Hotkey); + return; + case HotkeyControlDialog.EResultType.Save: + SetHotkey(dialog.ResultValue); + break; + case HotkeyControlDialog.EResultType.Delete: + Delete(); + break; + } + } + + + private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true) + { if (triggerValidate) { - bool hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture); - CurrentHotkeyAvailable = hotkeyAvailable; - SetMessage(hotkeyAvailable); - OnHotkeyChanged(); - - var token = hotkeyUpdateSource.Token; - await Task.Delay(500, token); - if (token.IsCancellationRequested) - return; - - if (CurrentHotkeyAvailable) + bool hotkeyAvailable = false; + // 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") { - CurrentHotkey = keyModel; - // To trigger LostFocus - FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); - Keyboard.ClearFocus(); + hotkeyAvailable = true; } + else + { + hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture); + } + + if (!hotkeyAvailable) + { + return; + } + + Hotkey = keyModel.ToString(); + SetKeysToDisplay(CurrentHotkey); + ChangeHotkey?.Execute(keyModel); } else { - CurrentHotkey = keyModel; + Hotkey = keyModel.ToString(); + ChangeHotkey?.Execute(keyModel); } } - - public Task SetHotkeyAsync(string keyStr, bool triggerValidate = true) + + public void Delete() { - return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate); + if (!string.IsNullOrEmpty(Hotkey)) + HotKeyMapper.RemoveHotkey(Hotkey); + Hotkey = ""; + SetKeysToDisplay(new HotkeyModel(false, false, false, false, Key.None)); } - private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); - - public new bool IsFocused => tbHotkey.IsFocused; - - private void tbHotkey_LostFocus(object sender, RoutedEventArgs e) + private void SetKeysToDisplay(HotkeyModel? hotkey) { - tbHotkey.Text = CurrentHotkey?.ToString() ?? ""; - tbHotkey.Select(tbHotkey.Text.Length, 0); - } + KeysToDisplay.Clear(); - private void tbHotkey_GotFocus(object sender, RoutedEventArgs e) - { - ResetMessage(); - } - - private void ResetMessage() - { - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("flowlauncherPressHotkey"); - tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B"); - } - - private void SetMessage(bool hotkeyAvailable) - { - if (!hotkeyAvailable) + if (hotkey == null || hotkey == default(HotkeyModel)) { - tbMsg.Foreground = new SolidColorBrush(Colors.Red); - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable"); + KeysToDisplay.Add(EmptyHotkey); + return; } - else + + foreach (var key in hotkey.Value.EnumerateDisplayKeys()!) { - tbMsg.Foreground = new SolidColorBrush(Colors.Green); - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success"); + KeysToDisplay.Add(key); } - tbMsg.Visibility = Visibility.Visible; + } + + public void SetHotkey(string? keyStr, bool triggerValidate = true) + { + SetHotkey(new HotkeyModel(keyStr), triggerValidate); } } } 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/ReportWindow.xaml.cs b/Flow.Launcher/ReportWindow.xaml.cs index 4899edc14..6fe90783e 100644 --- a/Flow.Launcher/ReportWindow.xaml.cs +++ b/Flow.Launcher/ReportWindow.xaml.cs @@ -1,6 +1,5 @@ using Flow.Launcher.Core.ExternalPlugins; using System; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; @@ -23,7 +22,7 @@ namespace Flow.Launcher SetException(exception); } - private static string GetIssueUrl(string website) + private static string GetIssuesUrl(string website) { if (!website.StartsWith("https://github.com")) { @@ -31,10 +30,10 @@ namespace Flow.Launcher } if(website.Contains("Flow-Launcher/Flow.Launcher")) { - return Constant.Issue; + return Constant.IssuesUrl; } var treeIndex = website.IndexOf("tree", StringComparison.Ordinal); - return treeIndex == -1 ? $"{website}/issues/new" : $"{website[..treeIndex]}/issues/new"; + return treeIndex == -1 ? $"{website}/issues" : $"{website[..treeIndex]}/issues"; } private void SetException(Exception exception) @@ -44,20 +43,21 @@ namespace Flow.Launcher var log = directory.GetFiles().OrderByDescending(f => f.LastWriteTime).First(); var websiteUrl = exception switch - { - FlowPluginException pluginException =>GetIssueUrl(pluginException.Metadata.Website), - _ => Constant.Issue - }; - + { + FlowPluginException pluginException =>GetIssuesUrl(pluginException.Metadata.Website), + _ => Constant.IssuesUrl + }; - var paragraph = Hyperlink("Please open new issue in: ", websiteUrl); - paragraph.Inlines.Add($"1. upload log file: {log.FullName}\n"); - paragraph.Inlines.Add($"2. copy below exception message"); + 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()); @@ -66,10 +66,12 @@ 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 { @@ -80,10 +82,16 @@ namespace Flow.Launcher 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(); + } } -} \ No newline at end of file +} diff --git a/Flow.Launcher/Resources/Controls/Card.xaml b/Flow.Launcher/Resources/Controls/Card.xaml new file mode 100644 index 000000000..c29a5f602 --- /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/InstalledPluginDisplay.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml new file mode 100644 index 000000000..ed3c29690 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs new file mode 100644 index 000000000..dfa03a204 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs @@ -0,0 +1,9 @@ +namespace Flow.Launcher.Resources.Controls; + +public partial class InstalledPluginDisplay +{ + public InstalledPluginDisplay() + { + InitializeComponent(); + } +} diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml new file mode 100644 index 000000000..83a771728 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml.cs b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml.cs new file mode 100644 index 000000000..f8d0afe61 --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayBottomData.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace Flow.Launcher.Resources.Controls; + +public partial class InstalledPluginDisplayBottomData : UserControl +{ + public InstalledPluginDisplayBottomData() + { + InitializeComponent(); + } +} diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml new file mode 100644 index 000000000..ff2f14c4b --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplayKeyword.xaml @@ -0,0 +1,47 @@ + + + + +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs new file mode 100644 index 000000000..de6a4df5e --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs @@ -0,0 +1,29 @@ +using System; +using System.Windows.Navigation; +using Flow.Launcher.SettingPages.ViewModels; + +namespace Flow.Launcher.SettingPages.Views; + +public partial class SettingsPaneAbout +{ + private SettingsPaneAboutViewModel _viewModel = null!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (!IsInitialized) + { + if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings, Updater: { } updater }) + throw new ArgumentException("Settings are required for SettingsPaneAbout."); + _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..814eda16b --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs new file mode 100644 index 000000000..db4763319 --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs @@ -0,0 +1,65 @@ +using System; +using System.ComponentModel; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Navigation; +using Flow.Launcher.SettingPages.ViewModels; +using Flow.Launcher.ViewModel; + +namespace Flow.Launcher.SettingPages.Views; + +public partial class SettingsPanePluginStore +{ + private SettingsPanePluginStoreViewModel _viewModel = null!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (!IsInitialized) + { + if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings }) + throw new ArgumentException($"Settings are required for {nameof(SettingsPanePluginStore)}."); + _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..37079a46f --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs new file mode 100644 index 000000000..d48505c3d --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs @@ -0,0 +1,30 @@ +using System; +using System.Windows.Input; +using System.Windows.Navigation; +using Flow.Launcher.SettingPages.ViewModels; + +namespace Flow.Launcher.SettingPages.Views; + +public partial class SettingsPanePlugins +{ + private SettingsPanePluginsViewModel _viewModel = null!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + if (!IsInitialized) + { + if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings }) + throw new ArgumentException("Settings are required for SettingsPaneHotkey."); + _viewModel = new SettingsPanePluginsViewModel(settings); + DataContext = _viewModel; + InitializeComponent(); + } + base.OnNavigatedTo(e); + } + + private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return; + PluginFilterTextbox.Focus(); + } +} diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml new file mode 100644 index 000000000..768abbf97 --- /dev/null +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + @@ -590,2651 +197,72 @@ Grid.Row="0" Width="50" Height="50" + RenderOptions.BitmapScalingMode="HighQuality" Source="images/app.png" /> + TextAlignment="Center" /> - - - - - - - - - - - -  - - - - + + - - - + + + + + - - - - - - - -  - - - + + + + + - - - - - - - -  - - - + + + + + - - - - - - - - + + + + + + + + + + - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - -  - - - - - - - - - - - - - -  - - - - - - - - - - - - - -  - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - - - + diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs index f39974142..f87194c30 100644 --- a/Flow.Launcher/SettingWindow.xaml.cs +++ b/Flow.Launcher/SettingWindow.xaml.cs @@ -1,526 +1,216 @@ -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.UserSettings; -using Flow.Launcher.Plugin; -using Flow.Launcher.ViewModel; -using ModernWpf; -using ModernWpf.Controls; -using System; -using System.Diagnostics; -using System.IO; -using System.Security.Policy; +using System; using System.Windows; -using System.Windows.Data; using System.Windows.Forms; using System.Windows.Input; using System.Windows.Interop; -using System.Windows.Navigation; -using Button = System.Windows.Controls.Button; -using Control = System.Windows.Controls.Control; -using KeyEventArgs = System.Windows.Input.KeyEventArgs; -using MessageBox = System.Windows.MessageBox; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Core; +using Flow.Launcher.Core.Configuration; +using Flow.Launcher.Helper; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.SettingPages.Views; +using Flow.Launcher.ViewModel; +using ModernWpf.Controls; using TextBox = System.Windows.Controls.TextBox; -using ThemeManager = ModernWpf.ThemeManager; -namespace Flow.Launcher +namespace Flow.Launcher; + +public partial class SettingWindow { - public partial class SettingWindow + private readonly Updater _updater; + private readonly IPortable _portable; + private readonly IPublicAPI _api; + private readonly Settings _settings; + private readonly SettingWindowViewModel _viewModel; + + public SettingWindow() { - public readonly IPublicAPI API; - private Settings settings; - private SettingWindowViewModel viewModel; + var viewModel = Ioc.Default.GetRequiredService(); + _settings = Ioc.Default.GetRequiredService(); + DataContext = viewModel; + _viewModel = viewModel; + _updater = Ioc.Default.GetRequiredService(); + _portable = Ioc.Default.GetRequiredService(); + _api = Ioc.Default.GetRequiredService(); + InitializePosition(); + InitializeComponent(); + } - public SettingWindow(IPublicAPI api, 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 + + 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) { - settings = viewModel.Settings; - DataContext = viewModel; - this.viewModel = viewModel; - API = api; - InitializePosition(); - InitializeComponent(); - + return; } + var tRequest = new TraversalRequest(FocusNavigationDirection.Next); + textBox.MoveFocus(tRequest); + } - #region General + /* Custom TitleBar */ - private void OnLoaded(object sender, RoutedEventArgs e) + private void OnMinimizeButtonClick(object sender, RoutedEventArgs e) + { + WindowState = WindowState.Minimized; + } + + private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e) + { + WindowState = WindowState switch { - RefreshMaximizeRestoreButton(); - // 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 + }; + } - pluginListView = (CollectionView)CollectionViewSource.GetDefaultView(Plugins.ItemsSource); - pluginListView.Filter = PluginListFilter; + private void OnCloseButtonClick(object sender, RoutedEventArgs e) + { + Close(); + } - pluginStoreView = (CollectionView)CollectionViewSource.GetDefaultView(StoreListBox.ItemsSource); - pluginStoreView.Filter = PluginStoreFilter; - - InitializePosition(); - } - - private void OnSelectPythonPathClick(object sender, RoutedEventArgs e) + private void RefreshMaximizeRestoreButton() + { + if (WindowState == WindowState.Maximized) { - var selectedFile = viewModel.GetFileFromDialog( - InternationalizationManager.Instance.GetTranslation("selectPythonExecutable"), - "Python|pythonw.exe"); - - if (!string.IsNullOrEmpty(selectedFile)) - settings.PluginSettings.PythonExecutablePath = selectedFile; + MaximizeButton.Visibility = Visibility.Collapsed; + RestoreButton.Visibility = Visibility.Visible; } - - private void OnSelectNodePathClick(object sender, RoutedEventArgs e) + else { - var selectedFile = viewModel.GetFileFromDialog( - InternationalizationManager.Instance.GetTranslation("selectNodeExecutable")); - - if (!string.IsNullOrEmpty(selectedFile)) - settings.PluginSettings.NodeExecutablePath = selectedFile; - } - - private void OnSelectFileManagerClick(object sender, RoutedEventArgs e) - { - SelectFileManagerWindow fileManagerChangeWindow = new SelectFileManagerWindow(settings); - fileManagerChangeWindow.ShowDialog(); - } - - private void OnSelectDefaultBrowserClick(object sender, RoutedEventArgs e) - { - var browserWindow = new SelectBrowserWindow(settings); - browserWindow.ShowDialog(); - } - - #endregion - - #region Hotkey - - private void OnHotkeyControlLoaded(object sender, RoutedEventArgs e) - { - _ = HotkeyControl.SetHotkeyAsync(viewModel.Settings.Hotkey, false); - } - - private void OnHotkeyControlFocused(object sender, RoutedEventArgs e) - { - HotKeyMapper.RemoveHotkey(settings.Hotkey); - } - - private void OnHotkeyControlFocusLost(object sender, RoutedEventArgs e) - { - if (HotkeyControl.CurrentHotkeyAvailable) - { - HotKeyMapper.SetHotkey(HotkeyControl.CurrentHotkey, HotKeyMapper.OnToggleHotkey); - HotKeyMapper.RemoveHotkey(settings.Hotkey); - settings.Hotkey = HotkeyControl.CurrentHotkey.ToString(); - } - else - { - HotKeyMapper.SetHotkey(new HotkeyModel(settings.Hotkey), HotKeyMapper.OnToggleHotkey); - } - } - - private void OnPreviewHotkeyControlLoaded(object sender, RoutedEventArgs e) - { - _ = PreviewHotkeyControl.SetHotkeyAsync(settings.PreviewHotkey, false); - } - - private void OnPreviewHotkeyControlFocusLost(object sender, RoutedEventArgs e) - { - if (PreviewHotkeyControl.CurrentHotkeyAvailable) - { - settings.PreviewHotkey = PreviewHotkeyControl.CurrentHotkey.ToString(); - } - } - - private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e) - { - var item = viewModel.SelectedCustomPluginHotkey; - if (item == null) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); - return; - } - - string deleteWarning = - string.Format(InternationalizationManager.Instance.GetTranslation("deleteCustomHotkeyWarning"), - item.Hotkey); - if ( - MessageBox.Show(deleteWarning, InternationalizationManager.Instance.GetTranslation("delete"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) - { - settings.CustomPluginHotkeys.Remove(item); - HotKeyMapper.RemoveHotkey(item.Hotkey); - } - } - - private void OnEditCustomHotkeyClick(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 OnAddCustomHotkeyClick(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, RoutedEventArgs e) - { - if (sender is Control { DataContext: PluginViewModel pluginViewModel }) - { - PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(pluginViewModel.PluginPair.Metadata.ID, pluginViewModel); - priorityChangeWindow.ShowDialog(); - } - } - - #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 void OnCheckUpdates(object sender, RoutedEventArgs e) - { - viewModel.UpdateApp(); // TODO: change to command - } - - private void OnRequestNavigate(object sender, RequestNavigateEventArgs e) - { - API.OpenUrl(e.Uri.AbsoluteUri); - e.Handled = true; - } - - private void OnClosed(object sender, EventArgs e) - { - settings.SettingWindowState = WindowState; - settings.SettingWindowTop = Top; - settings.SettingWindowLeft = Left; - viewModel.Save(); - } - - private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e) - { - Close(); - } - - private void OpenThemeFolder(object sender, RoutedEventArgs e) - { - PluginManager.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes)); - } - - private void OpenSettingFolder(object sender, RoutedEventArgs e) - { - PluginManager.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Settings)); - } - - private void OpenWelcomeWindow(object sender, RoutedEventArgs e) - { - var WelcomeWindow = new WelcomeWindow(settings); - WelcomeWindow.ShowDialog(); - } - private void OpenLogFolder(object sender, RoutedEventArgs e) - { - viewModel.OpenLogFolder(); - } - private void ClearLogFolder(object sender, RoutedEventArgs e) - { - var confirmResult = MessageBox.Show( - InternationalizationManager.Instance.GetTranslation("clearlogfolderMessage"), - InternationalizationManager.Instance.GetTranslation("clearlogfolder"), - MessageBoxButton.YesNo); - - if (confirmResult == MessageBoxResult.Yes) - { - viewModel.ClearLogFolder(); - } - } - - private void OnExternalPluginInstallClick(object sender, RoutedEventArgs e) - { - if (sender is not Button { DataContext: PluginStoreItemViewModel plugin } button) - { - return; - } - - if (storeClickedButton != null) - { - FlyoutService.GetFlyout(storeClickedButton).Hide(); - } - - viewModel.DisplayPluginQuery($"install {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); - } - - private void OnExternalPluginUninstallClick(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) - { - var name = viewModel.SelectedPlugin.PluginPair.Metadata.Name; - viewModel.DisplayPluginQuery($"uninstall {name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); - } - } - - private void OnExternalPluginUninstallClick(object sender, RoutedEventArgs e) - { - if (storeClickedButton != null) - { - FlyoutService.GetFlyout(storeClickedButton).Hide(); - } - - if (sender is Button { DataContext: PluginStoreItemViewModel plugin }) - viewModel.DisplayPluginQuery($"uninstall {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); - - } - - private void OnExternalPluginUpdateClick(object sender, RoutedEventArgs e) - { - if (storeClickedButton != null) - { - FlyoutService.GetFlyout(storeClickedButton).Hide(); - } - if (sender is Button { DataContext: PluginStoreItemViewModel plugin }) - viewModel.DisplayPluginQuery($"update {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); - - } - - private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */ - { - if (Keyboard.FocusedElement is not TextBox textBox) - { - return; - } - var tRequest = new TraversalRequest(FocusNavigationDirection.Next); - textBox.MoveFocus(tRequest); - } - - private void ColorSchemeSelectedIndexChanged(object sender, EventArgs e) - => ThemeManager.Current.ApplicationTheme = settings.ColorScheme switch - { - Constant.Light => ApplicationTheme.Light, - Constant.Dark => ApplicationTheme.Dark, - Constant.System => null, - _ => ThemeManager.Current.ApplicationTheme - }; - - /* Custom TitleBar */ - - private void OnMinimizeButtonClick(object sender, RoutedEventArgs e) - { - WindowState = WindowState.Minimized; - } - - private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e) - { - WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } - - private void OnCloseButtonClick(object sender, RoutedEventArgs e) - { - - Close(); - } - - private void RefreshMaximizeRestoreButton() - { - if (WindowState == WindowState.Maximized) - { - maximizeButton.Visibility = Visibility.Collapsed; - restoreButton.Visibility = Visibility.Visible; - } - else - { - maximizeButton.Visibility = Visibility.Visible; - restoreButton.Visibility = Visibility.Collapsed; - } - } - - private void Window_StateChanged(object sender, EventArgs e) - { - RefreshMaximizeRestoreButton(); - } - - #region Shortcut - - private void OnDeleteCustomShortCutClick(object sender, RoutedEventArgs e) - { - viewModel.DeleteSelectedCustomShortcut(); - } - - private void OnEditCustomShortCutClick(object sender, RoutedEventArgs e) - { - if (viewModel.EditSelectedCustomShortcut()) - { - customShortcutView.Items.Refresh(); - } - } - - private void OnAddCustomShortCutClick(object sender, RoutedEventArgs e) - { - viewModel.AddCustomShortcut(); - } - - #endregion - - private CollectionView pluginListView; - private CollectionView pluginStoreView; - - private bool PluginListFilter(object item) - { - if (string.IsNullOrEmpty(pluginFilterTxb.Text)) - return true; - if (item is PluginViewModel model) - { - return StringMatcher.FuzzySearch(pluginFilterTxb.Text, model.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet(); - } - return false; - } - - private bool PluginStoreFilter(object item) - { - if (string.IsNullOrEmpty(pluginStoreFilterTxb.Text)) - return true; - if (item is PluginStoreItemViewModel model) - { - return StringMatcher.FuzzySearch(pluginStoreFilterTxb.Text, model.Name).IsSearchPrecisionScoreMet() - || StringMatcher.FuzzySearch(pluginStoreFilterTxb.Text, model.Description).IsSearchPrecisionScoreMet(); - } - return false; - } - - private string lastPluginListSearch = ""; - private string lastPluginStoreSearch = ""; - - private void RefreshPluginListEventHandler(object sender, RoutedEventArgs e) - { - if (pluginFilterTxb.Text != lastPluginListSearch) - { - lastPluginListSearch = pluginFilterTxb.Text; - pluginListView.Refresh(); - } - } - - private void RefreshPluginStoreEventHandler(object sender, RoutedEventArgs e) - { - if (pluginStoreFilterTxb.Text != lastPluginStoreSearch) - { - lastPluginStoreSearch = pluginStoreFilterTxb.Text; - pluginStoreView.Refresh(); - } - } - - private void PluginFilterTxb_OnKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - RefreshPluginListEventHandler(sender, e); - } - - private void PluginStoreFilterTxb_OnKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - RefreshPluginStoreEventHandler(sender, e); - } - - private void OnPluginSettingKeydown(object sender, KeyEventArgs e) - { - if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && e.Key == Key.F) - pluginFilterTxb.Focus(); - } - - private void PluginStore_OnKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.F && (Keyboard.Modifiers & ModifierKeys.Control) != 0) - { - pluginStoreFilterTxb.Focus(); - } - } - - public void InitializePosition() - { - if (settings.SettingWindowTop >= 0 && settings.SettingWindowLeft >= 0) - { - Top = settings.SettingWindowTop; - Left = settings.SettingWindowLeft; - } - else - { - Top = WindowTop(); - Left = WindowLeft(); - } - WindowState = settings.SettingWindowState; - } - - public double WindowLeft() - { - var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); - var left = (dip2.X - this.ActualWidth) / 2 + dip1.X; - return left; - } - - public double WindowTop() - { - var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); - var top = (dip2.Y - this.ActualHeight) / 2 + dip1.Y - 20; - return top; - } - - private Button storeClickedButton; - - private void StoreListItem_Click(object sender, RoutedEventArgs e) - { - if (sender is not Button button) - return; - - storeClickedButton = button; - - var flyout = FlyoutService.GetFlyout(button); - flyout.Closed += (_, _) => - { - storeClickedButton = null; - }; - - } - - private void PluginStore_GotFocus(object sender, RoutedEventArgs e) - { - Keyboard.Focus(pluginStoreFilterTxb); - } - - private void Plugin_GotFocus(object sender, RoutedEventArgs e) - { - Keyboard.Focus(pluginFilterTxb); + MaximizeButton.Visibility = Visibility.Visible; + RestoreButton.Visibility = Visibility.Collapsed; } } + + 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)) + { + Top = WindowTop(); + Left = WindowLeft(); + } + else + { + 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) + { + return true; + } + } + return false; + } + + private double WindowLeft() + { + var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); + var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = WindowsInteropHelper.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 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var dip2 = WindowsInteropHelper.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) + { + var paneData = new PaneData(_settings, _updater, _portable); + if (args.IsSettingsSelected) + { + ContentFrame.Navigate(typeof(SettingsPaneGeneral), paneData); + } + else + { + var selectedItem = (NavigationViewItem)args.SelectedItem; + if (selectedItem == null) + { + NavView_Loaded(sender, null); /* Reset First Page */ + return; + } + + var pageType = selectedItem.Name switch + { + 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, paneData); + } + } + + 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 */ + } + + public record PaneData(Settings Settings, Updater Updater, IPortable Portable); } 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 2ba3a5929..b96cb661e 100644 --- a/Flow.Launcher/Themes/Base.xaml +++ b/Flow.Launcher/Themes/Base.xaml @@ -28,8 +28,8 @@ - - + + @@ -69,7 +69,7 @@ @@ -115,18 +117,20 @@ - + @@ -135,10 +139,11 @@ - + + @@ -188,7 +193,7 @@ @@ -312,7 +317,7 @@ x:Name="PART_VerticalScrollBar" Grid.Row="0" Grid.Column="0" - Margin="0,0,0,0" + Margin="0 0 0 0" HorizontalAlignment="Right" AutomationProperties.AutomationId="VerticalScrollBar" Cursor="Arrow" @@ -368,22 +373,7 @@ - + @@ -413,16 +403,30 @@ + @@ -495,7 +500,7 @@ @@ -503,7 +508,7 @@ x:Key="ClockPanelPosition" BasedOn="{StaticResource BaseClockPanelPosition}" TargetType="{x:Type Canvas}"> - + - \ No newline at end of file + diff --git a/Flow.Launcher/Themes/BlurWhite.xaml b/Flow.Launcher/Themes/BlurWhite.xaml index 4406724b8..a13f3bfcc 100644 --- a/Flow.Launcher/Themes/BlurWhite.xaml +++ b/Flow.Launcher/Themes/BlurWhite.xaml @@ -1,4 +1,9 @@ - + @@ -96,7 +101,7 @@ TargetType="{x:Type Rectangle}"> - + - - - - - - - - - - - - - #f1f1f1 - - - - - - - - - - 5 - 10 0 10 0 - 0 0 0 10 - - - - - - - - \ No newline at end of file diff --git a/Flow.Launcher/Themes/Circle Light.xaml b/Flow.Launcher/Themes/Circle Light.xaml deleted file mode 100644 index e52e3a957..000000000 --- a/Flow.Launcher/Themes/Circle Light.xaml +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - - - - - - - - - - - #5046e5 - - - - - - - - - - 8 - 10 0 10 0 - 0 0 0 10 - - - - - - - - diff --git a/Flow.Launcher/Themes/Circle System.xaml b/Flow.Launcher/Themes/Circle System.xaml index 2b2ce7ca3..343e7c5bc 100644 --- a/Flow.Launcher/Themes/Circle System.xaml +++ b/Flow.Launcher/Themes/Circle System.xaml @@ -1,3 +1,8 @@ + - + @@ -27,7 +32,7 @@ x:Key="QuerySuggestionBoxStyle" BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}" TargetType="{x:Type TextBox}"> - + @@ -72,7 +77,7 @@ TargetType="{x:Type Rectangle}"> - + @@ -150,7 +155,7 @@ x:Key="ClockPanel" BasedOn="{StaticResource ClockPanel}" TargetType="{x:Type StackPanel}"> - + - \ No newline at end of file + diff --git a/Flow.Launcher/Themes/Cyan Dark.xaml b/Flow.Launcher/Themes/Cyan Dark.xaml index 60bc09002..106b1b6d9 100644 --- a/Flow.Launcher/Themes/Cyan Dark.xaml +++ b/Flow.Launcher/Themes/Cyan Dark.xaml @@ -27,7 +27,7 @@ x:Key="ItemGlyph" BasedOn="{StaticResource BaseGlyphStyle}" TargetType="{x:Type TextBlock}"> - + - + \ No newline at end of file diff --git a/Flow.Launcher/Themes/Discord Dark.xaml b/Flow.Launcher/Themes/Discord Dark.xaml index 5315c7644..9e39ee5bd 100644 --- a/Flow.Launcher/Themes/Discord Dark.xaml +++ b/Flow.Launcher/Themes/Discord Dark.xaml @@ -1,4 +1,4 @@ - @@ -189,4 +189,4 @@ TargetType="{x:Type TextBlock}"> - + \ No newline at end of file diff --git a/Flow.Launcher/Themes/Dracula.xaml b/Flow.Launcher/Themes/Dracula.xaml index d150e7355..eb8cc9557 100644 --- a/Flow.Launcher/Themes/Dracula.xaml +++ b/Flow.Launcher/Themes/Dracula.xaml @@ -28,7 +28,6 @@ x:Key="QuerySuggestionBoxStyle" BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}" TargetType="{x:Type TextBox}"> - @@ -98,7 +97,7 @@ diff --git a/Flow.Launcher/Themes/Pink.xaml b/Flow.Launcher/Themes/Pink.xaml index dc97e4320..d6f9813d0 100644 --- a/Flow.Launcher/Themes/Pink.xaml +++ b/Flow.Launcher/Themes/Pink.xaml @@ -2,45 +2,46 @@ - 0 0 0 4 + 0 0 0 0 - #cc1081 + #0e172c \ No newline at end of file diff --git a/Flow.Launcher/Themes/SlimLight.xaml b/Flow.Launcher/Themes/SlimLight.xaml index dc08eec30..078b07048 100644 --- a/Flow.Launcher/Themes/SlimLight.xaml +++ b/Flow.Launcher/Themes/SlimLight.xaml @@ -179,15 +179,28 @@ BasedOn="{StaticResource BaseClockBox}" TargetType="{x:Type TextBlock}"> - + + + + + + + + - - - - - - - - - - - - - - - #ccd0d4 - - - - - - - - - - - - - - - diff --git a/Flow.Launcher/Themes/Win11System.xaml b/Flow.Launcher/Themes/Win10System.xaml similarity index 89% rename from Flow.Launcher/Themes/Win11System.xaml rename to Flow.Launcher/Themes/Win10System.xaml index 3025f9a07..4f8ea5532 100644 --- a/Flow.Launcher/Themes/Win11System.xaml +++ b/Flow.Launcher/Themes/Win10System.xaml @@ -1,3 +1,8 @@ + - + - - + + diff --git a/Flow.Launcher/Themes/Win11Dark.xaml b/Flow.Launcher/Themes/Win11Dark.xaml deleted file mode 100644 index 5abb96cce..000000000 --- a/Flow.Launcher/Themes/Win11Dark.xaml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - 0 0 0 8 - - - - - - - - - - - - - - - #198F8F8F - - - - - - - - - - - - - - - diff --git a/Flow.Launcher/Themes/Win11Light.xaml b/Flow.Launcher/Themes/Win11Light.xaml index e6f376f8b..4fe3c0495 100644 --- a/Flow.Launcher/Themes/Win11Light.xaml +++ b/Flow.Launcher/Themes/Win11Light.xaml @@ -1,72 +1,77 @@ + + - 0 0 0 8 + + - + + - + TargetType="{x:Type Window}" /> + - #198F8F8F - - - - + + + + + + + + 5 + 10 0 10 0 + 0 10 0 10 + diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 106970141..6b0144a03 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -19,12 +19,13 @@ using Microsoft.VisualStudio.Threading; using System.Text; using System.Threading.Channels; using ISavable = Flow.Launcher.Plugin.ISavable; -using System.IO; -using System.Collections.Specialized; using CommunityToolkit.Mvvm.Input; using System.Globalization; using System.Windows.Input; using System.ComponentModel; +using Flow.Launcher.Infrastructure.Image; +using System.Windows.Media; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.ViewModel { @@ -40,6 +41,7 @@ namespace Flow.Launcher.ViewModel private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; private readonly FlowLauncherJsonStorage _topMostRecordStorage; private readonly History _history; + private int lastHistoryIndex = 1; private readonly UserSelectedRecord _userSelectedRecord; private readonly TopMostRecord _topMostRecord; @@ -55,13 +57,13 @@ namespace Flow.Launcher.ViewModel #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) @@ -69,6 +71,21 @@ namespace Flow.Launcher.ViewModel 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; @@ -78,6 +95,42 @@ namespace Flow.Launcher.ViewModel 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; } }; @@ -91,15 +144,21 @@ namespace Flow.Launcher.ViewModel ContextMenu = new ResultsViewModel(Settings) { - LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview }; Results = new ResultsViewModel(Settings) { - LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview }; History = new ResultsViewModel(Settings) { - LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand + LeftClickResultCommand = OpenResultCommand, + RightClickResultCommand = LoadContextMenuCommand, + IsPreviewOn = Settings.AlwaysPreview }; _selectedResults = Results; @@ -173,8 +232,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"); } @@ -188,7 +251,8 @@ namespace Flow.Launcher.ViewModel Hide(); await PluginManager.ReloadDataAsync().ConfigureAwait(false); - Notification.Show(InternationalizationManager.Instance.GetTranslation("success"), InternationalizationManager.Instance.GetTranslation("completedSuccessfully")); + Notification.Show(InternationalizationManager.Instance.GetTranslation("success"), + InternationalizationManager.Instance.GetTranslation("completedSuccessfully")); } [RelayCommand] @@ -205,6 +269,47 @@ namespace Flow.Launcher.ViewModel } } + [RelayCommand] + public void ReQuery() + { + if (SelectedIsFromQueryResults()) + { + QueryResults(isReQuery: true); + } + } + + public void ReQuery(bool reselect) + { + BackToQueryResults(); + QueryResults(isReQuery: true, reSelect: reselect); + } + + [RelayCommand] + public void ReverseHistory() + { + if (_history.Items.Count > 0) + { + ChangeQueryText(_history.Items[_history.Items.Count - lastHistoryIndex].Query.ToString()); + if (lastHistoryIndex < _history.Items.Count) + { + lastHistoryIndex++; + } + } + } + + [RelayCommand] + public void ForwardHistory() + { + if (_history.Items.Count > 0) + { + ChangeQueryText(_history.Items[_history.Items.Count - lastHistoryIndex].Query.ToString()); + if (lastHistoryIndex > 1) + { + lastHistoryIndex--; + } + } + } + [RelayCommand] private void LoadContextMenu() { @@ -248,6 +353,13 @@ namespace Flow.Launcher.ViewModel } 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; } @@ -269,33 +381,58 @@ namespace Flow.Launcher.ViewModel { results.SelectedIndex = int.Parse(index); } + var result = results.SelectedItem?.Result; if (result == null) { return; } + var hideWindow = await result.ExecuteAsync(new ActionContext { - SpecialKeyState = GlobalHotkey.CheckModifiers() + // not null means pressing modifier key + number, should ignore the modifier key + SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() }) .ConfigureAwait(false); + if (SelectedIsFromQueryResults()) + { + _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(); } - - if (SelectedIsFromQueryResults()) - { - _userSelectedRecord.Add(result); - _history.Add(result.OriginQuery.RawQuery); - } - else - { - SelectedResults = Results; - } } + 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() { @@ -305,7 +442,7 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void SelectHelp() { - PluginManager.API.OpenUrl("https://www.flowlauncher.com/docs/#/usage-tips"); + App.API.OpenUrl("https://www.flowlauncher.com/docs/#/usage-tips"); } [RelayCommand] @@ -314,6 +451,13 @@ namespace Flow.Launcher.ViewModel SelectedResults.SelectFirstResult(); } + [RelayCommand] + private void SelectLastResult() + { + SelectedResults.SelectLastResult(); + } + + [RelayCommand] private void SelectPrevPage() { @@ -329,7 +473,18 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void SelectPrevItem() { - SelectedResults.SelectPrevResult(); + if (_history.Items.Count > 0 + && QueryText == string.Empty + && SelectedIsFromQueryResults()) + { + lastHistoryIndex = 1; + ReverseHistory(); + } + else + { + SelectedResults.SelectPrevResult(); + } + } [RelayCommand] @@ -351,12 +506,31 @@ namespace Flow.Launcher.ViewModel } } + public void BackToQueryResults() + { + if (!SelectedIsFromQueryResults()) + { + 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 @@ -364,7 +538,6 @@ namespace Flow.Launcher.ViewModel public Settings Settings { get; } public string ClockText { get; private set; } public string DateText { get; private set; } - public CultureInfo Culture => CultureInfo.DefaultThreadCurrentCulture; private async Task RegisterClockAndDateUpdateAsync() { @@ -373,9 +546,9 @@ namespace Flow.Launcher.ViewModel while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { if (Settings.UseClock) - ClockText = DateTime.Now.ToString(Settings.TimeFormat, Culture); + ClockText = DateTime.Now.ToString(Settings.TimeFormat, CultureInfo.CurrentCulture); if (Settings.UseDate) - DateText = DateTime.Now.ToString(Settings.DateFormat, Culture); + DateText = DateTime.Now.ToString(Settings.DateFormat, CultureInfo.CurrentCulture); } } @@ -388,6 +561,7 @@ namespace Flow.Launcher.ViewModel public bool GameModeStatus { get; set; } = false; private string _queryText; + public string QueryText { get => _queryText; @@ -402,16 +576,9 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void IncreaseWidth() { - if (MainWindowWidth + 100 > 1920 || Settings.WindowSize == 1920) - { - Settings.WindowSize = 1920; - } - else - { - Settings.WindowSize += 100; - Settings.WindowLeft -= 50; - } - OnPropertyChanged(); + Settings.WindowSize += 100; + Settings.WindowLeft -= 50; + OnPropertyChanged(nameof(MainWindowWidth)); } [RelayCommand] @@ -426,7 +593,8 @@ namespace Flow.Launcher.ViewModel Settings.WindowLeft += 50; Settings.WindowSize -= 100; } - OnPropertyChanged(); + + OnPropertyChanged(nameof(MainWindowWidth)); } [RelayCommand] @@ -447,76 +615,31 @@ namespace Flow.Launcher.ViewModel Settings.MaxResultsToShow -= 1; } - [RelayCommand] - public void TogglePreview() - { - if (!PreviewVisible) - { - ShowPreview(); - } - else - { - HidePreview(); - } - } - - private void ShowPreview() - { - ResultAreaColumn = 1; - PreviewVisible = true; - Results.SelectedItem?.LoadPreviewImage(); - } - - private void HidePreview() - { - ResultAreaColumn = 3; - PreviewVisible = false; - } - - public void ResetPreview() - { - if (Settings.AlwaysPreview == true) - { - ShowPreview(); - } - else - { - HidePreview(); - } - } - - private void UpdatePreview() - { - if (PreviewVisible) - { - Results.SelectedItem?.LoadPreviewImage(); - } - } - /// /// 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 /// /// - /// Force query even when Query Text doesn't change - public void ChangeQueryText(string queryText, bool reQuery = false) + /// Force query even when Query Text doesn't change + public void ChangeQueryText(string queryText, bool isReQuery = false) { Application.Current.Dispatcher.Invoke(() => { + BackToQueryResults(); + if (QueryText != queryText) { - // re-query is done in QueryText's setter method QueryText = queryText; // set to false so the subsequent set true triggers // PropertyChanged and MoveQueryTextToEnd is called QueryTextCursorMovedToEnd = false; - } - else if (reQuery) + else if (isReQuery) { - Query(); + Query(isReQuery: true); } + QueryTextCursorMovedToEnd = true; }); } @@ -533,12 +656,27 @@ namespace Flow.Launcher.ViewModel get { return _selectedResults; } set { + var isReturningFromContextMenu = ContextMenuSelected(); _selectedResults = value; if (SelectedIsFromQueryResults()) { ContextMenu.Visibility = Visibility.Collapsed; History.Visibility = Visibility.Collapsed; - ChangeQueryText(_queryTextBeforeLeaveResults); + + // 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); + } } else { @@ -572,54 +710,279 @@ namespace Flow.Launcher.ViewModel // because it is more accurate and reliable representation than using Visibility as a condition check public bool MainWindowVisibilityStatus { get; set; } = true; + public event VisibilityChangedEventHandler VisibilityChanged; + public Visibility SearchIconVisibility { get; set; } public double MainWindowWidth { get => Settings.WindowSize; - set => Settings.WindowSize = value; + 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; - public string PreviewHotkey - { - get + public string VerifyOrSetDefaultHotkey(string hotkey, string defaultHotkey) + { + try { - // TODO try to patch issue #1755 - // Added in v1.14.0, remove after v1.16.0. - try - { - var converter = new KeyGestureConverter(); - var key = (KeyGesture)converter.ConvertFromString(Settings.PreviewHotkey); - } - catch (Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException) - { - Settings.PreviewHotkey = "F1"; - } - return Settings.PreviewHotkey; + 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 string Image => Constant.QueryTextBoxIconImagePath; public bool StartWithEnglishMode => Settings.AlwaysStartEn; - public bool PreviewVisible { get; set; } = false; + #endregion - public int ResultAreaColumn { get; set; } = 1; + #region Preview + + 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"); +#endif + return false; + } + } + + private static readonly int ResultAreaColumnPreviewShown = 1; + + private static readonly int ResultAreaColumnPreviewHidden = 3; + + 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; set; } = false; + + private void ShowPreview() + { + 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(); + OpenExternalPreview(path); + break; + + case true + when !CanExternalPreviewSelectedResult(out var _): + if (ExternalPreviewVisible) + CloseExternalPreview(); + ShowInternalPreview(); + break; + + case false: + ShowInternalPreview(); + break; + } + } + + private void HidePreview() + { + if (PluginManager.UseExternalPreview()) + CloseExternalPreview(); + + if (InternalPreviewVisible) + HideInternalPreview(); + } + + [RelayCommand] + private void TogglePreview() + { + if (InternalPreviewVisible || ExternalPreviewVisible) + { + HidePreview(); + } + else + { + ShowPreview(); + } + } + + private void ToggleInternalPreview() + { + if (!InternalPreviewVisible) + { + ShowInternalPreview(); + } + else + { + HideInternalPreview(); + } + } + + private void OpenExternalPreview(string path, bool sendFailToast = true) + { + _ = PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false); + ExternalPreviewVisible = true; + } + + private void CloseExternalPreview() + { + _ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false); + ExternalPreviewVisible = false; + } + + private void SwitchExternalPreview(string path, bool sendFailToast = true) + { + _ = PluginManager.SwitchExternalPreviewAsync(path,sendFailToast).ConfigureAwait(false); + } + + private void ShowInternalPreview() + { + ResultAreaColumn = ResultAreaColumnPreviewShown; + Results.SelectedItem?.LoadPreviewImage(); + } + + private void HideInternalPreview() + { + ResultAreaColumn = ResultAreaColumnPreviewHidden; + } + + public void ResetPreview() + { + switch (Settings.AlwaysPreview) + { + case true + when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path): + OpenExternalPreview(path); + break; + + case true: + ShowInternalPreview(); + break; + + case false: + HidePreview(); + break; + } + } + + private void UpdatePreview() + { + switch (PluginManager.UseExternalPreview()) + { + case true + when CanExternalPreviewSelectedResult(out var path): + if (ExternalPreviewVisible) + { + SwitchExternalPreview(path, false); + } + else if (InternalPreviewVisible) + { + HideInternalPreview(); + OpenExternalPreview(path); + } + break; + + case true + when !CanExternalPreviewSelectedResult(out var _): + if (ExternalPreviewVisible) + { + CloseExternalPreview(); + ShowInternalPreview(); + } + break; + + case false + when InternalPreviewVisible: + Results.SelectedItem?.LoadPreviewImage(); + break; + } + } + + private bool CanExternalPreviewSelectedResult(out string path) + { + path = Results.SelectedItem?.Result?.Preview.FilePath; + return !string.IsNullOrEmpty(path); + } #endregion #region Query - public void Query() + public void Query(bool isReQuery = false) { if (SelectedIsFromQueryResults()) { - QueryResults(); + QueryResults(isReQuery); } else if (ContextMenuSelected()) { @@ -641,13 +1004,15 @@ namespace Flow.Launcher.ViewModel if (selected != null) // SelectedItem returns null if selection is empty. { - var results = PluginManager.GetContextMenusForPlugin(selected); + List results; + + results = PluginManager.GetContextMenusForPlugin(selected); results.Add(ContextMenuTopMost(selected)); results.Add(ContextMenuPluginInfo(selected.PluginID)); if (!string.IsNullOrEmpty(query)) { - var filtered = results.Where + var filtered = results.Select(x => x.Clone()).Where ( r => { @@ -661,7 +1026,6 @@ namespace Flow.Launcher.ViewModel r.Score = match.Score; return true; - }).ToList(); ContextMenu.AddResults(filtered, id); } @@ -688,10 +1052,7 @@ namespace Flow.Launcher.ViewModel Title = string.Format(title, h.Query), SubTitle = string.Format(time, h.ExecutedDateTime), IcoPath = "Images\\history.png", - OriginQuery = new Query - { - RawQuery = h.Query - }, + OriginQuery = new Query { RawQuery = h.Query }, Action = _ => { SelectedResults = Results; @@ -719,7 +1080,7 @@ namespace Flow.Launcher.ViewModel private readonly IReadOnlyList _emptyResult = new List(); - private async void QueryResults() + private async void QueryResults(bool isReQuery = false, bool reSelect = true) { _updateSource?.Cancel(); @@ -730,6 +1091,7 @@ namespace Flow.Launcher.ViewModel Results.Clear(); Results.Visibility = Visibility.Collapsed; PluginIconPath = null; + PluginIconSource = null; SearchIconVisibility = Visibility.Visible; return; } @@ -750,6 +1112,8 @@ namespace Flow.Launcher.ViewModel if (currentCancellationToken.IsCancellationRequested) return; + // 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); @@ -761,11 +1125,13 @@ namespace Flow.Launcher.ViewModel if (plugins.Count == 1) { PluginIconPath = plugins.Single().Metadata.IcoPath; + PluginIconSource = await ImageLoader.LoadAsync(PluginIconPath); SearchIconVisibility = Visibility.Hidden; } else { PluginIconPath = null; + PluginIconSource = null; SearchIconVisibility = Visibility.Visible; } @@ -792,7 +1158,7 @@ namespace Flow.Launcher.ViewModel var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch { - false => QueryTask(plugin), + false => QueryTask(plugin, reSelect), true => Task.CompletedTask }).ToArray(); @@ -820,26 +1186,38 @@ namespace Flow.Launcher.ViewModel } // Local function - async Task QueryTask(PluginPair plugin) + async Task QueryTask(PluginPair plugin, bool reSelect = true) { // 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, currentCancellationToken); currentCancellationToken.ThrowIfCancellationRequested(); - 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); + } - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, currentCancellationToken))) + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, + currentCancellationToken, reSelect))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); } } } - private Query ConstructQuery(string queryText, IEnumerable customShortcuts, IEnumerable builtInShortcuts) + private Query ConstructQuery(string queryText, IEnumerable customShortcuts, + IEnumerable builtInShortcuts) { if (string.IsNullOrWhiteSpace(queryText)) { @@ -849,7 +1227,8 @@ namespace Flow.Launcher.ViewModel StringBuilder queryBuilder = new(queryText); StringBuilder queryBuilderTmp = new(queryText); - foreach (var shortcut in customShortcuts) + // 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)) { @@ -876,7 +1255,9 @@ namespace Flow.Launcher.ViewModel } catch (Exception e) { - Log.Exception($"{nameof(MainViewModel)}.{nameof(ConstructQuery)}|Error when expanding shortcut {shortcut.Key}", e); + Log.Exception( + $"{nameof(MainViewModel)}.{nameof(ConstructQuery)}|Error when expanding shortcut {shortcut.Key}", + e); } } }); @@ -911,6 +1292,7 @@ namespace Flow.Launcher.ViewModel { _topMostRecord.Remove(result); App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success")); + App.API.ReQuery(); return false; } }; @@ -927,6 +1309,7 @@ namespace Flow.Launcher.ViewModel { _topMostRecord.AddOrUpdate(result); App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success")); + App.API.ReQuery(); return false; } }; @@ -1006,14 +1389,24 @@ namespace Flow.Launcher.ViewModel MainWindowOpacity = 1; MainWindowVisibilityStatus = true; + VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); }); } public async void Hide() { + lastHistoryIndex = 1; // Trick for no delay MainWindowOpacity = 0; + if (ExternalPreviewVisible) + CloseExternalPreview(); + + if (!SelectedIsFromQueryResults()) + { + SelectedResults = Results; + } + switch (Settings.LastQueryMode) { case LastQueryMode.Empty: @@ -1030,12 +1423,23 @@ namespace Flow.Launcher.ViewModel await Task.Delay(100); LastQuerySelected = false; break; + case LastQueryMode.ActionKeywordPreserved or LastQueryMode.ActionKeywordSelected: + var newQuery = _lastQuery.ActionKeyword; + if (!string.IsNullOrEmpty(newQuery)) + newQuery += " "; + ChangeQueryText(newQuery); + if (Settings.UseAnimation) + await Task.Delay(100); + if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected) + LastQuerySelected = false; + break; default: throw new ArgumentException($"wrong LastQueryMode: <{Settings.LastQueryMode}>"); } MainWindowVisibilityStatus = false; MainWindowVisibility = Visibility.Collapsed; + VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = false }); } /// @@ -1060,7 +1464,7 @@ namespace Flow.Launcher.ViewModel /// /// To avoid deadlock, this method should not called from main thread /// - public void UpdateResultView(IEnumerable resultsForUpdates) + public void UpdateResultView(ICollection resultsForUpdates) { if (!resultsForUpdates.Any()) return; @@ -1089,61 +1493,41 @@ namespace Flow.Launcher.ViewModel { 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; - } - } - } - - Results.AddResults(resultsForUpdates, token); - } - - /// - /// This is the global copy method for an individual result. If no text is passed, - /// the method will work out what is to be copied based on the result, so plugin can offer the text - /// to be copied via the result model. If the text is a directory/file path, - /// then actual file/folder will be copied instead. - /// The result's subtitle text is the default text to be copied - /// - public void ResultCopy(string stringToCopy) - { - if (string.IsNullOrEmpty(stringToCopy)) - { - var result = Results.SelectedItem?.Result; - if (result != null) - { - string copyText = result.CopyText; - var isFile = File.Exists(copyText); - var isFolder = Directory.Exists(copyText); - if (isFile || isFolder) - { - var paths = new StringCollection + if (result.AddSelectedCount) { - copyText - }; - - Clipboard.SetFileDropList(paths); - App.API.ShowMsg( - $"{App.API.GetTranslation("copy")} {(isFile ? App.API.GetTranslation("fileTitle") : App.API.GetTranslation("folderTitle"))}", - App.API.GetTranslation("completedSuccessfully")); - } - else - { - Clipboard.SetDataObject(copyText); - App.API.ShowMsg( - $"{App.API.GetTranslation("copy")} {App.API.GetTranslation("textTitle")}", - App.API.GetTranslation("completedSuccessfully")); + 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; + } + } } } - - return; } - Clipboard.SetDataObject(stringToCopy); + // it should be the same for all results + bool reSelect = resultsForUpdates.First().ReSelectFirstResult; + + Results.AddResults(resultsForUpdates, token, reSelect); } #endregion diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs index bc98efabc..38b5bec65 100644 --- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs @@ -1,12 +1,17 @@ using System; +using System.Linq; +using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; +using SemanticVersioning; +using Version = SemanticVersioning.Version; namespace Flow.Launcher.ViewModel { - public class PluginStoreItemViewModel : BaseModel + public partial class PluginStoreItemViewModel : BaseModel { + private PluginPair PluginManagerData => PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"); public PluginStoreItemViewModel(UserPlugin plugin) { _plugin = plugin; @@ -26,19 +31,13 @@ namespace Flow.Launcher.ViewModel public string IcoPath => _plugin.IcoPath; public bool LabelInstalled => PluginManager.GetPluginForId(_plugin.ID) != null; - public bool LabelUpdate => LabelInstalled && VersionConvertor(_plugin.Version) > VersionConvertor(PluginManager.GetPluginForId(_plugin.ID).Metadata.Version); + 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 Version VersionConvertor(string version) - { - Version ResultVersion = new Version(version); - return ResultVersion; - } - public string Category { get @@ -60,5 +59,13 @@ namespace Flow.Launcher.ViewModel 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 65ba657ba..209a81395 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Linq; using System.Windows; using System.Windows.Media; using Flow.Launcher.Plugin; @@ -7,6 +7,7 @@ using Flow.Launcher.Core.Plugin; using System.Windows.Controls; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Resource; +using Flow.Launcher.Resources.Controls; namespace Flow.Launcher.ViewModel { @@ -27,6 +28,21 @@ namespace Flow.Launcher.ViewModel } } + private 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 void LoadIconAsync() { @@ -47,7 +63,11 @@ namespace Flow.Launcher.ViewModel public bool PluginState { get => !PluginPair.Metadata.Disabled; - set => PluginPair.Metadata.Disabled = !value; + set + { + PluginPair.Metadata.Disabled = !value; + PluginSettingsObject.Disabled = !value; + } } public bool IsExpanded { @@ -63,12 +83,20 @@ namespace Flow.Launcher.ViewModel 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 - ??= PluginPair.Plugin is not ISettingProvider settingProvider - ? new Control() - : settingProvider.CreateSettingPanel() + ??= HasSettingControl + ? ((ISettingProvider)PluginPair.Plugin).CreateSettingPanel() + : null : null; private ImageSource _image = ImageLoader.MissingImage; @@ -79,6 +107,7 @@ namespace Flow.Launcher.ViewModel public string InitAndQueryTime => InternationalizationManager.Instance.GetTranslation("plugin_init_time") + " " + PluginPair.Metadata.InitTime + "ms, " + InternationalizationManager.Instance.GetTranslation("plugin_query_time") + " " + PluginPair.Metadata.AvgQueryTime + "ms"; public string ActionKeywordsText => string.Join(Query.ActionKeywordSeparator, PluginPair.Metadata.ActionKeywords); public int Priority => PluginPair.Metadata.Priority; + public Infrastructure.UserSettings.Plugin PluginSettingsObject { get; set; } public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword) { @@ -89,13 +118,14 @@ namespace Flow.Launcher.ViewModel public void ChangePriority(int newPriority) { PluginPair.Metadata.Priority = newPriority; + PluginSettingsObject.Priority = newPriority; OnPropertyChanged(nameof(Priority)); } [RelayCommand] private void EditPluginPriority() { - PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(PluginPair.Metadata.ID, this); + PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(PluginPair. Metadata.ID, this); priorityChangeWindow.ShowDialog(); } @@ -104,7 +134,20 @@ namespace Flow.Launcher.ViewModel { var directory = PluginPair.Metadata.PluginDirectory; if (!string.IsNullOrEmpty(directory)) - PluginManager.API.OpenDirectory(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(); } public static bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword); @@ -116,5 +159,4 @@ namespace Flow.Launcher.ViewModel changeKeywordsWindow.ShowDialog(); } } - } diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 3f204c16c..5130e7eba 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -29,7 +29,7 @@ namespace Flow.Launcher.ViewModel if (Result.Glyph is { FontFamily: not null } glyph) { - // Checks if it's a system installed font, which does not require path to be provided. + // 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; @@ -64,7 +64,7 @@ namespace Flow.Launcher.ViewModel } - private Settings Settings { get; } + public Settings Settings { get; } public Visibility ShowOpenResultHotkey => Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Collapsed; 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 bb07ce085..7d2b5bc93 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,4 +1,5 @@ -using Flow.Launcher.Infrastructure.UserSettings; +using System; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using System.Collections.Generic; using System.Collections.Specialized; @@ -32,9 +33,15 @@ namespace Flow.Launcher.ViewModel _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; } }; } @@ -43,14 +50,37 @@ namespace Flow.Launcher.ViewModel #region Properties - public double MaxHeight => MaxResults * (double)Application.Current.FindResource("ResultItemHeight")!; + 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 Visibility { get; set; } = Visibility.Collapsed; - + public ICommand RightClickResultCommand { get; init; } public ICommand LeftClickResultCommand { get; init; } @@ -117,6 +147,11 @@ namespace Flow.Launcher.ViewModel SelectedIndex = NewIndex(0); } + public void SelectLastResult() + { + SelectedIndex = NewIndex(Results.Count - 1); + } + public void Clear() { lock (_collectionLock) @@ -147,23 +182,23 @@ 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]; } @@ -193,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(); diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index e7298e12e..6ec799b82 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -1,1034 +1,46 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -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.ExternalPlugins; -using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; -using Flow.Launcher.Helper; -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using Flow.Launcher.Plugin.SharedModels; -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.Input; -using System.Globalization; -namespace Flow.Launcher.ViewModel +namespace Flow.Launcher.ViewModel; + +public partial class SettingWindowViewModel : BaseModel { - public partial class SettingWindowViewModel : BaseModel + private readonly Settings _settings; + + public SettingWindowViewModel(Settings settings) { - private readonly Updater _updater; - private readonly IPortable _portable; - private readonly FlowLauncherJsonStorage _storage; - - public SettingWindowViewModel(Updater updater, IPortable portable) - { - _updater = updater; - _portable = portable; - _storage = new FlowLauncherJsonStorage(); - Settings = _storage.Load(); - Settings.PropertyChanged += (s, e) => - { - switch (e.PropertyName) - { - case nameof(Settings.ActivateTimes): - OnPropertyChanged(nameof(ActivatedTimes)); - - break; - case nameof(Settings.WindowSize): - OnPropertyChanged(nameof(WindowWidthSize)); - - break; - case nameof(Settings.UseDate): - case nameof(Settings.DateFormat): - OnPropertyChanged(nameof(DateText)); - - break; - case nameof(Settings.UseClock): - case nameof(Settings.TimeFormat): - OnPropertyChanged(nameof(ClockText)); - - break; - case nameof(Settings.Language): - OnPropertyChanged(nameof(ClockText)); - OnPropertyChanged(nameof(DateText)); - OnPropertyChanged(nameof(AlwaysPreviewToolTip)); - - break; - case nameof(Settings.PreviewHotkey): - OnPropertyChanged(nameof(AlwaysPreviewToolTip)); - - break; - case nameof(Settings.PrereleaseUpdateSource): - _updater.UpdateToPrerelease = Settings.PrereleaseUpdateSource; - - break; - } - }; - - _updater.UpdateToPrerelease = Settings.PrereleaseUpdateSource; - - } - - public Settings Settings { get; set; } - - public async void UpdateApp() - { - await _updater.UpdateAppAsync(App.API, false); - } - - public bool AutoUpdates - { - get => Settings.AutoUpdates; - set - { - Settings.AutoUpdates = value; - - if (value) - { - UpdateApp(); - } - } - } - - public CultureInfo Culture => CultureInfo.DefaultThreadCurrentCulture; - - public bool StartFlowLauncherOnSystemStartup - { - get => Settings.StartFlowLauncherOnSystemStartup; - set - { - Settings.StartFlowLauncherOnSystemStartup = value; - - try - { - if (value) - AutoStartup.Enable(); - else - AutoStartup.Disable(); - } - catch (Exception e) - { - Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message); - } - } - } - - // 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 => _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(); - } - - public string GetFileFromDialog(string title, string filter = "") - { - var dlg = new System.Windows.Forms.OpenFileDialog - { - InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), - Multiselect = false, - CheckFileExists = true, - CheckPathExists = true, - Title = title, - Filter = filter - }; - - var result = dlg.ShowDialog(); - - if (result == System.Windows.Forms.DialogResult.OK) - { - return dlg.FileName; - } - else - { - return string.Empty; - } - } - - #region general - - // todo a better name? - public class LastQueryMode : BaseModel - { - public string Display { get; set; } - - public Infrastructure.UserSettings.LastQueryMode Value { get; set; } - } - - private List _lastQueryModes = new List(); - public List LastQueryModes - { - get - { - if (_lastQueryModes.Count == 0) - { - _lastQueryModes = InitLastQueryModes(); - } - - return _lastQueryModes; - } - } - - private List InitLastQueryModes() - { - var 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; - } - - private void UpdateLastQueryModeDisplay() - { - foreach (var item in LastQueryModes) - { - item.Display = _translater.GetTranslation($"LastQuery{item.Value}"); - } - } - - public string Language - { - get - { - return Settings.Language; - } - set - { - InternationalizationManager.Instance.ChangeLanguage(value); - - if (InternationalizationManager.Instance.PromptShouldUsePinyin(value)) - ShouldUsePinyin = true; - - UpdateLastQueryModeDisplay(); - } - } - - 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 AlwaysPreviewToolTip => string.Format(_translater.GetTranslation("AlwaysPreviewToolTip"), Settings.PreviewHotkey); - - 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 => PluginManager.AllPlugins - .OrderBy(x => x.Metadata.Disabled) - .ThenBy(y => y.Metadata.Name) - .Select(p => new PluginViewModel - { - PluginPair = p - }) - .ToList(); - } - - public IList ExternalPlugins - { - get - { - return LabelMaker(PluginsManifest.UserPlugins); - } - } - - private IList LabelMaker(IList list) - { - return list.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(); - } - - 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(); - } - } - } - - [RelayCommand] - private async Task RefreshExternalPluginsAsync() - { - await PluginsManifest.UpdateManifestAsync(); - OnPropertyChanged(nameof(ExternalPlugins)); - } - - - - internal void DisplayPluginQuery(string queryToDisplay, PluginPair plugin, int actionKeywordPosition = 0) - { - var actionKeyword = plugin.Metadata.ActionKeywords.Count == 0 - ? string.Empty - : plugin.Metadata.ActionKeywords[actionKeywordPosition]; - - App.API.ChangeQuery($"{actionKeyword} {queryToDisplay}"); - App.API.ShowMainWindow(); - } - - #endregion - - #region theme - - public static string Theme => @"https://flowlauncher.com/docs/#/how-to-create-a-theme"; - - public static string ThemeGallery => @"https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438"; - - public string SelectedTheme - { - get { return Settings.Theme; } - set - { - 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 class ColorScheme - { - public string Display { get; set; } - - public ColorSchemes Value { get; set; } - } - - public List ColorSchemes - { - get - { - List modes = new List(); - var enums = (ColorSchemes[])Enum.GetValues(typeof(ColorSchemes)); - - foreach (var e in enums) - { - var key = $"ColorScheme{e}"; - var display = _translater.GetTranslation(key); - var m = new ColorScheme - { - Display = display, Value = e, - }; - - modes.Add(m); - } - - return modes; - } - } - - public class SearchWindowScreen - { - public string Display { get; set; } - - public SearchWindowScreens Value { get; set; } - } - - public List SearchWindowScreens - { - get - { - List modes = new List(); - var enums = (SearchWindowScreens[])Enum.GetValues(typeof(SearchWindowScreens)); - - foreach (var e in enums) - { - var key = $"SearchWindowScreen{e}"; - var display = _translater.GetTranslation(key); - var m = new SearchWindowScreen - { - Display = display, Value = e, - }; - - modes.Add(m); - } - - return modes; - } - } - - public class SearchWindowAlign - { - public string Display { get; set; } - - public SearchWindowAligns Value { get; set; } - } - - public List SearchWindowAligns - { - get - { - List modes = new List(); - var enums = (SearchWindowAligns[])Enum.GetValues(typeof(SearchWindowAligns)); - - foreach (var e in enums) - { - var key = $"SearchWindowAlign{e}"; - var display = _translater.GetTranslation(key); - var m = new SearchWindowAlign - { - Display = display, Value = e, - }; - - modes.Add(m); - } - - return modes; - } - } - - public List ScreenNumbers - { - get - { - var screens = System.Windows.Forms.Screen.AllScreens; - var screenNumbers = new List(); - - for (int i = 1; i <= screens.Length; i++) - { - screenNumbers.Add(i); - } - - return screenNumbers; - } - } - - 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" - }; - - public string TimeFormat - { - get => Settings.TimeFormat; - set => Settings.TimeFormat = value; - } - - public string DateFormat - { - get => Settings.DateFormat; - set => Settings.DateFormat = value; - } - - public string ClockText => DateTime.Now.ToString(TimeFormat, Culture); - - public string DateText => DateTime.Now.ToString(DateFormat, Culture); - - public double WindowWidthSize - { - get => Settings.WindowSize; - set => Settings.WindowSize = value; - } - - public bool UseGlyphIcons - { - get => Settings.UseGlyphIcons; - set => Settings.UseGlyphIcons = value; - } - - public bool UseAnimation - { - get => Settings.UseAnimation; - set => Settings.UseAnimation = value; - } - - public bool UseSound - { - get => Settings.UseSound; - set => Settings.UseSound = value; - } - - public bool UseClock - { - get => Settings.UseClock; - set => Settings.UseClock = value; - } - - public bool UseDate - { - get => Settings.UseDate; - set => Settings.UseDate = value; - } - - public double SettingWindowWidth - { - get => Settings.SettingWindowWidth; - set => Settings.SettingWindowWidth = value; - } - - public double SettingWindowHeight - { - get => Settings.SettingWindowHeight; - set => Settings.SettingWindowHeight = value; - } - - public double SettingWindowTop - { - get => Settings.SettingWindowTop; - set => Settings.SettingWindowTop = value; - } - - public double SettingWindowLeft - { - get => Settings.SettingWindowLeft; - set => Settings.SettingWindowLeft = 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.DecodePixelWidth = 800; - bitmap.DecodePixelHeight = 600; - 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 = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png") - }, - new Result - { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png") - }, - new Result - { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"), - IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png") - }, - new Result - { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"), - SubTitle = InternationalizationManager.Instance.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 - { - 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 shortcut - - public ObservableCollection CustomShortcuts => Settings.CustomShortcuts; - - public ObservableCollection BuiltinShortcuts => Settings.BuiltinShortcuts; - - public CustomShortcutModel? SelectedCustomShortcut { get; set; } - - public void DeleteSelectedCustomShortcut() - { - var item = SelectedCustomShortcut; - - if (item == null) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); - - return; - } - - string deleteWarning = string.Format( - InternationalizationManager.Instance.GetTranslation("deleteCustomShortcutWarning"), - item.Key, - item.Value); - - if (MessageBox.Show(deleteWarning, - InternationalizationManager.Instance.GetTranslation("delete"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) - { - Settings.CustomShortcuts.Remove(item); - } - } - - public bool EditSelectedCustomShortcut() - { - var item = SelectedCustomShortcut; - - if (item == null) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem")); - - return false; - } - - var shortcutSettingWindow = new CustomShortcutSetting(item.Key, item.Value, this); - - if (shortcutSettingWindow.ShowDialog() == true) - { - // Fix un-selectable shortcut item after the first selection - // https://stackoverflow.com/questions/16789360/wpf-listbox-items-with-changing-hashcode - SelectedCustomShortcut = null; - item.Key = shortcutSettingWindow.Key; - item.Value = shortcutSettingWindow.Value; - SelectedCustomShortcut = item; - - return true; - } - - return false; - } - - public void AddCustomShortcut() - { - var shortcutSettingWindow = new CustomShortcutSetting(this); - - if (shortcutSettingWindow.ShowDialog() == true) - { - var shortcut = new CustomShortcutModel(shortcutSettingWindow.Key, shortcutSettingWindow.Value); - Settings.CustomShortcuts.Add(shortcut); - } - } - - public bool ShortcutExists(string key) - { - return Settings.CustomShortcuts.Any(x => x.Key == key) || Settings.BuiltinShortcuts.Any(x => x.Key == key); - } - - #endregion - - #region about - - 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 - { - get - { - if (Constant.Version == "1.0.0") - { - return Constant.Dev; - } - else - { - return Constant.Version; - } - } - } - - public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); - - public string CheckLogFolder - { - get - { - var logFiles = GetLogFiles(); - long size = logFiles.Sum(file => file.Length); - - return string.Format("{0} ({1})", _translater.GetTranslation("clearlogfolder"), BytesToReadableString(size)); - } - } - - private static DirectoryInfo GetLogDir(string version = "") - { - return new DirectoryInfo(Path.Combine(DataLocation.DataDirectory(), Constant.Logs, version)); - } - - private static List GetLogFiles(string version = "") - { - return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList(); - } - - internal void ClearLogFolder() - { - var logDirectory = GetLogDir(); - var logFiles = GetLogFiles(); - - logFiles.ForEach(f => f.Delete()); - - logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly) - .Where(dir => !Constant.Version.Equals(dir.Name)) - .ToList() - .ForEach(dir => dir.Delete()); - - OnPropertyChanged(nameof(CheckLogFolder)); - } - - internal void OpenLogFolder() - { - App.API.OpenDirectory(GetLogDir(Constant.Version).FullName); - } - - internal static string BytesToReadableString(long bytes) - { - const int scale = 1024; - string[] orders = new string[] - { - "GB", "MB", "KB", "B" - }; - - long max = (long)Math.Pow(scale, orders.Length - 1); - - foreach (string order in orders) - { - if (bytes > max) - return string.Format("{0:##.##} {1}", decimal.Divide(bytes, max), order); - - max /= scale; - } - - return "0 B"; - } - - #endregion + _settings = settings; + } + + /// + /// Save Flow settings. Plugins settings are not included. + /// + public void Save() + { + _settings.Save(); + } + + public double SettingWindowWidth + { + get => _settings.SettingWindowWidth; + set => _settings.SettingWindowWidth = value; + } + + public double SettingWindowHeight + { + get => _settings.SettingWindowHeight; + set => _settings.SettingWindowHeight = value; + } + + public double? SettingWindowTop + { + get => _settings.SettingWindowTop; + set => _settings.SettingWindowTop = value; + } + + public double? SettingWindowLeft + { + get => _settings.SettingWindowLeft; + set => _settings.SettingWindowLeft = value; } } diff --git a/Flow.Launcher/WelcomeWindow.xaml b/Flow.Launcher/WelcomeWindow.xaml index c7820d436..73f423d0b 100644 --- a/Flow.Launcher/WelcomeWindow.xaml +++ b/Flow.Launcher/WelcomeWindow.xaml @@ -10,6 +10,10 @@ Title="{DynamicResource Welcome_Page1_Title}" Width="550" Height="650" + MinWidth="550" + MinHeight="650" + MaxWidth="550" + MaxHeight="650" Activated="OnActivated" Background="{DynamicResource Color00B}" Foreground="{DynamicResource PopupTextColor}" @@ -32,8 +36,6 @@ - - - +public partial class CustomBrowserSettingWindow : Window { - /// - /// Interaction logic for CustomBrowserSetting.xaml - /// - public partial class CustomBrowserSettingWindow : Window + private CustomBrowser _currentCustomBrowser; + public CustomBrowserSettingWindow(CustomBrowser browser) { - private CustomBrowser currentCustomBrowser; - public CustomBrowserSettingWindow(CustomBrowser browser) + InitializeComponent(); + _currentCustomBrowser = browser; + DataContext = new CustomBrowser { - 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(); - } + Name = browser.Name, + DataDirectoryPath = browser.DataDirectoryPath, + BrowserType = browser.BrowserType, + }; + } - private void CancelEditCustomBrowser(object sender, RoutedEventArgs e) - { - Close(); - } + 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 WindowKeyDown(object sender, System.Windows.Input.KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - ConfirmEditCustomBrowser(sender, e); - } - } + private void CancelEditCustomBrowser(object sender, RoutedEventArgs e) + { + Close(); + } - private void OnSelectPathClick(object sender, RoutedEventArgs e) + private void WindowKeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == Key.Enter) { - var dialog = new FolderBrowserDialog(); - dialog.ShowDialog(); - CustomBrowser editBrowser = (CustomBrowser)DataContext; - editBrowser.DataDirectoryPath = dialog.SelectedPath; + 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.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs index 2947c2cb5..4bdab89a9 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs @@ -4,119 +4,116 @@ using System.Windows.Input; using System.ComponentModel; using System.Threading.Tasks; -namespace Flow.Launcher.Plugin.BrowserBookmark.Views +namespace Flow.Launcher.Plugin.BrowserBookmark.Views; + +public partial class SettingsControl : INotifyPropertyChanged { - public partial class SettingsControl : INotifyPropertyChanged + public Settings Settings { get; } + + public CustomBrowser SelectedCustomBrowser { get; set; } + + public bool LoadChromeBookmark { - public Settings Settings { get; } - - public CustomBrowser SelectedCustomBrowser { get; set; } - - public bool LoadChromeBookmark + get => Settings.LoadChromeBookmark; + set { - get => Settings.LoadChromeBookmark; - set + Settings.LoadChromeBookmark = value; + _ = Task.Run(() => Main.ReloadAllBookmarks()); + } + } + + public bool LoadFirefoxBookmark + { + get => Settings.LoadFirefoxBookmark; + set + { + Settings.LoadFirefoxBookmark = value; + _ = Task.Run(() => Main.ReloadAllBookmarks()); + } + } + + public bool LoadEdgeBookmark + { + get => Settings.LoadEdgeBookmark; + set + { + Settings.LoadEdgeBookmark = value; + _ = Task.Run(() => Main.ReloadAllBookmarks()); + } + } + + public bool OpenInNewBrowserWindow + { + get => Settings.OpenInNewBrowserWindow; + set + { + Settings.OpenInNewBrowserWindow = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OpenInNewBrowserWindow))); + } + } + + public SettingsControl(Settings settings) + { + Settings = settings; + InitializeComponent(); + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void NewCustomBrowser(object sender, RoutedEventArgs e) + { + var newBrowser = new CustomBrowser(); + var window = new CustomBrowserSettingWindow(newBrowser); + window.ShowDialog(); + if (newBrowser is not { - Settings.LoadChromeBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } - } - - public bool LoadFirefoxBookmark + Name: null, + DataDirectoryPath: null + }) { - get => Settings.LoadFirefoxBookmark; - set - { - Settings.LoadFirefoxBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } + Settings.CustomChromiumBrowsers.Add(newBrowser); + _ = Task.Run(() => Main.ReloadAllBookmarks()); } + } - public bool LoadEdgeBookmark + private void DeleteCustomBrowser(object sender, RoutedEventArgs e) + { + if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser) { - get => Settings.LoadEdgeBookmark; - set - { - Settings.LoadEdgeBookmark = value; - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } + Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser); + _ = Task.Run(() => Main.ReloadAllBookmarks()); } + } - public bool OpenInNewBrowserWindow + private void MouseDoubleClickOnSelectedCustomBrowser(object sender, MouseButtonEventArgs e) + { + EditSelectedCustomBrowser(); + } + + private void Others_Click(object sender, RoutedEventArgs e) + { + CustomBrowsersList.Visibility = CustomBrowsersList.Visibility switch { - get => Settings.OpenInNewBrowserWindow; - set - { - Settings.OpenInNewBrowserWindow = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OpenInNewBrowserWindow))); - } - } + Visibility.Collapsed => Visibility.Visible, + _ => Visibility.Collapsed + }; + } - public SettingsControl(Settings settings) + private void EditCustomBrowser(object sender, RoutedEventArgs e) + { + EditSelectedCustomBrowser(); + } + + private void EditSelectedCustomBrowser() + { + if (SelectedCustomBrowser is null) + return; + + var window = new CustomBrowserSettingWindow(SelectedCustomBrowser); + var result = window.ShowDialog() ?? false; + if (result) { - Settings = settings; - InitializeComponent(); - } - - public event PropertyChangedEventHandler PropertyChanged; - - private void NewCustomBrowser(object sender, RoutedEventArgs e) - { - var newBrowser = new CustomBrowser(); - var window = new CustomBrowserSettingWindow(newBrowser); - window.ShowDialog(); - if (newBrowser is not - { - Name: null, - DataDirectoryPath: null - }) - { - Settings.CustomChromiumBrowsers.Add(newBrowser); - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } - } - - private void DeleteCustomBrowser(object sender, RoutedEventArgs e) - { - if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser) - { - Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser); - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } - } - - private void MouseDoubleClickOnSelectedCustomBrowser(object sender, MouseButtonEventArgs e) - { - EditSelectedCustomBrowser(); - } - - private void Others_Click(object sender, RoutedEventArgs e) - { - - if (CustomBrowsersList.Visibility == Visibility.Collapsed) - { - CustomBrowsersList.Visibility = Visibility.Visible; - } - else - CustomBrowsersList.Visibility = Visibility.Collapsed; - } - - private void EditCustomBrowser(object sender, RoutedEventArgs e) - { - EditSelectedCustomBrowser(); - } - - private void EditSelectedCustomBrowser() - { - if (SelectedCustomBrowser is null) - return; - - var window = new CustomBrowserSettingWindow(SelectedCustomBrowser); - var result = window.ShowDialog() ?? false; - if (result) - { - _ = Task.Run(() => Main.ReloadAllBookmarks()); - } + _ = Task.Run(() => Main.ReloadAllBookmarks()); } } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json index 99f2b78b4..519141f6c 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json @@ -4,7 +4,7 @@ "Name": "Browser Bookmarks", "Description": "Search your browser bookmarks", "Author": "qianlifeng, Ioannis G.", - "Version": "3.1.0", + "Version": "3.3.4", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/DecimalSeparator.cs b/Plugins/Flow.Launcher.Plugin.Calculator/DecimalSeparator.cs index ac0da1c6f..81a68739b 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/DecimalSeparator.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/DecimalSeparator.cs @@ -1,8 +1,8 @@ using System.ComponentModel; using Flow.Launcher.Core.Resource; -namespace Flow.Launcher.Plugin.Caculator -{ +namespace Flow.Launcher.Plugin.Calculator +{ [TypeConverter(typeof(LocalizationConverter))] public enum DecimalSeparator { diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj index db731fb2c..1b985acf9 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj @@ -5,8 +5,8 @@ net7.0-windows {59BD9891-3837-438A-958D-ADC7F91F6F7E} Properties - Flow.Launcher.Plugin.Caculator - Flow.Launcher.Plugin.Caculator + Flow.Launcher.Plugin.Calculator + Flow.Launcher.Plugin.Calculator true true false @@ -18,7 +18,7 @@ true portable false - ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Caculator\ + ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Calculator\ DEBUG;TRACE prompt 4 @@ -28,7 +28,7 @@ pdbonly true - ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Caculator\ + ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Calculator\ TRACE prompt 4 @@ -62,7 +62,7 @@ - + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ar.xaml new file mode 100644 index 000000000..3219d647e --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ar.xaml @@ -0,0 +1,15 @@ + + + + آلة حاسبة + تمكنك من إجراء العمليات الحسابية. (جرب 5*3-2 في Flow Launcher) + ليست رقمًا (NaN) + التعبير خاطئ أو غير مكتمل (هل نسيت بعض الأقواس؟) + نسخ هذا الرقم إلى الحافظة + فاصل عشري + الفاصل العشري الذي سيتم استخدامه في الناتج. + استخدام إعدادات النظام المحلية + فاصلة (,) + نقطة (.) + أقصى عدد من المنازل العشرية + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/cs.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/cs.xaml new file mode 100644 index 000000000..ed8cb3fdb --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/cs.xaml @@ -0,0 +1,15 @@ + + + + Kalkulačka + Umožňuje provádět matematické výpočty.(Try 5*3-2 v průtokovém spouštěči) + Není číslo (NaN) + Nesprávný nebo neúplný výraz (Nezapomněli jste na závorky?) + Kopírování výsledku do schránky + Oddělovač desetinných míst + Oddělovač desetinných míst použitý ve výsledku. + Použít podle systému + Čárka (,) + Tečka (.) + Desetinná místa + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/de.xaml index 79936069c..d8da714dc 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/de.xaml @@ -2,14 +2,14 @@ Rechner - Stellt mathematische Berechnungen bereit.(Versuche 5*3-2 in Flow Launcher) - Keine Zahl (NaN) - Ausdruck falsch oder nicht vollständig (Klammern vergessen?) + Ermöglicht mathematische Berechnungen. (Versuchen Sie 5*3-2 in Flow Launcher) + Nicht eine Zahl (NaN) + Ausdruck falsch oder unvollständig (Haben Sie einige Klammern vergessen?) Diese Zahl in die Zwischenablage kopieren Dezimaltrennzeichen - Das Dezimaltrennzeichen, welches bei der Ausgabe verwendet werden soll. - Systemeinstellung nutzen + Das Dezimaltrennzeichen, das in der Ausgabe verwendet werden soll. + Systemgebietsschema verwenden Komma (,) Punkt (.) - Max. Nachkommastellen + Max. Dezimalstellen diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/fr.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/fr.xaml index 7dc4983de..c462ccc43 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/fr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/fr.xaml @@ -5,11 +5,11 @@ Permet de faire des calculs mathématiques.(Essayez 5*3-2 dans Flow Launcher) Pas un nombre (NaN) Expression incorrecte ou incomplète (avez-vous oublié certaines parenthèses ?) - Copy this number to the clipboard - Decimal separator - The decimal separator to be used in the output. - Use system locale - Comma (,) - Dot (.) - Max. decimal places + Copier ce chiffre dans le presse-papiers + Séparateur décimal + Le séparateur décimal à utiliser dans la sortie. + Utiliser les paramètres régionaux du système + Virgule (,) + Point (.) + Décimales max. diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/he.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/he.xaml new file mode 100644 index 000000000..15598118c --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/he.xaml @@ -0,0 +1,15 @@ + + + + Calculator + Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher) + Not a number (NaN) + Expression wrong or incomplete (Did you forget some parentheses?) + Copy this number to the clipboard + Decimal separator + The decimal separator to be used in the output. + Use system locale + Comma (,) + Dot (.) + Max. decimal places + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/it.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/it.xaml index 7809bcfa1..fa4651a97 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/it.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/it.xaml @@ -11,5 +11,5 @@ Usa il locale del sistema Virgola (,) Punto (.) - Max. decimal places + Max. cifre decimali diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/nb.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/nb.xaml index 15598118c..527df1d21 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/nb.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/nb.xaml @@ -1,15 +1,15 @@  - Calculator - Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher) - Not a number (NaN) - Expression wrong or incomplete (Did you forget some parentheses?) - Copy this number to the clipboard - Decimal separator - The decimal separator to be used in the output. - Use system locale - Comma (,) - Dot (.) - Max. decimal places + Kalkulator + Lar deg gjøre matematiske beregninger. (Prøv 5*3-2 i Flow Launcher) + Ikke et tall (NaN) + Uttrykk feil eller ufullstendig (glem noen parenteser?) + Kopier dette nummeret til utklippstavlen + Desimalskille + Desimalskilletegnet som skal brukes i utdataene. + Bruk systemets nasjonale innstilling + Komma (,) + Prikk (.) + Maks. desimaler diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/pl.xaml index aa00d1039..f697d2e6d 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/pl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/pl.xaml @@ -3,13 +3,13 @@ Kalkulator Szybkie wykonywanie obliczeń matematycznych. (Spróbuj wpisać 5*3-2 w oknie Flow Launchera) - Not a number (NaN) - Expression wrong or incomplete (Did you forget some parentheses?) - Copy this number to the clipboard - Decimal separator - The decimal separator to be used in the output. - Use system locale - Comma (,) - Dot (.) - Max. decimal places + Nie liczba (NaN) + Wyrażenie niepoprawne lub niekompletne (Czy zapomniałeś o nawiasach?) + Skopiuj ten numer do schowka + Separator dziesiętny + Separator dziesiętny używany w wyniku. + Użyj ustawień regionalnych systemu + Przecinek (,) + Kropka (.) + Maks. liczba miejsc po przecinku diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/pt-br.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/pt-br.xaml index 15598118c..6dd903fa4 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/pt-br.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/pt-br.xaml @@ -1,15 +1,15 @@  - Calculator - Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher) - Not a number (NaN) - Expression wrong or incomplete (Did you forget some parentheses?) - Copy this number to the clipboard - Decimal separator - The decimal separator to be used in the output. + Calculadora + Permite fazer cálculos matemáticos.(Tente 5*3-2 no Flow Launcher) + Não é um número (NaN) + Expressão errada ou incompleta (Você esqueceu de adicionar parênteses?) + Copiar este numero para a área de transferência + Separador decimal + O separador decimal a ser usado no resultado. Use system locale - Comma (,) - Dot (.) + Vírgula (,) + Ponto (.) Max. decimal places diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ru.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ru.xaml index 15598118c..30c557536 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ru.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/ru.xaml @@ -1,15 +1,15 @@  - Calculator - Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher) - Not a number (NaN) - Expression wrong or incomplete (Did you forget some parentheses?) - Copy this number to the clipboard - Decimal separator - The decimal separator to be used in the output. - Use system locale - Comma (,) - Dot (.) - Max. decimal places + Калькулятор + Позволяет выполнять математические вычисления. (Попробуйте 5*3-2 в Flow Launcher) + Не является числом (NaN) + Выражение неправильное или неполное (Вы забыли скобки?) + Скопировать этот номер в буфер обмена + Десятичный разделитель + Десятичный разделитель, который будет использоваться в результате. + Использовать системный язык + Запятая (,) + Точка (.) + Макс. число знаков после запятой diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/tr.xaml index 07a287d52..345c81433 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/tr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/tr.xaml @@ -6,10 +6,10 @@ Sayı değil (NaN) İfade hatalı ya da eksik. (Parantez koymayı mı unuttunuz?) Bu sayıyı panoya kopyala - Decimal separator - The decimal separator to be used in the output. - Use system locale - Comma (,) - Dot (.) - Max. decimal places + Ondalık ayracı + Ondalık kısımları ayırmak için kullanılacak işaret. + Sistem yerelleştirme ayarını kullan + Virgül (,) + Nokta (.) + Maks. ondalık basamak diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/uk-UA.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/uk-UA.xaml index 15598118c..014755929 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/uk-UA.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/uk-UA.xaml @@ -1,15 +1,15 @@  - Calculator - Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher) - Not a number (NaN) - Expression wrong or incomplete (Did you forget some parentheses?) - Copy this number to the clipboard - Decimal separator - The decimal separator to be used in the output. - Use system locale - Comma (,) - Dot (.) - Max. decimal places + Калькулятор + Дозволяє виконувати математичні обчислення (спробуйте 5*3-2 у Flow Launcher) + Не є числом (NaN) + Вираз неправильний або неповний (Ви забули якісь дужки?) + Скопіюйте це число в буфер обміну + Десятковий роздільник + Десятковий роздільник, який буде використовуватися у виведенні. + Використовувати системну локаль + Кома (,) + Крапка (.) + Макс. кількість знаків після коми diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/vi.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/vi.xaml new file mode 100644 index 000000000..82ebbbe93 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/vi.xaml @@ -0,0 +1,15 @@ + + + + Máy tính + Cho phép thực hiện các phép tính toán học. (Thử 5*3-2 trong Flow Launcher) + Không phải là số (NaN) + Biểu thức sai hoặc không đầy đủ (Bạn có quên một số dấu ngoặc đơn không?) + Sao chép số này vào clipboard + Dấu tách thập phân + Dấu phân cách thập phân được sử dụng ở đầu ra. + Sử dụng ngôn ngữ hệ thống + Dấu phẩy (,) + dấu chấm (.) + Tối đa. chữ số thập phân + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index d5dcdacea..7f42eacf6 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -3,13 +3,12 @@ using System.Collections.Generic; using System.Globalization; using System.Runtime.InteropServices; using System.Text.RegularExpressions; -using System.Windows; using System.Windows.Controls; using Mages.Core; -using Flow.Launcher.Plugin.Caculator.ViewModels; -using Flow.Launcher.Plugin.Caculator.Views; +using Flow.Launcher.Plugin.Calculator.ViewModels; +using Flow.Launcher.Plugin.Calculator.Views; -namespace Flow.Launcher.Plugin.Caculator +namespace Flow.Launcher.Plugin.Calculator { public class Main : IPlugin, IPluginI18n, ISettingProvider { @@ -19,8 +18,9 @@ namespace Flow.Launcher.Plugin.Caculator @"sin|cos|tan|arcsin|arccos|arctan|" + @"eigval|eigvec|eig|sum|polar|plot|round|sort|real|zeta|" + @"bin2dec|hex2dec|oct2dec|" + - @"==|~=|&&|\|\||" + - @"[ei]|[0-9]|[\+\-\*\/\^\., ""]|[\(\)\|\!\[\]]" + + @"factorial|sign|isprime|isinfty|" + + @"==|~=|&&|\|\||(?:\<|\>)=?|" + + @"[ei]|[0-9]|[\+\%\-\*\/\^\., ""]|[\(\)\|\!\[\]]" + @")+$", RegexOptions.Compiled); private static readonly Regex RegBrackets = new Regex(@"[\(\)\[\]]", RegexOptions.Compiled); private static Engine MagesEngine; @@ -61,7 +61,7 @@ namespace Flow.Launcher.Plugin.Caculator switch (_settings.DecimalSeparator) { case DecimalSeparator.Comma: - case DecimalSeparator.UseSystemLocale when CultureInfo.DefaultThreadCurrentCulture.NumberFormat.NumberDecimalSeparator == ",": + case DecimalSeparator.UseSystemLocale when CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator == ",": expression = query.Search.Replace(",", "."); break; default: @@ -95,12 +95,12 @@ namespace Flow.Launcher.Plugin.Caculator { try { - Clipboard.SetDataObject(newResult); + Context.API.CopyToClipboard(newResult); return true; } catch (ExternalException) { - MessageBox.Show("Copy failed, please try later"); + Context.API.ShowMsgBox("Copy failed, please try later"); return false; } } @@ -157,7 +157,7 @@ namespace Flow.Launcher.Plugin.Caculator private string GetDecimalSeparator() { - string systemDecimalSeperator = CultureInfo.DefaultThreadCurrentCulture.NumberFormat.NumberDecimalSeparator; + string systemDecimalSeperator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; switch (_settings.DecimalSeparator) { case DecimalSeparator.UseSystemLocale: return systemDecimalSeperator; diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs b/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs index 528b21e4d..4eacb9d34 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -namespace Flow.Launcher.Plugin.Caculator +namespace Flow.Launcher.Plugin.Calculator { /// /// Tries to convert all numbers in a text from one culture format to another. diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs index 615514873..8354863b8 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs @@ -1,5 +1,5 @@  -namespace Flow.Launcher.Plugin.Caculator +namespace Flow.Launcher.Plugin.Calculator { public class Settings { diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs index 9ee95e840..09f745669 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Flow.Launcher.Infrastructure.Storage; -using Flow.Launcher.Infrastructure.UserSettings; -namespace Flow.Launcher.Plugin.Caculator.ViewModels +namespace Flow.Launcher.Plugin.Calculator.ViewModels { public class SettingsViewModel : BaseModel { diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml index 9fd4bb17c..d6237c6da 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml @@ -1,13 +1,13 @@ /// Interaction logic for CalculatorSettings.xaml diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json index 5f27a088d..99e185928 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json @@ -4,9 +4,9 @@ "Name": "Calculator", "Description": "Provide mathematical calculations.(Try 5*3-2 in Flow Launcher)", "Author": "cxfksword", - "Version": "3.0.1", + "Version": "3.1.5", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", - "ExecuteFileName": "Flow.Launcher.Plugin.Caculator.dll", + "ExecuteFileName": "Flow.Launcher.Plugin.Calculator.dll", "IcoPath": "Images\\calculator.png" } \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs index f5733bbb5..3f3b7cb58 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs @@ -9,12 +9,7 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using System.Linq; -using System.Windows.Controls; -using System.Windows.Input; -using MessageBox = System.Windows.Forms.MessageBox; -using MessageBoxIcon = System.Windows.Forms.MessageBoxIcon; -using MessageBoxButton = System.Windows.Forms.MessageBoxButtons; -using DialogResult = System.Windows.Forms.DialogResult; +using Flow.Launcher.Plugin.Explorer.Helper; using Flow.Launcher.Plugin.Explorer.ViewModels; namespace Flow.Launcher.Plugin.Explorer @@ -56,6 +51,11 @@ namespace Flow.Launcher.Plugin.Explorer contextMenus.Add(CreateOpenContainingFolderResult(record)); + if (record.Type == ResultType.File) + { + contextMenus.Add(CreateOpenWithMenu(record)); + } + if (record.WindowsIndexed) { contextMenus.Add(CreateOpenWindowsIndexingOptions()); @@ -124,7 +124,7 @@ namespace Flow.Launcher.Plugin.Explorer { try { - Clipboard.SetText(record.FullPath); + Context.API.CopyToClipboard(record.FullPath); return true; } catch (Exception e) @@ -147,10 +147,7 @@ namespace Flow.Launcher.Plugin.Explorer { try { - Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection - { - record.FullPath - }); + Context.API.CopyToClipboard(record.FullPath, directCopy: true); return true; } catch (Exception e) @@ -176,12 +173,12 @@ namespace Flow.Launcher.Plugin.Explorer { try { - if (MessageBox.Show( + if (Context.API.ShowMsgBox( string.Format(Context.API.GetTranslation("plugin_explorer_delete_folder_link"), record.FullPath), string.Empty, MessageBoxButton.YesNo, - MessageBoxIcon.Warning) - == DialogResult.No) + MessageBoxImage.Warning) + == MessageBoxResult.No) return false; if (isFile) @@ -222,34 +219,7 @@ namespace Flow.Launcher.Plugin.Explorer if (record.Type is ResultType.Volume) return false; - var screenWithMouseCursor = System.Windows.Forms.Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var xOfScreenCenter = screenWithMouseCursor.WorkingArea.Left + screenWithMouseCursor.WorkingArea.Width / 2; - var yOfScreenCenter = screenWithMouseCursor.WorkingArea.Top + screenWithMouseCursor.WorkingArea.Height / 2; - var showPosition = new System.Drawing.Point(xOfScreenCenter, yOfScreenCenter); - - switch (record.Type) - { - case ResultType.File: - { - var fileInfos = new FileInfo[] - { - new(record.FullPath) - }; - - new Peter.ShellContextMenu().ShowContextMenu(fileInfos, showPosition); - break; - } - case ResultType.Folder: - { - var directoryInfos = new DirectoryInfo[] - { - new(record.FullPath) - }; - - new Peter.ShellContextMenu().ShowContextMenu(directoryInfos, showPosition); - break; - } - } + ResultManager.ShowNativeContextMenu(record.FullPath, record.Type); return false; }, @@ -272,12 +242,53 @@ namespace Flow.Launcher.Plugin.Explorer var name = "Plugin: Folder"; var message = $"File not found: {e.Message}"; Context.API.ShowMsgError(name, message); + return false; } return true; }, - IcoPath = Constants.DifferentUserIconImagePath + IcoPath = Constants.DifferentUserIconImagePath, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue748"), }); + + if (record.Type is ResultType.File or ResultType.Folder && Settings.ShowInlinedWindowsContextMenu) + { + var includedItems = Settings + .WindowsContextMenuIncludedItems + .Replace("\r", "") + .Split("\n") + .Where(v => !string.IsNullOrWhiteSpace(v)) + .ToArray(); + var excludedItems = Settings + .WindowsContextMenuExcludedItems + .Replace("\r", "") + .Split("\n") + .Where(v => !string.IsNullOrWhiteSpace(v)) + .ToArray(); + var menuItems = ShellContextMenuDisplayHelper + .GetContextMenuWithIcons(record.FullPath) + .Where(contextMenuItem => + (includedItems.Length == 0 || includedItems.Any(filter => + contextMenuItem.Label.Contains(filter, StringComparison.OrdinalIgnoreCase) + )) && + (excludedItems.Length == 0 || !excludedItems.Any(filter => + contextMenuItem.Label.Contains(filter, StringComparison.OrdinalIgnoreCase) + )) + ); + foreach (var menuItem in menuItems) + { + contextMenus.Add(new Result + { + Title = menuItem.Label, + Icon = () => menuItem.Icon, + Action = _ => + { + ShellContextMenuDisplayHelper.ExecuteContextMenuItem(record.FullPath, menuItem.CommandId); + return true; + } + }); + } + } } return contextMenus; @@ -403,7 +414,8 @@ namespace Flow.Launcher.Plugin.Explorer return false; }, - IcoPath = Constants.ExcludeFromIndexImagePath + IcoPath = Constants.ExcludeFromIndexImagePath, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uf140"), }; } @@ -435,7 +447,24 @@ namespace Flow.Launcher.Plugin.Explorer return false; } }, - IcoPath = Constants.IndexingOptionsIconImagePath + IcoPath = Constants.IndexingOptionsIconImagePath, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue773"), + }; + } + + private Result CreateOpenWithMenu(SearchResult record) + { + return new Result + { + Title = Context.API.GetTranslation("plugin_explorer_openwith"), + SubTitle = Context.API.GetTranslation("plugin_explorer_openwith_subtitle"), + Action = _ => + { + Process.Start("rundll32.exe", $"{Path.Combine(Environment.SystemDirectory, "shell32.dll")},OpenAs_RunDLL {record.FullPath}"); + return true; + }, + IcoPath = Constants.ShowContextMenuImagePath, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue7ac"), }; } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj index 1c0bdaad7..549217027 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj @@ -45,7 +45,8 @@ - + + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenu.cs index 3870c4876..abb76580d 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenu.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; using System.Text; using System.Runtime.InteropServices; using System.Drawing; using System.Windows.Forms; using System.IO; -using System.Security.Permissions; namespace Peter { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenuDisplayHelper.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenuDisplayHelper.cs new file mode 100644 index 000000000..304c47cb6 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenuDisplayHelper.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace Flow.Launcher.Plugin.Explorer.Helper; + +public static class ShellContextMenuDisplayHelper +{ + #region DllImport + + [DllImport("shell32.dll")] + private static extern Int32 SHGetMalloc(out IntPtr hObject); + + [DllImport("shell32.dll")] + private static extern Int32 SHParseDisplayName( + [MarshalAs(UnmanagedType.LPWStr)] string pszName, + IntPtr pbc, + out IntPtr ppidl, + UInt32 sfgaoIn, + out UInt32 psfgaoOut + ); + + [DllImport("shell32.dll")] + private static extern Int32 SHBindToParent( + IntPtr pidl, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid, + out IntPtr ppv, + ref IntPtr ppidlLast + ); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr CreatePopupMenu(); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool DestroyMenu(IntPtr hMenu); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern uint GetMenuItemCount(IntPtr hMenu); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern uint GetMenuString( + IntPtr hMenu, uint uIDItem, StringBuilder lpString, int nMaxCount, uint uFlag + ); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool GetMenuItemInfo(IntPtr hMenu, uint uItem, bool fByPosition, ref MENUITEMINFO lpmii); + + [DllImport("gdi32.dll")] + private static extern bool DeleteObject(IntPtr hObject); + + #endregion + + #region Constants + + private const uint ContextMenuStartId = 0x0001; + private const uint ContextMenuEndId = 0x7FFF; + + private static readonly string[] IgnoredContextMenuCommands = + { + // We haven't managed to make these work, so we don't display them in the context menu. + "Share", + "Windows.ModernShare", + "PinToStartScreen", + "CopyAsPath", + + // Hide functionality provided by the Explorer plugin itself + "Copy", + "Delete" + }; + + #endregion + + #region Enums + + [Flags] + enum ContextMenuFlags : uint + { + Normal = 0x00000000, + DefaultOnly = 0x00000001, + VerbsOnly = 0x00000002, + Explore = 0x00000004, + NoVerbs = 0x00000008, + CanRename = 0x00000010, + NoDefault = 0x00000020, + IncludeStatic = 0x00000040, + ItemMenu = 0x00000080, + ExtendedVerbs = 0x00000100, + DisabledVerbs = 0x00000200, + AsyncVerbState = 0x00000400, + OptimizeForInvoke = 0x00000800, + SyncCascadeMenu = 0x00001000, + DoNotPickDefault = 0x00002000, + Reserved = 0xffff0000 + } + + [Flags] + enum ContextMenuInvokeCommandFlags : uint + { + Icon = 0x00000010, + Hotkey = 0x00000020, + FlagNoUi = 0x00000400, + Unicode = 0x00004000, + NoConsole = 0x00008000, + AsyncOk = 0x00100000, + NoZoneChecks = 0x00800000, + ShiftDown = 0x10000000, + ControlDown = 0x40000000, + FlagLogUsage = 0x04000000, + PointInvoke = 0x20000000 + } + + [Flags] + enum MenuItemInformationMask : uint + { + Bitmap = 0x00000080, + Checkmarks = 0x00000008, + Data = 0x00000020, + Ftype = 0x00000100, + Id = 0x00000002, + State = 0x00000001, + String = 0x00000040, + Submenu = 0x00000004, + Type = 0x00000010 + } + + enum MenuItemFtype : uint + { + Bitmap = 0x00000004, + MenuBarBreak = 0x00000020, + MenuBreak = 0x00000040, + OwnerDraw = 0x00000100, + RadioCheck = 0x00000200, + RightJustify = 0x00004000, + RightOrder = 0x00002000, + Separator = 0x00000800, + String = 0x00000000, + } + + enum GetCommandStringFlags : uint + { + VerbA = 0x00000000, + HelpTextA = 0x00000001, + ValidateA = 0x00000002, + Unicode = VerbW, + Verb = VerbW, + VerbW = 0x00000004, + HelpText = HelpTextW, + HelpTextW = 0x00000005, + Validate = ValidateW, + ValidateW = 0x00000006, + VerbIconW = 0x00000014 + } + #endregion + + private static IMalloc GetMalloc() + { + SHGetMalloc(out var pMalloc); + return (IMalloc)Marshal.GetTypedObjectForIUnknown(pMalloc, typeof(IMalloc)); + } + + public static void ExecuteContextMenuItem(string fileName, uint menuItemId) + { + IMalloc malloc = null; + IntPtr originalPidl = IntPtr.Zero; + IntPtr pShellFolder = IntPtr.Zero; + IntPtr pContextMenu = IntPtr.Zero; + IntPtr hMenu = IntPtr.Zero; + IContextMenu contextMenu = null; + IShellFolder shellFolder = null; + + try + { + malloc = GetMalloc(); + var hr = SHParseDisplayName(fileName, IntPtr.Zero, out var pidl, 0, out _); + if (hr != 0) throw new Exception("SHParseDisplayName failed"); + + originalPidl = pidl; + + var guid = typeof(IShellFolder).GUID; + hr = SHBindToParent(pidl, guid, out pShellFolder, ref pidl); + if (hr != 0) throw new Exception("SHBindToParent failed"); + + shellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pShellFolder, typeof(IShellFolder)); + hr = shellFolder.GetUIObjectOf( + IntPtr.Zero, 1, new[] { pidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out pContextMenu + ); + if (hr != 0) throw new Exception("GetUIObjectOf failed"); + + contextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(pContextMenu, typeof(IContextMenu)); + + hMenu = CreatePopupMenu(); + contextMenu.QueryContextMenu(hMenu, 0, ContextMenuStartId, ContextMenuEndId, (uint)ContextMenuFlags.Explore); + + var directory = Path.GetDirectoryName(fileName); + var invokeCommandInfo = new CMINVOKECOMMANDINFO + { + cbSize = (uint)Marshal.SizeOf(typeof(CMINVOKECOMMANDINFO)), + fMask = (uint)ContextMenuInvokeCommandFlags.Unicode, + hwnd = IntPtr.Zero, + lpVerb = (IntPtr)(menuItemId - ContextMenuStartId), + lpParameters = null, + lpDirectory = null, + nShow = 1, + hIcon = IntPtr.Zero, + }; + + hr = contextMenu.InvokeCommand(ref invokeCommandInfo); + if (hr != 0) + { + throw new Exception($"InvokeCommand failed with code {hr:X}"); + } + } + finally + { + if (hMenu != IntPtr.Zero) + DestroyMenu(hMenu); + + if (contextMenu != null) + Marshal.ReleaseComObject(contextMenu); + + if (pContextMenu != IntPtr.Zero) + Marshal.Release(pContextMenu); + + if (shellFolder != null) + Marshal.ReleaseComObject(shellFolder); + + if (pShellFolder != IntPtr.Zero) + Marshal.Release(pShellFolder); + + if (originalPidl != IntPtr.Zero) + malloc?.Free(originalPidl); + + if (malloc != null) + Marshal.ReleaseComObject(malloc); + } + } + + public static List GetContextMenuWithIcons(string filePath) + { + IMalloc malloc = null; + IntPtr originalPidl = IntPtr.Zero; + IntPtr pShellFolder = IntPtr.Zero; + IntPtr pContextMenu = IntPtr.Zero; + IntPtr hMenu = IntPtr.Zero; + IShellFolder shellFolder = null; + IContextMenu contextMenu = null; + + try + { + malloc = GetMalloc(); + var hr = SHParseDisplayName(filePath, IntPtr.Zero, out var pidl, 0, out _); + if (hr != 0) throw new Exception("SHParseDisplayName failed"); + + originalPidl = pidl; + + var guid = typeof(IShellFolder).GUID; + hr = SHBindToParent(pidl, guid, out pShellFolder, ref pidl); + if (hr != 0) throw new Exception("SHBindToParent failed"); + + shellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pShellFolder, typeof(IShellFolder)); + hr = shellFolder.GetUIObjectOf( + IntPtr.Zero, 1, new[] { pidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out pContextMenu + ); + if (hr != 0) throw new Exception("GetUIObjectOf failed"); + + contextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(pContextMenu, typeof(IContextMenu)); + + // Without waiting, some items, such as "Send to > Documents", don't always appear, which shifts item ids + // even though it shouldn't. Please replace this if you find a better way to fix this bug. + Thread.Sleep(200); + + hMenu = CreatePopupMenu(); + contextMenu.QueryContextMenu(hMenu, 0, ContextMenuStartId, ContextMenuEndId, (uint)ContextMenuFlags.Explore); + + var menuItems = new List(); + ProcessMenuWithIcons(hMenu, contextMenu, menuItems); + + return menuItems; + } + finally + { + if (hMenu != IntPtr.Zero) + DestroyMenu(hMenu); + + if (contextMenu != null) + Marshal.ReleaseComObject(contextMenu); + + if (pContextMenu != IntPtr.Zero) + Marshal.Release(pContextMenu); + + if (shellFolder != null) + Marshal.ReleaseComObject(shellFolder); + + if (pShellFolder != IntPtr.Zero) + Marshal.Release(pShellFolder); + + if (originalPidl != IntPtr.Zero) + malloc?.Free(originalPidl); + + if (malloc != null) + Marshal.ReleaseComObject(malloc); + } + } + + + private static void ProcessMenuWithIcons(IntPtr hMenu, IContextMenu contextMenu, List menuItems, string prefix = "") + { + uint menuCount = GetMenuItemCount(hMenu); + + for (uint i = 0; i < menuCount; i++) + { + var mii = new MENUITEMINFO + { + cbSize = (uint)Marshal.SizeOf(typeof(MENUITEMINFO)), + fMask = (uint)(MenuItemInformationMask.Bitmap | MenuItemInformationMask.Ftype | + MenuItemInformationMask.Submenu | MenuItemInformationMask.Id) + }; + + GetMenuItemInfo(hMenu, i, true, ref mii); + var menuText = new StringBuilder(256); + uint result = GetMenuString(hMenu, mii.wID, menuText, menuText.Capacity, 0); + + if (result == 0 || string.IsNullOrWhiteSpace(menuText.ToString())) + { + continue; + } + + menuText.Replace("&", ""); + + IntPtr hSubMenu = GetSubMenu(hMenu, (int)i); + if (hSubMenu != IntPtr.Zero) + { + ProcessMenuWithIcons(hSubMenu, contextMenu, menuItems, prefix + menuText + " > "); + } + else if (!string.IsNullOrWhiteSpace(menuText.ToString())) + { + var commandBuilder = new StringBuilder(256); + contextMenu.GetCommandString( + mii.wID - ContextMenuStartId, + (uint)GetCommandStringFlags.Verb, + IntPtr.Zero, + commandBuilder, + commandBuilder.Capacity + ); + if (IgnoredContextMenuCommands.Contains(commandBuilder.ToString(), StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + ImageSource icon = null; + if (mii.hbmpItem != IntPtr.Zero) + { + icon = GetBitmapSourceFromHBitmap(mii.hbmpItem); + } + else if (mii.hbmpChecked != IntPtr.Zero) + { + icon = GetBitmapSourceFromHBitmap(mii.hbmpChecked); + } + + menuItems.Add(new ContextMenuItem(prefix + menuText, icon, mii.wID)); + } + } + } + + private static BitmapSource GetBitmapSourceFromHBitmap(IntPtr hBitmap) + { + try + { + var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap( + hBitmap, + IntPtr.Zero, + Int32Rect.Empty, + BitmapSizeOptions.FromWidthAndHeight(16, 16) + ); + + if (!DeleteObject(hBitmap)) + { + throw new Exception("Failed to delete HBitmap."); + } + + return bitmapSource; + } + catch (COMException) + { + // ignore + } + + return null; + } +} + +#region Data Structures + +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("000214E6-0000-0000-C000-000000000046")] +public interface IShellFolder +{ + [PreserveSig] + int ParseDisplayName( + IntPtr hwnd, IntPtr pbc, [In, MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, out uint pchEaten, + out IntPtr ppidl, ref uint pdwAttributes + ); + + [PreserveSig] + int EnumObjects(IntPtr hwnd, uint grfFlags, out IntPtr ppenumIDList); + + [PreserveSig] + int BindToObject(IntPtr pidl, IntPtr pbc, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); + + [PreserveSig] + int BindToStorage(IntPtr pidl, IntPtr pbc, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); + + [PreserveSig] + int CompareIDs(IntPtr lParam, IntPtr pidl1, IntPtr pidl2); + + [PreserveSig] + int CreateViewObject(IntPtr hwndOwner, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); + + [PreserveSig] + int GetAttributesOf( + uint cidl, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] apidl, ref uint rgfInOut + ); + + [PreserveSig] + int GetUIObjectOf( + IntPtr hwndOwner, uint cidl, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] apidl, + [In, MarshalAs(UnmanagedType.LPStruct)] + Guid riid, IntPtr rgfReserved, out IntPtr ppv + ); + + [PreserveSig] + int GetDisplayNameOf(IntPtr pidl, uint uFlags, IntPtr pName); + + [PreserveSig] + int SetNameOf( + IntPtr hwnd, IntPtr pidl, [In, MarshalAs(UnmanagedType.LPWStr)] string pszName, uint uFlags, out IntPtr ppidlOut + ); +} + +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("00000002-0000-0000-C000-000000000046")] +public interface IMalloc +{ + [PreserveSig] + IntPtr Alloc(UInt32 cb); + + [PreserveSig] + IntPtr Realloc(IntPtr pv, UInt32 cb); + + [PreserveSig] + void Free(IntPtr pv); + + [PreserveSig] + UInt32 GetSize(IntPtr pv); + + [PreserveSig] + Int16 DidAlloc(IntPtr pv); + + [PreserveSig] + void HeapMinimize(); +} + +[StructLayout(LayoutKind.Sequential)] +public struct CMINVOKECOMMANDINFO +{ + public uint cbSize; + public uint fMask; + public IntPtr hwnd; + public IntPtr lpVerb; + [MarshalAs(UnmanagedType.LPStr)] public string lpParameters; + [MarshalAs(UnmanagedType.LPStr)] public string lpDirectory; + public int nShow; + public uint dwHotKey; + public IntPtr hIcon; +} + +[StructLayout(LayoutKind.Sequential)] +public struct MENUITEMINFO +{ + public uint cbSize; + public uint fMask; + public uint fType; + public uint fState; + public uint wID; + public IntPtr hSubMenu; + public IntPtr hbmpChecked; + public IntPtr hbmpUnchecked; + public IntPtr dwItemData; + public IntPtr dwTypeData; + public uint cch; + public IntPtr hbmpItem; +} + +[ComImport] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +[Guid("000214E4-0000-0000-C000-000000000046")] +public interface IContextMenu +{ + [PreserveSig] + int QueryContextMenu(IntPtr hmenu, uint indexMenu, uint idCmdFirst, uint idCmdLast, uint uFlags); + + [PreserveSig] + int InvokeCommand(ref CMINVOKECOMMANDINFO pici); + + [PreserveSig] + int GetCommandString(uint idcmd, uint uflags, IntPtr reserved, StringBuilder commandstring, int cch); +} + +public record ContextMenuItem(string Label, ImageSource Icon, uint CommandId); + +#endregion diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png index 552b2c5bd..25da8e49c 100644 Binary files a/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ar.xaml new file mode 100644 index 000000000..db85ef7b1 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ar.xaml @@ -0,0 +1,165 @@ + + + + + يرجى إجراء تحديد أولاً + يرجى تحديد رابط المجلد + هل أنت متأكد أنك تريد حذف {0}؟ + هل أنت متأكد أنك تريد حذف هذا الملف نهائيًا؟ + هل أنت متأكد أنك تريد حذف هذا الملف/المجلد نهائيًا؟ + تم الحذف بنجاح + تم الحذف بنجاح {0} + قد يؤدي تعيين الكلمة المفتاحية العامة للإجراء إلى ظهور الكثير من النتائج أثناء البحث. يرجى اختيار كلمة مفتاحية محددة للإجراء + لا يمكن تعيين الوصول السريع للكلمة المفتاحية العامة عند التمكين. يرجى اختيار كلمة مفتاحية محددة للإجراء + يبدو أن الخدمة المطلوبة للبحث في فهرس Windows غير قيد التشغيل + لإصلاح ذلك، ابدأ خدمة بحث Windows. اختر هنا لإزالة هذا التحذير + تم إيقاف رسالة التحذير. كبديل للبحث عن الملفات والمجلدات، هل ترغب في تثبيت إضافة Everything؟{0}{0}اختر 'نعم' لتثبيت إضافة Everything، أو 'لا' للعودة + بديل المستكشف + حدث خطأ أثناء البحث: {0} + تعذر فتح المجلد + تعذر فتح الملف + + + حذف + تعديل + إضافة + إعدادات عامة + تخصيص الكلمات المفتاحية للإجراءات + روابط الوصول السريع + إعدادات Everything + لوحة المعاينة + الحجم + تاريخ الإنشاء + تاريخ التعديل + عرض معلومات الملف + تنسيق التاريخ والوقت + خيارات الترتيب: + مسار Everything: + إطلاق مخفي + مسار المحرر + مسار الشل + مسارات مستبعدة من البحث المفهرس + استخدام موقع نتيجة البحث كدليل العمل للتنفيذ + اضغط Enter لفتح المجلد في مدير الملفات الافتراضي + استخدام البحث المفهرس للبحث في المسار + خيارات الفهرسة + بحث: + بحث المسار: + بحث في محتوى الملفات: + بحث مفهرس: + وصول سريع: + الكلمة المفتاحية الحالية للإجراء + تم + مفعّل + عند التعطيل، لن يقوم Flow بتنفيذ هذا الخيار في البحث، وسيرجع إلى '*' لتحرير الكلمة المفتاحية للإجراء + كل شيء + فهرس Windows + تعداد مباشر + مسار محرر الملفات + مسار محرر المجلدات + مفع + مُعطّل + + محرك البحث في المحتوى + محرك البحث التكراري في المجلدات + محرك البحث المفهرس + فتح خيار فهرس Windows + أنواع الملفات المستبعدة (مفصولة بفاصلة) + على سبيل المثال: exe,jpg,png + الحد الأقصى للنتائج + الحد الأقصى لعدد النتائج المطلوبة من محرك البحث النشط + + + المستكشف + ابحث وأدر الملفات والمجلدات عبر بحث Windows أو Everything + + + Ctrl + Enter لفتح الدليل + Ctrl + Enter لفتح المجلد الحاوي + + + نسخ المسار + نسخ مسار العنصر الحالي إلى الحافظة + نسخ + نسخ الملف الحالي إلى الحافظة + نسخ المجلد الحالي إلى الحافظة + حذف + حذف الملف الحالي نهائيًا + حذف المجلد الحالي نهائيًا + المسار: + حذف المحدد + تشغيل كمستخدم مختلف + تشغيل العنصر المحدد باستخدام حساب مستخدم مختلف + فتح المجلد الحاوي + فتح الموقع الذي يحتوي على العنصر الحالي + فتح بالمحرر: + فشل في فتح الملف في {0} بالمحرر {1} في {2} + فتح بالشيل: + فشل في فتح المجلد {0} بالشيل {1} في {2} + استبعاد الحالي والمجلدات الفرعية من البحث المفهرس + تم الاستبعاد من البحث المفهرس + فتح خيارات فهرسة Windows + إدارة الملفات والمجلدات المفهرسة + فشل في فتح خيارات فهرسة Windows + إضافة إلى الوصول السريع + إضافة العنصر الحالي إلى الوصول السريع + تمت الإضافة بنجاح + تمت الإضافة إلى الوصول السريع بنجاح + تمت الإزالة بنجاح + تمت الإزالة من الوصول السريع بنجاح + أضف إلى الوصول السريع بحيث يمكن فتحه باستخدام كلمة مفتاحية لتفعيل البحث في المستكشف + إزالة من الوصول السريع + إزالة من الوصول السريع + إزالة العنصر الحالي من الوصول السريع + عرض قائمة السياق في Windows + فتح بواسطة + اختر برنامج لفتح العنصر + + + {0} متبقي من {1} + فتح في مدير الملفات الافتراضي + + استخدم '>' للبحث في هذا الدليل، '*' للبحث عن امتدادات الملفات أو '> *' للجمع بين البحثين. + + + + فشل في تحميل Everything SDK + تحذير: خدمة Everything غير قيد التشغيل + خطأ أثناء استعلام Everything + الترتيب حسب + الاسم + المسار + الحجم + الامتداد + نوع الاسم + تاريخ الإنشاء + تاريخ التعديل + السمات + اسم ملف قائمة الملفات + عدد مرات التشغيل + تاريخ التغيير الأخير + تاريخ الوصول + تاريخ التشغيل + + + تحذير: هذا ليس خيار ترتيب سريع، قد تكون عمليات البحث بطيئة + + بحث في المسار الكامل + تمكين عدد مرات التشغيل للملف/المجلد + + انقر لتشغيل أو تثبيت Everything + تثبيت Everything + تثبيت خدمة Everything. يرجى الانتظار... + تم تثبيت خدمة Everything بنجاح + فشل التثبيت التلقائي لخدمة Everything. يرجى تثبيتها يدويًا من https://www.voidtools.com + انقر هنا لتشغيلها + تعذر العثور على تثبيت Everything، هل ترغب في تحديد موقع يدويًا؟{0}{0}انقر لا وسيتم تثبيت Everything تلقائيًا لك + هل ترغب في تمكين البحث في المحتوى لـ Everything؟ + قد يكون بطيئًا جدًا بدون فهرسة (المدعومة فقط في Everything v1.5+) + + + قائمة السياق الأصلية + عرض قائمة السياق الأصلية (تجريبي) + أدناه يمكنك تحديد العناصر التي تريد تضمينها في قائمة السياق، يمكن أن تكون جزئية (على سبيل المثال 'pen wit') أو كاملة ('فتح بواسطة'). + يمكنك أدناه تحديد العناصر التي تريد استبعادها من قائمة الضغط الأيمن، يمكن أن تكون جزئية (على سبيل المثال، 'pen wit') أو كاملة ('Open with'). + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/cs.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/cs.xaml new file mode 100644 index 000000000..9d23e3afe --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/cs.xaml @@ -0,0 +1,165 @@ + + + + + Nejprve vyberte položku + Vyberte odkaz na složku + Opravdu chcete odstranit {0}? + Opravdu chcete trvale odstranit tento soubor? + Opravdu chcete trvale smazat tento soubor/složku? + Úspěšně odstraněno + Úspěšně odstraněno {0} + Přiřazení globálního aktivačního příkazu může při vyhledávání poskytnout příliš mnoho výsledků. Zvolte konkrétní aktivační příkaz + Pokud je povolen rychlý přístup, nelze nastavit globální aktivační příkaz. Zvolte konkrétní aktivační příkaz + Nezdá se, že by požadovaná služba Windows Index Search byla spuštěna + Chcete-li to opravit, spusťte vyhledávání ve Windows. Chcete-li toto upozornění odstranit, klikněte zde + Upozornění bylo vypnuto. Chcete nainstalovat zásuvný modul Everything jako alternativu pro vyhledávání souborů a složek?{0}{0}Pro instalaci zásuvného modulu Everything vyberte "Ano", pro návrat vyberte "Ne" + Alternativa pro Průzkumníka + Při vyhledávání došlo k chybě: {0} + Adresář nelze otevřít + Nelze otevřít soubor + + + Smazat + Editovat + Přidat + Všeobecné nastavení + Upravit aktivační příkaz + Odkazy rychlého přístupu + Nastavení Everything + Preview Panel + Velikost + Datum vytvoření + Datum změny + Display File Info + Date and time format + Možnosti řazení: + Umístění Everything: + Spustit skryté + Cesta k editoru + Cesta k příkazovému řádku + Vyloučená místa indexování + Použít umístění výsledků vyhledávání jako pracovní adresář spustitelného souboru + Klepnutím na Enter otevřete složku ve výchozím správci souborů + K vyhledání cesty použijte indexové vyhledávání + Možnosti indexování + Hledat: + Cesta vyhledávání: + Vyhledávání obsahu souborů: + Vyhledávání v indexu: + Rychlý přístup: + Aktuální aktivační příkaz + Hotovo + Povoleno + Pokud je tato možnost vypnuta, Flow tuto možnost vyhledávání neprovede a vrátí se zpět k "*", aby uvolnila akční zkratku + Everything + Index Windowsu + Seznam složek + Cesta k editoru souborů + Cesta k editoru složek + Povoleno + Vypnuto + + Vyhledávač obsahu + Rekurzivní vyhledávač ve složce + Indexový vyhledávač + Otevření možností vyhledávání v systému Windows + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine + + + Průzkumník + Vyhledává a spravuje soubory a složky pomocí funkce Windows Search nebo Everything + + + Ctrl + Enter pro otevření adresáře + Ctrl + Enter pro otevření umístění složky + + + Kopírovat cestu + Zkopírovat cestu k aktuální položce do schránky + Kopírovat + Kopírovat aktuální soubor do schránky + Kopírovat aktuální složku do schránky + Smazat + Trvale odstranit aktuální soubor + Trvale smazat aktuální složku + Cesta: + Odstranit vybraný + Spustit jako jiný uživatel + Spustí vybranou položku jako uživatel s jiným účtem + Otevřít umístění složky + Otevřít umístění aktuální položky + Otevřít v editoru: + Nepodařilo se otevřít soubor {0} v editoru {1} - {2} + Otevřete v příkazovém řádku: + Nepodařilo se otevřít složku {0} v {1} - {2} + Vyloučení položky a jejích podsložek z vyhledávacího indexu + Vyloučit z vyhledávacího indexu + Otevření možností vyhledávání v systému Windows + Správa indexovaných souborů a složek + Nepodařilo se otevřít možnosti indexu vyhledávání + Přidat k Rychlému přístupu + Přidat aktuální položku do Rychlého přístupu + Přidáno úspěšně + Úspěšně přidáno do Rychlého přístupu + Úspěšně odstraněno + Úspěšně odstraněno z Rychlého přístupu + Přidat do Rychlého přístupu, aby jej bylo možné otevřít pomocí příkazu pro aktivaci pluginu Průzkumník + Odstranit z Rychlého přístupu + Odstranit z Rychlého přístupu + Odstranit aktuální položku z rychlého přístupu + Zobrazit kontextové menu Windows + Open With + Select a program to open with + + + Volných {0} z {1} + Otevřít ve výchozím správci souborů + + Použijte ">" pro vyhledávání v této složce, "*" pro vyhledávání přípon souborů nebo ">*" pro kombinaci obou vyhledávání. + + + + Nepodařilo se načíst SDK Everything + Upozornění: Služba Everything není spuštěna + Chyba při dotazování Everything + Seřadit podle + Jméno + Cesta + Velikost + Rozšíření + Typ + Datum vytvoření + Datum změny + Atributy + Seznam názvů souborů + Počet spuštění + Poslední změna data + Datum přístupu + Datum spouštění + + + Poznámka: Toto není možnost Fast Sort, vyhledávání může být pomalé + + Hledat celou cestu + Enable File/Folder Run Count + + Kliknutím spustíte nebo nainstalujete aplikaci Everything + Instalace Everything + Služba Everything se nainstaluje. Počkejte prosím... + Služba Everything bylo úspěšně nainstalována + Automatická instalace aplikace Everything se nezdařila. Nainstalujte ji prosím ručně ze stránek https://www.voidtools.com + Klikni zde pro spuštění + Nepodařilo se najít instalaci Everything, chcete ručně vybrat její umístění?{0}{0}Kliknutím na ne se Everything nainstaluje automaticky + Chcete povolit vyhledávání obsahu prostřednictvím služby Everything? + Bez indexu (který je podporován pouze ve verzi Everything v1.5+) může být velmi pomalý + + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/da.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/da.xaml index 824a179af..0f7619954 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/da.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/da.xaml @@ -16,6 +16,8 @@ The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative Error occurred during search: {0} + Could not open folder + Could not open file Slet @@ -25,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -32,6 +40,7 @@ Shell Path Index Search Excluded Paths Use search result's location as the working directory of the executable + Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options Search: @@ -48,11 +57,17 @@ Direct Enumeration Filredigeringssti Mapperedigeringssti + Enabled + Disabled Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -97,11 +112,15 @@ Remove from Quick Access Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -126,7 +145,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -137,4 +157,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml index d96ac6c94..f2df655c7 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml @@ -2,69 +2,84 @@ - Please make a selection first - Bitte wähle eine Ordnerverknüpfung - Bist du sicher {0} zu löschen? + Bitte treffen Sie zuerst eine Auswahl + Bitte wählen Sie einen Ordner-Link aus + Sind Sie sicher, dass Sie {0} löschen wollen? Sind Sie sicher, dass Sie diese Datei dauerhaft löschen möchten? - Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} - Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword - Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword - The required service for Windows Index Search does not appear to be running - To fix this, start the Windows Search service. Select here to remove this warning - The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return - Explorer Alternative - Error occurred during search: {0} + Sind Sie sicher, dass Sie diese(n) Datei/Ordner dauerhaft löschen wollen? + Löschung erfolgreich + Erfolgreich gelöscht {0} + Die Zuweisung des globalen Aktions-Schlüsselwortes könnte zu viele Ergebnisse bei der Suche hervorrufen. Bitte wählen Sie ein spezifisches Aktions-Schlüsselwort + Schnellzugriff kann nicht auf das globale Aktions-Schlüsselwort gesetzt werden, wenn aktiviert. Bitte wählen Sie ein spezifisches Aktions-Schlüsselwort + Der erforderliche Dienst für Windows-Indexsuche scheint nicht ausgeführt zu werden + Um dies zu beheben, starten Sie den Windows-Suchdienst. Wählen Sie hier aus, um diese Warnung zu entfernen + Die Warnmeldung ist ausgeschaltet worden. Möchten Sie als Alternative für die Suche nach Dateien und Ordnern das Plugin Everything installieren?{0}{0}Wählen Sie 'Ja' aus, um das Plug-in Everything zu installieren, oder 'Nein', um zurückzukehren + Explorer-Alternative + Fehler aufgetreten während Suche: {0} + Ordner konnte nicht geöffnet werden + Datei konnte nicht geöffnet werden Löschen Bearbeiten Hinzufügen - General Setting - Customise Action Keywords - Quick Access Links - Everything Setting - Sort Option: - Everything Path: - Launch Hidden - Editor pad - Shell Path - Index Search Excluded Paths - Verwenden Suchergebnis Standort als ausführbare Arbeitsverzeichnis - Use Index Search For Path Search - Indexing Options + Allgemeine Einstellung + Aktions-Schlüsselwörter individuell anpassen + Schnellzugriff-Links + Everyhting-Einstellung + Vorschau-Panel + Größe + Erstellungsdatum + Änderungsdatum + Datei-Info anzeigen + Datums- und Zeitformat + Sortieroption: + Everything-Pfad: + Ausgeblendet starten + Editor-Pfad + Shell-Pfad + Indexsuche ausgeschlossene Pfade + Ort des Suchergebnisses als Arbeitsverzeichnis der ausführbaren Datei verwenden + Drücken Sie Enter, um Ordner im Default-Dateimanager zu öffnen + Indexsuche für Pfadsuche verwenden + Indexierungsoptionen Suche: Pfad-Suche: Suche nach Dateiinhalten: Index-Suche: Schnellzugriff: - Current Action Keyword + Aktuelles Aktions-Schlüsselwort Fertig Aktiviert - When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword + Wenn deaktiviert, führt Flow diese Suchoption nicht aus und kehrt zusätzlich zu '*' zurück, um das Aktions-Schlüsselwort freizugeben Everything - Windows Index - Direct Enumeration + Windows-Index + Direkte Aufzählung Datei-Editor-Pfad Ordner-Editor-Pfad + Aktiviert + Deaktiviert - Content Search Engine - Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + Content-Suchmaschine + Verzeichnis Rekursive Suchmaschine + Suchmaschine indizieren + Windows-Indexierungsoptionen öffnen + Ausgeschlossene Dateitypen (durch Komma getrennt) + Zum Beispiel: exe,jpg,png + Maximale Ergebnisse + Die maximale Anzahl der angeforderten Ergebnisse aus aktiver Suchmaschine Explorer - Find and manage files and folders via Windows Search or Everything + Dateien und Ordner via Windows-Suche oder Everything finden und verwalten - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Strg+Enter, um das Verzeichnis zu öffnen + Strg+Enter, um den enthaltenden Ordner zu öffnen Pfad kopieren - Copy path of current item to clipboard + Pfad des aktuellen Elements in Zwischenablage kopieren Kopieren Aktuelle Datei in Zwischenablage kopieren Aktuellen Ordner in Zwischenablage kopieren @@ -72,69 +87,79 @@ Aktuelle Datei dauerhaft löschen Aktuellen Ordner dauerhaft löschen Pfad: - Ausgewählte löschen + Ausgewähltes löschen Als anderer Benutzer ausführen - Ausgewählte mit einem anderen Benutzerkonto ausführen - Open containing folder - Open the location that contains current item - Open With Editor: - Failed to open file at {0} with Editor {1} at {2} - Open With Shell: - Failed to open folder {0} with Shell {1} at {2} - Exclude current and sub-directories from Index Search - Excluded from Index Search - Open Windows Indexing Options - Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access - Add current item to Quick Access - Successfully Added - Successfully added to Quick Access - Successfully Removed - Successfully removed from Quick Access - Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access - Remove current item from Quick Access - Show Windows Context Menu - - - {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Ausgewähltes unter Verwendung eines anderen Benutzerkontos ausführen + Enthaltenden Ordner öffnen + Den Ort öffnen, der aktuelles Element enthält + Öffnen mit Editor: + Datei bei {0} konnte nicht mit Editor {1} bei {2} geöffnet werden + Öffnen mit Shell: + Ordner {0} konnte nicht mit Shell {1} bei {2} geöffnet werden + Aktuelles und Unterverzeichnisse aus Indexsuche ausschließen + Aus Indexsuche ausgeschlossen + Windows-Indexierungsoptionen öffnen + Indexierte Dateien und Ordner verwalten + Windows-Indexierungsoptionen konnten nicht geöffnet werden + Zu Schnellzugriff hinzufügen + Aktuelles Element zu Schnellzugriff hinzufügen + Erfolgreich hinzugefügt + Erfolgreich zum Schnellzugriff hinzugefügt + Erfolgreich entfernt + Erfolgreich aus Schnellzugriff entfernt + Zum Schnellzugriff hinzufügen, so kann dieser mit dem Aktions-Schlüsselwort "Suchaktivierung" des Explorers geöffnet werden + Aus Schnellzugriff entfernen + Aus Schnellzugriff entfernen + Aktuelles Element aus Schnellzugriff entfernen + Windows-Kontextmenü zeigen + Öffnen mit + Ein Programm zum Öffnen auswählen + + + {0} frei von {1} + In Default-Dateimanager öffnen + + Verwenden Sie '>', um in diesem Verzeichnis zu suchen, '*', um nach Dateierweiterungen zu suchen, oder '>*', um beide Suchen zu kombinieren. + - Failed to load Everything SDK - Everything Service läuft nicht - Everything Plugin hat einen Fehler (drücke Enter zum kopieren der Fehlernachricht) - Sort By + Everything SDK konnte nicht geladen werden + Warnung: Everything-Dienst wird nicht ausgeführt + Fehler bei Abfrage von Everything + Sortieren nach Name - Path + Pfad Größe Extension - Type Name - Date Created - Date Modified - Attributes - File List FileName - Run Count - Date Recently Changed - Date Accessed - Date Run + Typname + Erstellungsdatum + Änderungsdatum + Attribute + Dateilistenname + Ausführungszahl + Datum kürzlich geändert + Zugriffsdatum + Ausführungsdatum - Warning: This is not a Fast Sort option, searches may be slow + Warnung: Dies ist keine Schnellsortieroption, Suchen können langsam sein + + Vollständigen Pfad suchen + Datei-/Ordnerlaufzähler aktivieren - Search Full Path - Klicken, um Everything zu starten oder zu installieren - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you - Möchten Sie die Inhaltssuche für alles aktivieren? - It can be very slow without index (which is only supported in Everything v1.5+) + Everything-Installation + Everything-Dienst wird installiert. Bitte warten Sie ... + Everything-Dienst erfolgreich installiert + Der Everything-Dienst konnte nicht automatisch installiert werden. Bitte installieren Sie ihn manuell über https://www.voidtools.com + Klicken Sie hier, um es zu starten + Es kann keine Everything-Installation gefuinden werden, möchten Sie einen Ort manuell auswählen?{0}{0}Klicken Sie auf Nein und Everything wird automatisch für Sie installiert + Möchten Sie die Inhaltssuche für Everything aktivieren? + Es kann sehr langsam sein ohne Index (was nur in Everything v1.5+ unterstützt wird) + + Natives Kontextmenü + Natives Kontextmenü anzeigen (experimentell) + Unten können Sie die Elemente angeben, die Sie in das Kontextmenü aufnehmen möchten. Diese können partiell (z. B. „Stift mit“) oder vollständig („Öffnen mit“) sein. + Unten können Sie die Elemente angeben, die Sie aus dem Kontextmenü ausschließen möchten. Diese können partiell (z. B. „Stift mit“) oder vollständig („Öffnen mit“) sein. diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index d5352ef1e..f7d5bdb18 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -29,6 +29,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -53,11 +59,17 @@ Direct Enumeration File Editor Path Folder Editor Path + Enabled + Disabled Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -102,11 +114,15 @@ Remove from Quick Access Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -131,7 +147,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -142,4 +159,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es-419.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es-419.xaml index 151879abf..304a41c23 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es-419.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es-419.xaml @@ -16,6 +16,8 @@ The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative Error occurred during search: {0} + Could not open folder + Could not open file Eliminar @@ -25,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Fecha de creación + Fecha de modificación + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -32,6 +40,7 @@ Shell Path Index Search Excluded Paths Usar la ubicación de los resultados de búsqueda como directorio de trabajo ejecutable + Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options Search: @@ -48,11 +57,17 @@ Direct Enumeration Ruta del editor Folder Editor Path + Enabled + Disabled Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -97,11 +112,15 @@ Remove from Quick Access Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -126,7 +145,8 @@ Advertencia: No es una opción de orden rápido, las búsquedas pueden ser lentas Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Instalación de Everything Instalando el servicio de Everything. Por favor, espere... @@ -137,4 +157,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es.xaml index d31421b64..e144b724c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/es.xaml @@ -4,8 +4,8 @@ Por favor haga una selección primero Por favor, seleccione un enlace de carpeta - ¿Está seguro que desea eliminar {0}? - ¿Está seguro que desea eliminar permanentemente este archivo? + ¿Está seguro de que desea eliminar {0}? + ¿Está seguro de que desea eliminar permanentemente este archivo? ¿Está seguro de que desea eliminar permanentemente este/esta archivo/carpeta? Eliminación correcta {0} se ha eliminado correctamente @@ -16,6 +16,8 @@ El mensaje de advertencia se ha desactivado. Como alternativa para buscar archivos y carpetas, ¿desea instalar el complemento Everything?{0}{0}Seleccione 'Sí' para instalar el complemento Everything, o 'No' para volver Explorador alternativo Se ha producido un error durante la búsqueda: {0} + No se ha podido abrir la carpeta + No se ha podido abrir el archivo Eliminar @@ -25,6 +27,12 @@ Personalizar palabras clave de acción Enlaces de acceso rápido Configuración Everything + Panel de vista previa + Tamaño + Fecha de creación + Fecha de modificación + Mostrar información del archivo + Formato de fecha y hora Ordenar por: Ruta de Everything: Iniciar oculto @@ -32,6 +40,7 @@ Ruta del Shell Rutas excluídas del índice de búsqueda Usar la ubicación de los resultados de búsqueda como directorio de trabajo del ejecutable + Pulsar Entrar para abrir la carpeta en el administrador de archivos predeterminado Usar búsqueda indexada para buscar rutas Opciones de indexación Buscar: @@ -48,11 +57,17 @@ Enumeración directa Ruta del editor de archivos Ruta del editor de carpetas + Activado + Desactivado Motor de búsqueda de contenido Motor de búsqueda recursiva de directorio Motor de búsqueda del Índice Abrir opciones de indexación de Windows + Tipos de archivo excluidos (separados por comas) + Por ejemplo: exe,jpg,png + Número máximo de resultados + Número máximo de resultados solicitados al motor de búsqueda activo Explorador @@ -97,11 +112,15 @@ Eliminar del acceso rápido Elimina elemento actual del acceso rápido Mostrar menú contextual de Windows - - + Abrir con + Seleccione un programa para abrir con + + {0} libre de {1} Abrir en administrador de archivos predeterminado - Use '>' para buscar en este directorio, '*' para buscar extensiones de archivo o '>*' para combinar ambas búsquedas. + + Use '>' para buscar en este directorio, '*' para buscar extensiones de archivo o '>*' para combinar ambas búsquedas. + No se ha podido cargar Everything SDK @@ -126,7 +145,8 @@ Advertencia: Esta no es una opción de clasificación rápida, las búsquedas pueden ser lentas Buscar ruta completa - + Activar número de ejecuciones de archivos/carpetas + Hacer clic para lanzar o instalar Everything Instalación de Everything Instalando el servicio de Everything. Por favor, espere... @@ -137,4 +157,9 @@ ¿Desea activar la búsqueda de contenidos de Everything? Puede ser muy lento sin índice (solo se admite en Everything v1.5+) + + Menú contextual nativo + Mostrar menú contextual nativo (experimental) + En el siguiente cuadro puede especificar los elementos que desea incluir en el menú contextual, puede describirlos de forma parcial (p. ej., 'brir co') o completa ('Abrir con'). + En el siguiente cuadro puede especificar los elementos que desea excluir en el menú contextual, puede describirlos de forma parcial (p. ej., 'brir co') o completa ('Abrir con'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/fr.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/fr.xaml index 0c921dc3e..1aeffd6ed 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/fr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/fr.xaml @@ -2,139 +2,164 @@ - Please make a selection first - Please select a folder link - Are you sure you want to delete {0}? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} - Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword - Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword - The required service for Windows Index Search does not appear to be running - To fix this, start the Windows Search service. Select here to remove this warning - The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return - Explorer Alternative - Error occurred during search: {0} + Veuillez d'abord effectuer une sélection + Veuillez sélectionner un lien vers un dossier + Êtes-vous sûr de vouloir supprimer {0} ? + Êtes-vous sûr de vouloir supprimer définitivement ce fichier ? + Êtes-vous sûr de vouloir supprimer définitivement ce fichier/dossier ? + Suppression réussie + {0} a été supprimé avec succès + L'attribution d'un mot-clé d'action global pourrait donner lieu à un trop grand nombre de résultats lors de la recherche. Veuillez choisir un mot-clé d'action spécifique + L'accès rapide ne peut pas être défini comme mot-clé d'action global lorsqu'il est activé. Veuillez choisir un mot-clé d'action spécifique + Le service requis pour Windows Index Search ne semble pas être en cours d'exécution. + Pour résoudre ce problème, démarrez le service Windows Search. Sélectionnez ici pour supprimer cet avertissement + Le message d'avertissement a été désactivé. Souhaitez-vous installer le plugin Everything comme alternative à la recherche de fichiers et de dossiers ? {0}{0} Sélectionnez "Oui" pour installer le plugin Everything, ou "Non" pour revenir à l'état initial. + Alternative à l'explorateur + Une erreur s'est produite pendant la recherche : {0} + Impossible d'ouvrir le dossier + Impossible d'ouvrir le fichier Supprimer Modifier Ajouter - General Setting - Customise Action Keywords - Quick Access Links - Everything Setting - Sort Option: - Everything Path: - Launch Hidden - Editor Path - Shell Path - Index Search Excluded Paths - Use search result's location as the working directory of the executable - Use Index Search For Path Search - Indexing Options - Search: - Path Search: - File Content Search: - Index Search: - Quick Access: - Current Action Keyword - Termin - Enabled - When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword + Paramètres généraux + Personnaliser les mots-clés d'action + Liens d'accès rapide + Paramètres Everything + Panneau d'aperçu + Taille + Date de création + Date de modification + Afficher les informations du fichier + Format de la date et de l'heure + Option de tri : + Chemin vers Everything : + Démarrer minimisé + Chemin de l'éditeur + Chemin d'accès au Shell + Chemins exclus de la recherche d'index + Utiliser l'emplacement du résultat de la recherche comme répertoire de travail de l'exécutable + Appuyez sur Entrée pour ouvrir le dossier dans le gestionnaire de fichiers par défaut + Utiliser la recherche d'index pour la recherche de chemin d'accès + Options d'indexation + Recherche : + Recherche de chemin : + Recherche de contenu de fichier : + Recherche dans l'index : + Accès rapide : + Mot-clé de l'action en cours + Terminé + Activé + Lorsqu'il est désactivé, Flow n'exécute pas cette option de recherche et revient en outre à "*" pour libérer le mot-clé d'action. Everything - Windows Index - Direct Enumeration + Index de Windows + Énumération directe Chemin de l'éditeur de fichiers Chemin de l'éditeur de dossier + Activé + Désactivé - Content Search Engine - Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + Moteur de recherche de contenu + Moteur de recherche récursif du répertoire + Moteur de recherche de l'index + Ouvrir l'option d'index de Windows + Types de fichiers exclus (séparés par des virgules) + Par exemple : exe,jpg,png + Résultats maximums + Le nombre maximum de résultats demandés au moteur de recherche actif - Explorer - Find and manage files and folders via Windows Search or Everything + Explorateur + Recherche et gestion de fichiers et de dossiers via Windows Search ou Everything - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Ctrl + Entrée pour ouvrir le répertoire + Ctrl + Entrée pour ouvrir le dossier contenant - Copy path - Copy path of current item to clipboard - Copy - Copy current file to clipboard - Copy current folder to clipboard + Copier le chemin + Copier le chemin de l'élément actuel dans le presse-papiers + Copier + Copier le fichier actuel dans le presse-papiers + Copier le dossier actuel dans le presse-papiers Supprimer - Permanently delete current file - Permanently delete current folder - Path: - Delete the selected - Run as different user - Run the selected using a different user account - Open containing folder - Open the location that contains current item - Open With Editor: - Failed to open file at {0} with Editor {1} at {2} - Open With Shell: - Failed to open folder {0} with Shell {1} at {2} - Exclude current and sub-directories from Index Search - Excluded from Index Search - Open Windows Indexing Options - Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access - Add current item to Quick Access - Successfully Added - Successfully added to Quick Access - Successfully Removed - Successfully removed from Quick Access - Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access - Remove current item from Quick Access - Show Windows Context Menu - - - {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Supprimer définitivement le fichier actuel + Supprimer définitivement le dossier actuel + Chemin : + Supprimer la sélection + Exécuter en tant qu'utilisateur différent + Exécuter le programme sélectionné en utilisant un autre compte d'utilisateur + Ouvrir le répertoire + Ouvrir l'emplacement qui contient l'élément actuel + Ouvrir avec l'éditeur : + Échec de l'ouverture du fichier à {0} avec l'éditeur {1} à {2} + Ouvrir avec le Shell : + Échec de l'ouverture du dossier {0} avec le shell {1} à {2} + Exclure les répertoires et sous-répertoires actuels de la recherche dans l'index + Exclus de la recherche dans l'index + Options d'indexation des fenêtres ouvertes + Gérer les fichiers et dossiers indexés + Échec de l'ouverture des options d'indexation de Windows + Ajouter à l'accès rapide + Ajouter l'élément à l'accès rapide + Ajouté avec succès + Ajouté avec succès à l'accès rapide + Supprimé avec succès + Suppression réussie de l'accès rapide + Ajouter à l'accès rapide pour qu'il puisse être ouvert avec le mot-clé d'activation de la recherche de l'explorateur. + Supprimer de l'accès rapide + Supprimer de l'accès rapide + Supprimer l'élément de l'accès rapide + Afficher le menu contextuel de Windows + Ouvrir avec + Sélectionnez un programme à ouvrir avec + + + {0} libres sur {1} + Ouvrir dans le gestionnaire de fichiers par défaut + + Utilisez '>' pour effectuer une recherche dans ce répertoire, '*' pour rechercher les extensions de fichiers ou '>*' pour combiner les deux recherches. + - Failed to load Everything SDK - Warning: Everything service is not running - Error while querying Everything - Sort By - Name - Path + Échec du chargement du SDK Everything + Avertissement : Le service Everything n'est pas en cours d'exécution + Erreur lors de l'interrogation de Everything + Trier par + Nom + Xhemin Taille Extension - Type Name - Date Created - Date Modified - Attributes - File List FileName - Run Count - Date Recently Changed - Date Accessed - Date Run + Nom du type + Date de création + Date de modification + Attributs + Fichiers Liste Nom du fichier + Nombre d'exécutions + Date récemment modifiée + Date d'accès + Date d'exécution - Warning: This is not a Fast Sort option, searches may be slow + Avertissement : Il ne s'agit pas d'une option de tri rapide, les recherches peuvent être lentes. - Search Full Path - - Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you - Do you want to enable content search for Everything? - It can be very slow without index (which is only supported in Everything v1.5+) + Recherche du chemin complet + Activer le nombre d'exécutions de fichiers/dossiers + Cliquez pour lancer ou installer Everything + Installation d'Everything + Installation du service Everything. Veuillez patienter... + Installation réussie du service Everything + Échec de l'installation automatique du service Everything. Veuillez l'installer manuellement à partir de https://www.voidtools.com + Cliquez ici pour démarrer + Impossible de trouver une installation d'Everything, souhaitez-vous sélectionner manuellement un emplacement ? {0}{0} Cliquez sur non et Everything sera automatiquement installé pour vous. + Voulez-vous activer la recherche de contenu pour Everything ? + Il peut être très lent sans index (qui n'est supporté que dans Everything v1.5+). + + + Menu contextuel natif + Afficher le menu contextuel natif (expérimental) + Ci-dessous, vous pouvez spécifier les éléments que vous souhaitez inclure dans le menu contextuel. Ils peuvent être partiels ('pen wit') ou complets ('Ouvrir avec'). + Vous pouvez spécifier ci-dessous les éléments que vous souhaitez exclure du menu contextuel. Ces éléments peuvent être partiels (par exemple, "pen wit") ou complets ("Ouvrir avec"). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/he.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/he.xaml new file mode 100644 index 000000000..f87dd7d63 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/he.xaml @@ -0,0 +1,165 @@ + + + + + Please make a selection first + Please select a folder link + Are you sure you want to delete {0}? + Are you sure you want to permanently delete this file? + Are you sure you want to permanently delete this file/folder? + Deletion successful + Successfully deleted {0} + Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword + Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword + The required service for Windows Index Search does not appear to be running + To fix this, start the Windows Search service. Select here to remove this warning + The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return + Explorer Alternative + Error occurred during search: {0} + Could not open folder + Could not open file + + + מחק + ערוך + הוסף + General Setting + Customise Action Keywords + Quick Access Links + Everything Setting + Preview Panel + Size + תאריך יצירה + תאריך שינוי + Display File Info + Date and time format + Sort Option: + Everything Path: + Launch Hidden + Editor Path + Shell Path + Index Search Excluded Paths + Use search result's location as the working directory of the executable + Hit Enter to open folder in Default File Manager + Use Index Search For Path Search + Indexing Options + Search: + Path Search: + File Content Search: + Index Search: + Quick Access: + Current Action Keyword + בוצע + Enabled + When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword + Everything + Windows Index + Direct Enumeration + File Editor Path + Folder Editor Path + Enabled + Disabled + + Content Search Engine + Directory Recursive Search Engine + Index Search Engine + Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine + + + Explorer + Find and manage files and folders via Windows Search or Everything + + + Ctrl + Enter to open the directory + Ctrl + Enter to open the containing folder + + + Copy path + Copy path of current item to clipboard + Copy + Copy current file to clipboard + Copy current folder to clipboard + מחק + Permanently delete current file + Permanently delete current folder + Path: + Delete the selected + Run as different user + Run the selected using a different user account + Open containing folder + Open the location that contains current item + Open With Editor: + Failed to open file at {0} with Editor {1} at {2} + Open With Shell: + Failed to open folder {0} with Shell {1} at {2} + Exclude current and sub-directories from Index Search + Excluded from Index Search + Open Windows Indexing Options + Manage indexed files and folders + Failed to open Windows Indexing Options + Add to Quick Access + Add current item to Quick Access + Successfully Added + Successfully added to Quick Access + Successfully Removed + Successfully removed from Quick Access + Add to Quick Access so it can be opened with Explorer's Search Activation action keyword + Remove from Quick Access + Remove from Quick Access + Remove current item from Quick Access + Show Windows Context Menu + Open With + Select a program to open with + + + {0} free of {1} + Open in Default File Manager + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + + + Failed to load Everything SDK + אזהרה: שירות Everything אינו פועל + שגיאה במהלך שאילתה לEverything + מיין לפי + Name + נתיב + Size + Extension + Type Name + תאריך יצירה + תאריך שינוי + מאפיינים + File List FileName + Run Count + תאריך שינוי אחרון + תאריך גישה + תאריך הרצה + + + Warning: This is not a Fast Sort option, searches may be slow + + Search Full Path + Enable File/Folder Run Count + + Click to launch or install Everything + Everything Installation + מתקין את שירות Everything. אנא המתן... + שירות Everything הותקן בהצלחה + התקנה אוטומטית של שירות Everything נכשלה. אנא הורד אותו ידנית מ- https://www.voidtools.com + Click here to start it + לא מצליח למצוא התקנה של Everything, האם תרצה לבחור מיקום באופן ידני?{0}{0}לחץ על לא וEverything יותקן עבורך אוטומטית + Do you want to enable content search for Everything? + It can be very slow without index (which is only supported in Everything v1.5+) + + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/it.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/it.xaml index ba36b0078..eb0ef2771 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/it.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/it.xaml @@ -2,113 +2,132 @@ - Please make a selection first - Please select a folder link - Are you sure you want to delete {0}? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} - Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword - Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword - The required service for Windows Index Search does not appear to be running - To fix this, start the Windows Search service. Select here to remove this warning - The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return - Explorer Alternative - Error occurred during search: {0} + Effettua prima una selezione + Si prega di selezionare un collegamento alla cartella + Sei sicuro di voler eliminare {0}? + Sei sicuro di voler eliminare definitivamente questo file? + Sei sicuro di voler eliminare definitivamente questo/a file/cartella? + Eliminato con successo + {0} eliminato correttamente + L'assegnazione della parola chiave globale potrebbe portare a troppi risultati durante la ricerca. Scegli una parola chiave specifica per l'azione + L'accesso rapido non può essere impostato sulla parola chiave globale quando abilitata. Si prega di scegliere una parola chiave specifica + Il servizio richiesto per Windows Index Search non sembra essere in esecuzione + Per risolvere il problema, avvia il servizio Ricerca Windows. Seleziona qui per rimuovere questo avviso + Il messaggio di avviso è stato spento. In alternativa per la ricerca di file e cartelle, vuoi installare il plugin Everything?{0}{0} Seleziona 'Sì' per installare il plugin Everything, o 'No' per tornare + Alternativa all'Esplora Risorse + Errore durante la ricerca: {0} + Impossibile aprire la cartella + Impossibile aprire il file Cancella Modifica Aggiungi - General Setting - Customise Action Keywords - Quick Access Links - Everything Setting - Sort Option: - Everything Path: - Launch Hidden + Impostazione Generale + Personalizza Parola Chiave + Collegamenti ad Accesso Rapido + Impostazioni di Everything + Pannello Anteprima + Dimensioni + Data di creazione + Data della modifica + Visualizza Informazioni File + Formato data e ora + Opzioni di ordinamento: + Percorso di Everything: + Avvia nascosto Tasto di accesso rapido alla finestra - Shell Path - Index Search Excluded Paths + Percorso Shell + Percorsi Esclusi dall'Indice di Ricerca Utilizza il percorso ottenuto dalla ricerca come cartella di lavoro - Use Index Search For Path Search - Indexing Options - Search: - Path Search: - File Content Search: - Index Search: - Quick Access: - Current Action Keyword + Premi Invio per aprire la cartella nel gestore file predefinito + Usa Indice di Ricerca per la Ricerca Percorso + Opzioni di Indicizzazione + Cerca: + Ricerca Percorso: + Ricerca Contenuto File: + Ricerca in Indice: + Accesso Rapido: + Parola Chiave Corrente Conferma - Enabled - When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword + Abilitato + Quando disabilitato Flow non eseguirà questa opzione di ricerca, e ripristinerà a "*" per liberare la parola chiave Tutto - Windows Index - Direct Enumeration + Indice Di Windows + Enumerazione Diretta Percorso dell'editor di file Percorso dell'editor delle cartelle + Abilitato + Disabilitato - Content Search Engine - Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + Motore di Ricerca dei Contenuti + Motore di Ricerca Ricorsivo sulle Catelle + Indice del Motore di Ricerca + Apri Opzioni di Indicizzazione di Windows + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine - Explorer - Find and manage files and folders via Windows Search or Everything + Esplora Risorse + Trova e gestisci file e cartelle tramite Ricerca di Windows o Everything - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Ctrl + Invio per aprire la cartella + Ctrl + Invio per aprire la cartella che lo contiene - Copy path - Copy path of current item to clipboard - Copy - Copy current file to clipboard - Copy current folder to clipboard + Copia percorso + Copia negli appunti il percorso dell'elemento corrente + Copia + Copia file corrente negli appunti + Copia cartella corrente negli appunti Cancella - Permanently delete current file - Permanently delete current folder - Path: - Delete the selected - Run as different user - Run the selected using a different user account - Open containing folder - Open the location that contains current item - Open With Editor: - Failed to open file at {0} with Editor {1} at {2} - Open With Shell: - Failed to open folder {0} with Shell {1} at {2} - Exclude current and sub-directories from Index Search - Excluded from Index Search - Open Windows Indexing Options - Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access - Add current item to Quick Access - Successfully Added - Successfully added to Quick Access - Successfully Removed - Successfully removed from Quick Access - Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access - Remove current item from Quick Access - Show Windows Context Menu - - - {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Elimina permanentemente il file corrente + Elimina definitivamente la cartella corrente + Percorso: + Elimina il selezionato + Esegui come utente differente + Esegui la selezione utilizzando un altro account utente + Apri percorso file + Apri la posizione che contiene l'elemento corrente + Apri nell'Editor: + Apertura del file in {0} con l'editor {1} in {2} non riuscita + Apri Con Shell: + Apertura della cartella {0} con Shell {1} in {2} non riuscita + Escludi cartelle e sottocartelle dall'Indice di Ricerca + Escludi dall'Indice di Ricerca + Apri Opzioni di Indicizzazione di Windows + Gestisci file e cartelle indicizzati + Impossibile aprire le Opzioni di Indicizzazione di Windows + Aggiungi ad Accesso Rapido + Aggiungi elemento corrente all'Accesso Rapido + Aggiunto con successo + Aggiunto con successo ad Accesso Rapido + Rimosso con Successo + Rimosso con successo da Accesso Rapido + Aggiungi ad Accesso Rapido in modo che possa essere aperto con la parola chiave di ricerca dell'Esplora Risorse + Rimuovi da Accesso Rapido + Rimuovi da Accesso Rapido + Rimuovi elemento corrente dall'Accesso Rapido + Mostra Menu Contestuale di Windows + Apri Con + Seleziona un programma con cui aprire + + + {0} disponibili su {1} + Aprire con il Gestore File Predefinito + + Usa '>' per cercare in questa directory, '*' per cercare estensioni file o '>*' per combinare entrambe le ricerche. + - Failed to load Everything SDK + Impossibile caricare l'SDK di Everything Attenzione: Il servizio "Everything" non è in esecuzione Errore nell'interrogazione di Everything Ordina per - Name + Nome Percorso Dimensioni Estensione @@ -125,16 +144,22 @@ Attenzione: Questa non è un'opzione di ordinamento rapido, le ricerche potrebbero essere lente - Search Full Path - - Click to launch or install Everything + Cerca Percorso Completo + Enable File/Folder Run Count + + Clicca per avviare o installare Everything Installazione di Everything Installazione di everything. Si prega di attendere... Everything è stato installato con successo - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com + Impossibile installare automaticamente il servizio Everything. Si prega di installarlo manualmente da https://www.voidtools.com Premi per avviare Impossibile trovare l'installazione di Everything, vuoi inserire manualmente un percorso? {0} {0} Premi no per installare automaticamente Everything - Do you want to enable content search for Everything? - It can be very slow without index (which is only supported in Everything v1.5+) + Vuoi abilitare la ricerca di contenuti per Everything? + Può essere molto lento senza indice (che è supportato solo in Everything v1.5+) + + Menu Contestuale Nativo + Visualizza il menu contestuale nativo (sperimentale) + Sotto si può specificare elementi che si vuole includere nel menu contestuale, che possono essere parziali (es 'pri co') o completi ('Apri con'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ja.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ja.xaml index bca840028..df6199976 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ja.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ja.xaml @@ -16,6 +16,8 @@ The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative Error occurred during search: {0} + Could not open folder + Could not open file 削除 @@ -25,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + サイズ + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -32,6 +40,7 @@ Shell Path Index Search Excluded Paths Use search result's location as the working directory of the executable + Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options Search: @@ -48,11 +57,17 @@ Direct Enumeration ファイル エディターのパス フォルダー エディターのパス + Enabled + Disabled Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -97,11 +112,15 @@ Remove from Quick Access Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -126,7 +145,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -137,4 +157,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ko.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ko.xaml index 6c888de04..0d96210e1 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ko.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ko.xaml @@ -16,6 +16,8 @@ 경고 메시지가 꺼졌습니다. 파일 및 폴더 검색을 위한 대안으로 Everything 플러그인을 설치하시겠습니까?{0}{0}Everything 플러그인을 설치하려면 '예'를 선택하고, 반환하려면 '아니오'를 선택하십시오 Explorer Alternative Error occurred during search: {0} + Could not open folder + Could not open file 삭제 @@ -25,6 +27,12 @@ 사용자 지정 액션 키워드 빠른 접근 항목 Everything 설정 + 미리보기 패널 + 크기 + 만든 날짜 + 수정한 날짜 + 파일 정보 표시 + 시간과 날짜 형식 정렬 옵션: Everything 경로: Launch Hidden @@ -32,6 +40,7 @@ 쉘 경로 색인 제외 경로 검색 결과위치를 실행 가능한 작업 디렉토리(Working Directory)로 사용 + Enter 키를 눌렀을 때 기본 파일 관리자에서 폴더 열기 Use Index Search For Path Search 색인 옵션 검색: @@ -48,11 +57,17 @@ Direct Enumeration 파일 편집기 경로 폴더 편집기 경로 + + Disabled 내용 검색 엔진 경로 재귀 검색 엔진 색인 검색 엔진 윈도우 색인 설정 열기 + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine 탐색기 @@ -97,11 +112,15 @@ 빠른 접근에서 제거 이 항목을 빠른 접근에서 제거 우클릭 메뉴 보기 - - + 함께 열기 + 이 항목을 열 프로그램을 선택 + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -126,7 +145,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Everything을 실행 또는 설치하려면 클릭 Everything 설치 Everything 서비스 설치 중. 잠시 기다려주세요... @@ -137,4 +157,9 @@ Eveyrhing으로 내용 검색을 활성화하시겠습니까? 인덱스가 없으면 매우 느릴 수 있습니다.(Everything v1.5+에서만 지원) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nb.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nb.xaml index c93b07bf3..9c908c569 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nb.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nb.xaml @@ -2,139 +2,164 @@ - Please make a selection first - Please select a folder link - Are you sure you want to delete {0}? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} - Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword - Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword - The required service for Windows Index Search does not appear to be running - To fix this, start the Windows Search service. Select here to remove this warning - The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return - Explorer Alternative - Error occurred during search: {0} + Vennligst gjør et valg først + Velg en mappekobling + Er du sikker på at du ønsker å slette {0}? + Er du sikker på at du vil slette denne filen permanent? + Er du sikker på at du vil slette denne filen/mappen permanent? + Vellykket sletting + {0} ble slettet + Hvis du tilordner det globale handlingsnøkkelordet, kan det få for mange resultater under søk. Velg et bestemt handlingsnøkkelord + Hurtigtilgang kan ikke stilles til det globale handlingsnøkkelordet når det er aktivert. Vennligst velg et spesifikt handlingsnøkkelord + Den nødvendige tjenesten for Windows Index Search ser ikke ut til å kjøre + For å fikse dette, start Windows-søketjenesten. Velg her for å fjerne denne advarselen + Advarselen er slått av. Som et alternativ for å søke filer og mapper, vil du installere Everything-programtillegget?{0}{0}Velg 'Ja' for å installere Everything-programtillegget, eller 'Nei' for å returnere + Alternativ til utforsker + Feil oppstod under søk: {0} + Kunne ikke åpne mappe + Kunne ikke åpne fil - Delete - Edit - Add - General Setting - Customise Action Keywords - Quick Access Links - Everything Setting - Sort Option: - Everything Path: - Launch Hidden - Editor Path - Shell Path - Index Search Excluded Paths - Use search result's location as the working directory of the executable - Use Index Search For Path Search - Indexing Options - Search: - Path Search: - File Content Search: - Index Search: - Quick Access: - Current Action Keyword - Done - Enabled - When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword + Slett + Rediger + Legg til + Generell innstilling + Tilpass handlingsnøkkelord + Koblinger for hurtigtilgang + Everything-innstilling + Forhåndsvisningspanel + Størrelse + Dato opprettet + Dato endret + Vis filinfo + Dato- og klokkeslettformat + Alternativ for sortering: + Everything-sti: + Start skjult + Sti til redigeringsprogram + Skall-sti + Søk i indekser ekskluderte baner + Bruk søkeresultatets plassering som arbeidskatalogen til den kjørbare filen + Trykk Enter for å åpne mappen i standard filbehandler + Bruk indekssøk for banesøk + Alternativer for indeksering + Søk: + Søk etter filbane: + Søk etter filinnhold: + Indekssøk: + Hurtigtilgang: + Nåværende nøkkelord for handling + Utført + Aktivert + Når deaktivert vil Flow ikke utføre dette søkealternativet, og vil i tillegg gå tilbake til '*' for å frigjøre handlingsnøkkelordet Everything - Windows Index - Direct Enumeration + Windows-indeks + Direkte oppregning Filredigeringsbane Mapperedigeringsbane + Aktivert + Deaktivert - Content Search Engine - Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + Søkemotor for innhold + Rekursiv søkemotor for katalog + Indeks-søkemotor + Åpne Windows-indeksalternativet + Ekskluderte filtyper (kommaseparert) + For eksempel: exe,jpg,png + Maksimalt antall resultater + Maksimalt antall forespurte resultater fra aktiv søkemotor - Explorer - Find and manage files and folders via Windows Search or Everything + Utforsker + Finn og administrer filer og mapper via Windows Søk eller Everything - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Ctrl + Enter for å åpne katalogen + Ctrl + Enter for å åpne mappen som inneholder - Copy path - Copy path of current item to clipboard - Copy - Copy current file to clipboard - Copy current folder to clipboard - Delete - Permanently delete current file - Permanently delete current folder - Path: - Delete the selected - Run as different user - Run the selected using a different user account - Open containing folder - Open the location that contains current item - Open With Editor: - Failed to open file at {0} with Editor {1} at {2} - Open With Shell: - Failed to open folder {0} with Shell {1} at {2} - Exclude current and sub-directories from Index Search - Excluded from Index Search - Open Windows Indexing Options - Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access - Add current item to Quick Access - Successfully Added - Successfully added to Quick Access - Successfully Removed - Successfully removed from Quick Access - Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access - Remove current item from Quick Access - Show Windows Context Menu - - - {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Kopier sti + Kopier sti til gjeldende element til utklippstavlen + Kopier + Kopier gjeldende fil til utklippstavlen + Kopier gjeldende mappe til utklippstavlen + Slett + Slett gjeldende fil permanent + Slett gjeldende mappe permanent + Sti: + Slett valgte + Kjør som annen bruker + Kjør valgte med en annen brukerkonto + Åpne inneholdende mappe + Åpne plasseringen som inneholder gjeldende element + Åpne med redigeringsprogram: + Kunne ikke åpne filen på {0} med redigeringsprogram {1} ved {2} + Åpne med Skall: + Kunne ikke å åpne mappe {0} med Skall {1} ved {2} + Utelukk gjeldende og underkataloger fra Indekssøk + Ekskludert fra indekssøk + Åpne Windows indekseringsalternativer + Behandle indekserte filer og mapper + Kunne ikke åpne Windows indekseringsalternativer + Legg til hurtigtilgang + Legg gjeldende element til hurtigtilgang + Lagt til + Vellykket lagt til hurtigtilgang + Fjernet + Fjernet fra hurtigtilgang + Legg til i hurtigtilgang, slik at den kan åpnes med handlingsnøkkelordet Søkeaktivering i Utforsker + Fjern fra hurtigtilgang + Fjern fra hurtigtilgang + Fjern gjeldende element fra hurtigtilgang + Vis Windows-hurtigmeny + Åpne med + Velg et program for å åpne med + + + {0} ledig av {1} + Åpne i standard filbehandler + + Bruk '>' for å søke i denne katalogen, '*' for å søke etter filendelser eller '>*' for å kombinere begge søkene. + - Failed to load Everything SDK - Warning: Everything service is not running - Error while querying Everything - Sort By - Name - Path - Size - Extension - Type Name - Date Created - Date Modified - Attributes - File List FileName - Run Count - Date Recently Changed - Date Accessed - Date Run + Kan ikke laste inn Everything-SDK + Advarsel: Everything-tjenesten kjører ikke + Feil under spørring av Everything + Sorter etter + Navn + Sti + Størrelse + Utvidelse + Typenavn + Dato opprettet + Dato endret + Attributter + Filliste Filnavn + Antall kjøringer + Dato nylig endret + Dato for tilgang + Dato for kjøring - Warning: This is not a Fast Sort option, searches may be slow + Advarsel: dette er ikke et hurtigsorteringsalternativ, søk kan være trege - Search Full Path - - Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you - Do you want to enable content search for Everything? - It can be very slow without index (which is only supported in Everything v1.5+) + Søk i hele banen + Aktiver antall fil-/mappekjøringer + Klikk for å starte eller installere Everything + Installasjon av Everything + Installerer Everything-tjenesten. Vennligst vent... + Everything-tjenesten vellykket installert + Kunne ikke installere Everything-tjenesten automatisk. Installer den manuelt fra https://www.voidtools.com + Klikk her for å starte den + Kunne ikke finne en Everything-installasjon, vil du manuelt velge en plassering?{0}{0}Klikk nei, og Everything vil bli automatisk installert for deg + Vil du aktivere innholdssøk for Everything? + Det kan være veldig tregt uten indeks (som bare støttes i Everything v1.5+) + + + Opprinnelig hurtigmeny + Vis opprinnelig hurtigmeny (eksperimentell) + Nedenfor kan du spesifisere elementer du vil inkludere i hurtigtmenyen, de kan være delvise (f.eks. 'pne me') eller komplette ('Åpne med'). + Nedenfor kan du spesifisere elementer du vil ekskludere fra hurtigtmenyen, de kan være delvise (f.eks. 'pne me') eller komplette ('Åpne med'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nl.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nl.xaml index f07c9f23a..38e45aaa8 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/nl.xaml @@ -16,6 +16,8 @@ The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative Error occurred during search: {0} + Could not open folder + Could not open file Verwijder @@ -25,13 +27,20 @@ Customise Action Keywords Quick Access Links Everything Setting + Voorbeeld Paneel + Grootte + Datum aangemaakt + Datum gewijzigd + Bestandsinformatie weergeven + Formaat voor datum en tijd Sort Option: Everything Path: Launch Hidden - Editor Path + Editor pad Shell Path Index Search Excluded Paths Use search result's location as the working directory of the executable + Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options Search: @@ -48,11 +57,17 @@ Direct Enumeration Bestandseditor pad Pad naar mapeditor + Enabled + Disabled Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -97,44 +112,54 @@ Remove from Quick Access Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK - Warning: Everything service is not running - Error while querying Everything - Sort By + Waarschuwing: Everything service is niet actief + Fout bij het opvragen Everything + Sorteer op Name - Path + Pad Size - Extension - Type Name - Date Created - Date Modified - Attributes - File List FileName - Run Count - Date Recently Changed - Date Accessed - Date Run + Uitbreiding + Type naam + Datum aangemaakt + Datum gewijzigd + Kenmerken + Bestandslijst Bestandsnaam + Aantal keer uitgevoerd + Datum onlangs gewijzigd + Datum laatst geopend + Datum uitvoering - Warning: This is not a Fast Sort option, searches may be slow + Waarschuwing: Dit is geen snelle sorteeroptie, zoekopdrachten kunnen traag zijn Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you + Everything geïnstalleerd + Installeren Everything alles service. Een ogenblik geduld... + Everything service is succesvol geïnstalleerd + Automatisch installeren mislukt. Installeer de Everything-service handmatig, vanaf https://www.voidtools.com + Klik hier om te starten + Kan geen 'Everything' vinden, wilt u handmatig een locatie selecteren?{0}{0}Klik op nee en alles zal automatisch voor u worden geïnstalleerd Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml index 58dc1d33b..324b8ad36 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml @@ -2,139 +2,164 @@ - Please make a selection first + Pierw dokonaj wyboru Musisz wybrać któryś folder z listy Czy jesteś pewien że chcesz usunąć {0}? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} - Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword - Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword - The required service for Windows Index Search does not appear to be running - To fix this, start the Windows Search service. Select here to remove this warning - The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return - Explorer Alternative - Error occurred during search: {0} + Jesteś pewny, że chcesz usunąć ten plik trwale? + Czy na pewno chcesz trwale usunąć ten plik/folder? + Usunięcie powiodło się + Pomyślnie usunięto {0} + Przypisanie globalnego słowa kluczowego akcji może spowodować wyświetlenie zbyt wielu wyników podczas wyszukiwania. Proszę wybrać konkretne słowo kluczowe akcji + Szybki dostęp nie może być ustawiony jako globalny klawisz akcji, gdy jest włączony. Proszę wybrać konkretny klawisz akcji + Wymagana usługa dla Windows Index Search nie wydaje się być uruchomiona + Aby to naprawić, uruchom usługę wyszukiwania Windows. Wybierz tutaj, aby usunąć to ostrzeżenie + Komunikat ostrzegawczy został wyłączony. Jako alternatywę do wyszukiwania plików i folderów, czy chcesz zainstalować wtyczkę Everything?{0}{0}Wybierz 'Tak', aby zainstalować wtyczkę Everything, lub 'Nie', aby powrócić + Alternatywa dla Eksploratora + Wystąpił błąd podczas wyszukiwania: {0} + Nie można otworzyć folderu + Nie można otworzyć pliku Usuń Edytuj Dodaj - General Setting - Customise Action Keywords - Quick Access Links - Everything Setting - Sort Option: - Everything Path: - Launch Hidden + Ustawienia ogólne + Zmień słowa kluczowe akcji + Linki szybkiego dostępu + Ustawienia Everything + Panel podglądu + Rozmiar + Data utworzenia + Data modyfikacji + Wyświetl informacje o pliku + Format daty i czasu + Opcje sortowania: + Ścieżka Everything: + Uruchom w tle Ścieżka edytora - Shell Path - Index Search Excluded Paths - Use search result's location as the working directory of the executable - Use Index Search For Path Search - Indexing Options - Search: - Path Search: - File Content Search: - Index Search: - Quick Access: - Current Action Keyword + Ścieżka powłoki + Wykluczone ścieżki indeksu + Użyj lokalizacji wyników wyszukiwania jako katalogu roboczego pliku wykonywalnego + Naciśnij Enter, aby otworzyć folder w domyślnym menedżerze plików + Użyj wyszukiwania indeksowego do przeszukiwania ścieżek + Opcje indeksowania + Szukaj: + Szukaj w ścieżce: + Przeszukiwanie zawartości plików: + Wyszukiwanie indeksowe: + Szybki dostęp: + Bieżące słowo kluczowe akcji Zapisz - Enabled - When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword + Aktywny + Po wyłączeniu Flow nie będzie wykonywać tej opcji wyszukiwania, a dodatkowo przywróci '*', aby zwolnić słowo kluczowe akcji Everything - Windows Index - Direct Enumeration + Indeks Windows + Bezpośrednie wyliczanie Ścieżka edytora plików Ścieżka edytora folderów + Aktywny + Wyłączony - Content Search Engine - Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + Wyszukiwarka zawartości + Rekurencyjna wyszukiwarka katalogów + Wyszukiwarka zawartości + Otwórz opcje indeksowania Windows + Wykluczone typy plików (oddzielone przecinkami) + Na przykład: exe,jpg,png + Maksymalna liczba wyników + Maksymalna liczba wyników żądanych przez aktywną wyszukiwarkę Explorer - Find and manage files and folders via Windows Search or Everything + Wyszukaj i zarządzaj plikami oraz folderami za pomocą Windows Search lub Everything - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Ctrl + Enter, aby otworzyć folder + Ctrl + Enter, aby otworzyć folder zawierający - Copy path - Copy path of current item to clipboard - Copy - Copy current file to clipboard - Copy current folder to clipboard + Skopiuj Ścieżkę + Kopiuj ścieżkę bieżącego elementu do schowka + Kopiuj + Kopiuj bieżący plik do schowka + Kopiuj bieżący folder do schowka Usu - Permanently delete current file - Permanently delete current folder - Path: - Delete the selected - Run as different user - Run the selected using a different user account - Open containing folder - Open the location that contains current item - Open With Editor: - Failed to open file at {0} with Editor {1} at {2} - Open With Shell: - Failed to open folder {0} with Shell {1} at {2} - Exclude current and sub-directories from Index Search - Excluded from Index Search - Open Windows Indexing Options - Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access - Add current item to Quick Access - Successfully Added - Successfully added to Quick Access - Successfully Removed - Successfully removed from Quick Access - Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access - Remove current item from Quick Access - Show Windows Context Menu - - - {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Trwale usuń bieżący plik + Trwale usuń bieżący folder + Ścieżka: + Usuń zaznaczone + Uruchom jako inny użytkownik + Uruchom wybrane używając innego konta użytkownika + Otwórz folder nadrzędny + Otwórz lokalizację zawierającą bieżący element + Otwórz w edytorze: + Nie udało się otworzyć pliku {0} w edytorze {1} pod ścieżką {2} + Otwórz za pomocą powłoki: + Nie udało się otworzyć folderu {0} za pomocą powłoki {1} w {2} + Wyklucz bieżący i podkatalogi z wyszukiwania indeksowego + Wykluczone z wyszukiwania indeksowego + Otwórz opcje indeksowania Windows + Zarządzaj zindeksowanymi plikami i folderami + Nie udało się otworzyć opcji indeksowania Windows + Dodaj do szybkiego dostępu + Dodaj bieżący element do szybkiego dostępu + Pomyślnie dodano + Pomyślnie dodano do szybkiego dostępu + Pomyślnie usunięto + Pomyślnie usunięto z szybkiego dostępu + Dodaj do szybkiego dostępu, aby można było otworzyć za pomocą słowa kluczowego akcji aktywacji wyszukiwania Eksploratora + Usunieto z szybkiego dostępu + Usunieto z szybkiego dostępu + Usuń bieżący element z szybkiego dostępu + Pokaż menu kontekstowe Windows + Otwórz za pomocą + Wybierz program do otwarcia + + + {0} wolne z {1} + Otwórz w domyślnym menedżerze plików + + Użyj '>' aby przeszukać ten folder, '' aby wyszukać rozszerzenia plików lub '>' aby połączyć oba wyszukiwania. + - Failed to load Everything SDK + Nie udało się załadować wszystkich SDK Everything Service nie jest uruchomiony Wystąpił błąd podczas pobierania wyników z Everything - Sort By - Name - Path + Sortuj wg + Nazwa + Ścieżka Rozmiar - Extension - Type Name - Date Created - Date Modified - Attributes - File List FileName - Run Count - Date Recently Changed - Date Accessed - Date Run + Rozszerzenia + Nazwa typu + Data utworzenia + Data modyfikacji + Atrybuty + Nazwa pliku na liście plików + Liczba uruchomień + Data ostatniej zmiany + Data dostępu + Data uruchomienia - Warning: This is not a Fast Sort option, searches may be slow + Uwaga: To nie jest opcja Szybkiego sortowania, wyszukiwania mogą być wolne - Search Full Path - - Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you - Do you want to enable content search for Everything? - It can be very slow without index (which is only supported in Everything v1.5+) + Wyszukaj pełną ścieżkę + Włącz licznik uruchomień plików/folderów + Kliknij, aby uruchomić lub zainstalować Everything + Instalacja Everything + Trwa instalacja usługi Everything. Proszę czekać... + Pomyślnie zainstalowano usługę Everything + Nie udało się automatycznie zainstalować usługi Everything. Proszę zainstalować ją ręcznie z https://www.voidtools.com + Kliknij tutaj, aby rozpocząć + Nie można znaleźć instalacji programu Everything. Czy chcesz ręcznie wybrać lokalizację?{0}{0}Kliknij "Nie", a program Everything zostanie automatycznie zainstalowany + Czy chcesz włączyć wyszukiwanie zawartości dla programu Everything? + Może działać bardzo wolno bez indeksu (który jest obsługiwany tylko w Everything w wersji 1.5 i nowszych) + + + Natywne menu kontekstowe + Wyświetl natywne menu kontekstowe (eksperymentalne) + Poniżej możesz określić elementy, które chcesz uwzględnić w menu kontekstowym. Mogą być one częściowe (np. "otw w") lub pełne ("Otwórz za pomocą"). + Poniżej możesz określić elementy, które chcesz wykluczyć z menu kontekstowego. Mogą być one częściowe (np. "otw w") lub pełne ("Otwórz za pomocą"). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-br.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-br.xaml index f1c227e4c..058bd353f 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-br.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-br.xaml @@ -16,6 +16,8 @@ The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative Error occurred during search: {0} + Could not open folder + Could not open file Apagar @@ -23,8 +25,14 @@ Adicionar General Setting Customise Action Keywords - Quick Access Links + Links de Acesso Rápido Everything Setting + Preview Panel + Tamanho + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -32,9 +40,10 @@ Shell Path Index Search Excluded Paths Use search result's location as the working directory of the executable + Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options - Search: + Pesquisar: Path Search: File Content Search: Index Search: @@ -48,14 +57,20 @@ Direct Enumeration File Editor Path Folder Editor Path + Enabled + Disabled Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine - Explorer + Explorador Find and manage files and folders via Windows Search or Everything @@ -65,13 +80,13 @@ Copy path Copy path of current item to clipboard - Copy + Copiar Copy current file to clipboard Copy current folder to clipboard Apagar Permanently delete current file Permanently delete current folder - Path: + Caminho: Delete the selected Run as different user Run the selected using a different user account @@ -97,18 +112,22 @@ Remove from Quick Access Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK Warning: Everything service is not running Error while querying Everything Sort By - Name + Nome Path Tamanho Extension @@ -126,7 +145,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -137,4 +157,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-pt.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-pt.xaml index 89939639b..27f98449f 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-pt.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pt-pt.xaml @@ -16,6 +16,8 @@ A mensagem de aviso foi desativada. Como alternativa ao serviço de pesquisa Windows, gostaria de instalar o plugin Everything?{0}{0}Selecione 'Sim' para instalar ou 'Não' para não instalar. Alternativa Ocorreu um erro ao pesquisar: {0} + Não foi possível abrir a pasta + Não foi possível abrir o ficheiro Eliminar @@ -25,6 +27,12 @@ Personalizar palavras-chave Ligações de acesso rápido Definições Everything + Painel de pré-visualização + Tamanho + Data de criação + Data de modificação + Mostrar informações do ficheiro + Formato de data e de hora Ordenação: Caminho para Everything: Iniciar oculto @@ -32,6 +40,7 @@ Caminho da consola Caminhos excluídos do índice de pesquisa Utilizar localização do resultado da pesquisa como pasta de trabalho do executável + Toque Enter para abrir a pasta no gestor de ficheiros Utilizar índice de pesquisa para o caminho Opções de indexação Pesquisar: @@ -48,11 +57,17 @@ Enumeração direta Caminho do editor de ficheiros Caminho do editor de pastas + Ativo + Inativo Mecanismo de pesquisa para conteúdo Mecanismo de pesquisa recursiva de diretórios Mecanismo de pesquisa do índice Abrir opções de índice do Windows + Tipos de ficheiros excluídos (separados por vírgula) + Exemplo: exe,jpg,png + Número máximo de resultados + O número máximo de resultados solicitados ao motor de pesquisa ativo Explorador @@ -97,11 +112,15 @@ Remover do acesso rápido Remover item do Acesso rápido Mostrar menu de contexto do Windows - - + Abrir com + Selecione o programa a utilizar + + {0} livre de {1} no total Abrir no gestor de ficheiros padrão - Utilize '>' para pesquisar nesta pasta, '*' para pesquisar por extensão de ficheiro e '>*' para combinar ambas as anteriores. + + Utilize '>' para pesquisar nesta pasta, '*' para pesquisar por extensão de ficheiro e '>*' para combinar ambas as anteriores. + Falha ao carregar Everything SDK @@ -126,7 +145,8 @@ Aviso: esta não é uma opção de ordenação rápida e as pesquisas podem ser demoradas Pesquisar caminho completo - + Ativar contagem de execução de ficheiros/pastas + Clique para iniciar ou instalar Everything Instalação Everything A instalar o serviço Everything. Por favor aguarde... @@ -137,4 +157,9 @@ Deseja ativar a pesquisa de conteúdo através de Everything? Pode ser muito lento sem índice (que apenas existe em Everything v1.5+) + + Menu de contexto nativo + Mostrar menu de contexto nativo (experimental) + Aqui pode especificar os itens a incluir no menu de contexto. Podem ser parciais (ex.: 'brir co') ou completos ('Abrir com'). + Aqui pode especificar os itens a excluir do menu de contexto. Podem ser parciais (ex.: 'brir co') ou completos ('Abrir com'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ru.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ru.xaml index 6f192d3c1..911364fdf 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ru.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/ru.xaml @@ -2,20 +2,22 @@ - Please make a selection first - Please select a folder link - Are you sure you want to delete {0}? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} - Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword + Сначала отметьте что-нибудь + Пожалуйста, выберите ссылку на папку + Вы уверены, что хотите удалить {0}? + Вы действительно хотите безвозвратно удалить этот файл? + Вы действительно хотите безвозвратно удалить этот файл/папку? + Удаление завершено + Успешно удалено {0} + Назначение ключевого слова глобальных действий может привести к слишком большому количеству результатов поиска. Пожалуйста, выберите конкретное ключевое слово действий Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword The required service for Windows Index Search does not appear to be running To fix this, start the Windows Search service. Select here to remove this warning The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative - Error occurred during search: {0} + При поиске произошла ошибка: {0} + Не удалось открыть папку + Не удалось открыть файл Удалить @@ -25,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Размер + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -32,9 +40,10 @@ Shell Path Index Search Excluded Paths Use search result's location as the working directory of the executable + Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options - Search: + Поиск: Path Search: File Content Search: Index Search: @@ -48,11 +57,17 @@ Direct Enumeration Путь к редактору файлов Путь к редактору папки + Enabled + Отключён Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -63,17 +78,17 @@ Ctrl + Enter to open the containing folder - Copy path + Скопировать путь Copy path of current item to clipboard - Copy + Скопировать Copy current file to clipboard Copy current folder to clipboard Удалить Permanently delete current file Permanently delete current folder - Path: + Путь: Delete the selected - Run as different user + Запустить от имени другого пользователя Run the selected using a different user account Open containing folder Open the location that contains current item @@ -97,11 +112,15 @@ Remove from Quick Access Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -126,7 +145,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -137,4 +157,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml index ee6357eda..35883a09c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sk.xaml @@ -16,6 +16,8 @@ Upozornenie bolo vypnuté. Chceli by ste ako alternatívu na vyhľadávanie súborov a priečinkov nainštalovať plugin Everything?{0}{0}Ak chcete nainštalovať plugin Everything, zvoľte 'Áno', pre návrat zvoľte 'Nie' Alternatíva pre Prieskumníka Počas vyhľadávania došlo k chybe: {0} + Nepodarilo sa otvoriť priečinok + Nepodarilo sa otvoriť súbor Odstrániť @@ -25,6 +27,12 @@ Upraviť aktivačný príkaz Odkazy Rýchleho prístupu Nastavenia Everything + Panel náhľadu + Veľkosť + Dátum vytvorenia + Dátum úpravy + Zobraziť informácie o súbore + Formát dátumu a času Zoradenie: Umiestnenie Everything: Spustiť skryté @@ -32,6 +40,7 @@ Cesta k príkazovému riadku Vylúčené umiestnenia indexovania Použiť umiestnenie výsledku vyhľadávania ako pracovný priečinok spustiteľného súboru + Stlačením klávesu Enter otvoriť priečinok v predvolenom správcovi súborov Na vyhľadanie cesty použiť vyhľadávanie v indexe Možnosti indexovania Vyhľadávanie: @@ -48,11 +57,17 @@ Zoznam priečinkov Cesta k editoru súborov Cesta k editoru priečinkov + Povolené + Vypnuté Vyhľadávač obsahu Priečinkový rekurzívny vyhľadávač Indexový vyhľadávač Otvoriť možnosti vyhľadávania vo Windowse + Vylúčené typy súborov (oddelené čiarkou) + Napr.: exe,jpg,png + Maximum výsledkov + Maximálny počet výsledkov pre aktívny vyhľadávač Prieskumník @@ -97,11 +112,15 @@ Odstráni z Rýchleho prístupu Odstrániť aktuálnu položku z Rýchleho prístupu Zobraziť kontextovú ponuku Windowsu - - + Otvoriť s + Vyberte program, ktorým chcete otvoriť súbor + + Voľných {0} z {1} Otvoriť v predvolenom správcovi súborov - Použite ">" na vyhľadávanie v tomto priečinku, "*" na vyhľadávanie prípon súborov alebo ">*" na kombináciu oboch hľadaní. + + Použite ">" na vyhľadávanie v tomto priečinku, "*" na vyhľadávanie prípon súborov alebo ">*" na kombináciu oboch hľadaní. + Nepodarilo sa načítať SDK Everything @@ -126,7 +145,8 @@ Upozornenie: Toto nie je voľba Fast Sort, vyhľadávanie môže byť pomalé Vyhľadávanie celej cesty - + Povoliť počítadlo spustení súboru/priečinka + Kliknutím spustíte alebo nainštalujete Everything Inštalácia Everything Inštaluje sa služba Everything. Čakajte, prosím… @@ -137,4 +157,9 @@ Chcete povoliť vyhľadávanie obsahu cez Everything? Bez indexu (ktorý je podporovaný len v Everything v1.5+) to môže byť veľmi pomalé + + Natívna kontextová ponuka + Zobraziť natívnu kontextovú ponuku (experimentálne) + Nižšie môžete určiť položky, ktoré chcete zahrnúť do kontextovej ponuky, môžu byť čiastočné (napr. "tvoriť v program") alebo úplné ("Otvoriť v programe"). + Nižšie môžete určiť položky, ktoré chcete vylúčiť z kontextovej ponuky, môžu byť čiastočné (napr. "tvoriť v program") alebo úplné ("Otvoriť v programe"). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sr.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sr.xaml index 2e30fbce1..f1227df40 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/sr.xaml @@ -16,6 +16,8 @@ The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative Error occurred during search: {0} + Could not open folder + Could not open file Obriši @@ -25,6 +27,12 @@ Customise Action Keywords Quick Access Links Everything Setting + Preview Panel + Size + Date Created + Date Modified + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -32,6 +40,7 @@ Shell Path Index Search Excluded Paths Use search result's location as the working directory of the executable + Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options Search: @@ -48,11 +57,17 @@ Direct Enumeration File Editor Path Folder Editor Path + Enabled + Disabled Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine Explorer @@ -97,11 +112,15 @@ Remove from Quick Access Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -126,7 +145,8 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything Installation Installing Everything service. Please wait... @@ -137,4 +157,9 @@ Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml index 8b4112dc3..1e09ba103 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml @@ -5,136 +5,161 @@ Please make a selection first Lütfen bir klasör bağlantısı seçin {0} bağlantısını silmek istediğinize emin misiniz? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? + Bu dosyayı kalıcı olarak silmek istediğinizden emin misiniz? + Bu öğeyi kalıcı olarak silmek istediğinizden emin misiniz? Deletion successful - Successfully deleted {0} + Başarıyla silindi: {0} Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword The required service for Windows Index Search does not appear to be running To fix this, start the Windows Search service. Select here to remove this warning The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative - Error occurred during search: {0} + Arama sırasında hata oluştu: {0} + Klasör açılamadı + Dosya açılamadı Sil Düzenle Ekle - General Setting - Customise Action Keywords + Genel Ayarlar + Anahtar Kelimeler Quick Access Links - Everything Setting - Sort Option: - Everything Path: + Everything Ayarları + Önizleme Paneli + Boyut + Oluşturma Tarihi + Değiştirme Tarihi + Dosya Özelliklerini Göster + Tarih ve saat biçimi + Sıralama Seçeneği: + Everything Kurulumu: Launch Hidden - Düzenleyici Konumu - Shell Path - Index Search Excluded Paths + Metin Düzenleyici + Komut İstemi + Hariç Tutulan Dizinler Programın çalışma klasörü olarak sonuç klasörünü kullan + Hit Enter to open folder in Default File Manager Use Index Search For Path Search Indexing Options - Search: - Path Search: - File Content Search: + Ara: + Yolu Ara: + Dosya İçeriğini Ara: Index Search: - Quick Access: - Current Action Keyword + Hızlı Erişim: + Geçerli Anahtar Kelime Tamam - Enabled + Etkin When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword Everything - Windows Index - Direct Enumeration - Düzenleyici Konumu - Folder Editor Path + Windows Arama + Doğrudan Sayım + Metin Düzenleyici + Klasör Düzenleyici + Enabled + Disabled - Content Search Engine + Dosya İçeriği Arama Motoru Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + Arama Motoru + Windows Dizin Oluşturma Ayarları + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine - Explorer + Dosya Gezgini Find and manage files and folders via Windows Search or Everything - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Dizini açmak içi Ctrl + Enter + Dosya konumunu açmak için Ctrl + Enter - Copy path - Copy path of current item to clipboard - Copy - Copy current file to clipboard - Copy current folder to clipboard + Yolu kopyala + Mevcut dosya konumunu panoya kopyala + Kopyala + Mevcut dosyayı panoya kopyala + Mevcut klasörü panoya kopyala Sil - Permanently delete current file - Permanently delete current folder - Path: - Delete the selected - Run as different user + Mevcut dosyayı kalıcı olarak sil + Mevcut klasörü kalıcı olarak sil + Yol: + Seçileni sil + Başka bir kullanıcı olarak çalıştır Run the selected using a different user account - Open containing folder + Dosya konumunu aç Open the location that contains current item - Open With Editor: + Metin Düzenleyici ile Aç: Failed to open file at {0} with Editor {1} at {2} - Open With Shell: + Komut İstemi ile Aç: Failed to open folder {0} with Shell {1} at {2} Exclude current and sub-directories from Index Search Excluded from Index Search - Open Windows Indexing Options + Windows Dizin Oluşturma Ayarları Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access + Windows Dizin Oluşturma ayarlarını açma başarısız oldu + Hızlı Erişime Sabitle Add current item to Quick Access - Successfully Added + Başarıyla Eklendi Successfully added to Quick Access - Successfully Removed + Başarıyla Kaldırıldı Successfully removed from Quick Access Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access + Hızlı Erişimden Kaldır + Hızlı Erişimden Kaldır Remove current item from Quick Access - Show Windows Context Menu - - + Windows Bağlam Menüsünü Aç + Birlikte Aç + Select a program to open with + + {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Dosya Yöneticisinde Aç + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + - Failed to load Everything SDK + Everything SDK'sini yükleme başarısız oldu Everything Servisi çalışmıyor Sorgu Everything üzerinde çalıştırılırken hata oluştu - Sort By + Şuna Göre Sırala Name - Path + Yol Boyut - Extension - Type Name - Date Created - Date Modified - Attributes + Uzantı + Tür + Oluşturma Tarihi + Değiştirme Tarihi + Özellikler File List FileName - Run Count + Erişim Sayısı Date Recently Changed - Date Accessed + Erişim Tarihi Date Run Warning: This is not a Fast Sort option, searches may be slow - Search Full Path - - Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it + Tam Yolu Ara + Enable File/Folder Run Count + + Everything'i yükle veya başlat + Everything Kurulumu + Everything hizmeti yükleniyor. Lütfen bekleyin... + Everything hizmeti başarıyla yüklendi + Everything hizmetini otomatik olarak yükleme başarısız oldu. Lütfen https://www.voidtools.com adresinden elle yükleyin. + Başlat Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/uk-UA.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/uk-UA.xaml index a842939e3..d96b5b503 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/uk-UA.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/uk-UA.xaml @@ -2,139 +2,165 @@ - Please make a selection first - Please select a folder link - Are you sure you want to delete {0}? - Are you sure you want to permanently delete this file? - Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} - Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword - Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword - The required service for Windows Index Search does not appear to be running - To fix this, start the Windows Search service. Select here to remove this warning - The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return - Explorer Alternative - Error occurred during search: {0} + Будь ласка, спочатку зробіть вибір + Будь ласка, оберіть посилання на теку + Ви впевнені, що хочете видалити {0}? + Ви впевнені, що хочете назавжди видалити цей файл? + Ви впевнені, що хочете назавжди видалити цей файл/теку? + Видалення виконано успішно + Успішно видалено {0} + Використання глобального ключового слова дії може призвести до надмірної кількості результатів під час пошуку. Будь ласка, виберіть конкретне ключове слово дії + Швидкий доступ не може бути встановлений на глобальне ключове слово дії, коли він увімкнений. Будь ласка, виберіть конкретне ключове слово дії + Здається, не запущено потрібну службу для індексного пошуку Windows + Щоб виправити це, запустіть службу пошуку Windows. Натисніть тут, щоб прибрати це попередження + Попередження вимкнено. Як альтернативу для пошуку файлів і папок, чи бажаєте ви встановити плагін Everything? {0}{0}Виберіть "Так", щоб встановити плагін Everything, або "Ні", щоб повернутися + Альтернативний провідник + Виникла помилка під час пошуку: {0} + Не вдалося відкрити папку + Не вдалося відкрити файл Видалити Редагувати Додати - General Setting - Customise Action Keywords - Quick Access Links - Everything Setting - Sort Option: - Everything Path: - Launch Hidden - Editor Path - Shell Path - Index Search Excluded Paths - Use search result's location as the working directory of the executable - Use Index Search For Path Search - Indexing Options - Search: - Path Search: - File Content Search: - Index Search: - Quick Access: - Current Action Keyword + Загальні налаштування + Налаштувати ключові слова дії + Посилання швидкого доступу + Налаштування Everything + Панель поперегляду + Розмір + Дата створення + Дата останньої зміни + Показати інформацію про файл + Формат дати й часу + Варіант сортування: + Шлях до Everything: + Запустити приховано + Шлях до редактора + Шлях до оболонки Shell + Виключені шляхи індексного пошуку + Використовувати розташування результату пошуку як робочу директорію виконуваного файлу + Натисніть Enter, щоб відкрити папку у файловому менеджері за замовчуванням + Використовуйте індексний пошук для пошуку шляху + Параметри індексації + Пошук: + Пошук шляху: + Пошук вмісту файлу: + Індексний пошук: + Швидкий доступ: + Поточне ключове слово дії Готово - Enabled - When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword + Увімкнено + Якщо вимкнено, Flow не виконуватиме цю опцію пошуку і додатково повернеться до '*', щоб звільнити ключове слово дії Everything - Windows Index - Direct Enumeration + Індекс Windows + Пряме перерахування Шлях редактора файлів Шлях редактора папок + Увімкнено + Вимкнено - Content Search Engine - Directory Recursive Search Engine - Index Search Engine - Open Windows Index Option + Пошукова система контенту + Рекурсивна пошукова система за каталогом + Пошукова система індексів + Відкрити параметри індексування Windows + Виключені типи файлів (через кому) + Наприклад: exe,jpg,png + Максимум результатів + Максимальна кількість результатів, отриманих від активної пошукової системи - Explorer - Find and manage files and folders via Windows Search or Everything + Провідник + Пошук і керування файлами та папками за допомогою програми "Пошук" або "Everything" - Ctrl + Enter to open the directory - Ctrl + Enter to open the containing folder + Ctrl + Enter, щоб відкрити каталог + Ctrl + Enter, щоб відкрити відповідну папку - Copy path - Copy path of current item to clipboard - Copy - Copy current file to clipboard - Copy current folder to clipboard + Копіювати шлях + Копіювати шлях до поточного елемента в буфер обміну + Копіювати + Копіювання поточного файлу в буфер обміну + Копіювати поточну папку в буфер обміну Видалити - Permanently delete current file - Permanently delete current folder - Path: - Delete the selected - Run as different user - Run the selected using a different user account - Open containing folder - Open the location that contains current item - Open With Editor: - Failed to open file at {0} with Editor {1} at {2} - Open With Shell: - Failed to open folder {0} with Shell {1} at {2} - Exclude current and sub-directories from Index Search - Excluded from Index Search - Open Windows Indexing Options - Manage indexed files and folders - Failed to open Windows Indexing Options - Add to Quick Access - Add current item to Quick Access - Successfully Added - Successfully added to Quick Access - Successfully Removed - Successfully removed from Quick Access - Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access - Remove current item from Quick Access - Show Windows Context Menu - - - {0} free of {1} - Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Безповоротно видалити поточний файл + Назавжди видалити поточну папку + Шлях: + Видалити вибране + Запустити від імені іншого користувача + Запустити вибране під обліковим записом іншого користувача + Відкрити папку + Відкрийте місце, яке містить поточний елемент + Відкрити за допомогою редактора: + Не вдалося відкрити файл за адресою {0} за допомогою редактора {1} на {2} + Відкрий за допомогою Shell: + Не вдалося відкрити папку {0} за допомогою Shell {1} за адресою {2} + Виключити поточні та вкладені каталоги з індексного пошуку + Виключено з індексного пошуку + Відкрити параметри індексування Windows + Керування проіндексованими файлами та папками + Не вдалося відкрити параметри індексування Windows + Додати до Швидкого доступу + Додати поточний елемент до швидкого доступу + Успішно додано + Успішно додано до швидкого доступу + Успішно вилучено + Успішно видалено з швидкого доступу + Додати до швидкого доступу, щоб його можна було відкрити за допомогою ключового слова дії "Активація пошуку" у Провіднику + Видалити зі швидкого доступу + Видалити зі швидкого доступу + Видалити поточний елемент зі швидкого доступу + Показати контекстне меню Windows + Відкрити за допомогою + Виберіть програму для відкриття + + + {0} не містить {1} + Відкрити у файловому менеджері за замовчуванням + + Використовуйте '>' для пошуку в цьому каталозі, '*' для пошуку за розширеннями файлів або '>*' для поєднання обох варіантів пошуку. + - Failed to load Everything SDK - Warning: Everything service is not running - Error while querying Everything - Sort By - Name - Path - Size - Extension - Type Name - Date Created - Date Modified - Attributes - File List FileName - Run Count - Date Recently Changed - Date Accessed - Date Run + Не вдалося завантажити Everything SDK + Попередження: Служба Everything не запущена + Помилка при виконанні запиту Everything + Сортувати за + Назва + Шлях + Розмір + Розширення + Назва типу + Дата створення + Дата останньої зміни + Атрибути + Список файлів FileName + Кількість запусків + Дата нещодавно змінена + Дата доступу + Дата запуску - Warning: This is not a Fast Sort option, searches may be slow + Попередження: Це не швидке сортування, пошук може бути повільним - Search Full Path - - Click to launch or install Everything - Everything Installation - Installing Everything service. Please wait... - Successfully installed Everything service - Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com - Click here to start it - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you - Do you want to enable content search for Everything? - It can be very slow without index (which is only supported in Everything v1.5+) + Шукати повний шлях + Enable File/Folder Run Count + + Натисніть, щоб запустити або встановити Everything + Встановлення програми Everything + Встановлення служби Everything. Будь ласка, зачекайте... + Успішно встановлено сервіс Everything + Не вдалося автоматично встановити службу Everything. Будь ласка, встановіть її вручну з https://www.voidtools.com + Натисніть тут, щоб розпочати + Не вдалося знайти інсталяцію Everything, бажаєте вручну вибрати розташування? {0}{0}Натисніть "ні", і Everything буде автоматично інстальовано для вас + Бажаєте увімкнути пошук контенту для Everything? + Без індексу (який підтримується лише у версії Everything v1.5+) воно може працювати дуже повільно + + + Рідне контекстне меню + Відображати рідне контекстне меню (експериментально) + Нижче ви можете вказати елементи, які хочете включити до контекстного меню, вони можуть бути частковими (наприклад, «шир пера») або повними («Відкрити за допомогою»). + Нижче ви можете вказати елементи, які хочете виключити з контекстного меню, вони можуть бути частковими (наприклад, «шир пера») або повними («Відкрити за допомогою»). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/vi.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/vi.xaml new file mode 100644 index 000000000..81b5a2f92 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/vi.xaml @@ -0,0 +1,165 @@ + + + + + Vui lòng lựa chọn trước + Hãy chọn một thư mục. + Bạn có chắc chắn muốn xóa {0} không? + Bạn có chắc là muốn xóa vĩnh viễn các ảnh này? + Bạn có chắc là muốn xóa vĩnh viễn các ảnh này? + Xóa thành công + Thành công trong việc xóa + Việc chỉ định từ khóa hành động chung có thể mang lại quá nhiều kết quả trong quá trình tìm kiếm. Vui lòng chọn từ khóa hành động cụ thể + Không thể đặt Truy cập nhanh thành từ khóa hành động chung khi được bật. Vui lòng chọn từ khóa hành động cụ thể + Dịch vụ cần thiết cho Windows Index Search dường như không chạy + Để khắc phục điều này, hãy khởi động dịch vụ Windows Search. Chọn vào đây để xóa cảnh báo này + Thông báo cảnh báo đã bị tắt. Là một giải pháp thay thế cho việc tìm kiếm tệp và thư mục, bạn có muốn cài đặt plugin Mọi thứ không?{0}{0}Chọn 'Có' để cài đặt plugin Mọi thứ hoặc 'Không' để quay lại + Nhà thám hiểm thay thế + Đã xảy ra lỗi trong quá trình tìm kiếm: {0} + Không thể mở thư mục + Không thể mở file + + + Xóa + Sửa + Thêm + Cài đặt chung + Tùy chỉnh từ khóa hành động + Liên kết truy cập nhanh + Cài đặt mọi thứ + Bảng xem trước + Kích thước + Date Created + Date Modified + Hiển thị thông tin tệp + Định dạng ngày và giờ + Tùy Chọn Sắp Xếp + Đường dẫn mọi thứ: + Khởi chạy ẩn + Đường dẫn soạn thảo + Đường dẫn Shell + Đường dẫn bị loại trừ tìm kiếm chỉ mục + Sử dụng vị trí của kết quả tìm kiếm làm thư mục làm việc của tệp thực thi + Chạy với tư cách quản trị viên / mở thư mục trong trình quản lý tệp tiêu chuẩn + Sử dụng Tìm kiếm Chỉ mục để Tìm kiếm Đường dẫn + Tùy chọn lập chỉ mục + Tìm kiếm trên web với sự hỗ trợ của công cụ tìm kiếm khác + Tìm kiếm đường dẫn: + Tìm kiếm nội dung tệp: + Tìm kiếm chỉ mục: + Truy cập nhanh + Từ hành động hiện tại + Xong + Đã bật + Khi bị tắt, Flow sẽ không thực thi tùy chọn tìm kiếm này và sẽ hoàn nguyên về '*' để giải phóng từ khóa hành động + Tất cả mọi thứ + Chỉ mục Windows + Đếm trực tiếp + Đường dẫn trình soạn thảo tệp + Folder Editor Path + Đã bật + Vô hiệu hóa + + Công cụ tìm kiếm nội dung + Directory Recursive Search Engine + Công cụ tìm kiếm chỉ mục + Mở tùy chọn lập chỉ mục Windows + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine + + + Thư mục + Tìm và quản lý tệp và thư mục thông qua Windows Search hoặc Everything + + + Ctrl + Enter để mở thư mục + Ctrl + Enter để mở thư mục chứa + + + Copy đường dẫn + Sao chép đường dẫn của mục hiện tại vào clipboard + Sao chép + Sao chép tập tin hiện tại vào clipboard + Copy current folder to clipboard + Xóa + Permanently delete current file + Permanently delete current folder + Đường dẫn: + Xóa đã chọn + Xóa lựa chọn đã chọn + Chạy phần đã chọn bằng tài khoản người dùng khác + Mở thư mục chứa + Open the location that contains current item + Mở bằng trình chỉnh sửa: + Failed to open file at {0} with Editor {1} at {2} + Mở bằng Shell: + Failed to open folder {0} with Shell {1} at {2} + Loại trừ các thư mục hiện tại và thư mục con khỏi Tìm kiếm chỉ mục + Bị loại trừ khỏi Tìm kiếm chỉ mục + Mở tùy chọn lập chỉ mục Windows + Quản lý các tập tin và thư mục được lập chỉ mục + Quản lý các tập tin và thư mục được lập chỉ mục + Thêm vào Bảng truy cập nhanh + Thêm {0} hiện tại vào Truy cập nhanh + Đã thêm thành công + Đã thêm thành công vào Truy cập nhanh + Đã được gỡ bỏ thành công + Đã xóa thành công khỏi Truy cập nhanh + Thêm vào Truy cập nhanh để có thể mở nó bằng từ khóa hành động Kích hoạt tìm kiếm của Explorer + Loại bỏ khỏi bảng truy cập nhanh + Loại bỏ khỏi bảng truy cập nhanh + Xóa {0} hiện tại khỏi Truy cập nhanh + Show Windows Context Menu + Mở bằng + Select a program to open with + + + {0} phần {1} + Trình quản lý tệp mặc định + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + + + Failed to load Everything SDK + Warning: Everything service is not running + Error while querying Everything + Sắp xếp theo + Tên + Đường dẫn + Kích thước + Mở rộng + Loại tên + Ngày Tạo + ngày sửa đổi + Thuộc tính + File List FileName + Số lần chạy + Date Recently Changed + Ngày truy cập + Date Run + + + Warning: This is not a Fast Sort option, searches may be slow + + Search Full Path + Enable File/Folder Run Count + + Click to launch or install Everything + Everything Installation + Installing Everything service. Please wait... + Successfully installed Everything service + Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com + Click here to start it + Không thể tìm thấy bản cài đặt Mọi thứ, bạn có muốn chọn vị trí theo cách thủ công không?{0}{0}Nhấp vào không và Mọi thứ sẽ được cài đặt tự động cho bạn + Do you want to enable content search for Everything? + It can be very slow without index (which is only supported in Everything v1.5+) + + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml index 4d451881b..64831ea13 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml @@ -16,6 +16,8 @@ 警告消息已关闭。 作为搜索文件和文件夹的一个替代办法,你想要安装 Everything 插件吗?{0}{0}选择 '是'安装Everything插件',或者'否' 退出 资源管理器选项 搜索时发生错误:{0} + 无法打开文件夹 + 无法打开文件 删除 @@ -25,6 +27,12 @@ 自定义动作关键字 快速访问链接 Everything 设置 + 预览面板 + 大小 + 创建日期 + 修改日期 + 显示文件信息 + 日期和时间格式 排序选项 Everything 路径 隐藏启动 @@ -32,6 +40,7 @@ Shell 路径 索引搜索排除的路径 使用搜索结果的位置作为应用程序的工作目录 + 点击回车在默认文件管理器中打开文件夹 使用索引进行路径搜索 索引选项 搜索: @@ -48,11 +57,17 @@ 直接枚举 编辑器路径(文件) 编辑器路径(文件夹) + 启用 + 已禁用 文件内容搜索引擎 目录递归搜索引擎 索引搜索引擎 打开 Windows 索引选项 + 排除的文件类型(以半角逗号分隔) + 例如:exe,jpg,png + 最大结果数 + 从活跃搜索引擎请求的最大结果数 文件管理器 @@ -97,11 +112,15 @@ 从快速访问中删除 从快速访问中移除当前结果 显示 Windows 上下文菜单 - - + 打开方式 + 选择要打开的程序 + + {0} 可用,共 {1} 在默认文件管理器中打开 - 使用 '>' 在这个目录中搜索,'*' 以搜索文件扩展名或 '>*' 以结合这两个搜索。 + + 使用 '>' 在这个目录中搜索,'*' 以搜索文件扩展名或 '>*' 以结合这两个搜索。 + 加载 Everything SDK 失败 @@ -126,7 +145,8 @@ 警告:这不是一个快速排序选项,搜索可能较慢。 搜索完整路径 - + 启用文件/文件夹运行计数 + 单击启动或安装 Everything Everything 安装 正在安装 Everything 服务。请稍后... @@ -137,4 +157,9 @@ 您想要启用 Everything 的内容搜索功能吗? 如果没有索引,它可能会非常慢(索引仅在 Everything v1.5+ 中支持) + + 本机上下文菜单 + 显示本机上下文菜单(实验性) + 您可以在下面指定想要包含在上下文菜单中的项目,它们可以是部分的(例如“pen wit”)或完整的(“打开方式”)。 + 您可以在下面指定要从上下文菜单中排除的项目,它们可以是部分的(例如“pen wit”)或完整的(“打开方式”)。 diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml index e24b7288d..9e9697255 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml @@ -7,8 +7,8 @@ 你確認要刪除{0}嗎? Are you sure you want to permanently delete this file? Are you sure you want to permanently delete this file/folder? - Deletion successful - Successfully deleted {0} + 成功刪除 + 已成功刪除 {0} Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword Quick Access can not be set to the global action keyword when enabled. Please choose a specific action keyword The required service for Windows Index Search does not appear to be running @@ -16,6 +16,8 @@ The warning message has been switched off. As an alternative for searching files and folders, would you like to install Everything plugin?{0}{0}Select 'Yes' to install Everything plugin, or 'No' to return Explorer Alternative Error occurred during search: {0} + Could not open folder + Could not open file 刪除 @@ -23,8 +25,14 @@ 新增 General Setting Customise Action Keywords - Quick Access Links + 快速訪問連結 Everything Setting + Preview Panel + 大小 + 創建日期 + 修改日期 + Display File Info + Date and time format Sort Option: Everything Path: Launch Hidden @@ -32,13 +40,14 @@ Shell Path Index Search Excluded Paths 使用程式所在目錄作為工作目錄 + Hit Enter to open folder in Default File Manager Use Index Search For Path Search 索引選項 搜尋: - Path Search: + 路徑搜尋: File Content Search: Index Search: - Quick Access: + 快速存取: Current Action Keyword 已啟用 @@ -48,18 +57,24 @@ Direct Enumeration 文件編輯器路徑 文件夾編輯器路徑 + 已啟用 + Disabled Content Search Engine Directory Recursive Search Engine Index Search Engine Open Windows Index Option + Excluded File Types (comma seperated) + For example: exe,jpg,png + Maximum results + The maximum number of results requested from active search engine 檔案總管 - Find and manage files and folders via Windows Search or Everything + 透過 Windows 搜尋或 Everything 搜尋和管理檔案和資料夾 - Ctrl + Enter to open the directory + Ctrl + Enter 以開啟該目錄。 Ctrl + Enter to open the containing folder @@ -88,20 +103,24 @@ Failed to open Windows Indexing Options Add to Quick Access Add current item to Quick Access - Successfully Added + 添加成功 Successfully added to Quick Access Successfully Removed Successfully removed from Quick Access Add to Quick Access so it can be opened with Explorer's Search Activation action keyword - Remove from Quick Access - Remove from Quick Access + 從快速訪問中移除 + 從快速訪問中移除 Remove current item from Quick Access Show Windows Context Menu - - + Open With + Select a program to open with + + {0} free of {1} Open in Default File Manager - Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + + Use '>' to search in this directory, '*' to search for file extensions or '>*' to combine both searches. + Failed to load Everything SDK @@ -126,15 +145,21 @@ Warning: This is not a Fast Sort option, searches may be slow Search Full Path - + Enable File/Folder Run Count + Click to launch or install Everything Everything 安裝程序 正在安裝 Everything 服務,請稍後... 成功安裝 Everything 服務 Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com 點此開始 - Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you + 無法找到任何已安裝的 Everything,您想手動選擇位置嗎?{0}{0}點擊不 Everything 將會自動為您安裝。 Do you want to enable content search for Everything? It can be very slow without index (which is only supported in Everything v1.5+) + + Native Context Menu + Display native context menu (experimental) + Below you can specify items you want to include in the context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). + Below you can specify items you want to exclude from context menu, they can be partial (e.g. 'pen wit') or complete ('Open with'). diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 82a5d5441..e4056131d 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -79,7 +79,7 @@ namespace Flow.Launcher.Plugin.Explorer ? action : _ => { - Clipboard.SetDataObject(e.ToString()); + Context.API.CopyToClipboard(e.ToString()); return new ValueTask(true); } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs index 2174aeee6..4bddfd9b2 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Reflection; namespace Flow.Launcher.Plugin.Explorer.Search @@ -23,7 +22,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search internal const string WindowsIndexErrorImagePath = "Images\\index_error2.png"; internal const string GeneralSearchErrorImagePath = "Images\\robot_error.png"; - internal const string ToolTipOpenDirectory = "Ctrl + Enter to open the directory"; internal const string ToolTipOpenContainingFolder = "Ctrl + Enter to open the containing folder"; @@ -32,6 +30,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search internal const string DefaultContentSearchActionKeyword = "doc:"; + internal const char UnixDirectorySeparator = '/'; + internal const char DirectorySeparator = '\\'; internal const string WindowsIndexingOptions = "srchadmin.dll"; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index e618b5c36..6159c9355 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -2,14 +2,10 @@ using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions; using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Windows.Forms.Design; -using Flow.Launcher.Plugin.Explorer.Exceptions; namespace Flow.Launcher.Plugin.Explorer.Search.Everything { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingDownloadHelper.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingDownloadHelper.cs index ce774281c..c8bd68279 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingDownloadHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingDownloadHelper.cs @@ -4,8 +4,8 @@ using Microsoft.Win32; using System; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; +using System.Windows; namespace Flow.Launcher.Plugin.Explorer.Search.Everything; @@ -20,10 +20,10 @@ public static class EverythingDownloadHelper if (string.IsNullOrEmpty(installedLocation)) { - if (System.Windows.Forms.MessageBox.Show( + if (api.ShowMsgBox( string.Format(api.GetTranslation("flowlauncher_plugin_everything_installing_select"), Environment.NewLine), api.GetTranslation("flowlauncher_plugin_everything_installing_title"), - System.Windows.Forms.MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes) + MessageBoxButton.YesNo) == MessageBoxResult.Yes) { var dlg = new System.Windows.Forms.OpenFileDialog { @@ -51,7 +51,7 @@ public static class EverythingDownloadHelper installedLocation = "C:\\Program Files\\Everything\\Everything.exe"; - FilesFolders.OpenPath(installedLocation); + FilesFolders.OpenPath(installedLocation, (string str) => api.ShowMsgBox(str)); return installedLocation; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs index 1ea23777c..6c9155539 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs @@ -64,7 +64,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything if (token.IsCancellationRequested) yield break; - var option = new EverythingSearchOption(search, Settings.SortOption, IsFullPathSearch: Settings.EverythingSearchFullPath); + var option = new EverythingSearchOption(search, + Settings.SortOption, + MaxCount: Settings.MaxResult, + IsFullPathSearch: Settings.EverythingSearchFullPath, + IsRunCounterEnabled: Settings.EverythingEnableRunCount); await foreach (var result in EverythingApi.SearchAsync(option, token)) yield return result; @@ -94,9 +98,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything var option = new EverythingSearchOption(plainSearch, Settings.SortOption, - true, - contentSearch, - IsFullPathSearch: Settings.EverythingSearchFullPath); + IsContentSearch: true, + ContentSearchKeyword: contentSearch, + MaxCount: Settings.MaxResult, + IsFullPathSearch: Settings.EverythingSearchFullPath, + IsRunCounterEnabled: Settings.EverythingEnableRunCount); await foreach (var result in EverythingApi.SearchAsync(option, token)) { @@ -115,12 +121,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything Settings.SortOption, ParentPath: path, IsRecursive: recursive, - IsFullPathSearch: Settings.EverythingSearchFullPath); + MaxCount: Settings.MaxResult, + IsFullPathSearch: Settings.EverythingSearchFullPath, + IsRunCounterEnabled: Settings.EverythingEnableRunCount); await foreach (var result in EverythingApi.SearchAsync(option, token)) - { yield return result; - } } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs index 0b1bbd0ec..92b8e9623 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs @@ -1,5 +1,4 @@ -using System; -using Flow.Launcher.Plugin.Everything.Everything; +using Flow.Launcher.Plugin.Everything.Everything; namespace Flow.Launcher.Plugin.Explorer.Search.Everything { @@ -12,6 +11,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything bool IsRecursive = true, int Offset = 0, int MaxCount = 100, - bool IsFullPathSearch = true + bool IsFullPathSearch = true, + bool IsRunCounterEnabled = true ); } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs index c57e3fe4a..3c2fc3660 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Flow.Launcher.Plugin.Everything.Everything +namespace Flow.Launcher.Plugin.Everything.Everything { public enum SortOption : uint { diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IContentIndexProvider.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IContentIndexProvider.cs index 6e036e058..7b8960b37 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IContentIndexProvider.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IContentIndexProvider.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; namespace Flow.Launcher.Plugin.Explorer.Search.IProvider diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IIndexProvider.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IIndexProvider.cs index d43dd7df3..9909b18d8 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IIndexProvider.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IIndexProvider.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; namespace Flow.Launcher.Plugin.Explorer.Search.IProvider diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IPathIndexProvider.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IPathIndexProvider.cs index 4622df5f9..56d735687 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IPathIndexProvider.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IPathIndexProvider.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; namespace Flow.Launcher.Plugin.Explorer.Search.IProvider diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs index cdd2c93e6..85b595390 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs @@ -13,20 +13,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks if (string.IsNullOrEmpty(query.Search)) return new List(); - string search = query.Search.ToLower(); - - var queriedAccessLinks = - accessLinks - .Where(x => x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || x.Path.Contains(search, StringComparison.OrdinalIgnoreCase)) + return accessLinks + .Where(x => Main.Context.API.FuzzySearch(query.Search, x.Name).IsSearchPrecisionScoreMet() || Main.Context.API.FuzzySearch(query.Search, x.Path).IsSearchPrecisionScoreMet()) .OrderBy(x => x.Type) - .ThenBy(x => x.Name); - - return queriedAccessLinks.Select(l => l.Type switch - { - ResultType.Folder => ResultManager.CreateFolderResult(l.Name, l.Path, l.Path, query, quickAccessResultScore), - ResultType.File => ResultManager.CreateFileResult(l.Path, query, quickAccessResultScore), - _ => throw new ArgumentOutOfRangeException() - }).ToList(); + .ThenBy(x => x.Name) + .Select(l => l.Type switch + { + ResultType.Folder => ResultManager.CreateFolderResult(l.Name, l.Path, l.Path, query, quickAccessResultScore), + ResultType.File => ResultManager.CreateFileResult(l.Path, query, quickAccessResultScore), + _ => throw new ArgumentOutOfRangeException() + }) + .ToList(); } internal static List AccessLinkListAll(Query query, IEnumerable accessLinks) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index 83c173ac6..1add84765 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -2,18 +2,21 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin.SharedCommands; using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using System.Windows; using Flow.Launcher.Plugin.Explorer.Search.Everything; using System.Windows.Input; +using Path = System.IO.Path; +using System.Windows.Controls; +using Flow.Launcher.Plugin.Explorer.Views; +using Peter; namespace Flow.Launcher.Plugin.Explorer.Search { public static class ResultManager { + private static readonly string[] SizeUnits = { "B", "KB", "MB", "GB", "TB" }; private static PluginInitContext Context; private static Settings Settings { get; set; } @@ -67,6 +70,27 @@ namespace Flow.Launcher.Plugin.Explorer.Search }; } + internal static void ShowNativeContextMenu(string path, ResultType type) + { + var screenWithMouseCursor = System.Windows.Forms.Screen.FromPoint(System.Windows.Forms.Cursor.Position); + var xOfScreenCenter = screenWithMouseCursor.WorkingArea.Left + screenWithMouseCursor.WorkingArea.Width / 2; + var yOfScreenCenter = screenWithMouseCursor.WorkingArea.Top + screenWithMouseCursor.WorkingArea.Height / 2; + var showPosition = new System.Drawing.Point(xOfScreenCenter, yOfScreenCenter); + + switch (type) + { + case ResultType.File: + var fileInfo = new FileInfo[] { new(path) }; + new ShellContextMenu().ShowContextMenu(fileInfo, showPosition); + break; + + case ResultType.Folder: + var folderInfo = new System.IO.DirectoryInfo[] { new(path) }; + new ShellContextMenu().ShowContextMenu(folderInfo, showPosition); + break; + } + } + internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool windowsIndexed = false) { return new Result @@ -77,8 +101,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder), TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, CopyText = path, + Preview = new Result.PreviewInfo + { + FilePath = path, + }, Action = c => { + if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) + { + ShowNativeContextMenu(path, ResultType.Folder); + return false; + } // open folder if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift)) { @@ -89,7 +122,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } catch (Exception ex) { - MessageBox.Show(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); + Context.API.ShowMsgBox(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); return false; } } @@ -103,7 +136,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } catch (Exception ex) { - MessageBox.Show(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); + Context.API.ShowMsgBox(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); return false; } } @@ -118,7 +151,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } catch (Exception ex) { - MessageBox.Show(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); + Context.API.ShowMsgBox(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); return false; } } @@ -162,6 +195,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search Score = 500, ProgressBar = progressValue, ProgressBarColor = progressBarColor, + Preview = new Result.PreviewInfo + { + FilePath = path, + }, Action = _ => { OpenFolder(path); @@ -173,36 +210,28 @@ namespace Flow.Launcher.Plugin.Explorer.Search }; } - private static string ToReadableSize(long pDrvSize, int pi) + internal static string ToReadableSize(long sizeOnDrive, int pi) { - int mok = 0; - double drvSize = pDrvSize; - string uom = "Byte"; // Unit Of Measurement + var unitIndex = 0; + double readableSize = sizeOnDrive; - while (drvSize > 1024.0) + while (readableSize > 1024.0 && unitIndex < SizeUnits.Length - 1) { - drvSize /= 1024.0; - mok++; + readableSize /= 1024.0; + unitIndex++; } - if (mok == 1) - uom = "KB"; - else if (mok == 2) - uom = " MB"; - else if (mok == 3) - uom = " GB"; - else if (mok == 4) - uom = " TB"; + var unit = SizeUnits[unitIndex] ?? ""; - var returnStr = $"{Convert.ToInt32(drvSize)}{uom}"; - if (mok != 0) + var returnStr = $"{Convert.ToInt32(readableSize)} {unit}"; + if (unitIndex != 0) { returnStr = pi switch { - 1 => $"{drvSize:F1}{uom}", - 2 => $"{drvSize:F2}{uom}", - 3 => $"{drvSize:F3}{uom}", - _ => $"{Convert.ToInt32(drvSize)}{uom}" + 1 => $"{readableSize:F1} {unit}", + 2 => $"{readableSize:F2} {unit}", + 3 => $"{readableSize:F3} {unit}", + _ => $"{Convert.ToInt32(readableSize)} {unit}" }; } @@ -223,8 +252,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search IcoPath = folderPath, Score = 500, CopyText = folderPath, - Action = _ => + Action = c => { + if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) + { + ShowNativeContextMenu(folderPath, ResultType.Folder); + return false; + } OpenFolder(folderPath); return true; }, @@ -234,24 +268,35 @@ namespace Flow.Launcher.Plugin.Explorer.Search internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) { - Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) - ? new Result.PreviewInfo { IsMedia = true, PreviewImagePath = filePath, } - : Result.PreviewInfo.Default; - + bool isMedia = IsMedia(Path.GetExtension(filePath)); var title = Path.GetFileName(filePath); + + /* Preview Detail */ + var result = new Result { Title = title, SubTitle = Path.GetDirectoryName(filePath), IcoPath = filePath, - Preview = preview, + Preview = new Result.PreviewInfo + { + IsMedia = isMedia, + PreviewImagePath = isMedia ? filePath : null, + FilePath = filePath, + }, AutoCompleteText = GetAutoCompleteText(title, query, filePath, ResultType.File), TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, Score = score, CopyText = filePath, + PreviewPanel = new Lazy(() => new PreviewPanel(Settings, filePath)), Action = c => { + if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) + { + ShowNativeContextMenu(filePath, ResultType.File); + return false; + } try { if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift)) @@ -269,7 +314,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } catch (Exception ex) { - MessageBox.Show(ex.Message, Context.API.GetTranslation("plugin_explorer_openfile_error")); + Context.API.ShowMsgBox(ex.Message, Context.API.GetTranslation("plugin_explorer_openfile_error")); } return true; @@ -291,7 +336,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search private static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false) { IncrementEverythingRunCounterIfNeeded(filePath); - FilesFolders.OpenFile(filePath, workingDir, asAdmin); + FilesFolders.OpenFile(filePath, workingDir, asAdmin, (string str) => Context.API.ShowMsgBox(str)); } private static void OpenFolder(string folderPath, string fileNameOrFilePath = null) @@ -302,7 +347,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search private static void IncrementEverythingRunCounterIfNeeded(string fileOrFolder) { - if (Settings.EverythingEnabled) + if (Settings.EverythingEnabled && Settings.EverythingEnableRunCount) _ = Task.Run(() => EverythingApi.IncrementRunCounterAsync(fileOrFolder)); } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 51c4c3d9d..12df6c145 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Plugin.Explorer.Exceptions; +using Path = System.IO.Path; namespace Flow.Launcher.Plugin.Explorer.Search { @@ -68,7 +69,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search IAsyncEnumerable searchResults; - bool isPathSearch = query.Search.IsLocationPathString() || IsEnvironmentVariableSearch(query.Search); + bool isPathSearch = query.Search.IsLocationPathString() + || EnvironmentVariables.IsEnvironmentVariableSearch(query.Search) + || EnvironmentVariables.HasEnvironmentVar(query.Search); string engineName; @@ -107,7 +110,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search try { await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false)) - results.Add(ResultManager.CreateResult(query, search)); + if (search.Type == ResultType.File && IsExcludedFile(search)) { + continue; + } else { + results.Add(ResultManager.CreateResult(query, search)); + } } catch (OperationCanceledException) { @@ -123,7 +130,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any( - excludedPath => FilesFolders.PathContains(excludedPath.Path, r.SubTitle))); + excludedPath => FilesFolders.PathContains(excludedPath.Path, r.SubTitle, allowEqual: true))); return results.ToList(); } @@ -178,16 +185,19 @@ namespace Flow.Launcher.Plugin.Explorer.Search // Query is a location path with a full environment variable, eg. %appdata%\somefolder\, c:\users\%USERNAME%\downloads var needToExpand = EnvironmentVariables.HasEnvironmentVar(querySearch); - var locationPath = needToExpand ? Environment.ExpandEnvironmentVariables(querySearch) : querySearch; + var path = needToExpand ? Environment.ExpandEnvironmentVariables(querySearch) : querySearch; + + // if user uses the unix directory separator, we need to convert it to windows directory separator + path = path.Replace(Constants.UnixDirectorySeparator, Constants.DirectorySeparator); // Check that actual location exists, otherwise directory search will throw directory not found exception - if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath).LocationExists()) + if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path).LocationExists()) return results.ToList(); var useIndexSearch = Settings.IndexSearchEngine is Settings.IndexSearchEngineOption.WindowsIndex - && UseWindowsIndexForDirectorySearch(locationPath); + && UseWindowsIndexForDirectorySearch(path); - var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath); + var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); results.Add(retrievedDirectoryPath.EndsWith(":\\") ? ResultManager.CreateDriveSpaceDisplayResult(retrievedDirectoryPath, query.ActionKeyword, useIndexSearch) @@ -198,21 +208,21 @@ namespace Flow.Launcher.Plugin.Explorer.Search IAsyncEnumerable directoryResult; - var recursiveIndicatorIndex = query.Search.IndexOf('>'); + var recursiveIndicatorIndex = path.IndexOf('>'); if (recursiveIndicatorIndex > 0 && Settings.PathEnumerationEngine != Settings.PathEnumerationEngineOption.DirectEnumeration) { directoryResult = Settings.PathEnumerator.EnumerateAsync( - query.Search[..recursiveIndicatorIndex], - query.Search[(recursiveIndicatorIndex + 1)..], + path[..recursiveIndicatorIndex].Trim(), + path[(recursiveIndicatorIndex + 1)..], true, token); } else { - directoryResult = DirectoryInfoSearch.TopLevelDirectorySearch(query, query.Search, token).ToAsyncEnumerable(); + directoryResult = DirectoryInfoSearch.TopLevelDirectorySearch(query, path, token).ToAsyncEnumerable(); } if (token.IsCancellationRequested) @@ -246,11 +256,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search && WindowsIndex.WindowsIndex.PathIsIndexed(pathToDirectory); } - internal static bool IsEnvironmentVariableSearch(string search) + private bool IsExcludedFile(SearchResult result) { - return search.StartsWith("%") - && search != "%%" - && !search.Contains('\\'); + string[] excludedFileTypes = Settings.ExcludedFileTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + string fileExtension = Path.GetExtension(result.FullPath).TrimStart('.'); + + return excludedFileTypes.Contains(fileExtension, StringComparer.OrdinalIgnoreCase); } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs index 92c24559d..3cd97df82 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs @@ -1,5 +1,3 @@ -using System; - namespace Flow.Launcher.Plugin.Explorer.Search { public record struct SearchResult diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs index 87eca91da..9e3707fbe 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs @@ -1,11 +1,14 @@ using System; -using System.Buffers; +using System.Text.RegularExpressions; using Microsoft.Search.Interop; namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { public class QueryConstructor { + private static Regex _specialCharacterMatcher = new(@"[\@\@\#\#\&\&*_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled); + private static Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled); + private Settings settings { get; } private const string SystemIndex = "SystemIndex"; @@ -77,8 +80,39 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex if (userSearchString.IsWhiteSpace()) userSearchString = "*"; + // Remove any special characters that might cause issues with the query + var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString); + // Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause - return $"{CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString.ToString())} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}"; + return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}"; + } + + /// + /// If one special character have white space on one side, replace it with one white space. + /// So command will not have "[special character]+*" which will cause OLEDB exception. + /// + private static string ReplaceSpecialCharacterWithTwoSideWhiteSpace(ReadOnlySpan input) + { + const string whiteSpace = " "; + + var inputString = input.ToString(); + + // Use regex to match special characters with whitespace on one side + // and replace them with a single space + var result = _specialCharacterMatcher.Replace(inputString, match => + { + // Check if the match has whitespace on one side + bool hasLeadingWhitespace = match.Index > 0 && char.IsWhiteSpace(inputString[match.Index - 1]); + bool hasTrailingWhitespace = match.Index + match.Length < inputString.Length && char.IsWhiteSpace(inputString[match.Index + match.Length]); + if (hasLeadingWhitespace || hasTrailingWhitespace) + { + return whiteSpace; + } + return match.Value; + }); + + // Remove any extra spaces that might have been introduced + return _multiWhiteSpacesMatcher.Replace(result, whiteSpace).Trim(); } /// diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndex.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndex.cs index 2093508a0..66230937c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndex.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndex.cs @@ -8,7 +8,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; -using System.Threading.Tasks; using Flow.Launcher.Plugin.Explorer.Exceptions; namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex @@ -48,21 +47,22 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex while (await dataReader.ReadAsync(token)) { token.ThrowIfCancellationRequested(); - if (dataReader.GetValue(0) == DBNull.Value || dataReader.GetValue(1) == DBNull.Value) + if (dataReader.GetValue(0) is DBNull + || dataReader.GetValue(1) is not string rawFragmentPath + || string.Equals(rawFragmentPath, "file:", StringComparison.OrdinalIgnoreCase) + || dataReader.GetValue(2) is not string extension) { continue; } // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path - var encodedFragmentPath = dataReader - .GetString(1) - .Replace("#", "%23", StringComparison.OrdinalIgnoreCase); + var encodedFragmentPath = rawFragmentPath.Replace("#", "%23", StringComparison.OrdinalIgnoreCase); var path = new Uri(encodedFragmentPath).LocalPath; yield return new SearchResult { FullPath = path, - Type = dataReader.GetString(2) == "Directory" ? ResultType.Folder : ResultType.File, + Type = string.Equals(extension, "Directory", StringComparison.Ordinal) ? ResultType.Folder : ResultType.File, WindowsIndexed = true }; } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs index 4077e2fcc..3d30bcf29 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -4,10 +4,8 @@ using Flow.Launcher.Plugin.Explorer.Search.Everything; using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Plugin.Explorer.Search.IProvider; @@ -27,10 +25,16 @@ namespace Flow.Launcher.Plugin.Explorer public string ShellPath { get; set; } = "cmd"; + public string ExcludedFileTypes { get; set; } = ""; + public bool UseLocationAsWorkingDir { get; set; } = false; - public bool ShowWindowsContextMenu { get; set; } = true; + public bool ShowInlinedWindowsContextMenu { get; set; } = false; + + public string WindowsContextMenuIncludedItems { get; set; } = string.Empty; + + public string WindowsContextMenuExcludedItems { get; set; } = string.Empty; public bool DefaultOpenFolderInFileManager { get; set; } = false; @@ -57,6 +61,16 @@ namespace Flow.Launcher.Plugin.Explorer public bool WarnWindowsSearchServiceOff { get; set; } = true; + public bool ShowFileSizeInPreviewPanel { get; set; } = true; + + public bool ShowCreatedDateInPreviewPanel { get; set; } = true; + + public bool ShowModifiedDateInPreviewPanel { get; set; } = true; + + public string PreviewPanelDateFormat { get; set; } = "yyyy-MM-dd"; + + public string PreviewPanelTimeFormat { get; set; } = "HH:mm"; + private EverythingSearchManager _everythingManagerInstance; private WindowsIndexSearchManager _windowsIndexSearchManager; @@ -139,7 +153,8 @@ namespace Flow.Launcher.Plugin.Explorer ContentSearchEngine == ContentIndexSearchEngineOption.Everything; public bool EverythingSearchFullPath { get; set; } = false; - + public bool EverythingEnableRunCount { get; set; } = true; + #endregion internal enum ActionKeyword diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/EnumBindingModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/EnumBindingModel.cs index 29c81a465..13971914b 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/EnumBindingModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/EnumBindingModel.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; namespace Flow.Launcher.Plugin.Explorer.ViewModels; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 189434eea..d2b85e687 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -6,15 +6,14 @@ using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.Views; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Linq; using System.Windows; using System.Windows.Forms; using System.Windows.Input; -using MessageBox = System.Windows.Forms.MessageBox; namespace Flow.Launcher.Plugin.Explorer.ViewModels { @@ -102,7 +101,141 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels #endregion + #region Native Context Menu + public bool ShowWindowsContextMenu + { + get => Settings.ShowInlinedWindowsContextMenu; + set + { + Settings.ShowInlinedWindowsContextMenu = value; + OnPropertyChanged(); + } + } + + public string WindowsContextMenuIncludedItems + { + get => Settings.WindowsContextMenuIncludedItems; + set + { + Settings.WindowsContextMenuIncludedItems = value; + OnPropertyChanged(); + } + } + + public string WindowsContextMenuExcludedItems + { + get => Settings.WindowsContextMenuExcludedItems; + set + { + Settings.WindowsContextMenuExcludedItems = value; + OnPropertyChanged(); + } + } + + #endregion + + #region Preview Panel + + public bool ShowFileSizeInPreviewPanel + { + get => Settings.ShowFileSizeInPreviewPanel; + set + { + Settings.ShowFileSizeInPreviewPanel = value; + OnPropertyChanged(); + } + } + + public bool ShowCreatedDateInPreviewPanel + { + get => Settings.ShowCreatedDateInPreviewPanel; + set + { + Settings.ShowCreatedDateInPreviewPanel = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ShowPreviewPanelDateTimeChoices)); + OnPropertyChanged(nameof(PreviewPanelDateTimeChoicesVisibility)); + } + } + + public bool ShowModifiedDateInPreviewPanel + { + get => Settings.ShowModifiedDateInPreviewPanel; + set + { + Settings.ShowModifiedDateInPreviewPanel = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ShowPreviewPanelDateTimeChoices)); + OnPropertyChanged(nameof(PreviewPanelDateTimeChoicesVisibility)); + } + } + + public string PreviewPanelDateFormat + { + get => Settings.PreviewPanelDateFormat; + set + { + Settings.PreviewPanelDateFormat = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(PreviewPanelDateFormatDemo)); + } + } + + public string PreviewPanelTimeFormat + { + get => Settings.PreviewPanelTimeFormat; + set + { + Settings.PreviewPanelTimeFormat = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(PreviewPanelTimeFormatDemo)); + } + } + + public string PreviewPanelDateFormatDemo => DateTime.Now.ToString(PreviewPanelDateFormat, CultureInfo.CurrentCulture); + public string PreviewPanelTimeFormatDemo => DateTime.Now.ToString(PreviewPanelTimeFormat, CultureInfo.CurrentCulture); + + public bool ShowPreviewPanelDateTimeChoices => ShowCreatedDateInPreviewPanel || ShowModifiedDateInPreviewPanel; + + public Visibility PreviewPanelDateTimeChoicesVisibility => ShowCreatedDateInPreviewPanel || ShowModifiedDateInPreviewPanel ? Visibility.Visible : Visibility.Collapsed; + + + 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() + { + "dd/MM/yyyy", + "dd/MM/yyyy ddd", + "dd/MM/yyyy, dddd", + "dd-MM-yyyy", + "dd-MM-yyyy ddd", + "dd-MM-yyyy, dddd", + "dd.MM.yyyy", + "dd.MM.yyyy ddd", + "dd.MM.yyyy, dddd", + "MM/dd/yyyy", + "MM/dd/yyyy ddd", + "MM/dd/yyyy, dddd", + "yyyy-MM-dd", + "yyyy-MM-dd ddd", + "yyyy-MM-dd, dddd", + }; + + #endregion #region ActionKeyword @@ -224,7 +357,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels private void ShowUnselectedMessage() { var warning = Context.API.GetTranslation("plugin_explorer_make_selection_warning"); - MessageBox.Show(warning); + Context.API.ShowMsgBox(warning); } @@ -379,6 +512,31 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels } } + public string ExcludedFileTypes + { + get => Settings.ExcludedFileTypes; + set + { + // remove spaces and dots from the string before saving + string sanitized = string.IsNullOrEmpty(value) ? "" : value.Replace(" ", "").Replace(".", ""); + Settings.ExcludedFileTypes = sanitized; + OnPropertyChanged(); + } + } + + public int MaxResultLowerLimit => 100; + public int MaxResultUpperLimit => 100000; + + public int MaxResult + { + get => Settings.MaxResult; + set + { + Settings.MaxResult = Math.Clamp(value, MaxResultLowerLimit, MaxResultUpperLimit); + OnPropertyChanged(); + } + } + #region Everything FastSortWarning diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs index 6ee4faad8..73c35b1c8 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml.cs @@ -1,9 +1,5 @@ -using Flow.Launcher.Plugin.Explorer.ViewModels; -using ICSharpCode.SharpZipLib.Zip; -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Input; @@ -35,13 +31,13 @@ namespace Flow.Launcher.Plugin.Explorer.Views } private string actionKeyword; - private readonly IPublicAPI api; + private readonly IPublicAPI _api; private bool _keywordEnabled; public ActionKeywordSetting(ActionKeywordModel selectedActionKeyword, IPublicAPI api) { CurrentActionKeyword = selectedActionKeyword; - this.api = api; + _api = api; ActionKeyword = selectedActionKeyword.Keyword; KeywordEnabled = selectedActionKeyword.Enabled; @@ -66,14 +62,14 @@ namespace Flow.Launcher.Plugin.Explorer.Views switch (CurrentActionKeyword.KeywordProperty, KeywordEnabled) { case (Settings.ActionKeyword.FileContentSearchActionKeyword, true): - MessageBox.Show(api.GetTranslation("plugin_explorer_globalActionKeywordInvalid")); + _api.ShowMsgBox(_api.GetTranslation("plugin_explorer_globalActionKeywordInvalid")); return; case (Settings.ActionKeyword.QuickAccessActionKeyword, true): - MessageBox.Show(api.GetTranslation("plugin_explorer_quickaccess_globalActionKeywordInvalid")); + _api.ShowMsgBox(_api.GetTranslation("plugin_explorer_quickaccess_globalActionKeywordInvalid")); return; } - if (!KeywordEnabled || !api.ActionKeywordAssigned(ActionKeyword)) + if (!KeywordEnabled || !_api.ActionKeywordAssigned(ActionKeyword)) { DialogResult = true; Close(); @@ -81,7 +77,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views } // The keyword is not valid, so show message - MessageBox.Show(api.GetTranslation("newActionKeywordsHasBeenAssigned")); + _api.ShowMsgBox(_api.GetTranslation("newActionKeywordsHasBeenAssigned")); } private void BtnCancel_OnClick(object sender, RoutedEventArgs e) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml index 9beb43a48..ecd6d1fca 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml @@ -22,7 +22,7 @@ @@ -105,13 +105,13 @@ - + @@ -127,8 +127,9 @@ + @@ -144,8 +145,26 @@ + + + + + + + @@ -160,20 +179,20 @@ Width="Auto" Header="{DynamicResource plugin_explorer_generalsetting_header}" Style="{DynamicResource ExplorerTabItem}"> - + - + IsChecked="{Binding Settings.DefaultOpenFolderInFileManager}" /> + @@ -188,7 +207,7 @@ public static class ShellLocalization { - internal const uint DONTRESOLVEDLLREFERENCES = 0x00000001; - internal const uint LOADLIBRARYASDATAFILE = 0x00000002; - - [DllImport("shell32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] - internal static extern int SHGetLocalizedName(string pszPath, StringBuilder pszResModule, ref int cch, out int pidsRes); - - [DllImport("user32.dll", EntryPoint = "LoadStringW", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] - internal static extern int LoadString(IntPtr hModule, int resourceID, StringBuilder resourceValue, int len); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, EntryPoint = "LoadLibraryExW")] - internal static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern int FreeLibrary(IntPtr hModule); - - [DllImport("kernel32.dll", EntryPoint = "ExpandEnvironmentStringsW", CharSet = CharSet.Unicode, ExactSpelling = true)] - internal static extern uint ExpandEnvironmentStrings(string lpSrc, StringBuilder lpDst, int nSize); - /// /// Returns the localized name of a shell item. /// /// Path to the shell item (e. g. shortcut 'File Explorer.lnk'). /// The localized name as string or . - public static string GetLocalizedName(string path) + public static unsafe string GetLocalizedName(string path) { - StringBuilder resourcePath = new StringBuilder(1024); - StringBuilder localizedName = new StringBuilder(1024); - int len, id; - len = resourcePath.Capacity; + const int capacity = 1024; + Span buffer = new char[capacity]; // If there is no resource to localize a file name the method returns a non zero value. - if (SHGetLocalizedName(path, resourcePath, ref len, out id) == 0) + fixed (char* bufferPtr = buffer) { - _ = ExpandEnvironmentStrings(resourcePath.ToString(), resourcePath, resourcePath.Capacity); - IntPtr hMod = LoadLibraryEx(resourcePath.ToString(), IntPtr.Zero, DONTRESOLVEDLLREFERENCES | LOADLIBRARYASDATAFILE); - if (hMod != IntPtr.Zero) + var result = PInvoke.SHGetLocalizedName(path, bufferPtr, capacity, out var id); + if (result != HRESULT.S_OK) { - if (LoadString(hMod, id, localizedName, localizedName.Capacity) != 0) - { - string lString = localizedName.ToString(); - _ = FreeLibrary(hMod); - return lString; - } + return string.Empty; + } - _ = FreeLibrary(hMod); + var resourcePathStr = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString(); + _ = PInvoke.ExpandEnvironmentStrings(resourcePathStr, bufferPtr, capacity); + using var handle = PInvoke.LoadLibraryEx(resourcePathStr, + LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES | LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_DATAFILE); + if (handle.IsInvalid) + { + return string.Empty; + } + + // not sure about the behavior of Pinvoke.LoadString, so we clear the buffer before using it (so it must be a null-terminated string) + buffer.Clear(); + + if (PInvoke.LoadString(handle, (uint)id, bufferPtr, capacity) != 0) + { + var lString = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bufferPtr).ToString(); + return lString; } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs deleted file mode 100644 index 8ba3ba800..000000000 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs +++ /dev/null @@ -1,730 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Security.Principal; -using System.Threading.Tasks; -using System.Windows.Media.Imaging; -using Windows.ApplicationModel; -using Windows.Management.Deployment; -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Plugin.Program.Logger; -using Flow.Launcher.Plugin.SharedModels; -using System.Threading.Channels; -using System.Xml; -using Windows.ApplicationModel.Core; - -namespace Flow.Launcher.Plugin.Program.Programs -{ - [Serializable] - public class UWP - { - public string Name { get; } - public string FullName { get; } - public string FamilyName { get; } - public string Location { get; set; } - - public Application[] Apps { get; set; } = Array.Empty(); - - - public UWP(Package package) - { - Location = package.InstalledLocation.Path; - Name = package.Id.Name; - FullName = package.Id.FullName; - FamilyName = package.Id.FamilyName; - } - - public void InitAppsInPackage(Package package) - { - var apps = new List(); - // WinRT - var appListEntries = package.GetAppListEntries(); - foreach (var app in appListEntries) - { - try - { - var tmp = new Application(app, this); - apps.Add(tmp); - } - catch (Exception e) - { - ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" + - "|Unexpected exception occurs when trying to construct a Application from package" - + $"{FullName} from location {Location}", e); - } - } - Apps = apps.ToArray(); - - try - { - var xmlDoc = GetManifestXml(); - if (xmlDoc == null) - { - return; - } - - var xmlRoot = xmlDoc.DocumentElement; - var packageVersion = GetPackageVersionFromManifest(xmlRoot); - if (!smallLogoNameFromVersion.TryGetValue(packageVersion, out string logoName) || - !bigLogoNameFromVersion.TryGetValue(packageVersion, out string bigLogoName)) - { - return; - } - - var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable); - namespaceManager.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); // still need a name - namespaceManager.AddNamespace("rescap", "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"); - namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10"); - - var allowElevationNode = xmlRoot.SelectSingleNode("//rescap:Capability[@Name='allowElevation']", namespaceManager); - bool packageCanElevate = allowElevationNode != null; - - var appsNode = xmlRoot.SelectSingleNode("d:Applications", namespaceManager); - foreach (var app in Apps) - { - // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package - // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes - var id = app.UserModelId.Split('!')[1]; - var appNode = appsNode?.SelectSingleNode($"d:Application[@Id='{id}']", namespaceManager); - if (appNode != null) - { - app.CanRunElevated = packageCanElevate || Application.IfAppCanRunElevated(appNode); - - // local name to fit all versions - var visualElement = appNode.SelectSingleNode($"*[local-name()='VisualElements']", namespaceManager); - var logoUri = visualElement?.Attributes[logoName]?.Value; - app.LogoPath = app.LogoPathFromUri(logoUri, (64, 64)); - // use small logo or may have a big margin - var previewUri = visualElement?.Attributes[logoName]?.Value; - app.PreviewImagePath = app.LogoPathFromUri(previewUri, (256, 256)); - } - } - } - catch (Exception e) - { - ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" + - "|Unexpected exception occurs when trying to construct a Application from package" - + $"{FullName} from location {Location}", e); - } - } - - private XmlDocument GetManifestXml() - { - var manifest = Path.Combine(Location, "AppxManifest.xml"); - try - { - var file = File.ReadAllText(manifest); - var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(file); - return xmlDoc; - } - catch (FileNotFoundException e) - { - ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "AppxManifest.xml not found.", e); - return null; - } - catch (Exception e) - { - ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "An unexpected error occurred and unable to parse AppxManifest.xml", e); - return null; - } - } - - private PackageVersion GetPackageVersionFromManifest(XmlNode xmlRoot) - { - if (xmlRoot != null) - { - - var namespaces = xmlRoot.Attributes; - foreach (XmlAttribute ns in namespaces) - { - if (versionFromNamespace.TryGetValue(ns.Value, out var packageVersion)) - { - return packageVersion; - } - } - - ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" + - "|Trying to get the package version of the UWP program, but an unknown UWP app-manifest version in package " - + $"{FullName} from location {Location}", new FormatException()); - return PackageVersion.Unknown; - } - else - { - ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" + - "|Can't parse AppManifest.xml of package " - + $"{FullName} from location {Location}", new ArgumentNullException(nameof(xmlRoot))); - return PackageVersion.Unknown; - } - } - - private static readonly Dictionary versionFromNamespace = new() - { - { - "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 - }, - { - "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81 - }, - { - "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 - }, - }; - - private static readonly Dictionary smallLogoNameFromVersion = new() - { - { - PackageVersion.Windows10, "Square44x44Logo" - }, - { - PackageVersion.Windows81, "Square30x30Logo" - }, - { - PackageVersion.Windows8, "SmallLogo" - }, - }; - - private static readonly Dictionary bigLogoNameFromVersion = new() - { - { - PackageVersion.Windows10, "Square150x150Logo" - }, - { - PackageVersion.Windows81, "Square150x150Logo" - }, - { - PackageVersion.Windows8, "Logo" - }, - }; - - public static Application[] All(Settings settings) - { - var support = SupportUWP(); - if (support && settings.EnableUWP) - { - var applications = CurrentUserPackages().AsParallel().SelectMany(p => - { - UWP u; - try - { - u = new UWP(p); - u.InitAppsInPackage(p); - } -#if !DEBUG - catch (Exception e) - { - ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and unable to convert Package to UWP for {p.Id.FullName}", e); - return Array.Empty(); - } -#endif -#if DEBUG //make developer aware and implement handling - catch - { - throw; - } -#endif - return u.Apps; - }).ToArray(); - - var updatedListWithoutDisabledApps = applications - .Where(t1 => !Main._settings.DisabledProgramSources - .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)); - - return updatedListWithoutDisabledApps.ToArray(); - } - else - { - return Array.Empty(); - } - } - - public static bool SupportUWP() - { - var windows10 = new Version(10, 0); - var support = Environment.OSVersion.Version.Major >= windows10.Major; - return support; - } - - private static IEnumerable CurrentUserPackages() - { - var user = WindowsIdentity.GetCurrent().User; - - if (user != null) - { - var userId = user.Value; - PackageManager packageManager; - try - { - packageManager = new PackageManager(); - } - catch - { - // Bug from https://github.com/microsoft/CsWinRT, using Microsoft.Windows.SDK.NET.Ref 10.0.19041.0. - // Only happens on the first time, so a try catch can fix it. - packageManager = new PackageManager(); - } - var packages = packageManager.FindPackagesForUser(userId); - packages = packages.Where(p => - { - try - { - var f = p.IsFramework; - var d = p.IsDevelopmentMode; - var path = p.InstalledLocation.Path; - return !f && !d && !string.IsNullOrEmpty(path); - } - catch (Exception e) - { - ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{p.Id.FullName}", "An unexpected error occurred and " - + $"unable to verify if package is valid", e); - return false; - } - }); - return packages; - } - else - { - return Array.Empty(); - } - } - - private static Channel PackageChangeChannel = Channel.CreateBounded(1); - - public static async Task WatchPackageChange() - { - if (Environment.OSVersion.Version.Major >= 10) - { - var catalog = PackageCatalog.OpenForCurrentUser(); - catalog.PackageInstalling += (_, args) => - { - if (args.IsComplete) - PackageChangeChannel.Writer.TryWrite(default); - }; - catalog.PackageUninstalling += (_, args) => - { - if (args.IsComplete) - PackageChangeChannel.Writer.TryWrite(default); - }; - catalog.PackageUpdating += (_, args) => - { - if (args.IsComplete) - PackageChangeChannel.Writer.TryWrite(default); - }; - - while (await PackageChangeChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) - { - await Task.Delay(3000).ConfigureAwait(false); - PackageChangeChannel.Reader.TryRead(out _); - await Task.Run(Main.IndexUwpPrograms); - } - - } - } - - public override string ToString() - { - return FamilyName; - } - - public override bool Equals(object obj) - { - if (obj is UWP uwp) - { - return FamilyName.Equals(uwp.FamilyName); - } - else - { - return false; - } - } - - public override int GetHashCode() - { - return FamilyName.GetHashCode(); - } - - [Serializable] - public class Application : IProgram - { - private string _uid = string.Empty; - public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } - public string DisplayName { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public string UserModelId { get; set; } = string.Empty; - //public string BackgroundColor { get; set; } = string.Empty; // preserve for future use - public string Name => DisplayName; - public string Location { get; set; } = string.Empty; - - public bool Enabled { get; set; } = false; - public bool CanRunElevated { get; set; } = false; - public string LogoPath { get; set; } = string.Empty; - public string PreviewImagePath { get; set; } = string.Empty; - - public Application(AppListEntry appListEntry, UWP package) - { - UserModelId = appListEntry.AppUserModelId; - UniqueIdentifier = appListEntry.AppUserModelId; - DisplayName = appListEntry.DisplayInfo.DisplayName; - Description = appListEntry.DisplayInfo.Description; - Location = package.Location; - Enabled = true; - } - - public Result Result(string query, IPublicAPI api) - { - string title; - MatchResult matchResult; - - // We suppose Name won't be null - if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description)) - { - title = Name; - matchResult = StringMatcher.FuzzySearch(query, Name); - } - else - { - title = $"{Name}: {Description}"; - var nameMatch = StringMatcher.FuzzySearch(query, Name); - var descriptionMatch = StringMatcher.FuzzySearch(query, Description); - if (descriptionMatch.Score > nameMatch.Score) - { - for (int i = 0; i < descriptionMatch.MatchData.Count; i++) - { - descriptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " - } - matchResult = descriptionMatch; - } - else - { - matchResult = nameMatch; - } - } - - if (!matchResult.IsSearchPrecisionScoreMet()) - return null; - - var result = new Result - { - Title = title, - AutoCompleteText = Name, - SubTitle = Main._settings.HideAppsPath ? string.Empty : Location, - IcoPath = LogoPath, - Preview = new Result.PreviewInfo - { - IsMedia = false, - PreviewImagePath = PreviewImagePath, - Description = Description - }, - Score = matchResult.Score, - TitleHighlightData = matchResult.MatchData, - ContextData = this, - Action = e => - { - var elevated = ( - e.SpecialKeyState.CtrlPressed && - e.SpecialKeyState.ShiftPressed && - !e.SpecialKeyState.AltPressed && - !e.SpecialKeyState.WinPressed - ); - - bool shouldRunElevated = elevated && CanRunElevated; - _ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false); - if (elevated && !shouldRunElevated) - { - var title = api.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error"); - var message = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator_not_supported_message"); - api.ShowMsg(title, message, string.Empty); - } - - return true; - } - }; - - - return result; - } - - public List ContextMenus(IPublicAPI api) - { - var contextMenus = new List - { - new Result - { - Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), - Action = _ => - { - Main.Context.API.OpenDirectory(Location); - - return true; - }, - IcoPath = "Images/folder.png" - } - }; - - if (CanRunElevated) - { - contextMenus.Add(new Result - { - Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), - Action = _ => - { - Task.Run(() => Launch(true)).ConfigureAwait(false); - return true; - }, - IcoPath = "Images/cmd.png" - }); - } - - return contextMenus; - } - - private void Launch(bool elevated = false) - { - string command = "shell:AppsFolder\\" + UserModelId; - command = Environment.ExpandEnvironmentVariables(command.Trim()); - - var info = new ProcessStartInfo(command) - { - UseShellExecute = true, - Verb = elevated ? "runas" : "" - }; - - Main.StartProcess(Process.Start, info); - } - - internal static bool IfAppCanRunElevated(XmlNode appNode) - { - // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package - // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes - - return appNode?.Attributes["EntryPoint"]?.Value == "Windows.FullTrustApplication" || - appNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL"; - } - - internal string LogoPathFromUri(string uri, (int, int) desiredSize) - { - // all https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets - // windows 10 https://msdn.microsoft.com/en-us/library/windows/apps/dn934817.aspx - // windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size - // windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx - - if (string.IsNullOrWhiteSpace(uri)) - { - ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + - $"|{UserModelId} 's logo uri is null or empty: {Location}", new ArgumentException("uri")); - return string.Empty; - } - - string path = Path.Combine(Location, uri); - - var pxCount = desiredSize.Item1 * desiredSize.Item2; - var logoPath = TryToFindLogo(uri, path, pxCount); - if (logoPath == string.Empty) - { - var tmp = Path.Combine(Location, "Assets", uri); - if (!path.Equals(tmp, StringComparison.OrdinalIgnoreCase)) - { - // TODO: Don't know why, just keep it at the moment - // Maybe on older version of Windows 10? - // for C:\Windows\MiracastView etc - return TryToFindLogo(uri, tmp, pxCount); - } - } - return logoPath; - - string TryToFindLogo(string uri, string path, int px) - { - var extension = Path.GetExtension(path); - if (extension != null) - { - //if (File.Exists(path)) - //{ - // return path; // shortcut, avoid enumerating files - //} - - var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44 - var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets - if (String.IsNullOrEmpty(logoNamePrefix) || !Directory.Exists(logoDir)) - { - // Known issue: Edge always triggers it since logo is not at uri - ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + - $"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Location}", new FileNotFoundException()); - return string.Empty; - } - - var logos = Directory.EnumerateFiles(logoDir, $"{logoNamePrefix}*{extension}"); - - // Currently we don't care which one to choose - // Just ignore all qualifiers - // select like logo.[xxx_yyy].png - // https://learn.microsoft.com/en-us/windows/uwp/app-resources/tailor-resources-lang-scale-contrast - - // todo select from file name like pt run - var selected = logos.FirstOrDefault(); - var closest = selected; - int min = int.MaxValue; - foreach (var logo in logos) - { - - var imageStream = File.OpenRead(logo); - var decoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.None); - var height = decoder.Frames[0].PixelHeight; - var width = decoder.Frames[0].PixelWidth; - int pixelCountDiff = Math.Abs(height * width - px); - if (pixelCountDiff < min) - { - // try to find the closest to desired size - closest = logo; - if (pixelCountDiff == 0) - break; // found - min = pixelCountDiff; - } - } - - selected = closest; - if (!string.IsNullOrEmpty(selected)) - { - return selected; - } - else - { - ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + - $"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Location}", new FileNotFoundException()); - return string.Empty; - } - } - else - { - ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + - $"|Unable to find extension from {uri} for {UserModelId} " + - $"in package location {Location}", new FileNotFoundException()); - return string.Empty; - } - } - } - - - #region logo legacy - // preserve for potential future use - - //public ImageSource Logo() - //{ - // var logo = ImageFromPath(LogoPath); - // var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package? - - // // todo magic! temp fix for cross thread object - // plated.Freeze(); - // return plated; - //} - //private BitmapImage ImageFromPath(string path) - //{ - // if (File.Exists(path)) - // { - // var image = new BitmapImage(); - // image.BeginInit(); - // image.UriSource = new Uri(path); - // image.CacheOption = BitmapCacheOption.OnLoad; - // image.EndInit(); - // image.Freeze(); - // return image; - // } - // else - // { - // ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Available" : path)}" + - // $"|Unable to get logo for {UserModelId} from {path} and" + - // $" located in {Location}", new FileNotFoundException()); - // return new BitmapImage(new Uri(Constant.MissingImgIcon)); - // } - //} - - //private ImageSource PlatedImage(BitmapImage image) - //{ - // if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent") - // { - // var width = image.Width; - // var height = image.Height; - // var x = 0; - // var y = 0; - - // var group = new DrawingGroup(); - - // var converted = ColorConverter.ConvertFromString(BackgroundColor); - // if (converted != null) - // { - // var color = (Color)converted; - // var brush = new SolidColorBrush(color); - // var pen = new Pen(brush, 1); - // var backgroundArea = new Rect(0, 0, width, width); - // var rectangle = new RectangleGeometry(backgroundArea); - // var rectDrawing = new GeometryDrawing(brush, pen, rectangle); - // group.Children.Add(rectDrawing); - - // var imageArea = new Rect(x, y, image.Width, image.Height); - // var imageDrawing = new ImageDrawing(image, imageArea); - // group.Children.Add(imageDrawing); - - // // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush - // var visual = new DrawingVisual(); - // var context = visual.RenderOpen(); - // context.DrawDrawing(group); - // context.Close(); - // const int dpiScale100 = 96; - // var bitmap = new RenderTargetBitmap( - // Convert.ToInt32(width), Convert.ToInt32(height), - // dpiScale100, dpiScale100, - // PixelFormats.Pbgra32 - // ); - // bitmap.Render(visual); - // return bitmap; - // } - // else - // { - // ProgramLogger.LogException($"|UWP|PlatedImage|{Location}" + - // $"|Unable to convert background string {BackgroundColor} " + - // $"to color for {Location}", new InvalidOperationException()); - - // return new BitmapImage(new Uri(Constant.MissingImgIcon)); - // } - // } - // else - // { - // // todo use windows theme as background - // return image; - // } - //} - - #endregion - public override string ToString() - { - return $"{DisplayName}: {Description}"; - } - - public override bool Equals(object obj) - { - if (obj is Application other) - { - return UniqueIdentifier == other.UniqueIdentifier; - } - else - { - return false; - } - } - - public override int GetHashCode() - { - return UniqueIdentifier.GetHashCode(); - } - } - - public enum PackageVersion - { - Windows10, - Windows81, - Windows8, - Unknown - } - } -} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs new file mode 100644 index 000000000..654897cc5 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs @@ -0,0 +1,752 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Principal; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using Windows.ApplicationModel; +using Windows.Management.Deployment; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin.Program.Logger; +using Flow.Launcher.Plugin.SharedModels; +using System.Threading.Channels; +using System.Xml; +using Windows.ApplicationModel.Core; +using System.Windows.Input; +using MemoryPack; + +namespace Flow.Launcher.Plugin.Program.Programs +{ + [MemoryPackable] + public partial class UWPPackage + { + public string Name { get; } + public string FullName { get; } + public string FamilyName { get; } + public string Location { get; set; } + + public UWPApp[] Apps { get; set; } = Array.Empty(); + + + /// + /// For serialization + /// + [MemoryPackConstructor] + private UWPPackage() + { + } + + public UWPPackage(Package package) + { + Location = package.InstalledLocation.Path; + Name = package.Id.Name; + FullName = package.Id.FullName; + FamilyName = package.Id.FamilyName; + } + + public void InitAppsInPackage(Package package) + { + var apps = new List(); + // WinRT + var appListEntries = package.GetAppListEntries(); + foreach (var app in appListEntries) + { + try + { + var tmp = new UWPApp(app, this); + apps.Add(tmp); + } + catch (Exception e) + { + ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" + + "|Unexpected exception occurs when trying to construct a Application from package" + + $"{FullName} from location {Location}", e); + } + } + + Apps = apps.ToArray(); + + try + { + var xmlDoc = GetManifestXml(); + if (xmlDoc == null) + { + return; + } + + var xmlRoot = xmlDoc.DocumentElement; + var packageVersion = GetPackageVersionFromManifest(xmlRoot); + if (!smallLogoNameFromVersion.TryGetValue(packageVersion, out string logoName) || + !bigLogoNameFromVersion.TryGetValue(packageVersion, out string bigLogoName)) + { + return; + } + + var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable); + namespaceManager.AddNamespace("d", + "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); // still need a name + namespaceManager.AddNamespace("rescap", + "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"); + namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10"); + + var allowElevationNode = + xmlRoot.SelectSingleNode("//rescap:Capability[@Name='allowElevation']", namespaceManager); + bool packageCanElevate = allowElevationNode != null; + + var appsNode = xmlRoot.SelectSingleNode("d:Applications", namespaceManager); + foreach (var app in Apps) + { + // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package + // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes + var id = app.UserModelId.Split('!')[1]; + var appNode = appsNode?.SelectSingleNode($"d:Application[@Id='{id}']", namespaceManager); + if (appNode != null) + { + app.CanRunElevated = packageCanElevate || UWPApp.IfAppCanRunElevated(appNode); + + // local name to fit all versions + var visualElement = + appNode.SelectSingleNode($"*[local-name()='VisualElements']", namespaceManager); + var logoUri = visualElement?.Attributes[logoName]?.Value; + app.LogoPath = app.LogoPathFromUri(logoUri, (64, 64)); + // use small logo or may have a big margin + var previewUri = visualElement?.Attributes[logoName]?.Value; + app.PreviewImagePath = app.LogoPathFromUri(previewUri, (256, 256)); + } + } + } + catch (Exception e) + { + ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" + + "|Unexpected exception occurs when trying to construct a Application from package" + + $"{FullName} from location {Location}", e); + } + } + + private XmlDocument GetManifestXml() + { + var manifest = Path.Combine(Location, "AppxManifest.xml"); + try + { + var file = File.ReadAllText(manifest); + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(file); + return xmlDoc; + } + catch (FileNotFoundException e) + { + ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "AppxManifest.xml not found.", e); + return null; + } + catch (Exception e) + { + ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", + "An unexpected error occurred and unable to parse AppxManifest.xml", e); + return null; + } + } + + private PackageVersion GetPackageVersionFromManifest(XmlNode xmlRoot) + { + if (xmlRoot != null) + { + var namespaces = xmlRoot.Attributes; + foreach (XmlAttribute ns in namespaces) + { + if (versionFromNamespace.TryGetValue(ns.Value, out var packageVersion)) + { + return packageVersion; + } + } + + ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" + + "|Trying to get the package version of the UWP program, but an unknown UWP app-manifest version in package " + + $"{FullName} from location {Location}", new FormatException()); + return PackageVersion.Unknown; + } + else + { + ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" + + "|Can't parse AppManifest.xml of package " + + $"{FullName} from location {Location}", + new ArgumentNullException(nameof(xmlRoot))); + return PackageVersion.Unknown; + } + } + + private static readonly Dictionary versionFromNamespace = new() + { + { "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 }, + { "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81 }, + { "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 }, + }; + + private static readonly Dictionary smallLogoNameFromVersion = new() + { + { PackageVersion.Windows10, "Square44x44Logo" }, + { PackageVersion.Windows81, "Square30x30Logo" }, + { PackageVersion.Windows8, "SmallLogo" }, + }; + + private static readonly Dictionary bigLogoNameFromVersion = new() + { + { PackageVersion.Windows10, "Square150x150Logo" }, + { PackageVersion.Windows81, "Square150x150Logo" }, + { PackageVersion.Windows8, "Logo" }, + }; + + public static UWPApp[] All(Settings settings) + { + var support = SupportUWP(); + if (support && settings.EnableUWP) + { + var applications = CurrentUserPackages().AsParallel().SelectMany(p => + { + UWPPackage u; + try + { + u = new UWPPackage(p); + u.InitAppsInPackage(p); + } +#if !DEBUG + catch (Exception e) + { + ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and unable to convert Package to UWP for {p.Id.FullName}", e); + return Array.Empty(); + } +#endif +#if DEBUG //make developer aware and implement handling + catch + { + throw; + } +#endif + return u.Apps; + }).ToArray(); + + var updatedListWithoutDisabledApps = applications + .Where(t1 => !Main._settings.DisabledProgramSources + .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)); + + return updatedListWithoutDisabledApps.ToArray(); + } + else + { + return Array.Empty(); + } + } + + public static bool SupportUWP() + { + var windows10 = new Version(10, 0); + var support = Environment.OSVersion.Version.Major >= windows10.Major; + return support; + } + + private static IEnumerable CurrentUserPackages() + { + var user = WindowsIdentity.GetCurrent().User; + + if (user != null) + { + var userId = user.Value; + PackageManager packageManager; + try + { + packageManager = new PackageManager(); + } + catch + { + // Bug from https://github.com/microsoft/CsWinRT, using Microsoft.Windows.SDK.NET.Ref 10.0.19041.0. + // Only happens on the first time, so a try catch can fix it. + packageManager = new PackageManager(); + } + + var packages = packageManager.FindPackagesForUser(userId); + packages = packages.Where(p => + { + try + { + var f = p.IsFramework; + var d = p.IsDevelopmentMode; + var path = p.InstalledLocation.Path; + return !f && !d && !string.IsNullOrEmpty(path); + } + catch (Exception e) + { + ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{p.Id.FullName}", + "An unexpected error occurred and " + + $"unable to verify if package is valid", e); + return false; + } + }); + return packages; + } + else + { + return Array.Empty(); + } + } + + private static Channel PackageChangeChannel = Channel.CreateBounded(1); + + public static async Task WatchPackageChange() + { + if (Environment.OSVersion.Version.Major >= 10) + { + var catalog = PackageCatalog.OpenForCurrentUser(); + catalog.PackageInstalling += (_, args) => + { + if (args.IsComplete) + PackageChangeChannel.Writer.TryWrite(default); + }; + catalog.PackageUninstalling += (_, args) => + { + if (args.IsComplete) + PackageChangeChannel.Writer.TryWrite(default); + }; + catalog.PackageUpdating += (_, args) => + { + if (args.IsComplete) + PackageChangeChannel.Writer.TryWrite(default); + }; + + while (await PackageChangeChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) + { + await Task.Delay(3000).ConfigureAwait(false); + PackageChangeChannel.Reader.TryRead(out _); + await Task.Run(Main.IndexUwpPrograms); + } + } + } + + public override string ToString() + { + return FamilyName; + } + + public override bool Equals(object obj) + { + if (obj is UWPPackage uwp) + { + return FamilyName.Equals(uwp.FamilyName); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + return FamilyName.GetHashCode(); + } + + + public enum PackageVersion + { + Windows10, + Windows81, + Windows8, + Unknown + } + } + + [MemoryPackable] + public partial class UWPApp : IProgram + { + private string _uid = string.Empty; + + public string UniqueIdentifier + { + get => _uid; + set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); + } + + public string DisplayName { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + + public string UserModelId { get; set; } = string.Empty; + + //public string BackgroundColor { get; set; } = string.Empty; // preserve for future use + public string Name => DisplayName; + public string Location { get; set; } = string.Empty; + + public bool Enabled { get; set; } = false; + public bool CanRunElevated { get; set; } = false; + public string LogoPath { get; set; } = string.Empty; + public string PreviewImagePath { get; set; } = string.Empty; + + [MemoryPackConstructor] + private UWPApp() + { + } + + public UWPApp(AppListEntry appListEntry, UWPPackage package) + { + UserModelId = appListEntry.AppUserModelId; + UniqueIdentifier = appListEntry.AppUserModelId; + DisplayName = appListEntry.DisplayInfo.DisplayName; + Description = appListEntry.DisplayInfo.Description; + Location = package.Location; + Enabled = true; + } + + public Result Result(string query, IPublicAPI api) + { + string title; + MatchResult matchResult; + + // We suppose Name won't be null + if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description)) + { + title = Name; + matchResult = StringMatcher.FuzzySearch(query, Name); + } + else + { + title = $"{Name}: {Description}"; + var nameMatch = StringMatcher.FuzzySearch(query, Name); + var descriptionMatch = StringMatcher.FuzzySearch(query, Description); + if (descriptionMatch.Score > nameMatch.Score) + { + for (int i = 0; i < descriptionMatch.MatchData.Count; i++) + { + descriptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " + } + + matchResult = descriptionMatch; + } + else + { + matchResult = nameMatch; + } + } + + if (!matchResult.IsSearchPrecisionScoreMet()) + return null; + + var result = new Result + { + Title = title, + AutoCompleteText = Name, + SubTitle = Main._settings.HideAppsPath ? string.Empty : Location, + IcoPath = LogoPath, + Preview = new Result.PreviewInfo + { + IsMedia = false, PreviewImagePath = PreviewImagePath, Description = Description + }, + Score = matchResult.Score, + TitleHighlightData = matchResult.MatchData, + ContextData = this, + Action = e => + { + // Ctrl + Enter to open containing folder + bool openFolder = e.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control; + if (openFolder) + { + Main.Context.API.OpenDirectory(Location); + return true; + } + + // Ctrl + Shift + Enter to run elevated + bool elevated = e.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); + + bool shouldRunElevated = elevated && CanRunElevated; + _ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false); + if (elevated && !shouldRunElevated) + { + var title = api.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error"); + var message = + api.GetTranslation( + "flowlauncher_plugin_program_run_as_administrator_not_supported_message"); + api.ShowMsg(title, message, string.Empty); + } + + return true; + } + }; + + + return result; + } + + public List ContextMenus(IPublicAPI api) + { + var contextMenus = new List + { + new Result + { + Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), + Action = _ => + { + Main.Context.API.OpenDirectory(Location); + + return true; + }, + IcoPath = "Images/folder.png", + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe838"), + } + }; + + if (CanRunElevated) + { + contextMenus.Add(new Result + { + Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), + Action = _ => + { + Task.Run(() => Launch(true)).ConfigureAwait(false); + return true; + }, + IcoPath = "Images/cmd.png", + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ef") + }); + } + + return contextMenus; + } + + private void Launch(bool elevated = false) + { + string command = "shell:AppsFolder\\" + UserModelId; + command = Environment.ExpandEnvironmentVariables(command.Trim()); + + var info = new ProcessStartInfo(command) { UseShellExecute = true, Verb = elevated ? "runas" : "" }; + + Main.StartProcess(Process.Start, info); + } + + internal static bool IfAppCanRunElevated(XmlNode appNode) + { + // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package + // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes + + return appNode?.Attributes["EntryPoint"]?.Value == "Windows.FullTrustApplication" || + appNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL"; + } + + internal string LogoPathFromUri(string uri, (int, int) desiredSize) + { + // all https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets + // windows 10 https://msdn.microsoft.com/en-us/library/windows/apps/dn934817.aspx + // windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size + // windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx + + if (string.IsNullOrWhiteSpace(uri)) + { + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + + $"|{UserModelId} 's logo uri is null or empty: {Location}", + new ArgumentException("uri")); + return string.Empty; + } + + string path = Path.Combine(Location, uri); + + var pxCount = desiredSize.Item1 * desiredSize.Item2; + var logoPath = TryToFindLogo(uri, path, pxCount); + if (logoPath == string.Empty) + { + var tmp = Path.Combine(Location, "Assets", uri); + if (!path.Equals(tmp, StringComparison.OrdinalIgnoreCase)) + { + // TODO: Don't know why, just keep it at the moment + // Maybe on older version of Windows 10? + // for C:\Windows\MiracastView etc + return TryToFindLogo(uri, tmp, pxCount); + } + } + + return logoPath; + + string TryToFindLogo(string uri, string path, int px) + { + var extension = Path.GetExtension(path); + if (extension != null) + { + //if (File.Exists(path)) + //{ + // return path; // shortcut, avoid enumerating files + //} + + var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44 + var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets + if (String.IsNullOrEmpty(logoNamePrefix) || !Directory.Exists(logoDir)) + { + // Known issue: Edge always triggers it since logo is not at uri + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + + $"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Location}", + new FileNotFoundException()); + return string.Empty; + } + + var logos = Directory.EnumerateFiles(logoDir, $"{logoNamePrefix}*{extension}"); + + // Currently we don't care which one to choose + // Just ignore all qualifiers + // select like logo.[xxx_yyy].png + // https://learn.microsoft.com/en-us/windows/uwp/app-resources/tailor-resources-lang-scale-contrast + + // todo select from file name like pt run + var selected = logos.FirstOrDefault(); + var closest = selected; + int min = int.MaxValue; + foreach (var logo in logos) + { + var imageStream = File.OpenRead(logo); + var decoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.IgnoreColorProfile, + BitmapCacheOption.None); + var height = decoder.Frames[0].PixelHeight; + var width = decoder.Frames[0].PixelWidth; + int pixelCountDiff = Math.Abs(height * width - px); + if (pixelCountDiff < min) + { + // try to find the closest to desired size + closest = logo; + if (pixelCountDiff == 0) + break; // found + min = pixelCountDiff; + } + } + + selected = closest; + if (!string.IsNullOrEmpty(selected)) + { + return selected; + } + else + { + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + + $"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Location}", + new FileNotFoundException()); + return string.Empty; + } + } + else + { + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" + + $"|Unable to find extension from {uri} for {UserModelId} " + + $"in package location {Location}", new FileNotFoundException()); + return string.Empty; + } + } + } + + + #region logo legacy + + // preserve for potential future use + + //public ImageSource Logo() + //{ + // var logo = ImageFromPath(LogoPath); + // var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package? + + // // todo magic! temp fix for cross thread object + // plated.Freeze(); + // return plated; + //} + //private BitmapImage ImageFromPath(string path) + //{ + // if (File.Exists(path)) + // { + // var image = new BitmapImage(); + // image.BeginInit(); + // image.UriSource = new Uri(path); + // image.CacheOption = BitmapCacheOption.OnLoad; + // image.EndInit(); + // image.Freeze(); + // return image; + // } + // else + // { + // ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Available" : path)}" + + // $"|Unable to get logo for {UserModelId} from {path} and" + + // $" located in {Location}", new FileNotFoundException()); + // return new BitmapImage(new Uri(Constant.MissingImgIcon)); + // } + //} + + //private ImageSource PlatedImage(BitmapImage image) + //{ + // if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent") + // { + // var width = image.Width; + // var height = image.Height; + // var x = 0; + // var y = 0; + + // var group = new DrawingGroup(); + + // var converted = ColorConverter.ConvertFromString(BackgroundColor); + // if (converted != null) + // { + // var color = (Color)converted; + // var brush = new SolidColorBrush(color); + // var pen = new Pen(brush, 1); + // var backgroundArea = new Rect(0, 0, width, width); + // var rectangle = new RectangleGeometry(backgroundArea); + // var rectDrawing = new GeometryDrawing(brush, pen, rectangle); + // group.Children.Add(rectDrawing); + + // var imageArea = new Rect(x, y, image.Width, image.Height); + // var imageDrawing = new ImageDrawing(image, imageArea); + // group.Children.Add(imageDrawing); + + // // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush + // var visual = new DrawingVisual(); + // var context = visual.RenderOpen(); + // context.DrawDrawing(group); + // context.Close(); + // const int dpiScale100 = 96; + // var bitmap = new RenderTargetBitmap( + // Convert.ToInt32(width), Convert.ToInt32(height), + // dpiScale100, dpiScale100, + // PixelFormats.Pbgra32 + // ); + // bitmap.Render(visual); + // return bitmap; + // } + // else + // { + // ProgramLogger.LogException($"|UWP|PlatedImage|{Location}" + + // $"|Unable to convert background string {BackgroundColor} " + + // $"to color for {Location}", new InvalidOperationException()); + + // return new BitmapImage(new Uri(Constant.MissingImgIcon)); + // } + // } + // else + // { + // // todo use windows theme as background + // return image; + // } + //} + + #endregion + + public override string ToString() + { + return $"{DisplayName}: {Description}"; + } + + public override bool Equals(object obj) + { + if (obj is UWPApp other) + { + return UniqueIdentifier == other.UniqueIdentifier; + } + else + { + return false; + } + } + + public override int GetHashCode() + { + return UniqueIdentifier.GetHashCode(); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index 4afedb9e4..a64a708ef 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Security; using System.Text; using System.Threading.Tasks; @@ -16,14 +15,22 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Channels; using Flow.Launcher.Plugin.Program.Views.Models; using IniParser; +using System.Windows.Input; +using MemoryPack; namespace Flow.Launcher.Plugin.Program.Programs { - [Serializable] - public class Win32 : IProgram, IEquatable + [MemoryPackable] + public partial class Win32 : IProgram, IEquatable { public string Name { get; set; } - public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison + + public string UniqueIdentifier + { + get => _uid; + set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); + } // For path comparison + public string IcoPath { get; set; } /// @@ -32,15 +39,20 @@ namespace Flow.Launcher.Plugin.Program.Programs public string FullPath { get; set; } /// - /// Path of the executable for .lnk, or the URL for .url. Arguments are included if any. + /// Path of the executable for .lnk, or the URL for .url /// public string LnkResolvedPath { get; set; } /// - /// Path of the actual executable file. Args are included. + /// Path of the actual executable file /// public string ExecutablePath => LnkResolvedPath ?? FullPath; + /// + /// Arguments for the executable. + /// + public string Args { get; set; } + public string ParentDirectory { get; set; } /// @@ -96,7 +108,8 @@ namespace Flow.Launcher.Plugin.Program.Programs bool useLocalizedName = !string.IsNullOrEmpty(LocalizedName) && !Name.Equals(LocalizedName); string resultName = useLocalizedName ? LocalizedName : Name; - if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || resultName.Equals(Description)) + if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || + resultName.Equals(Description)) { title = resultName; matchResult = StringMatcher.FuzzySearch(query, resultName); @@ -113,6 +126,7 @@ namespace Flow.Launcher.Plugin.Program.Programs { descriptionMatch.MatchData[i] += resultName.Length + 2; // 2 is ": " } + matchResult = descriptionMatch; } else @@ -129,10 +143,12 @@ namespace Flow.Launcher.Plugin.Program.Programs { candidates.Add(ExecutableName); } + if (useLocalizedName) { candidates.Add(Name); } + matchResult = Match(query, candidates); if (matchResult == null) { @@ -167,21 +183,26 @@ namespace Flow.Launcher.Plugin.Program.Programs Score = matchResult.Score, TitleHighlightData = matchResult.MatchData, ContextData = this, + TitleToolTip = $"{title}\n{ExecutablePath}", Action = c => { - var runAsAdmin = ( - c.SpecialKeyState.CtrlPressed && - c.SpecialKeyState.ShiftPressed && - !c.SpecialKeyState.AltPressed && - !c.SpecialKeyState.WinPressed - ); + // Ctrl + Enter to open containing folder + bool openFolder = c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control; + if (openFolder) + { + Main.Context.API.OpenDirectory(ParentDirectory, FullPath); + return true; + } + + // Ctrl + Shift + Enter to run as admin + bool runAsAdmin = c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); var info = new ProcessStartInfo { FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true, - Verb = runAsAdmin ? "runas" : "" + Verb = runAsAdmin ? "runas" : "", }; _ = Task.Run(() => Main.StartProcess(Process.Start, info)); @@ -205,9 +226,7 @@ namespace Flow.Launcher.Plugin.Program.Programs { var info = new ProcessStartInfo { - FileName = FullPath, - WorkingDirectory = ParentDirectory, - UseShellExecute = true + FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true }; Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info)); @@ -248,11 +267,29 @@ namespace Flow.Launcher.Plugin.Program.Programs }, IcoPath = "Images/folder.png", Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe838"), - } + }, }; + if (Extension(FullPath) == ShortcutExtension) + { + contextMenus.Add(OpenTargetFolderContextMenuResult(api)); + } return contextMenus; } + private Result OpenTargetFolderContextMenuResult(IPublicAPI api) + { + return new Result + { + Title = api.GetTranslation("flowlauncher_plugin_program_open_target_folder"), + Action = _ => + { + api.OpenDirectory(Path.GetDirectoryName(ExecutablePath), ExecutablePath); + return true; + }, + IcoPath = "Images/folder.png", + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe8de"), + }; + } public override string ToString() { @@ -309,12 +346,12 @@ namespace Flow.Launcher.Plugin.Program.Programs if (!string.IsNullOrEmpty(target) && File.Exists(target)) { program.LnkResolvedPath = Path.GetFullPath(target); - program.ExecutableName = Path.GetFileName(target); + program.ExecutableName = Path.GetFileNameWithoutExtension(target); var args = _helper.arguments; if (!string.IsNullOrEmpty(args)) { - program.LnkResolvedPath += " " + args; + program.Args = args; } var description = _helper.description; @@ -420,7 +457,8 @@ namespace Flow.Launcher.Plugin.Program.Programs } } - private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true) + private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes, + bool recursive = true) { if (!Directory.Exists(directory)) return Enumerable.Empty(); @@ -444,7 +482,8 @@ namespace Flow.Launcher.Plugin.Program.Programs } } - private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes, string[] protocols) + private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes, + string[] protocols) { // Disabled custom sources are not in DisabledProgramSources var paths = directories.AsParallel() @@ -461,12 +500,16 @@ namespace Flow.Launcher.Plugin.Program.Programs .SelectMany(p => EnumerateProgramsInDir(p, suffixes)) .Distinct(); + var startupPaths = GetStartupPaths(); + var programs = ExceptDisabledSource(allPrograms) + .Where(x => !startupPaths.Any(startup => FilesFolders.PathContains(startup, x))) .Select(x => GetProgramFromPath(x, protocols)); return programs; } - private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols, List commonParents) + private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols, + List commonParents) { var pathEnv = Environment.GetEnvironmentVariable("Path"); if (String.IsNullOrEmpty(pathEnv)) @@ -508,7 +551,8 @@ namespace Flow.Launcher.Plugin.Program.Programs toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p))); var programs = ExceptDisabledSource(toFilter) - .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid).ToList(); // ToList due to disposing issue + .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid) + .ToList(); // ToList due to disposing issue return programs; } @@ -603,13 +647,23 @@ namespace Flow.Launcher.Plugin.Program.Programs private static IEnumerable ProgramsHasher(IEnumerable programs) { - return programs.GroupBy(p => p.ExecutablePath.ToLowerInvariant()) + var startMenuPaths = GetStartMenuPaths(); + return programs.GroupBy(p => (p.ExecutablePath + p.Args).ToLowerInvariant()) .AsParallel() .SelectMany(g => { + // is shortcut and in start menu + var startMenu = g.Where(g => + g.LnkResolvedPath != null && + startMenuPaths.Any(x => FilesFolders.PathContains(x, g.FullPath))) + .ToList(); + if (startMenu.Any()) + return startMenu.Take(1); + + // distinct by description var temp = g.Where(g => !string.IsNullOrEmpty(g.Description)).ToList(); if (temp.Any()) - return DistinctBy(temp, x => x.Description); + return temp.Take(1); return g.Take(1); }); } @@ -705,6 +759,14 @@ namespace Flow.Launcher.Plugin.Program.Programs return new[] { userStartMenu, commonStartMenu }; } + private static IEnumerable GetStartupPaths() + { + var userStartup = Environment.GetFolderPath(Environment.SpecialFolder.Startup); + var commonStartup = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartup); + + return new[] { userStartup, commonStartup }; + } + public static void WatchProgramUpdate(Settings settings) { var paths = new List(); @@ -734,6 +796,7 @@ namespace Flow.Launcher.Plugin.Program.Programs while (reader.TryRead(out _)) { } + await Task.Run(Main.IndexWin32Programs); } } @@ -744,6 +807,7 @@ namespace Flow.Launcher.Plugin.Program.Programs { throw new ArgumentException("Path Not Exist"); } + var watcher = new FileSystemWatcher(directory); watcher.Created += static (_, _) => indexQueue.Writer.TryWrite(default); @@ -782,8 +846,10 @@ namespace Flow.Launcher.Plugin.Program.Programs parents.Remove(source); } } + result.AddRange(parents.Select(x => x.Location)); } + return result.DistinctBy(x => x.ToLowerInvariant()).ToList(); } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs index f59facaa4..b2aad63b3 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs @@ -102,7 +102,7 @@ namespace Flow.Launcher.Plugin.Program // CustomSuffixes no longer contains custom suffixes // users has tweaked the settings // or this function has been executed once - if (UseCustomSuffixes == true || ProgramSuffixes == null) + if (UseCustomSuffixes == true || ProgramSuffixes == null) return; var suffixes = ProgramSuffixes.ToList(); foreach(var item in BuiltinSuffixesStatus) @@ -117,17 +117,12 @@ namespace Flow.Launcher.Plugin.Program public bool EnableStartMenuSource { get; set; } = true; public bool EnableDescription { get; set; } = false; public bool HideAppsPath { get; set; } = true; + public bool HideUninstallers { get; set; } = false; public bool EnableRegistrySource { get; set; } = true; public bool EnablePathSource { get; set; } = false; public bool EnableUWP { get; set; } = true; - - public string CustomizedExplorer { get; set; } = Explorer; - public string CustomizedArgs { get; set; } = ExplorerArgs; + public bool HideDuplicatedWindowsApp { get; set; } = false; internal const char SuffixSeparator = ';'; - - internal const string Explorer = "explorer"; - - internal const string ExplorerArgs = "%s"; } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs index 93c33e9ad..0dc080c8d 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Text.Json.Serialization; using Flow.Launcher.Plugin.Program.Programs; diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml index fa97de4f2..973ac9f60 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml @@ -8,7 +8,7 @@ DataContext="{Binding RelativeSource={RelativeSource Self}}" mc:Ignorable="d"> - + @@ -18,40 +18,40 @@ + ToolTip="{DynamicResource flowlauncher_plugin_program_index_uwp_tooltip}" + Visibility="{Binding ShowUWPCheckbox, Converter={StaticResource BooleanToVisibilityConverter}}" /> @@ -67,30 +67,38 @@ BorderBrush="{DynamicResource Color03B}" BorderThickness="1" /> + +