mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into add-select-all-query
This commit is contained in:
commit
712e269f06
525 changed files with 20389 additions and 8877 deletions
2
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
242
.github/update_release_pr.py
vendored
Normal file
242
.github/update_release_pr.py
vendored
Normal file
|
|
@ -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}")
|
||||
85
.github/workflows/default_plugins.yml
vendored
85
.github/workflows/default_plugins.yml
vendored
|
|
@ -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"
|
||||
|
|
|
|||
91
.github/workflows/dotnet.yml
vendored
Normal file
91
.github/workflows/dotnet.yml
vendored
Normal file
|
|
@ -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
|
||||
34
.github/workflows/release_deploy.yml
vendored
Normal file
34
.github/workflows/release_deploy.yml
vendored
Normal file
|
|
@ -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
|
||||
25
.github/workflows/release_pr.yml
vendored
Normal file
25
.github/workflows/release_pr.yml
vendored
Normal file
|
|
@ -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
|
||||
5
.github/workflows/spelling.yml
vendored
5
.github/workflows/spelling.yml
vendored
|
|
@ -41,9 +41,8 @@ on:
|
|||
# tags-ignore:
|
||||
# - "**"
|
||||
pull_request_target:
|
||||
branches:
|
||||
- '**'
|
||||
# - '!l10n_dev'
|
||||
branches-ignore:
|
||||
- master
|
||||
tags-ignore:
|
||||
- "**"
|
||||
types:
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
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<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private UpdateManager NewUpdateManager()
|
||||
private static UpdateManager NewUpdateManager()
|
||||
{
|
||||
var applicationFolderName = Constant.ApplicationDirectory
|
||||
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
|
||||
|
|
@ -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,26 +76,22 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveShortcuts()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
|
||||
}
|
||||
|
||||
public void RemoveUninstallerEntry()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.RemoveUninstallerRegistryEntry();
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.RemoveUninstallerRegistryEntry();
|
||||
}
|
||||
|
||||
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
||||
|
|
@ -110,12 +107,10 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
public void CreateShortcuts()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
|
||||
}
|
||||
|
||||
public void CreateUninstallerEntry()
|
||||
|
|
@ -129,18 +124,14 @@ namespace Flow.Launcher.Core.Configuration
|
|||
subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String);
|
||||
}
|
||||
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||
}
|
||||
|
||||
internal void IndicateDeletion(string filePathTodelete)
|
||||
private static void IndicateDeletion(string filePathTodelete)
|
||||
{
|
||||
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);
|
||||
using (var _ = File.CreateText(deleteFilePath))
|
||||
{
|
||||
}
|
||||
using var _ = File.CreateText(deleteFilePath);
|
||||
}
|
||||
|
||||
///<summary>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,32 @@
|
|||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
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<IPublicAPI>();
|
||||
|
||||
private string latestEtag = "";
|
||||
|
||||
private List<UserPlugin> plugins = new();
|
||||
|
||||
private static JsonSerializerOptions PluginStoreItemSerializationOption = new JsonSerializerOptions()
|
||||
private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
|
||||
};
|
||||
|
|
@ -33,35 +41,49 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
/// </remarks>
|
||||
public async Task<List<UserPlugin>> 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<List<UserPlugin>>(PluginStoreItemSerializationOption, cancellationToken: token)
|
||||
.ConfigureAwait(false);
|
||||
this.latestEtag = response.Headers.ETag?.Tag;
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
plugins = await response.Content
|
||||
.ReadFromJsonAsync<List<UserPlugin>>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
|
|
@ -39,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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
public abstract class AbstractPluginEnvironment
|
||||
{
|
||||
private static readonly string ClassName = nameof(AbstractPluginEnvironment);
|
||||
|
||||
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
internal abstract string Language { get; }
|
||||
|
|
@ -42,8 +43,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
internal IEnumerable<PluginPair> Setup()
|
||||
{
|
||||
// If no plugin is using the language, return empty list
|
||||
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new List<PluginPair>();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
|
||||
{
|
||||
|
|
@ -55,24 +59,55 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
}
|
||||
|
||||
var noRuntimeMessage = string.Format(
|
||||
InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
|
||||
API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
{
|
||||
var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
|
||||
string selectedFile;
|
||||
var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
|
||||
|
||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedFile))
|
||||
{
|
||||
PluginsSettingsFilePath = selectedFile;
|
||||
|
||||
}
|
||||
// Nothing selected because user pressed cancel from the file dialog window
|
||||
if (string.IsNullOrEmpty(selectedFile))
|
||||
InstallEnvironment();
|
||||
else
|
||||
{
|
||||
var forceDownloadMessage = string.Format(
|
||||
API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
|
||||
// Let users select valid path or choose to download
|
||||
while (string.IsNullOrEmpty(selectedFile))
|
||||
{
|
||||
if (API.ShowMsgBox(forceDownloadMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
// Continue select file
|
||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
}
|
||||
else
|
||||
{
|
||||
// User selected no, break the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedFile))
|
||||
{
|
||||
PluginsSettingsFilePath = selectedFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
InstallEnvironment();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -85,8 +120,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
|
||||
Log.Error("PluginsLoader",
|
||||
API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
|
||||
API.LogError(ClassName,
|
||||
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
||||
$"{Language}Environment");
|
||||
|
||||
|
|
@ -98,13 +133,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
|
||||
{
|
||||
if (expectedPath == currentPath)
|
||||
return;
|
||||
if (expectedPath == currentPath) return;
|
||||
|
||||
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
|
||||
|
||||
InstallEnvironment();
|
||||
|
||||
}
|
||||
|
||||
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
|
||||
|
|
@ -116,13 +149,16 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
foreach (var metadata in PluginMetadataList)
|
||||
{
|
||||
if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
metadata.AssemblyName = string.Empty;
|
||||
pluginPairs.Add(CreatePluginPair(filePath, metadata));
|
||||
}
|
||||
}
|
||||
|
||||
return pluginPairs;
|
||||
}
|
||||
|
||||
private string GetFileFromDialog(string title, string filter = "")
|
||||
private static string GetFileFromDialog(string title, string filter = "")
|
||||
{
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
|
|
@ -136,7 +172,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
var result = dlg.ShowDialog();
|
||||
return result == DialogResult.OK ? dlg.FileName : string.Empty;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -179,31 +214,33 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
else
|
||||
{
|
||||
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
|
||||
{
|
||||
settings.PluginSettings.PythonExecutablePath
|
||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
|
||||
}
|
||||
|
||||
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
|
||||
{
|
||||
settings.PluginSettings.NodeExecutablePath
|
||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(filePath)) return false;
|
||||
|
||||
// DataLocation.PortableDataPath returns the current portable path, this determines if an out
|
||||
// of date path is also a portable path.
|
||||
var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}";
|
||||
var portableAppEnvLocation = Path.Combine("UserData", DataLocation.PluginEnvironments, pluginEnvironmentName);
|
||||
|
||||
return filePath.Contains(portableAppEnvLocation);
|
||||
}
|
||||
|
||||
private static bool IsUsingRoamingPath(string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(filePath)) return false;
|
||||
|
||||
return filePath.StartsWith(DataLocation.RoamingDataPath);
|
||||
}
|
||||
|
|
@ -213,8 +250,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
|
||||
|
||||
// get the substring after "Environments" because we can not determine it dynamically
|
||||
var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count());
|
||||
return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}";
|
||||
var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
|
||||
return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using Flow.Launcher.Plugin;
|
|||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
|
||||
internal class JavaScriptEnvironment : TypeScriptEnvironment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.JavaScript;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using Flow.Launcher.Plugin;
|
|||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
|
||||
internal class JavaScriptV2Environment : TypeScriptV2Environment
|
||||
{
|
||||
internal override string Language => AllowedLanguage.JavaScriptV2;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using Droplex;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
|
|
@ -22,17 +23,23 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
internal override string FileDialogFilter => "Python|pythonw.exe";
|
||||
|
||||
internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; }
|
||||
internal override string PluginsSettingsFilePath
|
||||
{
|
||||
get => PluginSettings.PythonExecutablePath;
|
||||
set => PluginSettings.PythonExecutablePath = value;
|
||||
}
|
||||
|
||||
internal PythonEnvironment(List<PluginMetadata> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.IO;
|
||||
using Droplex;
|
||||
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
|
||||
{
|
||||
|
|
@ -19,15 +20,21 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
|
||||
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
|
||||
|
||||
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; }
|
||||
internal override string PluginsSettingsFilePath
|
||||
{
|
||||
get => PluginSettings.NodeExecutablePath;
|
||||
set => PluginSettings.NodeExecutablePath = value;
|
||||
}
|
||||
|
||||
internal TypeScriptEnvironment(List<PluginMetadata> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.IO;
|
||||
using Droplex;
|
||||
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
|
||||
{
|
||||
|
|
@ -19,15 +20,21 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
|
||||
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
|
||||
|
||||
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; }
|
||||
internal override string PluginsSettingsFilePath
|
||||
{
|
||||
get => PluginSettings.NodeExecutablePath;
|
||||
set => PluginSettings.NodeExecutablePath = value;
|
||||
}
|
||||
|
||||
internal TypeScriptV2Environment(List<PluginMetadata> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
public static class PluginsManifest
|
||||
{
|
||||
private static readonly 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",
|
||||
|
|
@ -17,11 +20,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
||||
|
||||
private static DateTime lastFetchedAt = DateTime.MinValue;
|
||||
private static TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
|
||||
private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
public static List<UserPlugin> UserPlugins { get; private set; }
|
||||
|
||||
public static async Task<bool> UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
|
||||
public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -43,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e);
|
||||
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(ClassName, "Http request failed", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
public record UserPlugin
|
||||
{
|
||||
public string ID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string Website { get; set; }
|
||||
public string UrlDownload { get; set; }
|
||||
public string UrlSourceCode { get; set; }
|
||||
public string LocalInstallPath { get; set; }
|
||||
public string IcoPath { get; set; }
|
||||
public DateTime? LatestReleaseDate { get; set; }
|
||||
public DateTime? DateAdded { get; set; }
|
||||
|
||||
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,14 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Microsoft.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using CheckBox = System.Windows.Controls.CheckBox;
|
||||
using Control = System.Windows.Controls.Control;
|
||||
using Orientation = System.Windows.Controls.Orientation;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
using System.Windows.Documents;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -32,7 +18,9 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// </summary>
|
||||
internal abstract class JsonRPCPlugin : JsonRPCPluginBase
|
||||
{
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
public new const string JsonRPC = "JsonRPC";
|
||||
|
||||
private static readonly string ClassName = nameof(JsonRPCPlugin);
|
||||
|
||||
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
|
||||
protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
|
||||
|
|
@ -41,9 +29,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
private int RequestId { get; set; }
|
||||
|
||||
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
|
||||
|
||||
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
var request = new JsonRPCRequestModel(RequestId++,
|
||||
|
|
@ -69,13 +54,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions settingSerializeOption = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
|
||||
|
||||
private async Task<List<Result>> DeserializedResultAsync(Stream output)
|
||||
{
|
||||
await using (output)
|
||||
|
|
@ -134,7 +112,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return !result.JsonRPCAction.DontHideAfterAction;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Execute external program and return the output
|
||||
/// </summary>
|
||||
|
|
@ -172,11 +149,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var error = standardError.ReadToEnd();
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Log.Error($"|JsonRPCPlugin.Execute|{error}");
|
||||
Context.API.LogError(ClassName, error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error.");
|
||||
Context.API.LogError(ClassName, "Empty standard output and standard error.");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
|
|
@ -184,8 +161,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(
|
||||
$"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>",
|
||||
Context.API.LogException(ClassName,
|
||||
$"Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>",
|
||||
e);
|
||||
return string.Empty;
|
||||
}
|
||||
|
|
@ -196,7 +173,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
using var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
|
||||
Context.API.LogError(ClassName, "Can't start new process");
|
||||
return Stream.Null;
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +193,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception("|JsonRPCPlugin.ExecuteAsync|Exception when kill process", e);
|
||||
Context.API.LogException(ClassName, "Exception when kill process", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -237,7 +214,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
case (0, 0):
|
||||
const string errorMessage = "Empty JSON-RPC Response.";
|
||||
Log.Warn($"|{nameof(JsonRPCPlugin)}.{nameof(ExecuteAsync)}|{errorMessage}");
|
||||
Context.API.LogWarn(ClassName, errorMessage);
|
||||
break;
|
||||
case (_, not 0):
|
||||
throw new InvalidDataException(Encoding.UTF8.GetString(errorBuffer.ToArray())); // The process has exited with an error message
|
||||
|
|
|
|||
|
|
@ -1,32 +1,15 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Microsoft.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using CheckBox = System.Windows.Controls.CheckBox;
|
||||
using Control = System.Windows.Controls.Control;
|
||||
using Orientation = System.Windows.Controls.Orientation;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
using System.Windows.Documents;
|
||||
using static System.Windows.Forms.LinkLabel;
|
||||
using Droplex;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -36,16 +19,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// </summary>
|
||||
public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||
{
|
||||
protected PluginInitContext Context;
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
|
||||
private int RequestId { get; set; }
|
||||
protected PluginInitContext Context;
|
||||
|
||||
private string SettingConfigurationPath =>
|
||||
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||
|
||||
private string SettingDirectory => Path.Combine(DataLocation.PluginSettingsDirectory,
|
||||
Context.CurrentPluginMetadata.Name);
|
||||
private string SettingDirectory => Context.CurrentPluginMetadata.PluginSettingsDirectoryPath;
|
||||
|
||||
private string SettingPath => Path.Combine(SettingDirectory, "Settings.json");
|
||||
|
||||
|
|
@ -125,7 +106,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public abstract Task<List<Result>> QueryAsync(Query query, CancellationToken token);
|
||||
|
||||
|
||||
private async Task InitSettingAsync()
|
||||
{
|
||||
JsonRpcConfigurationModel configuration = null;
|
||||
|
|
@ -137,7 +117,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
await File.ReadAllTextAsync(SettingConfigurationPath));
|
||||
}
|
||||
|
||||
|
||||
Settings ??= new JsonRPCPluginSettings
|
||||
{
|
||||
Configuration = configuration, SettingPath = SettingPath, API = Context.API
|
||||
|
|
@ -148,7 +127,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public virtual async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
this.Context = context;
|
||||
Context = context;
|
||||
await InitSettingAsync();
|
||||
}
|
||||
|
||||
|
|
@ -166,13 +145,5 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
return Settings.CreateSettingPanel();
|
||||
}
|
||||
|
||||
public void DeletePluginSettingsDirectory()
|
||||
{
|
||||
if (Directory.Exists(SettingDirectory))
|
||||
{
|
||||
Directory.Delete(SettingDirectory, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Forms;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Plugin;
|
||||
using CheckBox = System.Windows.Controls.CheckBox;
|
||||
using ComboBox = System.Windows.Controls.ComboBox;
|
||||
using Control = System.Windows.Controls.Control;
|
||||
using Orientation = System.Windows.Controls.Orientation;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -23,51 +19,74 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public required string SettingPath { get; init; }
|
||||
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
|
||||
|
||||
public IReadOnlyDictionary<string, object> Inner => Settings;
|
||||
protected ConcurrentDictionary<string, object> Settings { get; set; }
|
||||
public IReadOnlyDictionary<string, object?> Inner => Settings;
|
||||
protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!;
|
||||
public required IPublicAPI API { get; init; }
|
||||
|
||||
private JsonStorage<ConcurrentDictionary<string, object>> _storage;
|
||||
private static readonly string ClassName = nameof(JsonRPCPluginSettings);
|
||||
|
||||
// maybe move to resource?
|
||||
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
|
||||
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
|
||||
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
|
||||
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
|
||||
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
|
||||
|
||||
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
|
||||
private static readonly Thickness SettingPanelItemLeftMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftMargin");
|
||||
private static readonly Thickness SettingPanelItemTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemTopBottomMargin");
|
||||
private static readonly Thickness SettingPanelItemLeftTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftTopBottomMargin");
|
||||
private static readonly double SettingPanelTextBoxMinWidth = (double)Application.Current.FindResource("SettingPanelTextBoxMinWidth");
|
||||
private static readonly double SettingPanelPathTextBoxWidth = (double)Application.Current.FindResource("SettingPanelPathTextBoxWidth");
|
||||
private static readonly double SettingPanelAreaTextBoxMinHeight = (double)Application.Current.FindResource("SettingPanelAreaTextBoxMinHeight");
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_storage = new JsonStorage<ConcurrentDictionary<string, object>>(SettingPath);
|
||||
Settings = await _storage.LoadAsync();
|
||||
|
||||
if (Configuration == null)
|
||||
if (Settings == null)
|
||||
{
|
||||
return;
|
||||
_storage = new JsonStorage<ConcurrentDictionary<string, object?>>(SettingPath);
|
||||
Settings = await _storage.LoadAsync();
|
||||
|
||||
// Because value type of settings dictionary is object which causes them to be JsonElement when loading from json files,
|
||||
// we need to convert it to the correct type
|
||||
foreach (var (key, value) in Settings)
|
||||
{
|
||||
if (value is not JsonElement jsonElement) continue;
|
||||
|
||||
Settings[key] = jsonElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => jsonElement.GetString() ?? value,
|
||||
JsonValueKind.True => jsonElement.GetBoolean(),
|
||||
JsonValueKind.False => jsonElement.GetBoolean(),
|
||||
JsonValueKind.Null => null,
|
||||
_ => value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (Configuration == null) return;
|
||||
|
||||
foreach (var (type, attributes) in Configuration.Body)
|
||||
{
|
||||
if (attributes.Name == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Skip if the setting does not have attributes or name
|
||||
if (attributes?.Name == null) continue;
|
||||
|
||||
if (!Settings.ContainsKey(attributes.Name))
|
||||
// Skip if the setting does not have attributes or name
|
||||
if (!NeedSaveInSettings(type)) continue;
|
||||
|
||||
// If need save in settings, we need to make sure the setting exists in the settings file
|
||||
if (Settings.ContainsKey(attributes.Name)) continue;
|
||||
|
||||
if (type == "checkbox")
|
||||
{
|
||||
// If can parse the default value to bool, use it, otherwise use false
|
||||
Settings[attributes.Name] = bool.TryParse(attributes.DefaultValue, out var value) && value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings[attributes.Name] = attributes.DefaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void UpdateSettings(IReadOnlyDictionary<string, object> settings)
|
||||
{
|
||||
if (settings == null || settings.Count == 0)
|
||||
return;
|
||||
if (settings == null || settings.Count == 0) return;
|
||||
|
||||
foreach (var (key, value) in settings)
|
||||
{
|
||||
|
|
@ -78,19 +97,23 @@ namespace Flow.Launcher.Core.Plugin
|
|||
switch (control)
|
||||
{
|
||||
case TextBox textBox:
|
||||
textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty);
|
||||
var text = value as string ?? string.Empty;
|
||||
textBox.Dispatcher.Invoke(() => textBox.Text = text);
|
||||
break;
|
||||
case PasswordBox passwordBox:
|
||||
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty);
|
||||
var password = value as string ?? string.Empty;
|
||||
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = password);
|
||||
break;
|
||||
case ComboBox comboBox:
|
||||
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
||||
break;
|
||||
case CheckBox checkBox:
|
||||
checkBox.Dispatcher.Invoke(() =>
|
||||
checkBox.IsChecked = value is bool isChecked
|
||||
? isChecked
|
||||
: bool.Parse(value as string ?? string.Empty));
|
||||
var isChecked = value is bool boolValue
|
||||
? boolValue
|
||||
// If can parse the default value to bool, use it, otherwise use false
|
||||
: value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString)
|
||||
&& boolValueFromString;
|
||||
checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = isChecked);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -101,14 +124,28 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
await _storage.SaveAsync();
|
||||
try
|
||||
{
|
||||
await _storage.SaveAsync();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_storage.Save();
|
||||
try
|
||||
{
|
||||
_storage.Save();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool NeedCreateSettingPanel()
|
||||
{
|
||||
// If there are no settings or the settings configuration is empty, return null
|
||||
|
|
@ -117,330 +154,350 @@ 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!;
|
||||
|
||||
var settingWindow = new UserControl();
|
||||
var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
||||
|
||||
ColumnDefinition gridCol1 = new ColumnDefinition();
|
||||
ColumnDefinition gridCol2 = new ColumnDefinition();
|
||||
|
||||
gridCol1.Width = new GridLength(70, GridUnitType.Star);
|
||||
gridCol2.Width = new GridLength(30, GridUnitType.Star);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol1);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol2);
|
||||
settingWindow.Content = mainPanel;
|
||||
int rowCount = 0;
|
||||
|
||||
foreach (var (type, attribute) in Configuration.Body)
|
||||
// Create main grid with two columns (Column 1: Auto, Column 2: *)
|
||||
var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
||||
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
|
||||
{
|
||||
Separator sep = new Separator();
|
||||
sep.VerticalAlignment = VerticalAlignment.Top;
|
||||
sep.Margin = settingSepMargin;
|
||||
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
|
||||
var panel = new StackPanel
|
||||
Width = new GridLength(0, GridUnitType.Auto)
|
||||
});
|
||||
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
|
||||
{
|
||||
Width = new GridLength(1, GridUnitType.Star)
|
||||
});
|
||||
|
||||
// Iterate over each setting and create one row for it
|
||||
var rowCount = 0;
|
||||
foreach (var (type, attributes) in Configuration!.Body)
|
||||
{
|
||||
// Skip if the setting does not have attributes or name
|
||||
if (attributes?.Name == null) continue;
|
||||
|
||||
// Add a new row to the main grid
|
||||
mainPanel.RowDefinitions.Add(new RowDefinition()
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelPanelMargin
|
||||
};
|
||||
|
||||
RowDefinition gridRow = new RowDefinition();
|
||||
mainPanel.RowDefinitions.Add(gridRow);
|
||||
var name = new TextBlock()
|
||||
{
|
||||
Text = attribute.Label,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
var desc = new TextBlock()
|
||||
{
|
||||
Text = attribute.Description,
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingDescMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
|
||||
|
||||
if (attribute.Description == null) /* if no description, hide */
|
||||
desc.Visibility = Visibility.Collapsed;
|
||||
|
||||
|
||||
if (type != "textBlock") /* if textBlock, hide desc */
|
||||
{
|
||||
panel.Children.Add(name);
|
||||
panel.Children.Add(desc);
|
||||
}
|
||||
|
||||
|
||||
Grid.SetColumn(panel, 0);
|
||||
Grid.SetRow(panel, rowCount);
|
||||
Height = new GridLength(0, GridUnitType.Auto)
|
||||
});
|
||||
|
||||
// State controls for column 0 and 1
|
||||
StackPanel? panel = null;
|
||||
FrameworkElement contentControl;
|
||||
|
||||
// If the type is textBlock, separator, or checkbox, we do not need to create a panel
|
||||
if (type != "textBlock" && type != "separator" && type != "checkbox")
|
||||
{
|
||||
// Create a panel to hold the label and description
|
||||
panel = new StackPanel
|
||||
{
|
||||
Margin = SettingPanelItemTopBottomMargin,
|
||||
Orientation = Orientation.Vertical,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
// Create a text block for name
|
||||
var name = new TextBlock()
|
||||
{
|
||||
Text = attributes.Label,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
// Create a text block for description
|
||||
TextBlock? desc = null;
|
||||
if (attributes.Description != null)
|
||||
{
|
||||
desc = new TextBlock()
|
||||
{
|
||||
Text = attributes.Description,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
desc.SetResourceReference(TextBlock.StyleProperty, "SettingPanelTextBlockDescriptionStyle"); // for theme change
|
||||
}
|
||||
|
||||
// Add the name and description to the panel
|
||||
panel.Children.Add(name);
|
||||
if (desc != null) panel.Children.Add(desc);
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "textBlock":
|
||||
{
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||
Margin = settingTextBlockMargin,
|
||||
Padding = new Thickness(0, 0, 0, 0),
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
Text = attributes.Description?.Replace("\\r\\n", "\r\n") ?? string.Empty,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemTopBottomMargin,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "input":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
MinWidth = SettingPanelTextBoxMinWidth,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = textBox.Text;
|
||||
};
|
||||
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = textBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "inputWithFileBtn":
|
||||
case "inputWithFolderBtn":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0),
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
|
||||
var Btn = new System.Windows.Controls.Button()
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
|
||||
};
|
||||
|
||||
Btn.Click += (_, _) =>
|
||||
{
|
||||
using CommonDialog dialog = type switch
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
"inputWithFolderBtn" => new FolderBrowserDialog(),
|
||||
_ => new OpenFileDialog(),
|
||||
Width = SettingPanelPathTextBoxWidth,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftMargin,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
if (dialog.ShowDialog() != DialogResult.OK) return;
|
||||
|
||||
var path = dialog switch
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
FolderBrowserDialog folderDialog => folderDialog.SelectedPath,
|
||||
OpenFileDialog fileDialog => fileDialog.FileName,
|
||||
Settings[attributes.Name] = textBox.Text;
|
||||
};
|
||||
textBox.Text = path;
|
||||
Settings[attribute.Name] = path;
|
||||
};
|
||||
|
||||
var dockPanel = new DockPanel() { Margin = settingControlMargin };
|
||||
var Btn = new Button()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftMargin,
|
||||
Content = API.GetTranslation("select")
|
||||
};
|
||||
|
||||
DockPanel.SetDock(Btn, Dock.Right);
|
||||
dockPanel.Children.Add(Btn);
|
||||
dockPanel.Children.Add(textBox);
|
||||
contentControl = dockPanel;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Btn.Click += (_, _) =>
|
||||
{
|
||||
using System.Windows.Forms.CommonDialog dialog = type switch
|
||||
{
|
||||
"inputWithFolderBtn" => new System.Windows.Forms.FolderBrowserDialog(),
|
||||
_ => new System.Windows.Forms.OpenFileDialog(),
|
||||
};
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
var path = dialog switch
|
||||
{
|
||||
System.Windows.Forms.FolderBrowserDialog folderDialog => folderDialog.SelectedPath,
|
||||
System.Windows.Forms.OpenFileDialog fileDialog => fileDialog.FileName,
|
||||
_ => throw new System.NotImplementedException()
|
||||
};
|
||||
|
||||
textBox.Text = path;
|
||||
Settings[attributes.Name] = path;
|
||||
};
|
||||
|
||||
var stackPanel = new StackPanel()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemTopBottomMargin,
|
||||
Orientation = Orientation.Horizontal
|
||||
};
|
||||
|
||||
// Create a stack panel to wrap the button and text box
|
||||
stackPanel.Children.Add(textBox);
|
||||
stackPanel.Children.Add(Btn);
|
||||
|
||||
contentControl = stackPanel;
|
||||
|
||||
break;
|
||||
}
|
||||
case "textarea":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Height = 120,
|
||||
Margin = settingControlMargin,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
MinHeight = SettingPanelAreaTextBoxMinHeight,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = textBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "passwordBox":
|
||||
{
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
Margin = settingControlMargin,
|
||||
Password = Settings[attribute.Name] as string ?? string.Empty,
|
||||
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
MinWidth = SettingPanelTextBoxMinWidth,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
Password = Settings[attributes.Name] as string ?? string.Empty,
|
||||
PasswordChar = attributes.passwordChar == default ? '*' : attributes.passwordChar,
|
||||
ToolTip = attributes.Description,
|
||||
};
|
||||
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
|
||||
contentControl = passwordBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = passwordBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "dropdown":
|
||||
{
|
||||
var comboBox = new System.Windows.Controls.ComboBox()
|
||||
{
|
||||
ItemsSource = attribute.Options,
|
||||
SelectedItem = Settings[attribute.Name],
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var comboBox = new ComboBox()
|
||||
{
|
||||
ItemsSource = attributes.Options,
|
||||
SelectedItem = Settings[attributes.Name],
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
|
||||
};
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = (string)((ComboBox)sender).SelectedItem;
|
||||
};
|
||||
|
||||
contentControl = comboBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = comboBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "checkbox":
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
IsChecked =
|
||||
Settings[attribute.Name] is bool isChecked
|
||||
// If can parse the default value to bool, use it, otherwise use false
|
||||
var defaultValue = bool.TryParse(attributes.DefaultValue, out var value) && value;
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
IsChecked =
|
||||
Settings[attributes.Name] is bool isChecked
|
||||
? isChecked
|
||||
: bool.Parse(attribute.DefaultValue),
|
||||
Margin = settingCheckboxMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
: defaultValue,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemTopBottomMargin,
|
||||
Content = attributes.Label,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
checkBox.Click += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
|
||||
};
|
||||
checkBox.Click += (sender, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = ((CheckBox)sender).IsChecked ?? defaultValue;
|
||||
};
|
||||
|
||||
contentControl = checkBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = checkBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
case "hyperlink":
|
||||
var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url };
|
||||
|
||||
var linkbtn = new System.Windows.Controls.Button
|
||||
{
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
Margin = settingControlMargin
|
||||
};
|
||||
var hyperlink = new Hyperlink
|
||||
{
|
||||
ToolTip = attributes.Description,
|
||||
NavigateUri = attributes.url
|
||||
};
|
||||
|
||||
linkbtn.Content = attribute.urlLabel;
|
||||
hyperlink.Inlines.Add(attributes.urlLabel);
|
||||
hyperlink.RequestNavigate += (sender, e) =>
|
||||
{
|
||||
API.OpenUrl(e.Uri);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
contentControl = linkbtn;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
var textBlock = new TextBlock()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
textBlock.Inlines.Add(hyperlink);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
contentControl = textBlock;
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
case "separator":
|
||||
{
|
||||
var sep = new Separator();
|
||||
|
||||
sep.SetResourceReference(Separator.StyleProperty, "SettingPanelSeparatorStyle");
|
||||
|
||||
contentControl = sep;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type != "textBlock")
|
||||
SettingControls[attribute.Name] = contentControl;
|
||||
// If type is textBlock or separator, we just add the content control to the main grid
|
||||
if (panel == null)
|
||||
{
|
||||
// Add the content control to the column 0, row rowCount and columnSpan 2 of the main grid
|
||||
mainPanel.Children.Add(contentControl);
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the panel to the column 0 and row rowCount of the main grid
|
||||
mainPanel.Children.Add(panel);
|
||||
Grid.SetColumn(panel, 0);
|
||||
Grid.SetRow(panel, rowCount);
|
||||
|
||||
// Add the content control to the column 1 and row rowCount of the main grid
|
||||
mainPanel.Children.Add(contentControl);
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
}
|
||||
|
||||
// Add into SettingControls for settings storage if need
|
||||
if (NeedSaveInSettings(type)) SettingControls[attributes.Name] = contentControl;
|
||||
|
||||
mainPanel.Children.Add(panel);
|
||||
mainPanel.Children.Add(contentControl);
|
||||
rowCount++;
|
||||
}
|
||||
|
||||
return settingWindow;
|
||||
// Wrap the main grid in a user control
|
||||
return new UserControl()
|
||||
{
|
||||
Content = mainPanel
|
||||
};
|
||||
}
|
||||
|
||||
private static bool NeedSaveInSettings(string type)
|
||||
{
|
||||
return type != "textBlock" && type != "separator" && type != "hyperlink";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,20 +10,20 @@ using Microsoft.VisualStudio.Threading;
|
|||
using StreamJsonRpc;
|
||||
using IAsyncDisposable = System.IAsyncDisposable;
|
||||
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated
|
||||
{
|
||||
public const string JsonRpc = "JsonRPC";
|
||||
|
||||
private static readonly string ClassName = nameof(JsonRPCPluginV2);
|
||||
|
||||
protected abstract IDuplexPipe ClientPipe { get; set; }
|
||||
|
||||
protected StreamReader ErrorStream { get; set; }
|
||||
|
||||
private JsonRpc RPC { get; set; }
|
||||
|
||||
|
||||
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||
{
|
||||
var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method,
|
||||
|
|
@ -55,7 +55,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return results;
|
||||
}
|
||||
|
||||
|
||||
public override async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
await base.InitAsync(context);
|
||||
|
|
@ -88,7 +87,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
protected abstract MessageHandlerType MessageHandler { get; }
|
||||
|
||||
|
||||
private void SetupJsonRPC()
|
||||
{
|
||||
var formatter = new SystemTextJsonFormatter { JsonSerializerOptions = RequestSerializeOption };
|
||||
|
|
@ -118,8 +116,17 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
await RPC.InvokeAsync("reload_data", Context);
|
||||
}
|
||||
catch (RemoteMethodNotFoundException e)
|
||||
catch (RemoteMethodNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (ConnectionLostException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Context.API.LogException(ClassName, $"Failed to call reload_data for plugin {Context.CurrentPluginMetadata.Name}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,8 +136,17 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
await RPC.InvokeAsync("close");
|
||||
}
|
||||
catch (RemoteMethodNotFoundException e)
|
||||
catch (RemoteMethodNotFoundException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (ConnectionLostException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Context.API.LogException(ClassName, $"Failed to call close for plugin {Context.CurrentPluginMetadata.Name}", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
|||
{
|
||||
public class JsonRPCPublicAPI
|
||||
{
|
||||
private IPublicAPI _api;
|
||||
private readonly IPublicAPI _api;
|
||||
|
||||
public JsonRPCPublicAPI(IPublicAPI api)
|
||||
{
|
||||
|
|
@ -104,7 +104,6 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
|||
return _api.GetAllPlugins();
|
||||
}
|
||||
|
||||
|
||||
public MatchResult FuzzySearch(string query, string stringToCompare)
|
||||
{
|
||||
return _api.FuzzySearch(query, stringToCompare);
|
||||
|
|
@ -156,6 +155,11 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
|||
_api.LogWarn(className, message, methodName);
|
||||
}
|
||||
|
||||
public void LogError(string className, string message, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
_api.LogError(className, message, methodName);
|
||||
}
|
||||
|
||||
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null)
|
||||
{
|
||||
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
|
||||
|
|
@ -185,5 +189,10 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
|||
{
|
||||
_api.StopLoadingBar();
|
||||
}
|
||||
|
||||
public void SavePluginCaches()
|
||||
{
|
||||
_api.SavePluginCaches();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
using System;
|
||||
using System;
|
||||
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<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// Parse plugin metadata in the given directories
|
||||
/// </summary>
|
||||
|
|
@ -33,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
|
||||
|
|
@ -50,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;
|
||||
}
|
||||
|
|
@ -102,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;
|
||||
}
|
||||
|
||||
|
|
@ -112,29 +117,29 @@ namespace Flow.Launcher.Core.Plugin
|
|||
metadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(configPath));
|
||||
metadata.PluginDirectory = pluginDirectory;
|
||||
// for plugins which doesn't has ActionKeywords key
|
||||
metadata.ActionKeywords = metadata.ActionKeywords ?? new List<string> { metadata.ActionKeyword };
|
||||
metadata.ActionKeywords ??= new List<string> { metadata.ActionKeyword };
|
||||
// for plugin still use old ActionKeyword
|
||||
metadata.ActionKeyword = metadata.ActionKeywords?[0];
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System.Text.Json;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using IRemovable = Flow.Launcher.Core.Storage.IRemovable;
|
||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -23,7 +22,10 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// </summary>
|
||||
public static class PluginManager
|
||||
{
|
||||
private static readonly string ClassName = nameof(PluginManager);
|
||||
|
||||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||
private static IEnumerable<PluginPair> _homePlugins;
|
||||
|
||||
public static List<PluginPair> AllPlugins { get; private set; }
|
||||
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||
|
|
@ -35,7 +37,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
private static PluginsSettings Settings;
|
||||
private static List<PluginMetadata> _metadatas;
|
||||
private static List<string> _modifiedPlugins = new List<string>();
|
||||
private static readonly List<string> _modifiedPlugins = new();
|
||||
|
||||
/// <summary>
|
||||
/// Directories that will hold Flow Launcher plugin directory
|
||||
|
|
@ -59,18 +61,34 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// </summary>
|
||||
public static void Save()
|
||||
{
|
||||
foreach (var plugin in AllPlugins)
|
||||
foreach (var pluginPair in AllPlugins)
|
||||
{
|
||||
var savable = plugin.Plugin as ISavable;
|
||||
savable?.Save();
|
||||
var savable = pluginPair.Plugin as ISavable;
|
||||
try
|
||||
{
|
||||
savable?.Save();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
|
||||
}
|
||||
}
|
||||
|
||||
API.SavePluginSettings();
|
||||
API.SavePluginCaches();
|
||||
}
|
||||
|
||||
public static async ValueTask DisposePluginsAsync()
|
||||
{
|
||||
foreach (var pluginPair in AllPlugins)
|
||||
{
|
||||
await DisposePluginAsync(pluginPair);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DisposePluginAsync(PluginPair pluginPair)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (pluginPair.Plugin)
|
||||
{
|
||||
|
|
@ -82,6 +100,10 @@ namespace Flow.Launcher.Core.Plugin
|
|||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ReloadDataAsync()
|
||||
|
|
@ -155,6 +177,35 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Settings = settings;
|
||||
Settings.UpdatePluginSettings(_metadatas);
|
||||
AllPlugins = PluginsLoader.Plugins(_metadatas, Settings);
|
||||
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
|
||||
UpdatePluginDirectory(_metadatas);
|
||||
}
|
||||
|
||||
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
|
||||
{
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -169,24 +220,37 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
try
|
||||
{
|
||||
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
|
||||
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
|
||||
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
|
||||
|
||||
pair.Metadata.InitTime += milliseconds;
|
||||
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(nameof(PluginManager), $"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<IContextMenu>();
|
||||
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
|
||||
|
||||
foreach (var plugin in AllPlugins)
|
||||
{
|
||||
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
|
||||
|
|
@ -205,9 +269,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
|
||||
InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService<Settings>().Language);
|
||||
|
||||
if (failedPlugins.Any())
|
||||
{
|
||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||
|
|
@ -228,17 +289,20 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (query is null)
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
||||
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
|
||||
return GlobalPlugins;
|
||||
|
||||
|
||||
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
||||
return new List<PluginPair>
|
||||
{
|
||||
plugin
|
||||
};
|
||||
}
|
||||
|
||||
public static ICollection<PluginPair> ValidPluginsForHomeQuery()
|
||||
{
|
||||
return _homePlugins.ToList();
|
||||
}
|
||||
|
||||
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
|
|
@ -246,7 +310,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
try
|
||||
{
|
||||
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
|
||||
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
|
@ -270,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,
|
||||
|
|
@ -283,6 +347,36 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return results;
|
||||
}
|
||||
|
||||
public static async Task<List<Result>> QueryHomeForPluginAsync(PluginPair pair, Query query, CancellationToken token)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
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<Result> results, PluginMetadata metadata, Query query)
|
||||
{
|
||||
foreach (var r in results)
|
||||
|
|
@ -334,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);
|
||||
}
|
||||
}
|
||||
|
|
@ -343,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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -367,7 +466,16 @@ namespace Flow.Launcher.Core.Plugin
|
|||
NonGlobalPlugins[newActionKeyword] = plugin;
|
||||
}
|
||||
|
||||
// Update action keywords and action keyword in plugin metadata
|
||||
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
|
||||
if (plugin.Metadata.ActionKeywords.Count > 0)
|
||||
{
|
||||
plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
plugin.Metadata.ActionKeyword = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -388,16 +496,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
|
||||
NonGlobalPlugins.Remove(oldActionkeyword);
|
||||
|
||||
|
||||
// Update action keywords and action keyword in plugin metadata
|
||||
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
|
||||
}
|
||||
|
||||
public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword)
|
||||
{
|
||||
if (oldActionKeyword != newActionKeyword)
|
||||
if (plugin.Metadata.ActionKeywords.Count > 0)
|
||||
{
|
||||
AddActionKeyword(id, newActionKeyword);
|
||||
RemoveActionKeyword(id, oldActionKeyword);
|
||||
plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
plugin.Metadata.ActionKeyword = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -428,37 +535,26 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
#region Public functions
|
||||
|
||||
public static bool PluginModified(string uuid)
|
||||
public static bool PluginModified(string id)
|
||||
{
|
||||
return _modifiedPlugins.Contains(uuid);
|
||||
return _modifiedPlugins.Contains(id);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
|
||||
/// unless it's a local path installation
|
||||
/// </summary>
|
||||
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||
public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(newVersion, zipFilePath, checkModified:false);
|
||||
UninstallPlugin(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
|
||||
await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
|
||||
_modifiedPlugins.Add(existingVersion.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
|
||||
/// </summary>
|
||||
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(plugin, zipFilePath, checkModified: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstall a plugin.
|
||||
/// </summary>
|
||||
public static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
|
||||
public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
|
||||
{
|
||||
UninstallPlugin(plugin, removePluginFromSettings, removePluginSettings, true);
|
||||
await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -499,20 +595,20 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
|
||||
|
||||
var defaultPluginIDs = new List<string>
|
||||
{
|
||||
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
|
||||
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
|
||||
"572be03c74c642baae319fc283e561a8", // Explorer
|
||||
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
|
||||
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
|
||||
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
|
||||
"791FC278BA414111B8D1886DFE447410", // Program
|
||||
"D409510CD0D2481F853690A07E6DC426", // Shell
|
||||
"CEA08895D2544B019B2E9C5009600DF4", // Sys
|
||||
"0308FD86DE0A4DEE8D62B9B535370992", // URL
|
||||
"565B73353DBF4806919830B9202EE3BF", // WebSearch
|
||||
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
|
||||
};
|
||||
{
|
||||
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
|
||||
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
|
||||
"572be03c74c642baae319fc283e561a8", // Explorer
|
||||
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
|
||||
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
|
||||
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
|
||||
"791FC278BA414111B8D1886DFE447410", // Program
|
||||
"D409510CD0D2481F853690A07E6DC426", // Shell
|
||||
"CEA08895D2544B019B2E9C5009600DF4", // Sys
|
||||
"0308FD86DE0A4DEE8D62B9B535370992", // URL
|
||||
"565B73353DBF4806919830B9202EE3BF", // WebSearch
|
||||
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
|
||||
};
|
||||
|
||||
// Treat default plugin differently, it needs to be removable along with each flow release
|
||||
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
|
||||
|
|
@ -530,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)
|
||||
|
|
@ -539,64 +635,63 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
internal static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
|
||||
internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
|
||||
{
|
||||
if (checkModified && PluginModified(plugin.ID))
|
||||
{
|
||||
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
|
||||
}
|
||||
|
||||
if (removePluginSettings || removePluginFromSettings)
|
||||
{
|
||||
// If we want to remove plugin from AllPlugins,
|
||||
// we need to dispose them so that they can release file handles
|
||||
// which can help FL to delete the plugin settings & cache folders successfully
|
||||
var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID);
|
||||
foreach (var pluginPair in pluginPairs)
|
||||
{
|
||||
await DisposePluginAsync(pluginPair);
|
||||
}
|
||||
}
|
||||
|
||||
if (removePluginSettings)
|
||||
{
|
||||
if (AllowedLanguage.IsDotNet(plugin.Language)) // for the plugin in .NET, we can use assembly loader
|
||||
// For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances
|
||||
if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable)
|
||||
{
|
||||
var assemblyLoader = new PluginAssemblyLoader(plugin.ExecuteFilePath);
|
||||
var assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||
var assemblyName = assembly.GetName().Name;
|
||||
|
||||
// if user want to remove the plugin settings, we cannot call save method for the plugin json storage instance of this plugin
|
||||
// so we need to remove it from the api instance
|
||||
var method = API.GetType().GetMethod("RemovePluginSettings");
|
||||
var pluginJsonStorage = method?.Invoke(API, new object[] { assemblyName });
|
||||
|
||||
// if there exists a json storage for current plugin, we need to delete the directory path
|
||||
if (pluginJsonStorage != null)
|
||||
{
|
||||
var deleteMethod = pluginJsonStorage.GetType().GetMethod("DeleteDirectory");
|
||||
try
|
||||
{
|
||||
deleteMethod?.Invoke(pluginJsonStorage, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e);
|
||||
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
|
||||
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
|
||||
}
|
||||
}
|
||||
removable.RemovePluginSettings(plugin.AssemblyName);
|
||||
removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath);
|
||||
}
|
||||
else // the plugin with json prc interface
|
||||
|
||||
try
|
||||
{
|
||||
var pluginPair = AllPlugins.FirstOrDefault(p => p.Metadata.ID == plugin.ID);
|
||||
if (pluginPair != null && pluginPair.Plugin is JsonRPCPlugin jsonRpcPlugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
jsonRpcPlugin.DeletePluginSettingsDirectory();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e);
|
||||
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
|
||||
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
|
||||
}
|
||||
}
|
||||
var pluginSettingsDirectory = plugin.PluginSettingsDirectoryPath;
|
||||
if (Directory.Exists(pluginSettingsDirectory))
|
||||
Directory.Delete(pluginSettingsDirectory, true);
|
||||
}
|
||||
catch (Exception 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));
|
||||
}
|
||||
}
|
||||
|
||||
if (removePluginFromSettings)
|
||||
{
|
||||
Settings.Plugins.Remove(plugin.ID);
|
||||
try
|
||||
{
|
||||
var pluginCacheDirectory = plugin.PluginCacheDirectoryPath;
|
||||
if (Directory.Exists(pluginCacheDirectory))
|
||||
Directory.Delete(pluginCacheDirectory, true);
|
||||
}
|
||||
catch (Exception 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));
|
||||
}
|
||||
Settings.RemovePluginSettings(plugin.ID);
|
||||
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,17 @@ using Flow.Launcher.Infrastructure.Logger;
|
|||
#pragma warning restore IDE0005
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
public static class PluginsLoader
|
||||
{
|
||||
private static readonly string ClassName = nameof(PluginsLoader);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||
{
|
||||
var dotnetPlugins = DotNetPlugins(metadatas);
|
||||
|
|
@ -50,7 +55,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
{
|
||||
var erroredPlugins = new List<string>();
|
||||
|
||||
|
|
@ -59,8 +64,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
var milliseconds = Stopwatch.Debug(
|
||||
$"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () =>
|
||||
var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
|
||||
{
|
||||
Assembly assembly = null;
|
||||
IAsyncPlugin plugin = null;
|
||||
|
|
@ -74,28 +78,30 @@ namespace Flow.Launcher.Core.Plugin
|
|||
typeof(IAsyncPlugin));
|
||||
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
|
||||
metadata.AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
#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
|
||||
|
||||
|
|
@ -112,7 +118,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (erroredPlugins.Count > 0)
|
||||
{
|
||||
var errorPluginString = String.Join(Environment.NewLine, erroredPlugins);
|
||||
var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
|
||||
|
||||
var errorMessage = "The following "
|
||||
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
|
||||
|
|
@ -130,23 +136,31 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
private static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(metadata => new PluginPair
|
||||
.Select(metadata =>
|
||||
{
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
|
||||
return new PluginPair
|
||||
{
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source)
|
||||
private static IEnumerable<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(metadata => new PluginPair
|
||||
.Select(metadata =>
|
||||
{
|
||||
Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata
|
||||
return new PluginPair
|
||||
{
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<string, PluginPair> nonGlobalPlugins)
|
||||
{
|
||||
// home query
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return new Query()
|
||||
{
|
||||
Search = string.Empty,
|
||||
RawQuery = string.Empty,
|
||||
SearchTerms = Array.Empty<string>(),
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IPublicAPI>();
|
||||
|
||||
private const string Folder = "Languages";
|
||||
private const string DefaultLanguageCode = "en";
|
||||
private const string DefaultFile = "en.xaml";
|
||||
private const string Extension = ".xaml";
|
||||
private readonly Settings _settings;
|
||||
private readonly List<string> _languageDirectories = new List<string>();
|
||||
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
|
||||
private readonly List<string> _languageDirectories = new();
|
||||
private readonly List<ResourceDictionary> _oldResources = new();
|
||||
private readonly string SystemLanguageCode;
|
||||
|
||||
public Internationalization(Settings settings)
|
||||
|
|
@ -67,9 +72,9 @@ namespace Flow.Launcher.Core.Resource
|
|||
return DefaultLanguageCode;
|
||||
}
|
||||
|
||||
internal void AddPluginLanguageDirectories(IEnumerable<PluginPair> plugins)
|
||||
private void AddPluginLanguageDirectories()
|
||||
{
|
||||
foreach (var plugin in plugins)
|
||||
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
{
|
||||
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
||||
var dir = Path.GetDirectoryName(location);
|
||||
|
|
@ -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}>");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +101,32 @@ namespace Flow.Launcher.Core.Resource
|
|||
_oldResources.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize language. Will change app language and plugin language based on settings.
|
||||
/// </summary>
|
||||
public async Task InitializeLanguageAsync()
|
||||
{
|
||||
// Get actual language
|
||||
var languageCode = _settings.Language;
|
||||
if (languageCode == Constant.SystemLanguageCode)
|
||||
{
|
||||
languageCode = SystemLanguageCode;
|
||||
}
|
||||
|
||||
// Get language by language code and change language
|
||||
var language = GetLanguageByLanguageCode(languageCode);
|
||||
|
||||
// Add plugin language directories first so that we can load language files from plugins
|
||||
AddPluginLanguageDirectories();
|
||||
|
||||
// Change language
|
||||
await ChangeLanguageAsync(language);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change language during runtime. Will change app language and plugin language & save settings.
|
||||
/// </summary>
|
||||
/// <param name="languageCode"></param>
|
||||
public void ChangeLanguage(string languageCode)
|
||||
{
|
||||
languageCode = languageCode.NonNull();
|
||||
|
|
@ -110,16 +141,21 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
// Get language by language code and change language
|
||||
var language = GetLanguageByLanguageCode(languageCode);
|
||||
ChangeLanguage(language, isSystem);
|
||||
|
||||
// Change language
|
||||
_ = ChangeLanguageAsync(language);
|
||||
|
||||
// Save settings
|
||||
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||
}
|
||||
|
||||
private Language GetLanguageByLanguageCode(string languageCode)
|
||||
private static Language GetLanguageByLanguageCode(string languageCode)
|
||||
{
|
||||
var lowercase = languageCode.ToLower();
|
||||
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
|
||||
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
|
||||
|
|
@ -128,26 +164,22 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
}
|
||||
|
||||
private void ChangeLanguage(Language language, bool isSystem)
|
||||
private async Task ChangeLanguageAsync(Language language)
|
||||
{
|
||||
language = language.NonNull();
|
||||
|
||||
// Remove old language files and load language
|
||||
RemoveOldLanguageFiles();
|
||||
if (language != AvailableLanguages.English)
|
||||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
|
||||
// Culture of main thread
|
||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||
|
||||
// Raise event after culture is set
|
||||
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
UpdatePluginMetadataTranslations();
|
||||
});
|
||||
// Raise event for plugins after culture is set
|
||||
await Task.Run(UpdatePluginMetadataTranslations);
|
||||
}
|
||||
|
||||
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
||||
|
|
@ -212,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)
|
||||
|
|
@ -221,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}";
|
||||
}
|
||||
}
|
||||
|
|
@ -230,8 +262,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
{
|
||||
var pluginI18N = p.Plugin as IPluginI18n;
|
||||
if (pluginI18N == null) return;
|
||||
if (p.Plugin is not IPluginI18n pluginI18N) return;
|
||||
try
|
||||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
|
|
@ -240,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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
using System;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
[Obsolete("InternationalizationManager.Instance is obsolete. Use Ioc.Default.GetRequiredService<Internationalization>() instead.")]
|
||||
public static class InternationalizationManager
|
||||
{
|
||||
public static Internationalization Instance
|
||||
=> Ioc.Default.GetRequiredService<Internationalization>();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Windows.Data;
|
|||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
[Obsolete("LocalizationConverter is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
|
||||
public class LocalizationConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class LocalizedDescriptionAttribute : DescriptionAttribute
|
||||
{
|
||||
private readonly Internationalization _translator;
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private readonly string _resourceKey;
|
||||
|
||||
public LocalizedDescriptionAttribute(string resourceKey)
|
||||
{
|
||||
_translator = InternationalizationManager.Instance;
|
||||
_resourceKey = resourceKey;
|
||||
}
|
||||
|
||||
|
|
@ -17,7 +21,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
get
|
||||
{
|
||||
string description = _translator.GetTranslation(_resourceKey);
|
||||
string description = API.GetTranslation(_resourceKey);
|
||||
return string.IsNullOrWhiteSpace(description) ?
|
||||
string.Format("[[{0}]]", _resourceKey) : description;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,31 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Shell;
|
||||
using System.Windows.Threading;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class Theme
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly string ClassName = nameof(Theme);
|
||||
|
||||
public bool BlurEnabled { get; private set; }
|
||||
|
||||
private const string ThemeMetadataNamePrefix = "Name:";
|
||||
private const string ThemeMetadataIsDarkPrefix = "IsDark:";
|
||||
private const string ThemeMetadataHasBlurPrefix = "HasBlur:";
|
||||
|
|
@ -31,14 +41,14 @@ namespace Flow.Launcher.Core.Resource
|
|||
private string _oldTheme;
|
||||
private const string Folder = Constant.Themes;
|
||||
private const string Extension = ".xaml";
|
||||
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
|
||||
private static string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
private static string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
|
||||
|
||||
public string CurrentTheme => _settings.Theme;
|
||||
private Thickness _themeResizeBorderThickness;
|
||||
|
||||
public bool BlurEnabled { get; set; }
|
||||
#endregion
|
||||
|
||||
private double mainWindowWidth;
|
||||
#region Constructor
|
||||
|
||||
public Theme(IPublicAPI publicAPI, Settings settings)
|
||||
{
|
||||
|
|
@ -50,22 +60,29 @@ namespace Flow.Launcher.Core.Resource
|
|||
MakeSureThemeDirectoriesExist();
|
||||
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
_oldResource = dicts.First(d =>
|
||||
_oldResource = dicts.FirstOrDefault(d =>
|
||||
{
|
||||
if (d.Source == null)
|
||||
return false;
|
||||
if (d.Source == null) return false;
|
||||
|
||||
var p = d.Source.AbsolutePath;
|
||||
var dir = Path.GetDirectoryName(p).NonNull();
|
||||
var info = new DirectoryInfo(dir);
|
||||
var f = info.Name;
|
||||
var e = Path.GetExtension(p);
|
||||
var found = f == Folder && e == Extension;
|
||||
return found;
|
||||
return p.Contains(Folder) && Path.GetExtension(p) == Extension;
|
||||
});
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
|
||||
if (_oldResource != null)
|
||||
{
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_api.LogError(ClassName, "Current theme resource not found. Initializing with default theme.");
|
||||
_oldTheme = Constant.DefaultTheme;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Theme Resources
|
||||
|
||||
private void MakeSureThemeDirectoriesExist()
|
||||
{
|
||||
foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir)))
|
||||
|
|
@ -76,73 +93,159 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ChangeTheme(string theme)
|
||||
{
|
||||
const string defaultTheme = Constant.DefaultTheme;
|
||||
|
||||
string path = GetThemePath(theme);
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
|
||||
|
||||
// reload all resources even if the theme itself hasn't changed in order to pickup changes
|
||||
// to things like fonts
|
||||
UpdateResourceDictionary(GetResourceDictionary(theme));
|
||||
|
||||
_settings.Theme = theme;
|
||||
|
||||
|
||||
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
|
||||
if (_oldTheme != theme || theme == defaultTheme)
|
||||
{
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
|
||||
BlurEnabled = Win32Helper.IsBlurTheme();
|
||||
|
||||
if (_settings.UseDropShadowEffect && !BlurEnabled)
|
||||
AddDropShadowEffectToCurrentTheme();
|
||||
|
||||
Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
|
||||
if (theme != defaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
|
||||
ChangeTheme(defaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (XamlParseException)
|
||||
{
|
||||
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
|
||||
if (theme != defaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
|
||||
ChangeTheme(defaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate)
|
||||
{
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
// Add new resources
|
||||
if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate))
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate);
|
||||
}
|
||||
|
||||
// Remove old resources
|
||||
if (_oldResource != null && _oldResource != dictionaryToUpdate &&
|
||||
Application.Current.Resources.MergedDictionaries.Contains(_oldResource))
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Remove(_oldResource);
|
||||
}
|
||||
|
||||
dicts.Remove(_oldResource);
|
||||
dicts.Add(dictionaryToUpdate);
|
||||
_oldResource = dictionaryToUpdate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates only the font settings and refreshes the UI.
|
||||
/// </summary>
|
||||
public void UpdateFonts()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load a ResourceDictionary for the specified theme.
|
||||
var themeName = _settings.Theme;
|
||||
var dict = GetThemeResourceDictionary(themeName);
|
||||
|
||||
// Apply font settings to the theme resource.
|
||||
ApplyFontSettings(dict);
|
||||
UpdateResourceDictionary(dict);
|
||||
|
||||
// Must apply blur and drop shadow effects
|
||||
_ = RefreshFrameAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_api.LogException(ClassName, "Error occurred while updating theme fonts", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads and applies font settings to the theme resource.
|
||||
/// </summary>
|
||||
private void ApplyFontSettings(ResourceDictionary dict)
|
||||
{
|
||||
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
|
||||
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
|
||||
{
|
||||
var fontFamily = new FontFamily(_settings.QueryBoxFont);
|
||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||
|
||||
SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true);
|
||||
SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
}
|
||||
|
||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
|
||||
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
||||
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
|
||||
{
|
||||
var fontFamily = new FontFamily(_settings.ResultFont);
|
||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch);
|
||||
|
||||
SetFontProperties(resultItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
}
|
||||
|
||||
if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
|
||||
{
|
||||
var fontFamily = new FontFamily(_settings.ResultSubFont);
|
||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch);
|
||||
|
||||
SetFontProperties(resultSubItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultSubItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies font properties to a Style.
|
||||
/// </summary>
|
||||
private static void SetFontProperties(Style style, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, bool isTextBox)
|
||||
{
|
||||
// Remove existing font-related setters
|
||||
if (isTextBox)
|
||||
{
|
||||
// First, find the setters to remove and store them in a list
|
||||
var settersToRemove = style.Setters
|
||||
.OfType<Setter>()
|
||||
.Where(setter =>
|
||||
setter.Property == Control.FontFamilyProperty ||
|
||||
setter.Property == Control.FontStyleProperty ||
|
||||
setter.Property == Control.FontWeightProperty ||
|
||||
setter.Property == Control.FontStretchProperty)
|
||||
.ToList();
|
||||
|
||||
// Remove each found setter one by one
|
||||
foreach (var setter in settersToRemove)
|
||||
{
|
||||
style.Setters.Remove(setter);
|
||||
}
|
||||
|
||||
// Add New font setter
|
||||
style.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||
style.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||
style.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||
style.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||
|
||||
// Set caret brush (retain existing logic)
|
||||
var caretBrushPropertyValue = style.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
|
||||
var foregroundPropertyValue = style.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
|
||||
.Select(x => x.Value).FirstOrDefault();
|
||||
if (!caretBrushPropertyValue && foregroundPropertyValue != null)
|
||||
style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
var settersToRemove = style.Setters
|
||||
.OfType<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));
|
||||
style.Setters.Add(new Setter(TextBlock.FontStretchProperty, fontStretch));
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceDictionary GetThemeResourceDictionary(string theme)
|
||||
{
|
||||
var uri = GetThemePath(theme);
|
||||
|
|
@ -154,9 +257,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
return dict;
|
||||
}
|
||||
|
||||
private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(_settings.Theme);
|
||||
|
||||
public ResourceDictionary GetResourceDictionary(string theme)
|
||||
private ResourceDictionary GetResourceDictionary(string theme)
|
||||
{
|
||||
var dict = GetThemeResourceDictionary(theme);
|
||||
|
||||
|
|
@ -168,22 +269,22 @@ namespace Flow.Launcher.Core.Resource
|
|||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch));
|
||||
queryBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||
queryBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||
queryBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||
queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||
|
||||
var caretBrushPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
|
||||
var foregroundPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
|
||||
.Select(x => x.Value).FirstOrDefault();
|
||||
if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue));
|
||||
|
||||
// Query suggestion box's font style is aligned with query box
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||
}
|
||||
|
||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||
|
|
@ -213,36 +314,20 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||
Array.ForEach(
|
||||
new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o
|
||||
new[] { resultSubItemStyle, resultSubItemSelectedStyle }, o
|
||||
=> Array.ForEach(setters, p => o.Setters.Add(p)));
|
||||
}
|
||||
|
||||
/* Ignore Theme Window Width and use setting */
|
||||
var windowStyle = dict["WindowStyle"] as Style;
|
||||
var width = _settings.WindowSize;
|
||||
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
|
||||
mainWindowWidth = (double)width;
|
||||
windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, width));
|
||||
return dict;
|
||||
}
|
||||
|
||||
private ResourceDictionary GetCurrentResourceDictionary( )
|
||||
public ResourceDictionary GetCurrentResourceDictionary()
|
||||
{
|
||||
return GetResourceDictionary(_settings.Theme);
|
||||
}
|
||||
|
||||
public List<ThemeData> LoadAvailableThemes()
|
||||
{
|
||||
List<ThemeData> themes = new List<ThemeData>();
|
||||
foreach (var themeDirectory in _themeDirectories)
|
||||
{
|
||||
var filePaths = Directory
|
||||
.GetFiles(themeDirectory)
|
||||
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
|
||||
.Select(GetThemeDataFromPath);
|
||||
themes.AddRange(filePaths);
|
||||
}
|
||||
|
||||
return themes.OrderBy(o => o.Name).ToList();
|
||||
return GetResourceDictionary(_settings.Theme);
|
||||
}
|
||||
|
||||
private ThemeData GetThemeDataFromPath(string path)
|
||||
|
|
@ -264,15 +349,15 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim();
|
||||
name = line[ThemeMetadataNamePrefix.Length..].Trim();
|
||||
}
|
||||
else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim());
|
||||
isDark = bool.Parse(line[ThemeMetadataIsDarkPrefix.Length..].Trim());
|
||||
}
|
||||
else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim());
|
||||
hasBlur = bool.Parse(line[ThemeMetadataHasBlurPrefix.Length..].Trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -293,6 +378,93 @@ namespace Flow.Launcher.Core.Resource
|
|||
return string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get & Change Theme
|
||||
|
||||
public ThemeData GetCurrentTheme()
|
||||
{
|
||||
var themes = GetAvailableThemes();
|
||||
var matchingTheme = themes.FirstOrDefault(t => t.FileNameWithoutExtension == _settings.Theme);
|
||||
if (matchingTheme == null)
|
||||
{
|
||||
_api.LogWarn(ClassName, $"No matching theme found for '{_settings.Theme}'. Falling back to the first available theme.");
|
||||
}
|
||||
return matchingTheme ?? themes.FirstOrDefault();
|
||||
}
|
||||
|
||||
public List<ThemeData> GetAvailableThemes()
|
||||
{
|
||||
List<ThemeData> themes = new List<ThemeData>();
|
||||
foreach (var themeDirectory in _themeDirectories)
|
||||
{
|
||||
var filePaths = Directory
|
||||
.GetFiles(themeDirectory)
|
||||
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
|
||||
.Select(GetThemeDataFromPath);
|
||||
themes.AddRange(filePaths);
|
||||
}
|
||||
|
||||
return themes.OrderBy(o => o.Name).ToList();
|
||||
}
|
||||
|
||||
public bool ChangeTheme(string theme = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(theme))
|
||||
theme = _settings.Theme;
|
||||
|
||||
string path = GetThemePath(theme);
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new DirectoryNotFoundException($"Theme path can't be found <{path}>");
|
||||
|
||||
// Retrieve theme resource – always use the resource with font settings applied.
|
||||
var resourceDict = GetResourceDictionary(theme);
|
||||
|
||||
UpdateResourceDictionary(resourceDict);
|
||||
|
||||
_settings.Theme = theme;
|
||||
|
||||
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
|
||||
if (_oldTheme != theme || theme == Constant.DefaultTheme)
|
||||
{
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
|
||||
BlurEnabled = IsBlurTheme();
|
||||
|
||||
// Apply blur and drop shadow effect so that we do not need to call it again
|
||||
_ = RefreshFrameAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
_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));
|
||||
ChangeTheme(Constant.DefaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (XamlParseException)
|
||||
{
|
||||
_api.LogError(ClassName, $"Theme <{theme}> fail to parse");
|
||||
if (theme != Constant.DefaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));
|
||||
ChangeTheme(Constant.DefaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Shadow Effect
|
||||
|
||||
public void AddDropShadowEffectToCurrentTheme()
|
||||
{
|
||||
var dict = GetCurrentResourceDictionary();
|
||||
|
|
@ -301,7 +473,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
var effectSetter = new Setter
|
||||
{
|
||||
Property = Border.EffectProperty,
|
||||
Property = UIElement.EffectProperty,
|
||||
Value = new DropShadowEffect
|
||||
{
|
||||
Opacity = 0.3,
|
||||
|
|
@ -311,13 +483,12 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
};
|
||||
|
||||
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
||||
if (marginSetter == null)
|
||||
if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is not Setter marginSetter)
|
||||
{
|
||||
var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin);
|
||||
marginSetter = new Setter()
|
||||
{
|
||||
Property = Border.MarginProperty,
|
||||
Property = FrameworkElement.MarginProperty,
|
||||
Value = margin,
|
||||
};
|
||||
windowBorderStyle.Setters.Add(marginSetter);
|
||||
|
|
@ -347,14 +518,12 @@ namespace Flow.Launcher.Core.Resource
|
|||
var dict = GetCurrentResourceDictionary();
|
||||
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
|
||||
|
||||
var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;
|
||||
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
|
||||
|
||||
if (effectSetter != null)
|
||||
if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == UIElement.EffectProperty) is Setter effectSetter)
|
||||
{
|
||||
windowBorderStyle.Setters.Remove(effectSetter);
|
||||
}
|
||||
if (marginSetter != null)
|
||||
|
||||
if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == FrameworkElement.MarginProperty) is Setter marginSetter)
|
||||
{
|
||||
var currentMargin = (Thickness)marginSetter.Value;
|
||||
var newMargin = new Thickness(
|
||||
|
|
@ -370,31 +539,393 @@ namespace Flow.Launcher.Core.Resource
|
|||
UpdateResourceDictionary(dict);
|
||||
}
|
||||
|
||||
public void SetResizeBorderThickness(WindowChrome windowChrome, bool fixedWindowSize)
|
||||
{
|
||||
if (fixedWindowSize)
|
||||
{
|
||||
windowChrome.ResizeBorderThickness = new Thickness(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
windowChrome.ResizeBorderThickness = _themeResizeBorderThickness;
|
||||
}
|
||||
}
|
||||
|
||||
// because adding drop shadow effect will change the margin of the window,
|
||||
// we need to update the window chrome thickness to correct set the resize border
|
||||
private static void SetResizeBoarderThickness(Thickness? effectMargin)
|
||||
private void SetResizeBoarderThickness(Thickness? effectMargin)
|
||||
{
|
||||
var window = Application.Current.MainWindow;
|
||||
if (WindowChrome.GetWindowChrome(window) is WindowChrome windowChrome)
|
||||
{
|
||||
Thickness thickness;
|
||||
// Save the theme resize border thickness so that we can restore it if we change ResizeWindow setting
|
||||
if (effectMargin == null)
|
||||
{
|
||||
thickness = SystemParameters.WindowResizeBorderThickness;
|
||||
_themeResizeBorderThickness = SystemParameters.WindowResizeBorderThickness;
|
||||
}
|
||||
else
|
||||
{
|
||||
thickness = new Thickness(
|
||||
_themeResizeBorderThickness = new Thickness(
|
||||
effectMargin.Value.Left + SystemParameters.WindowResizeBorderThickness.Left,
|
||||
effectMargin.Value.Top + SystemParameters.WindowResizeBorderThickness.Top,
|
||||
effectMargin.Value.Right + SystemParameters.WindowResizeBorderThickness.Right,
|
||||
effectMargin.Value.Bottom + SystemParameters.WindowResizeBorderThickness.Bottom);
|
||||
}
|
||||
|
||||
windowChrome.ResizeBorderThickness = thickness;
|
||||
// Apply the resize border thickness to the window chrome
|
||||
SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults);
|
||||
}
|
||||
}
|
||||
|
||||
public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null);
|
||||
#endregion
|
||||
|
||||
#region Blur Handling
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the frame to apply the current theme settings.
|
||||
/// </summary>
|
||||
public async Task RefreshFrameAsync()
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
// Get the actual backdrop type and drop shadow effect settings
|
||||
var (backdropType, useDropShadowEffect) = GetActualValue();
|
||||
|
||||
// Remove OS minimizing/maximizing animation
|
||||
// Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3);
|
||||
|
||||
// The timing of adding the shadow effect should vary depending on whether the theme is transparent.
|
||||
if (BlurEnabled)
|
||||
{
|
||||
AutoDropShadow(useDropShadowEffect);
|
||||
}
|
||||
SetBlurForWindow(_settings.Theme, backdropType);
|
||||
|
||||
if (!BlurEnabled)
|
||||
{
|
||||
AutoDropShadow(useDropShadowEffect);
|
||||
}
|
||||
}, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the blur for a window via SetWindowCompositionAttribute
|
||||
/// </summary>
|
||||
public async Task SetBlurForWindowAsync()
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
// Get the actual backdrop type and drop shadow effect settings
|
||||
var (backdropType, _) = GetActualValue();
|
||||
|
||||
SetBlurForWindow(_settings.Theme, backdropType);
|
||||
}, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actual backdrop type and drop shadow effect settings based on the current theme status.
|
||||
/// </summary>
|
||||
public (BackdropTypes BackdropType, bool UseDropShadowEffect) GetActualValue()
|
||||
{
|
||||
var backdropType = _settings.BackdropType;
|
||||
var useDropShadowEffect = _settings.UseDropShadowEffect;
|
||||
|
||||
// When changed non-blur theme, change to backdrop to none
|
||||
if (!BlurEnabled)
|
||||
{
|
||||
backdropType = BackdropTypes.None;
|
||||
}
|
||||
|
||||
// Dropshadow on and control disabled.(user can't change dropshadow with blur theme)
|
||||
if (BlurEnabled)
|
||||
{
|
||||
useDropShadowEffect = true;
|
||||
}
|
||||
|
||||
return (backdropType, useDropShadowEffect);
|
||||
}
|
||||
|
||||
private void SetBlurForWindow(string theme, BackdropTypes backdropType)
|
||||
{
|
||||
var dict = GetResourceDictionary(theme);
|
||||
if (dict == null) return;
|
||||
|
||||
var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null;
|
||||
if (windowBorderStyle == null) return;
|
||||
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null) return;
|
||||
|
||||
// Check if the theme supports blur
|
||||
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
|
||||
if (BlurEnabled && hasBlur && Win32Helper.IsBackdropSupported())
|
||||
{
|
||||
// If the BackdropType is Mica or MicaAlt, set the windowborderstyle's background to transparent
|
||||
if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt)
|
||||
{
|
||||
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property.Name == "Background"));
|
||||
windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(1, 0, 0, 0))));
|
||||
}
|
||||
else if (backdropType == BackdropTypes.Acrylic)
|
||||
{
|
||||
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().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<Setter>().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);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Apply default style when Blur is disabled
|
||||
Win32Helper.DWMSetBackdropForWindow(mainWindow, BackdropTypes.None);
|
||||
ColorizeWindow(theme, backdropType);
|
||||
}
|
||||
|
||||
UpdateResourceDictionary(dict);
|
||||
}
|
||||
|
||||
private void AutoDropShadow(bool useDropShadowEffect)
|
||||
{
|
||||
SetWindowCornerPreference("Default");
|
||||
RemoveDropShadowEffectFromCurrentTheme();
|
||||
if (useDropShadowEffect)
|
||||
{
|
||||
if (BlurEnabled && Win32Helper.IsBackdropSupported())
|
||||
{
|
||||
SetWindowCornerPreference("Round");
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWindowCornerPreference("Default");
|
||||
AddDropShadowEffectToCurrentTheme();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (BlurEnabled && Win32Helper.IsBackdropSupported())
|
||||
{
|
||||
SetWindowCornerPreference("Default");
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveDropShadowEffectFromCurrentTheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetWindowCornerPreference(string cornerType)
|
||||
{
|
||||
Window mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null)
|
||||
return;
|
||||
|
||||
Win32Helper.DWMSetCornerPreferenceForWindow(mainWindow, cornerType);
|
||||
}
|
||||
|
||||
// Get Background Color from WindowBorderStyle when there not color for BG.
|
||||
// for theme has not "LightBG" or "DarkBG" case.
|
||||
private Color GetWindowBorderStyleBackground(string theme)
|
||||
{
|
||||
var Resources = GetThemeResourceDictionary(theme);
|
||||
var windowBorderStyle = (Style)Resources["WindowBorderStyle"];
|
||||
|
||||
var backgroundSetter = windowBorderStyle.Setters
|
||||
.OfType<Setter>()
|
||||
.FirstOrDefault(s => s.Property == Border.BackgroundProperty);
|
||||
|
||||
if (backgroundSetter != null)
|
||||
{
|
||||
// Background's Value is DynamicColor Case
|
||||
var backgroundValue = backgroundSetter.Value;
|
||||
|
||||
if (backgroundValue is SolidColorBrush solidColorBrush)
|
||||
{
|
||||
return solidColorBrush.Color; // Return SolidColorBrush's Color
|
||||
}
|
||||
else if (backgroundValue is DynamicResourceExtension dynamicResource)
|
||||
{
|
||||
// When DynamicResource Extension it is, Key is resource's name.
|
||||
var resourceKey = backgroundSetter.Value.ToString();
|
||||
|
||||
// find key in resource and return color.
|
||||
if (Resources.Contains(resourceKey))
|
||||
{
|
||||
var colorResource = Resources[resourceKey];
|
||||
if (colorResource is SolidColorBrush colorBrush)
|
||||
{
|
||||
return colorBrush.Color;
|
||||
}
|
||||
else if (colorResource is Color color)
|
||||
{
|
||||
return color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Colors.Transparent; // Default is transparent
|
||||
}
|
||||
|
||||
private void ApplyPreviewBackground(Color? bgColor = null)
|
||||
{
|
||||
if (bgColor == null) return;
|
||||
|
||||
// Create a new Style for the preview
|
||||
var previewStyle = new Style(typeof(Border));
|
||||
|
||||
// Get the original WindowBorderStyle
|
||||
if (Application.Current.Resources.Contains("WindowBorderStyle") &&
|
||||
Application.Current.Resources["WindowBorderStyle"] is Style originalStyle)
|
||||
{
|
||||
// Copy the original style, including the base style if it exists
|
||||
CopyStyle(originalStyle, previewStyle);
|
||||
}
|
||||
|
||||
// Apply background color (remove transparency in color)
|
||||
Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B);
|
||||
previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor)));
|
||||
|
||||
// The blur theme keeps the corner round fixed (applying DWM code to modify it causes rendering issues).
|
||||
// The non-blur theme retains the previously set WindowBorderStyle.
|
||||
if (BlurEnabled)
|
||||
{
|
||||
previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5)));
|
||||
previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1)));
|
||||
}
|
||||
|
||||
// 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<Setter>())
|
||||
{
|
||||
targetStyle.Setters.Add(new Setter(setter.Property, setter.Value));
|
||||
}
|
||||
}
|
||||
|
||||
private void ColorizeWindow(string theme, BackdropTypes backdropType)
|
||||
{
|
||||
var dict = GetThemeResourceDictionary(theme);
|
||||
if (dict == null) return;
|
||||
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null) return;
|
||||
|
||||
// Check if the theme supports blur
|
||||
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
|
||||
|
||||
// SystemBG value check (Auto, Light, Dark)
|
||||
string systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto
|
||||
|
||||
// Check the user's ColorScheme setting
|
||||
string colorScheme = _settings.ColorScheme;
|
||||
|
||||
// Check system dark mode setting (read AppsUseLightTheme value)
|
||||
int themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1);
|
||||
bool isSystemDark = themeValue == 0;
|
||||
|
||||
// Final decision on whether to use dark mode
|
||||
bool useDarkMode = false;
|
||||
|
||||
// If systemBG is not "Auto", prioritize it over ColorScheme and set the mode based on systemBG value
|
||||
if (systemBG == "Dark")
|
||||
{
|
||||
useDarkMode = true; // Dark
|
||||
}
|
||||
else if (systemBG == "Light")
|
||||
{
|
||||
useDarkMode = false; // Light
|
||||
}
|
||||
else if (systemBG == "Auto")
|
||||
{
|
||||
// If systemBG is "Auto", decide based on ColorScheme
|
||||
if (colorScheme == "Dark")
|
||||
useDarkMode = true;
|
||||
else if (colorScheme == "Light")
|
||||
useDarkMode = false;
|
||||
else
|
||||
useDarkMode = isSystemDark; // Auto (based on system setting)
|
||||
}
|
||||
|
||||
// Apply DWM Dark Mode
|
||||
Win32Helper.DWMSetDarkModeForWindow(mainWindow, useDarkMode);
|
||||
|
||||
Color LightBG;
|
||||
Color DarkBG;
|
||||
|
||||
// Retrieve LightBG value (fallback to WindowBorderStyle background color if not found)
|
||||
try
|
||||
{
|
||||
LightBG = dict.Contains("LightBG") ? (Color)dict["LightBG"] : GetWindowBorderStyleBackground(theme);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
LightBG = GetWindowBorderStyleBackground(theme);
|
||||
}
|
||||
|
||||
// Retrieve DarkBG value (fallback to LightBG if not found)
|
||||
try
|
||||
{
|
||||
DarkBG = dict.Contains("DarkBG") ? (Color)dict["DarkBG"] : LightBG;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
DarkBG = LightBG;
|
||||
}
|
||||
|
||||
// Select background color based on ColorScheme and SystemBG
|
||||
Color selectedBG = useDarkMode ? DarkBG : LightBG;
|
||||
ApplyPreviewBackground(selectedBG);
|
||||
|
||||
bool isBlurAvailable = hasBlur && Win32Helper.IsBackdropSupported(); // Windows 11 미만이면 hasBlur를 강제 false
|
||||
|
||||
if (!isBlurAvailable)
|
||||
{
|
||||
mainWindow.Background = Brushes.Transparent;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only set the background to transparent if the theme supports blur
|
||||
if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt)
|
||||
{
|
||||
mainWindow.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
mainWindow.Background = new SolidColorBrush(selectedBG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBlurTheme()
|
||||
{
|
||||
if (!Win32Helper.IsBackdropSupported()) // Windows 11 미만이면 무조건 false
|
||||
return false;
|
||||
|
||||
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
||||
|
||||
return resource is bool b && b;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
using System;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
[Obsolete("ThemeManager.Instance is obsolete. Use Ioc.Default.GetRequiredService<Theme>() instead.")]
|
||||
public class ThemeManager
|
||||
{
|
||||
public static Theme Instance
|
||||
=> Ioc.Default.GetRequiredService<Theme>();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,25 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class TranslationConverter : IValueConverter
|
||||
{
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var key = value.ToString();
|
||||
if (String.IsNullOrEmpty(key))
|
||||
return key;
|
||||
return InternationalizationManager.Instance.GetTranslation(key);
|
||||
if (string.IsNullOrEmpty(key)) return key;
|
||||
return API.GetTranslation(key);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
Flow.Launcher.Core/Storage/IRemovable.cs
Normal file
19
Flow.Launcher.Core/Storage/IRemovable.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
namespace Flow.Launcher.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Remove storage instances from <see cref="Launcher.Plugin.IPublicAPI"/> instance
|
||||
/// </summary>
|
||||
public interface IRemovable
|
||||
{
|
||||
/// <summary>
|
||||
/// Remove all <see cref="Infrastructure.Storage.PluginJsonStorage{T}"/> instances of one plugin
|
||||
/// </summary>
|
||||
/// <param name="assemblyName"></param>
|
||||
public void RemovePluginSettings(string assemblyName);
|
||||
|
||||
/// <summary>
|
||||
/// Remove all <see cref="Infrastructure.Storage.PluginBinaryStorage{T}"/> instances of one plugin
|
||||
/// </summary>
|
||||
/// <param name="cacheDirectory"></param>
|
||||
public void RemovePluginCaches(string cacheDirectory);
|
||||
}
|
||||
|
|
@ -4,19 +4,18 @@ using System.Net;
|
|||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using JetBrains.Annotations;
|
||||
using Squirrel;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Squirrel;
|
||||
|
||||
namespace Flow.Launcher.Core
|
||||
{
|
||||
|
|
@ -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 <{newUpdateInfo.FutureReleaseEntry.Formatted()}>");
|
||||
_api.LogInfo(ClassName, $"Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
|
||||
|
||||
if (newReleaseVersion <= currentVersion)
|
||||
{
|
||||
|
|
@ -70,7 +71,7 @@ namespace Flow.Launcher.Core
|
|||
|
||||
if (DataLocation.PortableDataLocationInUse())
|
||||
{
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
|
||||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||
|
|
@ -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"),
|
||||
|
|
@ -122,14 +127,14 @@ namespace Flow.Launcher.Core
|
|||
}
|
||||
|
||||
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||
private static async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||
{
|
||||
var uri = new Uri(repository);
|
||||
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
|
||||
|
||||
await using var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false);
|
||||
|
||||
var releases = await System.Text.Json.JsonSerializer.DeserializeAsync<List<GithubRelease>>(jsonStream).ConfigureAwait(false);
|
||||
var releases = await JsonSerializer.DeserializeAsync<List<GithubRelease>>(jsonStream).ConfigureAwait(false);
|
||||
var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First();
|
||||
var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/");
|
||||
|
||||
|
|
@ -144,12 +149,21 @@ namespace Flow.Launcher.Core
|
|||
return manager;
|
||||
}
|
||||
|
||||
public string NewVersionTips(string version)
|
||||
private string NewVersionTips(string version)
|
||||
{
|
||||
var translator = InternationalizationManager.Instance;
|
||||
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
|
||||
var tips = string.Format(_api.GetTranslation("newVersionTips"), version);
|
||||
|
||||
return tips;
|
||||
}
|
||||
|
||||
private static string Formatted<T>(T t)
|
||||
{
|
||||
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
||||
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
||||
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
|
||||
public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png");
|
||||
|
||||
public static string PythonPath;
|
||||
public static string NodePath;
|
||||
|
|
@ -47,6 +48,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
public const string Themes = "Themes";
|
||||
public const string Settings = "Settings";
|
||||
public const string Logs = "Logs";
|
||||
public const string Cache = "Cache";
|
||||
|
||||
public const string Website = "https://flowlauncher.com";
|
||||
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
|
||||
|
|
|
|||
|
|
@ -67,8 +67,6 @@ namespace Flow.Launcher.Infrastructure
|
|||
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
||||
}
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,10 @@
|
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NLog" Version="4.7.10" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SharpVectors.Wpf" Version="1.8.4.2" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
|
||||
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
public static class Helper
|
||||
{
|
||||
static Helper()
|
||||
{
|
||||
jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
|
||||
/// </summary>
|
||||
|
|
@ -36,55 +28,5 @@ namespace Flow.Launcher.Infrastructure
|
|||
throw new NullReferenceException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
|
||||
{
|
||||
if (!Directory.Exists(dataDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
}
|
||||
|
||||
foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
|
||||
{
|
||||
var data = Path.GetFileName(bundledDataPath);
|
||||
var dataPath = Path.Combine(dataDirectory, data.NonNull());
|
||||
if (!File.Exists(dataPath))
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
|
||||
var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
|
||||
if (time1 != time2)
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
public static string Formatted<T>(this T t)
|
||||
{
|
||||
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IPublicAPI>().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
|
|||
/// <returns>The Http result as string. Null if cancellation requested</returns>
|
||||
public static Task<string> 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
|
|||
/// <returns>The Http result as string. Null if cancellation requested</returns>
|
||||
public static async Task<string> 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<Stream> GetStreamAsync([NotNull] string url,
|
||||
CancellationToken token = default) => GetStreamAsync(new Uri(url), token);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
|
||||
/// </summary>
|
||||
|
|
@ -191,7 +191,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
public static async Task<Stream> 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<HttpResponseMessage> 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,14 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
/// </summary>
|
||||
public static async Task<HttpResponseMessage> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using BitFaster.Caching.Lfu;
|
||||
|
|
@ -55,7 +53,6 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return image != null;
|
||||
}
|
||||
|
||||
|
||||
image = null;
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,19 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using static Flow.Launcher.Infrastructure.Http.Http;
|
||||
using SharpVectors.Converters;
|
||||
using SharpVectors.Renderers.Wpf;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
public static class ImageLoader
|
||||
{
|
||||
private static readonly string ClassName = nameof(ImageLoader);
|
||||
|
||||
private static readonly ImageCache ImageCache = new();
|
||||
private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
|
||||
private static BinaryStorage<List<(string, bool)>> _storage;
|
||||
|
|
@ -27,9 +29,10 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
||||
public const int SmallIconSize = 64;
|
||||
public const int FullIconSize = 256;
|
||||
|
||||
public const int FullImageSize = 320;
|
||||
|
||||
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
||||
private static readonly string SvgExtension = ".svg";
|
||||
|
||||
public static async Task InitializeAsync()
|
||||
{
|
||||
|
|
@ -37,6 +40,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
_hashGenerator = new ImageHashGenerator();
|
||||
|
||||
var usage = await LoadStorageToConcurrentDictionaryAsync();
|
||||
_storage.ClearData();
|
||||
|
||||
ImageCache.Initialize(usage);
|
||||
|
||||
|
|
@ -49,19 +53,18 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () =>
|
||||
await Stopwatch.InfoAsync(ClassName, "Preload images cost", async () =>
|
||||
{
|
||||
foreach (var (path, isFullImage) in usage)
|
||||
{
|
||||
await LoadAsync(path, isFullImage);
|
||||
}
|
||||
});
|
||||
Log.Info(
|
||||
$"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
|
||||
Log.Info(ClassName, $"Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task Save()
|
||||
public static async Task SaveAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
|
||||
|
|
@ -71,12 +74,22 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
.Select(x => x.Key)
|
||||
.ToList());
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, "Failed to save image cache to file", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WaitSaveAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
storageLock.Release();
|
||||
}
|
||||
|
||||
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
|
|
@ -158,8 +171,8 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
}
|
||||
catch (System.Exception e2)
|
||||
{
|
||||
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e);
|
||||
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2);
|
||||
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;
|
||||
|
|
@ -173,7 +186,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
||||
{
|
||||
// Download image from url
|
||||
await using var resp = await GetStreamAsync(uriResult);
|
||||
await using var resp = await Http.Http.GetStreamAsync(uriResult);
|
||||
await using var buffer = new MemoryStream();
|
||||
await resp.CopyToAsync(buffer);
|
||||
buffer.Seek(0, SeekOrigin.Begin);
|
||||
|
|
@ -221,10 +234,11 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
image = LoadFullImage(path);
|
||||
type = ImageType.FullImageFile;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
image = Image;
|
||||
type = ImageType.Error;
|
||||
Log.Exception(ClassName, $"Failed to load image file from path {path}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -237,6 +251,20 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
|
||||
}
|
||||
}
|
||||
else if (extension == SvgExtension)
|
||||
{
|
||||
try
|
||||
{
|
||||
image = LoadSvgImage(path, loadFullImage);
|
||||
type = ImageType.FullImageFile;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
image = Image;
|
||||
type = ImageType.Error;
|
||||
Log.Exception(ClassName, $"Failed to load SVG image from path {path}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
type = ImageType.File;
|
||||
|
|
@ -277,7 +305,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return ImageCache.TryGetValue(path, loadFullImage, out image);
|
||||
}
|
||||
|
||||
public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false)
|
||||
public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false, bool cacheImage = true)
|
||||
{
|
||||
var imageResult = await LoadInternalAsync(path, loadFullImage);
|
||||
|
||||
|
|
@ -293,22 +321,24 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
// image already exists
|
||||
img = ImageCache[key, loadFullImage] ?? img;
|
||||
}
|
||||
else
|
||||
else if (cacheImage)
|
||||
{
|
||||
// new guid
|
||||
|
||||
// save guid key
|
||||
GuidToKey[hash] = path;
|
||||
}
|
||||
}
|
||||
|
||||
// update cache
|
||||
ImageCache[path, loadFullImage] = img;
|
||||
if (cacheImage)
|
||||
{
|
||||
// update cache
|
||||
ImageCache[path, loadFullImage] = img;
|
||||
}
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
private static BitmapImage LoadFullImage(string path)
|
||||
private static ImageSource LoadFullImage(string path)
|
||||
{
|
||||
BitmapImage image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
|
|
@ -317,24 +347,24 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
image.EndInit();
|
||||
|
||||
if (image.PixelWidth > 320)
|
||||
if (image.PixelWidth > FullImageSize)
|
||||
{
|
||||
BitmapImage resizedWidth = new BitmapImage();
|
||||
resizedWidth.BeginInit();
|
||||
resizedWidth.CacheOption = BitmapCacheOption.OnLoad;
|
||||
resizedWidth.UriSource = new Uri(path);
|
||||
resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
resizedWidth.DecodePixelWidth = 320;
|
||||
resizedWidth.DecodePixelWidth = FullImageSize;
|
||||
resizedWidth.EndInit();
|
||||
|
||||
if (resizedWidth.PixelHeight > 320)
|
||||
if (resizedWidth.PixelHeight > FullImageSize)
|
||||
{
|
||||
BitmapImage resizedHeight = new BitmapImage();
|
||||
resizedHeight.BeginInit();
|
||||
resizedHeight.CacheOption = BitmapCacheOption.OnLoad;
|
||||
resizedHeight.UriSource = new Uri(path);
|
||||
resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
|
||||
resizedHeight.DecodePixelHeight = 320;
|
||||
resizedHeight.DecodePixelHeight = FullImageSize;
|
||||
resizedHeight.EndInit();
|
||||
return resizedHeight;
|
||||
}
|
||||
|
|
@ -344,5 +374,50 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
return image;
|
||||
}
|
||||
|
||||
private static ImageSource LoadSvgImage(string path, bool loadFullImage = false)
|
||||
{
|
||||
// Set up drawing settings
|
||||
var desiredHeight = loadFullImage ? FullImageSize : SmallIconSize;
|
||||
var drawingSettings = new WpfDrawingSettings
|
||||
{
|
||||
IncludeRuntime = true,
|
||||
// Set IgnoreRootViewbox to false to respect the SVG's viewBox
|
||||
IgnoreRootViewbox = false
|
||||
};
|
||||
|
||||
// Load and render the SVG
|
||||
var converter = new FileSvgReader(drawingSettings);
|
||||
var drawing = converter.Read(new Uri(path));
|
||||
|
||||
// Calculate scale to achieve desired height
|
||||
var drawingBounds = drawing.Bounds;
|
||||
if (drawingBounds.Height <= 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid SVG dimensions: Height must be greater than zero in {path}");
|
||||
}
|
||||
var scale = desiredHeight / drawingBounds.Height;
|
||||
var scaledWidth = drawingBounds.Width * scale;
|
||||
var scaledHeight = drawingBounds.Height * scale;
|
||||
|
||||
// Convert the Drawing to a Bitmap
|
||||
var drawingVisual = new DrawingVisual();
|
||||
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
|
||||
{
|
||||
drawingContext.PushTransform(new ScaleTransform(scale, scale));
|
||||
drawingContext.DrawDrawing(drawing);
|
||||
}
|
||||
|
||||
// Create a RenderTargetBitmap to hold the rendered image
|
||||
var bitmap = new RenderTargetBitmap(
|
||||
(int)Math.Ceiling(scaledWidth),
|
||||
(int)Math.Ceiling(scaledHeight),
|
||||
96, // DpiX
|
||||
96, // DpiY
|
||||
PixelFormats.Pbgra32);
|
||||
bitmap.Render(drawingVisual);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Windows.Win32.Graphics.Gdi;
|
|||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// Subclass of <see cref="Windows.Win32.UI.Shell.SIIGBF"/>
|
||||
/// Subclass of <see cref="SIIGBF"/>
|
||||
/// </summary>
|
||||
[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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using NLog;
|
||||
using NLog.Config;
|
||||
using NLog.Targets;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using NLog.Targets.Wrappers;
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Logger
|
||||
{
|
||||
public static class Log
|
||||
{
|
||||
public const string DirectoryName = "Logs";
|
||||
public const string DirectoryName = Constant.Logs;
|
||||
|
||||
public static string CurrentLogDirectory { get; }
|
||||
|
||||
static Log()
|
||||
{
|
||||
CurrentLogDirectory = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version);
|
||||
CurrentLogDirectory = DataLocation.VersionLogDirectory;
|
||||
if (!Directory.Exists(CurrentLogDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(CurrentLogDirectory);
|
||||
|
|
@ -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"
|
||||
/// <param name="message">Example: "|ClassName.MethodName|Message" </param>
|
||||
/// <param name="e">Exception</param>
|
||||
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
|
||||
|
|
|
|||
123
Flow.Launcher.Infrastructure/MonitorInfo.cs
Normal file
123
Flow.Launcher.Infrastructure/MonitorInfo.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Gdi;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Contains full information about a display monitor.
|
||||
/// Codes are edited from: <see href="https://github.com/Jack251970/DesktopWidgets3">.
|
||||
/// </summary>
|
||||
internal class MonitorInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the display monitors (including invisible pseudo-monitors associated with the mirroring drivers).
|
||||
/// </summary>
|
||||
/// <returns>A list of display monitors</returns>
|
||||
public static unsafe IList<MonitorInfo> GetDisplayMonitors()
|
||||
{
|
||||
var monitorCount = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CMONITORS);
|
||||
var list = new List<MonitorInfo>(monitorCount);
|
||||
var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
|
||||
{
|
||||
list.Add(new MonitorInfo(monitor, rect));
|
||||
return true;
|
||||
});
|
||||
var dwData = new LPARAM();
|
||||
var hdc = new HDC();
|
||||
bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
|
||||
if (!ok)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display monitor that is nearest to a given window.
|
||||
/// </summary>
|
||||
/// <param name="hwnd">Window handle</param>
|
||||
/// <returns>The display monitor that is nearest to a given window, or null if no monitor is found.</returns>
|
||||
public static unsafe MonitorInfo GetNearestDisplayMonitor(HWND hwnd)
|
||||
{
|
||||
var nearestMonitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||
MonitorInfo nearestMonitorInfo = null;
|
||||
var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
|
||||
{
|
||||
if (monitor == nearestMonitor)
|
||||
{
|
||||
nearestMonitorInfo = new MonitorInfo(monitor, rect);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
var dwData = new LPARAM();
|
||||
var hdc = new HDC();
|
||||
bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
|
||||
if (!ok)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
|
||||
}
|
||||
return nearestMonitorInfo;
|
||||
}
|
||||
|
||||
private readonly HMONITOR _monitor;
|
||||
|
||||
internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect)
|
||||
{
|
||||
RectMonitor =
|
||||
new Rect(new Point(rect->left, rect->top),
|
||||
new Point(rect->right, rect->bottom));
|
||||
_monitor = monitor;
|
||||
var info = new MONITORINFOEXW() { monitorInfo = new MONITORINFO() { cbSize = (uint)sizeof(MONITORINFOEXW) } };
|
||||
GetMonitorInfo(monitor, ref info);
|
||||
RectWork =
|
||||
new Rect(new Point(info.monitorInfo.rcWork.left, info.monitorInfo.rcWork.top),
|
||||
new Point(info.monitorInfo.rcWork.right, info.monitorInfo.rcWork.bottom));
|
||||
Name = new string(info.szDevice.AsSpan()).Replace("\0", "").Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the display.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display monitor rectangle, expressed in virtual-screen coordinates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <note>If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.</note>
|
||||
/// </remarks>
|
||||
public Rect RectMonitor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <note>If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.</note>
|
||||
/// </remarks>
|
||||
public Rect RectWork { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets if the monitor is the the primary display monitor.
|
||||
/// </summary>
|
||||
public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(IntPtr.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"{Name} {RectMonitor.Width}x{RectMonitor.Height}";
|
||||
|
||||
private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi)
|
||||
{
|
||||
fixed (MONITORINFOEXW* lpmiLocal = &lpmi)
|
||||
{
|
||||
var lpmiBase = (MONITORINFO*)lpmiLocal;
|
||||
var __result = PInvoke.GetMonitorInfo(hMonitor, lpmiBase);
|
||||
return __result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,58 @@ GetModuleHandle
|
|||
GetKeyState
|
||||
VIRTUAL_KEY
|
||||
|
||||
WM_KEYDOWN
|
||||
WM_KEYUP
|
||||
WM_SYSKEYDOWN
|
||||
WM_SYSKEYUP
|
||||
EnumWindows
|
||||
|
||||
EnumWindows
|
||||
DwmSetWindowAttribute
|
||||
DWM_SYSTEMBACKDROP_TYPE
|
||||
DWM_WINDOW_CORNER_PREFERENCE
|
||||
|
||||
MAX_PATH
|
||||
SystemParametersInfo
|
||||
|
||||
SetForegroundWindow
|
||||
|
||||
WINDOW_LONG_PTR_INDEX
|
||||
GetForegroundWindow
|
||||
GetDesktopWindow
|
||||
GetShellWindow
|
||||
GetWindowRect
|
||||
GetClassName
|
||||
FindWindowEx
|
||||
WINDOW_STYLE
|
||||
|
||||
SetLastError
|
||||
WINDOW_EX_STYLE
|
||||
|
||||
GetSystemMetrics
|
||||
EnumDisplayMonitors
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
MONITORINFOEXW
|
||||
|
||||
WM_ENTERSIZEMOVE
|
||||
WM_EXITSIZEMOVE
|
||||
WM_NCLBUTTONDBLCLK
|
||||
WM_SYSCOMMAND
|
||||
|
||||
SC_MAXIMIZE
|
||||
SC_MINIMIZE
|
||||
|
||||
OleInitialize
|
||||
OleUninitialize
|
||||
|
||||
GetKeyboardLayout
|
||||
GetWindowThreadProcessId
|
||||
ActivateKeyboardLayout
|
||||
GetKeyboardLayoutList
|
||||
PostMessage
|
||||
WM_INPUTLANGCHANGEREQUEST
|
||||
INPUTLANGCHANGE_FORWARD
|
||||
LOCALE_TRANSIENT_KEYBOARD1
|
||||
LOCALE_TRANSIENT_KEYBOARD2
|
||||
LOCALE_TRANSIENT_KEYBOARD3
|
||||
LOCALE_TRANSIENT_KEYBOARD4
|
||||
|
||||
SHParseDisplayName
|
||||
SHOpenFolderAndSelectItems
|
||||
CoTaskMemFree
|
||||
|
|
|
|||
45
Flow.Launcher.Infrastructure/PInvokeExtensions.cs
Normal file
45
Flow.Launcher.Infrastructure/PInvokeExtensions.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Windows.Win32;
|
||||
|
||||
internal static partial class PInvoke
|
||||
{
|
||||
// SetWindowLong
|
||||
// Edited from: https://github.com/files-community/Files
|
||||
|
||||
[DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)]
|
||||
private static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)]
|
||||
private static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong);
|
||||
|
||||
// NOTE:
|
||||
// CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa.
|
||||
// For more info, visit https://github.com/microsoft/CsWin32/issues/882
|
||||
public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong)
|
||||
{
|
||||
return sizeof(nint) is 4
|
||||
? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong)
|
||||
: _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
||||
|
|
@ -7,91 +7,54 @@ namespace Flow.Launcher.Infrastructure
|
|||
{
|
||||
public static class Stopwatch
|
||||
{
|
||||
private static readonly Dictionary<string, long> Count = new Dictionary<string, long>();
|
||||
private static readonly object Locker = new object();
|
||||
/// <summary>
|
||||
/// This stopwatch will appear only in Debug mode
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This stopwatch will appear only in Debug mode
|
||||
/// </summary>
|
||||
public static async Task<long> DebugAsync(string message, Func<Task> action)
|
||||
public static async Task<long> DebugAsync(string className, string message, Func<Task> 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<long> NormalAsync(string message, Func<Task> action)
|
||||
public static async Task<long> InfoAsync(string className, string message, Func<Task> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void StartCount(string name, Action action)
|
||||
{
|
||||
var stopWatch = new System.Diagnostics.Stopwatch();
|
||||
stopWatch.Start();
|
||||
action();
|
||||
stopWatch.Stop();
|
||||
var milliseconds = stopWatch.ElapsedMilliseconds;
|
||||
lock (Locker)
|
||||
{
|
||||
if (Count.ContainsKey(name))
|
||||
{
|
||||
Count[name] += milliseconds;
|
||||
}
|
||||
else
|
||||
{
|
||||
Count[name] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void EndCount()
|
||||
{
|
||||
foreach (var key in Count.Keys)
|
||||
{
|
||||
string info = $"{key} already cost {Count[key]}ms";
|
||||
Log.Debug(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using MemoryPack;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -16,64 +16,111 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
/// Normally, it has better performance, but not readable
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable
|
||||
/// https://github.com/Cysharp/MemoryPack
|
||||
/// It utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
|
||||
/// </remarks>
|
||||
public class BinaryStorage<T>
|
||||
public class BinaryStorage<T> : ISavable
|
||||
{
|
||||
const string DirectoryName = "Cache";
|
||||
private static readonly string ClassName = "BinaryStorage";
|
||||
|
||||
const string FileSuffix = ".cache";
|
||||
protected T? Data;
|
||||
|
||||
public const string FileSuffix = ".cache";
|
||||
|
||||
protected string FilePath { get; init; } = null!;
|
||||
|
||||
protected string DirectoryPath { get; init; } = null!;
|
||||
|
||||
// Let the derived class to set the file path
|
||||
protected BinaryStorage()
|
||||
{
|
||||
}
|
||||
|
||||
public BinaryStorage(string filename)
|
||||
{
|
||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||
Helper.ValidateDirectory(directoryPath);
|
||||
DirectoryPath = DataLocation.CacheDirectory;
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
|
||||
}
|
||||
|
||||
public string FilePath { get; }
|
||||
// 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<T> TryLoadAsync(T defaultData)
|
||||
{
|
||||
if (Data != null) return Data;
|
||||
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
if (new FileInfo(FilePath).Length == 0)
|
||||
{
|
||||
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>");
|
||||
await SaveAsync(defaultData);
|
||||
return defaultData;
|
||||
Log.Error(ClassName, $"Zero length cache file <{FilePath}>");
|
||||
Data = defaultData;
|
||||
await SaveAsync();
|
||||
}
|
||||
|
||||
await using var stream = new FileStream(FilePath, FileMode.Open);
|
||||
var d = await DeserializeAsync(stream, defaultData);
|
||||
return d;
|
||||
Data = await DeserializeAsync(stream, defaultData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data");
|
||||
await SaveAsync(defaultData);
|
||||
return defaultData;
|
||||
Log.Info(ClassName, "Cache file not exist, load default data");
|
||||
Data = defaultData;
|
||||
await SaveAsync();
|
||||
}
|
||||
|
||||
return Data;
|
||||
}
|
||||
|
||||
private async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
|
||||
private static async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
|
||||
return t;
|
||||
return t ?? defaultData;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
catch (System.Exception)
|
||||
{
|
||||
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
// User may delete the directory, so we need to check it
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
var serialized = MemoryPackSerializer.Serialize(Data);
|
||||
File.WriteAllBytes(FilePath, serialized);
|
||||
}
|
||||
|
||||
public async ValueTask SaveAsync()
|
||||
{
|
||||
await SaveAsync(Data.NonNull());
|
||||
}
|
||||
|
||||
// ImageCache need to convert data into concurrent dictionary for usage,
|
||||
// so we would better to clear the data
|
||||
public void ClearData()
|
||||
{
|
||||
Data = default;
|
||||
}
|
||||
|
||||
// ImageCache storages data in its class,
|
||||
// so we need to pass it to SaveAsync
|
||||
public async ValueTask SaveAsync(T data)
|
||||
{
|
||||
// User may delete the directory, so we need to check it
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
await using var stream = new FileStream(FilePath, FileMode.Create);
|
||||
await MemoryPackSerializer.SerializeAsync(stream, data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,46 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
|
||||
{
|
||||
private static readonly string ClassName = "FlowLauncherJsonStorage";
|
||||
|
||||
public FlowLauncherJsonStorage()
|
||||
{
|
||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||
Helper.ValidateDirectory(directoryPath);
|
||||
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
var filename = typeof(T).Name;
|
||||
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
|
||||
}
|
||||
|
||||
public new void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Save();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public new async Task SaveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await base.SaveAsync();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize object using json format.
|
||||
/// </summary>
|
||||
public class JsonStorage<T> where T : new()
|
||||
public class JsonStorage<T> : ISavable where T : new()
|
||||
{
|
||||
private static readonly string ClassName = "JsonStorage";
|
||||
|
||||
protected T? Data;
|
||||
|
||||
// need a new directory name
|
||||
public const string DirectoryName = "Settings";
|
||||
public const string DirectoryName = Constant.Settings;
|
||||
public const string FileSuffix = ".json";
|
||||
|
||||
protected string FilePath { get; init; } = null!;
|
||||
|
|
@ -37,7 +42,23 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
FilePath = filePath;
|
||||
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
|
||||
|
||||
Helper.ValidateDirectory(DirectoryPath);
|
||||
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<T> LoadAsync()
|
||||
|
|
@ -101,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);
|
||||
|
|
@ -180,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);
|
||||
|
|
@ -190,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 });
|
||||
|
|
|
|||
44
Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs
Normal file
44
Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
public class PluginBinaryStorage<T> : BinaryStorage<T> where T : new()
|
||||
{
|
||||
private static readonly string ClassName = "PluginBinaryStorage";
|
||||
|
||||
public PluginBinaryStorage(string cacheName, string cacheDirectory)
|
||||
{
|
||||
DirectoryPath = cacheDirectory;
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
FilePath = Path.Combine(DirectoryPath, $"{cacheName}{FileSuffix}");
|
||||
}
|
||||
|
||||
public new void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Save();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public new async Task SaveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await base.SaveAsync();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Failed to save plugin caches to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
|
|
@ -8,13 +11,15 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
// Use assembly name to check which plugin is using this storage
|
||||
public readonly string AssemblyName;
|
||||
|
||||
private static readonly string ClassName = "PluginJsonStorage";
|
||||
|
||||
public PluginJsonStorage()
|
||||
{
|
||||
// C# related, add python related below
|
||||
var dataType = typeof(T);
|
||||
AssemblyName = dataType.Assembly.GetName().Name;
|
||||
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName);
|
||||
Helper.ValidateDirectory(DirectoryPath);
|
||||
DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName);
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
||||
}
|
||||
|
|
@ -24,11 +29,27 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
Data = data;
|
||||
}
|
||||
|
||||
public void DeleteDirectory()
|
||||
public new void Save()
|
||||
{
|
||||
if (Directory.Exists(DirectoryPath))
|
||||
try
|
||||
{
|
||||
Directory.Delete(DirectoryPath, true);
|
||||
base.Save();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public new async Task SaveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await base.SaveAsync();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Windows.Markup;
|
|||
|
||||
namespace Flow.Launcher.Infrastructure.UI
|
||||
{
|
||||
[Obsolete("EnumBindingSourceExtension is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
|
||||
public class EnumBindingSourceExtension : MarkupExtension
|
||||
{
|
||||
private Type _enumType;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
#region Base
|
||||
|
||||
public abstract class ShortcutBaseModel
|
||||
{
|
||||
public string Key { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Func<string> Expand { get; set; } = () => { return ""; };
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ShortcutBaseModel other &&
|
||||
|
|
@ -22,16 +22,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 +38,69 @@ 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<string> expand)
|
||||
public BaseBuiltinShortcutModel(string key, string description)
|
||||
{
|
||||
Key = key;
|
||||
Description = description;
|
||||
Expand = expand ?? (() => { return ""; });
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Custom Shortcut
|
||||
|
||||
public class CustomShortcutModel : BaseCustomShortcutModel
|
||||
{
|
||||
[JsonIgnore]
|
||||
public Func<string> 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<string> Expand { get; set; } = () => { return string.Empty; };
|
||||
|
||||
public BuiltinShortcutModel(string key, string description, Func<string> expand) : base(key, description)
|
||||
{
|
||||
Expand = expand ?? (() => { return string.Empty; });
|
||||
}
|
||||
}
|
||||
|
||||
public class AsyncBuiltinShortcutModel : BaseBuiltinShortcutModel
|
||||
{
|
||||
[JsonIgnore]
|
||||
public Func<Task<string>> ExpandAsync { get; set; } = () => { return Task.FromResult(string.Empty); };
|
||||
|
||||
public AsyncBuiltinShortcutModel(string key, string description, Func<Task<string>> expandAsync) : base(key, description)
|
||||
{
|
||||
ExpandAsync = expandAsync ?? (() => { return Task.FromResult(string.Empty); });
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
return false;
|
||||
}
|
||||
|
||||
public static string VersionLogDirectory => Path.Combine(LogDirectory, Constant.Version);
|
||||
public static string LogDirectory => Path.Combine(DataDirectory(), Constant.Logs);
|
||||
|
||||
public static readonly string CacheDirectory = Path.Combine(DataDirectory(), Constant.Cache);
|
||||
public static readonly string SettingsDirectory = Path.Combine(DataDirectory(), Constant.Settings);
|
||||
public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins);
|
||||
public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins);
|
||||
public static readonly string ThemesDirectory = Path.Combine(DataDirectory(), Constant.Themes);
|
||||
|
||||
public static readonly string PluginSettingsDirectory = Path.Combine(SettingsDirectory, Constant.Plugins);
|
||||
public static readonly string PluginCacheDirectory = Path.Combine(DataDirectory(), Constant.Cache, Constant.Plugins);
|
||||
|
||||
public const string PythonEnvironmentName = "Python";
|
||||
public const string NodeEnvironmentName = "Node.js";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
|
|
@ -6,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public class PluginsSettings : BaseModel
|
||||
{
|
||||
private string pythonExecutablePath = string.Empty;
|
||||
public string PythonExecutablePath {
|
||||
get { return pythonExecutablePath; }
|
||||
public string PythonExecutablePath
|
||||
{
|
||||
get => pythonExecutablePath;
|
||||
set
|
||||
{
|
||||
pythonExecutablePath = value;
|
||||
|
|
@ -18,7 +20,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
private string nodeExecutablePath = string.Empty;
|
||||
public string NodeExecutablePath
|
||||
{
|
||||
get { return nodeExecutablePath; }
|
||||
get => nodeExecutablePath;
|
||||
set
|
||||
{
|
||||
nodeExecutablePath = value;
|
||||
|
|
@ -26,19 +28,32 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, Plugin> Plugins { get; set; } = new Dictionary<string, Plugin>();
|
||||
/// <summary>
|
||||
/// Only used for serialization
|
||||
/// </summary>
|
||||
public Dictionary<string, Plugin> Plugins { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Update plugin settings with metadata.
|
||||
/// FL will get default values from metadata first and then load settings to metadata
|
||||
/// </summary>
|
||||
/// <param name="metadatas">Parsed plugin metadatas</param>
|
||||
public void UpdatePluginSettings(List<PluginMetadata> metadatas)
|
||||
{
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
if (Plugins.ContainsKey(metadata.ID))
|
||||
if (Plugins.TryGetValue(metadata.ID, out var settings))
|
||||
{
|
||||
var settings = Plugins[metadata.ID];
|
||||
|
||||
// If settings exist, update settings & metadata value
|
||||
// update settings values with metadata
|
||||
if (string.IsNullOrEmpty(settings.Version))
|
||||
{
|
||||
settings.Version = metadata.Version;
|
||||
}
|
||||
settings.DefaultActionKeywords = metadata.ActionKeywords; // metadata provides default values
|
||||
settings.DefaultSearchDelayTime = metadata.SearchDelayTime; // metadata provides default values
|
||||
|
||||
// update metadata values with settings
|
||||
if (settings.ActionKeywords?.Count > 0)
|
||||
{
|
||||
metadata.ActionKeywords = settings.ActionKeywords;
|
||||
|
|
@ -51,33 +66,70 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
metadata.Disabled = settings.Disabled;
|
||||
metadata.Priority = settings.Priority;
|
||||
metadata.SearchDelayTime = settings.SearchDelayTime;
|
||||
metadata.HomeDisabled = settings.HomeDisabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If settings does not exist, create a new one
|
||||
Plugins[metadata.ID] = new Plugin
|
||||
{
|
||||
ID = metadata.ID,
|
||||
Name = metadata.Name,
|
||||
Version = metadata.Version,
|
||||
ActionKeywords = metadata.ActionKeywords,
|
||||
DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values
|
||||
ActionKeywords = metadata.ActionKeywords, // use default value
|
||||
Disabled = metadata.Disabled,
|
||||
Priority = metadata.Priority
|
||||
HomeDisabled = metadata.HomeDisabled,
|
||||
Priority = metadata.Priority,
|
||||
DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values
|
||||
SearchDelayTime = metadata.SearchDelayTime, // use default value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Plugin GetPluginSettings(string id)
|
||||
{
|
||||
if (Plugins.TryGetValue(id, out var plugin))
|
||||
{
|
||||
return plugin;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Plugin RemovePluginSettings(string id)
|
||||
{
|
||||
Plugins.Remove(id, out var plugin);
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
|
||||
public class Plugin
|
||||
{
|
||||
public string ID { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Version { get; set; }
|
||||
public List<string> ActionKeywords { get; set; } // a reference of the action keywords from plugin manager
|
||||
|
||||
[JsonIgnore]
|
||||
public List<string> DefaultActionKeywords { get; set; }
|
||||
|
||||
// a reference of the action keywords from plugin manager
|
||||
public List<string> ActionKeywords { get; set; }
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int? DefaultSearchDelayTime { get; set; }
|
||||
|
||||
public int? SearchDelayTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used only to save the state of the plugin in settings
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
public bool HomeDisabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +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;
|
||||
|
|
@ -51,15 +50,17 @@ 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -68,30 +69,32 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
get => _theme;
|
||||
set
|
||||
{
|
||||
if (value == _theme)
|
||||
return;
|
||||
_theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(MaxResultsToShow));
|
||||
if (value != _theme)
|
||||
{
|
||||
_theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(MaxResultsToShow));
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool UseDropShadowEffect { get; set; } = true;
|
||||
public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None;
|
||||
|
||||
/* Appearance Settings. It should be separated from the setting later.*/
|
||||
public double WindowHeightSize { get; set; } = 42;
|
||||
public double ItemHeightSize { get; set; } = 58;
|
||||
public double QueryBoxFontSize { get; set; } = 20;
|
||||
public double 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; }
|
||||
|
|
@ -99,6 +102,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;
|
||||
|
|
@ -110,7 +131,64 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public double SettingWindowHeight { get; set; } = 700;
|
||||
public double? SettingWindowTop { get; set; } = null;
|
||||
public double? SettingWindowLeft { get; set; } = null;
|
||||
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
|
||||
public WindowState SettingWindowState { get; set; } = WindowState.Normal;
|
||||
|
||||
private bool _showPlaceholder { get; set; } = true;
|
||||
public bool ShowPlaceholder
|
||||
{
|
||||
get => _showPlaceholder;
|
||||
set
|
||||
{
|
||||
if (_showPlaceholder != value)
|
||||
{
|
||||
_showPlaceholder = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
private string _placeholderText { get; set; } = string.Empty;
|
||||
public string PlaceholderText
|
||||
{
|
||||
get => _placeholderText;
|
||||
set
|
||||
{
|
||||
if (_placeholderText != value)
|
||||
{
|
||||
_placeholderText = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -149,8 +227,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
new()
|
||||
{
|
||||
Name = "Files",
|
||||
Path = "Files",
|
||||
DirectoryArgument = "-select \"%d\"",
|
||||
Path = "Files-Stable",
|
||||
DirectoryArgument = "\"%d\"",
|
||||
FileArgument = "-select \"%f\""
|
||||
}
|
||||
};
|
||||
|
|
@ -229,6 +307,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; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom left position on selected monitor
|
||||
|
|
@ -240,19 +322,35 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
/// </summary>
|
||||
public double CustomWindowTop { get; set; } = 0;
|
||||
|
||||
public bool KeepMaxResults { get; set; } = false;
|
||||
public int MaxResultsToShow { get; set; } = 5;
|
||||
public int ActivateTimes { get; set; }
|
||||
/// <summary>
|
||||
/// Fixed window size
|
||||
/// </summary>
|
||||
private bool _keepMaxResults { get; set; } = false;
|
||||
public bool KeepMaxResults
|
||||
{
|
||||
get => _keepMaxResults;
|
||||
set
|
||||
{
|
||||
if (_keepMaxResults != value)
|
||||
{
|
||||
_keepMaxResults = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int MaxResultsToShow { get; set; } = 5;
|
||||
|
||||
public int ActivateTimes { get; set; }
|
||||
|
||||
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
|
||||
|
||||
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>();
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
|
||||
public ObservableCollection<BaseBuiltinShortcutModel> 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)
|
||||
};
|
||||
|
||||
|
|
@ -262,10 +360,10 @@ 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 { return _hideNotifyIcon; }
|
||||
get => _hideNotifyIcon;
|
||||
set
|
||||
{
|
||||
_hideNotifyIcon = value;
|
||||
|
|
@ -275,6 +373,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public bool LeaveCmdOpen { get; set; }
|
||||
public bool HideWhenDeactivated { get; set; } = true;
|
||||
|
||||
public bool SearchQueryResultsWithDelay { get; set; }
|
||||
public int SearchDelayTime { get; set; } = 150;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
|
||||
|
||||
|
|
@ -310,29 +411,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 = ""));
|
||||
|
|
@ -363,7 +466,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"),
|
||||
|
|
@ -431,4 +533,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
Fast,
|
||||
Custom
|
||||
}
|
||||
|
||||
public enum BackdropTypes
|
||||
{
|
||||
None,
|
||||
Acrylic,
|
||||
Mica,
|
||||
MicaAlt
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,27 @@
|
|||
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.Windows.Interop;
|
||||
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;
|
||||
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
|
||||
{
|
||||
|
|
@ -9,93 +29,767 @@ namespace Flow.Launcher.Infrastructure
|
|||
{
|
||||
#region Blur Handling
|
||||
|
||||
/*
|
||||
Found on https://github.com/riverar/sample-win10-aeroglass
|
||||
*/
|
||||
|
||||
private enum AccentState
|
||||
public static bool IsBackdropSupported()
|
||||
{
|
||||
ACCENT_DISABLED = 0,
|
||||
ACCENT_ENABLE_GRADIENT = 1,
|
||||
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
|
||||
ACCENT_ENABLE_BLURBEHIND = 3,
|
||||
ACCENT_INVALID_STATE = 4
|
||||
// Mica and Acrylic only supported Windows 11 22000+
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
Environment.OSVersion.Version.Build >= 22000;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct AccentPolicy
|
||||
public static unsafe bool DWMSetCloakForWindow(Window window, bool cloak)
|
||||
{
|
||||
public AccentState AccentState;
|
||||
public int AccentFlags;
|
||||
public int GradientColor;
|
||||
public int AnimationId;
|
||||
var cloaked = cloak ? 1 : 0;
|
||||
|
||||
return PInvoke.DwmSetWindowAttribute(
|
||||
GetWindowHandle(window),
|
||||
DWMWINDOWATTRIBUTE.DWMWA_CLOAK,
|
||||
&cloaked,
|
||||
(uint)Marshal.SizeOf<int>()).Succeeded;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WindowCompositionAttributeData
|
||||
public static unsafe bool DWMSetBackdropForWindow(Window window, BackdropTypes backdrop)
|
||||
{
|
||||
public WindowCompositionAttribute Attribute;
|
||||
public IntPtr Data;
|
||||
public int SizeOfData;
|
||||
var backdropType = backdrop switch
|
||||
{
|
||||
BackdropTypes.Acrylic => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TRANSIENTWINDOW,
|
||||
BackdropTypes.Mica => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_MAINWINDOW,
|
||||
BackdropTypes.MicaAlt => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TABBEDWINDOW,
|
||||
_ => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_AUTO
|
||||
};
|
||||
|
||||
return PInvoke.DwmSetWindowAttribute(
|
||||
GetWindowHandle(window),
|
||||
DWMWINDOWATTRIBUTE.DWMWA_SYSTEMBACKDROP_TYPE,
|
||||
&backdropType,
|
||||
(uint)Marshal.SizeOf<int>()).Succeeded;
|
||||
}
|
||||
|
||||
private enum WindowCompositionAttribute
|
||||
public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMode)
|
||||
{
|
||||
WCA_ACCENT_POLICY = 19
|
||||
}
|
||||
var darkMode = useDarkMode ? 1 : 0;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
|
||||
return PInvoke.DwmSetWindowAttribute(
|
||||
GetWindowHandle(window),
|
||||
DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&darkMode,
|
||||
(uint)Marshal.SizeOf<int>()).Succeeded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the blur theme is enabled
|
||||
///
|
||||
/// </summary>
|
||||
public static bool IsBlurTheme()
|
||||
/// <param name="window"></param>
|
||||
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
|
||||
/// <returns></returns>
|
||||
public static unsafe bool DWMSetCornerPreferenceForWindow(Window window, string cornerType)
|
||||
{
|
||||
if (Environment.OSVersion.Version >= new Version(6, 2))
|
||||
var preference = cornerType switch
|
||||
{
|
||||
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
||||
"DoNotRound" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND,
|
||||
"Round" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND,
|
||||
"RoundSmall" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUNDSMALL,
|
||||
"Default" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DEFAULT,
|
||||
_ => throw new InvalidOperationException("Invalid corner type")
|
||||
};
|
||||
|
||||
if (resource is bool b)
|
||||
return b;
|
||||
return PInvoke.DwmSetWindowAttribute(
|
||||
GetWindowHandle(window),
|
||||
DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE,
|
||||
&preference,
|
||||
(uint)Marshal.SizeOf<int>()).Succeeded;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Wallpaper
|
||||
|
||||
public static unsafe string GetWallpaperPath()
|
||||
{
|
||||
var wallpaperPtr = stackalloc char[(int)PInvoke.MAX_PATH];
|
||||
PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, PInvoke.MAX_PATH,
|
||||
wallpaperPtr,
|
||||
0);
|
||||
var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr);
|
||||
|
||||
return wallpaper.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Window Foreground
|
||||
|
||||
public static nint GetForegroundWindow()
|
||||
{
|
||||
return PInvoke.GetForegroundWindow().Value;
|
||||
}
|
||||
|
||||
public static bool SetForegroundWindow(Window window)
|
||||
{
|
||||
return PInvoke.SetForegroundWindow(GetWindowHandle(window));
|
||||
}
|
||||
|
||||
public static bool SetForegroundWindow(nint handle)
|
||||
{
|
||||
return PInvoke.SetForegroundWindow(new(handle));
|
||||
}
|
||||
|
||||
public static bool IsForegroundWindow(Window window)
|
||||
{
|
||||
return IsForegroundWindow(GetWindowHandle(window));
|
||||
}
|
||||
|
||||
internal static bool IsForegroundWindow(HWND handle)
|
||||
{
|
||||
return handle.Equals(PInvoke.GetForegroundWindow());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task Switching
|
||||
|
||||
/// <summary>
|
||||
/// Hide windows in the Alt+Tab window list
|
||||
/// </summary>
|
||||
/// <param name="window">To hide a window</param>
|
||||
public static void HideFromAltTab(Window window)
|
||||
{
|
||||
var hwnd = GetWindowHandle(window);
|
||||
|
||||
var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
||||
|
||||
// Add TOOLWINDOW style, remove APPWINDOW style
|
||||
var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
|
||||
|
||||
SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore window display in the Alt+Tab window list.
|
||||
/// </summary>
|
||||
/// <param name="window">To restore the displayed window</param>
|
||||
public static void ShowInAltTab(Window window)
|
||||
{
|
||||
var hwnd = GetWindowHandle(window);
|
||||
|
||||
var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
||||
|
||||
// Remove the TOOLWINDOW style and add the APPWINDOW style.
|
||||
var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
|
||||
|
||||
SetWindowStyle(GetWindowHandle(window), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable windows toolbar's control box
|
||||
/// This will also disable system menu with Alt+Space hotkey
|
||||
/// </summary>
|
||||
public static void DisableControlBox(Window window)
|
||||
{
|
||||
var hwnd = GetWindowHandle(window);
|
||||
|
||||
var style = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
|
||||
|
||||
style &= ~(int)WINDOW_STYLE.WS_SYSMENU;
|
||||
|
||||
SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style);
|
||||
}
|
||||
|
||||
private static nint GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex)
|
||||
{
|
||||
var style = PInvoke.GetWindowLongPtr(hWnd, nIndex);
|
||||
if (style == 0 && Marshal.GetLastPInvokeError() != 0)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastPInvokeError());
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong)
|
||||
{
|
||||
PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error
|
||||
|
||||
var result = PInvoke.SetWindowLongPtr(hWnd, nIndex, dwNewLong);
|
||||
if (result == 0 && Marshal.GetLastPInvokeError() != 0)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastPInvokeError());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Window Fullscreen
|
||||
|
||||
private const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
|
||||
private const string WINDOW_CLASS_WINTAB = "Flip3D";
|
||||
private const string WINDOW_CLASS_PROGMAN = "Progman";
|
||||
private const string WINDOW_CLASS_WORKERW = "WorkerW";
|
||||
|
||||
private static HWND _hwnd_shell;
|
||||
private static HWND HWND_SHELL =>
|
||||
_hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow();
|
||||
|
||||
private static HWND _hwnd_desktop;
|
||||
private static HWND HWND_DESKTOP =>
|
||||
_hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow();
|
||||
|
||||
public static unsafe bool IsForegroundWindowFullscreen()
|
||||
{
|
||||
// Get current active window
|
||||
var hWnd = PInvoke.GetForegroundWindow();
|
||||
if (hWnd.Equals(HWND.Null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If current active window is desktop or shell, exit early
|
||||
if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string windowClass;
|
||||
const int capacity = 256;
|
||||
Span<char> buffer = stackalloc char[capacity];
|
||||
int validLength;
|
||||
fixed (char* pBuffer = buffer)
|
||||
{
|
||||
validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity);
|
||||
}
|
||||
|
||||
windowClass = buffer[..validLength].ToString();
|
||||
|
||||
// For Win+Tab (Flip3D)
|
||||
if (windowClass == WINDOW_CLASS_WINTAB)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PInvoke.GetWindowRect(hWnd, out var appBounds);
|
||||
|
||||
// For console (ConsoleWindowClass), we have to check for negative dimensions
|
||||
if (windowClass == WINDOW_CLASS_CONSOLE)
|
||||
{
|
||||
return appBounds.top < 0 && appBounds.bottom < 0;
|
||||
}
|
||||
|
||||
// For desktop (Progman or WorkerW, depends on the system), we have to check
|
||||
if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW)
|
||||
{
|
||||
var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null);
|
||||
hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView");
|
||||
if (hWndDesktop.Value != IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var monitorInfo = MonitorInfo.GetNearestDisplayMonitor(hWnd);
|
||||
return (appBounds.bottom - appBounds.top) == monitorInfo.RectMonitor.Height &&
|
||||
(appBounds.right - appBounds.left) == monitorInfo.RectMonitor.Width;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pixel to DIP
|
||||
|
||||
/// <summary>
|
||||
/// Transforms pixels to Device Independent Pixels used by WPF
|
||||
/// </summary>
|
||||
/// <param name="visual">current window, required to get presentation source</param>
|
||||
/// <param name="unitX">horizontal position in pixels</param>
|
||||
/// <param name="unitY">vertical position in pixels</param>
|
||||
/// <returns>point containing device independent pixels</returns>
|
||||
public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
|
||||
{
|
||||
Matrix matrix;
|
||||
var source = PresentationSource.FromVisual(visual);
|
||||
if (source is not null)
|
||||
{
|
||||
matrix = source.CompositionTarget.TransformFromDevice;
|
||||
}
|
||||
else
|
||||
{
|
||||
using var src = new HwndSource(new HwndSourceParameters());
|
||||
matrix = src.CompositionTarget.TransformFromDevice;
|
||||
}
|
||||
|
||||
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WndProc
|
||||
|
||||
public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE;
|
||||
public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE;
|
||||
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
|
||||
|
||||
#region Window Handle
|
||||
|
||||
internal static HWND GetWindowHandle(Window window, bool ensure = false)
|
||||
{
|
||||
var windowHelper = new WindowInteropHelper(window);
|
||||
if (ensure)
|
||||
{
|
||||
windowHelper.EnsureHandle();
|
||||
}
|
||||
return new(windowHelper.Handle);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 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<T> StartSTATaskAsync<T>(Func<T> func)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||
|
||||
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";
|
||||
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
|
||||
private const string EnglishLanguageTag = "en";
|
||||
|
||||
private static readonly string[] ImeLanguageTags =
|
||||
{
|
||||
"zh", // Chinese
|
||||
"ja", // Japanese
|
||||
"ko", // Korean
|
||||
};
|
||||
|
||||
private const uint KeyboardLayoutLoWord = 0xFFFF;
|
||||
|
||||
// Store the previous keyboard layout
|
||||
private static HKL _previousLayout;
|
||||
|
||||
/// <summary>
|
||||
/// Switches the keyboard layout to English if available.
|
||||
/// </summary>
|
||||
/// <param name="backupPrevious">If true, the current keyboard layout will be stored for later restoration.</param>
|
||||
/// <exception cref="Win32Exception">Thrown when there's an error getting the window thread process ID.</exception>
|
||||
public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious)
|
||||
{
|
||||
// Find an installed English layout
|
||||
var enHKL = FindEnglishKeyboardLayout();
|
||||
|
||||
// No installed English layout found
|
||||
if (enHKL == HKL.Null) return;
|
||||
|
||||
// Get the foreground window
|
||||
var hwnd = PInvoke.GetForegroundWindow();
|
||||
if (hwnd == HWND.Null) return;
|
||||
|
||||
// Get the current foreground window thread ID
|
||||
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
|
||||
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
|
||||
// If the current layout has an IME mode, disable it without switching to another layout.
|
||||
// This is needed because for languages with IME mode, Flow Launcher just temporarily disables
|
||||
// the IME mode instead of switching to another layout.
|
||||
var currentLayout = PInvoke.GetKeyboardLayout(threadId);
|
||||
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
|
||||
foreach (var imeLangTag in ImeLanguageTags)
|
||||
{
|
||||
var langTag = GetLanguageTag(currentLangId);
|
||||
if (langTag.StartsWith(imeLangTag, StringComparison.OrdinalIgnoreCase)) return;
|
||||
}
|
||||
|
||||
// Backup current keyboard layout
|
||||
if (backupPrevious) _previousLayout = currentLayout;
|
||||
|
||||
// Switch to English layout
|
||||
PInvoke.ActivateKeyboardLayout(enHKL, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the previously backed-up keyboard layout.
|
||||
/// If it wasn't backed up or has already been restored, this method does nothing.
|
||||
/// </summary>
|
||||
public static void RestorePreviousKeyboardLayout()
|
||||
{
|
||||
if (_previousLayout == HKL.Null) return;
|
||||
|
||||
var hwnd = PInvoke.GetForegroundWindow();
|
||||
if (hwnd == HWND.Null) return;
|
||||
|
||||
PInvoke.PostMessage(
|
||||
hwnd,
|
||||
PInvoke.WM_INPUTLANGCHANGEREQUEST,
|
||||
PInvoke.INPUTLANGCHANGE_FORWARD,
|
||||
_previousLayout.Value
|
||||
);
|
||||
|
||||
_previousLayout = HKL.Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an installed English keyboard layout.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Win32Exception"></exception>
|
||||
private static unsafe HKL FindEnglishKeyboardLayout()
|
||||
{
|
||||
// Get the number of keyboard layouts
|
||||
int count = PInvoke.GetKeyboardLayoutList(0, null);
|
||||
if (count <= 0) return HKL.Null;
|
||||
|
||||
// Get all keyboard layouts
|
||||
var handles = new HKL[count];
|
||||
fixed (HKL* h = handles)
|
||||
{
|
||||
var result = PInvoke.GetKeyboardLayoutList(count, h);
|
||||
if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
// Look for any English keyboard layout
|
||||
foreach (var hkl in handles)
|
||||
{
|
||||
// The lower word contains the language identifier
|
||||
var langId = (uint)hkl.Value & KeyboardLayoutLoWord;
|
||||
var langTag = GetLanguageTag(langId);
|
||||
|
||||
// Check if it's an English layout
|
||||
if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return hkl;
|
||||
}
|
||||
}
|
||||
|
||||
return HKL.Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the
|
||||
/// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names">
|
||||
/// BCP 47 language tag
|
||||
/// </see>
|
||||
/// of the current input language.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Edited from: https://github.com/dotnet/winforms
|
||||
/// </remarks>
|
||||
private static string GetLanguageTag(uint langId)
|
||||
{
|
||||
// We need to convert the language identifier to a language tag, because they are deprecated and may have a
|
||||
// transient value.
|
||||
// https://learn.microsoft.com/globalization/locale/other-locale-names#lcid
|
||||
// https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks
|
||||
//
|
||||
// It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect
|
||||
// language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID"
|
||||
// instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet).
|
||||
//
|
||||
// Try to extract proper language tag from registry as a workaround approved by a Windows team.
|
||||
// https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949
|
||||
//
|
||||
// NOTE: this logic may break in future versions of Windows since it is not documented.
|
||||
if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1
|
||||
or PInvoke.LOCALE_TRANSIENT_KEYBOARD2
|
||||
or PInvoke.LOCALE_TRANSIENT_KEYBOARD3
|
||||
or PInvoke.LOCALE_TRANSIENT_KEYBOARD4)
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath);
|
||||
if (key?.GetValue("Languages") is string[] languages)
|
||||
{
|
||||
foreach (string language in languages)
|
||||
{
|
||||
using var subKey = key.OpenSubKey(language);
|
||||
if (subKey?.GetValue("TransientLangId") is int transientLangId
|
||||
&& transientLangId == langId)
|
||||
{
|
||||
return language;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CultureInfo.GetCultureInfo((int)langId).Name;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Notification
|
||||
|
||||
public static bool IsNotificationSupported()
|
||||
{
|
||||
// Notifications only supported on Windows 10 19041+
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
Environment.OSVersion.Version.Build >= 19041;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Korean IME
|
||||
|
||||
public static bool IsWindows11()
|
||||
{
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
Environment.OSVersion.Version.Build >= 22000;
|
||||
}
|
||||
|
||||
public static bool IsKoreanIMEExist()
|
||||
{
|
||||
return GetLegacyKoreanIMERegistryValue() != null;
|
||||
}
|
||||
|
||||
public static bool IsLegacyKoreanIMEEnabled()
|
||||
{
|
||||
object value = GetLegacyKoreanIMERegistryValue();
|
||||
|
||||
if (value is int intValue)
|
||||
{
|
||||
return intValue == 1;
|
||||
}
|
||||
else if (value != null && int.TryParse(value.ToString(), out int parsedValue))
|
||||
{
|
||||
return parsedValue == 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the blur for a window via SetWindowCompositionAttribute
|
||||
/// </summary>
|
||||
public static void SetBlurForWindow(Window w, bool blur)
|
||||
public static bool SetLegacyKoreanIMEEnabled(bool enable)
|
||||
{
|
||||
SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED);
|
||||
}
|
||||
const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}";
|
||||
const string valueName = "NoTsf3Override5";
|
||||
|
||||
private static void SetWindowAccent(Window w, AccentState state)
|
||||
{
|
||||
var windowHelper = new WindowInteropHelper(w);
|
||||
|
||||
windowHelper.EnsureHandle();
|
||||
|
||||
var accent = new AccentPolicy { AccentState = state };
|
||||
var accentStructSize = Marshal.SizeOf(accent);
|
||||
|
||||
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
|
||||
Marshal.StructureToPtr(accent, accentPtr, false);
|
||||
|
||||
var data = new WindowCompositionAttributeData
|
||||
try
|
||||
{
|
||||
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
|
||||
SizeOfData = accentStructSize,
|
||||
Data = accentPtr
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
SetWindowCompositionAttribute(windowHelper.Handle, ref data);
|
||||
|
||||
Marshal.FreeHGlobal(accentPtr);
|
||||
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<string, string> _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" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system default font.
|
||||
/// </summary>
|
||||
/// <param name="useNoto">
|
||||
/// If true, it will try to find the Noto font for the current culture.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The name of the system default font.
|
||||
/// </returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ namespace Flow.Launcher.Plugin
|
|||
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="SpecialKeyState"/> object with all keys not pressed.
|
||||
/// </summary>
|
||||
public static readonly SpecialKeyState Default = new () {
|
||||
CtrlPressed = false,
|
||||
ShiftPressed = false,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this language is a Python language
|
||||
/// </summary>
|
||||
/// <param name="language"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsPython(string language)
|
||||
{
|
||||
return language.Equals(Python, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this language is a Node.js language
|
||||
/// </summary>
|
||||
/// <param name="language"></param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this language is a executable language
|
||||
/// </summary>
|
||||
/// <param name="language"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsExecutable(string language)
|
||||
{
|
||||
return language.Equals(Executable, StringComparison.OrdinalIgnoreCase)
|
||||
|| language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>4.4.0</Version>
|
||||
<PackageVersion>4.4.0</PackageVersion>
|
||||
<AssemblyVersion>4.4.0</AssemblyVersion>
|
||||
<FileVersion>4.4.0</FileVersion>
|
||||
<Version>4.5.0</Version>
|
||||
<PackageVersion>4.5.0</PackageVersion>
|
||||
<AssemblyVersion>4.5.0</AssemblyVersion>
|
||||
<FileVersion>4.5.0</FileVersion>
|
||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||
<Authors>Flow-Launcher</Authors>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fody" Version="6.5.4">
|
||||
<PackageReference Include="Fody" Version="6.5.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
@ -76,7 +76,9 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
23
Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs
Normal file
23
Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronous Query Model for Flow Launcher When Query Text is Empty
|
||||
/// </summary>
|
||||
public interface IAsyncHomeQuery : IFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronous Querying When Query Text is Empty
|
||||
/// </summary>
|
||||
/// <para>
|
||||
/// If the Querying method requires high IO transmission
|
||||
/// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncHomeQuery interface
|
||||
/// </para>
|
||||
/// <param name="token">Cancel when querying job is obsolete</param>
|
||||
/// <returns></returns>
|
||||
Task<List<Result>> HomeQueryAsync(CancellationToken token);
|
||||
}
|
||||
}
|
||||
28
Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs
Normal file
28
Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronous Query Model for Flow Launcher When Query Text is Empty
|
||||
/// <para>
|
||||
/// If the Querying method requires high IO transmission
|
||||
/// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncHomeQuery interface
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IHomeQuery : IAsyncHomeQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Querying When Query Text is Empty
|
||||
/// <para>
|
||||
/// This method will be called within a Task.Run,
|
||||
/// so please avoid synchronously wait for long.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
List<Result> HomeQuery();
|
||||
|
||||
Task<List<Result>> IAsyncHomeQuery.HomeQueryAsync(CancellationToken token) => Task.Run(HomeQuery);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using JetBrains.Annotations;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
|
|
@ -8,6 +6,9 @@ using System.Runtime.CompilerServices;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
|
|
@ -87,6 +88,11 @@ namespace Flow.Launcher.Plugin
|
|||
/// Show the MainWindow when hiding
|
||||
/// </summary>
|
||||
void ShowMainWindow();
|
||||
|
||||
/// <summary>
|
||||
/// Focus the query text box in the main window
|
||||
/// </summary>
|
||||
void FocusQueryTextBox();
|
||||
|
||||
/// <summary>
|
||||
/// Hide MainWindow
|
||||
|
|
@ -141,18 +147,49 @@ namespace Flow.Launcher.Plugin
|
|||
List<PluginPair> GetAllPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// Register a callback for Global Keyboard Event
|
||||
/// Registers a callback function for global keyboard events.
|
||||
/// </summary>
|
||||
/// <param name="callback"></param>
|
||||
/// <param name="callback">
|
||||
/// The callback function to invoke when a global keyboard event occurs.
|
||||
/// <para>
|
||||
/// Parameters:
|
||||
/// <list type="number">
|
||||
/// <item><description>int: The type of <see cref="KeyEvent"/> (key down, key up, etc.)</description></item>
|
||||
/// <item><description>int: The virtual key code of the pressed/released key</description></item>
|
||||
/// <item><description><see cref="SpecialKeyState"/>: The state of modifier keys (Ctrl, Alt, Shift, etc.)</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Returns: <c>true</c> to allow normal system processing of the key event,
|
||||
/// or <c>false</c> to intercept and prevent default handling.
|
||||
/// </para>
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This callback will be invoked for all keyboard events system-wide.
|
||||
/// Use with caution as intercepting system keys may affect normal system operation.
|
||||
/// </remarks>
|
||||
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Remove a callback for Global Keyboard Event
|
||||
/// </summary>
|
||||
/// <param name="callback"></param>
|
||||
/// <param name="callback">
|
||||
/// The callback function to invoke when a global keyboard event occurs.
|
||||
/// <para>
|
||||
/// Parameters:
|
||||
/// <list type="number">
|
||||
/// <item><description>int: The type of <see cref="KeyEvent"/> (key down, key up, etc.)</description></item>
|
||||
/// <item><description>int: The virtual key code of the pressed/released key</description></item>
|
||||
/// <item><description><see cref="SpecialKeyState"/>: The state of modifier keys (Ctrl, Alt, Shift, etc.)</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Returns: <c>true</c> to allow normal system processing of the key event,
|
||||
/// or <c>false</c> to intercept and prevent default handling.
|
||||
/// </para>
|
||||
/// </param>
|
||||
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
|
||||
/// </summary>
|
||||
|
|
@ -191,14 +228,19 @@ namespace Flow.Launcher.Plugin
|
|||
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Add ActionKeyword for specific plugin
|
||||
/// Add ActionKeyword and update action keyword metadata for specific plugin.
|
||||
/// Before adding, please check if action keyword is already assigned by <see cref="ActionKeywordAssigned"/>
|
||||
/// </summary>
|
||||
/// <param name="pluginId">ID for plugin that needs to add action keyword</param>
|
||||
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
void AddActionKeyword(string pluginId, string newActionKeyword);
|
||||
|
||||
/// <summary>
|
||||
/// Remove ActionKeyword for specific plugin
|
||||
/// Remove ActionKeyword and update action keyword metadata for specific plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
|
||||
/// <param name="oldActionKeyword">The actionkeyword that is supposed to be removed</param>
|
||||
|
|
@ -227,6 +269,11 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
void LogWarn(string className, string message, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log error message. Preferred error logging method for plugins.
|
||||
/// </summary>
|
||||
void LogError(string className, string message, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log an Exception. Will throw if in debug mode so developer will be aware,
|
||||
/// otherwise logs the eror message. This is the primary logging method used for Flow
|
||||
|
|
@ -242,9 +289,10 @@ namespace Flow.Launcher.Plugin
|
|||
T LoadSettingJsonStorage<T>() where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="LoadSettingJsonStorage"/> or <see cref="SaveSettingJsonStorage"/> previously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type for Serialization</typeparam>
|
||||
/// <returns></returns>
|
||||
|
|
@ -344,5 +392,163 @@ namespace Flow.Launcher.Plugin
|
|||
/// Stop the loading bar in main window
|
||||
/// </summary>
|
||||
public void StopLoadingBar();
|
||||
|
||||
/// <summary>
|
||||
/// Get all available themes
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<ThemeData> GetAvailableThemes();
|
||||
|
||||
/// <summary>
|
||||
/// Get the current theme
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ThemeData GetCurrentTheme();
|
||||
|
||||
/// <summary>
|
||||
/// Set the current theme
|
||||
/// </summary>
|
||||
/// <param name="theme"></param>
|
||||
/// <returns>
|
||||
/// True if the theme is set successfully, false otherwise.
|
||||
/// </returns>
|
||||
public bool SetCurrentTheme(ThemeData theme);
|
||||
|
||||
/// <summary>
|
||||
/// Save all Flow's plugins caches
|
||||
/// </summary>
|
||||
void SavePluginCaches();
|
||||
|
||||
/// <summary>
|
||||
/// Load BinaryStorage for current plugin's cache. This is the method used to load cache from binary in Flow.
|
||||
/// When the file is not exist, it will create a new instance for the specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type for deserialization</typeparam>
|
||||
/// <param name="cacheName">Cache file name</param>
|
||||
/// <param name="cacheDirectory">Cache directory from plugin metadata</param>
|
||||
/// <param name="defaultData">Default data to return</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
|
||||
/// </remarks>
|
||||
Task<T> LoadCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory, T defaultData) where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="LoadCacheBinaryStorageAsync"/> or <see cref="SaveCacheBinaryStorageAsync"/> previously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type for Serialization</typeparam>
|
||||
/// <param name="cacheName">Cache file name</param>
|
||||
/// <param name="cacheDirectory">Cache directory from plugin metadata</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
|
||||
/// </remarks>
|
||||
Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// Load image from path.
|
||||
/// Support local, remote and data:image url.
|
||||
/// Support png, jpg, jpeg, gif, bmp, tiff, ico, svg image files.
|
||||
/// If image path is missing, it will return a missing icon.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the image.</param>
|
||||
/// <param name="loadFullImage">
|
||||
/// Load full image or not.
|
||||
/// </param>
|
||||
/// <param name="cacheImage">
|
||||
/// Cache the image or not. Cached image will be stored in FL cache.
|
||||
/// If the image is just used one time, it's better to set this to false.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true);
|
||||
|
||||
/// <summary>
|
||||
/// Update the plugin manifest
|
||||
/// </summary>
|
||||
/// <param name="usePrimaryUrlOnly">
|
||||
/// FL has multiple urls to download the plugin manifest. Set this to true to only use the primary url.
|
||||
/// </param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns>True if the manifest is updated successfully, false otherwise</returns>
|
||||
public Task<bool> UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the plugin manifest.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If Flow cannot get manifest data, this could be null
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
public IReadOnlyList<UserPlugin> GetPluginManifest();
|
||||
|
||||
/// <summary>
|
||||
/// Check if the plugin has been modified.
|
||||
/// If this plugin is updated, installed or uninstalled and users do not restart the app,
|
||||
/// it will be marked as modified
|
||||
/// </summary>
|
||||
/// <param name="id">Plugin id</param>
|
||||
/// <returns></returns>
|
||||
public bool PluginModified(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
|
||||
/// unless it's a local path installation
|
||||
/// </summary>
|
||||
/// <param name="pluginMetadata">The metadata of the old plugin to update</param>
|
||||
/// <param name="plugin">The new plugin to update</param>
|
||||
/// <param name="zipFilePath">
|
||||
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Install a plugin. By default will remove the zip file if installation is from url,
|
||||
/// unless it's a local path installation
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin to install</param>
|
||||
/// <param name="zipFilePath">
|
||||
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
|
||||
/// </param>
|
||||
public void InstallPlugin(UserPlugin plugin, string zipFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Uninstall a plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginMetadata">The metadata of the plugin to uninstall</param>
|
||||
/// <param name="removePluginSettings">
|
||||
/// Plugin has their own settings. If this is set to true, the plugin settings will be removed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
|
||||
|
||||
/// <summary>
|
||||
/// Log debug message of the time taken to execute a method
|
||||
/// Message will only be logged in Debug mode
|
||||
/// </summary>
|
||||
/// <returns>The time taken to execute the method in milliseconds</returns>
|
||||
public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log debug message of the time taken to execute a method asynchronously
|
||||
/// Message will only be logged in Debug mode
|
||||
/// </summary>
|
||||
/// <returns>The time taken to execute the method in milliseconds</returns>
|
||||
public Task<long> StopwatchLogDebugAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log info message of the time taken to execute a method
|
||||
/// </summary>
|
||||
/// <returns>The time taken to execute the method in milliseconds</returns>
|
||||
public long StopwatchLogInfo(string className, string message, Action action, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log info message of the time taken to execute a method asynchronously
|
||||
/// </summary>
|
||||
/// <returns>The time taken to execute the method in milliseconds</returns>
|
||||
public Task<long> StopwatchLogInfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,42 @@ using System.Threading;
|
|||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for plugins that want to manually update their results
|
||||
/// </summary>
|
||||
public interface IResultUpdated : IFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Event that is triggered when the results are updated
|
||||
/// </summary>
|
||||
event ResultUpdatedEventHandler ResultsUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for the ResultsUpdated event
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for the ResultsUpdated event
|
||||
/// </summary>
|
||||
public class ResultUpdatedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// List of results that should be displayed
|
||||
/// </summary>
|
||||
public List<Result> Results;
|
||||
|
||||
/// <summary>
|
||||
/// Query that triggered the update
|
||||
/// </summary>
|
||||
public Query Query;
|
||||
|
||||
/// <summary>
|
||||
/// Token that can be used to cancel the update
|
||||
/// </summary>
|
||||
public CancellationToken Token { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
namespace Flow.Launcher.Plugin
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Inherit this interface if additional data e.g. cache needs to be saved.
|
||||
/// Inherit this interface if you need to save additional data which is not a setting or cache,
|
||||
/// please implement this interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For storing plugin settings, prefer <see cref="IPublicAPI.LoadSettingJsonStorage{T}"/>
|
||||
/// or <see cref="IPublicAPI.SaveSettingJsonStorage{T}"/>.
|
||||
/// Once called, your settings will be automatically saved by Flow.
|
||||
/// or <see cref="IPublicAPI.SaveSettingJsonStorage{T}"/>.
|
||||
/// For storing plugin caches, prefer <see cref="IPublicAPI.LoadCacheBinaryStorageAsync{T}"/>
|
||||
/// or <see cref="IPublicAPI.SaveCacheBinaryStorageAsync{T}(string, string)"/>.
|
||||
/// Once called, those settings and caches will be automatically saved by Flow.
|
||||
/// </remarks>
|
||||
public interface ISavable : IFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Save additional plugin data, such as cache.
|
||||
/// Save additional plugin data.
|
||||
/// </summary>
|
||||
void Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,15 @@
|
|||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface is used to create settings panel for .Net plugins
|
||||
/// </summary>
|
||||
public interface ISettingProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Create settings panel control for .Net plugins
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Control CreateSettingPanel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
using Windows.Win32;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumeration of key events for
|
||||
/// <see cref="IPublicAPI.RegisterGlobalKeyboardCallback(System.Func{int, int, SpecialKeyState, bool})"/>
|
||||
/// and <see cref="IPublicAPI.RemoveGlobalKeyboardCallback(System.Func{int, int, SpecialKeyState, bool})"/>
|
||||
/// </summary>
|
||||
public enum KeyEvent
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
EnumThreadWindows
|
||||
GetWindowText
|
||||
GetWindowTextLength
|
||||
GetWindowTextLength
|
||||
|
||||
WM_KEYDOWN
|
||||
WM_KEYUP
|
||||
WM_SYSKEYDOWN
|
||||
WM_SYSKEYUP
|
||||
|
|
@ -5,10 +5,18 @@
|
|||
/// </summary>
|
||||
public class PluginInitContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Default constructor.
|
||||
/// </summary>
|
||||
public PluginInitContext()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="currentPluginMetadata"></param>
|
||||
/// <param name="api"></param>
|
||||
public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api)
|
||||
{
|
||||
CurrentPluginMetadata = currentPluginMetadata;
|
||||
|
|
|
|||
|
|
@ -4,24 +4,82 @@ using System.Text.Json.Serialization;
|
|||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin metadata
|
||||
/// </summary>
|
||||
public class PluginMetadata : BaseModel
|
||||
{
|
||||
private string _pluginDirectory;
|
||||
/// <summary>
|
||||
/// Plugin ID.
|
||||
/// </summary>
|
||||
public string ID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Website { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public string ExecuteFilePath { get; private set;}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin author.
|
||||
/// </summary>
|
||||
public string Author { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin version.
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin language.
|
||||
/// See <see cref="AllowedLanguage"/>
|
||||
/// </summary>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin description.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin website.
|
||||
/// </summary>
|
||||
public string Website { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether plugin is disabled.
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether plugin is disabled in home query.
|
||||
/// </summary>
|
||||
public bool HomeDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin execute file path.
|
||||
/// </summary>
|
||||
public string ExecuteFilePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin execute file name.
|
||||
/// </summary>
|
||||
public string ExecuteFileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin assembly name.
|
||||
/// Only available for .Net plugins.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string AssemblyName { get; internal set; }
|
||||
|
||||
private string _pluginDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin source directory.
|
||||
/// </summary>
|
||||
public string PluginDirectory
|
||||
{
|
||||
get { return _pluginDirectory; }
|
||||
get => _pluginDirectory;
|
||||
internal set
|
||||
{
|
||||
_pluginDirectory = value;
|
||||
|
|
@ -30,28 +88,77 @@ namespace Flow.Launcher.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The first action keyword of plugin.
|
||||
/// </summary>
|
||||
public string ActionKeyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All action keywords of plugin.
|
||||
/// </summary>
|
||||
public List<string> ActionKeywords { get; set; }
|
||||
|
||||
public string IcoPath { get; set;}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
/// <summary>
|
||||
/// Hide plugin keyword setting panel.
|
||||
/// </summary>
|
||||
public bool HideActionKeywordPanel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin search delay time in ms. Null means use default search delay time.
|
||||
/// </summary>
|
||||
public int? SearchDelayTime { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin icon path.
|
||||
/// </summary>
|
||||
public string IcoPath { get; set;}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin priority.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Init time include both plugin load time and init time
|
||||
/// Init time include both plugin load time and init time.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public long InitTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average query time.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public long AvgQueryTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Query count.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int QueryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the plugin settings directory which is not validated.
|
||||
/// It is used to store plugin settings files and data files.
|
||||
/// When plugin is deleted, FL will ask users whether to keep its settings.
|
||||
/// If users do not want to keep, this directory will be deleted.
|
||||
/// </summary>
|
||||
public string PluginSettingsDirectoryPath { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the plugin cache directory which is not validated.
|
||||
/// It is used to store cache files.
|
||||
/// When plugin is deleted, this directory will be deleted as well.
|
||||
/// </summary>
|
||||
public string PluginCacheDirectoryPath { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert <see cref="PluginMetadata"/> to string.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,37 @@
|
|||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin instance and plugin metadata
|
||||
/// </summary>
|
||||
public class PluginPair
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin instance
|
||||
/// </summary>
|
||||
public IAsyncPlugin Plugin { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin metadata
|
||||
/// </summary>
|
||||
public PluginMetadata Metadata { get; internal set; }
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Convert to string
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Metadata.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare by plugin metadata ID
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
PluginPair r = obj as PluginPair;
|
||||
if (r != null)
|
||||
if (obj is PluginPair r)
|
||||
{
|
||||
return string.Equals(r.Metadata.ID, Metadata.ID);
|
||||
}
|
||||
|
|
@ -25,6 +41,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get hash code
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a query that is sent to a plugin.
|
||||
/// </summary>
|
||||
public class Query
|
||||
{
|
||||
public Query() { }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public string RawQuery { get; internal init; }
|
||||
|
|
@ -19,6 +21,11 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public bool IsReQuery { get; internal set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the query is a home query.
|
||||
/// </summary>
|
||||
public bool IsHomeQuery { get; internal init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Search part of a query.
|
||||
/// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as RawQuery.
|
||||
|
|
@ -39,10 +46,9 @@ namespace Flow.Launcher.Plugin
|
|||
public const string TermSeparator = " ";
|
||||
|
||||
/// <summary>
|
||||
/// User can set multiple action keywords seperated by ';'
|
||||
/// User can set multiple action keywords seperated by whitespace
|
||||
/// </summary>
|
||||
public const string ActionKeywordSeparator = ";";
|
||||
|
||||
public const string ActionKeywordSeparator = TermSeparator;
|
||||
|
||||
/// <summary>
|
||||
/// Wildcard action keyword. Plugins using this value will be queried on every search.
|
||||
|
|
@ -55,18 +61,18 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public string ActionKeyword { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
/// <summary>
|
||||
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string FirstSearch => SplitSearch(0);
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
private string _secondToEndSearch;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// strings from second search (including) to last search
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Runtime;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -13,6 +12,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public class Result
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public const int MaxScore = int.MaxValue;
|
||||
|
||||
private string _pluginDirectory;
|
||||
|
||||
|
|
@ -20,6 +23,8 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
private string _copyText = string.Empty;
|
||||
|
||||
private string _badgeIcoPath;
|
||||
|
||||
/// <summary>
|
||||
/// The title of the result. This is always required.
|
||||
/// </summary>
|
||||
|
|
@ -62,7 +67,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <remarks>GlyphInfo is prioritized if not null</remarks>
|
||||
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
|
||||
|
|
@ -82,6 +87,33 @@ namespace Flow.Launcher.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The image to be displayed for the badge of the result.
|
||||
/// </summary>
|
||||
/// <value>Can be a local file path or a URL.</value>
|
||||
/// <remarks>If null or empty, will use plugin icon</remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if Icon has a border radius
|
||||
/// </summary>
|
||||
|
|
@ -96,14 +128,18 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Delegate to load an icon for this result.
|
||||
/// </summary>
|
||||
public IconDelegate Icon;
|
||||
public IconDelegate Icon = null;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate to load an icon for the badge of this result.
|
||||
/// </summary>
|
||||
public IconDelegate BadgeIcon = null;
|
||||
|
||||
/// <summary>
|
||||
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
|
||||
/// </summary>
|
||||
public GlyphInfo Glyph { get; init; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An action to take in the form of a function call when the result has been selected.
|
||||
/// </summary>
|
||||
|
|
@ -145,59 +181,19 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Title + SubTitle + Score;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the current result
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Additional data associated with this result
|
||||
/// </summary>
|
||||
|
|
@ -226,16 +222,6 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public Lazy<UserControl> PreviewPanel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Run this result, asynchronously
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public ValueTask<bool> ExecuteAsync(ActionContext context)
|
||||
{
|
||||
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result
|
||||
/// </summary>
|
||||
|
|
@ -257,11 +243,6 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public bool AddSelectedCount { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public const int MaxScore = int.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
|
@ -270,6 +251,66 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public string RecordKey { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool ShowBadge { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Run this result, asynchronously
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public ValueTask<bool> ExecuteAsync(ActionContext context)
|
||||
{
|
||||
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Title + SubTitle + Score;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the current result
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Info of the preview section of a <see cref="Result"/>
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -318,5 +318,51 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
{
|
||||
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a directory, creating it if it doesn't exist
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
public static void ValidateDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a data directory, synchronizing it by ensuring all files from a bundled source directory exist in it.
|
||||
/// If files are missing or outdated, they are copied from the bundled directory to the data directory.
|
||||
/// </summary>
|
||||
/// <param name="bundledDataDirectory"></param>
|
||||
/// <param name="dataDirectory"></param>
|
||||
public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
|
||||
{
|
||||
if (!Directory.Exists(dataDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
}
|
||||
|
||||
foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
|
||||
{
|
||||
var data = Path.GetFileName(bundledDataPath);
|
||||
if (data == null) continue;
|
||||
var dataPath = Path.Combine(dataDirectory, data);
|
||||
if (!File.Exists(dataPath))
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
|
||||
var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
|
||||
if (time1 != time2)
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains methods to open a search in a new browser window or tab.
|
||||
/// </summary>
|
||||
public static class SearchWeb
|
||||
{
|
||||
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);
|
||||
|
|
@ -20,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
|
||||
{
|
||||
|
|
@ -62,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,13 +109,21 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,26 @@ using Windows.Win32.Foundation;
|
|||
|
||||
namespace Flow.Launcher.Plugin.SharedCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains methods for running shell commands
|
||||
/// </summary>
|
||||
public static class ShellCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for EnumThreadWindows
|
||||
/// </summary>
|
||||
/// <param name="hwnd"></param>
|
||||
/// <param name="lParam"></param>
|
||||
/// <returns></returns>
|
||||
public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
|
||||
|
||||
private static bool containsSecurityWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Runs a windows command using the provided ProcessStartInfo
|
||||
/// </summary>
|
||||
/// <param name="processStartInfo"></param>
|
||||
/// <returns></returns>
|
||||
public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo)
|
||||
{
|
||||
processStartInfo.Verb = "RunAsUser";
|
||||
|
|
@ -65,6 +79,15 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
return buffer[..length].ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a windows command using the provided ProcessStartInfo
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="workingDirectory"></param>
|
||||
/// <param name="arguments"></param>
|
||||
/// <param name="verb"></param>
|
||||
/// <param name="createNoWindow"></param>
|
||||
/// <returns></returns>
|
||||
public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "",
|
||||
string arguments = "", string verb = "", bool createNoWindow = false)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,14 +2,29 @@
|
|||
|
||||
namespace Flow.Launcher.Plugin.SharedModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of a match operation.
|
||||
/// </summary>
|
||||
public class MatchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MatchResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="success"></param>
|
||||
/// <param name="searchPrecision"></param>
|
||||
public MatchResult(bool success, SearchPrecisionScore searchPrecision)
|
||||
{
|
||||
Success = success;
|
||||
SearchPrecision = searchPrecision;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MatchResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="success"></param>
|
||||
/// <param name="searchPrecision"></param>
|
||||
/// <param name="matchData"></param>
|
||||
/// <param name="rawScore"></param>
|
||||
public MatchResult(bool success, SearchPrecisionScore searchPrecision, List<int> matchData, int rawScore)
|
||||
{
|
||||
Success = success;
|
||||
|
|
@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels
|
|||
RawScore = rawScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the match operation was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels
|
|||
/// </summary>
|
||||
private int _rawScore;
|
||||
|
||||
/// <summary>
|
||||
/// The raw calculated search score without any search precision filtering applied.
|
||||
/// </summary>
|
||||
public int RawScore
|
||||
{
|
||||
get { return _rawScore; }
|
||||
|
|
@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels
|
|||
/// </summary>
|
||||
public List<int> MatchData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The search precision score used to filter the search results.
|
||||
/// </summary>
|
||||
public SearchPrecisionScore SearchPrecision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the search precision score is met.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsSearchPrecisionScoreMet()
|
||||
{
|
||||
return IsSearchPrecisionScoreMet(_rawScore);
|
||||
|
|
@ -63,10 +91,24 @@ namespace Flow.Launcher.Plugin.SharedModels
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the search precision score used to filter search results.
|
||||
/// </summary>
|
||||
public enum SearchPrecisionScore
|
||||
{
|
||||
/// <summary>
|
||||
/// The highest search precision score.
|
||||
/// </summary>
|
||||
Regular = 50,
|
||||
|
||||
/// <summary>
|
||||
/// The medium search precision score.
|
||||
/// </summary>
|
||||
Low = 20,
|
||||
|
||||
/// <summary>
|
||||
/// The lowest search precision score.
|
||||
/// </summary>
|
||||
None = 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
77
Flow.Launcher.Plugin/SharedModels/ThemeData.cs
Normal file
77
Flow.Launcher.Plugin/SharedModels/ThemeData.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
|
||||
namespace Flow.Launcher.Plugin.SharedModels;
|
||||
|
||||
/// <summary>
|
||||
/// Theme data model
|
||||
/// </summary>
|
||||
public class ThemeData
|
||||
{
|
||||
/// <summary>
|
||||
/// Theme file name without extension
|
||||
/// </summary>
|
||||
public string FileNameWithoutExtension { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Theme name
|
||||
/// </summary>
|
||||
public string Name { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the theme supports dark mode
|
||||
/// </summary>
|
||||
public bool? IsDark { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the theme supports blur effects
|
||||
/// </summary>
|
||||
public bool? HasBlur { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Theme data constructor
|
||||
/// </summary>
|
||||
public ThemeData(string fileNameWithoutExtension, string name, bool? isDark = null, bool? hasBlur = null)
|
||||
{
|
||||
FileNameWithoutExtension = fileNameWithoutExtension;
|
||||
Name = name;
|
||||
IsDark = isDark;
|
||||
HasBlur = hasBlur;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator ==(ThemeData left, ThemeData right)
|
||||
{
|
||||
if (left is null && right is null)
|
||||
return true;
|
||||
if (left is null || right is null)
|
||||
return false;
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator !=(ThemeData left, ThemeData right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is not ThemeData other)
|
||||
return false;
|
||||
return FileNameWithoutExtension == other.FileNameWithoutExtension &&
|
||||
Name == other.Name;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(FileNameWithoutExtension, Name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
80
Flow.Launcher.Plugin/UserPlugin.cs
Normal file
80
Flow.Launcher.Plugin/UserPlugin.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// User Plugin Model for Flow Launcher
|
||||
/// </summary>
|
||||
public record UserPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier of the plugin
|
||||
/// </summary>
|
||||
public string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the plugin
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Description of the plugin
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Author of the plugin
|
||||
/// </summary>
|
||||
public string Author { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the plugin
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Allow language of the plugin <see cref="AllowedLanguage"/>
|
||||
/// </summary>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Website of the plugin
|
||||
/// </summary>
|
||||
public string Website { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to download the plugin
|
||||
/// </summary>
|
||||
public string UrlDownload { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to the source code of the plugin
|
||||
/// </summary>
|
||||
public string UrlSourceCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local path where the plugin is installed
|
||||
/// </summary>
|
||||
public string LocalInstallPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Icon path of the plugin
|
||||
/// </summary>
|
||||
public string IcoPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date when the plugin was last updated
|
||||
/// </summary>
|
||||
public DateTime? LatestReleaseDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date when the plugin was added to the local system
|
||||
/// </summary>
|
||||
public DateTime? DateAdded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the plugin is installed from a local path
|
||||
/// </summary>
|
||||
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,11 +53,11 @@
|
|||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="26,12,26,0">
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,12">
|
||||
<StackPanel Margin="26 12 26 0">
|
||||
<StackPanel Grid.Row="0" Margin="0 0 0 12">
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0,0,0,0"
|
||||
Margin="0 0 0 0"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource actionKeywordsTitle}"
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
TextWrapping="WrapWithOverflow" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,18,0,0" Orientation="Horizontal">
|
||||
<StackPanel Margin="0 18 0 0" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
|
|
@ -83,14 +83,14 @@
|
|||
x:Name="tbOldActionKeyword"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="14,10,10,10"
|
||||
Margin="14 10 10 10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Color05B}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<StackPanel Margin="0 0 0 10" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
<TextBox
|
||||
x:Name="tbAction"
|
||||
Width="105"
|
||||
Margin="10,10,15,10"
|
||||
Margin="10 10 15 10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
|
@ -112,20 +112,20 @@
|
|||
Grid.Row="1"
|
||||
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0,1,0,0">
|
||||
BorderThickness="0 1 0 0">
|
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="btnCancel"
|
||||
Width="145"
|
||||
Height="30"
|
||||
Margin="10,0,5,0"
|
||||
Margin="10 0 5 0"
|
||||
Click="BtnCancel_OnClick"
|
||||
Content="{DynamicResource cancel}" />
|
||||
<Button
|
||||
x:Name="btnDone"
|
||||
Width="145"
|
||||
Height="30"
|
||||
Margin="5,0,10,0"
|
||||
Margin="5 0 10 0"
|
||||
Click="btnDone_OnClick"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
||||
|
|
|
|||
|
|
@ -1,27 +1,26 @@
|
|||
using System.Windows;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using Flow.Launcher.Core;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class ActionKeywords
|
||||
{
|
||||
private readonly PluginPair plugin;
|
||||
private readonly Internationalization translater = InternationalizationManager.Instance;
|
||||
private readonly PluginViewModel pluginViewModel;
|
||||
private readonly PluginPair _plugin;
|
||||
private readonly PluginViewModel _pluginViewModel;
|
||||
|
||||
public ActionKeywords(PluginViewModel pluginViewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
plugin = pluginViewModel.PluginPair;
|
||||
this.pluginViewModel = pluginViewModel;
|
||||
_plugin = pluginViewModel.PluginPair;
|
||||
_pluginViewModel = pluginViewModel;
|
||||
}
|
||||
|
||||
private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, plugin.Metadata.ActionKeywords.ToArray());
|
||||
tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, _plugin.Metadata.ActionKeywords.ToArray());
|
||||
tbAction.Focus();
|
||||
}
|
||||
|
||||
|
|
@ -32,20 +31,57 @@ namespace Flow.Launcher
|
|||
|
||||
private void btnDone_OnClick(object sender, RoutedEventArgs _)
|
||||
{
|
||||
var oldActionKeyword = plugin.Metadata.ActionKeywords[0];
|
||||
var newActionKeyword = tbAction.Text.Trim();
|
||||
newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*";
|
||||
|
||||
if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword))
|
||||
var oldActionKeywords = _plugin.Metadata.ActionKeywords;
|
||||
|
||||
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList();
|
||||
newActionKeywords.RemoveAll(string.IsNullOrEmpty);
|
||||
newActionKeywords = newActionKeywords.Distinct().ToList();
|
||||
|
||||
newActionKeywords = newActionKeywords.Count > 0 ? newActionKeywords : new() { Query.GlobalPluginWildcardSign };
|
||||
|
||||
var addedActionKeywords = newActionKeywords.Except(oldActionKeywords).ToList();
|
||||
var removedActionKeywords = oldActionKeywords.Except(newActionKeywords).ToList();
|
||||
if (!addedActionKeywords.Any(App.API.ActionKeywordAssigned))
|
||||
{
|
||||
pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword);
|
||||
Close();
|
||||
if (oldActionKeywords.Count != newActionKeywords.Count)
|
||||
{
|
||||
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||
return;
|
||||
}
|
||||
|
||||
var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList();
|
||||
var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList();
|
||||
|
||||
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
|
||||
{
|
||||
// User just changes the sequence of action keywords
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned");
|
||||
App.API.ShowMsgBox(msg);
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
|
||||
}
|
||||
}
|
||||
|
||||
private void ReplaceActionKeyword(string id, IReadOnlyList<string> removedActionKeywords, IReadOnlyList<string> addedActionKeywords)
|
||||
{
|
||||
foreach (var actionKeyword in removedActionKeywords)
|
||||
{
|
||||
App.API.RemoveActionKeyword(id, actionKeyword);
|
||||
}
|
||||
foreach (var actionKeyword in addedActionKeywords)
|
||||
{
|
||||
App.API.AddActionKeyword(id, actionKeyword);
|
||||
}
|
||||
|
||||
// Update action keywords text and close window
|
||||
_pluginViewModel.OnActionKeywordsTextChanged();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
ShutdownMode="OnMainWindowClose"
|
||||
Startup="OnStartupAsync">
|
||||
Startup="OnStartup">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core;
|
||||
using Flow.Launcher.Core.Configuration;
|
||||
|
|
@ -18,20 +19,39 @@ using Flow.Launcher.Infrastructure.Logger;
|
|||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.SettingPages.ViewModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class App : IDisposable, ISingleInstanceApp
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
public static IPublicAPI API { get; private set; }
|
||||
private const string Unique = "Flow.Launcher_Unique_Application_Mutex";
|
||||
public static bool LoadingOrExiting => _mainWindow == null || _mainWindow.CanClose;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private static readonly string ClassName = nameof(App);
|
||||
|
||||
private static bool _disposed;
|
||||
private static MainWindow _mainWindow;
|
||||
private readonly MainViewModel _mainVM;
|
||||
private readonly Settings _settings;
|
||||
|
||||
// To prevent two disposals running at the same time.
|
||||
private static readonly object _disposingLock = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public App()
|
||||
{
|
||||
// Initialize settings
|
||||
|
|
@ -57,13 +77,31 @@ namespace Flow.Launcher
|
|||
.AddSingleton(_ => _settings)
|
||||
.AddSingleton(sp => new Updater(sp.GetRequiredService<IPublicAPI>(), Launcher.Properties.Settings.Default.GithubRepo))
|
||||
.AddSingleton<Portable>()
|
||||
.AddSingleton<SettingWindowViewModel>()
|
||||
.AddSingleton<IAlphabet, PinyinAlphabet>()
|
||||
.AddSingleton<StringMatcher>()
|
||||
.AddSingleton<Internationalization>()
|
||||
.AddSingleton<IPublicAPI, PublicAPIInstance>()
|
||||
.AddSingleton<MainViewModel>()
|
||||
.AddSingleton<Theme>()
|
||||
// Use one instance for main window view model because we only have one main window
|
||||
.AddSingleton<MainViewModel>()
|
||||
// Use one instance for welcome window view model & setting window view model because
|
||||
// pages in welcome window & setting window need to share the same instance and
|
||||
// these two view models do not need to be reset when creating new windows
|
||||
.AddSingleton<WelcomeViewModel>()
|
||||
.AddSingleton<SettingWindowViewModel>()
|
||||
// Use transient instance for setting window page view models because
|
||||
// pages in setting window need to be recreated when setting window is closed
|
||||
.AddTransient<SettingsPaneAboutViewModel>()
|
||||
.AddTransient<SettingsPaneGeneralViewModel>()
|
||||
.AddTransient<SettingsPaneHotkeyViewModel>()
|
||||
.AddTransient<SettingsPanePluginsViewModel>()
|
||||
.AddTransient<SettingsPanePluginStoreViewModel>()
|
||||
.AddTransient<SettingsPaneProxyViewModel>()
|
||||
.AddTransient<SettingsPaneThemeViewModel>()
|
||||
// Use transient instance for dialog view models because
|
||||
// settings will change and we need to recreate them
|
||||
.AddTransient<SelectBrowserViewModel>()
|
||||
.AddTransient<SelectFileManagerViewModel>()
|
||||
).Build();
|
||||
Ioc.Default.ConfigureServices(host.Services);
|
||||
}
|
||||
|
|
@ -78,77 +116,99 @@ namespace Flow.Launcher
|
|||
{
|
||||
API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
_settings.Initialize();
|
||||
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Local function
|
||||
static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
|
||||
{
|
||||
// Firstly show users the message
|
||||
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
|
||||
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
|
||||
Environment.FailFast(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
|
||||
{
|
||||
// Firstly show users the message
|
||||
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
#endregion
|
||||
|
||||
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
|
||||
Environment.FailFast(message, e);
|
||||
}
|
||||
#region Main
|
||||
|
||||
[STAThread]
|
||||
public static void Main()
|
||||
{
|
||||
if (SingleInstance<App>.InitializeAsFirstInstance(Unique))
|
||||
if (SingleInstance<App>.InitializeAsFirstInstance())
|
||||
{
|
||||
using (var application = new App())
|
||||
{
|
||||
application.InitializeComponent();
|
||||
application.Run();
|
||||
}
|
||||
using var application = new App();
|
||||
application.InitializeComponent();
|
||||
application.Run();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnStartupAsync(object sender, StartupEventArgs e)
|
||||
#endregion
|
||||
|
||||
#region App Events
|
||||
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
|
||||
private async void OnStartup(object sender, StartupEventArgs e)
|
||||
{
|
||||
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
|
||||
await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
|
||||
{
|
||||
// Because new message box api uses MessageBoxEx window,
|
||||
// if it is created and closed before main window is created, it will cause the application to exit.
|
||||
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
|
||||
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
|
||||
Log.SetLogLevel(_settings.LogLevel);
|
||||
|
||||
// Update dynamic resources base on settings
|
||||
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
|
||||
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
|
||||
|
||||
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
|
||||
|
||||
Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
||||
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
|
||||
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
|
||||
API.LogInfo(ClassName, $"Runtime info:{ErrorReporting.RuntimeInfo()}");
|
||||
|
||||
RegisterAppDomainExceptions();
|
||||
RegisterDispatcherUnhandledException();
|
||||
RegisterTaskSchedulerUnhandledException();
|
||||
|
||||
var imageLoadertask = ImageLoader.InitializeAsync();
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
|
||||
InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
|
||||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
|
||||
// Register ResultsUpdated event after all plugins are loaded
|
||||
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
|
||||
|
||||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
await PluginManager.InitializePluginsAsync();
|
||||
|
||||
// Change language after all plugins are initialized because we need to update plugin title based on their api
|
||||
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
|
||||
|
||||
await imageLoadertask;
|
||||
|
||||
var mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||
var window = new MainWindow(_settings, mainVM);
|
||||
_mainWindow = new MainWindow();
|
||||
|
||||
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
||||
|
||||
Current.MainWindow = window;
|
||||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
// Initialize hotkey mapper instantly after main window is created because
|
||||
// it will steal focus from main window which causes window hide
|
||||
HotKeyMapper.Initialize();
|
||||
|
||||
// main windows needs initialized before theme change because of blur settings
|
||||
// TODO: Clean ThemeManager.Instance in future
|
||||
ThemeManager.Instance.ChangeTheme(_settings.Theme);
|
||||
// Initialize theme for main window
|
||||
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
|
|
@ -158,40 +218,37 @@ namespace Flow.Launcher
|
|||
AutoUpdates();
|
||||
|
||||
API.SaveAppAllSettings();
|
||||
Log.Info(
|
||||
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
||||
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
|
||||
});
|
||||
}
|
||||
|
||||
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||
|
||||
/// <summary>
|
||||
/// Check startup only for Release
|
||||
/// </summary>
|
||||
[Conditional("RELEASE")]
|
||||
private void AutoStartup()
|
||||
{
|
||||
// we try to enable auto-startup on first launch, or reenable if it was removed
|
||||
// but the user still has the setting set
|
||||
if (_settings.StartFlowLauncherOnSystemStartup && !Helper.AutoStartup.IsEnabled)
|
||||
if (_settings.StartFlowLauncherOnSystemStartup)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_settings.UseLogonTaskForStartup)
|
||||
{
|
||||
Helper.AutoStartup.EnableViaLogonTask();
|
||||
}
|
||||
else
|
||||
{
|
||||
Helper.AutoStartup.EnableViaRegistry();
|
||||
}
|
||||
Helper.AutoStartup.CheckIsEnabled(_settings.UseLogonTaskForStartup);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// but if it fails (permissions, etc) then don't keep retrying
|
||||
// this also gives the user a visual indication in the Settings widget
|
||||
_settings.StartFlowLauncherOnSystemStartup = false;
|
||||
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
|
||||
e.Message);
|
||||
API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//[Conditional("RELEASE")]
|
||||
[Conditional("RELEASE")]
|
||||
private void AutoUpdates()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
|
|
@ -209,15 +266,33 @@ namespace Flow.Launcher
|
|||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Register Events
|
||||
|
||||
private void RegisterExitEvents()
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
|
||||
Current.Exit += (s, e) => Dispose();
|
||||
Current.SessionEnding += (s, e) => Dispose();
|
||||
AppDomain.CurrentDomain.ProcessExit += (s, e) =>
|
||||
{
|
||||
API.LogInfo(ClassName, "Process Exit");
|
||||
Dispose();
|
||||
};
|
||||
|
||||
Current.Exit += (s, e) =>
|
||||
{
|
||||
API.LogInfo(ClassName, "Application Exit");
|
||||
Dispose();
|
||||
};
|
||||
|
||||
Current.SessionEnding += (s, e) =>
|
||||
{
|
||||
API.LogInfo(ClassName, "Session Ending");
|
||||
Dispose();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// let exception throw as normal is better for Debug
|
||||
/// Let exception throw as normal is better for Debug
|
||||
/// </summary>
|
||||
[Conditional("RELEASE")]
|
||||
private void RegisterDispatcherUnhandledException()
|
||||
|
|
@ -226,28 +301,84 @@ namespace Flow.Launcher
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// let exception throw as normal is better for Debug
|
||||
/// Let exception throw as normal is better for Debug
|
||||
/// </summary>
|
||||
[Conditional("RELEASE")]
|
||||
private static void RegisterAppDomainExceptions()
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
|
||||
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Let exception throw as normal is better for Debug
|
||||
/// </summary>
|
||||
private static void RegisterTaskSchedulerUnhandledException()
|
||||
{
|
||||
TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
// Prevent two disposes at the same time.
|
||||
lock (_disposingLock)
|
||||
{
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close()
|
||||
// Accessing _mainWindow?.Dispatcher will cause the application stuck
|
||||
// So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher
|
||||
if (!_mainWindow.CanClose)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
API.StopwatchLogInfo(ClassName, "Dispose cost", () =>
|
||||
{
|
||||
API.LogInfo(ClassName, "Begin Flow Launcher dispose ----------------------------------------------------");
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Dispose needs to be called on the main Windows thread,
|
||||
// since some resources owned by the thread need to be disposed.
|
||||
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
|
||||
_mainVM?.Dispose();
|
||||
}
|
||||
|
||||
API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------");
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// if sessionending is called, exit proverbially be called when log off / shutdown
|
||||
// but if sessionending is not called, exit won't be called when log off / shutdown
|
||||
if (!_disposed)
|
||||
{
|
||||
API.SaveAppAllSettings();
|
||||
_disposed = true;
|
||||
}
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISingleInstanceApp
|
||||
|
||||
public void OnSecondAppStarted()
|
||||
{
|
||||
Ioc.Default.GetRequiredService<MainViewModel>().Show();
|
||||
API.ShowMainWindow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
Flow.Launcher/Converters/BadgePositionConverter.cs
Normal file
32
Flow.Launcher/Converters/BadgePositionConverter.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class BadgePositionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is double actualWidth && parameter is string param)
|
||||
{
|
||||
double offset = actualWidth / 2 - 8;
|
||||
|
||||
if (param == "1") // X-Offset
|
||||
{
|
||||
return offset + 2;
|
||||
}
|
||||
else if (param == "2") // Y-Offset
|
||||
{
|
||||
return offset + 2;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class QuerySuggestionBoxConverter : IMultiValueConverter
|
||||
{
|
||||
private static readonly string ClassName = nameof(QuerySuggestionBoxConverter);
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
// values[0] is TextBox: The textbox displaying the autocomplete suggestion
|
||||
|
|
@ -43,8 +45,16 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter
|
|||
|
||||
// Check if Text will be larger than our QueryTextBox
|
||||
Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch);
|
||||
// TODO: Obsolete warning?
|
||||
var ft = new FormattedText(queryTextBox.Text, CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black);
|
||||
var dpi = VisualTreeHelper.GetDpi(queryTextBox);
|
||||
var ft = new FormattedText(
|
||||
queryTextBox.Text,
|
||||
CultureInfo.CurrentCulture,
|
||||
FlowDirection.LeftToRight,
|
||||
typeface,
|
||||
queryTextBox.FontSize,
|
||||
Brushes.Black,
|
||||
dpi.PixelsPerDip
|
||||
);
|
||||
|
||||
var offset = queryTextBox.Padding.Right;
|
||||
|
||||
|
|
@ -55,7 +65,7 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e);
|
||||
App.API.LogException(ClassName, "fail to convert text for suggestion box", e);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
Flow.Launcher/Converters/SizeRatioConverter.cs
Normal file
27
Flow.Launcher/Converters/SizeRatioConverter.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System.Windows.Data;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class SizeRatioConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is double size && parameter is string ratioString)
|
||||
{
|
||||
if (double.TryParse(ratioString, NumberStyles.Any, CultureInfo.InvariantCulture, out double ratio))
|
||||
{
|
||||
return size * ratio;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
|
@ -23,7 +22,7 @@ public class TextConverter : IValueConverter
|
|||
if (translationKey is null)
|
||||
return id;
|
||||
|
||||
return InternationalizationManager.Instance.GetTranslation(translationKey);
|
||||
return App.API.GetTranslation(translationKey);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@
|
|||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="26,0,26,0">
|
||||
<StackPanel Margin="26 0 26 0">
|
||||
<TextBlock
|
||||
Margin="0,0,0,12"
|
||||
Margin="0 0 0 12"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource customeQueryHotkeyTitle}"
|
||||
|
|
@ -72,10 +72,10 @@
|
|||
TextWrapping="WrapWithOverflow" />
|
||||
<Image
|
||||
Width="478"
|
||||
Margin="0,20,0,0"
|
||||
Margin="0 20 0 0"
|
||||
Source="/Images/illustration_01.png" />
|
||||
|
||||
<Grid Width="478" Margin="0,20,0,0">
|
||||
<Grid Width="478" Margin="0 20 0 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
|
|
@ -99,12 +99,12 @@
|
|||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="10,0,10,0"
|
||||
Margin="10 0 10 0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
HotkeySettings="{Binding Settings}"
|
||||
DefaultHotkey="" />
|
||||
DefaultHotkey=""
|
||||
Type="CustomQueryHotkey" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
|
|
@ -124,8 +124,8 @@
|
|||
x:Name="btnTestActionKeyword"
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,10,0"
|
||||
Padding="10,5,10,5"
|
||||
Margin="0 0 10 0"
|
||||
Padding="10 5 10 5"
|
||||
Click="BtnTestActionKeyword_OnClick"
|
||||
Content="{DynamicResource preview}" />
|
||||
</Grid>
|
||||
|
|
@ -133,21 +133,21 @@
|
|||
</StackPanel>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="0,14,0,0"
|
||||
Margin="0 14 0 0"
|
||||
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0,1,0,0">
|
||||
BorderThickness="0 1 0 0">
|
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="btnCancel"
|
||||
MinWidth="140"
|
||||
Margin="10,0,5,0"
|
||||
Margin="10 0 5 0"
|
||||
Click="BtnCancel_OnClick"
|
||||
Content="{DynamicResource cancel}" />
|
||||
<Button
|
||||
x:Name="btnAdd"
|
||||
MinWidth="140"
|
||||
Margin="5,0,10,0"
|
||||
Margin="5 0 10 0"
|
||||
Click="btnAdd_OnClick"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Controls;
|
||||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class CustomQueryHotkeySetting : Window
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
|
||||
private bool update;
|
||||
private CustomPluginHotkey updateCustomHotkey;
|
||||
|
||||
|
|
@ -53,14 +53,13 @@ namespace Flow.Launcher
|
|||
Close();
|
||||
}
|
||||
|
||||
|
||||
public void UpdateItem(CustomPluginHotkey item)
|
||||
{
|
||||
updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o =>
|
||||
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
|
||||
if (updateCustomHotkey == null)
|
||||
{
|
||||
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("invalidPluginHotkey"));
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
|
@ -68,7 +67,7 @@ namespace Flow.Launcher
|
|||
tbAction.Text = updateCustomHotkey.ActionKeyword;
|
||||
HotkeyControl.SetHotkey(updateCustomHotkey.Hotkey, false);
|
||||
update = true;
|
||||
lblAdd.Text = InternationalizationManager.Instance.GetTranslation("update");
|
||||
lblAdd.Text = App.API.GetTranslation("update");
|
||||
}
|
||||
|
||||
private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e)
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@
|
|||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="26,0,26,0">
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,12">
|
||||
<StackPanel Margin="26 0 26 0">
|
||||
<StackPanel Grid.Row="0" Margin="0 0 0 12">
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0,0,0,0"
|
||||
Margin="0 0 0 0"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource customQueryShortcut}"
|
||||
|
|
@ -73,18 +73,18 @@
|
|||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<TextBlock
|
||||
Margin="0,20,0,0"
|
||||
Margin="0 20 0 0"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource customeQueryShortcutGuide}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<Image
|
||||
Width="478"
|
||||
Margin="0,20,0,0"
|
||||
Margin="0 20 0 0"
|
||||
Source="/Images/illustration_02.png" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,10,0,10" Orientation="Horizontal">
|
||||
<StackPanel Margin="0 10 0 10" Orientation="Horizontal">
|
||||
<Grid Width="478">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
|
|
@ -124,14 +124,14 @@
|
|||
LastChildFill="True">
|
||||
<Button
|
||||
x:Name="btnTestShortcut"
|
||||
Margin="0,0,10,0"
|
||||
Padding="10,5,10,5"
|
||||
Margin="0 0 10 0"
|
||||
Padding="10 5 10 5"
|
||||
Click="BtnTestShortcut_OnClick"
|
||||
Content="{DynamicResource preview}"
|
||||
DockPanel.Dock="Right" />
|
||||
<TextBox
|
||||
x:Name="tbExpand"
|
||||
Margin="10,0,10,0"
|
||||
Margin="10 0 10 0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Value}" />
|
||||
|
|
@ -142,21 +142,21 @@
|
|||
</StackPanel>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="0,10,0,0"
|
||||
Margin="0 10 0 0"
|
||||
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0,1,0,0">
|
||||
BorderThickness="0 1 0 0">
|
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="btnCancel"
|
||||
MinWidth="140"
|
||||
Margin="10,0,5,0"
|
||||
Margin="10 0 5 0"
|
||||
Click="BtnCancel_OnClick"
|
||||
Content="{DynamicResource cancel}" />
|
||||
<Button
|
||||
x:Name="btnAdd"
|
||||
MinWidth="140"
|
||||
Margin="5,0,10,0"
|
||||
Margin="5 0 10 0"
|
||||
Click="BtnAdd_OnClick"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Flow.Launcher.SettingPages.ViewModels;
|
||||
using Flow.Launcher.Core;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class CustomShortcutSetting : Window
|
||||
{
|
||||
private readonly SettingsPaneHotkeyViewModel _hotkeyVm;
|
||||
public string Key { get; set; } = String.Empty;
|
||||
public string Value { get; set; } = String.Empty;
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
private string originalKey { get; } = null;
|
||||
private string originalValue { get; } = null;
|
||||
private bool update { get; } = false;
|
||||
|
|
@ -41,15 +38,15 @@ namespace Flow.Launcher
|
|||
|
||||
private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
|
||||
if (string.IsNullOrEmpty(Key) || string.IsNullOrEmpty(Value))
|
||||
{
|
||||
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut"));
|
||||
return;
|
||||
}
|
||||
// Check if key is modified or adding a new one
|
||||
if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key))
|
||||
{
|
||||
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut"));
|
||||
return;
|
||||
}
|
||||
DialogResult = !update || originalKey != Key || originalValue != Value;
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="ChefKeys" Version="0.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Fody" Version="6.5.4">
|
||||
<PackageReference Include="Fody" Version="6.5.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
|
@ -94,15 +94,13 @@
|
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<!-- ModernWpfUI v0.9.5 introduced WinRT changes that causes Notification platform unavailable error on some machines -->
|
||||
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
|
||||
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
|
||||
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
|
||||
<PackageReference Include="TaskScheduler" Version="2.12.1" />
|
||||
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" />
|
||||
|
|
|
|||
|
|
@ -1,43 +1,53 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32.TaskScheduler;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public class AutoStartup
|
||||
{
|
||||
private static readonly string ClassName = nameof(AutoStartup);
|
||||
|
||||
private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
|
||||
private const string LogonTaskName = $"{Constant.FlowLauncher} Startup";
|
||||
private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup";
|
||||
|
||||
public static bool IsEnabled
|
||||
public static void CheckIsEnabled(bool useLogonTaskForStartup)
|
||||
{
|
||||
get
|
||||
// We need to check both because if both of them are enabled,
|
||||
// Hide Flow Launcher on startup will not work since the later one will trigger main window show event
|
||||
var logonTaskEnabled = CheckLogonTask();
|
||||
var registryEnabled = CheckRegistry();
|
||||
if (useLogonTaskForStartup)
|
||||
{
|
||||
// Check if logon task is enabled
|
||||
if (CheckLogonTask())
|
||||
// Enable logon task
|
||||
if (!logonTaskEnabled)
|
||||
{
|
||||
return true;
|
||||
Enable(true);
|
||||
}
|
||||
|
||||
// Check if registry is enabled
|
||||
try
|
||||
// Disable registry
|
||||
if (registryEnabled)
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
var path = key?.GetValue(Constant.FlowLauncher) as string;
|
||||
return path == Constant.ExecutablePath;
|
||||
Disable(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Enable registry
|
||||
if (!registryEnabled)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Ignoring non-critical registry error (querying if enabled): {e}");
|
||||
Enable(false);
|
||||
}
|
||||
// Disable logon task
|
||||
if (logonTaskEnabled)
|
||||
{
|
||||
Disable(true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,40 +60,63 @@ public class AutoStartup
|
|||
try
|
||||
{
|
||||
// Check if the action is the same as the current executable path
|
||||
var action = task.Definition.Actions.FirstOrDefault()!.ToString().Trim();
|
||||
if (!Constant.ExecutablePath.Equals(action, StringComparison.OrdinalIgnoreCase) && !File.Exists(action))
|
||||
// If not, we need to unschedule and reschedule the task
|
||||
if (task.Definition.Actions.FirstOrDefault() is Microsoft.Win32.TaskScheduler.Action taskAction)
|
||||
{
|
||||
UnscheduleLogonTask();
|
||||
ScheduleLogonTask();
|
||||
var action = taskAction.ToString().Trim();
|
||||
if (!action.Equals(Constant.ExecutablePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
UnscheduleLogonTask();
|
||||
ScheduleLogonTask();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to check logon task: {e}");
|
||||
App.API.LogError(ClassName, $"Failed to check logon task: {e}");
|
||||
throw; // Throw exception so that App.AutoStartup can show error message
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CheckRegistry()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
if (key != null)
|
||||
{
|
||||
// Check if the action is the same as the current executable path
|
||||
// If not, we need to unschedule and reschedule the task
|
||||
var action = (key.GetValue(Constant.FlowLauncher) as string) ?? string.Empty;
|
||||
if (!action.Equals(Constant.ExecutablePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
UnscheduleRegistry();
|
||||
ScheduleRegistry();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
App.API.LogError(ClassName, $"Failed to check registry: {e}");
|
||||
throw; // Throw exception so that App.AutoStartup can show error message
|
||||
}
|
||||
}
|
||||
|
||||
public static void DisableViaLogonTaskAndRegistry()
|
||||
{
|
||||
Disable(true);
|
||||
Disable(false);
|
||||
}
|
||||
|
||||
public static void EnableViaLogonTask()
|
||||
{
|
||||
Enable(true);
|
||||
}
|
||||
|
||||
public static void EnableViaRegistry()
|
||||
{
|
||||
Enable(false);
|
||||
}
|
||||
|
||||
public static void ChangeToViaLogonTask()
|
||||
{
|
||||
Disable(false);
|
||||
|
|
@ -106,13 +139,12 @@ public class AutoStartup
|
|||
}
|
||||
else
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.DeleteValue(Constant.FlowLauncher, false);
|
||||
UnscheduleRegistry();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to disable auto-startup: {e}");
|
||||
App.API.LogError(ClassName, $"Failed to disable auto-startup: {e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -127,13 +159,12 @@ public class AutoStartup
|
|||
}
|
||||
else
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
||||
ScheduleRegistry();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to enable auto-startup: {e}");
|
||||
App.API.LogError(ClassName, $"Failed to enable auto-startup: {e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +192,7 @@ public class AutoStartup
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to schedule logon task: {e}");
|
||||
App.API.LogError(ClassName, $"Failed to schedule logon task: {e}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -176,7 +207,7 @@ public class AutoStartup
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("AutoStartup", $"Failed to unschedule logon task: {e}");
|
||||
App.API.LogError(ClassName, $"Failed to unschedule logon task: {e}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -187,4 +218,18 @@ public class AutoStartup
|
|||
var principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
|
||||
private static bool UnscheduleRegistry()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.DeleteValue(Constant.FlowLauncher, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ScheduleRegistry()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Controls;
|
||||
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public class DwmDropShadow
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven).
|
||||
/// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect,
|
||||
/// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window).
|
||||
/// </summary>
|
||||
/// <param name="window">Window to which the shadow will be applied</param>
|
||||
public static void DropShadowToWindow(Window window)
|
||||
{
|
||||
if (!DropShadow(window))
|
||||
{
|
||||
window.SourceInitialized += window_SourceInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo
|
||||
{
|
||||
Window window = (Window)sender;
|
||||
|
||||
DropShadow(window);
|
||||
|
||||
window.SourceInitialized -= window_SourceInitialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The actual method that makes API calls to drop the shadow to the window
|
||||
/// </summary>
|
||||
/// <param name="window">Window to which the shadow will be applied</param>
|
||||
/// <returns>True if the method succeeded, false if not</returns>
|
||||
private static unsafe bool DropShadow(Window window)
|
||||
{
|
||||
try
|
||||
{
|
||||
WindowInteropHelper helper = new WindowInteropHelper(window);
|
||||
int val = 2;
|
||||
var ret1 = PInvoke.DwmSetWindowAttribute(new (helper.Handle), DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY, &val, 4);
|
||||
|
||||
if (ret1 == HRESULT.S_OK)
|
||||
{
|
||||
var m = new MARGINS { cyBottomHeight = 0, cxLeftWidth = 0, cxRightWidth = 0, cyTopHeight = 0 };
|
||||
var ret2 = PInvoke.DwmExtendFrameIntoClientArea(new(helper.Handle), &m);
|
||||
return ret2 == HRESULT.S_OK;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Probably dwmapi.dll not found (incompatible OS)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -28,18 +28,18 @@ public class DataWebRequestFactory : IWebRequestCreate
|
|||
|
||||
public DataWebResponse(Uri uri)
|
||||
{
|
||||
string uriString = uri.AbsoluteUri;
|
||||
var uriString = uri.AbsoluteUri;
|
||||
|
||||
int commaIndex = uriString.IndexOf(',');
|
||||
var headers = uriString.Substring(0, commaIndex).Split(';');
|
||||
var commaIndex = uriString.IndexOf(',');
|
||||
var headers = uriString[..commaIndex].Split(';');
|
||||
_contentType = headers[0];
|
||||
string dataString = uriString.Substring(commaIndex + 1);
|
||||
var dataString = uriString[(commaIndex + 1)..];
|
||||
_data = Convert.FromBase64String(dataString);
|
||||
}
|
||||
|
||||
public override string ContentType
|
||||
{
|
||||
get { return _contentType; }
|
||||
get => _contentType;
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
|
|
@ -48,7 +48,7 @@ public class DataWebRequestFactory : IWebRequestCreate
|
|||
|
||||
public override long ContentLength
|
||||
{
|
||||
get { return _data.Length; }
|
||||
get => _data.Length;
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue