diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 294c06fc1..11a921955 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -16,6 +16,8 @@ body: I have checked that this issue has not already been reported. - label: > I am using the latest version of Flow Launcher. + - label: > + I am using the prerelease version of Flow Launcher. - type: textarea attributes: diff --git a/.github/update_release_pr.py b/.github/update_release_pr.py new file mode 100644 index 000000000..ccea511b3 --- /dev/null +++ b/.github/update_release_pr.py @@ -0,0 +1,242 @@ +from os import getenv + +import requests + + +def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: str = "all") -> list[dict]: + """ + Fetches pull requests from a GitHub repository that match a given milestone and label. + + Args: + token (str): GitHub token. + owner (str): The owner of the repository. + repo (str): The name of the repository. + label (str): The label name. Filter is not applied when empty string. + state (str): State of PR, e.g. open, closed, all + + Returns: + list: A list of dictionaries, where each dictionary represents a pull request. + Returns an empty list if no PRs are found or an error occurs. + """ + headers = { + "Authorization": f"token {token}", + "Accept": "application/vnd.github.v3+json", + } + + milestone_id = None + milestone_url = f"https://api.github.com/repos/{owner}/{repo}/milestones" + params = {"state": "open"} + + try: + response = requests.get(milestone_url, headers=headers, params=params) + response.raise_for_status() + milestones = response.json() + + if len(milestones) > 2: + print("More than two milestones found, unable to determine the milestone required.") + exit(1) + + # milestones.pop() + for ms in milestones: + if ms["title"] != "Future": + milestone_id = ms["number"] + print(f"Gathering PRs with milestone {ms['title']}...") + break + + if not milestone_id: + print(f"No suitable milestone found in repository '{owner}/{repo}'.") + exit(1) + + except requests.exceptions.RequestException as e: + print(f"Error fetching milestones: {e}") + exit(1) + + # This endpoint allows filtering by milestone and label. A PR in GH's perspective is a type of issue. + prs_url = f"https://api.github.com/repos/{owner}/{repo}/issues" + params = { + "state": state, + "milestone": milestone_id, + "labels": label, + "per_page": 100, + } + + all_prs = [] + page = 1 + while True: + try: + params["page"] = page + response = requests.get(prs_url, headers=headers, params=params) + response.raise_for_status() # Raise an exception for HTTP errors + prs = response.json() + + if not prs: + break # No more PRs to fetch + + # Check for pr key since we are using issues endpoint instead. + all_prs.extend([item for item in prs if "pull_request" in item]) + page += 1 + + except requests.exceptions.RequestException as e: + print(f"Error fetching pull requests: {e}") + exit(1) + + return all_prs + + +def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all") -> list[dict]: + """ + Returns a list of pull requests after applying the label and state filters. + + Args: + pull_request_items (list[dict]): List of PR items. + label (str): The label name. Filter is not applied when empty string. + state (str): State of PR, e.g. open, closed, all + + Returns: + list: A list of dictionaries, where each dictionary represents a pull request. + Returns an empty list if no PRs are found. + """ + pr_list = [] + count = 0 + for pr in pull_request_items: + if state in [pr["state"], "all"] and (not label or [item for item in pr["labels"] if item["name"] == label]): + pr_list.append(pr) + count += 1 + + print(f"Found {count} PRs with {label if label else 'no filter on'} label and state as {state}") + + return pr_list + +def get_prs_assignees(pull_request_items: list[dict], label: str = "", state: str = "all") -> list[str]: + """ + Returns a list of pull request assignees after applying the label and state filters, excludes jjw24. + + Args: + pull_request_items (list[dict]): List of PR items. + label (str): The label name. Filter is not applied when empty string. + state (str): State of PR, e.g. open, closed, all + + Returns: + list: A list of strs, where each string is an assignee name. List is not distinct, so can contain + duplicate names. + Returns an empty list if none are found. + """ + assignee_list = [] + for pr in pull_request_items: + if state in [pr["state"], "all"] and (not label or [item for item in pr["labels"] if item["name"] == label]): + [assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24" ] + + print(f"Found {len(assignee_list)} assignees with {label if label else 'no filter on'} label and state as {state}") + + return assignee_list + +def get_pr_descriptions(pull_request_items: list[dict]) -> str: + """ + Returns the concatenated string of pr title and number in the format of + '- PR title 1 #3651 + - PR title 2 #3652 + - PR title 3 #3653 + ' + + Args: + pull_request_items (list[dict]): List of PR items. + + Returns: + str: a string of PR titles and numbers + """ + description_content = "" + for pr in pull_request_items: + description_content += f"- {pr['title']} #{pr['number']}\n" + + return description_content + + +def update_pull_request_description(token: str, owner: str, repo: str, pr_number: int, new_description: str) -> None: + """ + Updates the description (body) of a GitHub Pull Request. + + Args: + token (str): Token. + owner (str): The owner of the repository. + repo (str): The name of the repository. + pr_number (int): The number of the pull request to update. + new_description (str): The new content for the PR's description. + + Returns: + dict or None: The updated PR object (as a dictionary) if successful, + None otherwise. + """ + headers = { + "Authorization": f"token {token}", + "Accept": "application/vnd.github.v3+json", + "Content-Type": "application/json", + } + + url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}" + + payload = {"body": new_description} + + print(f"Attempting to update PR #{pr_number} in {owner}/{repo}...") + print(f"URL: {url}") + + try: + response = None + response = requests.patch(url, headers=headers, json=payload) + response.raise_for_status() + + print(f"Successfully updated PR #{pr_number}.") + + except requests.exceptions.RequestException as e: + print(f"Error updating pull request #{pr_number}: {e}") + if response is not None: + print(f"Response status code: {response.status_code}") + print(f"Response text: {response.text}") + exit(1) + + +if __name__ == "__main__": + github_token = getenv("GITHUB_TOKEN") + + if not github_token: + print("Error: GITHUB_TOKEN environment variable not set.") + exit(1) + + repository_owner = "flow-launcher" + repository_name = "flow.launcher" + state = "all" + + print(f"Fetching {state} PRs for {repository_owner}/{repository_name} ...") + + pull_requests = get_github_prs(github_token, repository_owner, repository_name) + + if not pull_requests: + print("No matching pull requests found") + exit(1) + + print(f"\nFound total of {len(pull_requests)} pull requests") + + release_pr = get_prs(pull_requests, "release", "open") + + if len(release_pr) != 1: + print(f"Unable to find the exact release PR. Returned result: {release_pr}") + exit(1) + + print(f"Found release PR: {release_pr[0]['title']}") + + enhancement_prs = get_prs(pull_requests, "enhancement", "closed") + bug_fix_prs = get_prs(pull_requests, "bug", "closed") + + description_content = "# Release notes\n" + description_content += f"## Features\n{get_pr_descriptions(enhancement_prs)}" if enhancement_prs else "" + description_content += f"## Bug fixes\n{get_pr_descriptions(bug_fix_prs)}" if bug_fix_prs else "" + + assignees = list(set(get_prs_assignees(pull_requests, "enhancement", "closed") + get_prs_assignees(pull_requests, "bug", "closed"))) + assignees.sort(key=str.lower) + + description_content += f"### Authors:\n{', '.join(assignees)}" + + update_pull_request_description( + github_token, repository_owner, repository_name, release_pr[0]["number"], description_content + ) + + print(f"PR content updated to:\n{description_content}") diff --git a/.github/workflows/default_plugins.yml b/.github/workflows/default_plugins.yml index 85acafae1..ec8dfcd4e 100644 --- a/.github/workflows/default_plugins.yml +++ b/.github/workflows/default_plugins.yml @@ -3,11 +3,10 @@ name: Publish Default Plugins on: push: branches: ['master'] - paths: ['Plugins/**'] workflow_dispatch: jobs: - build: + publish: runs-on: windows-latest steps: @@ -17,39 +16,24 @@ jobs: 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: Update Plugins To Production Version + run: | + $version = "1.0.0" + Get-Content appveyor.yml | ForEach-Object { + if ($_ -match "version:\s*'(\d+\.\d+\.\d+)\.") { + $version = $matches[1] + } + } + + $jsonFiles = Get-ChildItem -Path ".\Plugins\*\plugin.json" + foreach ($file in $jsonFiles) { + $plugin_old_ver = Get-Content $file.FullName -Raw | ConvertFrom-Json + (Get-Content $file) -replace '"Version"\s*:\s*".*?"', "`"Version`": `"$version`"" | Set-Content $file + $plugin_new_ver = Get-Content $file.FullName -Raw | ConvertFrom-Json + Write-Host "Updated" $plugin_old_ver.Name "version from" $plugin_old_ver.Version "to" $plugin_new_ver.Version + } - name: Get BrowserBookmark Version - if: steps.changes.outputs.browserbookmark == 'true' id: updated-version-browserbookmark uses: notiz-dev/github-action-json-property@release with: @@ -57,14 +41,12 @@ jobs: 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" @@ -76,7 +58,6 @@ jobs: - name: Get Calculator Version - if: steps.changes.outputs.calculator == 'true' id: updated-version-calculator uses: notiz-dev/github-action-json-property@release with: @@ -84,14 +65,12 @@ jobs: 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" @@ -103,7 +82,6 @@ jobs: - name: Get Explorer Version - if: steps.changes.outputs.explorer == 'true' id: updated-version-explorer uses: notiz-dev/github-action-json-property@release with: @@ -111,14 +89,12 @@ jobs: 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" @@ -130,7 +106,6 @@ jobs: - name: Get PluginIndicator Version - if: steps.changes.outputs.pluginindicator == 'true' id: updated-version-pluginindicator uses: notiz-dev/github-action-json-property@release with: @@ -138,14 +113,12 @@ jobs: 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" @@ -157,7 +130,6 @@ jobs: - name: Get PluginsManager Version - if: steps.changes.outputs.pluginsmanager == 'true' id: updated-version-pluginsmanager uses: notiz-dev/github-action-json-property@release with: @@ -165,14 +137,12 @@ jobs: 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" @@ -184,7 +154,6 @@ jobs: - name: Get ProcessKiller Version - if: steps.changes.outputs.processkiller == 'true' id: updated-version-processkiller uses: notiz-dev/github-action-json-property@release with: @@ -192,14 +161,12 @@ jobs: 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" @@ -211,7 +178,6 @@ jobs: - name: Get Program Version - if: steps.changes.outputs.program == 'true' id: updated-version-program uses: notiz-dev/github-action-json-property@release with: @@ -219,14 +185,12 @@ jobs: 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" @@ -238,7 +202,6 @@ jobs: - name: Get Shell Version - if: steps.changes.outputs.shell == 'true' id: updated-version-shell uses: notiz-dev/github-action-json-property@release with: @@ -246,14 +209,12 @@ jobs: 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" @@ -265,7 +226,6 @@ jobs: - name: Get Sys Version - if: steps.changes.outputs.sys == 'true' id: updated-version-sys uses: notiz-dev/github-action-json-property@release with: @@ -273,14 +233,12 @@ jobs: 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" @@ -292,7 +250,6 @@ jobs: - name: Get Url Version - if: steps.changes.outputs.url == 'true' id: updated-version-url uses: notiz-dev/github-action-json-property@release with: @@ -300,14 +257,12 @@ jobs: 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" @@ -319,7 +274,6 @@ jobs: - name: Get WebSearch Version - if: steps.changes.outputs.websearch == 'true' id: updated-version-websearch uses: notiz-dev/github-action-json-property@release with: @@ -327,14 +281,12 @@ jobs: 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" @@ -346,7 +298,6 @@ jobs: - name: Get WindowsSettings Version - if: steps.changes.outputs.windowssettings == 'true' id: updated-version-windowssettings uses: notiz-dev/github-action-json-property@release with: @@ -354,14 +305,12 @@ jobs: 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" diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 000000000..7498262de --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,91 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: Build + +on: + workflow_dispatch: + push: + branches: + - dev + - master + pull_request: + +jobs: + build: + + runs-on: windows-latest + env: + FlowVersion: 1.19.5 + NUGET_CERT_REVOCATION_MODE: offline + BUILD_NUMBER: ${{ github.run_number }} + steps: + - uses: actions/checkout@v4 + - name: Set Flow.Launcher.csproj version + id: update + uses: vers-one/dotnet-project-version-updater@v1.7 + with: + file: | + "**/SolutionAssemblyInfo.cs" + version: ${{ env.FlowVersion }}.${{ env.BUILD_NUMBER }} + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 7.0.x +# cache: true +# cache-dependency-path: | +# Flow.Launcher/packages.lock.json +# Flow.Launcher.Core/packages.lock.json +# Flow.Launcher.Infrastructure/packages.lock.json +# Flow.Launcher.Plugin/packages.lock.json + - name: Install vpk + run: dotnet tool install -g vpk + - name: Restore dependencies + run: nuget restore + - name: Build + run: dotnet build --no-restore -c Release + - name: Initialize Service + run: | + sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest + net start WSearch + - name: Test + run: dotnet test --no-build --verbosity normal -c Release + - name: Perform post_build tasks + shell: powershell + run: .\Scripts\post_build.ps1 + - name: Upload Plugin Nupkg + uses: actions/upload-artifact@v4 + with: + name: Plugin nupkg + path: | + Output\Release\Flow.Launcher.Plugin.*.nupkg + compression-level: 0 + - name: Upload Setup + uses: actions/upload-artifact@v4 + with: + name: Flow Installer + path: | + Output\Packages\Flow-Launcher-*.exe + compression-level: 0 + - name: Upload Portable Version + uses: actions/upload-artifact@v4 + with: + name: Portable Version + path: | + Output\Packages\Flow-Launcher-Portable.zip + compression-level: 0 + - name: Upload Full Nupkg + uses: actions/upload-artifact@v4 + with: + name: Full nupkg + path: | + Output\Packages\FlowLauncher-*-full.nupkg + + compression-level: 0 + - name: Upload Release Information + uses: actions/upload-artifact@v4 + with: + name: RELEASES + path: | + Output\Packages\RELEASES + compression-level: 0 diff --git a/.github/workflows/release_deploy.yml b/.github/workflows/release_deploy.yml new file mode 100644 index 000000000..9e082b95f --- /dev/null +++ b/.github/workflows/release_deploy.yml @@ -0,0 +1,34 @@ +--- + +name: New Release Deployments +on: + release: + types: [published] + workflow_dispatch: + +jobs: + deploy-website: + runs-on: ubuntu-latest + steps: + - name: Trigger dispatch event for deploying website + run: | + http_status=$(curl -L -f -s -o /dev/null -w "%{http_code}" \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.DEPLOY_FLOW_WEBSITE }}" \ + https://api.github.com/repos/Flow-Launcher/flow-launcher.github.io/dispatches \ + -d '{"event_type":"deploy"}') + if [ "$http_status" -ne 204 ]; then echo "Error: Deploy website failed, HTTP status code is $http_status"; exit 1; fi + + publish-chocolatey: + runs-on: ubuntu-latest + steps: + - name: Trigger dispatch event for publishing to Chocolatey + run: | + http_status=$(curl -L -f -s -o /dev/null -w "%{http_code}" \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.Publish_Chocolatey }}" \ + https://api.github.com/repos/Flow-Launcher/chocolatey-package/dispatches \ + -d '{"event_type":"publish"}') + if [ "$http_status" -ne 204 ]; then echo "Error: Publish Chocolatey package failed, HTTP status code is $http_status"; exit 1; fi diff --git a/.github/workflows/release_pr.yml b/.github/workflows/release_pr.yml new file mode 100644 index 000000000..451bf386c --- /dev/null +++ b/.github/workflows/release_pr.yml @@ -0,0 +1,25 @@ +name: Update release PR + +on: + pull_request: + types: [opened, reopened, synchronize] + branches: + - master + workflow_dispatch: + +jobs: + update-pr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Run release PR update + env: + GITHUB_TOKEN: ${{ secrets.PR_TOKEN }} + run: | + pip install requests -q + python3 ./.github/update_release_pr.py diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 7aaa9296a..47bd66107 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -41,9 +41,8 @@ on: # tags-ignore: # - "**" pull_request_target: - branches: - - '**' - # - '!l10n_dev' + branches-ignore: + - master tags-ignore: - "**" types: diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index 2b570d2c0..7f02cef09 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -1,21 +1,22 @@ -using Microsoft.Win32; -using Squirrel; -using System; +using System; using System.IO; +using System.Linq; using System.Reflection; using System.Windows; -using Flow.Launcher.Infrastructure; -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.Infrastructure; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; +using Microsoft.Win32; +using Squirrel; namespace Flow.Launcher.Core.Configuration { public class Portable : IPortable { + private static readonly string ClassName = nameof(Portable); + private readonly IPublicAPI API = Ioc.Default.GetRequiredService(); /// @@ -51,7 +52,7 @@ namespace Flow.Launcher.Core.Configuration } catch (Exception e) { - Log.Exception("|Portable.DisablePortableMode|Error occurred while disabling portable mode", e); + API.LogException(ClassName, "Error occurred while disabling portable mode", e); } } @@ -75,7 +76,7 @@ namespace Flow.Launcher.Core.Configuration } catch (Exception e) { - Log.Exception("|Portable.EnablePortableMode|Error occurred while enabling portable mode", e); + API.LogException(ClassName, "Error occurred while enabling portable mode", e); } } diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs index e9713564e..6f3b23e11 100644 --- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs @@ -1,25 +1,32 @@ -using Flow.Launcher.Infrastructure.Http; -using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Plugin; -using System; +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Json; +using System.Net.Sockets; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Http; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.ExternalPlugins { public record CommunityPluginSource(string ManifestFileUrl) { + private static readonly string ClassName = nameof(CommunityPluginSource); + + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + private string latestEtag = ""; private List plugins = new(); - private static JsonSerializerOptions PluginStoreItemSerializationOption = new JsonSerializerOptions() + private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }; @@ -34,35 +41,49 @@ namespace Flow.Launcher.Core.ExternalPlugins /// public async Task> FetchAsync(CancellationToken token) { - Log.Info(nameof(CommunityPluginSource), $"Loading plugins from {ManifestFileUrl}"); + API.LogInfo(ClassName, $"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) + try + { + 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; + if (response.StatusCode == HttpStatusCode.OK) + { + plugins = await response.Content + .ReadFromJsonAsync>(PluginStoreItemSerializationOption, cancellationToken: token) + .ConfigureAwait(false); + latestEtag = response.Headers.ETag?.Tag; - Log.Info(nameof(CommunityPluginSource), $"Loaded {this.plugins.Count} plugins from {ManifestFileUrl}"); - return this.plugins; + API.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}"); + return plugins; + } + else if (response.StatusCode == HttpStatusCode.NotModified) + { + API.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified."); + return plugins; + } + else + { + API.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}"); + return null; + } } - else if (response.StatusCode == HttpStatusCode.NotModified) + catch (Exception e) { - 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}"); + if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException) + { + API.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e); + } + else + { + API.LogException(ClassName, "Error Occurred", e); + } + return null; } } } diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs index 1f23c2f66..bdc1ad3dd 100644 --- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginStore.cs @@ -40,10 +40,14 @@ namespace Flow.Launcher.Core.ExternalPlugins 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(); + var result = await completedTask; + if (result != null) + { + // one of the requests completed successfully; keep its results + // and cancel the remaining http requests. + pluginResults = result; + cts.Cancel(); + } } tasks.Remove(completedTask); } diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index bbb6cf638..14796a87a 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Windows; using System.Windows.Forms; using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -14,6 +13,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments { public abstract class AbstractPluginEnvironment { + private static readonly string ClassName = nameof(AbstractPluginEnvironment); + protected readonly IPublicAPI API = Ioc.Default.GetRequiredService(); internal abstract string Language { get; } @@ -120,7 +121,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments else { API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language)); - Log.Error("PluginsLoader", + API.LogError(ClassName, $"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.", $"{Language}Environment"); diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs index b67059b1b..62d2d3e91 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptEnvironment.cs @@ -4,7 +4,6 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.ExternalPlugins.Environments { - internal class JavaScriptEnvironment : TypeScriptEnvironment { internal override string Language => AllowedLanguage.JavaScript; diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs index 6c8c5aa57..726bc4cd4 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs @@ -4,7 +4,6 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.ExternalPlugins.Environments { - internal class JavaScriptV2Environment : TypeScriptV2Environment { internal override string Language => AllowedLanguage.JavaScriptV2; diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs index fab5738de..455ee096d 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonEnvironment.cs @@ -5,6 +5,7 @@ using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; +using Microsoft.VisualStudio.Threading; namespace Flow.Launcher.Core.ExternalPlugins.Environments { @@ -30,13 +31,15 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal PythonEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); + internal override void InstallEnvironment() { FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); // Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and // uses Python plugin they need to custom install and use v3.8.9 - DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath).Wait(); + JTF.Run(() => DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs index 8a4f527ba..12965286f 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptEnvironment.cs @@ -5,6 +5,7 @@ using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; +using Microsoft.VisualStudio.Threading; namespace Flow.Launcher.Core.ExternalPlugins.Environments { @@ -27,11 +28,13 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal TypeScriptEnvironment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); + internal override void InstallEnvironment() { FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); - DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait(); + JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs index 61fd28376..6960b79c9 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -5,6 +5,7 @@ using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; +using Microsoft.VisualStudio.Threading; namespace Flow.Launcher.Core.ExternalPlugins.Environments { @@ -27,11 +28,13 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments internal TypeScriptV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext()); + internal override void InstallEnvironment() { FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s)); - DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait(); + JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath)); PluginsSettingsFilePath = ExecutablePath; } diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index 44d3ef0ff..7ca91eaec 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -9,6 +9,8 @@ namespace Flow.Launcher.Core.ExternalPlugins { public static class PluginsManifest { + private static readonly string ClassName = nameof(PluginsManifest); + private static readonly CommunityPluginStore mainPluginStore = new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json", "https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json", @@ -44,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins } catch (Exception e) { - Ioc.Default.GetRequiredService().LogException(nameof(PluginsManifest), "Http request failed", e); + Ioc.Default.GetRequiredService().LogException(ClassName, "Http request failed", e); } finally { diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index e0a217251..435d97ab7 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -12,7 +12,7 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { - public class JsonRPCPluginSettings + public class JsonRPCPluginSettings : ISavable { public required JsonRpcConfigurationModel? Configuration { get; init; } @@ -113,7 +113,7 @@ namespace Flow.Launcher.Core.Plugin // If can parse the default value to bool, use it, otherwise use false : value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString) && boolValueFromString; - checkBox.Dispatcher.Invoke(() =>checkBox.IsChecked = isChecked); + checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = isChecked); break; } } @@ -154,8 +154,7 @@ namespace Flow.Launcher.Core.Plugin public Control CreateSettingPanel() { - // No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true - // if (!NeedCreateSettingPanel()) return null; + if (!NeedCreateSettingPanel()) return null!; // Create main grid with two columns (Column 1: Auto, Column 2: *) var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; diff --git a/Flow.Launcher.Core/Plugin/PluginConfig.cs b/Flow.Launcher.Core/Plugin/PluginConfig.cs index 163f97046..f7457b4e1 100644 --- a/Flow.Launcher.Core/Plugin/PluginConfig.cs +++ b/Flow.Launcher.Core/Plugin/PluginConfig.cs @@ -3,14 +3,20 @@ using System.Collections.Generic; using System.Linq; using System.IO; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin; using System.Text.Json; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Flow.Launcher.Core.Plugin { internal abstract class PluginConfig { + private static readonly string ClassName = nameof(PluginConfig); + + // 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(); + /// /// Parse plugin metadata in the given directories /// @@ -32,7 +38,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception($"|PluginConfig.ParsePLuginConfigs|Can't delete <{directory}>", e); + API.LogException(ClassName, $"Can't delete <{directory}>", e); } } else @@ -49,11 +55,11 @@ namespace Flow.Launcher.Core.Plugin duplicateList .ForEach( - x => Log.Warn("PluginConfig", - string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " + - "not loaded due to version not the highest of the duplicates", - x.Name, x.ID, x.Version), - "GetUniqueLatestPluginMetadata")); + x => API.LogWarn(ClassName, + string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " + + "not loaded due to version not the highest of the duplicates", + x.Name, x.ID, x.Version), + "GetUniqueLatestPluginMetadata")); return uniqueList; } @@ -101,7 +107,7 @@ namespace Flow.Launcher.Core.Plugin string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName); if (!File.Exists(configPath)) { - Log.Error($"|PluginConfig.GetPluginMetadata|Didn't find config file <{configPath}>"); + API.LogError(ClassName, $"Didn't find config file <{configPath}>"); return null; } @@ -117,19 +123,19 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception($"|PluginConfig.GetPluginMetadata|invalid json for config <{configPath}>", e); + API.LogException(ClassName, $"Invalid json for config <{configPath}>", e); return null; } if (!AllowedLanguage.IsAllowed(metadata.Language)) { - Log.Error($"|PluginConfig.GetPluginMetadata|Invalid language <{metadata.Language}> for config <{configPath}>"); + API.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>"); return null; } if (!File.Exists(metadata.ExecuteFilePath)) { - Log.Error($"|PluginConfig.GetPluginMetadata|execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}"); + API.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}"); return null; } diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 29d91dc8d..9b525f331 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -26,6 +25,7 @@ namespace Flow.Launcher.Core.Plugin private static readonly string ClassName = nameof(PluginManager); private static IEnumerable _contextMenuPlugins; + private static IEnumerable _homePlugins; public static List AllPlugins { get; private set; } public static readonly HashSet GlobalPlugins = new(); @@ -37,7 +37,7 @@ namespace Flow.Launcher.Core.Plugin private static PluginsSettings Settings; private static List _metadatas; - private static List _modifiedPlugins = new(); + private static readonly List _modifiedPlugins = new(); /// /// Directories that will hold Flow Launcher plugin directory @@ -61,10 +61,17 @@ namespace Flow.Launcher.Core.Plugin /// public static void Save() { - foreach (var plugin in AllPlugins) + foreach (var pluginPair in AllPlugins) { - var savable = plugin.Plugin as ISavable; - savable?.Save(); + var savable = pluginPair.Plugin as ISavable; + try + { + savable?.Save(); + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e); + } } API.SavePluginSettings(); @@ -81,14 +88,21 @@ namespace Flow.Launcher.Core.Plugin private static async Task DisposePluginAsync(PluginPair pluginPair) { - switch (pluginPair.Plugin) + try { - case IDisposable disposable: - disposable.Dispose(); - break; - case IAsyncDisposable asyncDisposable: - await asyncDisposable.DisposeAsync(); - break; + switch (pluginPair.Plugin) + { + case IDisposable disposable: + disposable.Dispose(); + break; + case IAsyncDisposable asyncDisposable: + await asyncDisposable.DisposeAsync(); + break; + } + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e); } } @@ -173,11 +187,21 @@ namespace Flow.Launcher.Core.Plugin { if (AllowedLanguage.IsDotNet(metadata.Language)) { + if (string.IsNullOrEmpty(metadata.AssemblyName)) + { + API.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}"); + continue; // Skip if AssemblyName is not set, which can happen for erroneous plugins + } metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName); metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.AssemblyName); } else { + if (string.IsNullOrEmpty(metadata.Name)) + { + API.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}"); + continue; // Skip if Name is not set, which can happen for erroneous plugins + } metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name); metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.Name); } @@ -200,20 +224,33 @@ namespace Flow.Launcher.Core.Plugin () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API))); pair.Metadata.InitTime += milliseconds; - Log.Info( - $"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); + API.LogInfo(ClassName, + $"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>"); } catch (Exception e) { - Log.Exception(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e); - pair.Metadata.Disabled = true; - failedPlugins.Enqueue(pair); + API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e); + if (pair.Metadata.Disabled && pair.Metadata.HomeDisabled) + { + // If this plugin is already disabled, do not show error message again + // Or else it will be shown every time + API.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error"); + } + else + { + pair.Metadata.Disabled = true; + pair.Metadata.HomeDisabled = true; + failedPlugins.Enqueue(pair); + API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed"); + } } })); await Task.WhenAll(InitTasks); _contextMenuPlugins = GetPluginsForInterface(); + _homePlugins = GetPluginsForInterface(); + foreach (var plugin in AllPlugins) { // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin @@ -261,6 +298,11 @@ namespace Flow.Launcher.Core.Plugin }; } + public static ICollection ValidPluginsForHomeQuery() + { + return _homePlugins.ToList(); + } + public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) { var results = new List(); @@ -292,7 +334,7 @@ namespace Flow.Launcher.Core.Plugin { Title = $"{metadata.Name}: Failed to respond!", SubTitle = "Select this result for more info", - IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon, + IcoPath = Constant.ErrorIcon, PluginDirectory = metadata.PluginDirectory, ActionKeywordAssigned = query.ActionKeyword, PluginID = metadata.ID, @@ -305,6 +347,36 @@ namespace Flow.Launcher.Core.Plugin return results; } + public static async Task> QueryHomeForPluginAsync(PluginPair pair, Query query, CancellationToken token) + { + var results = new List(); + var metadata = pair.Metadata; + + try + { + var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", + async () => results = await ((IAsyncHomeQuery)pair.Plugin).HomeQueryAsync(token).ConfigureAwait(false)); + + token.ThrowIfCancellationRequested(); + if (results == null) + return null; + UpdatePluginMetadata(results, metadata, query); + + token.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException) + { + // null will be fine since the results will only be added into queue if the token hasn't been cancelled + return null; + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e); + return null; + } + return results; + } + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) @@ -356,8 +428,8 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception( - $"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>", + API.LogException(ClassName, + $"Can't load context menus for plugin <{pluginPair.Metadata.Name}>", e); } } @@ -365,12 +437,17 @@ namespace Flow.Launcher.Core.Plugin return results; } + public static bool IsHomePlugin(string id) + { + return _homePlugins.Any(p => p.Metadata.ID == id); + } + public static bool ActionKeywordRegistered(string actionKeyword) { // this method is only checking for action keywords (defined as not '*') registration // hence the actionKeyword != Query.GlobalPluginWildcardSign logic - return actionKeyword != Query.GlobalPluginWildcardSign - && NonGlobalPlugins.ContainsKey(actionKeyword); + return actionKeyword != Query.GlobalPluginWildcardSign + && NonGlobalPlugins.ContainsKey(actionKeyword); } /// @@ -549,7 +626,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception($"|PluginManager.InstallPlugin|Failed to delete temp folder {tempFolderPluginPath}", e); + API.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e); } if (checkModified) @@ -594,7 +671,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin settings folder for {plugin.Name}", e); + API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e); API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); } @@ -610,7 +687,7 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin cache folder for {plugin.Name}", e); + API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e); API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"), string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); } diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 1010d9f08..256c36065 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -89,19 +89,19 @@ namespace Flow.Launcher.Core.Plugin #else catch (Exception e) when (assembly == null) { - Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e); + Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e); } catch (InvalidOperationException e) { - Log.Exception($"|PluginsLoader.DotNetPlugins|Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); + Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e); } catch (ReflectionTypeLoadException e) { - Log.Exception($"|PluginsLoader.DotNetPlugins|The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); + Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e); } catch (Exception e) { - Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e); + Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e); } #endif diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs index bae263157..7a6bf07e2 100644 --- a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -1,21 +1,19 @@ -#nullable enable - -using System; -using System.Collections.Generic; +using System; 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; +#nullable enable + namespace Flow.Launcher.Core.Plugin { internal abstract class ProcessStreamPluginV2 : JsonRPCPluginV2 { - private static JobObject _jobObject = new JobObject(); + private static readonly JobObject _jobObject = new(); static ProcessStreamPluginV2() { @@ -66,11 +64,10 @@ namespace Flow.Launcher.Core.Plugin ClientPipe = new DuplexPipe(reader, writer); } - public override async Task ReloadDataAsync() { var oldProcess = ClientProcess; - ClientProcess = Process.Start(StartInfo); + ClientProcess = Process.Start(StartInfo)!; ArgumentNullException.ThrowIfNull(ClientProcess); SetupPipe(ClientProcess); await base.ReloadDataAsync(); @@ -79,7 +76,6 @@ namespace Flow.Launcher.Core.Plugin oldProcess.Dispose(); } - public override async ValueTask DisposeAsync() { await base.DisposeAsync(); diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index 3dc7877ac..25a32a728 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Flow.Launcher.Plugin; @@ -8,10 +8,24 @@ namespace Flow.Launcher.Core.Plugin { public static Query Build(string text, Dictionary nonGlobalPlugins) { + // home query + if (string.IsNullOrEmpty(text)) + { + return new Query() + { + Search = string.Empty, + RawQuery = string.Empty, + SearchTerms = Array.Empty(), + ActionKeyword = string.Empty, + IsHomeQuery = true + }; + } + // replace multiple white spaces with one white space var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries); if (terms.Length == 0) - { // nothing was typed + { + // nothing was typed return null; } @@ -21,25 +35,28 @@ namespace Flow.Launcher.Core.Plugin string[] searchTerms; if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled) - { // use non global plugin for query + { + // use non global plugin for query actionKeyword = possibleActionKeyword; search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty; searchTerms = terms[1..]; } else - { // non action keyword + { + // non action keyword actionKeyword = string.Empty; search = rawQuery.TrimStart(); searchTerms = terms; } - return new Query () + return new Query() { Search = search, RawQuery = rawQuery, SearchTerms = searchTerms, - ActionKeyword = actionKeyword + ActionKeyword = actionKeyword, + IsHomeQuery = false }; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index ffa17ab4d..b32b09e8f 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Windows; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using System.Globalization; @@ -17,13 +16,19 @@ namespace Flow.Launcher.Core.Resource { public class Internationalization { + private static readonly string ClassName = nameof(Internationalization); + + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + private 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 List _languageDirectories = new(); + private readonly List _oldResources = new(); private readonly string SystemLanguageCode; public Internationalization(Settings settings) @@ -80,7 +85,7 @@ namespace Flow.Launcher.Core.Resource } else { - Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>"); + API.LogError(ClassName, $"Can't find plugin path <{location}> for <{plugin.Metadata.Name}>"); } } @@ -144,13 +149,13 @@ namespace Flow.Launcher.Core.Resource _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } - private Language GetLanguageByLanguageCode(string languageCode) + private static Language GetLanguageByLanguageCode(string languageCode) { var lowercase = languageCode.ToLower(); var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); if (language == null) { - Log.Error($"|Internationalization.GetLanguageByLanguageCode|Language code can't be found <{languageCode}>"); + API.LogError(ClassName, $"Language code can't be found <{languageCode}>"); return AvailableLanguages.English; } else @@ -239,7 +244,7 @@ namespace Flow.Launcher.Core.Resource return list; } - public string GetTranslation(string key) + public static string GetTranslation(string key) { var translation = Application.Current.TryFindResource(key); if (translation is string) @@ -248,7 +253,7 @@ namespace Flow.Launcher.Core.Resource } else { - Log.Error($"|Internationalization.GetTranslation|No Translation for key {key}"); + API.LogError(ClassName, $"No Translation for key {key}"); return $"No Translation for key {key}"; } } @@ -257,8 +262,7 @@ namespace Flow.Launcher.Core.Resource { foreach (var p in PluginManager.GetPluginsForInterface()) { - var pluginI18N = p.Plugin as IPluginI18n; - if (pluginI18N == null) return; + if (p.Plugin is not IPluginI18n pluginI18N) return; try { p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); @@ -267,31 +271,31 @@ namespace Flow.Launcher.Core.Resource } catch (Exception e) { - Log.Exception($"|Internationalization.UpdatePluginMetadataTranslations|Failed for <{p.Metadata.Name}>", e); + API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); } } } - public string LanguageFile(string folder, string language) + private static string LanguageFile(string folder, string language) { if (Directory.Exists(folder)) { - string path = Path.Combine(folder, language); + var path = Path.Combine(folder, language); if (File.Exists(path)) { return path; } else { - Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>"); - string english = Path.Combine(folder, DefaultFile); + API.LogError(ClassName, $"Language path can't be found <{path}>"); + var english = Path.Combine(folder, DefaultFile); if (File.Exists(english)) { return english; } else { - Log.Error($"|Internationalization.LanguageFile|Default English Language path can't be found <{path}>"); + API.LogError(ClassName, $"Default English Language path can't be found <{path}>"); return string.Empty; } } diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 59e76e2d2..a6e8dc6bf 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -13,7 +13,6 @@ using System.Windows.Media.Effects; using System.Windows.Shell; using System.Windows.Threading; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; @@ -25,6 +24,8 @@ namespace Flow.Launcher.Core.Resource { #region Properties & Fields + private readonly string ClassName = nameof(Theme); + public bool BlurEnabled { get; private set; } private const string ThemeMetadataNamePrefix = "Name:"; @@ -73,9 +74,9 @@ namespace Flow.Launcher.Core.Resource } else { - Log.Error("Current theme resource not found. Initializing with default theme."); + _api.LogError(ClassName, "Current theme resource not found. Initializing with default theme."); _oldTheme = Constant.DefaultTheme; - }; + } } #endregion @@ -92,7 +93,7 @@ namespace Flow.Launcher.Core.Resource } catch (Exception e) { - Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e); + _api.LogException(ClassName, $"Exception when create directory <{dir}>", e); } } } @@ -125,7 +126,7 @@ namespace Flow.Launcher.Core.Resource // Load a ResourceDictionary for the specified theme. var themeName = _settings.Theme; var dict = GetThemeResourceDictionary(themeName); - + // Apply font settings to the theme resource. ApplyFontSettings(dict); UpdateResourceDictionary(dict); @@ -135,7 +136,7 @@ namespace Flow.Launcher.Core.Resource } catch (Exception e) { - Log.Exception("Error occurred while updating theme fonts", e); + _api.LogException(ClassName, "Error occurred while updating theme fonts", e); } } @@ -151,11 +152,11 @@ namespace Flow.Launcher.Core.Resource var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); - + SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true); SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); } - + if (dict["ItemTitleStyle"] is Style resultItemStyle && dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && @@ -171,7 +172,7 @@ namespace Flow.Launcher.Core.Resource SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); } - + if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle && dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) { @@ -196,7 +197,7 @@ namespace Flow.Launcher.Core.Resource // First, find the setters to remove and store them in a list var settersToRemove = style.Setters .OfType() - .Where(setter => + .Where(setter => setter.Property == Control.FontFamilyProperty || setter.Property == Control.FontStyleProperty || setter.Property == Control.FontWeightProperty || @@ -226,18 +227,18 @@ namespace Flow.Launcher.Core.Resource { var settersToRemove = style.Setters .OfType() - .Where(setter => + .Where(setter => setter.Property == TextBlock.FontFamilyProperty || setter.Property == TextBlock.FontStyleProperty || setter.Property == TextBlock.FontWeightProperty || setter.Property == TextBlock.FontStretchProperty) .ToList(); - + foreach (var setter in settersToRemove) { style.Setters.Remove(setter); } - + style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily)); style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle)); style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight)); @@ -324,7 +325,7 @@ namespace Flow.Launcher.Core.Resource return dict; } - private ResourceDictionary GetCurrentResourceDictionary() + public ResourceDictionary GetCurrentResourceDictionary() { return GetResourceDictionary(_settings.Theme); } @@ -387,7 +388,7 @@ namespace Flow.Launcher.Core.Resource var matchingTheme = themes.FirstOrDefault(t => t.FileNameWithoutExtension == _settings.Theme); if (matchingTheme == null) { - Log.Warn($"No matching theme found for '{_settings.Theme}'. Falling back to the first available theme."); + _api.LogWarn(ClassName, $"No matching theme found for '{_settings.Theme}'. Falling back to the first available theme."); } return matchingTheme ?? themes.FirstOrDefault(); } @@ -420,7 +421,7 @@ namespace Flow.Launcher.Core.Resource // Retrieve theme resource – always use the resource with font settings applied. var resourceDict = GetResourceDictionary(theme); - + UpdateResourceDictionary(resourceDict); _settings.Theme = theme; @@ -440,7 +441,7 @@ namespace Flow.Launcher.Core.Resource } catch (DirectoryNotFoundException) { - Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); + _api.LogError(ClassName, $"Theme <{theme}> path can't be found"); if (theme != Constant.DefaultTheme) { _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme)); @@ -450,7 +451,7 @@ namespace Flow.Launcher.Core.Resource } catch (XamlParseException) { - Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); + _api.LogError(ClassName, $"Theme <{theme}> fail to parse"); if (theme != Constant.DefaultTheme) { _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme)); @@ -670,7 +671,15 @@ namespace Flow.Launcher.Core.Resource windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Transparent))); } - + + // For themes with blur enabled, the window border is rendered by the system, so it's treated as a simple rectangle regardless of thickness. + //(This is to avoid issues when the window is forcibly changed to a rectangular shape during snap scenarios.) + var cornerRadiusSetter = windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property == Border.CornerRadiusProperty); + if (cornerRadiusSetter != null) + cornerRadiusSetter.Value = new CornerRadius(0); + else + windowBorderStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(0))); + // Apply the blur effect Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType); ColorizeWindow(theme, backdropType); @@ -771,22 +780,18 @@ namespace Flow.Launcher.Core.Resource { if (bgColor == null) return; - // Copy the existing WindowBorderStyle + // Create a new Style for the preview var previewStyle = new Style(typeof(Border)); - if (Application.Current.Resources.Contains("WindowBorderStyle")) + + // Get the original WindowBorderStyle + if (Application.Current.Resources.Contains("WindowBorderStyle") && + Application.Current.Resources["WindowBorderStyle"] is Style originalStyle) { - if (Application.Current.Resources["WindowBorderStyle"] is Style originalStyle) - { - foreach (var setter in originalStyle.Setters.OfType()) - { - previewStyle.Setters.Add(new Setter(setter.Property, setter.Value)); - } - } + // Copy the original style, including the base style if it exists + CopyStyle(originalStyle, previewStyle); } // Apply background color (remove transparency in color) - // WPF does not allow the use of an acrylic brush within the window's internal area, - // so transparency effects are not applied to the preview. Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B); previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor))); @@ -797,9 +802,26 @@ namespace Flow.Launcher.Core.Resource previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5))); previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1))); } + + // Set the new style to the resource Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle; } + private void CopyStyle(Style originalStyle, Style targetStyle) + { + // If the style is based on another style, copy the base style first + if (originalStyle.BasedOn != null) + { + CopyStyle(originalStyle.BasedOn, targetStyle); + } + + // Copy the setters from the original style + foreach (var setter in originalStyle.Setters.OfType()) + { + targetStyle.Setters.Add(new Setter(setter.Property, setter.Value)); + } + } + private void ColorizeWindow(string theme, BackdropTypes backdropType) { var dict = GetThemeResourceDictionary(theme); diff --git a/Flow.Launcher.Core/Resource/TranslationConverter.cs b/Flow.Launcher.Core/Resource/TranslationConverter.cs deleted file mode 100644 index eb0032758..000000000 --- a/Flow.Launcher.Core/Resource/TranslationConverter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; -using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Plugin; - -namespace Flow.Launcher.Core.Resource -{ - public class TranslationConverter : IValueConverter - { - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - var key = value.ToString(); - if (string.IsNullOrEmpty(key)) return key; - return API.GetTranslation(key); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => - throw new InvalidOperationException(); - } -} diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 83d4fd9e0..bc3655f69 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -12,7 +12,6 @@ using System.Windows; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; -using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using JetBrains.Annotations; @@ -24,6 +23,8 @@ namespace Flow.Launcher.Core { public string GitHubRepository { get; init; } + private static readonly string ClassName = nameof(Updater); + private readonly IPublicAPI _api; public Updater(IPublicAPI publicAPI, string gitHubRepository) @@ -51,7 +52,7 @@ namespace Flow.Launcher.Core var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); var currentVersion = Version.Parse(Constant.Version); - Log.Info($"|Updater.UpdateApp|Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>"); + _api.LogInfo(ClassName, $"Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>"); if (newReleaseVersion <= currentVersion) { @@ -84,7 +85,7 @@ namespace Flow.Launcher.Core var newVersionTips = NewVersionTips(newReleaseVersion.ToString()); - Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); + _api.LogInfo(ClassName, $"Update success:{newVersionTips}"); if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) { @@ -93,10 +94,14 @@ namespace Flow.Launcher.Core } catch (Exception e) { - if ((e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)) - Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); + if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException) + { + _api.LogException(ClassName, $"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); + } else - Log.Exception($"|Updater.UpdateApp|Error Occurred", e); + { + _api.LogException(ClassName, $"Error Occurred", e); + } if (!silentUpdate) _api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"), diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index f02b2297f..31547200b 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -66,7 +66,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 030aff7cf..22eb065f5 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -1,30 +1,31 @@ -using System.IO; +using System; +using System.IO; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; -using System; -using System.Threading; using Flow.Launcher.Plugin; -using CommunityToolkit.Mvvm.DependencyInjection; +using JetBrains.Annotations; namespace Flow.Launcher.Infrastructure.Http { public static class Http { + private static readonly string ClassName = nameof(Http); + private const string UserAgent = @"Mozilla/5.0 (Trident/7.0; rv:11.0) like Gecko"; - private static HttpClient client = new HttpClient(); + private static readonly HttpClient client = new(); static Http() { // need to be added so it would work on a win10 machine ServicePointManager.Expect100Continue = true; ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls - | SecurityProtocolType.Tls11 - | SecurityProtocolType.Tls12; + | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; client.DefaultRequestHeaders.Add("User-Agent", UserAgent); HttpClient.DefaultProxy = WebProxy; @@ -34,7 +35,7 @@ namespace Flow.Launcher.Infrastructure.Http public static HttpProxy Proxy { - private get { return proxy; } + private get => proxy; set { proxy = value; @@ -72,13 +73,13 @@ namespace Flow.Launcher.Infrastructure.Http ProxyProperty.Port => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), WebProxy.Credentials), ProxyProperty.UserName => (WebProxy.Address, new NetworkCredential(Proxy.UserName, Proxy.Password)), ProxyProperty.Password => (WebProxy.Address, new NetworkCredential(Proxy.UserName, Proxy.Password)), - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(null) }; } catch (UriFormatException e) { Ioc.Default.GetRequiredService().ShowMsg("Please try again", "Unable to parse Http Proxy"); - Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e); + Log.Exception(ClassName, "Unable to parse Uri", e); } } @@ -134,7 +135,7 @@ namespace Flow.Launcher.Infrastructure.Http } catch (HttpRequestException e) { - Log.Exception("Infrastructure.Http", "Http Request Error", e, "DownloadAsync"); + Log.Exception(ClassName, "Http Request Error", e, "DownloadAsync"); throw; } } @@ -147,7 +148,7 @@ namespace Flow.Launcher.Infrastructure.Http /// The Http result as string. Null if cancellation requested public static Task GetAsync([NotNull] string url, CancellationToken token = default) { - Log.Debug($"|Http.Get|Url <{url}>"); + Log.Debug(ClassName, $"Url <{url}>"); return GetAsync(new Uri(url), token); } @@ -159,7 +160,7 @@ namespace Flow.Launcher.Infrastructure.Http /// The Http result as string. Null if cancellation requested public static async Task GetAsync([NotNull] Uri url, CancellationToken token = default) { - Log.Debug($"|Http.Get|Url <{url}>"); + Log.Debug(ClassName, $"Url <{url}>"); using var response = await client.GetAsync(url, token); var content = await response.Content.ReadAsStringAsync(token); if (response.StatusCode != HttpStatusCode.OK) @@ -181,7 +182,6 @@ namespace Flow.Launcher.Infrastructure.Http public static Task GetStreamAsync([NotNull] string url, CancellationToken token = default) => GetStreamAsync(new Uri(url), token); - /// /// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation. /// @@ -191,7 +191,7 @@ namespace Flow.Launcher.Infrastructure.Http public static async Task GetStreamAsync([NotNull] Uri url, CancellationToken token = default) { - Log.Debug($"|Http.Get|Url <{url}>"); + Log.Debug(ClassName, $"Url <{url}>"); return await client.GetStreamAsync(url, token); } @@ -202,7 +202,7 @@ namespace Flow.Launcher.Infrastructure.Http public static async Task GetResponseAsync([NotNull] Uri url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken token = default) { - Log.Debug($"|Http.Get|Url <{url}>"); + Log.Debug(ClassName, $"Url <{url}>"); return await client.GetAsync(url, completionOption, token); } @@ -211,7 +211,27 @@ namespace Flow.Launcher.Infrastructure.Http /// public static async Task SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken token = default) { - return await client.SendAsync(request, completionOption, token); + try + { + return await client.SendAsync(request, completionOption, token); + } + catch (System.Exception) + { + return new HttpResponseMessage(HttpStatusCode.InternalServerError); + } + } + + public static async Task GetStringAsync(string url, CancellationToken token = default) + { + try + { + Log.Debug(ClassName, $"Url <{url}>"); + return await client.GetStringAsync(url, token); + } + catch (System.Exception e) + { + return string.Empty; + } } } } diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 9e31d2b4e..86df01a30 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -7,9 +7,8 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; -using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; -using Flow.Launcher.Plugin; using SharpVectors.Converters; using SharpVectors.Renderers.Wpf; @@ -17,10 +16,6 @@ namespace Flow.Launcher.Infrastructure.Image { public static class ImageLoader { - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - private static readonly string ClassName = nameof(ImageLoader); private static readonly ImageCache ImageCache = new(); @@ -58,14 +53,14 @@ namespace Flow.Launcher.Infrastructure.Image _ = Task.Run(async () => { - await API.StopwatchLogInfoAsync(ClassName, "Preload images cost", async () => + await Stopwatch.InfoAsync(ClassName, "Preload images cost", async () => { foreach (var (path, isFullImage) in usage) { await LoadAsync(path, isFullImage); } }); - API.LogInfo(ClassName, $"Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); + Log.Info(ClassName, $"Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); }); } @@ -81,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Image } catch (System.Exception e) { - API.LogException(ClassName, "Failed to save image cache to file", e); + Log.Exception(ClassName, "Failed to save image cache to file", e); } finally { @@ -176,8 +171,8 @@ namespace Flow.Launcher.Infrastructure.Image } catch (System.Exception e2) { - API.LogException(ClassName, $"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e); - API.LogException(ClassName, $"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2); + Log.Exception(ClassName, $"Failed to get thumbnail for {path} on first try", e); + Log.Exception(ClassName, $"Failed to get thumbnail for {path} on second try", e2); ImageSource image = ImageCache[Constant.MissingImgIcon, false]; ImageCache[path, false] = image; @@ -243,7 +238,7 @@ namespace Flow.Launcher.Infrastructure.Image { image = Image; type = ImageType.Error; - API.LogException(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex); + Log.Exception(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex); } } else @@ -267,7 +262,7 @@ namespace Flow.Launcher.Infrastructure.Image { image = Image; type = ImageType.Error; - API.LogException(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex); + Log.Exception(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex); } } else diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index b98ea50fe..4ce0df026 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -12,7 +12,7 @@ using Windows.Win32.Graphics.Gdi; namespace Flow.Launcher.Infrastructure.Image { /// - /// Subclass of + /// Subclass of /// [Flags] public enum ThumbnailOptions @@ -31,7 +31,9 @@ namespace Flow.Launcher.Infrastructure.Image private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID; - private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200; + private static readonly HRESULT S_EXTRACTIONFAILED = (HRESULT)0x8004B200; + + private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205; public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options) { @@ -79,9 +81,10 @@ namespace Flow.Launcher.Infrastructure.Image { imageFactory.GetImage(size, (SIIGBF)options, &hBitmap); } - catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly) + catch (COMException ex) when (options == ThumbnailOptions.ThumbnailOnly && + (ex.HResult == S_PATHNOTFOUND || ex.HResult == S_EXTRACTIONFAILED)) { - // Fallback to IconOnly if ThumbnailOnly fails + // Fallback to IconOnly if extraction fails or files cannot be found imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap); } catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly) @@ -89,6 +92,11 @@ namespace Flow.Launcher.Infrastructure.Image // Fallback to IconOnly if files cannot be found imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap); } + catch (System.Exception ex) + { + // Handle other exceptions + throw new InvalidOperationException("Failed to get thumbnail", ex); + } } finally { diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 807d631c7..09eb98f46 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -94,13 +94,6 @@ namespace Flow.Launcher.Infrastructure.Logger logger.Fatal(message); } - private static bool FormatValid(string message) - { - var parts = message.Split('|'); - var valid = parts.Length == 3 && !string.IsNullOrWhiteSpace(parts[1]) && !string.IsNullOrWhiteSpace(parts[2]); - return valid; - } - public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") { exception = exception.Demystify(); @@ -135,57 +128,14 @@ namespace Flow.Launcher.Infrastructure.Logger return className; } +#if !DEBUG private static void ExceptionInternal(string classAndMethod, string message, System.Exception e) { var logger = LogManager.GetLogger(classAndMethod); logger.Error(e, message); } - - private static void LogInternal(string message, LogLevel level) - { - if (FormatValid(message)) - { - var parts = message.Split('|'); - var prefix = parts[1]; - var unprefixed = parts[2]; - var logger = LogManager.GetLogger(prefix); - logger.Log(level, unprefixed); - } - else - { - LogFaultyFormat(message); - } - } - - /// Example: "|ClassName.MethodName|Message" - /// Example: "|ClassName.MethodName|Message" - /// Exception - public static void Exception(string message, System.Exception e) - { - e = e.Demystify(); -#if DEBUG - ExceptionDispatchInfo.Capture(e).Throw(); -#else - if (FormatValid(message)) - { - var parts = message.Split('|'); - var prefix = parts[1]; - var unprefixed = parts[2]; - ExceptionInternal(prefix, unprefixed, e); - } - else - { - LogFaultyFormat(message); - } #endif - } - - /// Example: "|ClassName.MethodName|Message" - public static void Error(string message) - { - LogInternal(message, LogLevel.Error); - } public static void Error(string className, string message, [CallerMemberName] string methodName = "") { @@ -206,33 +156,15 @@ namespace Flow.Launcher.Infrastructure.Logger LogInternal(LogLevel.Debug, className, message, methodName); } - /// Example: "|ClassName.MethodName|Message"" - public static void Debug(string message) - { - LogInternal(message, LogLevel.Debug); - } - public static void Info(string className, string message, [CallerMemberName] string methodName = "") { LogInternal(LogLevel.Info, className, message, methodName); } - /// Example: "|ClassName.MethodName|Message" - public static void Info(string message) - { - LogInternal(message, LogLevel.Info); - } - public static void Warn(string className, string message, [CallerMemberName] string methodName = "") { LogInternal(LogLevel.Warn, className, message, methodName); } - - /// Example: "|ClassName.MethodName|Message" - public static void Warn(string message) - { - LogInternal(message, LogLevel.Warn); - } } public enum LOGLEVEL diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index 18b206022..edc71feef 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -22,7 +22,7 @@ SystemParametersInfo SetForegroundWindow -GetWindowLong +WINDOW_LONG_PTR_INDEX GetForegroundWindow GetDesktopWindow GetShellWindow @@ -42,6 +42,14 @@ MONITORINFOEXW WM_ENTERSIZEMOVE WM_EXITSIZEMOVE +WM_NCLBUTTONDBLCLK +WM_SYSCOMMAND + +SC_MAXIMIZE +SC_MINIMIZE + +OleInitialize +OleUninitialize GetKeyboardLayout GetWindowThreadProcessId @@ -53,4 +61,8 @@ INPUTLANGCHANGE_FORWARD LOCALE_TRANSIENT_KEYBOARD1 LOCALE_TRANSIENT_KEYBOARD2 LOCALE_TRANSIENT_KEYBOARD3 -LOCALE_TRANSIENT_KEYBOARD4 \ No newline at end of file +LOCALE_TRANSIENT_KEYBOARD4 + +SHParseDisplayName +SHOpenFolderAndSelectItems +CoTaskMemFree diff --git a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs index 1a72ab7a6..18b992043 100644 --- a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs +++ b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs @@ -4,14 +4,16 @@ using Windows.Win32.UI.WindowsAndMessaging; namespace Windows.Win32; -// Edited from: https://github.com/files-community/Files internal static partial class PInvoke { + // SetWindowLong + // Edited from: https://github.com/files-community/Files + [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] - static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); + private static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] - static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); + private static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); // NOTE: // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa. @@ -22,4 +24,22 @@ internal static partial class PInvoke ? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong) : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); } + + // GetWindowLong + + [DllImport("User32", EntryPoint = "GetWindowLongW", ExactSpelling = true)] + private static extern int _GetWindowLong(HWND hWnd, int nIndex); + + [DllImport("User32", EntryPoint = "GetWindowLongPtrW", ExactSpelling = true)] + private static extern nint _GetWindowLongPtr(HWND hWnd, int nIndex); + + // NOTE: + // CsWin32 doesn't generate GetWindowLong on other than x86 and vice versa. + // For more info, visit https://github.com/microsoft/CsWin32/issues/882 + public static unsafe nint GetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex) + { + return sizeof(nint) is 4 + ? _GetWindowLong(hWnd, (int)nIndex) + : _GetWindowLongPtr(hWnd, (int)nIndex); + } } diff --git a/Flow.Launcher.Infrastructure/Stopwatch.cs b/Flow.Launcher.Infrastructure/Stopwatch.cs index 784d323fe..870e0fe26 100644 --- a/Flow.Launcher.Infrastructure/Stopwatch.cs +++ b/Flow.Launcher.Infrastructure/Stopwatch.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; @@ -9,54 +10,50 @@ namespace Flow.Launcher.Infrastructure /// /// This stopwatch will appear only in Debug mode /// - public static long Debug(string message, Action action) + public static long Debug(string className, string message, Action action, [CallerMemberName] string methodName = "") { var stopWatch = new System.Diagnostics.Stopwatch(); stopWatch.Start(); action(); stopWatch.Stop(); var milliseconds = stopWatch.ElapsedMilliseconds; - string info = $"{message} <{milliseconds}ms>"; - Log.Debug(info); + Log.Debug(className, $"{message} <{milliseconds}ms>", methodName); return milliseconds; } /// /// This stopwatch will appear only in Debug mode /// - public static async Task DebugAsync(string message, Func action) + public static async Task DebugAsync(string className, string message, Func action, [CallerMemberName] string methodName = "") { var stopWatch = new System.Diagnostics.Stopwatch(); stopWatch.Start(); await action(); stopWatch.Stop(); var milliseconds = stopWatch.ElapsedMilliseconds; - string info = $"{message} <{milliseconds}ms>"; - Log.Debug(info); + Log.Debug(className, $"{message} <{milliseconds}ms>", methodName); return milliseconds; } - public static long Normal(string message, Action action) + public static long Info(string className, string message, Action action, [CallerMemberName] string methodName = "") { var stopWatch = new System.Diagnostics.Stopwatch(); stopWatch.Start(); action(); stopWatch.Stop(); var milliseconds = stopWatch.ElapsedMilliseconds; - string info = $"{message} <{milliseconds}ms>"; - Log.Info(info); + Log.Info(className, $"{message} <{milliseconds}ms>", methodName); return milliseconds; } - public static async Task NormalAsync(string message, Func action) + public static async Task InfoAsync(string className, string message, Func action, [CallerMemberName] string methodName = "") { var stopWatch = new System.Diagnostics.Stopwatch(); stopWatch.Start(); await action(); stopWatch.Stop(); var milliseconds = stopWatch.ElapsedMilliseconds; - string info = $"{message} <{milliseconds}ms>"; - Log.Info(info); + Log.Info(className, $"{message} <{milliseconds}ms>", methodName); return milliseconds; } } diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index 43bb8dade..48e6b5523 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -19,6 +20,8 @@ namespace Flow.Launcher.Infrastructure.Storage /// public class BinaryStorage : ISavable { + private static readonly string ClassName = "BinaryStorage"; + protected T? Data; public const string FileSuffix = ".cache"; @@ -40,6 +43,16 @@ namespace Flow.Launcher.Infrastructure.Storage FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } + // Let the old Program plugin get this constructor + [Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")] + public BinaryStorage(string filename, string directoryPath = null!) + { + DirectoryPath = directoryPath ?? DataLocation.CacheDirectory; + FilesFolders.ValidateDirectory(DirectoryPath); + + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); + } + public async ValueTask TryLoadAsync(T defaultData) { if (Data != null) return Data; @@ -48,7 +61,7 @@ namespace Flow.Launcher.Infrastructure.Storage { if (new FileInfo(FilePath).Length == 0) { - Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>"); + Log.Error(ClassName, $"Zero length cache file <{FilePath}>"); Data = defaultData; await SaveAsync(); } @@ -58,7 +71,7 @@ namespace Flow.Launcher.Infrastructure.Storage } else { - Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data"); + Log.Info(ClassName, "Cache file not exist, load default data"); Data = defaultData; await SaveAsync(); } @@ -82,8 +95,10 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { - var serialized = MemoryPackSerializer.Serialize(Data); + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + var serialized = MemoryPackSerializer.Serialize(Data); File.WriteAllBytes(FilePath, serialized); } @@ -103,6 +118,9 @@ namespace Flow.Launcher.Infrastructure.Storage // so we need to pass it to SaveAsync public async ValueTask SaveAsync(T data) { + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + await using var stream = new FileStream(FilePath, FileMode.Create); await MemoryPackSerializer.SerializeAsync(stream, data); } diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs index 8b4062b6b..857490bad 100644 --- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs @@ -1,27 +1,24 @@ using System.IO; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { - public class FlowLauncherJsonStorage : JsonStorage where T : new() + // Expose ISaveable interface in derived class to make sure we are calling the new version of Save method + public class FlowLauncherJsonStorage : JsonStorage, ISavable where T : new() { private static readonly string ClassName = "FlowLauncherJsonStorage"; - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - public FlowLauncherJsonStorage() { - var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); - FilesFolders.ValidateDirectory(directoryPath); + DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); + FilesFolders.ValidateDirectory(DirectoryPath); var filename = typeof(T).Name; - FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } public new void Save() @@ -32,7 +29,7 @@ namespace Flow.Launcher.Infrastructure.Storage } catch (System.Exception e) { - API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e); + Log.Exception(ClassName, $"Failed to save FL settings to path: {FilePath}", e); } } @@ -44,7 +41,7 @@ namespace Flow.Launcher.Infrastructure.Storage } catch (System.Exception e) { - API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e); + Log.Exception(ClassName, $"Failed to save FL settings to path: {FilePath}", e); } } } diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index cdf3ae909..c7eba05fd 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -16,6 +16,8 @@ namespace Flow.Launcher.Infrastructure.Storage /// public class JsonStorage : ISavable where T : new() { + private static readonly string ClassName = "JsonStorage"; + protected T? Data; // need a new directory name @@ -43,6 +45,22 @@ namespace Flow.Launcher.Infrastructure.Storage FilesFolders.ValidateDirectory(DirectoryPath); } + public bool Exists() + { + return File.Exists(FilePath); + } + + public void Delete() + { + foreach (var path in new[] { FilePath, BackupFilePath, TempFilePath }) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + public async Task LoadAsync() { if (Data != null) @@ -104,7 +122,7 @@ namespace Flow.Launcher.Infrastructure.Storage private void RestoreBackup() { - Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully"); + Log.Info(ClassName, $"Failed to load settings.json, {BackupFilePath} restored successfully"); if (File.Exists(FilePath)) File.Replace(BackupFilePath, FilePath, null); @@ -183,7 +201,10 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { - string serialized = JsonSerializer.Serialize(Data, + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + + var serialized = JsonSerializer.Serialize(Data, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(TempFilePath, serialized); @@ -193,6 +214,9 @@ namespace Flow.Launcher.Infrastructure.Storage public async Task SaveAsync() { + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + await using var tempOutput = File.OpenWrite(TempFilePath); await JsonSerializer.SerializeAsync(tempOutput, Data, new JsonSerializerOptions { WriteIndented = true }); diff --git a/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs index d18060e3d..0e0906e73 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs @@ -1,19 +1,16 @@ using System.IO; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { - public class PluginBinaryStorage : BinaryStorage where T : new() + // Expose ISaveable interface in derived class to make sure we are calling the new version of Save method + public class PluginBinaryStorage : BinaryStorage, ISavable where T : new() { private static readonly string ClassName = "PluginBinaryStorage"; - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - public PluginBinaryStorage(string cacheName, string cacheDirectory) { DirectoryPath = cacheDirectory; @@ -30,7 +27,7 @@ namespace Flow.Launcher.Infrastructure.Storage } catch (System.Exception e) { - API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e); + Log.Exception(ClassName, $"Failed to save plugin caches to path: {FilePath}", e); } } @@ -42,7 +39,7 @@ namespace Flow.Launcher.Infrastructure.Storage } catch (System.Exception e) { - API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e); + Log.Exception(ClassName, $"Failed to save plugin caches to path: {FilePath}", e); } } } diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index e8cbd70fb..d59083071 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -1,23 +1,20 @@ using System.IO; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.Storage { - public class PluginJsonStorage : JsonStorage where T : new() + // Expose ISaveable interface in derived class to make sure we are calling the new version of Save method + public class PluginJsonStorage : JsonStorage, ISavable where T : new() { // Use assembly name to check which plugin is using this storage public readonly string AssemblyName; private static readonly string ClassName = "PluginJsonStorage"; - // We should not initialize API in static constructor because it will create another API instance - private static IPublicAPI api = null; - private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); - public PluginJsonStorage() { // C# related, add python related below @@ -42,7 +39,7 @@ namespace Flow.Launcher.Infrastructure.Storage } catch (System.Exception e) { - API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); + Log.Exception(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); } } @@ -54,7 +51,7 @@ namespace Flow.Launcher.Infrastructure.Storage } catch (System.Exception e) { - API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); + Log.Exception(ClassName, $"Failed to save plugin settings to path: {FilePath}", e); } } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs index 71020369a..2603d4675 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs @@ -1,15 +1,17 @@ using System; using System.Text.Json.Serialization; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings { + #region Base + public abstract class ShortcutBaseModel { public string Key { get; set; } - [JsonIgnore] - public Func Expand { get; set; } = () => { return ""; }; - public override bool Equals(object obj) { return obj is ShortcutBaseModel other && @@ -22,16 +24,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } - public class CustomShortcutModel : ShortcutBaseModel + public class BaseCustomShortcutModel : ShortcutBaseModel { public string Value { get; set; } - [JsonConstructorAttribute] - public CustomShortcutModel(string key, string value) + public BaseCustomShortcutModel(string key, string value) { Key = key; Value = value; - Expand = () => { return Value; }; } public void Deconstruct(out string key, out string value) @@ -40,26 +40,75 @@ namespace Flow.Launcher.Infrastructure.UserSettings value = Value; } - public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut) + public static implicit operator (string Key, string Value)(BaseCustomShortcutModel shortcut) { return (shortcut.Key, shortcut.Value); } - public static implicit operator CustomShortcutModel((string Key, string Value) shortcut) + public static implicit operator BaseCustomShortcutModel((string Key, string Value) shortcut) { - return new CustomShortcutModel(shortcut.Key, shortcut.Value); + return new BaseCustomShortcutModel(shortcut.Key, shortcut.Value); } } - public class BuiltinShortcutModel : ShortcutBaseModel + public class BaseBuiltinShortcutModel : ShortcutBaseModel { public string Description { get; set; } - public BuiltinShortcutModel(string key, string description, Func expand) + public string LocalizedDescription => API.GetTranslation(Description); + + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + + public BaseBuiltinShortcutModel(string key, string description) { Key = key; Description = description; - Expand = expand ?? (() => { return ""; }); } } + + #endregion + + #region Custom Shortcut + + public class CustomShortcutModel : BaseCustomShortcutModel + { + [JsonIgnore] + public Func Expand { get; set; } = () => { return string.Empty; }; + + [JsonConstructor] + public CustomShortcutModel(string key, string value) : base(key, value) + { + Expand = () => { return Value; }; + } + } + + #endregion + + #region Builtin Shortcut + + public class BuiltinShortcutModel : BaseBuiltinShortcutModel + { + [JsonIgnore] + public Func Expand { get; set; } = () => { return string.Empty; }; + + public BuiltinShortcutModel(string key, string description, Func expand) : base(key, description) + { + Expand = expand ?? (() => { return string.Empty; }); + } + } + + public class AsyncBuiltinShortcutModel : BaseBuiltinShortcutModel + { + [JsonIgnore] + public Func> ExpandAsync { get; set; } = () => { return Task.FromResult(string.Empty); }; + + public AsyncBuiltinShortcutModel(string key, string description, Func> expandAsync) : base(key, description) + { + ExpandAsync = expandAsync ?? (() => { return Task.FromResult(string.Empty); }); + } + } + + #endregion } diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 7fb9b895a..920abc284 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -67,6 +67,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings metadata.Disabled = settings.Disabled; metadata.Priority = settings.Priority; metadata.SearchDelayTime = settings.SearchDelayTime; + metadata.HomeDisabled = settings.HomeDisabled; } else { @@ -79,6 +80,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values ActionKeywords = metadata.ActionKeywords, // use default value Disabled = metadata.Disabled, + HomeDisabled = metadata.HomeDisabled, Priority = metadata.Priority, DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values SearchDelayTime = metadata.SearchDelayTime, // use default value @@ -128,5 +130,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// Used only to save the state of the plugin in settings /// public bool Disabled { get; set; } + public bool HomeDisabled { get; set; } } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 243c549c5..85fb0dd03 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Drawing; using System.Text.Json.Serialization; using System.Windows; +using System.Windows.Media; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Logger; @@ -33,8 +33,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings _storage.Save(); } - private string language = Constant.SystemLanguageCode; - private string _theme = Constant.DefaultTheme; public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}"; public string OpenResultModifiers { get; set; } = KeyConstant.Alt; public string ColorScheme { get; set; } = "System"; @@ -51,18 +49,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings public string SelectPrevPageHotkey { get; set; } = $"PageDown"; public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O"; public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; + public string OpenHistoryHotkey { get; set; } = $"Ctrl+H"; public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; + private string _language = Constant.SystemLanguageCode; public string Language { - get => language; + get => _language; set { - language = value; + _language = value; OnPropertyChanged(); } } + private string _theme = Constant.DefaultTheme; public string Theme { get => _theme; @@ -78,22 +79,23 @@ namespace Flow.Launcher.Infrastructure.UserSettings } public bool UseDropShadowEffect { get; set; } = true; public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None; + public string ReleaseNotesVersion { get; set; } = string.Empty; /* 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 QueryBoxFontSize { get; set; } = 16; public double ResultItemFontSize { get; set; } = 16; public double ResultSubItemFontSize { get; set; } = 13; - public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name; + public string QueryBoxFont { get; set; } = Win32Helper.GetSystemDefaultFont(); public string QueryBoxFontStyle { get; set; } public string QueryBoxFontWeight { get; set; } public string QueryBoxFontStretch { get; set; } - public string ResultFont { get; set; } = FontFamily.GenericSansSerif.Name; + public string ResultFont { get; set; } = Win32Helper.GetSystemDefaultFont(); 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 ResultSubFont { get; set; } = Win32Helper.GetSystemDefaultFont(); public string ResultSubFontStyle { get; set; } public string ResultSubFontWeight { get; set; } public string ResultSubFontStretch { get; set; } @@ -101,6 +103,24 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool UseAnimation { get; set; } = true; public bool UseSound { get; set; } = true; public double SoundVolume { get; set; } = 50; + public bool ShowBadges { get; set; } = false; + public bool ShowBadgesGlobalOnly { get; set; } = false; + + private string _settingWindowFont { get; set; } = Win32Helper.GetSystemDefaultFont(false); + public string SettingWindowFont + { + get => _settingWindowFont; + set + { + if (_settingWindowFont != value) + { + _settingWindowFont = value; + OnPropertyChanged(); + Application.Current.Resources["SettingWindowFont"] = new FontFamily(value); + Application.Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(value); + } + } + } public bool UseClock { get; set; } = true; public bool UseDate { get; set; } = false; @@ -114,7 +134,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public double? SettingWindowLeft { get; set; } = null; public WindowState SettingWindowState { get; set; } = WindowState.Normal; - bool _showPlaceholder { get; set; } = false; + private bool _showPlaceholder { get; set; } = true; public bool ShowPlaceholder { get => _showPlaceholder; @@ -127,7 +147,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } } - string _placeholderText { get; set; } = string.Empty; + private string _placeholderText { get; set; } = string.Empty; public string PlaceholderText { get => _placeholderText; @@ -141,6 +161,36 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } + private bool _showHomePage { get; set; } = true; + public bool ShowHomePage + { + get => _showHomePage; + set + { + if (_showHomePage != value) + { + _showHomePage = value; + OnPropertyChanged(); + } + } + } + + private bool _showHistoryResultsForHomePage = false; + public bool ShowHistoryResultsForHomePage + { + get => _showHistoryResultsForHomePage; + set + { + if (_showHistoryResultsForHomePage != value) + { + _showHistoryResultsForHomePage = value; + OnPropertyChanged(); + } + } + } + + public int MaxHistoryResultsToShowForHomePage { get; set; } = 5; + public int CustomExplorerIndex { get; set; } = 0; [JsonIgnore] @@ -178,8 +228,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings new() { Name = "Files", - Path = "Files", - DirectoryArgument = "-select \"%d\"", + Path = "Files-Stable", + DirectoryArgument = "\"%d\"", FileArgument = "-select \"%f\"" } }; @@ -260,6 +310,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings public double WindowLeft { get; set; } public double WindowTop { get; set; } + public double PreviousScreenWidth { get; set; } + public double PreviousScreenHeight { get; set; } + public double PreviousDpiX { get; set; } + public double PreviousDpiY { get; set; } /// /// Custom left position on selected monitor @@ -297,9 +351,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings public ObservableCollection CustomShortcuts { get; set; } = new ObservableCollection(); [JsonIgnore] - public ObservableCollection BuiltinShortcuts { get; set; } = new() + public ObservableCollection BuiltinShortcuts { get; set; } = new() { - new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), + new AsyncBuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText)), new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) }; @@ -309,7 +363,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool StartFlowLauncherOnSystemStartup { get; set; } = false; public bool UseLogonTaskForStartup { get; set; } = false; public bool HideOnStartup { get; set; } = true; - bool _hideNotifyIcon { get; set; } + private bool _hideNotifyIcon; public bool HideNotifyIcon { get => _hideNotifyIcon; @@ -358,29 +412,31 @@ namespace Flow.Launcher.Infrastructure.UserSettings var list = FixedHotkeys(); // Customizeable hotkeys - if(!string.IsNullOrEmpty(Hotkey)) + if (!string.IsNullOrEmpty(Hotkey)) list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = "")); - if(!string.IsNullOrEmpty(PreviewHotkey)) + if (!string.IsNullOrEmpty(PreviewHotkey)) list.Add(new(PreviewHotkey, "previewHotkey", () => PreviewHotkey = "")); - if(!string.IsNullOrEmpty(AutoCompleteHotkey)) + if (!string.IsNullOrEmpty(AutoCompleteHotkey)) list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = "")); - if(!string.IsNullOrEmpty(AutoCompleteHotkey2)) + if (!string.IsNullOrEmpty(AutoCompleteHotkey2)) list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = "")); - if(!string.IsNullOrEmpty(SelectNextItemHotkey)) + if (!string.IsNullOrEmpty(SelectNextItemHotkey)) list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = "")); - if(!string.IsNullOrEmpty(SelectNextItemHotkey2)) + if (!string.IsNullOrEmpty(SelectNextItemHotkey2)) list.Add(new(SelectNextItemHotkey2, "SelectNextItemHotkey", () => SelectNextItemHotkey2 = "")); - if(!string.IsNullOrEmpty(SelectPrevItemHotkey)) + if (!string.IsNullOrEmpty(SelectPrevItemHotkey)) list.Add(new(SelectPrevItemHotkey, "SelectPrevItemHotkey", () => SelectPrevItemHotkey = "")); - if(!string.IsNullOrEmpty(SelectPrevItemHotkey2)) + if (!string.IsNullOrEmpty(SelectPrevItemHotkey2)) list.Add(new(SelectPrevItemHotkey2, "SelectPrevItemHotkey", () => SelectPrevItemHotkey2 = "")); - if(!string.IsNullOrEmpty(SettingWindowHotkey)) + if (!string.IsNullOrEmpty(SettingWindowHotkey)) list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = "")); - if(!string.IsNullOrEmpty(OpenContextMenuHotkey)) + if (!string.IsNullOrEmpty(OpenHistoryHotkey)) + list.Add(new(OpenHistoryHotkey, "OpenHistoryHotkey", () => OpenHistoryHotkey = "")); + if (!string.IsNullOrEmpty(OpenContextMenuHotkey)) list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = "")); - if(!string.IsNullOrEmpty(SelectNextPageHotkey)) + if (!string.IsNullOrEmpty(SelectNextPageHotkey)) list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = "")); - if(!string.IsNullOrEmpty(SelectPrevPageHotkey)) + if (!string.IsNullOrEmpty(SelectPrevPageHotkey)) list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = "")); if (!string.IsNullOrEmpty(CycleHistoryUpHotkey)) list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = "")); @@ -411,7 +467,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings 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"), diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index f9c548de8..86e7b7c97 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1,9 +1,16 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Globalization; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Interop; +using System.Windows.Markup; using System.Windows.Media; using Flow.Launcher.Infrastructure.UserSettings; using Microsoft.Win32; @@ -11,8 +18,10 @@ using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dwm; using Windows.Win32.UI.Input.KeyboardAndMouse; +using Windows.Win32.UI.Shell.Common; using Windows.Win32.UI.WindowsAndMessaging; using Point = System.Windows.Point; +using SystemFonts = System.Windows.SystemFonts; namespace Flow.Launcher.Infrastructure { @@ -185,9 +194,9 @@ namespace Flow.Launcher.Infrastructure SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style); } - private static int GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex) + private static nint GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex) { - var style = PInvoke.GetWindowLong(hWnd, nIndex); + var style = PInvoke.GetWindowLongPtr(hWnd, nIndex); if (style == 0 && Marshal.GetLastPInvokeError() != 0) { throw new Win32Exception(Marshal.GetLastPInvokeError()); @@ -195,7 +204,7 @@ namespace Flow.Launcher.Infrastructure return style; } - private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong) + private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong) { PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error @@ -315,6 +324,11 @@ namespace Flow.Launcher.Infrastructure public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE; public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE; + public const int WM_NCLBUTTONDBLCLK = (int)PInvoke.WM_NCLBUTTONDBLCLK; + public const int WM_SYSCOMMAND = (int)PInvoke.WM_SYSCOMMAND; + + public const int SC_MAXIMIZE = (int)PInvoke.SC_MAXIMIZE; + public const int SC_MINIMIZE = (int)PInvoke.SC_MINIMIZE; #endregion @@ -332,6 +346,78 @@ namespace Flow.Launcher.Infrastructure #endregion + #region STA Thread + + /* + Inspired by https://github.com/files-community/Files code on STA Thread handling. + */ + + public static Task StartSTATaskAsync(Action action) + { + var taskCompletionSource = new TaskCompletionSource(); + Thread thread = new(() => + { + PInvoke.OleInitialize(); + + try + { + action(); + taskCompletionSource.SetResult(); + } + catch (System.Exception ex) + { + taskCompletionSource.SetException(ex); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return taskCompletionSource.Task; + } + + public static Task StartSTATaskAsync(Func func) + { + var taskCompletionSource = new TaskCompletionSource(); + + Thread thread = new(() => + { + PInvoke.OleInitialize(); + + try + { + taskCompletionSource.SetResult(func()); + } + catch (System.Exception ex) + { + taskCompletionSource.SetException(ex); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return taskCompletionSource.Task; + } + + #endregion + #region Keyboard Layout private const string UserProfileRegistryPath = @"Control Panel\International\User Profile"; @@ -364,20 +450,10 @@ namespace Flow.Launcher.Infrastructure // No installed English layout found if (enHKL == HKL.Null) return; - // When application is exiting, the Application.Current will be null - if (Application.Current == null) return; - - // Get the FL main window - var hwnd = GetWindowHandle(Application.Current.MainWindow, true); + // Get the foreground window + var hwnd = PInvoke.GetForegroundWindow(); if (hwnd == HWND.Null) return; - // Check if the FL main window is the current foreground window - if (!IsForegroundWindow(hwnd)) - { - var result = PInvoke.SetForegroundWindow(hwnd); - if (!result) throw new Win32Exception(Marshal.GetLastWin32Error()); - } - // Get the current foreground window thread ID var threadId = PInvoke.GetWindowThreadProcessId(hwnd); if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); @@ -517,5 +593,203 @@ namespace Flow.Launcher.Infrastructure } #endregion + + #region Korean IME + + public static bool IsWindows11() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 22000; + } + + public static bool IsKoreanIMEExist() + { + return GetLegacyKoreanIMERegistryValue() != null; + } + + public static bool IsLegacyKoreanIMEEnabled() + { + object value = GetLegacyKoreanIMERegistryValue(); + + if (value is int intValue) + { + return intValue == 1; + } + else if (value != null && int.TryParse(value.ToString(), out int parsedValue)) + { + return parsedValue == 1; + } + + return false; + } + + public static bool SetLegacyKoreanIMEEnabled(bool enable) + { + const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}"; + const string valueName = "NoTsf3Override5"; + + try + { + using RegistryKey key = Registry.CurrentUser.CreateSubKey(subKeyPath); + if (key != null) + { + int value = enable ? 1 : 0; + key.SetValue(valueName, value, RegistryValueKind.DWord); + return true; + } + } + catch (System.Exception) + { + // Ignored + } + + return false; + } + + public static object GetLegacyKoreanIMERegistryValue() + { + const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}"; + const string valueName = "NoTsf3Override5"; + + try + { + using RegistryKey key = Registry.CurrentUser.OpenSubKey(subKeyPath); + if (key != null) + { + return key.GetValue(valueName); + } + } + catch (System.Exception) + { + // Ignored + } + + return null; + } + + public static void OpenImeSettings() + { + try + { + Process.Start(new ProcessStartInfo("ms-settings:regionlanguage") { UseShellExecute = true }); + } + catch (System.Exception) + { + // Ignored + } + } + + #endregion + + #region System Font + + private static readonly Dictionary _languageToNotoSans = new() + { + { "ko", "Noto Sans KR" }, + { "ja", "Noto Sans JP" }, + { "zh-CN", "Noto Sans SC" }, + { "zh-SG", "Noto Sans SC" }, + { "zh-Hans", "Noto Sans SC" }, + { "zh-TW", "Noto Sans TC" }, + { "zh-HK", "Noto Sans TC" }, + { "zh-MO", "Noto Sans TC" }, + { "zh-Hant", "Noto Sans TC" }, + { "th", "Noto Sans Thai" }, + { "ar", "Noto Sans Arabic" }, + { "he", "Noto Sans Hebrew" }, + { "hi", "Noto Sans Devanagari" }, + { "bn", "Noto Sans Bengali" }, + { "ta", "Noto Sans Tamil" }, + { "el", "Noto Sans Greek" }, + { "ru", "Noto Sans" }, + { "en", "Noto Sans" }, + { "fr", "Noto Sans" }, + { "de", "Noto Sans" }, + { "es", "Noto Sans" }, + { "pt", "Noto Sans" } + }; + + /// + /// Gets the system default font. + /// + /// + /// If true, it will try to find the Noto font for the current culture. + /// + /// + /// The name of the system default font. + /// + public static string GetSystemDefaultFont(bool useNoto = true) + { + try + { + if (useNoto) + { + var culture = CultureInfo.CurrentCulture; + var language = culture.Name; // e.g., "zh-TW" + var langPrefix = language.Split('-')[0]; // e.g., "zh" + + // First, try to find by full name, and if not found, fallback to prefix + if (TryGetNotoFont(language, out var notoFont) || TryGetNotoFont(langPrefix, out notoFont)) + { + // If the font is installed, return it + if (Fonts.SystemFontFamilies.Any(f => f.Source.Equals(notoFont))) + { + return notoFont; + } + } + } + + // If Noto font is not found, fallback to the system default font + var font = SystemFonts.MessageFontFamily; + if (font.FamilyNames.TryGetValue(XmlLanguage.GetLanguage("en-US"), out var englishName)) + { + return englishName; + } + + return font.Source ?? "Segoe UI"; + } + catch + { + return "Segoe UI"; + } + } + + private static bool TryGetNotoFont(string langKey, out string notoFont) + { + return _languageToNotoSans.TryGetValue(langKey, out notoFont); + } + + #endregion + + #region Explorer + + // https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems + + public static unsafe void OpenFolderAndSelectFile(string filePath) + { + ITEMIDLIST* pidlFolder = null; + ITEMIDLIST* pidlFile = null; + + var folderPath = Path.GetDirectoryName(filePath); + + try + { + var hrFolder = PInvoke.SHParseDisplayName(folderPath, null, out pidlFolder, 0, null); + if (hrFolder.Failed) throw new COMException("Failed to parse folder path", hrFolder); + + var hrFile = PInvoke.SHParseDisplayName(filePath, null, out pidlFile, 0, null); + if (hrFile.Failed) throw new COMException("Failed to parse file path", hrFile); + + var hrSelect = PInvoke.SHOpenFolderAndSelectItems(pidlFolder, 1, &pidlFile, 0); + if (hrSelect.Failed) throw new COMException("Failed to open folder and select item", hrSelect); + } + finally + { + if (pidlFile != null) PInvoke.CoTaskMemFree(pidlFile); + if (pidlFolder != null) PInvoke.CoTaskMemFree(pidlFolder); + } + } + + #endregion } } diff --git a/Flow.Launcher.Plugin/AllowedLanguage.cs b/Flow.Launcher.Plugin/AllowedLanguage.cs index 619a94deb..0d22756a7 100644 --- a/Flow.Launcher.Plugin/AllowedLanguage.cs +++ b/Flow.Launcher.Plugin/AllowedLanguage.cs @@ -65,7 +65,42 @@ 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); + } + + /// + /// Determines if this language is a Python language + /// + /// + /// + public static bool IsPython(string language) + { + return language.Equals(Python, StringComparison.OrdinalIgnoreCase) + || language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines if this language is a Node.js language + /// + /// + /// + public static bool IsNodeJs(string language) + { + return language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(TypeScriptV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScriptV2, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines if this language is a executable language + /// + /// + /// + public static bool IsExecutable(string language) + { + return language.Equals(Executable, StringComparison.OrdinalIgnoreCase) + || language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase); } /// @@ -76,15 +111,9 @@ namespace Flow.Launcher.Plugin public static bool IsAllowed(string language) { return IsDotNet(language) - || 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); - ; + || IsPython(language) + || IsNodeJs(language) + || IsExecutable(language); } } } diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 1472813b8..4a26cec95 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -14,10 +14,10 @@ - 4.4.0 - 4.4.0 - 4.4.0 - 4.4.0 + 4.5.0 + 4.5.0 + 4.5.0 + 4.5.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -76,7 +76,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs new file mode 100644 index 000000000..78d6454ae --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// Asynchronous Query Model for Flow Launcher When Query Text is Empty + /// + public interface IAsyncHomeQuery : IFeatures + { + /// + /// Asynchronous Querying When Query Text is Empty + /// + /// + /// If the Querying method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncHomeQuery interface + /// + /// Cancel when querying job is obsolete + /// + Task> HomeQueryAsync(CancellationToken token); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs b/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs new file mode 100644 index 000000000..81186fca2 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// Synchronous Query Model for Flow Launcher When Query Text is Empty + /// + /// If the Querying method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncHomeQuery interface + /// + /// + public interface IHomeQuery : IAsyncHomeQuery + { + /// + /// Querying When Query Text is Empty + /// + /// This method will be called within a Task.Run, + /// so please avoid synchronously wait for long. + /// + /// + /// + List HomeQuery(); + + Task> IAsyncHomeQuery.HomeQueryAsync(CancellationToken token) => Task.Run(HomeQuery); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index a73ead814..76c7a4911 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -84,10 +84,24 @@ namespace Flow.Launcher.Plugin /// Optional message subtitle void ShowMsgError(string title, string subTitle = ""); + /// + /// Show the error message using Flow's standard error icon. + /// + /// Message title + /// Message button content + /// Message button action + /// Optional message subtitle + void ShowMsgErrorWithButton(string title, string buttonText, Action buttonAction, string subTitle = ""); + /// /// Show the MainWindow when hiding /// void ShowMainWindow(); + + /// + /// Focus the query text box in the main window + /// + void FocusQueryTextBox(); /// /// Hide MainWindow @@ -122,6 +136,27 @@ namespace Flow.Launcher.Plugin /// when true will use main windows as the owner void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true); + /// + /// Show message box with button + /// + /// Message title + /// Message button content + /// Message button action + /// Message subtitle + /// Message icon path (relative path to your plugin folder) + void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle = "", string iconPath = ""); + + /// + /// Show message box with button + /// + /// Message title + /// Message button content + /// Message button action + /// Message subtitle + /// Message icon path (relative path to your plugin folder) + /// when true will use main windows as the owner + void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath, bool useMainWindowAsOwner = true); + /// /// Open setting dialog /// @@ -223,11 +258,15 @@ namespace Flow.Launcher.Plugin Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default); /// - /// Add ActionKeyword and update action keyword metadata for specific plugin + /// Add ActionKeyword and update action keyword metadata for specific plugin. /// Before adding, please check if action keyword is already assigned by /// /// ID for plugin that needs to add action keyword /// The actionkeyword that is supposed to be added + /// + /// If new action keyword contains any whitespace, FL will still add it but it will not work for users. + /// So plugin should check the whitespace before calling this function. + /// void AddActionKeyword(string pluginId, string newActionKeyword); /// @@ -280,9 +319,10 @@ namespace Flow.Launcher.Plugin T LoadSettingJsonStorage() where T : new(); /// - /// Save JsonStorage for current plugin's setting. This is the method used to save settings to json in Flow.Launcher + /// Save JsonStorage for current plugin's setting. This is the method used to save settings to json in Flow. /// This method will save the original instance loaded with LoadJsonStorage. - /// This API call is for manually Save. Flow will automatically save all setting type that has called LoadSettingJsonStorage or SaveSettingJsonStorage previously. + /// This API call is for manually Save. + /// Flow will automatically save all setting type that has called or previously. /// /// Type for Serialization /// @@ -424,9 +464,10 @@ namespace Flow.Launcher.Plugin Task LoadCacheBinaryStorageAsync(string cacheName, string cacheDirectory, T defaultData) where T : new(); /// - /// Save BinaryStorage for current plugin's cache. This is the method used to save cache to binary in Flow.Launcher + /// Save BinaryStorage for current plugin's cache. This is the method used to save cache to binary in Flow. /// This method will save the original instance loaded with LoadCacheBinaryStorageAsync. - /// This API call is for manually Save. Flow will automatically save all cache type that has called LoadCacheBinaryStorageAsync or SaveCacheBinaryStorageAsync previously. + /// This API call is for manually Save. + /// Flow will automatically save all cache type that has called or previously. /// /// Type for Serialization /// Cache file name @@ -465,8 +506,11 @@ namespace Flow.Launcher.Plugin public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default); /// - /// Get the plugin manifest + /// Get the plugin manifest. /// + /// + /// If Flow cannot get manifest data, this could be null + /// /// public IReadOnlyList GetPluginManifest(); diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index da10bc6a5..09803cbd7 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -50,6 +50,11 @@ namespace Flow.Launcher.Plugin /// public bool Disabled { get; set; } + /// + /// Whether plugin is disabled in home query. + /// + public bool HomeDisabled { get; set; } + /// /// Plugin execute file path. /// diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 913dc31ae..f50614699 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -8,7 +8,8 @@ namespace Flow.Launcher.Plugin public class Query { /// - /// Raw query, this includes action keyword if it has + /// Raw query, this includes action keyword if it has. + /// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace. /// We didn't recommend use this property directly. You should always use Search property. /// public string RawQuery { get; internal init; } @@ -20,6 +21,11 @@ namespace Flow.Launcher.Plugin /// public bool IsReQuery { get; internal set; } = false; + /// + /// Determines whether the query is a home query. + /// + public bool IsHomeQuery { get; internal init; } = false; + /// /// Search part of a query. /// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as RawQuery. @@ -63,10 +69,10 @@ namespace Flow.Launcher.Plugin /// [JsonIgnore] public string FirstSearch => SplitSearch(0); - + [JsonIgnore] private string _secondToEndSearch; - + /// /// strings from second search (including) to last search /// diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 910485438..f0fcd48ff 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -12,12 +12,19 @@ namespace Flow.Launcher.Plugin /// public class Result { + /// + /// 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; + private string _pluginDirectory; private string _icoPath; private string _copyText = string.Empty; + private string _badgeIcoPath; + /// /// The title of the result. This is always required. /// @@ -60,7 +67,7 @@ namespace Flow.Launcher.Plugin /// GlyphInfo is prioritized if not null public string IcoPath { - get { return _icoPath; } + get => _icoPath; set { // As a standard this property will handle prepping and converting to absolute local path for icon image processing @@ -80,6 +87,33 @@ namespace Flow.Launcher.Plugin } } + /// + /// The image to be displayed for the badge of the result. + /// + /// Can be a local file path or a URL. + /// If null or empty, will use plugin icon + public string BadgeIcoPath + { + get => _badgeIcoPath; + set + { + // As a standard this property will handle prepping and converting to absolute local path for icon image processing + if (!string.IsNullOrEmpty(value) + && !string.IsNullOrEmpty(PluginDirectory) + && !Path.IsPathRooted(value) + && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) + { + _badgeIcoPath = Path.Combine(PluginDirectory, value); + } + else + { + _badgeIcoPath = value; + } + } + } + /// /// Determines if Icon has a border radius /// @@ -94,14 +128,18 @@ namespace Flow.Launcher.Plugin /// /// Delegate to load an icon for this result. /// - public IconDelegate Icon; + public IconDelegate Icon = null; + + /// + /// Delegate to load an icon for the badge of this result. + /// + public IconDelegate BadgeIcon = null; /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// public GlyphInfo Glyph { get; init; } - /// /// An action to take in the form of a function call when the result has been selected. /// @@ -143,59 +181,19 @@ namespace Flow.Launcher.Plugin /// public string PluginDirectory { - get { return _pluginDirectory; } + get => _pluginDirectory; set { _pluginDirectory = value; // When the Result object is returned from the query call, PluginDirectory is not provided until // UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available - // we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded. + // we need to update (only if not Uri path) the IcoPath and BadgeIcoPath with the full absolute path so the image can be loaded. IcoPath = _icoPath; + BadgeIcoPath = _badgeIcoPath; } } - /// - 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 /// @@ -224,16 +222,6 @@ namespace Flow.Launcher.Plugin /// public Lazy PreviewPanel { get; set; } - /// - /// Run this result, asynchronously - /// - /// - /// - public ValueTask ExecuteAsync(ActionContext context) - { - return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false); - } - /// /// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result /// @@ -255,11 +243,6 @@ namespace Flow.Launcher.Plugin /// 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. @@ -268,6 +251,66 @@ namespace Flow.Launcher.Plugin /// public string RecordKey { get; set; } = null; + /// + /// Determines if the badge icon should be shown. + /// If users want to show the result badges and here you set this to true, the results will show the badge icon. + /// + public bool ShowBadge { get; set; } = false; + + /// + /// Run this result, asynchronously + /// + /// + /// + public ValueTask ExecuteAsync(ActionContext context) + { + return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false); + } + + /// + 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, + BadgeIcoPath = BadgeIcoPath, + RoundedIcon = RoundedIcon, + Icon = Icon, + BadgeIcon = BadgeIcon, + 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, + ShowBadge = ShowBadge, + }; + } + /// /// Info of the preview section of a /// diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 1de5841a5..6c506cfc0 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -264,12 +264,12 @@ namespace Flow.Launcher.Plugin.SharedCommands var index = path.LastIndexOf('\\'); if (index > 0 && index < (path.Length - 1)) { - string previousDirectoryPath = path.Substring(0, index + 1); - return locationExists(previousDirectoryPath) ? previousDirectoryPath : ""; + string previousDirectoryPath = path[..(index + 1)]; + return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty; } else { - return ""; + return string.Empty; } } @@ -285,7 +285,7 @@ namespace Flow.Launcher.Plugin.SharedCommands // not full path, get previous level directory string var indexOfSeparator = path.LastIndexOf('\\'); - return path.Substring(0, indexOfSeparator + 1); + return path[..(indexOfSeparator + 1)]; } return path; diff --git a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs index 752c85933..ed3e91daf 100644 --- a/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs +++ b/Flow.Launcher.Plugin/SharedCommands/SearchWeb.cs @@ -1,8 +1,9 @@ -using Microsoft.Win32; -using System; +using System; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; +using Microsoft.Win32; namespace Flow.Launcher.Plugin.SharedCommands { @@ -13,7 +14,7 @@ namespace Flow.Launcher.Plugin.SharedCommands { private static string GetDefaultBrowserPath() { - string name = string.Empty; + var name = string.Empty; try { using var regDefault = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice", false); @@ -23,8 +24,7 @@ namespace Flow.Launcher.Plugin.SharedCommands name = regKey.GetValue(null).ToString().ToLower().Replace("\"", ""); if (!name.EndsWith("exe")) - name = name.Substring(0, name.LastIndexOf(".exe") + 4); - + name = name[..(name.LastIndexOf(".exe") + 4)]; } catch { @@ -65,12 +65,21 @@ namespace Flow.Launcher.Plugin.SharedCommands { Process.Start(psi)?.Dispose(); } - catch (System.ComponentModel.Win32Exception) + // This error may be thrown if browser path is incorrect + catch (Win32Exception) { - Process.Start(new ProcessStartInfo + try { - FileName = url, UseShellExecute = true - }); + Process.Start(new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + } + catch + { + throw; // Re-throw the exception if we cannot open the URL in the default browser + } } } @@ -100,12 +109,20 @@ namespace Flow.Launcher.Plugin.SharedCommands Process.Start(psi)?.Dispose(); } // This error may be thrown if browser path is incorrect - catch (System.ComponentModel.Win32Exception) + catch (Win32Exception) { - Process.Start(new ProcessStartInfo + try { - FileName = url, UseShellExecute = true - }); + Process.Start(new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + } + catch + { + throw; // Re-throw the exception if we cannot open the URL in the default browser + } } } } diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 420da266d..9ec952155 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -39,8 +39,8 @@ namespace Flow.Launcher.Test.Plugins } [SupportedOSPlatform("windows7.0")] - [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")] - [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")] + [TestCase("C:\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY {QueryConstructor.OrderIdentifier}")] + [TestCase("C:\\SomeFolder\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY {QueryConstructor.OrderIdentifier}")] public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString) { // Given @@ -59,7 +59,7 @@ namespace Flow.Launcher.Test.Plugins [TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" + " FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" + " AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" + - " ORDER BY System.FileName")] + $" ORDER BY {QueryConstructor.OrderIdentifier}")] public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString( string folderPath, string userSearchString, string expectedString) { @@ -87,8 +87,8 @@ namespace Flow.Launcher.Test.Plugins [SupportedOSPlatform("windows7.0")] [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " + "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + - "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] - [TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")] + $"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")] + [TestCase("", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -107,7 +107,6 @@ namespace Flow.Launcher.Test.Plugins ClassicAssert.AreEqual(expectedString, resultString); } - [SupportedOSPlatform("windows7.0")] [TestCase(@"some words", @"FREETEXT('some words')")] public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString( @@ -127,7 +126,7 @@ namespace Flow.Launcher.Test.Plugins [SupportedOSPlatform("windows7.0")] [TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + - "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] + $"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")] public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { diff --git a/Flow.Launcher/ActionKeywords.xaml b/Flow.Launcher/ActionKeywords.xaml index 740b0d402..887b13126 100644 --- a/Flow.Launcher/ActionKeywords.xaml +++ b/Flow.Launcher/ActionKeywords.xaml @@ -53,11 +53,11 @@ - - + + - + - + @@ -112,20 +112,20 @@ Grid.Row="1" Background="{DynamicResource PopupButtonAreaBGColor}" BorderBrush="{DynamicResource PopupButtonAreaBorderColor}" - BorderThickness="0,1,0,0"> + BorderThickness="0 1 0 0"> - - + + - + @@ -124,14 +124,14 @@ LastChildFill="True"> + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs index 3bd24bc13..c0a77957a 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs @@ -5,21 +5,28 @@ using System.Windows.Navigation; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.SettingPages.ViewModels; using Flow.Launcher.ViewModel; -using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher.SettingPages.Views; public partial class SettingsPanePluginStore { private SettingsPanePluginStoreViewModel _viewModel = null!; + private readonly SettingWindowViewModel _settingViewModel = Ioc.Default.GetRequiredService(); protected override void OnNavigatedTo(NavigationEventArgs e) { + // Sometimes the navigation is not triggered by button click, + // so we need to reset the page type + _settingViewModel.PageType = typeof(SettingsPanePluginStore); + + // If the navigation is not triggered by button click, view model will be null again + if (_viewModel == null) + { + _viewModel = Ioc.Default.GetRequiredService(); + DataContext = _viewModel; + } if (!IsInitialized) { - var settings = Ioc.Default.GetRequiredService(); - _viewModel = new SettingsPanePluginStoreViewModel(); - DataContext = _viewModel; InitializeComponent(); } _viewModel.PropertyChanged += ViewModel_PropertyChanged; @@ -28,9 +35,15 @@ public partial class SettingsPanePluginStore private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(SettingsPanePluginStoreViewModel.FilterText)) + switch (e.PropertyName) { - ((CollectionViewSource)FindResource("PluginStoreCollectionView")).View.Refresh(); + case nameof(SettingsPanePluginStoreViewModel.FilterText): + case nameof(SettingsPanePluginStoreViewModel.ShowDotNet): + case nameof(SettingsPanePluginStoreViewModel.ShowPython): + case nameof(SettingsPanePluginStoreViewModel.ShowNodeJs): + case nameof(SettingsPanePluginStoreViewModel.ShowExecutable): + ((CollectionViewSource)FindResource("PluginStoreCollectionView")).View.Refresh(); + break; } } diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml index f9f708314..52d77f914 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml @@ -15,6 +15,12 @@ FocusManager.FocusedElement="{Binding ElementName=PluginFilterTextbox}" KeyDown="SettingsPanePlugins_OnKeyDown" mc:Ignorable="d"> + + + @@ -57,9 +63,10 @@ Height="34" Margin="0 0 20 0" Command="{Binding OpenHelperCommand}" - Content="" - FontFamily="{DynamicResource SymbolThemeFontFamily}" - FontSize="14" /> + CommandParameter="{Binding RelativeSource={RelativeSource Self}}" + FontSize="14"> + + (); protected override void OnNavigatedTo(NavigationEventArgs e) { + // Sometimes the navigation is not triggered by button click, + // so we need to reset the page type + _settingViewModel.PageType = typeof(SettingsPanePlugins); + + // If the navigation is not triggered by button click, view model will be null again + if (_viewModel == null) + { + _viewModel = Ioc.Default.GetRequiredService(); + DataContext = _viewModel; + } if (!IsInitialized) { - var settings = Ioc.Default.GetRequiredService(); - _viewModel = new SettingsPanePluginsViewModel(settings); - DataContext = _viewModel; InitializeComponent(); } + _viewModel.PropertyChanged += ViewModel_PropertyChanged; base.OnNavigatedTo(e); } + private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SettingsPanePluginsViewModel.FilterText)) + { + ((CollectionViewSource)FindResource("PluginCollectionView")).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; PluginFilterTextbox.Focus(); } + + private void PluginCollectionView_OnFilter(object sender, FilterEventArgs e) + { + if (e.Item is not PluginViewModel plugin) + { + e.Accepted = false; + return; + } + + e.Accepted = _viewModel.SatisfiesFilter(plugin); + } } diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml index 768abbf97..f429a6e29 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml @@ -2,11 +2,11 @@ x:Class="Flow.Launcher.SettingPages.Views.SettingsPaneProxy" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:ui="http://schemas.modernwpf.com/2019" - xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls" xmlns:viewModels="clr-namespace:Flow.Launcher.SettingPages.ViewModels" Title="Proxy" d:DataContext="{d:DesignInstance viewModels:SettingsPaneProxyViewModel}" @@ -18,8 +18,8 @@ @@ -71,9 +71,9 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/QuickAccessLinkSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Views/QuickAccessLinkSettings.xaml.cs new file mode 100644 index 000000000..36a00e9e5 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/QuickAccessLinkSettings.xaml.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Forms; +using Flow.Launcher.Plugin.Explorer.Helper; +using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; + +namespace Flow.Launcher.Plugin.Explorer.Views; + +public partial class QuickAccessLinkSettings : INotifyPropertyChanged +{ + private string _selectedPath; + public string SelectedPath + { + get => _selectedPath; + set + { + if (_selectedPath != value) + { + _selectedPath = value; + OnPropertyChanged(); + if (string.IsNullOrEmpty(_selectedName)) + { + SelectedName = _selectedPath.GetPathName(); + } + } + } + } + + private string _selectedName; + public string SelectedName + { + get + { + return string.IsNullOrEmpty(_selectedName) ? _selectedPath.GetPathName() : _selectedName; + } + set + { + if (_selectedName != value) + { + _selectedName = value; + OnPropertyChanged(); + } + } + } + + private bool IsEdit { get; } + private AccessLink SelectedAccessLink { get; } + + public ObservableCollection QuickAccessLinks { get; } + + public QuickAccessLinkSettings(ObservableCollection quickAccessLinks) + { + IsEdit = false; + QuickAccessLinks = quickAccessLinks; + InitializeComponent(); + } + + public QuickAccessLinkSettings(ObservableCollection quickAccessLinks, AccessLink selectedAccessLink) + { + IsEdit = true; + _selectedName = selectedAccessLink.Name; + _selectedPath = selectedAccessLink.Path; + SelectedAccessLink = selectedAccessLink; + QuickAccessLinks = quickAccessLinks; + InitializeComponent(); + } + + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + { + DialogResult = false; + Close(); + } + + private void OnDoneButtonClick(object sender, RoutedEventArgs e) + { + // Validate the input before proceeding + if (string.IsNullOrEmpty(SelectedName) || string.IsNullOrEmpty(SelectedPath)) + { + var warning = Main.Context.API.GetTranslation("plugin_explorer_quick_access_link_no_folder_selected"); + Main.Context.API.ShowMsgBox(warning); + return; + } + + // Check if the path already exists in the quick access links + if (QuickAccessLinks.Any(x => + x.Path.Equals(SelectedPath, StringComparison.OrdinalIgnoreCase) && + x.Name.Equals(SelectedName, StringComparison.OrdinalIgnoreCase))) + { + var warning = Main.Context.API.GetTranslation("plugin_explorer_quick_access_link_path_already_exists"); + Main.Context.API.ShowMsgBox(warning); + return; + } + + // If editing, update the existing link + if (IsEdit) + { + if (SelectedAccessLink == null) return; + + var index = QuickAccessLinks.IndexOf(SelectedAccessLink); + if (index >= 0) + { + var updatedLink = new AccessLink + { + Name = SelectedName, + Type = SelectedAccessLink.Type, + Path = SelectedPath + }; + QuickAccessLinks[index] = updatedLink; + } + DialogResult = true; + Close(); + } + // Otherwise, add a new one + else + { + var newAccessLink = new AccessLink + { + Name = SelectedName, + Path = SelectedPath + }; + QuickAccessLinks.Add(newAccessLink); + DialogResult = true; + Close(); + } + } + + private void SelectPath_OnClick(object commandParameter, RoutedEventArgs e) + { + var folderBrowserDialog = new FolderBrowserDialog(); + + if (folderBrowserDialog.ShowDialog() != System.Windows.Forms.DialogResult.OK) + return; + + SelectedPath = folderBrowserDialog.SelectedPath; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json index d2440ab61..5eea2646e 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json @@ -11,7 +11,7 @@ "Name": "Explorer", "Description": "Find and manage files and folders via Windows Search or Everything", "Author": "Jeremy Wu", - "Version": "3.2.4", + "Version": "1.0.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll", diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Languages/ko.xaml b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Languages/ko.xaml index cc78c9a9a..7af9e9306 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Languages/ko.xaml +++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Languages/ko.xaml @@ -1,9 +1,9 @@  - Activate {0} plugin action keyword + {0} 플러그인의 액션 키워드 - 플러그인 인디케이터 + 플러그인 키워드 힌트 사용중인 플러그인들의 전체 액션 키워드 목록을 보여줍니다 diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Main.cs index 05e8d960f..48717816b 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Main.cs @@ -3,21 +3,33 @@ using System.Linq; namespace Flow.Launcher.Plugin.PluginIndicator { - public class Main : IPlugin, IPluginI18n + public class Main : IPlugin, IPluginI18n, IHomeQuery { internal PluginInitContext Context { get; private set; } public List Query(Query query) + { + return QueryResults(query); + } + + public List HomeQuery() + { + return QueryResults(); + } + + private List QueryResults(Query query = null) { var nonGlobalPlugins = GetNonGlobalPlugins(); + var querySearch = query?.Search ?? string.Empty; + var results = from keyword in nonGlobalPlugins.Keys let plugin = nonGlobalPlugins[keyword].Metadata - let keywordSearchResult = Context.API.FuzzySearch(query.Search, keyword) - let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : Context.API.FuzzySearch(query.Search, plugin.Name) + let keywordSearchResult = Context.API.FuzzySearch(querySearch, keyword) + let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : Context.API.FuzzySearch(querySearch, plugin.Name) let score = searchResult.Score where (searchResult.IsSearchPrecisionScoreMet() - || string.IsNullOrEmpty(query.Search)) // To list all available action keywords + || string.IsNullOrEmpty(querySearch)) // To list all available action keywords && !plugin.Disabled select new Result { diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json index 2b4870792..7e6a2e613 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json @@ -4,7 +4,7 @@ "Name": "Plugin Indicator", "Description": "Provides plugin action keyword suggestions", "Author": "qianlifeng", - "Version": "3.0.7", + "Version": "1.0.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginIndicator.dll", diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/zh-cn.xaml index 9542bd474..1a4199965 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/zh-cn.xaml +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/zh-cn.xaml @@ -13,8 +13,8 @@ 正在安装插件 下载与安装 {0} 插件卸载 - Keep plugin settings - Do you want to keep the settings of the plugin for the next usage? + 保留插件设置 + 你想要保留插件设置以便下一次的使用吗? 插件安装成功。正在重新启动 Flow Launcher,请稍候... 安装失败:无法从新插件中找到plugin.json元数据文件 错误:具有相同或更高版本的 {0} 的插件已经存在。 diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index a16778ff4..25182f6d3 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -316,61 +316,75 @@ namespace Flow.Launcher.Plugin.PluginsManager var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{x.Name}-{x.NewVersion}.zip"); - _ = Task.Run(async delegate + _ = Task.Run(async () => { - using var cts = new CancellationTokenSource(); + try + { + using var cts = new CancellationTokenSource(); - if (!x.PluginNewUserPlugin.IsFromLocalInstallPath) - { - await DownloadFileAsync( - $"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {x.PluginNewUserPlugin.Name}", - x.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts); - } - else - { - downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath; - } - - // check if user cancelled download before installing plugin - if (cts.IsCancellationRequested) - { - return; - } - else - { - await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin, - downloadToFilePath); - - if (Settings.AutoRestartAfterChanging) + if (!x.PluginNewUserPlugin.IsFromLocalInstallPath) { - Context.API.ShowMsg( - Context.API.GetTranslation("plugin_pluginsmanager_update_title"), - string.Format( - Context.API.GetTranslation( - "plugin_pluginsmanager_update_success_restart"), - x.Name)); - Context.API.RestartApp(); + await DownloadFileAsync( + $"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {x.PluginNewUserPlugin.Name}", + x.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts); } else { - Context.API.ShowMsg( - Context.API.GetTranslation("plugin_pluginsmanager_update_title"), - string.Format( - Context.API.GetTranslation( - "plugin_pluginsmanager_update_success_no_restart"), - x.Name)); + downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath; + } + + // check if user cancelled download before installing plugin + if (cts.IsCancellationRequested) + { + return; + } + else + { + await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin, + downloadToFilePath); + + if (Settings.AutoRestartAfterChanging) + { + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_update_title"), + string.Format( + Context.API.GetTranslation( + "plugin_pluginsmanager_update_success_restart"), + x.Name)); + Context.API.RestartApp(); + } + else + { + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_update_title"), + string.Format( + Context.API.GetTranslation( + "plugin_pluginsmanager_update_success_no_restart"), + x.Name)); + } } } - }).ContinueWith(t => - { - Context.API.LogException(ClassName, $"Update failed for {x.Name}", - t.Exception.InnerException); - Context.API.ShowMsg( - Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), - string.Format( - Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), - x.Name)); - }, token, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + catch (HttpRequestException e) + { + // show error message + Context.API.ShowMsgError( + string.Format(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), x.Name), + Context.API.GetTranslation("plugin_pluginsmanager_download_error")); + Context.API.LogException(ClassName, "An error occurred while downloading plugin", e); + return; + } + catch (Exception e) + { + // show error message + Context.API.LogException(ClassName, $"Update failed for {x.Name}", e); + Context.API.ShowMsgError( + Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), + string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), + x.Name)); + return; + } + }); return true; }, @@ -436,7 +450,7 @@ namespace Flow.Launcher.Plugin.PluginsManager catch (Exception ex) { Context.API.LogException(ClassName, $"Update failed for {plugin.Name}", ex.InnerException); - Context.API.ShowMsg( + Context.API.ShowMsgError( Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), string.Format( Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs index 811bec50c..f23ff71f0 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs @@ -10,6 +10,6 @@ public bool WarnFromUnknownSource { get; set; } = true; - public bool AutoRestartAfterChanging { get; set; } = true; + public bool AutoRestartAfterChanging { get; set; } = false; } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index df5a2c784..327011ac3 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -6,7 +6,7 @@ "Name": "Plugins Manager", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Author": "Jeremy Wu", - "Version": "3.2.4", + "Version": "1.0.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ar.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ar.xaml index 04619239d..006dd93d6 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ar.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ar.xaml @@ -8,6 +8,7 @@ قتل عمليات {0} قتل جميع الأمثلة + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/cs.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/cs.xaml index d621b63c4..d53616cc0 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/cs.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/cs.xaml @@ -8,6 +8,7 @@ ukončit {0} procesů ukončit všechny instance + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/da.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/da.xaml index 8563f49a0..0a7176d2c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/da.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/da.xaml @@ -8,6 +8,7 @@ kill {0} processes kill all instances + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/de.xaml index 766d170a4..d2492fab9 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/de.xaml @@ -8,6 +8,7 @@ {0} Prozesse beenden Alle Instanzen beenden - Put processes with visible windows on the top + Titel für Prozesse mit sichtbaren Fenstern zeigen + Prozesse mit sichtbaren Fenstern ganz oben setzen diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml index ea6e54fef..ddabd31f8 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml @@ -10,6 +10,7 @@ kill {0} processes kill all instances + Show title for processes with visible windows Put processes with visible windows on the top \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/es-419.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/es-419.xaml index 9b4a20a69..50799dca2 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/es-419.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/es-419.xaml @@ -8,6 +8,7 @@ terminar {0} procesos termina todas las instancias + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/es.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/es.xaml index ab788075e..6ab3f9797 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/es.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/es.xaml @@ -8,6 +8,7 @@ finalizar {0} procesos finalizar todas las instancias + Mostrar el título de los procesos con ventanas visibles Colocar procesos con ventanas visibles en la parte superior diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/fr.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/fr.xaml index 7064de1fe..ae3b6bab2 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/fr.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/fr.xaml @@ -8,6 +8,7 @@ Tuer {0} processus Tuer toutes les instances + Afficher le titre des processus avec des fenêtres visibles Placer les processus dont les fenêtres sont visibles en haut de la page diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/he.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/he.xaml index 8567b5412..15d14a42c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/he.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/he.xaml @@ -8,6 +8,7 @@ סגור {0} תהליכים סגור את כל המופעים + הצג כותרת עבור תהליכים בעלי חלונות גלויים הצב תהליכים עם חלונות גלויים בחלק העליון diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/it.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/it.xaml index 1333fe573..5bd4a9eac 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/it.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/it.xaml @@ -8,6 +8,7 @@ termina {0} processi termina tutte le istanze + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ja.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ja.xaml index 8563f49a0..0a7176d2c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ja.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ja.xaml @@ -8,6 +8,7 @@ kill {0} processes kill all instances + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ko.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ko.xaml index c3d1f6d08..09679a58b 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ko.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ko.xaml @@ -8,6 +8,7 @@ {0} 프로세스 종료 모든 인스턴스 종료 + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nb.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nb.xaml index 1210818bf..37413385d 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nb.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nb.xaml @@ -8,6 +8,7 @@ terminer {0} processes terminer alle forekomstene + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nl.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nl.xaml index cfcd71c37..ea9f9a591 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nl.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/nl.xaml @@ -8,6 +8,7 @@ kill {0} processes kill all instances + Show title for processes with visible windows Zet processen met zichtbare vensters bovenaan diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pl.xaml index 650f14657..7e59db5ec 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pl.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pl.xaml @@ -8,6 +8,7 @@ zamknij {0} procesów zamknij wszystkie instancje + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pt-br.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pt-br.xaml index 8563f49a0..0a7176d2c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pt-br.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pt-br.xaml @@ -8,6 +8,7 @@ kill {0} processes kill all instances + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pt-pt.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pt-pt.xaml index 944a883c9..b50a31744 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pt-pt.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/pt-pt.xaml @@ -8,6 +8,7 @@ terminar {0} processos terminar todas as instâncias + Mostrar título dos processos com janelas visíveis Colocar processos com janelas visíveis por cima diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ru.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ru.xaml index 0e2bef052..d030a778e 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ru.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/ru.xaml @@ -8,6 +8,7 @@ удалить {0} процессов удалить все экземпляры + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/sk.xaml index 2b06d3bbe..6c85b9476 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/sk.xaml @@ -8,6 +8,7 @@ ukončiť {0} procesov ukončiť všetky inštancie + Zobraziť nadpis pre procesy s viditeľnými oknami Zobraziť procesy s viditeľným oknom navrchu diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/sr.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/sr.xaml index 8563f49a0..0a7176d2c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/sr.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/sr.xaml @@ -8,6 +8,7 @@ kill {0} processes kill all instances + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/tr.xaml index 8563f49a0..0a7176d2c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/tr.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/tr.xaml @@ -8,6 +8,7 @@ kill {0} processes kill all instances + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/uk-UA.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/uk-UA.xaml index 02dab46ba..56004028b 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/uk-UA.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/uk-UA.xaml @@ -8,6 +8,7 @@ вбити {0} процесів вбити всі екземпляри + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/vi.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/vi.xaml index 89fd67635..5ce54a0dc 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/vi.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/vi.xaml @@ -8,6 +8,7 @@ Buộc tắt các tiến trình {0} Tắt tất cả phên bản + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/zh-cn.xaml index 15e717c79..b99cbea28 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/zh-cn.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/zh-cn.xaml @@ -8,6 +8,7 @@ 杀死 {0} 进程 杀死所有实例 - Put processes with visible windows on the top + 显示带有可见窗口的进程标题 + 在顶部放置带有可见窗口的进程 diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/zh-tw.xaml index 8563f49a0..0a7176d2c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/zh-tw.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/zh-tw.xaml @@ -8,6 +8,7 @@ kill {0} processes kill all instances + Show title for processes with visible windows Put processes with visible windows on the top diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index 4f5d1becd..8f5ba4bd2 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -81,7 +81,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller // Filter processes based on search term var searchTerm = query.Search; var processlist = new List(); - var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); + var processWindowTitle = + Settings.ShowWindowTitle || Settings.PutVisibleWindowProcessesTop ? + ProcessHelper.GetProcessesWithNonEmptyWindowTitle() : + new Dictionary(); if (string.IsNullOrWhiteSpace(searchTerm)) { foreach (var p in allPocessList) @@ -91,12 +94,22 @@ namespace Flow.Launcher.Plugin.ProcessKiller if (processWindowTitle.TryGetValue(p.Id, out var windowTitle)) { // Add score to prioritize processes with visible windows - // And use window title for those processes - processlist.Add(new ProcessResult(p, Settings.PutVisibleWindowProcessesTop ? 200 : 0, windowTitle, null, progressNameIdTitle)); + // Use window title for those processes if enabled + processlist.Add(new ProcessResult( + p, + Settings.PutVisibleWindowProcessesTop ? 200 : 0, + Settings.ShowWindowTitle ? windowTitle : progressNameIdTitle, + null, + progressNameIdTitle)); } else { - processlist.Add(new ProcessResult(p, 0, progressNameIdTitle, null, progressNameIdTitle)); + processlist.Add(new ProcessResult( + p, + 0, + progressNameIdTitle, + null, + progressNameIdTitle)); } } } @@ -115,13 +128,17 @@ namespace Flow.Launcher.Plugin.ProcessKiller if (score > 0) { // Add score to prioritize processes with visible windows - // And use window title for those processes + // Use window title for those processes if (Settings.PutVisibleWindowProcessesTop) { score += 200; } - processlist.Add(new ProcessResult(p, score, windowTitle, - score == windowTitleMatch.Score ? windowTitleMatch : null, progressNameIdTitle)); + processlist.Add(new ProcessResult( + p, + score, + Settings.ShowWindowTitle ? windowTitle : progressNameIdTitle, + score == windowTitleMatch.Score ? windowTitleMatch : null, + progressNameIdTitle)); } } else @@ -130,7 +147,12 @@ namespace Flow.Launcher.Plugin.ProcessKiller var score = processNameIdMatch.Score; if (score > 0) { - processlist.Add(new ProcessResult(p, score, progressNameIdTitle, processNameIdMatch, progressNameIdTitle)); + processlist.Add(new ProcessResult( + p, + score, + progressNameIdTitle, + processNameIdMatch, + progressNameIdTitle)); } } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 4c07341ec..386782905 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -1,9 +1,11 @@ -using Microsoft.Win32.SafeHandles; -using System; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Threading; @@ -12,6 +14,8 @@ namespace Flow.Launcher.Plugin.ProcessKiller { internal class ProcessHelper { + private static readonly string ClassName = nameof(ProcessHelper); + private readonly HashSet _systemProcessList = new() { "conhost", @@ -70,8 +74,21 @@ namespace Flow.Launcher.Plugin.ProcessKiller /// public static unsafe Dictionary GetProcessesWithNonEmptyWindowTitle() { - var processDict = new Dictionary(); + // Collect all window handles + var windowHandles = new List(); PInvoke.EnumWindows((hWnd, _) => + { + if (PInvoke.IsWindowVisible(hWnd)) + { + windowHandles.Add(hWnd); + } + return true; + }, IntPtr.Zero); + + // Concurrently process each window handle + var processDict = new ConcurrentDictionary(); + var processedProcessIds = new ConcurrentDictionary(); + Parallel.ForEach(windowHandles, hWnd => { var windowTitle = GetWindowTitle(hWnd); if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd)) @@ -80,20 +97,26 @@ namespace Flow.Launcher.Plugin.ProcessKiller var result = PInvoke.GetWindowThreadProcessId(hWnd, &processId); if (result == 0u || processId == 0u) { - return false; + return; } - var process = Process.GetProcessById((int)processId); - if (!processDict.ContainsKey((int)processId)) + // Ensure each process ID is processed only once + if (processedProcessIds.TryAdd((int)processId, 0)) { - processDict.Add((int)processId, windowTitle); + try + { + var process = Process.GetProcessById((int)processId); + processDict.TryAdd((int)processId, windowTitle); + } + catch + { + // Handle exceptions (e.g., process exited) + } } } + }); - return true; - }, IntPtr.Zero); - - return processDict; + return new Dictionary(processDict); } private static unsafe string GetWindowTitle(HWND hwnd) @@ -131,7 +154,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller } catch (Exception e) { - context.API.LogException($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e); + context.API.LogException(ClassName, $"Failed to kill process {p.ProcessName}", e); } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs index 916bc6a39..57cd2ab86 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs @@ -2,6 +2,8 @@ { public class Settings { + public bool ShowWindowTitle { get; set; } = true; + public bool PutVisibleWindowProcessesTop { get; set; } = false; } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs index bacf1ba08..0728d9c0f 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs @@ -9,6 +9,12 @@ Settings = settings; } + public bool ShowWindowTitle + { + get => Settings.ShowWindowTitle; + set => Settings.ShowWindowTitle = value; + } + public bool PutVisibleWindowProcessesTop { get => Settings.PutVisibleWindowProcessesTop; diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml index d15d6c3e0..b969be4e8 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml @@ -12,10 +12,16 @@ + + diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json index 956c4b4e1..0379194c4 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json @@ -4,7 +4,7 @@ "Name":"Process Killer", "Description":"Kill running processes from Flow", "Author":"Flow-Launcher", - "Version":"3.0.8", + "Version": "1.0.0", "Language":"csharp", "Website":"https://github.com/Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller", "IcoPath":"Images\\app.png", diff --git a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml index 9848f51cf..26efc6813 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml @@ -5,21 +5,21 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:Flow.Launcher.Plugin.Program.ViewModels" - mc:Ignorable="d" Title="{DynamicResource flowlauncher_plugin_program_directory}" - d:DataContext="{d:DesignInstance vm:AddProgramSourceViewModel}" Width="Auto" Height="276" + d:DataContext="{d:DesignInstance vm:AddProgramSourceViewModel}" Background="{DynamicResource PopuBGColor}" Foreground="{DynamicResource PopupTextColor}" ResizeMode="NoResize" SizeToContent="Width" - WindowStartupLocation="CenterScreen"> + WindowStartupLocation="CenterScreen" + mc:Ignorable="d"> - + @@ -59,11 +59,11 @@ - - + + - + @@ -104,16 +104,16 @@ HorizontalAlignment="Stretch" Click="BrowseButton_Click" Content="{DynamicResource flowlauncher_plugin_program_browse}" - Visibility="{Binding IsCustomSource, Converter={StaticResource BooleanToVisibilityConverter}}" - DockPanel.Dock="Right" /> + DockPanel.Dock="Right" + Visibility="{Binding IsCustomSource, Converter={StaticResource BooleanToVisibilityConverter}}" /> + VerticalAlignment="Center" + IsReadOnly="{Binding IsNotCustomSource}" + Text="{Binding Location, Mode=TwoWay}" /> + Margin="10 0" + VerticalAlignment="Center" + IsChecked="{Binding Enabled, Mode=TwoWay}" /> + BorderThickness="0 1 0 0"> - + - + @@ -151,82 +151,82 @@ TextWrapping="Wrap" /> - + - + appref-ms exe lnk + BorderThickness="1 0 0 0"> @@ -237,27 +237,27 @@ Grid.Row="1" Background="{DynamicResource PopupButtonAreaBGColor}" BorderBrush="{DynamicResource PopupButtonAreaBorderColor}" - BorderThickness="0,1,0,0"> + BorderThickness="0 1 0 0"> - + - +