mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into double-pin
This commit is contained in:
commit
c8a9e5e947
510 changed files with 23650 additions and 11801 deletions
80
.cm/gitstream.cm
Normal file
80
.cm/gitstream.cm
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# -*- mode: yaml -*-
|
||||||
|
# This example configuration for provides basic automations to get started with gitStream.
|
||||||
|
# View the gitStream quickstart for more examples: https://docs.gitstream.cm/examples/
|
||||||
|
manifest:
|
||||||
|
version: 1.0
|
||||||
|
|
||||||
|
|
||||||
|
triggers:
|
||||||
|
exclude:
|
||||||
|
branch:
|
||||||
|
- l10n_dev
|
||||||
|
- dev
|
||||||
|
- r/([Dd]ependabot|[Rr]enovate)/
|
||||||
|
|
||||||
|
|
||||||
|
automations:
|
||||||
|
# Add a label that indicates how many minutes it will take to review the PR.
|
||||||
|
estimated_time_to_review:
|
||||||
|
on:
|
||||||
|
- commit
|
||||||
|
if:
|
||||||
|
- true
|
||||||
|
run:
|
||||||
|
- action: add-label@v1
|
||||||
|
args:
|
||||||
|
label: "{{ calc.etr }} min review"
|
||||||
|
color: {{ colors.red if (calc.etr >= 20) else ( colors.yellow if (calc.etr >= 5) else colors.green ) }}
|
||||||
|
# Post a comment that lists the best experts for the files that were modified.
|
||||||
|
explain_code_experts:
|
||||||
|
if:
|
||||||
|
- true
|
||||||
|
run:
|
||||||
|
- action: explain-code-experts@v1
|
||||||
|
args:
|
||||||
|
gt: 10
|
||||||
|
# Post a comment notifying that the PR contains a TODO statement.
|
||||||
|
review_todo_comments:
|
||||||
|
if:
|
||||||
|
- {{ source.diff.files | matchDiffLines(regex=r/^[+].*\b(TODO|todo)\b/) | some }}
|
||||||
|
run:
|
||||||
|
- action: add-comment@v1
|
||||||
|
args:
|
||||||
|
comment: |
|
||||||
|
This PR contains a TODO statement. Please check to see if they should be removed.
|
||||||
|
# Post a comment that request a before and after screenshot
|
||||||
|
request_screenshot:
|
||||||
|
# Triggered for PRs that lack an image file or link to an image in the PR description
|
||||||
|
if:
|
||||||
|
- {{ not (has.screenshot_link or has.image_uploaded) }}
|
||||||
|
run:
|
||||||
|
- action: add-comment@v1
|
||||||
|
args:
|
||||||
|
comment: |
|
||||||
|
Be a legend :trophy: by adding a before and after screenshot of the changes you made, especially if they are around UI/UX.
|
||||||
|
|
||||||
|
|
||||||
|
# +----------------------------------------------------------------------------+
|
||||||
|
# | Custom Expressions |
|
||||||
|
# | https://docs.gitstream.cm/how-it-works/#custom-expressions |
|
||||||
|
# +----------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
calc:
|
||||||
|
etr: {{ branch | estimatedReviewTime }}
|
||||||
|
|
||||||
|
colors:
|
||||||
|
red: 'b60205'
|
||||||
|
yellow: 'fbca04'
|
||||||
|
green: '0e8a16'
|
||||||
|
|
||||||
|
changes:
|
||||||
|
# Sum all the lines added/edited in the PR
|
||||||
|
additions: {{ branch.diff.files_metadata | map(attr='additions') | sum }}
|
||||||
|
# Sum all the line removed in the PR
|
||||||
|
deletions: {{ branch.diff.files_metadata | map(attr='deletions') | sum }}
|
||||||
|
# Calculate the ratio of new code
|
||||||
|
ratio: {{ (changes.additions / (changes.additions + changes.deletions)) * 100 | round(2) }}
|
||||||
|
|
||||||
|
has:
|
||||||
|
screenshot_link: {{ pr.description | includes(regex=r/!\[.*\]\(.*(jpg|svg|png|gif|psd).*\)/) }}
|
||||||
|
image_uploaded: {{ pr.description | includes(regex=r/(<img.*src.*(jpg|svg|png|gif|psd).*>)|!\[image\]\(.*github\.com.*\)/) }}
|
||||||
1
.github/actions/spelling/expect.txt
vendored
1
.github/actions/spelling/expect.txt
vendored
|
|
@ -98,6 +98,7 @@ Português
|
||||||
Português (Brasil)
|
Português (Brasil)
|
||||||
Italiano
|
Italiano
|
||||||
Slovenský
|
Slovenský
|
||||||
|
quicklook
|
||||||
Tiếng Việt
|
Tiếng Việt
|
||||||
Droplex
|
Droplex
|
||||||
Preinstalled
|
Preinstalled
|
||||||
|
|
|
||||||
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
|
|
@ -8,7 +8,8 @@ updates:
|
||||||
- package-ecosystem: "nuget" # See documentation for possible values
|
- package-ecosystem: "nuget" # See documentation for possible values
|
||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 3
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "squirrel-windows"
|
- dependency-name: "squirrel-windows"
|
||||||
reviewers:
|
reviewers:
|
||||||
|
|
|
||||||
31
.github/pr-labeler.yml
vendored
Normal file
31
.github/pr-labeler.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# The bot always updates the labels, add/remove as necessary [default: false]
|
||||||
|
alwaysReplace: false
|
||||||
|
# Treats the text and labels as case sensitive [default: true]
|
||||||
|
caseSensitive: false
|
||||||
|
# Array of labels to be applied to the PR [default: []]
|
||||||
|
customLabels:
|
||||||
|
# Finds the `text` within the PR title and body and applies the `label`
|
||||||
|
- text: 'bug'
|
||||||
|
label: 'bug'
|
||||||
|
- text: 'fix'
|
||||||
|
label: 'bug'
|
||||||
|
- text: 'dependabot'
|
||||||
|
label: 'bug'
|
||||||
|
- text: 'New Crowdin updates'
|
||||||
|
label: 'bug'
|
||||||
|
- text: 'New Crowdin updates'
|
||||||
|
label: 'kind/i18n'
|
||||||
|
- text: 'feature'
|
||||||
|
label: 'enhancement'
|
||||||
|
- text: 'add new'
|
||||||
|
label: 'enhancement'
|
||||||
|
- text: 'refactor'
|
||||||
|
label: 'enhancement'
|
||||||
|
- text: 'refactor'
|
||||||
|
label: 'Code Refactor'
|
||||||
|
# Search the body of the PR for the `text` [default: true]
|
||||||
|
searchBody: true
|
||||||
|
# Search the title of the PR for the `text` [default: true]
|
||||||
|
searchTitle: true
|
||||||
|
# Search for whole words only [default: false]
|
||||||
|
wholeWords: false
|
||||||
4
.github/workflows/default_plugins.yml
vendored
4
.github/workflows/default_plugins.yml
vendored
|
|
@ -2,7 +2,7 @@ name: Publish Default Plugins
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['dev']
|
branches: ['master']
|
||||||
paths: ['Plugins/**']
|
paths: ['Plugins/**']
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
@ -369,4 +369,4 @@ jobs:
|
||||||
tag_name: "v${{steps.updated-version-windowssettings.outputs.prop}}"
|
tag_name: "v${{steps.updated-version-windowssettings.outputs.prop}}"
|
||||||
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||||
|
|
|
||||||
49
.github/workflows/gitstream.yml
vendored
Normal file
49
.github/workflows/gitstream.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Code generated by gitStream GitHub app - DO NOT EDIT
|
||||||
|
|
||||||
|
name: gitStream workflow automation
|
||||||
|
run-name: |
|
||||||
|
/:\ gitStream: PR #${{ fromJSON(fromJSON(github.event.inputs.client_payload)).pullRequestNumber }} from ${{ github.event.inputs.full_repository }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
client_payload:
|
||||||
|
description: The Client payload
|
||||||
|
required: true
|
||||||
|
full_repository:
|
||||||
|
description: the repository name include the owner in `owner/repo_name` format
|
||||||
|
required: true
|
||||||
|
head_ref:
|
||||||
|
description: the head sha
|
||||||
|
required: true
|
||||||
|
base_ref:
|
||||||
|
description: the base ref
|
||||||
|
required: true
|
||||||
|
installation_id:
|
||||||
|
description: the installation id
|
||||||
|
required: false
|
||||||
|
resolver_url:
|
||||||
|
description: the resolver url to pass results to
|
||||||
|
required: true
|
||||||
|
resolver_token:
|
||||||
|
description: Optional resolver token for resolver service
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitStream:
|
||||||
|
timeout-minutes: 5
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: gitStream workflow automation
|
||||||
|
steps:
|
||||||
|
- name: Evaluate Rules
|
||||||
|
uses: linear-b/gitstream-github-action@v2
|
||||||
|
id: rules-engine
|
||||||
|
with:
|
||||||
|
full_repository: ${{ github.event.inputs.full_repository }}
|
||||||
|
head_ref: ${{ github.event.inputs.head_ref }}
|
||||||
|
base_ref: ${{ github.event.inputs.base_ref }}
|
||||||
|
client_payload: ${{ github.event.inputs.client_payload }}
|
||||||
|
installation_id: ${{ github.event.inputs.installation_id }}
|
||||||
|
resolver_url: ${{ github.event.inputs.resolver_url }}
|
||||||
|
resolver_token: ${{ github.event.inputs.resolver_token }}
|
||||||
17
.github/workflows/pr_assignee.yml
vendored
Normal file
17
.github/workflows/pr_assignee.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
name: Assign PR to creator
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
branches-ignore:
|
||||||
|
- l10n_dev
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Assign PR to creator
|
||||||
|
uses: toshimaru/auto-author-assign@v2.1.1
|
||||||
22
.github/workflows/pr_milestone.yml
vendored
Normal file
22
.github/workflows/pr_milestone.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: Set Milestone
|
||||||
|
|
||||||
|
# Assigns the earliest created milestone that matches the below glob pattern.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: set-milestone
|
||||||
|
uses: andrefcdias/add-to-milestone@v1.3.0
|
||||||
|
with:
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
milestone: "+([0-9]).+([0-9]).+([0-9])"
|
||||||
|
use-expression: true
|
||||||
25
.github/workflows/top-ranking-issues.yml
vendored
Normal file
25
.github/workflows/top-ranking-issues.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: Top-Ranking Issues
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 */1 * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ShowAndLabelTopIssues:
|
||||||
|
name: Display and label top issues.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Top Issues action
|
||||||
|
uses: rickstaa/top-issues-action@v1.3.101
|
||||||
|
env:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
dashboard: true
|
||||||
|
dashboard_show_total_reactions: true
|
||||||
|
top_list_size: 10
|
||||||
|
top_features: true
|
||||||
|
top_bugs: true
|
||||||
|
dashboard_title: Top-Ranking Issues 📈
|
||||||
|
dashboard_label: ⭐ Dashboard
|
||||||
|
hide_dashboard_footer: true
|
||||||
|
top_issues: false
|
||||||
|
|
@ -9,16 +9,20 @@ using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Configuration
|
namespace Flow.Launcher.Core.Configuration
|
||||||
{
|
{
|
||||||
public class Portable : IPortable
|
public class Portable : IPortable
|
||||||
{
|
{
|
||||||
|
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private UpdateManager NewUpdateManager()
|
private static UpdateManager NewUpdateManager()
|
||||||
{
|
{
|
||||||
var applicationFolderName = Constant.ApplicationDirectory
|
var applicationFolderName = Constant.ApplicationDirectory
|
||||||
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
|
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
|
||||||
|
|
@ -40,7 +44,7 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
#endif
|
#endif
|
||||||
IndicateDeletion(DataLocation.PortableDataPath);
|
IndicateDeletion(DataLocation.PortableDataPath);
|
||||||
|
|
||||||
MessageBox.Show("Flow Launcher needs to restart to finish disabling portable mode, " +
|
API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " +
|
||||||
"after the restart your portable data profile will be deleted and roaming data profile kept");
|
"after the restart your portable data profile will be deleted and roaming data profile kept");
|
||||||
|
|
||||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||||
|
|
@ -64,7 +68,7 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
#endif
|
#endif
|
||||||
IndicateDeletion(DataLocation.RoamingDataPath);
|
IndicateDeletion(DataLocation.RoamingDataPath);
|
||||||
|
|
||||||
MessageBox.Show("Flow Launcher needs to restart to finish enabling portable mode, " +
|
API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " +
|
||||||
"after the restart your roaming data profile will be deleted and portable data profile kept");
|
"after the restart your roaming data profile will be deleted and portable data profile kept");
|
||||||
|
|
||||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||||
|
|
@ -77,41 +81,35 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
|
|
||||||
public void RemoveShortcuts()
|
public void RemoveShortcuts()
|
||||||
{
|
{
|
||||||
using (var portabilityUpdater = NewUpdateManager())
|
using var portabilityUpdater = NewUpdateManager();
|
||||||
{
|
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
|
||||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
|
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
|
||||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
|
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
|
||||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveUninstallerEntry()
|
public void RemoveUninstallerEntry()
|
||||||
{
|
{
|
||||||
using (var portabilityUpdater = NewUpdateManager())
|
using var portabilityUpdater = NewUpdateManager();
|
||||||
{
|
portabilityUpdater.RemoveUninstallerRegistryEntry();
|
||||||
portabilityUpdater.RemoveUninstallerRegistryEntry();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
||||||
{
|
{
|
||||||
FilesFolders.CopyAll(fromLocation, toLocation);
|
FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||||
VerifyUserDataAfterMove(fromLocation, toLocation);
|
VerifyUserDataAfterMove(fromLocation, toLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
|
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
|
||||||
{
|
{
|
||||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation);
|
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateShortcuts()
|
public void CreateShortcuts()
|
||||||
{
|
{
|
||||||
using (var portabilityUpdater = NewUpdateManager())
|
using var portabilityUpdater = NewUpdateManager();
|
||||||
{
|
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
|
||||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
|
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
|
||||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
|
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
|
||||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateUninstallerEntry()
|
public void CreateUninstallerEntry()
|
||||||
|
|
@ -125,18 +123,14 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String);
|
subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var portabilityUpdater = NewUpdateManager())
|
using var portabilityUpdater = NewUpdateManager();
|
||||||
{
|
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||||
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void IndicateDeletion(string filePathTodelete)
|
private static void IndicateDeletion(string filePathTodelete)
|
||||||
{
|
{
|
||||||
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);
|
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);
|
||||||
using (var _ = File.CreateText(deleteFilePath))
|
using var _ = File.CreateText(deleteFilePath);
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///<summary>
|
///<summary>
|
||||||
|
|
@ -157,13 +151,13 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
// delete it and prompt the user to pick the portable data location
|
// delete it and prompt the user to pick the portable data location
|
||||||
if (File.Exists(roamingDataDeleteFilePath))
|
if (File.Exists(roamingDataDeleteFilePath))
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(roamingDataDir);
|
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
if (MessageBox.Show("Flow Launcher has detected you enabled portable mode, " +
|
if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " +
|
||||||
"would you like to move it to a different location?", string.Empty,
|
"would you like to move it to a different location?", string.Empty,
|
||||||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||||
{
|
{
|
||||||
FilesFolders.OpenPath(Constant.RootDirectory);
|
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
|
|
@ -172,9 +166,9 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
// delete it and notify the user about it.
|
// delete it and notify the user about it.
|
||||||
else if (File.Exists(portableDataDeleteFilePath))
|
else if (File.Exists(portableDataDeleteFilePath))
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(portableDataDir);
|
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
MessageBox.Show("Flow Launcher has detected you disabled portable mode, " +
|
API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " +
|
||||||
"the relevant shortcuts and uninstaller entry have been created");
|
"the relevant shortcuts and uninstaller entry have been created");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +180,7 @@ namespace Flow.Launcher.Core.Configuration
|
||||||
|
|
||||||
if (roamingLocationExists && portableLocationExists)
|
if (roamingLocationExists && portableLocationExists)
|
||||||
{
|
{
|
||||||
MessageBox.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " +
|
API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " +
|
||||||
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
|
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
|
||||||
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Flow.Launcher.Infrastructure.Http;
|
using Flow.Launcher.Infrastructure.Http;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.ExternalPlugins
|
namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using System;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
|
||||||
using Flow.Launcher.Plugin;
|
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
{
|
{
|
||||||
public abstract class AbstractPluginEnvironment
|
public abstract class AbstractPluginEnvironment
|
||||||
{
|
{
|
||||||
|
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
|
|
||||||
internal abstract string Language { get; }
|
internal abstract string Language { get; }
|
||||||
|
|
||||||
internal abstract string EnvName { get; }
|
internal abstract string EnvName { get; }
|
||||||
|
|
@ -24,7 +28,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
internal virtual string FileDialogFilter => string.Empty;
|
internal virtual string FileDialogFilter => string.Empty;
|
||||||
|
|
||||||
internal abstract string PluginsSettingsFilePath { get; set; }
|
internal abstract string PluginsSettingsFilePath { get; set; }
|
||||||
|
|
||||||
internal List<PluginMetadata> PluginMetadataList;
|
internal List<PluginMetadata> PluginMetadataList;
|
||||||
|
|
||||||
|
|
@ -38,8 +42,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
internal IEnumerable<PluginPair> Setup()
|
internal IEnumerable<PluginPair> Setup()
|
||||||
{
|
{
|
||||||
|
// If no plugin is using the language, return empty list
|
||||||
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
|
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
return new List<PluginPair>();
|
return new List<PluginPair>();
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
|
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
|
||||||
{
|
{
|
||||||
|
|
@ -50,24 +57,56 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " +
|
var noRuntimeMessage = string.Format(
|
||||||
$"will require {EnvName} to run. Would you like to download {EnvName}? " +
|
API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
|
||||||
Environment.NewLine + Environment.NewLine +
|
Language,
|
||||||
"Click no if it's already installed, " +
|
EnvName,
|
||||||
$"and you will be prompted to select the folder that contains the {EnvName} executable",
|
Environment.NewLine
|
||||||
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
|
);
|
||||||
|
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||||
{
|
{
|
||||||
var msg = $"Please select the {EnvName} executable";
|
var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
|
||||||
string selectedFile;
|
|
||||||
|
|
||||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(selectedFile))
|
if (!string.IsNullOrEmpty(selectedFile))
|
||||||
|
{
|
||||||
PluginsSettingsFilePath = selectedFile;
|
PluginsSettingsFilePath = selectedFile;
|
||||||
|
}
|
||||||
// Nothing selected because user pressed cancel from the file dialog window
|
// Nothing selected because user pressed cancel from the file dialog window
|
||||||
if (string.IsNullOrEmpty(selectedFile))
|
else
|
||||||
InstallEnvironment();
|
{
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -80,8 +119,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
|
||||||
$"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom).");
|
|
||||||
Log.Error("PluginsLoader",
|
Log.Error("PluginsLoader",
|
||||||
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
||||||
$"{Language}Environment");
|
$"{Language}Environment");
|
||||||
|
|
@ -94,13 +132,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
|
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
|
||||||
{
|
{
|
||||||
if (expectedPath == currentPath)
|
if (expectedPath == currentPath) return;
|
||||||
return;
|
|
||||||
|
|
||||||
FilesFolders.RemoveFolderIfExists(installedDirPath);
|
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
InstallEnvironment();
|
InstallEnvironment();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
|
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
|
||||||
|
|
@ -112,13 +148,16 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
foreach (var metadata in PluginMetadataList)
|
foreach (var metadata in PluginMetadataList)
|
||||||
{
|
{
|
||||||
if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase))
|
if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
metadata.AssemblyName = string.Empty;
|
||||||
pluginPairs.Add(CreatePluginPair(filePath, metadata));
|
pluginPairs.Add(CreatePluginPair(filePath, metadata));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pluginPairs;
|
return pluginPairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetFileFromDialog(string title, string filter = "")
|
private static string GetFileFromDialog(string title, string filter = "")
|
||||||
{
|
{
|
||||||
var dlg = new OpenFileDialog
|
var dlg = new OpenFileDialog
|
||||||
{
|
{
|
||||||
|
|
@ -132,7 +171,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
var result = dlg.ShowDialog();
|
var result = dlg.ShowDialog();
|
||||||
return result == DialogResult.OK ? dlg.FileName : string.Empty;
|
return result == DialogResult.OK ? dlg.FileName : string.Empty;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -175,31 +213,33 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
|
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
|
||||||
|
{
|
||||||
settings.PluginSettings.PythonExecutablePath
|
settings.PluginSettings.PythonExecutablePath
|
||||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
|
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
|
||||||
|
}
|
||||||
|
|
||||||
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
|
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
|
||||||
|
{
|
||||||
settings.PluginSettings.NodeExecutablePath
|
settings.PluginSettings.NodeExecutablePath
|
||||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
|
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
|
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(filePath))
|
if (string.IsNullOrEmpty(filePath)) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
// DataLocation.PortableDataPath returns the current portable path, this determines if an out
|
// DataLocation.PortableDataPath returns the current portable path, this determines if an out
|
||||||
// of date path is also a portable path.
|
// 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);
|
return filePath.Contains(portableAppEnvLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsUsingRoamingPath(string filePath)
|
private static bool IsUsingRoamingPath(string filePath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(filePath))
|
if (string.IsNullOrEmpty(filePath)) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
return filePath.StartsWith(DataLocation.RoamingDataPath);
|
return filePath.StartsWith(DataLocation.RoamingDataPath);
|
||||||
}
|
}
|
||||||
|
|
@ -209,8 +249,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
|
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
|
||||||
|
|
||||||
// get the substring after "Environments" because we can not determine it dynamically
|
// get the substring after "Environments" because we can not determine it dynamically
|
||||||
var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count());
|
var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
|
||||||
return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}";
|
return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using Droplex;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using Droplex;
|
||||||
using Flow.Launcher.Core.Plugin;
|
using Flow.Launcher.Core.Plugin;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
{
|
{
|
||||||
|
|
@ -22,13 +22,17 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
|
|
||||||
internal override string FileDialogFilter => "Python|pythonw.exe";
|
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) { }
|
internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||||
|
|
||||||
internal override void InstallEnvironment()
|
internal override void InstallEnvironment()
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(InstallPath);
|
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
// Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and
|
// 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
|
// uses Python plugin they need to custom install and use v3.8.9
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Droplex;
|
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
|
||||||
using Flow.Launcher.Plugin;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Droplex;
|
||||||
using Flow.Launcher.Core.Plugin;
|
using Flow.Launcher.Core.Plugin;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
{
|
{
|
||||||
|
|
@ -19,13 +19,17 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
|
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 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) { }
|
internal TypeScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||||
|
|
||||||
internal override void InstallEnvironment()
|
internal override void InstallEnvironment()
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(InstallPath);
|
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Droplex;
|
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
|
||||||
using Flow.Launcher.Plugin;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Droplex;
|
||||||
using Flow.Launcher.Core.Plugin;
|
using Flow.Launcher.Core.Plugin;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
{
|
{
|
||||||
|
|
@ -19,13 +19,17 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||||
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
|
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 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) { }
|
internal TypeScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
|
||||||
|
|
||||||
internal override void InstallEnvironment()
|
internal override void InstallEnvironment()
|
||||||
{
|
{
|
||||||
FilesFolders.RemoveFolderIfExists(InstallPath);
|
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.ExternalPlugins
|
namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
{
|
{
|
||||||
|
|
@ -17,11 +18,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
||||||
|
|
||||||
private static DateTime lastFetchedAt = DateTime.MinValue;
|
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 List<UserPlugin> UserPlugins { get; private set; }
|
||||||
|
|
||||||
public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
|
public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -31,18 +32,26 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
||||||
{
|
{
|
||||||
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
|
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
|
||||||
|
|
||||||
UserPlugins = results;
|
// If the results are empty, we shouldn't update the manifest because the results are invalid.
|
||||||
lastFetchedAt = DateTime.Now;
|
if (results.Count != 0)
|
||||||
|
{
|
||||||
|
UserPlugins = results;
|
||||||
|
lastFetchedAt = DateTime.Now;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e);
|
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(nameof(PluginsManifest), "Http request failed", e);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
manifestUpdateLock.Release();
|
manifestUpdateLock.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -54,11 +54,11 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Droplex" Version="1.7.0" />
|
<PackageReference Include="Droplex" Version="1.7.0" />
|
||||||
<PackageReference Include="FSharp.Core" Version="7.0.401" />
|
<PackageReference Include="FSharp.Core" Version="9.0.201" />
|
||||||
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.2.1" />
|
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.0" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||||
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
||||||
<PackageReference Include="StreamJsonRpc" Version="2.17.11" />
|
<PackageReference Include="StreamJsonRpc" Version="2.21.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,16 @@
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
using Flow.Launcher.Infrastructure;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Microsoft.IO;
|
using Microsoft.IO;
|
||||||
using System.Windows;
|
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
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -42,7 +30,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
private int RequestId { get; set; }
|
private int RequestId { get; set; }
|
||||||
|
|
||||||
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
|
private string SettingPath => Path.Combine(Context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "Settings.json");
|
||||||
|
|
||||||
public override List<Result> LoadContextMenus(Result selectedResult)
|
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,15 @@
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
using Flow.Launcher.Infrastructure;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Microsoft.IO;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
using YamlDotNet.Serialization.NamingConventions;
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
using CheckBox = System.Windows.Controls.CheckBox;
|
|
||||||
using Control = System.Windows.Controls.Control;
|
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
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -34,7 +17,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
/// Represent the plugin that using JsonPRC
|
/// Represent the plugin that using JsonPRC
|
||||||
/// every JsonRPC plugin should has its own plugin instance
|
/// every JsonRPC plugin should has its own plugin instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||||
{
|
{
|
||||||
protected PluginInitContext Context;
|
protected PluginInitContext Context;
|
||||||
public const string JsonRPC = "JsonRPC";
|
public const string JsonRPC = "JsonRPC";
|
||||||
|
|
@ -44,8 +27,9 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
private string SettingConfigurationPath =>
|
private string SettingConfigurationPath =>
|
||||||
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||||
|
|
||||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory,
|
private string SettingDirectory => Context.CurrentPluginMetadata.PluginSettingsDirectoryPath;
|
||||||
Context.CurrentPluginMetadata.Name, "Settings.json");
|
|
||||||
|
private string SettingPath => Path.Combine(SettingDirectory, "Settings.json");
|
||||||
|
|
||||||
public abstract List<Result> LoadContextMenus(Result selectedResult);
|
public abstract List<Result> LoadContextMenus(Result selectedResult);
|
||||||
|
|
||||||
|
|
@ -155,6 +139,11 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
Settings?.Save();
|
Settings?.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool NeedCreateSettingPanel()
|
||||||
|
{
|
||||||
|
return Settings.NeedCreateSettingPanel();
|
||||||
|
}
|
||||||
|
|
||||||
public Control CreateSettingPanel()
|
public Control CreateSettingPanel()
|
||||||
{
|
{
|
||||||
return Settings.CreateSettingPanel();
|
return Settings.CreateSettingPanel();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
|
@ -7,6 +8,8 @@ using System.Windows.Documents;
|
||||||
using Flow.Launcher.Infrastructure.Storage;
|
using Flow.Launcher.Infrastructure.Storage;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
public class JsonRPCPluginSettings
|
public class JsonRPCPluginSettings
|
||||||
|
|
@ -16,51 +19,74 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
public required string SettingPath { get; init; }
|
public required string SettingPath { get; init; }
|
||||||
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
|
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, object> Inner => Settings;
|
public IReadOnlyDictionary<string, object?> Inner => Settings;
|
||||||
protected ConcurrentDictionary<string, object> Settings { get; set; }
|
protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!;
|
||||||
public required IPublicAPI API { get; init; }
|
public required IPublicAPI API { get; init; }
|
||||||
|
|
||||||
private JsonStorage<ConcurrentDictionary<string, object>> _storage;
|
private static readonly string ClassName = nameof(JsonRPCPluginSettings);
|
||||||
|
|
||||||
// maybe move to resource?
|
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
|
||||||
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 = (Thickness)Application.Current.FindResource("SettingPanelMargin");
|
||||||
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
|
private static readonly Thickness SettingPanelItemLeftMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftMargin");
|
||||||
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
|
private static readonly Thickness SettingPanelItemTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemTopBottomMargin");
|
||||||
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
|
private static readonly Thickness SettingPanelItemLeftTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftTopBottomMargin");
|
||||||
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
|
private static readonly double SettingPanelTextBoxMinWidth = (double)Application.Current.FindResource("SettingPanelTextBoxMinWidth");
|
||||||
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
|
private static readonly double SettingPanelPathTextBoxWidth = (double)Application.Current.FindResource("SettingPanelPathTextBoxWidth");
|
||||||
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
|
private static readonly double SettingPanelAreaTextBoxMinHeight = (double)Application.Current.FindResource("SettingPanelAreaTextBoxMinHeight");
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
_storage = new JsonStorage<ConcurrentDictionary<string, object>>(SettingPath);
|
if (Settings == null)
|
||||||
Settings = await _storage.LoadAsync();
|
|
||||||
|
|
||||||
if (Configuration == 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)
|
foreach (var (type, attributes) in Configuration.Body)
|
||||||
{
|
{
|
||||||
if (attributes.Name == null)
|
// Skip if the setting does not have attributes or name
|
||||||
{
|
if (attributes?.Name == null) continue;
|
||||||
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;
|
Settings[attributes.Name] = attributes.DefaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void UpdateSettings(IReadOnlyDictionary<string, object> settings)
|
public void UpdateSettings(IReadOnlyDictionary<string, object> settings)
|
||||||
{
|
{
|
||||||
if (settings == null || settings.Count == 0)
|
if (settings == null || settings.Count == 0) return;
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var (key, value) in settings)
|
foreach (var (key, value) in settings)
|
||||||
{
|
{
|
||||||
|
|
@ -71,19 +97,23 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
switch (control)
|
switch (control)
|
||||||
{
|
{
|
||||||
case TextBox textBox:
|
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;
|
break;
|
||||||
case PasswordBox passwordBox:
|
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;
|
break;
|
||||||
case ComboBox comboBox:
|
case ComboBox comboBox:
|
||||||
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
||||||
break;
|
break;
|
||||||
case CheckBox checkBox:
|
case CheckBox checkBox:
|
||||||
checkBox.Dispatcher.Invoke(() =>
|
var isChecked = value is bool boolValue
|
||||||
checkBox.IsChecked = value is bool isChecked
|
? boolValue
|
||||||
? isChecked
|
// If can parse the default value to bool, use it, otherwise use false
|
||||||
: bool.Parse(value as string ?? string.Empty));
|
: value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString)
|
||||||
|
&& boolValueFromString;
|
||||||
|
checkBox.Dispatcher.Invoke(() =>checkBox.IsChecked = isChecked);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,322 +124,381 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
public async Task SaveAsync()
|
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()
|
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
|
||||||
|
return Settings != null && Configuration != null && Configuration.Body.Count != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Control CreateSettingPanel()
|
public Control CreateSettingPanel()
|
||||||
{
|
{
|
||||||
if (Settings == null || Settings.Count == 0)
|
// No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true
|
||||||
return new();
|
// if (!NeedCreateSettingPanel()) return null;
|
||||||
|
|
||||||
var settingWindow = new UserControl();
|
// Create main grid with two columns (Column 1: Auto, Column 2: *)
|
||||||
var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
||||||
|
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
|
||||||
ColumnDefinition gridCol1 = new ColumnDefinition();
|
|
||||||
ColumnDefinition gridCol2 = new ColumnDefinition();
|
|
||||||
|
|
||||||
gridCol1.Width = new GridLength(70, GridUnitType.Star);
|
|
||||||
gridCol2.Width = new GridLength(30, GridUnitType.Star);
|
|
||||||
mainPanel.ColumnDefinitions.Add(gridCol1);
|
|
||||||
mainPanel.ColumnDefinitions.Add(gridCol2);
|
|
||||||
settingWindow.Content = mainPanel;
|
|
||||||
int rowCount = 0;
|
|
||||||
|
|
||||||
foreach (var (type, attribute) in Configuration.Body)
|
|
||||||
{
|
{
|
||||||
Separator sep = new Separator();
|
Width = new GridLength(0, GridUnitType.Auto)
|
||||||
sep.VerticalAlignment = VerticalAlignment.Top;
|
});
|
||||||
sep.Margin = settingSepMargin;
|
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
|
||||||
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
|
{
|
||||||
var panel = new StackPanel
|
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,
|
Height = new GridLength(0, GridUnitType.Auto)
|
||||||
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);
|
|
||||||
|
|
||||||
|
// State controls for column 0 and 1
|
||||||
|
StackPanel? panel = null;
|
||||||
FrameworkElement contentControl;
|
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)
|
switch (type)
|
||||||
{
|
{
|
||||||
case "textBlock":
|
case "textBlock":
|
||||||
{
|
|
||||||
contentControl = new TextBlock
|
|
||||||
{
|
{
|
||||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
contentControl = new TextBlock
|
||||||
Margin = settingTextBlockMargin,
|
{
|
||||||
Padding = new Thickness(0, 0, 0, 0),
|
Text = attributes.Description?.Replace("\\r\\n", "\r\n") ?? string.Empty,
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
TextAlignment = TextAlignment.Left,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
TextWrapping = TextWrapping.Wrap
|
Margin = SettingPanelItemTopBottomMargin,
|
||||||
};
|
TextAlignment = TextAlignment.Left,
|
||||||
|
TextWrapping = TextWrapping.Wrap
|
||||||
|
};
|
||||||
|
|
||||||
Grid.SetColumn(contentControl, 0);
|
break;
|
||||||
Grid.SetColumnSpan(contentControl, 2);
|
}
|
||||||
Grid.SetRow(contentControl, rowCount);
|
|
||||||
if (rowCount != 0)
|
|
||||||
mainPanel.Children.Add(sep);
|
|
||||||
|
|
||||||
Grid.SetRow(sep, rowCount);
|
|
||||||
Grid.SetColumn(sep, 0);
|
|
||||||
Grid.SetColumnSpan(sep, 2);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "input":
|
case "input":
|
||||||
{
|
|
||||||
var textBox = new TextBox()
|
|
||||||
{
|
{
|
||||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
var textBox = new TextBox()
|
||||||
Margin = settingControlMargin,
|
{
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
MinWidth = SettingPanelTextBoxMinWidth,
|
||||||
ToolTip = attribute.Description
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
};
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||||
|
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||||
|
ToolTip = attributes.Description
|
||||||
|
};
|
||||||
|
|
||||||
textBox.TextChanged += (_, _) =>
|
textBox.TextChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
Settings[attribute.Name] = textBox.Text;
|
Settings[attributes.Name] = textBox.Text;
|
||||||
};
|
};
|
||||||
|
|
||||||
contentControl = textBox;
|
contentControl = textBox;
|
||||||
Grid.SetColumn(contentControl, 1);
|
|
||||||
Grid.SetRow(contentControl, rowCount);
|
|
||||||
if (rowCount != 0)
|
|
||||||
mainPanel.Children.Add(sep);
|
|
||||||
|
|
||||||
Grid.SetRow(sep, rowCount);
|
break;
|
||||||
Grid.SetColumn(sep, 0);
|
}
|
||||||
Grid.SetColumnSpan(sep, 2);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "inputWithFileBtn":
|
case "inputWithFileBtn":
|
||||||
{
|
case "inputWithFolderBtn":
|
||||||
var textBox = new TextBox()
|
|
||||||
{
|
{
|
||||||
Margin = new Thickness(10, 0, 0, 0),
|
var textBox = new TextBox()
|
||||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
{
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
Width = SettingPanelPathTextBoxWidth,
|
||||||
ToolTip = attribute.Description
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
};
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = SettingPanelItemLeftMargin,
|
||||||
|
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||||
|
ToolTip = attributes.Description
|
||||||
|
};
|
||||||
|
|
||||||
textBox.TextChanged += (_, _) =>
|
textBox.TextChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
Settings[attribute.Name] = textBox.Text;
|
Settings[attributes.Name] = textBox.Text;
|
||||||
};
|
};
|
||||||
|
|
||||||
var Btn = new System.Windows.Controls.Button()
|
var Btn = new Button()
|
||||||
{
|
{
|
||||||
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
};
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = SettingPanelItemLeftMargin,
|
||||||
|
Content = API.GetTranslation("select")
|
||||||
|
};
|
||||||
|
|
||||||
var dockPanel = new DockPanel() { Margin = settingControlMargin };
|
Btn.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
using System.Windows.Forms.CommonDialog dialog = type switch
|
||||||
|
{
|
||||||
|
"inputWithFolderBtn" => new System.Windows.Forms.FolderBrowserDialog(),
|
||||||
|
_ => new System.Windows.Forms.OpenFileDialog(),
|
||||||
|
};
|
||||||
|
|
||||||
DockPanel.SetDock(Btn, Dock.Right);
|
if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
||||||
dockPanel.Children.Add(Btn);
|
{
|
||||||
dockPanel.Children.Add(textBox);
|
return;
|
||||||
contentControl = dockPanel;
|
}
|
||||||
Grid.SetColumn(contentControl, 1);
|
|
||||||
Grid.SetRow(contentControl, rowCount);
|
|
||||||
if (rowCount != 0)
|
|
||||||
mainPanel.Children.Add(sep);
|
|
||||||
|
|
||||||
Grid.SetRow(sep, rowCount);
|
var path = dialog switch
|
||||||
Grid.SetColumn(sep, 0);
|
{
|
||||||
Grid.SetColumnSpan(sep, 2);
|
System.Windows.Forms.FolderBrowserDialog folderDialog => folderDialog.SelectedPath,
|
||||||
|
System.Windows.Forms.OpenFileDialog fileDialog => fileDialog.FileName,
|
||||||
|
_ => throw new System.NotImplementedException()
|
||||||
|
};
|
||||||
|
|
||||||
break;
|
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":
|
case "textarea":
|
||||||
{
|
|
||||||
var textBox = new TextBox()
|
|
||||||
{
|
{
|
||||||
Height = 120,
|
var textBox = new TextBox()
|
||||||
Margin = settingControlMargin,
|
{
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
MinHeight = SettingPanelAreaTextBoxMinHeight,
|
||||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
AcceptsReturn = true,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||||
ToolTip = attribute.Description
|
AcceptsReturn = true,
|
||||||
};
|
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||||
|
ToolTip = attributes.Description
|
||||||
|
};
|
||||||
|
|
||||||
textBox.TextChanged += (sender, _) =>
|
textBox.TextChanged += (sender, _) =>
|
||||||
{
|
{
|
||||||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
Settings[attributes.Name] = ((TextBox)sender).Text;
|
||||||
};
|
};
|
||||||
|
|
||||||
contentControl = textBox;
|
contentControl = textBox;
|
||||||
Grid.SetColumn(contentControl, 1);
|
|
||||||
Grid.SetRow(contentControl, rowCount);
|
|
||||||
if (rowCount != 0)
|
|
||||||
mainPanel.Children.Add(sep);
|
|
||||||
|
|
||||||
Grid.SetRow(sep, rowCount);
|
break;
|
||||||
Grid.SetColumn(sep, 0);
|
}
|
||||||
Grid.SetColumnSpan(sep, 2);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "passwordBox":
|
case "passwordBox":
|
||||||
{
|
|
||||||
var passwordBox = new PasswordBox()
|
|
||||||
{
|
{
|
||||||
Margin = settingControlMargin,
|
var passwordBox = new PasswordBox()
|
||||||
Password = Settings[attribute.Name] as string ?? string.Empty,
|
{
|
||||||
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
|
MinWidth = SettingPanelTextBoxMinWidth,
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
ToolTip = attribute.Description
|
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, _) =>
|
passwordBox.PasswordChanged += (sender, _) =>
|
||||||
{
|
{
|
||||||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
Settings[attributes.Name] = ((PasswordBox)sender).Password;
|
||||||
};
|
};
|
||||||
|
|
||||||
contentControl = passwordBox;
|
contentControl = passwordBox;
|
||||||
Grid.SetColumn(contentControl, 1);
|
|
||||||
Grid.SetRow(contentControl, rowCount);
|
|
||||||
if (rowCount != 0)
|
|
||||||
mainPanel.Children.Add(sep);
|
|
||||||
|
|
||||||
Grid.SetRow(sep, rowCount);
|
break;
|
||||||
Grid.SetColumn(sep, 0);
|
}
|
||||||
Grid.SetColumnSpan(sep, 2);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "dropdown":
|
case "dropdown":
|
||||||
{
|
|
||||||
var comboBox = new System.Windows.Controls.ComboBox()
|
|
||||||
{
|
{
|
||||||
ItemsSource = attribute.Options,
|
var comboBox = new ComboBox()
|
||||||
SelectedItem = Settings[attribute.Name],
|
{
|
||||||
Margin = settingControlMargin,
|
ItemsSource = attributes.Options,
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
SelectedItem = Settings[attributes.Name],
|
||||||
ToolTip = attribute.Description
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
};
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||||
|
ToolTip = attributes.Description
|
||||||
|
};
|
||||||
|
|
||||||
comboBox.SelectionChanged += (sender, _) =>
|
comboBox.SelectionChanged += (sender, _) =>
|
||||||
{
|
{
|
||||||
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
|
Settings[attributes.Name] = (string)((ComboBox)sender).SelectedItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
contentControl = comboBox;
|
contentControl = comboBox;
|
||||||
Grid.SetColumn(contentControl, 1);
|
|
||||||
Grid.SetRow(contentControl, rowCount);
|
|
||||||
if (rowCount != 0)
|
|
||||||
mainPanel.Children.Add(sep);
|
|
||||||
|
|
||||||
Grid.SetRow(sep, rowCount);
|
break;
|
||||||
Grid.SetColumn(sep, 0);
|
}
|
||||||
Grid.SetColumnSpan(sep, 2);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
var checkBox = new CheckBox
|
|
||||||
{
|
{
|
||||||
IsChecked =
|
// If can parse the default value to bool, use it, otherwise use false
|
||||||
Settings[attribute.Name] is bool isChecked
|
var defaultValue = bool.TryParse(attributes.DefaultValue, out var value) && value;
|
||||||
|
var checkBox = new CheckBox
|
||||||
|
{
|
||||||
|
IsChecked =
|
||||||
|
Settings[attributes.Name] is bool isChecked
|
||||||
? isChecked
|
? isChecked
|
||||||
: bool.Parse(attribute.DefaultValue),
|
: defaultValue,
|
||||||
Margin = settingCheckboxMargin,
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
ToolTip = attribute.Description
|
Margin = SettingPanelItemTopBottomMargin,
|
||||||
};
|
Content = attributes.Label,
|
||||||
|
ToolTip = attributes.Description
|
||||||
|
};
|
||||||
|
|
||||||
checkBox.Click += (sender, _) =>
|
checkBox.Click += (sender, _) =>
|
||||||
{
|
{
|
||||||
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
|
Settings[attributes.Name] = ((CheckBox)sender).IsChecked ?? defaultValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
contentControl = checkBox;
|
contentControl = checkBox;
|
||||||
Grid.SetColumn(contentControl, 1);
|
|
||||||
Grid.SetRow(contentControl, rowCount);
|
|
||||||
if (rowCount != 0)
|
|
||||||
mainPanel.Children.Add(sep);
|
|
||||||
|
|
||||||
Grid.SetRow(sep, rowCount);
|
break;
|
||||||
Grid.SetColumn(sep, 0);
|
}
|
||||||
Grid.SetColumnSpan(sep, 2);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "hyperlink":
|
case "hyperlink":
|
||||||
var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url };
|
|
||||||
|
|
||||||
var linkbtn = new System.Windows.Controls.Button
|
|
||||||
{
|
{
|
||||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
var hyperlink = new Hyperlink
|
||||||
Margin = settingControlMargin
|
{
|
||||||
};
|
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;
|
var textBlock = new TextBlock()
|
||||||
Grid.SetColumn(contentControl, 1);
|
{
|
||||||
Grid.SetRow(contentControl, rowCount);
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
if (rowCount != 0)
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
mainPanel.Children.Add(sep);
|
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||||
|
TextAlignment = TextAlignment.Left,
|
||||||
|
TextWrapping = TextWrapping.Wrap
|
||||||
|
};
|
||||||
|
textBlock.Inlines.Add(hyperlink);
|
||||||
|
|
||||||
Grid.SetRow(sep, rowCount);
|
contentControl = textBlock;
|
||||||
Grid.SetColumn(sep, 0);
|
|
||||||
Grid.SetColumnSpan(sep, 2);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case "separator":
|
||||||
|
{
|
||||||
|
var sep = new Separator();
|
||||||
|
|
||||||
|
sep.SetResourceReference(Separator.StyleProperty, "SettingPanelSeparatorStyle");
|
||||||
|
|
||||||
|
contentControl = sep;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type != "textBlock")
|
// If type is textBlock or separator, we just add the content control to the main grid
|
||||||
SettingControls[attribute.Name] = contentControl;
|
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++;
|
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,54 +26,33 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
protected override async Task<bool> ExecuteResultAsync(JsonRPCResult result)
|
||||||
{
|
{
|
||||||
try
|
var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method,
|
||||||
{
|
argument: result.JsonRPCAction.Parameters);
|
||||||
var res = await RPC.InvokeAsync<JsonRPCExecuteResponse>(result.JsonRPCAction.Method,
|
|
||||||
argument: result.JsonRPCAction.Parameters);
|
|
||||||
|
|
||||||
return res.Hide;
|
return res.Hide;
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
|
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
|
||||||
|
|
||||||
public override List<Result> LoadContextMenus(Result selectedResult)
|
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||||
{
|
{
|
||||||
try
|
var res = JTF.Run(() => RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("context_menu",
|
||||||
{
|
new object[] { selectedResult.ContextData }));
|
||||||
var res = JTF.Run(() => RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("context_menu",
|
|
||||||
new object[] { selectedResult.ContextData }));
|
|
||||||
|
|
||||||
var results = ParseResults(res);
|
var results = ParseResults(res);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return new List<Result>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
var res = await RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("query",
|
||||||
{
|
new object[] { query, Settings.Inner },
|
||||||
var res = await RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("query",
|
token);
|
||||||
new object[] { query, Settings.Inner },
|
|
||||||
token);
|
|
||||||
|
|
||||||
var results = ParseResults(res);
|
var results = ParseResults(res);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return new List<Result>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -133,10 +112,15 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
RPC.StartListening();
|
RPC.StartListening();
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual Task ReloadDataAsync()
|
public virtual async Task ReloadDataAsync()
|
||||||
{
|
{
|
||||||
SetupJsonRPC();
|
try
|
||||||
return Task.CompletedTask;
|
{
|
||||||
|
await RPC.InvokeAsync("reload_data", Context);
|
||||||
|
}
|
||||||
|
catch (RemoteMethodNotFoundException e)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async ValueTask DisposeAsync()
|
public virtual async ValueTask DisposeAsync()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
@ -121,10 +120,10 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||||
return _api.HttpGetStreamAsync(url, token);
|
return _api.HttpGetStreamAsync(url, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
|
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return _api.HttpDownloadAsync(url, filePath, token);
|
return _api.HttpDownloadAsync(url, filePath, reportProgress, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddActionKeyword(string pluginId, string newActionKeyword)
|
public void AddActionKeyword(string pluginId, string newActionKeyword)
|
||||||
|
|
@ -162,16 +161,29 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
||||||
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
|
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void OpenUrl(string url, bool? inPrivate = null)
|
public void OpenUrl(string url, bool? inPrivate = null)
|
||||||
{
|
{
|
||||||
_api.OpenUrl(url, inPrivate);
|
_api.OpenUrl(url, inPrivate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void OpenAppUri(string appUri)
|
public void OpenAppUri(string appUri)
|
||||||
{
|
{
|
||||||
_api.OpenAppUri(appUri);
|
_api.OpenAppUri(appUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BackToQueryResults()
|
||||||
|
{
|
||||||
|
_api.BackToQueryResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartLoadingBar()
|
||||||
|
{
|
||||||
|
_api.StartLoadingBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopLoadingBar()
|
||||||
|
{
|
||||||
|
_api.StopLoadingBar();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
@ -9,7 +9,6 @@ using System.Text.Json;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
||||||
internal abstract class PluginConfig
|
internal abstract class PluginConfig
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -112,7 +111,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
metadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(configPath));
|
metadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(configPath));
|
||||||
metadata.PluginDirectory = pluginDirectory;
|
metadata.PluginDirectory = pluginDirectory;
|
||||||
// for plugins which doesn't has ActionKeywords key
|
// 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
|
// for plugin still use old ActionKeyword
|
||||||
metadata.ActionKeyword = metadata.ActionKeywords?[0];
|
metadata.ActionKeyword = metadata.ActionKeywords?[0];
|
||||||
}
|
}
|
||||||
|
|
@ -137,4 +136,4 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
using Flow.Launcher.Core.ExternalPlugins;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Core.ExternalPlugins;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using System.Text.Json;
|
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Plugin
|
namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -27,11 +28,13 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
||||||
|
|
||||||
public static IPublicAPI API { private set; get; }
|
// We should not initialize API in static constructor because it will create another API instance
|
||||||
|
private static IPublicAPI api = null;
|
||||||
|
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
|
|
||||||
private static PluginsSettings Settings;
|
private static PluginsSettings Settings;
|
||||||
private static List<PluginMetadata> _metadatas;
|
private static List<PluginMetadata> _metadatas;
|
||||||
private static List<string> _modifiedPlugins = new List<string>();
|
private static List<string> _modifiedPlugins = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Directories that will hold Flow Launcher plugin directory
|
/// Directories that will hold Flow Launcher plugin directory
|
||||||
|
|
@ -51,7 +54,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save json and ISavable
|
/// Save json and ISavable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Save()
|
public static void Save()
|
||||||
{
|
{
|
||||||
|
|
@ -68,15 +71,20 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
foreach (var pluginPair in AllPlugins)
|
foreach (var pluginPair in AllPlugins)
|
||||||
{
|
{
|
||||||
switch (pluginPair.Plugin)
|
await DisposePluginAsync(pluginPair);
|
||||||
{
|
}
|
||||||
case IDisposable disposable:
|
}
|
||||||
disposable.Dispose();
|
|
||||||
break;
|
private static async Task DisposePluginAsync(PluginPair pluginPair)
|
||||||
case IAsyncDisposable asyncDisposable:
|
{
|
||||||
await asyncDisposable.DisposeAsync();
|
switch (pluginPair.Plugin)
|
||||||
break;
|
{
|
||||||
}
|
case IDisposable disposable:
|
||||||
|
disposable.Dispose();
|
||||||
|
break;
|
||||||
|
case IAsyncDisposable asyncDisposable:
|
||||||
|
await asyncDisposable.DisposeAsync();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +98,48 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}).ToArray());
|
}).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||||
|
{
|
||||||
|
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||||
|
{
|
||||||
|
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
|
||||||
|
_ => Task.CompletedTask,
|
||||||
|
}).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task CloseExternalPreviewAsync()
|
||||||
|
{
|
||||||
|
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||||
|
{
|
||||||
|
IAsyncExternalPreview p => p.ClosePreviewAsync(),
|
||||||
|
_ => Task.CompletedTask,
|
||||||
|
}).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||||
|
{
|
||||||
|
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||||
|
{
|
||||||
|
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
|
||||||
|
_ => Task.CompletedTask,
|
||||||
|
}).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool UseExternalPreview()
|
||||||
|
{
|
||||||
|
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool AllowAlwaysPreview()
|
||||||
|
{
|
||||||
|
var plugin = GetPluginsForInterface<IAsyncExternalPreview>().FirstOrDefault(x => !x.Metadata.Disabled);
|
||||||
|
|
||||||
|
if (plugin is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
|
||||||
|
}
|
||||||
|
|
||||||
static PluginManager()
|
static PluginManager()
|
||||||
{
|
{
|
||||||
// validate user directory
|
// validate user directory
|
||||||
|
|
@ -109,15 +159,33 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
Settings = settings;
|
Settings = settings;
|
||||||
Settings.UpdatePluginSettings(_metadatas);
|
Settings.UpdatePluginSettings(_metadatas);
|
||||||
AllPlugins = PluginsLoader.Plugins(_metadatas, Settings);
|
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))
|
||||||
|
{
|
||||||
|
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName);
|
||||||
|
metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.AssemblyName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name);
|
||||||
|
metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Call initialize for all plugins
|
/// Call initialize for all plugins
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>return the list of failed to init plugins or null for none</returns>
|
/// <returns>return the list of failed to init plugins or null for none</returns>
|
||||||
public static async Task InitializePluginsAsync(IPublicAPI api)
|
public static async Task InitializePluginsAsync()
|
||||||
{
|
{
|
||||||
API = api;
|
|
||||||
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
||||||
|
|
||||||
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
|
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
|
||||||
|
|
@ -163,9 +231,15 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
if (failedPlugins.Any())
|
if (failedPlugins.Any())
|
||||||
{
|
{
|
||||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||||
API.ShowMsg($"Fail to Init Plugins",
|
API.ShowMsg(
|
||||||
$"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help",
|
API.GetTranslation("failedToInitializePluginsTitle"),
|
||||||
"", false);
|
string.Format(
|
||||||
|
API.GetTranslation("failedToInitializePluginsMessage"),
|
||||||
|
failed
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,12 +247,10 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
{
|
{
|
||||||
if (query is null)
|
if (query is null)
|
||||||
return Array.Empty<PluginPair>();
|
return Array.Empty<PluginPair>();
|
||||||
|
|
||||||
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
|
||||||
return GlobalPlugins;
|
return GlobalPlugins;
|
||||||
|
|
||||||
|
|
||||||
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
|
||||||
return new List<PluginPair>
|
return new List<PluginPair>
|
||||||
{
|
{
|
||||||
plugin
|
plugin
|
||||||
|
|
@ -229,7 +301,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UpdatePluginMetadata(List<Result> results, PluginMetadata metadata, Query query)
|
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
|
||||||
{
|
{
|
||||||
foreach (var r in results)
|
foreach (var r in results)
|
||||||
{
|
{
|
||||||
|
|
@ -237,8 +309,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
r.PluginID = metadata.ID;
|
r.PluginID = metadata.ID;
|
||||||
r.OriginQuery = query;
|
r.OriginQuery = query;
|
||||||
|
|
||||||
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
|
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
|
||||||
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
|
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
|
||||||
if (metadata.ActionKeywords.Count == 1)
|
if (metadata.ActionKeywords.Count == 1)
|
||||||
r.ActionKeywordAssigned = query.ActionKeyword;
|
r.ActionKeywordAssigned = query.ActionKeyword;
|
||||||
}
|
}
|
||||||
|
|
@ -256,7 +328,8 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
|
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
|
||||||
{
|
{
|
||||||
return AllPlugins.Where(p => p.Plugin is T);
|
// Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup
|
||||||
|
return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty<PluginPair>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Result> GetContextMenusForPlugin(Result result)
|
public static List<Result> GetContextMenusForPlugin(Result result)
|
||||||
|
|
@ -312,7 +385,16 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
NonGlobalPlugins[newActionKeyword] = plugin;
|
NonGlobalPlugins[newActionKeyword] = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update action keywords and action keyword in plugin metadata
|
||||||
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
|
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>
|
/// <summary>
|
||||||
|
|
@ -333,16 +415,15 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
|
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
|
||||||
NonGlobalPlugins.Remove(oldActionkeyword);
|
NonGlobalPlugins.Remove(oldActionkeyword);
|
||||||
|
|
||||||
|
// Update action keywords and action keyword in plugin metadata
|
||||||
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
|
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
|
||||||
}
|
if (plugin.Metadata.ActionKeywords.Count > 0)
|
||||||
|
|
||||||
public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword)
|
|
||||||
{
|
|
||||||
if (oldActionKeyword != newActionKeyword)
|
|
||||||
{
|
{
|
||||||
AddActionKeyword(id, newActionKeyword);
|
plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
|
||||||
RemoveActionKeyword(id, oldActionKeyword);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
plugin.Metadata.ActionKeyword = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,37 +454,26 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
#region Public functions
|
#region Public functions
|
||||||
|
|
||||||
public static bool PluginModified(string uuid)
|
public static bool PluginModified(string id)
|
||||||
{
|
{
|
||||||
return _modifiedPlugins.Contains(uuid);
|
return _modifiedPlugins.Contains(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||||
/// <summary>
|
|
||||||
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
|
|
||||||
/// unless it's a local path installation
|
|
||||||
/// </summary>
|
|
||||||
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
|
||||||
{
|
{
|
||||||
InstallPlugin(newVersion, zipFilePath, checkModified:false);
|
InstallPlugin(newVersion, zipFilePath, checkModified:false);
|
||||||
UninstallPlugin(existingVersion, removeSettings:false, checkModified:false);
|
await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
|
||||||
_modifiedPlugins.Add(existingVersion.ID);
|
_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)
|
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
|
||||||
{
|
{
|
||||||
InstallPlugin(plugin, zipFilePath, checkModified: true);
|
InstallPlugin(plugin, zipFilePath, checkModified: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
|
||||||
/// Uninstall a plugin.
|
|
||||||
/// </summary>
|
|
||||||
public static void UninstallPlugin(PluginMetadata plugin, bool removeSettings = true)
|
|
||||||
{
|
{
|
||||||
UninstallPlugin(plugin, removeSettings, true);
|
await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -421,7 +491,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
// Unzip plugin files to temp folder
|
// Unzip plugin files to temp folder
|
||||||
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
|
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
|
||||||
|
|
||||||
if(!plugin.IsFromLocalInstallPath)
|
if(!plugin.IsFromLocalInstallPath)
|
||||||
File.Delete(zipFilePath);
|
File.Delete(zipFilePath);
|
||||||
|
|
||||||
|
|
@ -444,20 +514,20 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
|
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
|
||||||
|
|
||||||
var defaultPluginIDs = new List<string>
|
var defaultPluginIDs = new List<string>
|
||||||
{
|
{
|
||||||
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
|
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
|
||||||
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
|
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
|
||||||
"572be03c74c642baae319fc283e561a8", // Explorer
|
"572be03c74c642baae319fc283e561a8", // Explorer
|
||||||
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
|
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
|
||||||
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
|
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
|
||||||
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
|
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
|
||||||
"791FC278BA414111B8D1886DFE447410", // Program
|
"791FC278BA414111B8D1886DFE447410", // Program
|
||||||
"D409510CD0D2481F853690A07E6DC426", // Shell
|
"D409510CD0D2481F853690A07E6DC426", // Shell
|
||||||
"CEA08895D2544B019B2E9C5009600DF4", // Sys
|
"CEA08895D2544B019B2E9C5009600DF4", // Sys
|
||||||
"0308FD86DE0A4DEE8D62B9B535370992", // URL
|
"0308FD86DE0A4DEE8D62B9B535370992", // URL
|
||||||
"565B73353DBF4806919830B9202EE3BF", // WebSearch
|
"565B73353DBF4806919830B9202EE3BF", // WebSearch
|
||||||
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
|
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
// Treat default plugin differently, it needs to be removable along with each flow release
|
// Treat default plugin differently, it needs to be removable along with each flow release
|
||||||
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
|
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
|
||||||
|
|
@ -466,9 +536,17 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
var newPluginPath = Path.Combine(installDirectory, folderName);
|
var newPluginPath = Path.Combine(installDirectory, folderName);
|
||||||
|
|
||||||
FilesFolders.CopyAll(pluginFolderPath, newPluginPath);
|
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s));
|
||||||
|
|
||||||
Directory.Delete(tempFolderPluginPath, true);
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(tempFolderPluginPath))
|
||||||
|
Directory.Delete(tempFolderPluginPath, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Exception($"|PluginManager.InstallPlugin|Failed to delete temp folder {tempFolderPluginPath}", e);
|
||||||
|
}
|
||||||
|
|
||||||
if (checkModified)
|
if (checkModified)
|
||||||
{
|
{
|
||||||
|
|
@ -476,16 +554,63 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void UninstallPlugin(PluginMetadata plugin, bool removeSettings, bool checkModified)
|
internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
|
||||||
{
|
{
|
||||||
if (checkModified && PluginModified(plugin.ID))
|
if (checkModified && PluginModified(plugin.ID))
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
|
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removeSettings)
|
if (removePluginSettings || removePluginFromSettings)
|
||||||
{
|
{
|
||||||
Settings.Plugins.Remove(plugin.ID);
|
// If we want to remove plugin from AllPlugins,
|
||||||
|
// we need to dispose them so that they can release file handles
|
||||||
|
// which can help FL to delete the plugin settings & cache folders successfully
|
||||||
|
var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID);
|
||||||
|
foreach (var pluginPair in pluginPairs)
|
||||||
|
{
|
||||||
|
await DisposePluginAsync(pluginPair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removePluginSettings)
|
||||||
|
{
|
||||||
|
// For dotnet plugins, we need to remove their PluginJsonStorage instance
|
||||||
|
if (AllowedLanguage.IsDotNet(plugin.Language))
|
||||||
|
{
|
||||||
|
var method = API.GetType().GetMethod("RemovePluginSettings");
|
||||||
|
method?.Invoke(API, new object[] { plugin.AssemblyName });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pluginSettingsDirectory = plugin.PluginSettingsDirectoryPath;
|
||||||
|
if (Directory.Exists(pluginSettingsDirectory))
|
||||||
|
Directory.Delete(pluginSettingsDirectory, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin settings folder for {plugin.Name}", e);
|
||||||
|
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
|
||||||
|
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removePluginFromSettings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pluginCacheDirectory = plugin.PluginCacheDirectoryPath;
|
||||||
|
if (Directory.Exists(pluginCacheDirectory))
|
||||||
|
Directory.Delete(pluginCacheDirectory, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin cache folder for {plugin.Name}", e);
|
||||||
|
API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"),
|
||||||
|
string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name));
|
||||||
|
}
|
||||||
|
Settings.RemovePluginSettings(plugin.ID);
|
||||||
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
||||||
#pragma warning disable IDE0005
|
#pragma warning disable IDE0005
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
|
@ -49,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||||
{
|
{
|
||||||
var erroredPlugins = new List<string>();
|
var erroredPlugins = new List<string>();
|
||||||
|
|
||||||
|
|
@ -73,9 +74,11 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
typeof(IAsyncPlugin));
|
typeof(IAsyncPlugin));
|
||||||
|
|
||||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||||
|
|
||||||
|
metadata.AssemblyName = assembly.GetName().Name;
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
catch (Exception e)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +114,7 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
if (erroredPlugins.Count > 0)
|
if (erroredPlugins.Count > 0)
|
||||||
{
|
{
|
||||||
var errorPluginString = String.Join(Environment.NewLine, erroredPlugins);
|
var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
|
||||||
|
|
||||||
var errorMessage = "The following "
|
var errorMessage = "The following "
|
||||||
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
|
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
|
||||||
|
|
@ -119,33 +122,41 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||||
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
||||||
$"Please refer to the logs for more information", "",
|
$"Please refer to the logs for more information", "",
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
private static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||||
{
|
{
|
||||||
return source
|
return source
|
||||||
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
|
.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
|
return source
|
||||||
.Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase))
|
.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
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,10 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
public override async ValueTask DisposeAsync()
|
public override async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
|
await base.DisposeAsync();
|
||||||
ClientProcess.Kill(true);
|
ClientProcess.Kill(true);
|
||||||
await ClientProcess.WaitForExitAsync();
|
await ClientProcess.WaitForExitAsync();
|
||||||
ClientProcess.Dispose();
|
ClientProcess.Dispose();
|
||||||
await base.DisposeAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Diagnostics;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -25,14 +26,13 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
||||||
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||||
|
// Prevent Python from writing .py[co] files.
|
||||||
|
// Because .pyc contains location infos which will prevent python portable.
|
||||||
|
_startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
|
||||||
|
|
||||||
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
|
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
|
||||||
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
|
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
|
||||||
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
|
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
|
||||||
|
|
||||||
|
|
||||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
|
||||||
_startInfo.ArgumentList.Add("-B");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||||
|
|
@ -50,10 +50,53 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
// TODO: Async Action
|
// TODO: Async Action
|
||||||
return Execute(_startInfo);
|
return Execute(_startInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task InitAsync(PluginInitContext context)
|
public override async Task InitAsync(PluginInitContext context)
|
||||||
{
|
{
|
||||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
// Run .py files via `-c <code>`
|
||||||
_startInfo.ArgumentList.Add("");
|
if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
var libDirectory = Path.Combine(rootDirectory, "lib");
|
||||||
|
var libPyWin32Directory = Path.Combine(libDirectory, "win32");
|
||||||
|
var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
|
||||||
|
var pluginDirectory = Path.Combine(rootDirectory, "plugin");
|
||||||
|
|
||||||
|
// This makes it easier for plugin authors to import their own modules.
|
||||||
|
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
|
||||||
|
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
|
||||||
|
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
|
||||||
|
_startInfo.ArgumentList.Add("-c");
|
||||||
|
_startInfo.ArgumentList.Add(
|
||||||
|
$"""
|
||||||
|
import sys
|
||||||
|
sys.path.append(r'{rootDirectory}')
|
||||||
|
sys.path.append(r'{libDirectory}')
|
||||||
|
sys.path.append(r'{libPyWin32LibDirectory}')
|
||||||
|
sys.path.append(r'{libPyWin32Directory}')
|
||||||
|
sys.path.append(r'{pluginDirectory}')
|
||||||
|
|
||||||
|
import runpy
|
||||||
|
runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__')
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
// Plugins always expect the JSON data to be in the third argument
|
||||||
|
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
|
||||||
|
_startInfo.ArgumentList.Add("");
|
||||||
|
}
|
||||||
|
// Run .pyz files as is
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now,
|
||||||
|
// but the plugins still expect data to be sent as the third argument, so we're keeping
|
||||||
|
// the flag here, even though it's not necessary anymore.
|
||||||
|
_startInfo.ArgumentList.Add("-B");
|
||||||
|
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||||
|
// Plugins always expect the JSON data to be in the third argument
|
||||||
|
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
|
||||||
|
_startInfo.ArgumentList.Add("");
|
||||||
|
}
|
||||||
|
|
||||||
await base.InitAsync(context);
|
await base.InitAsync(context);
|
||||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,45 @@ namespace Flow.Launcher.Core.Plugin
|
||||||
|
|
||||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRpc);
|
var path = Path.Combine(Constant.ProgramDirectory, JsonRpc);
|
||||||
StartInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
StartInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||||
|
StartInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
|
||||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
|
||||||
StartInfo.ArgumentList.Add("-B");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task InitAsync(PluginInitContext context)
|
public override async Task InitAsync(PluginInitContext context)
|
||||||
{
|
{
|
||||||
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
// Run .py files via `-c <code>`
|
||||||
|
if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||||
|
var libDirectory = Path.Combine(rootDirectory, "lib");
|
||||||
|
var libPyWin32Directory = Path.Combine(libDirectory, "win32");
|
||||||
|
var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
|
||||||
|
var pluginDirectory = Path.Combine(rootDirectory, "plugin");
|
||||||
|
var filePath = context.CurrentPluginMetadata.ExecuteFilePath;
|
||||||
|
|
||||||
|
// This makes it easier for plugin authors to import their own modules.
|
||||||
|
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
|
||||||
|
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
|
||||||
|
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
|
||||||
|
StartInfo.ArgumentList.Add("-c");
|
||||||
|
StartInfo.ArgumentList.Add(
|
||||||
|
$"""
|
||||||
|
import sys
|
||||||
|
sys.path.append(r'{rootDirectory}')
|
||||||
|
sys.path.append(r'{libDirectory}')
|
||||||
|
sys.path.append(r'{libPyWin32LibDirectory}')
|
||||||
|
sys.path.append(r'{libPyWin32Directory}')
|
||||||
|
sys.path.append(r'{pluginDirectory}')
|
||||||
|
|
||||||
|
import runpy
|
||||||
|
runpy.run_path(r'{filePath}', None, '__main__')
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Run .pyz files as is
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||||
|
}
|
||||||
await base.InitAsync(context);
|
await base.InitAsync(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ namespace Flow.Launcher.Core.Resource
|
||||||
public static Language Czech = new Language("cs", "čeština");
|
public static Language Czech = new Language("cs", "čeština");
|
||||||
public static Language Arabic = new Language("ar", "اللغة العربية");
|
public static Language Arabic = new Language("ar", "اللغة العربية");
|
||||||
public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt");
|
public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt");
|
||||||
|
public static Language Hebrew = new Language("he", "עברית");
|
||||||
|
|
||||||
public static List<Language> GetAvailableLanguages()
|
public static List<Language> GetAvailableLanguages()
|
||||||
{
|
{
|
||||||
|
|
@ -57,9 +57,43 @@ namespace Flow.Launcher.Core.Resource
|
||||||
Turkish,
|
Turkish,
|
||||||
Czech,
|
Czech,
|
||||||
Arabic,
|
Arabic,
|
||||||
Vietnamese
|
Vietnamese,
|
||||||
|
Hebrew
|
||||||
};
|
};
|
||||||
return languages;
|
return languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetSystemTranslation(string languageCode)
|
||||||
|
{
|
||||||
|
return languageCode switch
|
||||||
|
{
|
||||||
|
"en" => "System",
|
||||||
|
"zh-cn" => "系统",
|
||||||
|
"zh-tw" => "系統",
|
||||||
|
"uk-UA" => "Система",
|
||||||
|
"ru" => "Система",
|
||||||
|
"fr" => "Système",
|
||||||
|
"ja" => "システム",
|
||||||
|
"nl" => "Systeem",
|
||||||
|
"pl" => "System",
|
||||||
|
"da" => "System",
|
||||||
|
"de" => "System",
|
||||||
|
"ko" => "시스템",
|
||||||
|
"sr" => "Систем",
|
||||||
|
"pt-pt" => "Sistema",
|
||||||
|
"pt-br" => "Sistema",
|
||||||
|
"es" => "Sistema",
|
||||||
|
"es-419" => "Sistema",
|
||||||
|
"it" => "Sistema",
|
||||||
|
"nb-NO" => "System",
|
||||||
|
"sk" => "Systém",
|
||||||
|
"tr" => "Sistem",
|
||||||
|
"cs" => "Systém",
|
||||||
|
"ar" => "النظام",
|
||||||
|
"vi-vn" => "Hệ thống",
|
||||||
|
"he" => "מערכת",
|
||||||
|
_ => "System",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,34 +11,61 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core.Resource
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
public class Internationalization
|
public class Internationalization
|
||||||
{
|
{
|
||||||
public Settings Settings { get; set; }
|
|
||||||
private const string Folder = "Languages";
|
private const string Folder = "Languages";
|
||||||
|
private const string DefaultLanguageCode = "en";
|
||||||
private const string DefaultFile = "en.xaml";
|
private const string DefaultFile = "en.xaml";
|
||||||
private const string Extension = ".xaml";
|
private const string Extension = ".xaml";
|
||||||
|
private readonly Settings _settings;
|
||||||
private readonly List<string> _languageDirectories = new List<string>();
|
private readonly List<string> _languageDirectories = new List<string>();
|
||||||
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
|
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
|
||||||
|
private readonly string SystemLanguageCode;
|
||||||
|
|
||||||
public Internationalization()
|
public Internationalization(Settings settings)
|
||||||
{
|
{
|
||||||
AddPluginLanguageDirectories();
|
_settings = settings;
|
||||||
LoadDefaultLanguage();
|
|
||||||
// we don't want to load /Languages/en.xaml twice
|
|
||||||
// so add flowlauncher language directory after load plugin language files
|
|
||||||
AddFlowLauncherLanguageDirectory();
|
AddFlowLauncherLanguageDirectory();
|
||||||
|
SystemLanguageCode = GetSystemLanguageCodeAtStartup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void AddFlowLauncherLanguageDirectory()
|
private void AddFlowLauncherLanguageDirectory()
|
||||||
{
|
{
|
||||||
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
||||||
_languageDirectories.Add(directory);
|
_languageDirectories.Add(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetSystemLanguageCodeAtStartup()
|
||||||
|
{
|
||||||
|
var availableLanguages = AvailableLanguages.GetAvailableLanguages();
|
||||||
|
|
||||||
|
// Retrieve the language identifiers for the current culture.
|
||||||
|
// ChangeLanguage method overrides the CultureInfo.CurrentCulture, so this needs to
|
||||||
|
// be called at startup in order to get the correct lang code of system.
|
||||||
|
var currentCulture = CultureInfo.CurrentCulture;
|
||||||
|
var twoLetterCode = currentCulture.TwoLetterISOLanguageName;
|
||||||
|
var threeLetterCode = currentCulture.ThreeLetterISOLanguageName;
|
||||||
|
var fullName = currentCulture.Name;
|
||||||
|
|
||||||
|
// Try to find a match in the available languages list
|
||||||
|
foreach (var language in availableLanguages)
|
||||||
|
{
|
||||||
|
var languageCode = language.LanguageCode;
|
||||||
|
|
||||||
|
if (string.Equals(languageCode, twoLetterCode, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return languageCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultLanguageCode;
|
||||||
|
}
|
||||||
|
|
||||||
private void AddPluginLanguageDirectories()
|
private void AddPluginLanguageDirectories()
|
||||||
{
|
{
|
||||||
|
|
@ -56,19 +83,65 @@ namespace Flow.Launcher.Core.Resource
|
||||||
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
|
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadDefaultLanguage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadDefaultLanguage()
|
private void LoadDefaultLanguage()
|
||||||
{
|
{
|
||||||
|
// Removes language files loaded before any plugins were loaded.
|
||||||
|
// Prevents the language Flow started in from overwriting English if the user switches back to English
|
||||||
|
RemoveOldLanguageFiles();
|
||||||
LoadLanguage(AvailableLanguages.English);
|
LoadLanguage(AvailableLanguages.English);
|
||||||
_oldResources.Clear();
|
_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)
|
public void ChangeLanguage(string languageCode)
|
||||||
{
|
{
|
||||||
languageCode = languageCode.NonNull();
|
languageCode = languageCode.NonNull();
|
||||||
Language language = GetLanguageByLanguageCode(languageCode);
|
|
||||||
ChangeLanguage(language);
|
// Get actual language if language code is system
|
||||||
|
var isSystem = false;
|
||||||
|
if (languageCode == Constant.SystemLanguageCode)
|
||||||
|
{
|
||||||
|
languageCode = SystemLanguageCode;
|
||||||
|
isSystem = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get language by language code and change language
|
||||||
|
var language = GetLanguageByLanguageCode(languageCode);
|
||||||
|
|
||||||
|
// Change language
|
||||||
|
_ = ChangeLanguageAsync(language);
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Language GetLanguageByLanguageCode(string languageCode)
|
private Language GetLanguageByLanguageCode(string languageCode)
|
||||||
|
|
@ -86,34 +159,29 @@ namespace Flow.Launcher.Core.Resource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeLanguage(Language language)
|
private async Task ChangeLanguageAsync(Language language)
|
||||||
{
|
{
|
||||||
language = language.NonNull();
|
// Remove old language files and load language
|
||||||
|
|
||||||
|
|
||||||
RemoveOldLanguageFiles();
|
RemoveOldLanguageFiles();
|
||||||
if (language != AvailableLanguages.English)
|
if (language != AvailableLanguages.English)
|
||||||
{
|
{
|
||||||
LoadLanguage(language);
|
LoadLanguage(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Culture of main thread
|
// 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
|
// 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.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||||
|
|
||||||
// Raise event after culture is set
|
// Raise event for plugins after culture is set
|
||||||
Settings.Language = language.LanguageCode;
|
await Task.Run(UpdatePluginMetadataTranslations);
|
||||||
_ = Task.Run(() =>
|
|
||||||
{
|
|
||||||
UpdatePluginMetadataTranslations();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
||||||
{
|
{
|
||||||
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
|
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
|
||||||
|
|
||||||
if (Settings.ShouldUsePinyin)
|
if (_settings.ShouldUsePinyin)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
|
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
|
||||||
|
|
@ -123,7 +191,7 @@ namespace Flow.Launcher.Core.Resource
|
||||||
// "Do you want to search with pinyin?"
|
// "Do you want to search with pinyin?"
|
||||||
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
|
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
|
||||||
|
|
||||||
if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
if (Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -140,11 +208,14 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
private void LoadLanguage(Language language)
|
private void LoadLanguage(Language language)
|
||||||
{
|
{
|
||||||
|
var flowEnglishFile = Path.Combine(Constant.ProgramDirectory, Folder, DefaultFile);
|
||||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||||
var filename = $"{language.LanguageCode}{Extension}";
|
var filename = $"{language.LanguageCode}{Extension}";
|
||||||
var files = _languageDirectories
|
var files = _languageDirectories
|
||||||
.Select(d => LanguageFile(d, filename))
|
.Select(d => LanguageFile(d, filename))
|
||||||
.Where(f => !string.IsNullOrEmpty(f))
|
// Exclude Flow's English language file since it's built into the binary, and there's no need to load
|
||||||
|
// it again from the file system.
|
||||||
|
.Where(f => !string.IsNullOrEmpty(f) && f != flowEnglishFile)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (files.Length > 0)
|
if (files.Length > 0)
|
||||||
|
|
@ -163,7 +234,9 @@ namespace Flow.Launcher.Core.Resource
|
||||||
|
|
||||||
public List<Language> LoadAvailableLanguages()
|
public List<Language> LoadAvailableLanguages()
|
||||||
{
|
{
|
||||||
return AvailableLanguages.GetAvailableLanguages();
|
var list = AvailableLanguages.GetAvailableLanguages();
|
||||||
|
list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetTranslation(string key)
|
public string GetTranslation(string key)
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,12 @@
|
||||||
namespace Flow.Launcher.Core.Resource
|
using System;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Core.Resource
|
||||||
{
|
{
|
||||||
|
[Obsolete("InternationalizationManager.Instance is obsolete. Use Ioc.Default.GetRequiredService<Internationalization>() instead.")]
|
||||||
public static class InternationalizationManager
|
public static class InternationalizationManager
|
||||||
{
|
{
|
||||||
private static Internationalization instance;
|
|
||||||
private static object syncObject = new object();
|
|
||||||
|
|
||||||
public static Internationalization Instance
|
public static Internationalization Instance
|
||||||
{
|
=> Ioc.Default.GetRequiredService<Internationalization>();
|
||||||
get
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
lock (syncObject)
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new Internationalization();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,26 +0,0 @@
|
||||||
namespace Flow.Launcher.Core.Resource
|
|
||||||
{
|
|
||||||
public class ThemeManager
|
|
||||||
{
|
|
||||||
private static Theme instance;
|
|
||||||
private static object syncObject = new object();
|
|
||||||
|
|
||||||
public static Theme Instance
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
lock (syncObject)
|
|
||||||
{
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
instance = new Theme();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,10 +4,12 @@ using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using JetBrains.Annotations;
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
using Squirrel;
|
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
|
|
@ -15,30 +17,33 @@ using Flow.Launcher.Infrastructure.Http;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using System.Text.Json.Serialization;
|
using JetBrains.Annotations;
|
||||||
using System.Threading;
|
using Squirrel;
|
||||||
|
|
||||||
namespace Flow.Launcher.Core
|
namespace Flow.Launcher.Core
|
||||||
{
|
{
|
||||||
public class Updater
|
public class Updater
|
||||||
{
|
{
|
||||||
public string GitHubRepository { get; }
|
public string GitHubRepository { get; init; }
|
||||||
|
|
||||||
public Updater(string gitHubRepository)
|
private readonly IPublicAPI _api;
|
||||||
|
|
||||||
|
public Updater(IPublicAPI publicAPI, string gitHubRepository)
|
||||||
{
|
{
|
||||||
|
_api = publicAPI;
|
||||||
GitHubRepository = gitHubRepository;
|
GitHubRepository = gitHubRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1);
|
private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1);
|
||||||
|
|
||||||
public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
|
public async Task UpdateAppAsync(bool silentUpdate = true)
|
||||||
{
|
{
|
||||||
await UpdateLock.WaitAsync().ConfigureAwait(false);
|
await UpdateLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!silentUpdate)
|
if (!silentUpdate)
|
||||||
api.ShowMsg(api.GetTranslation("pleaseWait"),
|
_api.ShowMsg(_api.GetTranslation("pleaseWait"),
|
||||||
api.GetTranslation("update_flowlauncher_update_check"));
|
_api.GetTranslation("update_flowlauncher_update_check"));
|
||||||
|
|
||||||
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
@ -48,18 +53,18 @@ namespace Flow.Launcher.Core
|
||||||
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
|
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
|
||||||
var currentVersion = Version.Parse(Constant.Version);
|
var currentVersion = Version.Parse(Constant.Version);
|
||||||
|
|
||||||
Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>");
|
Log.Info($"|Updater.UpdateApp|Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
|
||||||
|
|
||||||
if (newReleaseVersion <= currentVersion)
|
if (newReleaseVersion <= currentVersion)
|
||||||
{
|
{
|
||||||
if (!silentUpdate)
|
if (!silentUpdate)
|
||||||
MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest"));
|
_api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!silentUpdate)
|
if (!silentUpdate)
|
||||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"),
|
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
|
||||||
api.GetTranslation("update_flowlauncher_updating"));
|
_api.GetTranslation("update_flowlauncher_updating"));
|
||||||
|
|
||||||
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
|
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
@ -67,10 +72,10 @@ namespace Flow.Launcher.Core
|
||||||
|
|
||||||
if (DataLocation.PortableDataLocationInUse())
|
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);
|
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
|
||||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination))
|
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
|
||||||
MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||||
DataLocation.PortableDataPath,
|
DataLocation.PortableDataPath,
|
||||||
targetDestination));
|
targetDestination));
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +88,7 @@ namespace Flow.Launcher.Core
|
||||||
|
|
||||||
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
|
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
|
||||||
|
|
||||||
if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||||
{
|
{
|
||||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||||
}
|
}
|
||||||
|
|
@ -96,8 +101,8 @@ namespace Flow.Launcher.Core
|
||||||
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
|
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
|
||||||
|
|
||||||
if (!silentUpdate)
|
if (!silentUpdate)
|
||||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
|
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
|
||||||
api.GetTranslation("update_flowlauncher_check_connection"));
|
_api.GetTranslation("update_flowlauncher_check_connection"));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -119,7 +124,7 @@ namespace Flow.Launcher.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||||
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
private static async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||||
{
|
{
|
||||||
var uri = new Uri(repository);
|
var uri = new Uri(repository);
|
||||||
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
|
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
|
||||||
|
|
@ -141,12 +146,22 @@ namespace Flow.Launcher.Core
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string NewVersionTips(string version)
|
private static string NewVersionTips(string version)
|
||||||
{
|
{
|
||||||
var translator = InternationalizationManager.Instance;
|
var translator = Ioc.Default.GetRequiredService<Internationalization>();
|
||||||
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
|
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
|
||||||
|
|
||||||
return tips;
|
return tips;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string Formatted<T>(T t)
|
||||||
|
{
|
||||||
|
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ namespace Flow.Launcher.Infrastructure
|
||||||
public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png");
|
public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png");
|
||||||
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
||||||
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.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 PythonPath;
|
||||||
public static string NodePath;
|
public static string NodePath;
|
||||||
|
|
@ -46,10 +48,13 @@ namespace Flow.Launcher.Infrastructure
|
||||||
public const string Themes = "Themes";
|
public const string Themes = "Themes";
|
||||||
public const string Settings = "Settings";
|
public const string Settings = "Settings";
|
||||||
public const string Logs = "Logs";
|
public const string Logs = "Logs";
|
||||||
|
public const string Cache = "Cache";
|
||||||
|
|
||||||
public const string Website = "https://flowlauncher.com";
|
public const string Website = "https://flowlauncher.com";
|
||||||
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
|
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
|
||||||
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
|
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
|
||||||
public const string Docs = "https://flowlauncher.com/docs";
|
public const string Docs = "https://flowlauncher.com/docs";
|
||||||
|
|
||||||
|
public const string SystemLanguageCode = "system";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using Windows.Win32;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
|
|
@ -15,7 +15,20 @@ namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
var explorerWindow = GetActiveExplorer();
|
var explorerWindow = GetActiveExplorer();
|
||||||
string locationUrl = explorerWindow?.LocationURL;
|
string locationUrl = explorerWindow?.LocationURL;
|
||||||
return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null;
|
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get directory path from a file path
|
||||||
|
/// </summary>
|
||||||
|
private static string GetDirectoryPath(string path)
|
||||||
|
{
|
||||||
|
if (!path.EndsWith("\\"))
|
||||||
|
{
|
||||||
|
return path + "\\";
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -54,12 +67,6 @@ namespace Flow.Launcher.Infrastructure
|
||||||
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
||||||
|
|
||||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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>
|
/// </summary>
|
||||||
|
|
@ -70,9 +77,9 @@ namespace Flow.Launcher.Infrastructure
|
||||||
|
|
||||||
var index = 0;
|
var index = 0;
|
||||||
var numRemaining = hWnds.Count;
|
var numRemaining = hWnds.Count;
|
||||||
EnumWindows((wnd, _) =>
|
PInvoke.EnumWindows((wnd, _) =>
|
||||||
{
|
{
|
||||||
var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32());
|
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
|
||||||
if (searchIndex != -1)
|
if (searchIndex != -1)
|
||||||
{
|
{
|
||||||
z[searchIndex] = index;
|
z[searchIndex] = index;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Include="NativeMethods.txt" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SolutionAssemblyInfo.cs" Link="Properties\SolutionAssemblyInfo.cs" />
|
<Compile Include="..\SolutionAssemblyInfo.cs" Link="Properties\SolutionAssemblyInfo.cs" />
|
||||||
<None Include="FodyWeavers.xml" />
|
<None Include="FodyWeavers.xml" />
|
||||||
|
|
@ -49,13 +53,18 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||||
<PackageReference Include="BitFaster.Caching" Version="2.5.0" />
|
<PackageReference Include="BitFaster.Caching" Version="2.5.3" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="Fody" Version="6.5.5">
|
<PackageReference Include="Fody" Version="6.5.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MemoryPack" Version="1.21.1" />
|
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.7.30" />
|
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.12.19" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="NLog" Version="4.7.10" />
|
<PackageReference Include="NLog" Version="4.7.10" />
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
public static class Helper
|
public static class Helper
|
||||||
{
|
{
|
||||||
static Helper()
|
|
||||||
{
|
|
||||||
jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
|
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -36,55 +28,5 @@ namespace Flow.Launcher.Infrastructure
|
||||||
throw new NullReferenceException();
|
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,6 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||||
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
{
|
{
|
||||||
|
|
@ -10,44 +15,45 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe class GlobalHotkey : IDisposable
|
public unsafe class GlobalHotkey : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly IntPtr hookId;
|
private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback;
|
||||||
|
private static readonly UnhookWindowsHookExSafeHandle hookId;
|
||||||
|
|
||||||
|
|
||||||
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
|
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
|
||||||
internal static Func<KeyEvent, int, SpecialKeyState, bool> hookedKeyboardCallback;
|
internal static Func<KeyEvent, int, SpecialKeyState, bool> hookedKeyboardCallback;
|
||||||
|
|
||||||
//Modifier key constants
|
|
||||||
private const int VK_SHIFT = 0x10;
|
|
||||||
private const int VK_CONTROL = 0x11;
|
|
||||||
private const int VK_ALT = 0x12;
|
|
||||||
private const int VK_WIN = 91;
|
|
||||||
|
|
||||||
static GlobalHotkey()
|
static GlobalHotkey()
|
||||||
{
|
{
|
||||||
// Set the hook
|
// Set the hook
|
||||||
hookId = InterceptKeys.SetHook(& LowLevelKeyboardProc);
|
hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UnhookWindowsHookExSafeHandle SetHook(HOOKPROC proc, WINDOWS_HOOK_ID hookId)
|
||||||
|
{
|
||||||
|
using var curProcess = Process.GetCurrentProcess();
|
||||||
|
using var curModule = curProcess.MainModule;
|
||||||
|
return PInvoke.SetWindowsHookEx(hookId, proc, PInvoke.GetModuleHandle(curModule.ModuleName), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SpecialKeyState CheckModifiers()
|
public static SpecialKeyState CheckModifiers()
|
||||||
{
|
{
|
||||||
SpecialKeyState state = new SpecialKeyState();
|
SpecialKeyState state = new SpecialKeyState();
|
||||||
if ((InterceptKeys.GetKeyState(VK_SHIFT) & 0x8000) != 0)
|
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) & 0x8000) != 0)
|
||||||
{
|
{
|
||||||
//SHIFT is pressed
|
//SHIFT is pressed
|
||||||
state.ShiftPressed = true;
|
state.ShiftPressed = true;
|
||||||
}
|
}
|
||||||
if ((InterceptKeys.GetKeyState(VK_CONTROL) & 0x8000) != 0)
|
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) & 0x8000) != 0)
|
||||||
{
|
{
|
||||||
//CONTROL is pressed
|
//CONTROL is pressed
|
||||||
state.CtrlPressed = true;
|
state.CtrlPressed = true;
|
||||||
}
|
}
|
||||||
if ((InterceptKeys.GetKeyState(VK_ALT) & 0x8000) != 0)
|
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) & 0x8000) != 0)
|
||||||
{
|
{
|
||||||
//ALT is pressed
|
//ALT is pressed
|
||||||
state.AltPressed = true;
|
state.AltPressed = true;
|
||||||
}
|
}
|
||||||
if ((InterceptKeys.GetKeyState(VK_WIN) & 0x8000) != 0)
|
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_LWIN) & 0x8000) != 0 ||
|
||||||
|
(PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_RWIN) & 0x8000) != 0)
|
||||||
{
|
{
|
||||||
//WIN is pressed
|
//WIN is pressed
|
||||||
state.WinPressed = true;
|
state.WinPressed = true;
|
||||||
|
|
@ -56,33 +62,33 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly]
|
private static LRESULT HookKeyboardCallback(int nCode, WPARAM wParam, LPARAM lParam)
|
||||||
private static IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
|
|
||||||
{
|
{
|
||||||
bool continues = true;
|
bool continues = true;
|
||||||
|
|
||||||
if (nCode >= 0)
|
if (nCode >= 0)
|
||||||
{
|
{
|
||||||
if (wParam.ToUInt32() == (int)KeyEvent.WM_KEYDOWN ||
|
if (wParam.Value == (int)KeyEvent.WM_KEYDOWN ||
|
||||||
wParam.ToUInt32() == (int)KeyEvent.WM_KEYUP ||
|
wParam.Value == (int)KeyEvent.WM_KEYUP ||
|
||||||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYDOWN ||
|
wParam.Value == (int)KeyEvent.WM_SYSKEYDOWN ||
|
||||||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYUP)
|
wParam.Value == (int)KeyEvent.WM_SYSKEYUP)
|
||||||
{
|
{
|
||||||
if (hookedKeyboardCallback != null)
|
if (hookedKeyboardCallback != null)
|
||||||
continues = hookedKeyboardCallback((KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), CheckModifiers());
|
continues = hookedKeyboardCallback((KeyEvent)wParam.Value, Marshal.ReadInt32(lParam), CheckModifiers());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (continues)
|
if (continues)
|
||||||
{
|
{
|
||||||
return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam);
|
return PInvoke.CallNextHookEx(hookId, nCode, wParam, lParam);
|
||||||
}
|
}
|
||||||
return (IntPtr)(-1);
|
|
||||||
|
return new LRESULT(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
InterceptKeys.UnhookWindowsHookEx(hookId);
|
hookId.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
~GlobalHotkey()
|
~GlobalHotkey()
|
||||||
|
|
@ -90,4 +96,4 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
|
||||||
{
|
|
||||||
internal static unsafe class InterceptKeys
|
|
||||||
{
|
|
||||||
public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam);
|
|
||||||
|
|
||||||
private const int WH_KEYBOARD_LL = 13;
|
|
||||||
|
|
||||||
public static IntPtr SetHook(delegate* unmanaged<int, UIntPtr, IntPtr, IntPtr> proc)
|
|
||||||
{
|
|
||||||
using (Process curProcess = Process.GetCurrentProcess())
|
|
||||||
using (ProcessModule curModule = curProcess.MainModule)
|
|
||||||
{
|
|
||||||
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
public static extern IntPtr SetWindowsHookEx(int idHook, delegate* unmanaged<int, UIntPtr, IntPtr, IntPtr> lpfn, IntPtr hMod, uint dwThreadId);
|
|
||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
|
||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
|
||||||
public static extern IntPtr GetModuleHandle(string lpModuleName);
|
|
||||||
|
|
||||||
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
|
|
||||||
public static extern short GetKeyState(int keyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Windows.Win32;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
{
|
{
|
||||||
public enum KeyEvent
|
public enum KeyEvent
|
||||||
|
|
@ -5,21 +7,21 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key down
|
/// Key down
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WM_KEYDOWN = 256,
|
WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Key up
|
/// Key up
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WM_KEYUP = 257,
|
WM_KEYUP = (int)PInvoke.WM_KEYUP,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System key up
|
/// System key up
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WM_SYSKEYUP = 261,
|
WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System key down
|
/// System key down
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WM_SYSKEYDOWN = 260
|
WM_SYSKEYDOWN = (int)PInvoke.WM_SYSKEYDOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Http
|
namespace Flow.Launcher.Infrastructure.Http
|
||||||
{
|
{
|
||||||
|
|
@ -17,8 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http
|
||||||
|
|
||||||
private static HttpClient client = new HttpClient();
|
private static HttpClient client = new HttpClient();
|
||||||
|
|
||||||
public static IPublicAPI API { get; set; }
|
|
||||||
|
|
||||||
static Http()
|
static Http()
|
||||||
{
|
{
|
||||||
// need to be added so it would work on a win10 machine
|
// need to be added so it would work on a win10 machine
|
||||||
|
|
@ -78,20 +77,55 @@ namespace Flow.Launcher.Infrastructure.Http
|
||||||
}
|
}
|
||||||
catch (UriFormatException e)
|
catch (UriFormatException e)
|
||||||
{
|
{
|
||||||
API.ShowMsg("Please try again", "Unable to parse Http Proxy");
|
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsg("Please try again", "Unable to parse Http Proxy");
|
||||||
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
|
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default)
|
public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.OK)
|
if (response.StatusCode == HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
||||||
await response.Content.CopyToAsync(fileStream, token);
|
var canReportProgress = totalBytes != -1;
|
||||||
|
|
||||||
|
if (canReportProgress && reportProgress != null)
|
||||||
|
{
|
||||||
|
await using var contentStream = await response.Content.ReadAsStreamAsync(token);
|
||||||
|
await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true);
|
||||||
|
|
||||||
|
var buffer = new byte[8192];
|
||||||
|
long totalRead = 0;
|
||||||
|
int read;
|
||||||
|
double progressValue = 0;
|
||||||
|
|
||||||
|
reportProgress(0);
|
||||||
|
|
||||||
|
while ((read = await contentStream.ReadAsync(buffer, token)) > 0)
|
||||||
|
{
|
||||||
|
await fileStream.WriteAsync(buffer.AsMemory(0, read), token);
|
||||||
|
totalRead += read;
|
||||||
|
|
||||||
|
progressValue = totalRead * 100.0 / totalBytes;
|
||||||
|
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
else
|
||||||
|
reportProgress(progressValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressValue < 100)
|
||||||
|
reportProgress(100);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||||
|
await response.Content.CopyToAsync(fileStream, token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,10 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.Storage;
|
using Flow.Launcher.Infrastructure.Storage;
|
||||||
using static Flow.Launcher.Infrastructure.Http.Http;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Image
|
namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
|
|
@ -22,12 +20,12 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
|
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
|
||||||
private static IImageHashGenerator _hashGenerator;
|
private static IImageHashGenerator _hashGenerator;
|
||||||
private static readonly bool EnableImageHash = true;
|
private static readonly bool EnableImageHash = true;
|
||||||
|
public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon));
|
||||||
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||||
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
||||||
public const int SmallIconSize = 64;
|
public const int SmallIconSize = 64;
|
||||||
public const int FullIconSize = 256;
|
public const int FullIconSize = 256;
|
||||||
|
|
||||||
|
|
||||||
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
||||||
|
|
||||||
public static async Task InitializeAsync()
|
public static async Task InitializeAsync()
|
||||||
|
|
@ -60,7 +58,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Save()
|
public static async Task SaveAsync()
|
||||||
{
|
{
|
||||||
await storageLock.WaitAsync();
|
await storageLock.WaitAsync();
|
||||||
|
|
||||||
|
|
@ -70,12 +68,22 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
.Select(x => x.Key)
|
.Select(x => x.Key)
|
||||||
.ToList());
|
.ToList());
|
||||||
}
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
Log.Exception($"|ImageLoader.SaveAsync|Failed to save image cache to file", e);
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
storageLock.Release();
|
storageLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task WaitSaveAsync()
|
||||||
|
{
|
||||||
|
await storageLock.WaitAsync();
|
||||||
|
storageLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
|
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
|
||||||
{
|
{
|
||||||
await storageLock.WaitAsync();
|
await storageLock.WaitAsync();
|
||||||
|
|
@ -139,7 +147,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
return new ImageResult(image, ImageType.ImageFile);
|
return new ImageResult(image, ImageType.ImageFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
if (path.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var imageSource = new BitmapImage(new Uri(path));
|
var imageSource = new BitmapImage(new Uri(path));
|
||||||
imageSource.Freeze();
|
imageSource.Freeze();
|
||||||
|
|
@ -172,7 +180,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
||||||
{
|
{
|
||||||
// Download image from url
|
// 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 using var buffer = new MemoryStream();
|
||||||
await resp.CopyToAsync(buffer);
|
await resp.CopyToAsync(buffer);
|
||||||
buffer.Seek(0, SeekOrigin.Begin);
|
buffer.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
@ -215,8 +223,16 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
type = ImageType.ImageFile;
|
type = ImageType.ImageFile;
|
||||||
if (loadFullImage)
|
if (loadFullImage)
|
||||||
{
|
{
|
||||||
image = LoadFullImage(path);
|
try
|
||||||
type = ImageType.FullImageFile;
|
{
|
||||||
|
image = LoadFullImage(path);
|
||||||
|
type = ImageType.FullImageFile;
|
||||||
|
}
|
||||||
|
catch (NotSupportedException)
|
||||||
|
{
|
||||||
|
image = Image;
|
||||||
|
type = ImageType.Error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows;
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.UI.Shell;
|
||||||
|
using Windows.Win32.Graphics.Gdi;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Image
|
namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Subclass of <see cref="Windows.Win32.UI.Shell.SIIGBF"/>
|
||||||
|
/// </summary>
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum ThumbnailOptions
|
public enum ThumbnailOptions
|
||||||
{
|
{
|
||||||
|
|
@ -22,91 +29,13 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
{
|
{
|
||||||
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
|
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
|
||||||
|
|
||||||
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
|
private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID;
|
||||||
|
|
||||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
||||||
internal static extern int SHCreateItemFromParsingName(
|
|
||||||
[MarshalAs(UnmanagedType.LPWStr)] string path,
|
|
||||||
IntPtr pbc,
|
|
||||||
ref Guid riid,
|
|
||||||
[MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
|
|
||||||
|
|
||||||
[DllImport("gdi32.dll")]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
internal static extern bool DeleteObject(IntPtr hObject);
|
|
||||||
|
|
||||||
[ComImport]
|
|
||||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
||||||
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
|
|
||||||
internal interface IShellItem
|
|
||||||
{
|
|
||||||
void BindToHandler(IntPtr pbc,
|
|
||||||
[MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
|
|
||||||
[MarshalAs(UnmanagedType.LPStruct)]Guid riid,
|
|
||||||
out IntPtr ppv);
|
|
||||||
|
|
||||||
void GetParent(out IShellItem ppsi);
|
|
||||||
void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
|
|
||||||
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
||||||
void Compare(IShellItem psi, uint hint, out int piOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
internal enum SIGDN : uint
|
|
||||||
{
|
|
||||||
NORMALDISPLAY = 0,
|
|
||||||
PARENTRELATIVEPARSING = 0x80018001,
|
|
||||||
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
|
|
||||||
DESKTOPABSOLUTEPARSING = 0x80028000,
|
|
||||||
PARENTRELATIVEEDITING = 0x80031001,
|
|
||||||
DESKTOPABSOLUTEEDITING = 0x8004c000,
|
|
||||||
FILESYSPATH = 0x80058000,
|
|
||||||
URL = 0x80068000
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum HResult
|
|
||||||
{
|
|
||||||
Ok = 0x0000,
|
|
||||||
False = 0x0001,
|
|
||||||
InvalidArguments = unchecked((int)0x80070057),
|
|
||||||
OutOfMemory = unchecked((int)0x8007000E),
|
|
||||||
NoInterface = unchecked((int)0x80004002),
|
|
||||||
Fail = unchecked((int)0x80004005),
|
|
||||||
ExtractionFailed = unchecked((int)0x8004B200),
|
|
||||||
ElementNotFound = unchecked((int)0x80070490),
|
|
||||||
TypeElementNotFound = unchecked((int)0x8002802B),
|
|
||||||
NoObject = unchecked((int)0x800401E5),
|
|
||||||
Win32ErrorCanceled = 1223,
|
|
||||||
Canceled = unchecked((int)0x800704C7),
|
|
||||||
ResourceInUse = unchecked((int)0x800700AA),
|
|
||||||
AccessDenied = unchecked((int)0x80030005)
|
|
||||||
}
|
|
||||||
|
|
||||||
[ComImportAttribute()]
|
|
||||||
[GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
|
|
||||||
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
|
|
||||||
internal interface IShellItemImageFactory
|
|
||||||
{
|
|
||||||
[PreserveSig]
|
|
||||||
HResult GetImage(
|
|
||||||
[In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
|
|
||||||
[In] ThumbnailOptions flags,
|
|
||||||
[Out] out IntPtr phbm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
internal struct NativeSize
|
|
||||||
{
|
|
||||||
private int width;
|
|
||||||
private int height;
|
|
||||||
|
|
||||||
public int Width { set { width = value; } }
|
|
||||||
public int Height { set { height = value; } }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200;
|
||||||
|
|
||||||
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
|
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
|
||||||
{
|
{
|
||||||
IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -115,39 +44,61 @@ namespace Flow.Launcher.Infrastructure.Image
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// delete HBitmap to avoid memory leaks
|
// delete HBitmap to avoid memory leaks
|
||||||
DeleteObject(hBitmap);
|
PInvoke.DeleteObject(hBitmap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
|
||||||
{
|
|
||||||
IShellItem nativeShellItem;
|
|
||||||
Guid shellItem2Guid = new Guid(IShellItem2Guid);
|
|
||||||
int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
|
|
||||||
|
|
||||||
if (retCode != 0)
|
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
||||||
|
{
|
||||||
|
var retCode = PInvoke.SHCreateItemFromParsingName(
|
||||||
|
fileName,
|
||||||
|
null,
|
||||||
|
GUID_IShellItem,
|
||||||
|
out var nativeShellItem);
|
||||||
|
|
||||||
|
if (retCode != HRESULT.S_OK)
|
||||||
throw Marshal.GetExceptionForHR(retCode);
|
throw Marshal.GetExceptionForHR(retCode);
|
||||||
|
|
||||||
NativeSize nativeSize = new NativeSize
|
if (nativeShellItem is not IShellItemImageFactory imageFactory)
|
||||||
{
|
{
|
||||||
Width = width,
|
Marshal.ReleaseComObject(nativeShellItem);
|
||||||
Height = height
|
nativeShellItem = null;
|
||||||
};
|
throw new InvalidOperationException("Failed to get IShellItemImageFactory");
|
||||||
|
|
||||||
IntPtr hBitmap;
|
|
||||||
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap);
|
|
||||||
|
|
||||||
// if extracting image thumbnail and failed, extract shell icon
|
|
||||||
if (options == ThumbnailOptions.ThumbnailOnly && hr == HResult.ExtractionFailed)
|
|
||||||
{
|
|
||||||
hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, out hBitmap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Marshal.ReleaseComObject(nativeShellItem);
|
SIZE size = new SIZE
|
||||||
|
{
|
||||||
|
cx = width,
|
||||||
|
cy = height
|
||||||
|
};
|
||||||
|
|
||||||
if (hr == HResult.Ok) return hBitmap;
|
HBITMAP hBitmap = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
imageFactory.GetImage(size, (SIIGBF)options, &hBitmap);
|
||||||
|
}
|
||||||
|
catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly)
|
||||||
|
{
|
||||||
|
// Fallback to IconOnly if ThumbnailOnly fails
|
||||||
|
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly)
|
||||||
|
{
|
||||||
|
// Fallback to IconOnly if files cannot be found
|
||||||
|
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (nativeShellItem != null)
|
||||||
|
{
|
||||||
|
Marshal.ReleaseComObject(nativeShellItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr));
|
return hBitmap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
{
|
{
|
||||||
public static class Log
|
public static class Log
|
||||||
{
|
{
|
||||||
public const string DirectoryName = "Logs";
|
public const string DirectoryName = Constant.Logs;
|
||||||
|
|
||||||
public static string CurrentLogDirectory { get; }
|
public static string CurrentLogDirectory { get; }
|
||||||
|
|
||||||
static Log()
|
static Log()
|
||||||
{
|
{
|
||||||
CurrentLogDirectory = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version);
|
CurrentLogDirectory = DataLocation.VersionLogDirectory;
|
||||||
if (!Directory.Exists(CurrentLogDirectory))
|
if (!Directory.Exists(CurrentLogDirectory))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(CurrentLogDirectory);
|
Directory.CreateDirectory(CurrentLogDirectory);
|
||||||
|
|
@ -48,17 +48,45 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
configuration.AddTarget("file", fileTargetASyncWrapper);
|
configuration.AddTarget("file", fileTargetASyncWrapper);
|
||||||
configuration.AddTarget("debug", debugTarget);
|
configuration.AddTarget("debug", debugTarget);
|
||||||
|
|
||||||
|
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper)
|
||||||
|
{
|
||||||
|
RuleName = "file"
|
||||||
|
};
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper);
|
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget)
|
||||||
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget);
|
{
|
||||||
|
RuleName = "debug"
|
||||||
|
};
|
||||||
configuration.LoggingRules.Add(debugRule);
|
configuration.LoggingRules.Add(debugRule);
|
||||||
#else
|
|
||||||
var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
|
|
||||||
#endif
|
#endif
|
||||||
configuration.LoggingRules.Add(fileRule);
|
configuration.LoggingRules.Add(fileRule);
|
||||||
LogManager.Configuration = configuration;
|
LogManager.Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SetLogLevel(LOGLEVEL level)
|
||||||
|
{
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case LOGLEVEL.DEBUG:
|
||||||
|
UseDebugLogLevel();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
UseInfoLogLevel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Info(nameof(Logger), $"Using log level: {level}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UseDebugLogLevel()
|
||||||
|
{
|
||||||
|
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UseInfoLogLevel()
|
||||||
|
{
|
||||||
|
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
|
||||||
|
}
|
||||||
|
|
||||||
private static void LogFaultyFormat(string message)
|
private static void LogFaultyFormat(string message)
|
||||||
{
|
{
|
||||||
var logger = LogManager.GetLogger("FaultyLogger");
|
var logger = LogManager.GetLogger("FaultyLogger");
|
||||||
|
|
@ -206,4 +234,10 @@ namespace Flow.Launcher.Infrastructure.Logger
|
||||||
LogInternal(message, LogLevel.Warn);
|
LogInternal(message, LogLevel.Warn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum LOGLEVEL
|
||||||
|
{
|
||||||
|
DEBUG,
|
||||||
|
INFO
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Flow.Launcher.Infrastructure/NativeMethods.txt
Normal file
61
Flow.Launcher.Infrastructure/NativeMethods.txt
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
SHCreateItemFromParsingName
|
||||||
|
DeleteObject
|
||||||
|
IShellItem
|
||||||
|
IShellItemImageFactory
|
||||||
|
S_OK
|
||||||
|
|
||||||
|
SetWindowsHookEx
|
||||||
|
UnhookWindowsHookEx
|
||||||
|
CallNextHookEx
|
||||||
|
GetModuleHandle
|
||||||
|
GetKeyState
|
||||||
|
VIRTUAL_KEY
|
||||||
|
|
||||||
|
WM_KEYDOWN
|
||||||
|
WM_KEYUP
|
||||||
|
WM_SYSKEYDOWN
|
||||||
|
WM_SYSKEYUP
|
||||||
|
|
||||||
|
EnumWindows
|
||||||
|
|
||||||
|
DwmSetWindowAttribute
|
||||||
|
DWM_SYSTEMBACKDROP_TYPE
|
||||||
|
DWM_WINDOW_CORNER_PREFERENCE
|
||||||
|
|
||||||
|
MAX_PATH
|
||||||
|
SystemParametersInfo
|
||||||
|
|
||||||
|
SetForegroundWindow
|
||||||
|
|
||||||
|
GetWindowLong
|
||||||
|
GetForegroundWindow
|
||||||
|
GetDesktopWindow
|
||||||
|
GetShellWindow
|
||||||
|
GetWindowRect
|
||||||
|
GetClassName
|
||||||
|
FindWindowEx
|
||||||
|
WINDOW_STYLE
|
||||||
|
|
||||||
|
SetLastError
|
||||||
|
WINDOW_EX_STYLE
|
||||||
|
|
||||||
|
GetSystemMetrics
|
||||||
|
EnumDisplayMonitors
|
||||||
|
MonitorFromWindow
|
||||||
|
GetMonitorInfo
|
||||||
|
MONITORINFOEXW
|
||||||
|
|
||||||
|
WM_ENTERSIZEMOVE
|
||||||
|
WM_EXITSIZEMOVE
|
||||||
|
|
||||||
|
GetKeyboardLayout
|
||||||
|
GetWindowThreadProcessId
|
||||||
|
ActivateKeyboardLayout
|
||||||
|
GetKeyboardLayoutList
|
||||||
|
PostMessage
|
||||||
|
WM_INPUTLANGCHANGEREQUEST
|
||||||
|
INPUTLANGCHANGE_FORWARD
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD1
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD2
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD3
|
||||||
|
LOCALE_TRANSIENT_KEYBOARD4
|
||||||
25
Flow.Launcher.Infrastructure/PInvokeExtensions.cs
Normal file
25
Flow.Launcher.Infrastructure/PInvokeExtensions.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
|
|
||||||
|
namespace Windows.Win32;
|
||||||
|
|
||||||
|
// Edited from: https://github.com/files-community/Files
|
||||||
|
internal static partial class PInvoke
|
||||||
|
{
|
||||||
|
[DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)]
|
||||||
|
static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong);
|
||||||
|
|
||||||
|
[DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)]
|
||||||
|
static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong);
|
||||||
|
|
||||||
|
// NOTE:
|
||||||
|
// CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa.
|
||||||
|
// For more info, visit https://github.com/microsoft/CsWin32/issues/882
|
||||||
|
public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong)
|
||||||
|
{
|
||||||
|
return sizeof(nint) is 4
|
||||||
|
? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong)
|
||||||
|
: _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using ToolGood.Words.Pinyin;
|
using ToolGood.Words.Pinyin;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
|
|
@ -103,13 +104,21 @@ namespace Flow.Launcher.Infrastructure
|
||||||
{"Ou", "ou"}
|
{"Ou", "ou"}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
private static readonly ReadOnlyDictionary<string, string> first = new(new Dictionary<string, string>(){
|
private static readonly ReadOnlyDictionary<string, string> first = new(new Dictionary<string, string>(){
|
||||||
{"Ch", "i"},
|
{"Ch", "i"},
|
||||||
{"Sh", "u"},
|
{"Sh", "u"},
|
||||||
{"Zh", "v"}
|
{"Zh", "v"}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
public PinyinAlphabet()
|
||||||
|
{
|
||||||
|
Initialize(Ioc.Default.GetRequiredService<Settings>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Initialize([NotNull] Settings settings)
|
||||||
|
{
|
||||||
|
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly ReadOnlyDictionary<string, string> second = new(new Dictionary<string, string>()
|
private static readonly ReadOnlyDictionary<string, string> second = new(new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
using System;
|
using System.IO;
|
||||||
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 System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
|
|
@ -16,19 +12,17 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
/// Normally, it has better performance, but not readable
|
/// Normally, it has better performance, but not readable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable
|
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
|
||||||
/// https://github.com/Cysharp/MemoryPack
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class BinaryStorage<T>
|
public class BinaryStorage<T>
|
||||||
{
|
{
|
||||||
const string DirectoryName = "Cache";
|
public const string FileSuffix = ".cache";
|
||||||
|
|
||||||
const string FileSuffix = ".cache";
|
// Let the derived class to set the file path
|
||||||
|
public BinaryStorage(string filename, string directoryPath = null)
|
||||||
public BinaryStorage(string filename)
|
|
||||||
{
|
{
|
||||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
directoryPath ??= DataLocation.CacheDirectory;
|
||||||
Helper.ValidateDirectory(directoryPath);
|
FilesFolders.ValidateDirectory(directoryPath);
|
||||||
|
|
||||||
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||||
}
|
}
|
||||||
|
|
@ -58,14 +52,14 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
|
private static async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
|
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
catch (System.Exception e)
|
catch (System.Exception)
|
||||||
{
|
{
|
||||||
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
|
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
|
||||||
return defaultData;
|
return defaultData;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,51 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
|
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
|
||||||
{
|
{
|
||||||
|
private static readonly string ClassName = "FlowLauncherJsonStorage";
|
||||||
|
|
||||||
|
// We should not initialize API in static constructor because it will create another API instance
|
||||||
|
private static IPublicAPI api = null;
|
||||||
|
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
|
|
||||||
public FlowLauncherJsonStorage()
|
public FlowLauncherJsonStorage()
|
||||||
{
|
{
|
||||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||||
Helper.ValidateDirectory(directoryPath);
|
FilesFolders.ValidateDirectory(directoryPath);
|
||||||
|
|
||||||
var filename = typeof(T).Name;
|
var filename = typeof(T).Name;
|
||||||
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public new void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
base.Save();
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new async Task SaveAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await base.SaveAsync();
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
|
|
@ -16,7 +17,7 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
protected T? Data;
|
protected T? Data;
|
||||||
|
|
||||||
// need a new directory name
|
// need a new directory name
|
||||||
public const string DirectoryName = "Settings";
|
public const string DirectoryName = Constant.Settings;
|
||||||
public const string FileSuffix = ".json";
|
public const string FileSuffix = ".json";
|
||||||
|
|
||||||
protected string FilePath { get; init; } = null!;
|
protected string FilePath { get; init; } = null!;
|
||||||
|
|
@ -31,12 +32,13 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
protected JsonStorage()
|
protected JsonStorage()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonStorage(string filePath)
|
public JsonStorage(string filePath)
|
||||||
{
|
{
|
||||||
FilePath = filePath;
|
FilePath = filePath;
|
||||||
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
|
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
|
||||||
|
|
||||||
Helper.ValidateDirectory(DirectoryPath);
|
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> LoadAsync()
|
public async Task<T> LoadAsync()
|
||||||
|
|
@ -97,6 +99,7 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RestoreBackup()
|
private void RestoreBackup()
|
||||||
{
|
{
|
||||||
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
|
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
|
||||||
|
|
@ -179,25 +182,21 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
public void Save()
|
public void Save()
|
||||||
{
|
{
|
||||||
string serialized = JsonSerializer.Serialize(Data,
|
string serialized = JsonSerializer.Serialize(Data,
|
||||||
new JsonSerializerOptions
|
new JsonSerializerOptions { WriteIndented = true });
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
});
|
|
||||||
|
|
||||||
File.WriteAllText(TempFilePath, serialized);
|
File.WriteAllText(TempFilePath, serialized);
|
||||||
|
|
||||||
AtomicWriteSetting();
|
AtomicWriteSetting();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync()
|
public async Task SaveAsync()
|
||||||
{
|
{
|
||||||
var tempOutput = File.OpenWrite(TempFilePath);
|
await using var tempOutput = File.OpenWrite(TempFilePath);
|
||||||
await JsonSerializer.SerializeAsync(tempOutput, Data,
|
await JsonSerializer.SerializeAsync(tempOutput, Data,
|
||||||
new JsonSerializerOptions
|
new JsonSerializerOptions { WriteIndented = true });
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
});
|
|
||||||
AtomicWriteSetting();
|
AtomicWriteSetting();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AtomicWriteSetting()
|
private void AtomicWriteSetting()
|
||||||
{
|
{
|
||||||
if (!File.Exists(FilePath))
|
if (!File.Exists(FilePath))
|
||||||
|
|
@ -206,9 +205,9 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
File.Replace(TempFilePath, FilePath, BackupFilePath);
|
var finalFilePath = new FileInfo(FilePath).LinkTarget ?? FilePath;
|
||||||
|
File.Replace(TempFilePath, finalFilePath, BackupFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,30 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.Storage
|
namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
public class PluginJsonStorage<T> :JsonStorage<T> where T : new()
|
public class PluginJsonStorage<T> : JsonStorage<T> where T : new()
|
||||||
{
|
{
|
||||||
|
// Use assembly name to check which plugin is using this storage
|
||||||
|
public readonly string AssemblyName;
|
||||||
|
|
||||||
|
private static readonly string ClassName = "PluginJsonStorage";
|
||||||
|
|
||||||
|
// We should not initialize API in static constructor because it will create another API instance
|
||||||
|
private static IPublicAPI api = null;
|
||||||
|
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||||
|
|
||||||
public PluginJsonStorage()
|
public PluginJsonStorage()
|
||||||
{
|
{
|
||||||
// C# related, add python related below
|
// C# related, add python related below
|
||||||
var dataType = typeof(T);
|
var dataType = typeof(T);
|
||||||
var assemblyName = dataType.Assembly.GetName().Name;
|
AssemblyName = dataType.Assembly.GetName().Name;
|
||||||
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName);
|
DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName);
|
||||||
Helper.ValidateDirectory(DirectoryPath);
|
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||||
|
|
||||||
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +33,29 @@ namespace Flow.Launcher.Infrastructure.Storage
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public new void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
base.Save();
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new async Task SaveAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await base.SaveAsync();
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,35 @@
|
||||||
using Flow.Launcher.Plugin.SharedModels;
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
using Flow.Launcher.Plugin.SharedModels;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure
|
namespace Flow.Launcher.Infrastructure
|
||||||
{
|
{
|
||||||
public class StringMatcher
|
public class StringMatcher
|
||||||
{
|
{
|
||||||
private readonly MatchOption _defaultMatchOption = new MatchOption();
|
private readonly MatchOption _defaultMatchOption = new();
|
||||||
|
|
||||||
public SearchPrecisionScore UserSettingSearchPrecision { get; set; }
|
public SearchPrecisionScore UserSettingSearchPrecision { get; set; }
|
||||||
|
|
||||||
private readonly IAlphabet _alphabet;
|
private readonly IAlphabet _alphabet;
|
||||||
|
|
||||||
public StringMatcher(IAlphabet alphabet = null)
|
public StringMatcher(IAlphabet alphabet, Settings settings)
|
||||||
|
{
|
||||||
|
_alphabet = alphabet;
|
||||||
|
UserSettingSearchPrecision = settings.QuerySearchPrecision;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a workaround to allow unit tests to set the instance
|
||||||
|
public StringMatcher(IAlphabet alphabet)
|
||||||
{
|
{
|
||||||
_alphabet = alphabet;
|
_alphabet = alphabet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StringMatcher Instance { get; internal set; }
|
|
||||||
|
|
||||||
public static MatchResult FuzzySearch(string query, string stringToCompare)
|
public static MatchResult FuzzySearch(string query, string stringToCompare)
|
||||||
{
|
{
|
||||||
return Instance.FuzzyMatch(query, stringToCompare);
|
return Ioc.Default.GetRequiredService<StringMatcher>().FuzzyMatch(query, stringToCompare);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MatchResult FuzzyMatch(string query, string stringToCompare)
|
public MatchResult FuzzyMatch(string query, string stringToCompare)
|
||||||
|
|
@ -241,16 +248,16 @@ namespace Flow.Launcher.Infrastructure
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAcronymChar(string stringToCompare, int compareStringIndex)
|
private static bool IsAcronymChar(string stringToCompare, int compareStringIndex)
|
||||||
=> char.IsUpper(stringToCompare[compareStringIndex]) ||
|
=> char.IsUpper(stringToCompare[compareStringIndex]) ||
|
||||||
compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
|
compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
|
||||||
char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
|
char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
|
||||||
|
|
||||||
private bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
|
private static bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
|
||||||
=> stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;
|
=> stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;
|
||||||
|
|
||||||
// To get the index of the closest space which preceeds the first matching index
|
// To get the index of the closest space which preceeds the first matching index
|
||||||
private int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
private static int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
||||||
{
|
{
|
||||||
var closestSpaceIndex = -1;
|
var closestSpaceIndex = -1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
return false;
|
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 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 PythonEnvironmentName = "Python";
|
||||||
public const string NodeEnvironmentName = "Node.js";
|
public const string NodeEnvironmentName = "Node.js";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
|
|
@ -6,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public class PluginsSettings : BaseModel
|
public class PluginsSettings : BaseModel
|
||||||
{
|
{
|
||||||
private string pythonExecutablePath = string.Empty;
|
private string pythonExecutablePath = string.Empty;
|
||||||
public string PythonExecutablePath {
|
public string PythonExecutablePath
|
||||||
get { return pythonExecutablePath; }
|
{
|
||||||
|
get => pythonExecutablePath;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
pythonExecutablePath = value;
|
pythonExecutablePath = value;
|
||||||
|
|
@ -18,7 +20,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
private string nodeExecutablePath = string.Empty;
|
private string nodeExecutablePath = string.Empty;
|
||||||
public string NodeExecutablePath
|
public string NodeExecutablePath
|
||||||
{
|
{
|
||||||
get { return nodeExecutablePath; }
|
get => nodeExecutablePath;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
nodeExecutablePath = value;
|
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)
|
public void UpdatePluginSettings(List<PluginMetadata> metadatas)
|
||||||
{
|
{
|
||||||
foreach (var metadata in 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))
|
if (string.IsNullOrEmpty(settings.Version))
|
||||||
|
{
|
||||||
settings.Version = metadata.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)
|
if (settings.ActionKeywords?.Count > 0)
|
||||||
{
|
{
|
||||||
metadata.ActionKeywords = settings.ActionKeywords;
|
metadata.ActionKeywords = settings.ActionKeywords;
|
||||||
|
|
@ -51,30 +66,65 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
}
|
}
|
||||||
metadata.Disabled = settings.Disabled;
|
metadata.Disabled = settings.Disabled;
|
||||||
metadata.Priority = settings.Priority;
|
metadata.Priority = settings.Priority;
|
||||||
|
metadata.SearchDelayTime = settings.SearchDelayTime;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// If settings does not exist, create a new one
|
||||||
Plugins[metadata.ID] = new Plugin
|
Plugins[metadata.ID] = new Plugin
|
||||||
{
|
{
|
||||||
ID = metadata.ID,
|
ID = metadata.ID,
|
||||||
Name = metadata.Name,
|
Name = metadata.Name,
|
||||||
Version = metadata.Version,
|
Version = metadata.Version,
|
||||||
ActionKeywords = metadata.ActionKeywords,
|
DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values
|
||||||
|
ActionKeywords = metadata.ActionKeywords, // use default value
|
||||||
Disabled = metadata.Disabled,
|
Disabled = metadata.Disabled,
|
||||||
Priority = metadata.Priority
|
Priority = metadata.Priority,
|
||||||
|
DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values
|
||||||
|
SearchDelayTime = metadata.SearchDelayTime, // use default value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Plugin GetPluginSettings(string id)
|
||||||
|
{
|
||||||
|
if (Plugins.TryGetValue(id, out var plugin))
|
||||||
|
{
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plugin RemovePluginSettings(string id)
|
||||||
|
{
|
||||||
|
Plugins.Remove(id, out var plugin);
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Plugin
|
public class Plugin
|
||||||
{
|
{
|
||||||
public string ID { get; set; }
|
public string ID { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public string Version { 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; }
|
public int Priority { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public SearchDelayTime? DefaultSearchDelayTime { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public SearchDelayTime? SearchDelayTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used only to save the state of the plugin in settings
|
/// Used only to save the state of the plugin in settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
using Flow.Launcher.Infrastructure.Hotkey;
|
using Flow.Launcher.Infrastructure.Hotkey;
|
||||||
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using Flow.Launcher.Infrastructure.Storage;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Flow.Launcher.Plugin.SharedModels;
|
using Flow.Launcher.Plugin.SharedModels;
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
|
@ -13,7 +15,25 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
{
|
{
|
||||||
public class Settings : BaseModel, IHotkeySettings
|
public class Settings : BaseModel, IHotkeySettings
|
||||||
{
|
{
|
||||||
private string language = "en";
|
private FlowLauncherJsonStorage<Settings> _storage;
|
||||||
|
private StringMatcher _stringMatcher = null;
|
||||||
|
|
||||||
|
public void SetStorage(FlowLauncherJsonStorage<Settings> storage)
|
||||||
|
{
|
||||||
|
_storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_stringMatcher = Ioc.Default.GetRequiredService<StringMatcher>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
_storage.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string language = Constant.SystemLanguageCode;
|
||||||
private string _theme = Constant.DefaultTheme;
|
private string _theme = Constant.DefaultTheme;
|
||||||
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
|
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
|
||||||
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
|
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
|
||||||
|
|
@ -48,21 +68,23 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
get => _theme;
|
get => _theme;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value == _theme)
|
if (value != _theme)
|
||||||
return;
|
{
|
||||||
_theme = value;
|
_theme = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(MaxResultsToShow));
|
OnPropertyChanged(nameof(MaxResultsToShow));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool UseDropShadowEffect { get; set; } = false;
|
public bool UseDropShadowEffect { get; set; } = true;
|
||||||
|
public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None;
|
||||||
|
|
||||||
/* Appearance Settings. It should be separated from the setting later.*/
|
/* Appearance Settings. It should be separated from the setting later.*/
|
||||||
public double WindowHeightSize { get; set; } = 42;
|
public double WindowHeightSize { get; set; } = 42;
|
||||||
public double ItemHeightSize { get; set; } = 58;
|
public double ItemHeightSize { get; set; } = 58;
|
||||||
public double QueryBoxFontSize { get; set; } = 20;
|
public double QueryBoxFontSize { get; set; } = 20;
|
||||||
public double ResultItemFontSize { get; set; } = 16;
|
public double ResultItemFontSize { get; set; } = 16;
|
||||||
public double ResultSubItemFontSize { get; set; } = 13;
|
public double ResultSubItemFontSize { get; set; } = 13;
|
||||||
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
||||||
public string QueryBoxFontStyle { get; set; }
|
public string QueryBoxFontStyle { get; set; }
|
||||||
public string QueryBoxFontWeight { get; set; }
|
public string QueryBoxFontWeight { get; set; }
|
||||||
|
|
@ -90,7 +112,34 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public double SettingWindowHeight { get; set; } = 700;
|
public double SettingWindowHeight { get; set; } = 700;
|
||||||
public double? SettingWindowTop { get; set; } = null;
|
public double? SettingWindowTop { get; set; } = null;
|
||||||
public double? SettingWindowLeft { 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;
|
||||||
|
|
||||||
|
bool _showPlaceholder { get; set; } = false;
|
||||||
|
public bool ShowPlaceholder
|
||||||
|
{
|
||||||
|
get => _showPlaceholder;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_showPlaceholder != value)
|
||||||
|
{
|
||||||
|
_showPlaceholder = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string _placeholderText { get; set; } = string.Empty;
|
||||||
|
public string PlaceholderText
|
||||||
|
{
|
||||||
|
get => _placeholderText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_placeholderText != value)
|
||||||
|
{
|
||||||
|
_placeholderText = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int CustomExplorerIndex { get; set; } = 0;
|
public int CustomExplorerIndex { get; set; } = 0;
|
||||||
|
|
||||||
|
|
@ -180,6 +229,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// when false Alphabet static service will always return empty results
|
/// when false Alphabet static service will always return empty results
|
||||||
|
|
@ -189,6 +240,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public bool UseDoublePinyin { get; set; } = true; //For developing
|
public bool UseDoublePinyin { get; set; } = true; //For developing
|
||||||
|
|
||||||
public bool AlwaysPreview { get; set; } = false;
|
public bool AlwaysPreview { get; set; } = false;
|
||||||
|
|
||||||
public bool AlwaysStartEn { get; set; } = false;
|
public bool AlwaysStartEn { get; set; } = false;
|
||||||
|
|
||||||
private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular;
|
private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular;
|
||||||
|
|
@ -199,8 +251,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_querySearchPrecision = value;
|
_querySearchPrecision = value;
|
||||||
if (StringMatcher.Instance != null)
|
if (_stringMatcher != null)
|
||||||
StringMatcher.Instance.UserSettingSearchPrecision = value;
|
_stringMatcher.UserSettingSearchPrecision = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,10 +271,26 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double CustomWindowTop { get; set; } = 0;
|
public double CustomWindowTop { get; set; } = 0;
|
||||||
|
|
||||||
public bool KeepMaxResults { get; set; } = false;
|
/// <summary>
|
||||||
public int MaxResultsToShow { get; set; } = 5;
|
/// Fixed window size
|
||||||
public int ActivateTimes { get; set; }
|
/// </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<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
|
||||||
|
|
||||||
|
|
@ -239,11 +307,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public bool EnableUpdateLog { get; set; }
|
public bool EnableUpdateLog { get; set; }
|
||||||
|
|
||||||
public bool StartFlowLauncherOnSystemStartup { get; set; } = false;
|
public bool StartFlowLauncherOnSystemStartup { get; set; } = false;
|
||||||
|
public bool UseLogonTaskForStartup { get; set; } = false;
|
||||||
public bool HideOnStartup { get; set; } = true;
|
public bool HideOnStartup { get; set; } = true;
|
||||||
bool _hideNotifyIcon { get; set; }
|
bool _hideNotifyIcon { get; set; }
|
||||||
public bool HideNotifyIcon
|
public bool HideNotifyIcon
|
||||||
{
|
{
|
||||||
get { return _hideNotifyIcon; }
|
get => _hideNotifyIcon;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_hideNotifyIcon = value;
|
_hideNotifyIcon = value;
|
||||||
|
|
@ -253,6 +322,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
public bool LeaveCmdOpen { get; set; }
|
public bool LeaveCmdOpen { get; set; }
|
||||||
public bool HideWhenDeactivated { get; set; } = true;
|
public bool HideWhenDeactivated { get; set; } = true;
|
||||||
|
|
||||||
|
public bool SearchQueryResultsWithDelay { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public SearchDelayTime SearchDelayTime { get; set; } = SearchDelayTime.Normal;
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
|
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
|
||||||
|
|
||||||
|
|
@ -275,7 +349,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool WMPInstalled { get; set; } = true;
|
public bool WMPInstalled { get; set; } = true;
|
||||||
|
|
||||||
|
|
||||||
// This needs to be loaded last by staying at the bottom
|
// This needs to be loaded last by staying at the bottom
|
||||||
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
|
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
|
||||||
|
|
||||||
|
|
@ -371,7 +444,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
{
|
{
|
||||||
Selected,
|
Selected,
|
||||||
Empty,
|
Empty,
|
||||||
Preserved
|
Preserved,
|
||||||
|
ActionKeywordPreserved,
|
||||||
|
ActionKeywordSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ColorSchemes
|
public enum ColorSchemes
|
||||||
|
|
@ -406,4 +481,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
||||||
Fast,
|
Fast,
|
||||||
Custom
|
Custom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum BackdropTypes
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Acrylic,
|
||||||
|
Mica,
|
||||||
|
MicaAlt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
521
Flow.Launcher.Infrastructure/Win32Helper.cs
Normal file
521
Flow.Launcher.Infrastructure/Win32Helper.cs
Normal file
|
|
@ -0,0 +1,521 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
using Windows.Win32.Graphics.Dwm;
|
||||||
|
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||||
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
|
using Point = System.Windows.Point;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Infrastructure
|
||||||
|
{
|
||||||
|
public static class Win32Helper
|
||||||
|
{
|
||||||
|
#region Blur Handling
|
||||||
|
|
||||||
|
public static bool IsBackdropSupported()
|
||||||
|
{
|
||||||
|
// Mica and Acrylic only supported Windows 11 22000+
|
||||||
|
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||||
|
Environment.OSVersion.Version.Build >= 22000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe bool DWMSetCloakForWindow(Window window, bool cloak)
|
||||||
|
{
|
||||||
|
var cloaked = cloak ? 1 : 0;
|
||||||
|
|
||||||
|
return PInvoke.DwmSetWindowAttribute(
|
||||||
|
GetWindowHandle(window),
|
||||||
|
DWMWINDOWATTRIBUTE.DWMWA_CLOAK,
|
||||||
|
&cloaked,
|
||||||
|
(uint)Marshal.SizeOf<int>()).Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe bool DWMSetBackdropForWindow(Window window, BackdropTypes backdrop)
|
||||||
|
{
|
||||||
|
var backdropType = backdrop switch
|
||||||
|
{
|
||||||
|
BackdropTypes.Acrylic => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TRANSIENTWINDOW,
|
||||||
|
BackdropTypes.Mica => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_MAINWINDOW,
|
||||||
|
BackdropTypes.MicaAlt => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TABBEDWINDOW,
|
||||||
|
_ => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_AUTO
|
||||||
|
};
|
||||||
|
|
||||||
|
return PInvoke.DwmSetWindowAttribute(
|
||||||
|
GetWindowHandle(window),
|
||||||
|
DWMWINDOWATTRIBUTE.DWMWA_SYSTEMBACKDROP_TYPE,
|
||||||
|
&backdropType,
|
||||||
|
(uint)Marshal.SizeOf<int>()).Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMode)
|
||||||
|
{
|
||||||
|
var darkMode = useDarkMode ? 1 : 0;
|
||||||
|
|
||||||
|
return PInvoke.DwmSetWindowAttribute(
|
||||||
|
GetWindowHandle(window),
|
||||||
|
DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||||
|
&darkMode,
|
||||||
|
(uint)Marshal.SizeOf<int>()).Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="window"></param>
|
||||||
|
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static unsafe bool DWMSetCornerPreferenceForWindow(Window window, string cornerType)
|
||||||
|
{
|
||||||
|
var preference = cornerType switch
|
||||||
|
{
|
||||||
|
"DoNotRound" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND,
|
||||||
|
"Round" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND,
|
||||||
|
"RoundSmall" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUNDSMALL,
|
||||||
|
"Default" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DEFAULT,
|
||||||
|
_ => throw new InvalidOperationException("Invalid corner type")
|
||||||
|
};
|
||||||
|
|
||||||
|
return PInvoke.DwmSetWindowAttribute(
|
||||||
|
GetWindowHandle(window),
|
||||||
|
DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE,
|
||||||
|
&preference,
|
||||||
|
(uint)Marshal.SizeOf<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 int GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex)
|
||||||
|
{
|
||||||
|
var style = PInvoke.GetWindowLong(hWnd, nIndex);
|
||||||
|
if (style == 0 && Marshal.GetLastPInvokeError() != 0)
|
||||||
|
{
|
||||||
|
throw new Win32Exception(Marshal.GetLastPInvokeError());
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong)
|
||||||
|
{
|
||||||
|
PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error
|
||||||
|
|
||||||
|
var result = PInvoke.SetWindowLongPtr(hWnd, nIndex, dwNewLong);
|
||||||
|
if (result == 0 && Marshal.GetLastPInvokeError() != 0)
|
||||||
|
{
|
||||||
|
throw new Win32Exception(Marshal.GetLastPInvokeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Window Fullscreen
|
||||||
|
|
||||||
|
private const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass";
|
||||||
|
private const string WINDOW_CLASS_WINTAB = "Flip3D";
|
||||||
|
private const string WINDOW_CLASS_PROGMAN = "Progman";
|
||||||
|
private const string WINDOW_CLASS_WORKERW = "WorkerW";
|
||||||
|
|
||||||
|
private static HWND _hwnd_shell;
|
||||||
|
private static HWND HWND_SHELL =>
|
||||||
|
_hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow();
|
||||||
|
|
||||||
|
private static HWND _hwnd_desktop;
|
||||||
|
private static HWND HWND_DESKTOP =>
|
||||||
|
_hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow();
|
||||||
|
|
||||||
|
public static unsafe bool IsForegroundWindowFullscreen()
|
||||||
|
{
|
||||||
|
// Get current active window
|
||||||
|
var hWnd = PInvoke.GetForegroundWindow();
|
||||||
|
if (hWnd.Equals(HWND.Null))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current active window is desktop or shell, exit early
|
||||||
|
if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string windowClass;
|
||||||
|
const int capacity = 256;
|
||||||
|
Span<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;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Window Handle
|
||||||
|
|
||||||
|
internal static HWND GetWindowHandle(Window window, bool ensure = false)
|
||||||
|
{
|
||||||
|
var windowHelper = new WindowInteropHelper(window);
|
||||||
|
if (ensure)
|
||||||
|
{
|
||||||
|
windowHelper.EnsureHandle();
|
||||||
|
}
|
||||||
|
return new(windowHelper.Handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Keyboard Layout
|
||||||
|
|
||||||
|
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile";
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
|
||||||
|
private const string EnglishLanguageTag = "en";
|
||||||
|
|
||||||
|
private static readonly string[] ImeLanguageTags =
|
||||||
|
{
|
||||||
|
"zh", // Chinese
|
||||||
|
"ja", // Japanese
|
||||||
|
"ko", // Korean
|
||||||
|
};
|
||||||
|
|
||||||
|
private const uint KeyboardLayoutLoWord = 0xFFFF;
|
||||||
|
|
||||||
|
// Store the previous keyboard layout
|
||||||
|
private static HKL _previousLayout;
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
|
||||||
|
// When application is exiting, the Application.Current will be null
|
||||||
|
if (Application.Current == null) return;
|
||||||
|
|
||||||
|
// Get the FL main window
|
||||||
|
var hwnd = GetWindowHandle(Application.Current.MainWindow, true);
|
||||||
|
if (hwnd == HWND.Null) return;
|
||||||
|
|
||||||
|
// Check if the FL main window is the current foreground window
|
||||||
|
if (!IsForegroundWindow(hwnd))
|
||||||
|
{
|
||||||
|
var result = PInvoke.SetForegroundWindow(hwnd);
|
||||||
|
if (!result) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current foreground window thread ID
|
||||||
|
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
|
||||||
|
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||||
|
|
||||||
|
// If the current layout has an IME mode, disable it without switching to another layout.
|
||||||
|
// This is needed because for languages with IME mode, Flow Launcher just temporarily disables
|
||||||
|
// the IME mode instead of switching to another layout.
|
||||||
|
var currentLayout = PInvoke.GetKeyboardLayout(threadId);
|
||||||
|
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
|
||||||
|
foreach (var imeLangTag in ImeLanguageTags)
|
||||||
|
{
|
||||||
|
var langTag = GetLanguageTag(currentLangId);
|
||||||
|
if (langTag.StartsWith(imeLangTag, StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup current keyboard layout
|
||||||
|
if (backupPrevious) _previousLayout = currentLayout;
|
||||||
|
|
||||||
|
// Switch to English layout
|
||||||
|
PInvoke.ActivateKeyboardLayout(enHKL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,9 @@ namespace Flow.Launcher.Plugin
|
||||||
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
|
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default <see cref="SpecialKeyState"/> object with all keys not pressed.
|
||||||
|
/// </summary>
|
||||||
public static readonly SpecialKeyState Default = new () {
|
public static readonly SpecialKeyState Default = new () {
|
||||||
CtrlPressed = false,
|
CtrlPressed = false,
|
||||||
ShiftPressed = false,
|
ShiftPressed = false,
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>4.3.0</Version>
|
<Version>4.4.0</Version>
|
||||||
<PackageVersion>4.3.0</PackageVersion>
|
<PackageVersion>4.4.0</PackageVersion>
|
||||||
<AssemblyVersion>4.3.0</AssemblyVersion>
|
<AssemblyVersion>4.4.0</AssemblyVersion>
|
||||||
<FileVersion>4.3.0</FileVersion>
|
<FileVersion>4.4.0</FileVersion>
|
||||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||||
<Authors>Flow-Launcher</Authors>
|
<Authors>Flow-Launcher</Authors>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|
@ -57,17 +57,25 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Readme.md" Pack="true" PackagePath="\"/>
|
<AdditionalFiles Include="NativeMethods.txt" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Readme.md" Pack="true" PackagePath="\" />
|
||||||
<None Include="FodyWeavers.xml" />
|
<None Include="FodyWeavers.xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Fody" Version="6.5.4">
|
<PackageReference Include="Fody" Version="6.5.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
|
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||||
|
<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" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
40
Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs
Normal file
40
Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This interface is for plugins that wish to provide file preview (external preview)
|
||||||
|
/// via a third party app instead of the default preview.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAsyncExternalPreview : IFeatures
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Method for opening/showing the preview.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The file path to open the preview for</param>
|
||||||
|
/// <param name="sendFailToast">Whether to send a toast message notification on failure for the user</param>
|
||||||
|
public Task OpenPreviewAsync(string path, bool sendFailToast = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method for closing/hiding the preview.
|
||||||
|
/// </summary>
|
||||||
|
public Task ClosePreviewAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method for switching the preview to the next file result.
|
||||||
|
/// This requires the external preview be already open/showing
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The file path to switch the preview for</param>
|
||||||
|
/// <param name="sendFailToast">Whether to send a toast message notification on failure for the user</param>
|
||||||
|
public Task SwitchPreviewAsync(string path, bool sendFailToast = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows the preview plugin to override the AlwaysPreview setting. Typically useful if plugin's preview does not
|
||||||
|
/// fully work well with being shown together when the query window appears with results.
|
||||||
|
/// When AlwaysPreview setting is on and this is set to false, the preview will not be shown when query
|
||||||
|
/// window appears with results, instead the internal preview will be shown.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool AllowAlwaysPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -16,7 +17,8 @@ namespace Flow.Launcher.Plugin
|
||||||
public interface IPublicAPI
|
public interface IPublicAPI
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Change Flow.Launcher query
|
/// Change Flow.Launcher query.
|
||||||
|
/// When current results are from context menu or history, it will go back to query results before changing query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">query text</param>
|
/// <param name="query">query text</param>
|
||||||
/// <param name="requery">
|
/// <param name="requery">
|
||||||
|
|
@ -150,7 +152,6 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <param name="callback"></param>
|
/// <param name="callback"></param>
|
||||||
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
|
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
|
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -180,19 +181,24 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url">URL to download file</param>
|
/// <param name="url">URL to download file</param>
|
||||||
/// <param name="filePath">path to save downloaded file</param>
|
/// <param name="filePath">path to save downloaded file</param>
|
||||||
|
/// <param name="reportProgress">
|
||||||
|
/// Action to report progress. The input of the action is the progress value which is a double value between 0 and 100.
|
||||||
|
/// It will be called if url support range request and the reportProgress is not null.
|
||||||
|
/// </param>
|
||||||
/// <param name="token">place to store file</param>
|
/// <param name="token">place to store file</param>
|
||||||
/// <returns>Task showing the progress</returns>
|
/// <returns>Task showing the progress</returns>
|
||||||
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
|
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="pluginId">ID for plugin that needs to add action keyword</param>
|
/// <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>
|
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
|
||||||
void AddActionKeyword(string pluginId, string newActionKeyword);
|
void AddActionKeyword(string pluginId, string newActionKeyword);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Remove ActionKeyword for specific plugin
|
/// Remove ActionKeyword and update action keyword metadata for specific plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
|
/// <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>
|
/// <param name="oldActionKeyword">The actionkeyword that is supposed to be removed</param>
|
||||||
|
|
@ -294,9 +300,106 @@ namespace Flow.Launcher.Plugin
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reloads the query.
|
/// Reloads the query.
|
||||||
/// This method should run
|
/// When current results are from context menu or history, it will go back to query results before requerying.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reselect">Choose the first result after reload if true; keep the last selected result if false. Default is true.</param>
|
/// <param name="reselect">Choose the first result after reload if true; keep the last selected result if false. Default is true.</param>
|
||||||
public void ReQuery(bool reselect = true);
|
public void ReQuery(bool reselect = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Back to the query results.
|
||||||
|
/// This method should run when selected item is from context menu or history.
|
||||||
|
/// </summary>
|
||||||
|
public void BackToQueryResults();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays a standardised Flow message box.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageBoxText">The message of the message box.</param>
|
||||||
|
/// <param name="caption">The caption of the message box.</param>
|
||||||
|
/// <param name="button">Specifies which button or buttons to display.</param>
|
||||||
|
/// <param name="icon">Specifies the icon to display.</param>
|
||||||
|
/// <param name="defaultResult">Specifies the default result of the message box.</param>
|
||||||
|
/// <returns>Specifies which message box button is clicked by the user.</returns>
|
||||||
|
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays a standardised Flow progress box.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="caption">The caption of the progress box.</param>
|
||||||
|
/// <param name="reportProgressAsync">
|
||||||
|
/// Time-consuming task function, whose input is the action to report progress.
|
||||||
|
/// The input of the action is the progress value which is a double value between 0 and 100.
|
||||||
|
/// If there are any exceptions, this action will be null.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="cancelProgress">When user cancel the progress, this action will be called.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action cancelProgress = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start the loading bar in main window
|
||||||
|
/// </summary>
|
||||||
|
public void StartLoadingBar();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop the loading bar in main window
|
||||||
|
/// </summary>
|
||||||
|
public void StopLoadingBar();
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,42 @@ using System.Threading;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for plugins that want to manually update their results
|
||||||
|
/// </summary>
|
||||||
public interface IResultUpdated : IFeatures
|
public interface IResultUpdated : IFeatures
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is triggered when the results are updated
|
||||||
|
/// </summary>
|
||||||
event ResultUpdatedEventHandler ResultsUpdated;
|
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);
|
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event arguments for the ResultsUpdated event
|
||||||
|
/// </summary>
|
||||||
public class ResultUpdatedEventArgs : EventArgs
|
public class ResultUpdatedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// List of results that should be displayed
|
||||||
|
/// </summary>
|
||||||
public List<Result> Results;
|
public List<Result> Results;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query that triggered the update
|
||||||
|
/// </summary>
|
||||||
public Query Query;
|
public Query Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Token that can be used to cancel the update
|
||||||
|
/// </summary>
|
||||||
public CancellationToken Token { get; init; }
|
public CancellationToken Token { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,15 @@
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This interface is used to create settings panel for .Net plugins
|
||||||
|
/// </summary>
|
||||||
public interface ISettingProvider
|
public interface ISettingProvider
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create settings panel control for .Net plugins
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
Control CreateSettingPanel();
|
Control CreateSettingPanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
Flow.Launcher.Plugin/NativeMethods.txt
Normal file
3
Flow.Launcher.Plugin/NativeMethods.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
EnumThreadWindows
|
||||||
|
GetWindowText
|
||||||
|
GetWindowTextLength
|
||||||
|
|
@ -5,10 +5,18 @@
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PluginInitContext
|
public class PluginInitContext
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default constructor.
|
||||||
|
/// </summary>
|
||||||
public PluginInitContext()
|
public PluginInitContext()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentPluginMetadata"></param>
|
||||||
|
/// <param name="api"></param>
|
||||||
public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api)
|
public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api)
|
||||||
{
|
{
|
||||||
CurrentPluginMetadata = currentPluginMetadata;
|
CurrentPluginMetadata = currentPluginMetadata;
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,77 @@ using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin metadata
|
||||||
|
/// </summary>
|
||||||
public class PluginMetadata : BaseModel
|
public class PluginMetadata : BaseModel
|
||||||
{
|
{
|
||||||
private string _pluginDirectory;
|
/// <summary>
|
||||||
|
/// Plugin ID.
|
||||||
|
/// </summary>
|
||||||
public string ID { get; set; }
|
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>
|
||||||
|
/// Plugin execute file path.
|
||||||
|
/// </summary>
|
||||||
|
public string ExecuteFilePath { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin execute file name.
|
||||||
|
/// </summary>
|
||||||
public string ExecuteFileName { get; set; }
|
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
|
public string PluginDirectory
|
||||||
{
|
{
|
||||||
get { return _pluginDirectory; }
|
get => _pluginDirectory;
|
||||||
internal set
|
internal set
|
||||||
{
|
{
|
||||||
_pluginDirectory = value;
|
_pluginDirectory = value;
|
||||||
|
|
@ -30,28 +83,78 @@ namespace Flow.Launcher.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The first action keyword of plugin.
|
||||||
|
/// </summary>
|
||||||
public string ActionKeyword { get; set; }
|
public string ActionKeyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All action keywords of plugin.
|
||||||
|
/// </summary>
|
||||||
public List<string> ActionKeywords { get; set; }
|
public List<string> ActionKeywords { get; set; }
|
||||||
|
|
||||||
public string IcoPath { get; set;}
|
/// <summary>
|
||||||
|
/// Hide plugin keyword setting panel.
|
||||||
public override string ToString()
|
/// </summary>
|
||||||
{
|
public bool HideActionKeywordPanel { get; set; }
|
||||||
return Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin search delay time. Null means use default search delay time.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public SearchDelayTime? SearchDelayTime { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin icon path.
|
||||||
|
/// </summary>
|
||||||
|
public string IcoPath { get; set;}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin priority.
|
||||||
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Init time include both plugin load time and init time
|
/// Init time include both plugin load time and init time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public long InitTime { get; set; }
|
public long InitTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Average query time.
|
||||||
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public long AvgQueryTime { get; set; }
|
public long AvgQueryTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query count.
|
||||||
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public int QueryCount { get; set; }
|
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
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin instance and plugin metadata
|
||||||
|
/// </summary>
|
||||||
public class PluginPair
|
public class PluginPair
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin instance
|
||||||
|
/// </summary>
|
||||||
public IAsyncPlugin Plugin { get; internal set; }
|
public IAsyncPlugin Plugin { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin metadata
|
||||||
|
/// </summary>
|
||||||
public PluginMetadata Metadata { get; internal set; }
|
public PluginMetadata Metadata { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert to string
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return Metadata.Name;
|
return Metadata.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compare by plugin metadata ID
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object obj)
|
||||||
{
|
{
|
||||||
PluginPair r = obj as PluginPair;
|
if (obj is PluginPair r)
|
||||||
if (r != null)
|
|
||||||
{
|
{
|
||||||
return string.Equals(r.Metadata.ID, Metadata.ID);
|
return string.Equals(r.Metadata.ID, Metadata.ID);
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +41,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get hash code
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
|
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,12 @@
|
||||||
using System;
|
using System.Text.Json.Serialization;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a query that is sent to a plugin.
|
||||||
|
/// </summary>
|
||||||
public class Query
|
public class Query
|
||||||
{
|
{
|
||||||
public Query() { }
|
|
||||||
|
|
||||||
[Obsolete("Use the default Query constructor.")]
|
|
||||||
public Query(string rawQuery, string search, string[] terms, string[] searchTerms, string actionKeyword = "")
|
|
||||||
{
|
|
||||||
Search = search;
|
|
||||||
RawQuery = rawQuery;
|
|
||||||
SearchTerms = searchTerms;
|
|
||||||
ActionKeyword = actionKeyword;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raw query, this includes action keyword if it has
|
/// Raw query, this includes action keyword if it has
|
||||||
/// We didn't recommend use this property directly. You should always use Search property.
|
/// We didn't recommend use this property directly. You should always use Search property.
|
||||||
|
|
@ -51,10 +40,9 @@ namespace Flow.Launcher.Plugin
|
||||||
public const string TermSeparator = " ";
|
public const string TermSeparator = " ";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// User can set multiple action keywords seperated by ';'
|
/// User can set multiple action keywords seperated by whitespace
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ActionKeywordSeparator = ";";
|
public const string ActionKeywordSeparator = TermSeparator;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wildcard action keyword. Plugins using this value will be queried on every search.
|
/// Wildcard action keyword. Plugins using this value will be queried on every search.
|
||||||
|
|
@ -67,13 +55,13 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ActionKeyword { get; init; }
|
public string ActionKeyword { get; init; }
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
|
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
public string FirstSearch => SplitSearch(0);
|
public string FirstSearch => SplitSearch(0);
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -13,7 +12,6 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Result
|
public class Result
|
||||||
{
|
{
|
||||||
|
|
||||||
private string _pluginDirectory;
|
private string _pluginDirectory;
|
||||||
|
|
||||||
private string _icoPath;
|
private string _icoPath;
|
||||||
|
|
@ -70,7 +68,8 @@ namespace Flow.Launcher.Plugin
|
||||||
&& !string.IsNullOrEmpty(PluginDirectory)
|
&& !string.IsNullOrEmpty(PluginDirectory)
|
||||||
&& !Path.IsPathRooted(value)
|
&& !Path.IsPathRooted(value)
|
||||||
&& !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
&& !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_icoPath = Path.Combine(PluginDirectory, value);
|
_icoPath = Path.Combine(PluginDirectory, value);
|
||||||
}
|
}
|
||||||
|
|
@ -156,27 +155,6 @@ namespace Flow.Launcher.Plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
var r = obj as Result;
|
|
||||||
|
|
||||||
var equality = string.Equals(r?.Title, Title) &&
|
|
||||||
string.Equals(r?.SubTitle, SubTitle) &&
|
|
||||||
string.Equals(r?.AutoCompleteText, AutoCompleteText) &&
|
|
||||||
string.Equals(r?.CopyText, CopyText) &&
|
|
||||||
string.Equals(r?.IcoPath, IcoPath) &&
|
|
||||||
TitleHighlightData == r.TitleHighlightData;
|
|
||||||
|
|
||||||
return equality;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return HashCode.Combine(Title, SubTitle, AutoCompleteText, CopyText, IcoPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|
@ -205,6 +183,16 @@ namespace Flow.Launcher.Plugin
|
||||||
TitleHighlightData = TitleHighlightData,
|
TitleHighlightData = TitleHighlightData,
|
||||||
OriginQuery = OriginQuery,
|
OriginQuery = OriginQuery,
|
||||||
PluginDirectory = PluginDirectory,
|
PluginDirectory = PluginDirectory,
|
||||||
|
ContextData = ContextData,
|
||||||
|
PluginID = PluginID,
|
||||||
|
TitleToolTip = TitleToolTip,
|
||||||
|
SubTitleToolTip = SubTitleToolTip,
|
||||||
|
PreviewPanel = PreviewPanel,
|
||||||
|
ProgressBar = ProgressBar,
|
||||||
|
ProgressBarColor = ProgressBarColor,
|
||||||
|
Preview = Preview,
|
||||||
|
AddSelectedCount = AddSelectedCount,
|
||||||
|
RecordKey = RecordKey
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,6 +250,24 @@ namespace Flow.Launcher.Plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PreviewInfo Preview { get; set; } = PreviewInfo.Default;
|
public PreviewInfo Preview { get; set; } = PreviewInfo.Default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the user selection count should be added to the score. This can be useful when set to false to allow the result sequence order to be the same everytime instead of changing based on selection.
|
||||||
|
/// </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.
|
||||||
|
/// If the plugin does not specific this, FL just uses Title and SubTitle to identify this result.
|
||||||
|
/// Note: Because old data does not have this key, we should use null as the default value for consistency.
|
||||||
|
/// </summary>
|
||||||
|
public string RecordKey { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Info of the preview section of a <see cref="Result"/>
|
/// Info of the preview section of a <see cref="Result"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -270,12 +276,12 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full image used for preview panel
|
/// Full image used for preview panel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PreviewImagePath { get; set; }
|
public string PreviewImagePath { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if the preview image should occupy the full width of the preview panel.
|
/// Determines if the preview image should occupy the full width of the preview panel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsMedia { get; set; }
|
public bool IsMedia { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result description text that is shown at the bottom of the preview panel.
|
/// Result description text that is shown at the bottom of the preview panel.
|
||||||
|
|
@ -283,12 +289,17 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// When a value is not set, the <see cref="SubTitle"/> will be used.
|
/// When a value is not set, the <see cref="SubTitle"/> will be used.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string Description { get; set; }
|
public string Description { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delegate to get the preview panel's image
|
/// Delegate to get the preview panel's image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IconDelegate PreviewDelegate { get; set; }
|
public IconDelegate PreviewDelegate { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File path of the result. For third-party programs providing external preview.
|
||||||
|
/// </summary>
|
||||||
|
public string FilePath { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default instance of <see cref="PreviewInfo"/>
|
/// Default instance of <see cref="PreviewInfo"/>
|
||||||
|
|
@ -299,6 +310,7 @@ namespace Flow.Launcher.Plugin
|
||||||
Description = null,
|
Description = null,
|
||||||
IsMedia = false,
|
IsMedia = false,
|
||||||
PreviewDelegate = null,
|
PreviewDelegate = null,
|
||||||
|
FilePath = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
Flow.Launcher.Plugin/SearchDelayTime.cs
Normal file
32
Flow.Launcher.Plugin/SearchDelayTime.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
namespace Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enum for search delay time
|
||||||
|
/// </summary>
|
||||||
|
public enum SearchDelayTime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Very long search delay time. 250ms.
|
||||||
|
/// </summary>
|
||||||
|
VeryLong,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Long search delay time. 200ms.
|
||||||
|
/// </summary>
|
||||||
|
Long,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normal search delay time. 150ms. Default value.
|
||||||
|
/// </summary>
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Short search delay time. 100ms.
|
||||||
|
/// </summary>
|
||||||
|
Short,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Very short search delay time. 50ms.
|
||||||
|
/// </summary>
|
||||||
|
VeryShort
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sourcePath"></param>
|
/// <param name="sourcePath"></param>
|
||||||
/// <param name="targetPath"></param>
|
/// <param name="targetPath"></param>
|
||||||
public static void CopyAll(this string sourcePath, string targetPath)
|
/// <param name="messageBoxExShow"></param>
|
||||||
|
public static void CopyAll(this string sourcePath, string targetPath, Func<string, MessageBoxResult> messageBoxExShow = null)
|
||||||
{
|
{
|
||||||
// Get the subdirectories for the specified directory.
|
// Get the subdirectories for the specified directory.
|
||||||
DirectoryInfo dir = new DirectoryInfo(sourcePath);
|
DirectoryInfo dir = new DirectoryInfo(sourcePath);
|
||||||
|
|
@ -54,7 +55,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
foreach (DirectoryInfo subdir in dirs)
|
foreach (DirectoryInfo subdir in dirs)
|
||||||
{
|
{
|
||||||
string temppath = Path.Combine(targetPath, subdir.Name);
|
string temppath = Path.Combine(targetPath, subdir.Name);
|
||||||
CopyAll(subdir.FullName, temppath);
|
CopyAll(subdir.FullName, temppath, messageBoxExShow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
|
|
@ -62,8 +63,9 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
throw;
|
throw;
|
||||||
#else
|
#else
|
||||||
MessageBox.Show(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath));
|
messageBoxExShow ??= MessageBox.Show;
|
||||||
RemoveFolderIfExists(targetPath);
|
messageBoxExShow(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath));
|
||||||
|
RemoveFolderIfExists(targetPath, messageBoxExShow);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,8 +77,9 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fromPath"></param>
|
/// <param name="fromPath"></param>
|
||||||
/// <param name="toPath"></param>
|
/// <param name="toPath"></param>
|
||||||
|
/// <param name="messageBoxExShow"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath)
|
public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath, Func<string, MessageBoxResult> messageBoxExShow = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -96,7 +99,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
throw;
|
throw;
|
||||||
#else
|
#else
|
||||||
MessageBox.Show(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath));
|
messageBoxExShow ??= MessageBox.Show;
|
||||||
|
messageBoxExShow(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath));
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +111,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
/// Deletes a folder if it exists
|
/// Deletes a folder if it exists
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
public static void RemoveFolderIfExists(this string path)
|
/// <param name="messageBoxExShow"></param>
|
||||||
|
public static void RemoveFolderIfExists(this string path, Func<string, MessageBoxResult> messageBoxExShow = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -119,7 +124,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
throw;
|
throw;
|
||||||
#else
|
#else
|
||||||
MessageBox.Show(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path));
|
messageBoxExShow ??= MessageBox.Show;
|
||||||
|
messageBoxExShow(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +154,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
/// Open a directory window (using the OS's default handler, usually explorer)
|
/// Open a directory window (using the OS's default handler, usually explorer)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileOrFolderPath"></param>
|
/// <param name="fileOrFolderPath"></param>
|
||||||
public static void OpenPath(string fileOrFolderPath)
|
/// <param name="messageBoxExShow"></param>
|
||||||
|
public static void OpenPath(string fileOrFolderPath, Func<string, MessageBoxResult> messageBoxExShow = null)
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
|
|
@ -166,7 +173,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
throw;
|
throw;
|
||||||
#else
|
#else
|
||||||
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath));
|
messageBoxExShow ??= MessageBox.Show;
|
||||||
|
messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +185,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
/// <param name="filePath">File path</param>
|
/// <param name="filePath">File path</param>
|
||||||
/// <param name="workingDir">Working directory</param>
|
/// <param name="workingDir">Working directory</param>
|
||||||
/// <param name="asAdmin">Open as Administrator</param>
|
/// <param name="asAdmin">Open as Administrator</param>
|
||||||
public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false)
|
/// <param name="messageBoxExShow"></param>
|
||||||
|
public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false, Func<string, MessageBoxResult> messageBoxExShow = null)
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
|
|
@ -196,7 +205,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
throw;
|
throw;
|
||||||
#else
|
#else
|
||||||
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", filePath));
|
messageBoxExShow ??= MessageBox.Show;
|
||||||
|
messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", filePath));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -308,5 +318,51 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
{
|
{
|
||||||
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ using System.Linq;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin.SharedCommands
|
namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains methods to open a search in a new browser window or tab.
|
||||||
|
/// </summary>
|
||||||
public static class SearchWeb
|
public static class SearchWeb
|
||||||
{
|
{
|
||||||
private static string GetDefaultBrowserPath()
|
private static string GetDefaultBrowserPath()
|
||||||
|
|
@ -106,4 +109,4 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,32 @@
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Windows.Win32;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin.SharedCommands
|
namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains methods for running shell commands
|
||||||
|
/// </summary>
|
||||||
public static class ShellCommand
|
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);
|
public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
|
||||||
[DllImport("user32.dll")] static extern bool EnumThreadWindows(uint threadId, EnumThreadDelegate lpfn, IntPtr lParam);
|
|
||||||
[DllImport("user32.dll")] static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount);
|
|
||||||
[DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hwnd);
|
|
||||||
|
|
||||||
private static bool containsSecurityWindow;
|
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)
|
public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo)
|
||||||
{
|
{
|
||||||
processStartInfo.Verb = "RunAsUser";
|
processStartInfo.Verb = "RunAsUser";
|
||||||
|
|
@ -28,6 +39,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
CheckSecurityWindow();
|
CheckSecurityWindow();
|
||||||
Thread.Sleep(25);
|
Thread.Sleep(25);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open
|
while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open
|
||||||
{
|
{
|
||||||
containsSecurityWindow = false;
|
containsSecurityWindow = false;
|
||||||
|
|
@ -42,24 +54,42 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
||||||
{
|
{
|
||||||
ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads;
|
ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads;
|
||||||
for (int i = 0; i < ptc.Count; i++)
|
for (int i = 0; i < ptc.Count; i++)
|
||||||
EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero);
|
PInvoke.EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool CheckSecurityThread(IntPtr hwnd, IntPtr lParam)
|
private static BOOL CheckSecurityThread(HWND hwnd, LPARAM lParam)
|
||||||
{
|
{
|
||||||
if (GetWindowTitle(hwnd) == "Windows Security")
|
if (GetWindowTitle(hwnd) == "Windows Security")
|
||||||
containsSecurityWindow = true;
|
containsSecurityWindow = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetWindowTitle(IntPtr hwnd)
|
private static unsafe string GetWindowTitle(HWND hwnd)
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder(GetWindowTextLength(hwnd) + 1);
|
var capacity = PInvoke.GetWindowTextLength(hwnd) + 1;
|
||||||
GetWindowText(hwnd, sb, sb.Capacity);
|
int length;
|
||||||
return sb.ToString();
|
Span<char> buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity];
|
||||||
|
fixed (char* pBuffer = buffer)
|
||||||
|
{
|
||||||
|
// If the window has no title bar or text, if the title bar is empty,
|
||||||
|
// or if the window or control handle is invalid, the return value is zero.
|
||||||
|
length = PInvoke.GetWindowText(hwnd, pBuffer, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer[..length].ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", string arguments = "", string verb = "", bool createNoWindow = false)
|
/// <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)
|
||||||
{
|
{
|
||||||
var info = new ProcessStartInfo
|
var info = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,29 @@
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin.SharedModels
|
namespace Flow.Launcher.Plugin.SharedModels
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the result of a match operation.
|
||||||
|
/// </summary>
|
||||||
public class MatchResult
|
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)
|
public MatchResult(bool success, SearchPrecisionScore searchPrecision)
|
||||||
{
|
{
|
||||||
Success = success;
|
Success = success;
|
||||||
SearchPrecision = searchPrecision;
|
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)
|
public MatchResult(bool success, SearchPrecisionScore searchPrecision, List<int> matchData, int rawScore)
|
||||||
{
|
{
|
||||||
Success = success;
|
Success = success;
|
||||||
|
|
@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels
|
||||||
RawScore = rawScore;
|
RawScore = rawScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the match operation was successful.
|
||||||
|
/// </summary>
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private int _rawScore;
|
private int _rawScore;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The raw calculated search score without any search precision filtering applied.
|
||||||
|
/// </summary>
|
||||||
public int RawScore
|
public int RawScore
|
||||||
{
|
{
|
||||||
get { return _rawScore; }
|
get { return _rawScore; }
|
||||||
|
|
@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<int> MatchData { get; set; }
|
public List<int> MatchData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The search precision score used to filter the search results.
|
||||||
|
/// </summary>
|
||||||
public SearchPrecisionScore SearchPrecision { get; set; }
|
public SearchPrecisionScore SearchPrecision { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the search precision score is met.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public bool IsSearchPrecisionScoreMet()
|
public bool IsSearchPrecisionScoreMet()
|
||||||
{
|
{
|
||||||
return IsSearchPrecisionScoreMet(_rawScore);
|
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
|
public enum SearchPrecisionScore
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The highest search precision score.
|
||||||
|
/// </summary>
|
||||||
Regular = 50,
|
Regular = 50,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The medium search precision score.
|
||||||
|
/// </summary>
|
||||||
Low = 20,
|
Low = 20,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The lowest search precision score.
|
||||||
|
/// </summary>
|
||||||
None = 0
|
None = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
|
|
||||||
namespace Flow.Launcher.Test
|
namespace Flow.Launcher.Test
|
||||||
{
|
{
|
||||||
|
|
@ -35,7 +36,7 @@ namespace Flow.Launcher.Test
|
||||||
[TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)]
|
[TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)]
|
||||||
public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult)
|
public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult)
|
||||||
{
|
{
|
||||||
Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path));
|
ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equality
|
// Equality
|
||||||
|
|
@ -47,7 +48,7 @@ namespace Flow.Launcher.Test
|
||||||
[TestCase(@"c:\foo", @"c:\foo\", true)]
|
[TestCase(@"c:\foo", @"c:\foo\", true)]
|
||||||
public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult)
|
public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult)
|
||||||
{
|
{
|
||||||
Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult));
|
ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,12 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="nunit" Version="3.14.0" />
|
<PackageReference Include="nunit" Version="4.3.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Flow.Launcher.Plugin.SharedModels;
|
using Flow.Launcher.Plugin.SharedModels;
|
||||||
|
|
@ -21,6 +22,8 @@ namespace Flow.Launcher.Test
|
||||||
private const string MicrosoftSqlServerManagementStudio = "Microsoft SQL Server Management Studio";
|
private const string MicrosoftSqlServerManagementStudio = "Microsoft SQL Server Management Studio";
|
||||||
private const string VisualStudioCode = "Visual Studio Code";
|
private const string VisualStudioCode = "Visual Studio Code";
|
||||||
|
|
||||||
|
private readonly IAlphabet alphabet = null;
|
||||||
|
|
||||||
public List<string> GetSearchStrings()
|
public List<string> GetSearchStrings()
|
||||||
=> new List<string>
|
=> new List<string>
|
||||||
{
|
{
|
||||||
|
|
@ -34,7 +37,7 @@ namespace Flow.Launcher.Test
|
||||||
OneOneOneOne
|
OneOneOneOne
|
||||||
};
|
};
|
||||||
|
|
||||||
public List<int> GetPrecisionScores()
|
public static List<int> GetPrecisionScores()
|
||||||
{
|
{
|
||||||
var listToReturn = new List<int>();
|
var listToReturn = new List<int>();
|
||||||
|
|
||||||
|
|
@ -59,7 +62,7 @@ namespace Flow.Launcher.Test
|
||||||
};
|
};
|
||||||
|
|
||||||
var results = new List<Result>();
|
var results = new List<Result>();
|
||||||
var matcher = new StringMatcher();
|
var matcher = new StringMatcher(alphabet);
|
||||||
foreach (var str in sources)
|
foreach (var str in sources)
|
||||||
{
|
{
|
||||||
results.Add(new Result
|
results.Add(new Result
|
||||||
|
|
@ -71,20 +74,20 @@ namespace Flow.Launcher.Test
|
||||||
|
|
||||||
results = results.Where(x => x.Score > 0).OrderByDescending(x => x.Score).ToList();
|
results = results.Where(x => x.Score > 0).OrderByDescending(x => x.Score).ToList();
|
||||||
|
|
||||||
Assert.IsTrue(results.Count == 3);
|
ClassicAssert.IsTrue(results.Count == 3);
|
||||||
Assert.IsTrue(results[0].Title == "Inste");
|
ClassicAssert.IsTrue(results[0].Title == "Inste");
|
||||||
Assert.IsTrue(results[1].Title == "Install Package");
|
ClassicAssert.IsTrue(results[1].Title == "Install Package");
|
||||||
Assert.IsTrue(results[2].Title == "file open in browser-test");
|
ClassicAssert.IsTrue(results[2].Title == "file open in browser-test");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Chrome")]
|
[TestCase("Chrome")]
|
||||||
public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString)
|
public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString)
|
||||||
{
|
{
|
||||||
var compareString = "Can have rum only in my glass";
|
var compareString = "Can have rum only in my glass";
|
||||||
var matcher = new StringMatcher();
|
var matcher = new StringMatcher(alphabet);
|
||||||
var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore;
|
var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore;
|
||||||
|
|
||||||
Assert.True(scoreResult == 0);
|
ClassicAssert.True(scoreResult == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("chr")]
|
[TestCase("chr")]
|
||||||
|
|
@ -97,7 +100,7 @@ namespace Flow.Launcher.Test
|
||||||
string searchTerm)
|
string searchTerm)
|
||||||
{
|
{
|
||||||
var results = new List<Result>();
|
var results = new List<Result>();
|
||||||
var matcher = new StringMatcher();
|
var matcher = new StringMatcher(alphabet);
|
||||||
foreach (var str in GetSearchStrings())
|
foreach (var str in GetSearchStrings())
|
||||||
{
|
{
|
||||||
results.Add(new Result
|
results.Add(new Result
|
||||||
|
|
@ -125,7 +128,7 @@ namespace Flow.Launcher.Test
|
||||||
Debug.WriteLine("###############################################");
|
Debug.WriteLine("###############################################");
|
||||||
Debug.WriteLine("");
|
Debug.WriteLine("");
|
||||||
|
|
||||||
Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore));
|
ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,11 +150,11 @@ namespace Flow.Launcher.Test
|
||||||
string queryString, string compareString, int expectedScore)
|
string queryString, string compareString, int expectedScore)
|
||||||
{
|
{
|
||||||
// When, Given
|
// When, Given
|
||||||
var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
|
var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
|
||||||
var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore;
|
var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore;
|
||||||
|
|
||||||
// Should
|
// Should
|
||||||
Assert.AreEqual(expectedScore, rawScore,
|
ClassicAssert.AreEqual(expectedScore, rawScore,
|
||||||
$"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
|
$"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,7 +184,7 @@ namespace Flow.Launcher.Test
|
||||||
bool expectedPrecisionResult)
|
bool expectedPrecisionResult)
|
||||||
{
|
{
|
||||||
// When
|
// When
|
||||||
var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore};
|
var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore};
|
||||||
|
|
||||||
// Given
|
// Given
|
||||||
var matchResult = matcher.FuzzyMatch(queryString, compareString);
|
var matchResult = matcher.FuzzyMatch(queryString, compareString);
|
||||||
|
|
@ -190,12 +193,12 @@ namespace Flow.Launcher.Test
|
||||||
Debug.WriteLine("###############################################");
|
Debug.WriteLine("###############################################");
|
||||||
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
|
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
|
||||||
Debug.WriteLine(
|
Debug.WriteLine(
|
||||||
$"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
|
$"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
|
||||||
Debug.WriteLine("###############################################");
|
Debug.WriteLine("###############################################");
|
||||||
Debug.WriteLine("");
|
Debug.WriteLine("");
|
||||||
|
|
||||||
// Should
|
// Should
|
||||||
Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
|
ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
|
||||||
$"Query: {queryString}{Environment.NewLine} " +
|
$"Query: {queryString}{Environment.NewLine} " +
|
||||||
$"Compare: {compareString}{Environment.NewLine}" +
|
$"Compare: {compareString}{Environment.NewLine}" +
|
||||||
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
|
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
|
||||||
|
|
@ -232,7 +235,7 @@ namespace Flow.Launcher.Test
|
||||||
bool expectedPrecisionResult)
|
bool expectedPrecisionResult)
|
||||||
{
|
{
|
||||||
// When
|
// When
|
||||||
var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore};
|
var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore};
|
||||||
|
|
||||||
// Given
|
// Given
|
||||||
var matchResult = matcher.FuzzyMatch(queryString, compareString);
|
var matchResult = matcher.FuzzyMatch(queryString, compareString);
|
||||||
|
|
@ -241,12 +244,12 @@ namespace Flow.Launcher.Test
|
||||||
Debug.WriteLine("###############################################");
|
Debug.WriteLine("###############################################");
|
||||||
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
|
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
|
||||||
Debug.WriteLine(
|
Debug.WriteLine(
|
||||||
$"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
|
$"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
|
||||||
Debug.WriteLine("###############################################");
|
Debug.WriteLine("###############################################");
|
||||||
Debug.WriteLine("");
|
Debug.WriteLine("");
|
||||||
|
|
||||||
// Should
|
// Should
|
||||||
Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
|
ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
|
||||||
$"Query:{queryString}{Environment.NewLine} " +
|
$"Query:{queryString}{Environment.NewLine} " +
|
||||||
$"Compare:{compareString}{Environment.NewLine}" +
|
$"Compare:{compareString}{Environment.NewLine}" +
|
||||||
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
|
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
|
||||||
|
|
@ -260,7 +263,7 @@ namespace Flow.Launcher.Test
|
||||||
string queryString, string compareString1, string compareString2)
|
string queryString, string compareString1, string compareString2)
|
||||||
{
|
{
|
||||||
// When
|
// When
|
||||||
var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
|
var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
|
||||||
|
|
||||||
// Given
|
// Given
|
||||||
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
|
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
|
||||||
|
|
@ -277,7 +280,7 @@ namespace Flow.Launcher.Test
|
||||||
Debug.WriteLine("");
|
Debug.WriteLine("");
|
||||||
|
|
||||||
// Should
|
// Should
|
||||||
Assert.True(compareString1Result.Score > compareString2Result.Score,
|
ClassicAssert.True(compareString1Result.Score > compareString2Result.Score,
|
||||||
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
||||||
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
|
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
|
||||||
$"Should be greater than{Environment.NewLine}" +
|
$"Should be greater than{Environment.NewLine}" +
|
||||||
|
|
@ -293,7 +296,7 @@ namespace Flow.Launcher.Test
|
||||||
string queryString, string compareString1, string compareString2)
|
string queryString, string compareString1, string compareString2)
|
||||||
{
|
{
|
||||||
// When
|
// When
|
||||||
var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
|
var matcher = new StringMatcher(alphabet) { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
|
||||||
|
|
||||||
// Given
|
// Given
|
||||||
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
|
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
|
||||||
|
|
@ -310,7 +313,7 @@ namespace Flow.Launcher.Test
|
||||||
Debug.WriteLine("");
|
Debug.WriteLine("");
|
||||||
|
|
||||||
// Should
|
// Should
|
||||||
Assert.True(compareString1Result.Score > compareString2Result.Score,
|
ClassicAssert.True(compareString1Result.Score > compareString2Result.Score,
|
||||||
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
||||||
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
|
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
|
||||||
$"Should be greater than{Environment.NewLine}" +
|
$"Should be greater than{Environment.NewLine}" +
|
||||||
|
|
@ -323,7 +326,7 @@ namespace Flow.Launcher.Test
|
||||||
string secondName, string secondDescription, string secondExecutableName)
|
string secondName, string secondDescription, string secondExecutableName)
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var matcher = new StringMatcher();
|
var matcher = new StringMatcher(alphabet);
|
||||||
var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore;
|
var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore;
|
||||||
var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore;
|
var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore;
|
||||||
var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore;
|
var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore;
|
||||||
|
|
@ -336,7 +339,7 @@ namespace Flow.Launcher.Test
|
||||||
var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max();
|
var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsTrue(firstScore > secondScore,
|
ClassicAssert.IsTrue(firstScore > secondScore,
|
||||||
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
||||||
$"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" +
|
$"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" +
|
||||||
$"Should be greater than{Environment.NewLine}" +
|
$"Should be greater than{Environment.NewLine}" +
|
||||||
|
|
@ -358,9 +361,9 @@ namespace Flow.Launcher.Test
|
||||||
public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString,
|
public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString,
|
||||||
int desiredScore)
|
int desiredScore)
|
||||||
{
|
{
|
||||||
var matcher = new StringMatcher();
|
var matcher = new StringMatcher(alphabet);
|
||||||
var score = matcher.FuzzyMatch(queryString, compareString).Score;
|
var score = matcher.FuzzyMatch(queryString, compareString).Score;
|
||||||
Assert.IsTrue(score == desiredScore,
|
ClassicAssert.IsTrue(score == desiredScore,
|
||||||
$@"Query: ""{queryString}""
|
$@"Query: ""{queryString}""
|
||||||
CompareString: ""{compareString}""
|
CompareString: ""{compareString}""
|
||||||
Score: {score}
|
Score: {score}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
using Flow.Launcher.Infrastructure.Http;
|
using Flow.Launcher.Infrastructure.Http;
|
||||||
|
|
@ -16,16 +17,16 @@ namespace Flow.Launcher.Test
|
||||||
|
|
||||||
proxy.Enabled = true;
|
proxy.Enabled = true;
|
||||||
proxy.Server = "127.0.0.1";
|
proxy.Server = "127.0.0.1";
|
||||||
Assert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}"));
|
ClassicAssert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}"));
|
||||||
Assert.IsNull(Http.WebProxy.Credentials);
|
ClassicAssert.IsNull(Http.WebProxy.Credentials);
|
||||||
|
|
||||||
proxy.UserName = "test";
|
proxy.UserName = "test";
|
||||||
Assert.NotNull(Http.WebProxy.Credentials);
|
ClassicAssert.NotNull(Http.WebProxy.Credentials);
|
||||||
Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName);
|
ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName);
|
||||||
Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, "");
|
ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, "");
|
||||||
|
|
||||||
proxy.Password = "test password";
|
proxy.Password = "test password";
|
||||||
Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password);
|
ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using Flow.Launcher.Core.Plugin;
|
using Flow.Launcher.Core.Plugin;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -15,37 +16,37 @@ namespace Flow.Launcher.Test
|
||||||
// Given
|
// Given
|
||||||
var duplicateList = new List<PluginMetadata>
|
var duplicateList = new List<PluginMetadata>
|
||||||
{
|
{
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||||
Version = "1.0.0"
|
Version = "1.0.0"
|
||||||
},
|
},
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||||
Version = "1.0.1"
|
Version = "1.0.1"
|
||||||
},
|
},
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||||
Version = "1.0.2"
|
Version = "1.0.2"
|
||||||
},
|
},
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||||
Version = "1.0.0"
|
Version = "1.0.0"
|
||||||
},
|
},
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||||
Version = "1.0.0"
|
Version = "1.0.0"
|
||||||
},
|
},
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "ABC0TYUC6D3B7855823D60DC76F28855",
|
ID = "ABC0TYUC6D3B7855823D60DC76F28855",
|
||||||
Version = "1.0.0"
|
Version = "1.0.0"
|
||||||
},
|
},
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "ABC0TYUC6D3B7855823D60DC76F28855",
|
ID = "ABC0TYUC6D3B7855823D60DC76F28855",
|
||||||
Version = "1.0.0"
|
Version = "1.0.0"
|
||||||
|
|
@ -56,11 +57,11 @@ namespace Flow.Launcher.Test
|
||||||
(var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList);
|
(var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2");
|
ClassicAssert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2");
|
||||||
Assert.True(unique.Count() == 1);
|
ClassicAssert.True(unique.Count == 1);
|
||||||
|
|
||||||
Assert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855"));
|
ClassicAssert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855"));
|
||||||
Assert.True(duplicates.Count() == 6);
|
ClassicAssert.True(duplicates.Count == 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -69,12 +70,12 @@ namespace Flow.Launcher.Test
|
||||||
// Given
|
// Given
|
||||||
var duplicateList = new List<PluginMetadata>
|
var duplicateList = new List<PluginMetadata>
|
||||||
{
|
{
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "CEA0TYUC6D3B7855823D60DC76F28855",
|
ID = "CEA0TYUC6D3B7855823D60DC76F28855",
|
||||||
Version = "1.0.0"
|
Version = "1.0.0"
|
||||||
},
|
},
|
||||||
new PluginMetadata
|
new()
|
||||||
{
|
{
|
||||||
ID = "CEA0TYUC6D3B7855823D60DC76F28855",
|
ID = "CEA0TYUC6D3B7855823D60DC76F28855",
|
||||||
Version = "1.0.0"
|
Version = "1.0.0"
|
||||||
|
|
@ -85,8 +86,8 @@ namespace Flow.Launcher.Test
|
||||||
(var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList);
|
(var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.True(unique.Count() == 0);
|
ClassicAssert.True(unique.Count == 0);
|
||||||
Assert.True(duplicates.Count() == 2);
|
ClassicAssert.True(duplicates.Count == 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,10 @@ using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
|
||||||
using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
|
using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
|
||||||
using Flow.Launcher.Plugin.SharedCommands;
|
using Flow.Launcher.Plugin.SharedCommands;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using static Flow.Launcher.Plugin.Explorer.Search.SearchManager;
|
using static Flow.Launcher.Plugin.Explorer.Search.SearchManager;
|
||||||
|
|
||||||
namespace Flow.Launcher.Test.Plugins
|
namespace Flow.Launcher.Test.Plugins
|
||||||
|
|
@ -22,28 +20,6 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class ExplorerTest
|
public class ExplorerTest
|
||||||
{
|
{
|
||||||
#pragma warning disable CS1998 // async method with no await (more readable to leave it async to match the tested signature)
|
|
||||||
private async Task<List<Result>> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken)
|
|
||||||
{
|
|
||||||
return new List<Result>();
|
|
||||||
}
|
|
||||||
#pragma warning restore CS1998
|
|
||||||
|
|
||||||
private List<Result> MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token)
|
|
||||||
{
|
|
||||||
return new List<Result>
|
|
||||||
{
|
|
||||||
new Result
|
|
||||||
{
|
|
||||||
Title = "Result 1"
|
|
||||||
},
|
|
||||||
new Result
|
|
||||||
{
|
|
||||||
Title = "Result 2"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool PreviousLocationExistsReturnsTrue(string dummyString) => true;
|
private bool PreviousLocationExistsReturnsTrue(string dummyString) => true;
|
||||||
|
|
||||||
private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false;
|
private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false;
|
||||||
|
|
@ -57,7 +33,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath);
|
var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.IsTrue(result == expectedString,
|
ClassicAssert.IsTrue(result == expectedString,
|
||||||
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
|
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
|
||||||
$"Actual: {result}{Environment.NewLine}");
|
$"Actual: {result}{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +50,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var queryString = queryConstructor.Directory(folderPath);
|
var queryString = queryConstructor.Directory(folderPath);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
|
ClassicAssert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
|
||||||
$"Expected string: {expectedString}{Environment.NewLine} " +
|
$"Expected string: {expectedString}{Environment.NewLine} " +
|
||||||
$"Actual string was: {queryString}{Environment.NewLine}");
|
$"Actual string was: {queryString}{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +70,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var queryString = queryConstructor.Directory(folderPath, userSearchString);
|
var queryString = queryConstructor.Directory(folderPath, userSearchString);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(expectedString, queryString);
|
ClassicAssert.AreEqual(expectedString, queryString);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows7.0")]
|
[SupportedOSPlatform("windows7.0")]
|
||||||
|
|
@ -105,7 +81,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch;
|
const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch;
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(expectedString, resultString);
|
ClassicAssert.AreEqual(expectedString, resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows7.0")]
|
[SupportedOSPlatform("windows7.0")]
|
||||||
|
|
@ -128,7 +104,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var resultString = queryConstructor.FilesAndFolders(userSearchString);
|
var resultString = queryConstructor.FilesAndFolders(userSearchString);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(expectedString, resultString);
|
ClassicAssert.AreEqual(expectedString, resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -138,13 +114,13 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
string querySearchString, string expectedString)
|
string querySearchString, string expectedString)
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var queryConstructor = new QueryConstructor(new Settings());
|
_ = new QueryConstructor(new Settings());
|
||||||
|
|
||||||
//When
|
//When
|
||||||
var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString);
|
var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.IsTrue(resultString == expectedString,
|
ClassicAssert.IsTrue(resultString == expectedString,
|
||||||
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
|
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
|
||||||
$"Actual string was: {resultString}{Environment.NewLine}");
|
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
@ -162,12 +138,12 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var resultString = queryConstructor.FileContent(userSearchString);
|
var resultString = queryConstructor.FileContent(userSearchString);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.IsTrue(resultString == expectedString,
|
ClassicAssert.IsTrue(resultString == expectedString,
|
||||||
$"Expected query string: {expectedString}{Environment.NewLine} " +
|
$"Expected query string: {expectedString}{Environment.NewLine} " +
|
||||||
$"Actual string was: {resultString}{Environment.NewLine}");
|
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
|
public static void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
|
||||||
{
|
{
|
||||||
// Given
|
// Given
|
||||||
var query = new Query
|
var query = new Query
|
||||||
|
|
@ -181,7 +157,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
|
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.IsTrue(result,
|
ClassicAssert.IsTrue(result,
|
||||||
$"Expected True for file content search. {Environment.NewLine} " +
|
$"Expected True for file content search. {Environment.NewLine} " +
|
||||||
$"Actual result was: {result}{Environment.NewLine}");
|
$"Actual result was: {result}{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
@ -206,7 +182,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var result = FilesFolders.IsLocationPathString(querySearchString);
|
var result = FilesFolders.IsLocationPathString(querySearchString);
|
||||||
|
|
||||||
//Then
|
//Then
|
||||||
Assert.IsTrue(result == expectedResult,
|
ClassicAssert.IsTrue(result == expectedResult,
|
||||||
$"Expected query search string check result is: {expectedResult} {Environment.NewLine} " +
|
$"Expected query search string check result is: {expectedResult} {Environment.NewLine} " +
|
||||||
$"Actual check result is {result} {Environment.NewLine}");
|
$"Actual check result is {result} {Environment.NewLine}");
|
||||||
|
|
||||||
|
|
@ -233,7 +209,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path);
|
var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path);
|
||||||
|
|
||||||
//Then
|
//Then
|
||||||
Assert.IsTrue(previousDirectoryPath == expectedString,
|
ClassicAssert.IsTrue(previousDirectoryPath == expectedString,
|
||||||
$"Expected path string: {expectedString} {Environment.NewLine} " +
|
$"Expected path string: {expectedString} {Environment.NewLine} " +
|
||||||
$"Actual path string is {previousDirectoryPath} {Environment.NewLine}");
|
$"Actual path string is {previousDirectoryPath} {Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +222,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
|
var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
|
||||||
|
|
||||||
//Then
|
//Then
|
||||||
Assert.IsTrue(returnedPath == expectedString,
|
ClassicAssert.IsTrue(returnedPath == expectedString,
|
||||||
$"Expected path string: {expectedString} {Environment.NewLine} " +
|
$"Expected path string: {expectedString} {Environment.NewLine} " +
|
||||||
$"Actual path string is {returnedPath} {Environment.NewLine}");
|
$"Actual path string is {returnedPath} {Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +236,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var resultString = QueryConstructor.RecursiveDirectoryConstraint(path);
|
var resultString = QueryConstructor.RecursiveDirectoryConstraint(path);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(expectedString, resultString);
|
ClassicAssert.AreEqual(expectedString, resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows7.0")]
|
[SupportedOSPlatform("windows7.0")]
|
||||||
|
|
@ -274,7 +250,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
|
var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(expectedString, resultString);
|
ClassicAssert.AreEqual(expectedString, resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")]
|
[TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")]
|
||||||
|
|
@ -305,7 +281,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
|
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(result, expectedResult);
|
ClassicAssert.AreEqual(result, expectedResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")]
|
[TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")]
|
||||||
|
|
@ -334,7 +310,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
|
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(result, expectedResult);
|
ClassicAssert.AreEqual(result, expectedResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")]
|
[TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")]
|
||||||
|
|
@ -366,7 +342,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
|
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(result, expectedResult);
|
ClassicAssert.AreEqual(result, expectedResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")]
|
[TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")]
|
||||||
|
|
@ -398,7 +374,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
|
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(result, expectedResult);
|
ClassicAssert.AreEqual(result, expectedResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(@"c:\foo", @"c:\foo", true)]
|
[TestCase(@"c:\foo", @"c:\foo", true)]
|
||||||
|
|
@ -420,7 +396,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
};
|
};
|
||||||
|
|
||||||
// When, Then
|
// When, Then
|
||||||
Assert.AreEqual(expectedResult, comparator.Equals(result1, result2));
|
ClassicAssert.AreEqual(expectedResult, comparator.Equals(result1, result2));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(@"c:\foo\", @"c:\foo\")]
|
[TestCase(@"c:\foo\", @"c:\foo\")]
|
||||||
|
|
@ -444,7 +420,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var hash2 = comparator.GetHashCode(result2);
|
var hash2 = comparator.GetHashCode(result2);
|
||||||
|
|
||||||
// When, Then
|
// When, Then
|
||||||
Assert.IsTrue(hash1 == hash2);
|
ClassicAssert.IsTrue(hash1 == hash2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(@"%appdata%", true)]
|
[TestCase(@"%appdata%", true)]
|
||||||
|
|
@ -461,7 +437,7 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
var result = EnvironmentVariables.HasEnvironmentVar(path);
|
var result = EnvironmentVariables.HasEnvironmentVar(path);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
Assert.AreEqual(result, expectedResult);
|
ClassicAssert.AreEqual(result, expectedResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using Flow.Launcher.Core.Plugin;
|
using Flow.Launcher.Core.Plugin;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Flow.Launcher.Test.Plugins
|
namespace Flow.Launcher.Test.Plugins
|
||||||
|
|
@ -40,13 +39,13 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
Search = resultText
|
Search = resultText
|
||||||
}, default);
|
}, default);
|
||||||
|
|
||||||
Assert.IsNotNull(results);
|
ClassicAssert.IsNotNull(results);
|
||||||
|
|
||||||
foreach (var result in results)
|
foreach (var result in results)
|
||||||
{
|
{
|
||||||
Assert.IsNotNull(result);
|
ClassicAssert.IsNotNull(result);
|
||||||
Assert.IsNotNull(result.AsyncAction);
|
ClassicAssert.IsNotNull(result.AsyncAction);
|
||||||
Assert.IsNotNull(result.Title);
|
ClassicAssert.IsNotNull(result.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -56,35 +55,11 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>()),
|
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>()),
|
||||||
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>
|
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>
|
||||||
{
|
{
|
||||||
new JsonRPCResult
|
new()
|
||||||
{
|
{
|
||||||
Title = "Test1", SubTitle = "Test2"
|
Title = "Test1", SubTitle = "Test2"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
|
|
||||||
public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference)
|
|
||||||
{
|
|
||||||
var camelText = JsonSerializer.Serialize(reference, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
|
||||||
|
|
||||||
var pascalText = JsonSerializer.Serialize(reference);
|
|
||||||
|
|
||||||
var results1 = await QueryAsync(new Query { Search = camelText }, default);
|
|
||||||
var results2 = await QueryAsync(new Query { Search = pascalText }, default);
|
|
||||||
|
|
||||||
Assert.IsNotNull(results1);
|
|
||||||
Assert.IsNotNull(results2);
|
|
||||||
|
|
||||||
foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result))
|
|
||||||
{
|
|
||||||
Assert.AreEqual(result1, result2);
|
|
||||||
Assert.AreEqual(result1, referenceResult);
|
|
||||||
|
|
||||||
Assert.IsNotNull(result1);
|
|
||||||
Assert.IsNotNull(result1.AsyncAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using Flow.Launcher.Plugin.Url;
|
using Flow.Launcher.Plugin.Url;
|
||||||
|
|
||||||
namespace Flow.Launcher.Test
|
namespace Flow.Launcher.Test.Plugins
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class UrlPluginTest
|
public class UrlPluginTest
|
||||||
|
|
@ -10,23 +11,23 @@ namespace Flow.Launcher.Test
|
||||||
public void URLMatchTest()
|
public void URLMatchTest()
|
||||||
{
|
{
|
||||||
var plugin = new Main();
|
var plugin = new Main();
|
||||||
Assert.IsTrue(plugin.IsURL("http://www.google.com"));
|
ClassicAssert.IsTrue(plugin.IsURL("http://www.google.com"));
|
||||||
Assert.IsTrue(plugin.IsURL("https://www.google.com"));
|
ClassicAssert.IsTrue(plugin.IsURL("https://www.google.com"));
|
||||||
Assert.IsTrue(plugin.IsURL("http://google.com"));
|
ClassicAssert.IsTrue(plugin.IsURL("http://google.com"));
|
||||||
Assert.IsTrue(plugin.IsURL("www.google.com"));
|
ClassicAssert.IsTrue(plugin.IsURL("www.google.com"));
|
||||||
Assert.IsTrue(plugin.IsURL("google.com"));
|
ClassicAssert.IsTrue(plugin.IsURL("google.com"));
|
||||||
Assert.IsTrue(plugin.IsURL("http://localhost"));
|
ClassicAssert.IsTrue(plugin.IsURL("http://localhost"));
|
||||||
Assert.IsTrue(plugin.IsURL("https://localhost"));
|
ClassicAssert.IsTrue(plugin.IsURL("https://localhost"));
|
||||||
Assert.IsTrue(plugin.IsURL("http://localhost:80"));
|
ClassicAssert.IsTrue(plugin.IsURL("http://localhost:80"));
|
||||||
Assert.IsTrue(plugin.IsURL("https://localhost:80"));
|
ClassicAssert.IsTrue(plugin.IsURL("https://localhost:80"));
|
||||||
Assert.IsTrue(plugin.IsURL("http://110.10.10.10"));
|
ClassicAssert.IsTrue(plugin.IsURL("http://110.10.10.10"));
|
||||||
Assert.IsTrue(plugin.IsURL("110.10.10.10"));
|
ClassicAssert.IsTrue(plugin.IsURL("110.10.10.10"));
|
||||||
Assert.IsTrue(plugin.IsURL("ftp://110.10.10.10"));
|
ClassicAssert.IsTrue(plugin.IsURL("ftp://110.10.10.10"));
|
||||||
|
|
||||||
|
|
||||||
Assert.IsFalse(plugin.IsURL("wwww"));
|
ClassicAssert.IsFalse(plugin.IsURL("wwww"));
|
||||||
Assert.IsFalse(plugin.IsURL("wwww.c"));
|
ClassicAssert.IsFalse(plugin.IsURL("wwww.c"));
|
||||||
Assert.IsFalse(plugin.IsURL("wwww.c"));
|
ClassicAssert.IsFalse(plugin.IsURL("wwww.c"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Legacy;
|
||||||
using Flow.Launcher.Core.Plugin;
|
using Flow.Launcher.Core.Plugin;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
|
@ -17,17 +18,17 @@ namespace Flow.Launcher.Test
|
||||||
|
|
||||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||||
|
|
||||||
Assert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);
|
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);
|
||||||
Assert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
|
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
|
||||||
Assert.AreEqual(">", q.ActionKeyword);
|
ClassicAssert.AreEqual(">", q.ActionKeyword);
|
||||||
|
|
||||||
Assert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match.");
|
ClassicAssert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match.");
|
||||||
|
|
||||||
Assert.AreEqual("ping", q.FirstSearch);
|
ClassicAssert.AreEqual("ping", q.FirstSearch);
|
||||||
Assert.AreEqual("google.com", q.SecondSearch);
|
ClassicAssert.AreEqual("google.com", q.SecondSearch);
|
||||||
Assert.AreEqual("-n", q.ThirdSearch);
|
ClassicAssert.AreEqual("-n", q.ThirdSearch);
|
||||||
|
|
||||||
Assert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
|
ClassicAssert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -40,11 +41,11 @@ namespace Flow.Launcher.Test
|
||||||
|
|
||||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||||
|
|
||||||
Assert.AreEqual("> ping google.com -n 20 -6", q.Search);
|
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search);
|
||||||
Assert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
|
ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
|
||||||
Assert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
|
ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
|
||||||
Assert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
|
ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
|
||||||
Assert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
|
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -52,13 +53,13 @@ namespace Flow.Launcher.Test
|
||||||
{
|
{
|
||||||
Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary<string, PluginPair>());
|
Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary<string, PluginPair>());
|
||||||
|
|
||||||
Assert.AreEqual("file.txt file2 file3", q.Search);
|
ClassicAssert.AreEqual("file.txt file2 file3", q.Search);
|
||||||
Assert.AreEqual("", q.ActionKeyword);
|
ClassicAssert.AreEqual("", q.ActionKeyword);
|
||||||
|
|
||||||
Assert.AreEqual("file.txt", q.FirstSearch);
|
ClassicAssert.AreEqual("file.txt", q.FirstSearch);
|
||||||
Assert.AreEqual("file2", q.SecondSearch);
|
ClassicAssert.AreEqual("file2", q.SecondSearch);
|
||||||
Assert.AreEqual("file3", q.ThirdSearch);
|
ClassicAssert.AreEqual("file3", q.ThirdSearch);
|
||||||
Assert.AreEqual("file2 file3", q.SecondToEndSearch);
|
ClassicAssert.AreEqual("file2 file3", q.SecondToEndSearch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
using Flow.Launcher.Core;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Flow.Launcher
|
namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
|
|
@ -31,20 +34,59 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
private void btnDone_OnClick(object sender, RoutedEventArgs _)
|
private void btnDone_OnClick(object sender, RoutedEventArgs _)
|
||||||
{
|
{
|
||||||
var oldActionKeyword = plugin.Metadata.ActionKeywords[0];
|
var oldActionKeywords = plugin.Metadata.ActionKeywords;
|
||||||
var newActionKeyword = tbAction.Text.Trim();
|
|
||||||
newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*";
|
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList();
|
||||||
|
newActionKeywords.RemoveAll(string.IsNullOrEmpty);
|
||||||
if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword))
|
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);
|
if (oldActionKeywords.Count != newActionKeywords.Count)
|
||||||
Close();
|
{
|
||||||
|
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
|
||||||
|
var msg = translater.GetTranslation("newActionKeywordsSameAsOld");
|
||||||
|
MessageBoxEx.Show(msg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned");
|
string msg = translater.GetTranslation("newActionKeywordsHasBeenAssigned");
|
||||||
MessageBox.Show(msg);
|
App.API.ShowMsgBox(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.OnActionKeywordsChanged();
|
||||||
|
Close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
ShutdownMode="OnMainWindowClose"
|
ShutdownMode="OnMainWindowClose"
|
||||||
Startup="OnStartupAsync">
|
Startup="OnStartup">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
|
@ -20,12 +20,17 @@
|
||||||
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
<ResourceDictionary x:Key="HighContrast">
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
</ui:ThemeResources.ThemeDictionaries>
|
</ui:ThemeResources.ThemeDictionaries>
|
||||||
</ui:ThemeResources>
|
</ui:ThemeResources>
|
||||||
<ui:XamlControlsResources />
|
<ui:XamlControlsResources />
|
||||||
<ResourceDictionary Source="pack://application:,,,/Resources/CustomControlTemplate.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/Resources/CustomControlTemplate.xaml" />
|
||||||
<ResourceDictionary Source="pack://application:,,,/Resources/SettingWindowStyle.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/Resources/SettingWindowStyle.xaml" />
|
||||||
<ResourceDictionary Source="pack://application:,,,/Themes/Win11System.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/Themes/Win11Light.xaml" />
|
||||||
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
using Flow.Launcher.Core;
|
using Flow.Launcher.Core;
|
||||||
using Flow.Launcher.Core.Configuration;
|
using Flow.Launcher.Core.Configuration;
|
||||||
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
||||||
|
|
@ -14,91 +15,172 @@ using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Http;
|
using Flow.Launcher.Infrastructure.Http;
|
||||||
using Flow.Launcher.Infrastructure.Image;
|
using Flow.Launcher.Infrastructure.Image;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using Flow.Launcher.Infrastructure.Storage;
|
||||||
using Flow.Launcher.Infrastructure.UserSettings;
|
using Flow.Launcher.Infrastructure.UserSettings;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||||
|
|
||||||
namespace Flow.Launcher
|
namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
public partial class App : IDisposable, ISingleInstanceApp
|
public partial class App : IDisposable, ISingleInstanceApp
|
||||||
{
|
{
|
||||||
public static PublicAPIInstance API { get; private set; }
|
#region Public Properties
|
||||||
private const string Unique = "Flow.Launcher_Unique_Application_Mutex";
|
|
||||||
|
public static IPublicAPI API { get; private set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Fields
|
||||||
|
|
||||||
private static bool _disposed;
|
private static bool _disposed;
|
||||||
private Settings _settings;
|
private MainWindow _mainWindow;
|
||||||
private MainViewModel _mainVM;
|
private readonly MainViewModel _mainVM;
|
||||||
private SettingWindowViewModel _settingsVM;
|
private readonly Settings _settings;
|
||||||
private readonly Updater _updater = new Updater(Flow.Launcher.Properties.Settings.Default.GithubRepo);
|
|
||||||
private readonly Portable _portable = new Portable();
|
// To prevent two disposals running at the same time.
|
||||||
private readonly PinyinAlphabet _alphabet = new PinyinAlphabet();
|
private static readonly object _disposingLock = new();
|
||||||
private StringMatcher _stringMatcher;
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
// Initialize settings
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var storage = new FlowLauncherJsonStorage<Settings>();
|
||||||
|
_settings = storage.Load();
|
||||||
|
_settings.SetStorage(storage);
|
||||||
|
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
ShowErrorMsgBoxAndFailFast("Cannot load setting storage, please check local data directory", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the dependency injection container
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var host = Host.CreateDefaultBuilder()
|
||||||
|
.UseContentRoot(AppContext.BaseDirectory)
|
||||||
|
.ConfigureServices(services => services
|
||||||
|
.AddSingleton(_ => _settings)
|
||||||
|
.AddSingleton(sp => new Updater(sp.GetRequiredService<IPublicAPI>(), Launcher.Properties.Settings.Default.GithubRepo))
|
||||||
|
.AddSingleton<Portable>()
|
||||||
|
.AddSingleton<SettingWindowViewModel>()
|
||||||
|
.AddSingleton<IAlphabet, PinyinAlphabet>()
|
||||||
|
.AddSingleton<StringMatcher>()
|
||||||
|
.AddSingleton<Internationalization>()
|
||||||
|
.AddSingleton<IPublicAPI, PublicAPIInstance>()
|
||||||
|
.AddSingleton<MainViewModel>()
|
||||||
|
.AddSingleton<Theme>()
|
||||||
|
.AddSingleton<WelcomeViewModel>()
|
||||||
|
).Build();
|
||||||
|
Ioc.Default.ConfigureServices(host.Services);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
ShowErrorMsgBoxAndFailFast("Cannot configure dependency injection container, please open new issue in Flow.Launcher", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the public API and Settings first
|
||||||
|
try
|
||||||
|
{
|
||||||
|
API = Ioc.Default.GetRequiredService<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Main
|
||||||
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main()
|
public static void Main()
|
||||||
{
|
{
|
||||||
if (SingleInstance<App>.InitializeAsFirstInstance(Unique))
|
if (SingleInstance<App>.InitializeAsFirstInstance())
|
||||||
{
|
{
|
||||||
using (var application = new App())
|
using var application = new App();
|
||||||
{
|
application.InitializeComponent();
|
||||||
application.InitializeComponent();
|
application.Run();
|
||||||
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 Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
|
||||||
{
|
{
|
||||||
_portable.PreStartCleanUpAfterPortabilityUpdate();
|
// Because new message box api uses MessageBoxEx window,
|
||||||
|
// if it is created and closed before main window is created, it will cause the application to exit.
|
||||||
|
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
|
||||||
|
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||||
|
|
||||||
Log.Info(
|
Log.SetLogLevel(_settings.LogLevel);
|
||||||
"|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
|
||||||
|
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
|
||||||
|
|
||||||
|
Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
||||||
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
|
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
|
||||||
|
|
||||||
RegisterAppDomainExceptions();
|
RegisterAppDomainExceptions();
|
||||||
RegisterDispatcherUnhandledException();
|
RegisterDispatcherUnhandledException();
|
||||||
|
|
||||||
var imageLoadertask = ImageLoader.InitializeAsync();
|
var imageLoadertask = ImageLoader.InitializeAsync();
|
||||||
|
|
||||||
_settingsVM = new SettingWindowViewModel(_updater, _portable);
|
|
||||||
_settings = _settingsVM.Settings;
|
|
||||||
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
|
|
||||||
|
|
||||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||||
|
|
||||||
_alphabet.Initialize(_settings);
|
|
||||||
_stringMatcher = new StringMatcher(_alphabet);
|
|
||||||
StringMatcher.Instance = _stringMatcher;
|
|
||||||
_stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision;
|
|
||||||
|
|
||||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||||
_mainVM = new MainViewModel(_settings);
|
|
||||||
|
|
||||||
API = new PublicAPIInstance(_settingsVM, _mainVM);
|
// Register ResultsUpdated event after all plugins are loaded
|
||||||
|
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
|
||||||
|
|
||||||
Http.API = API;
|
|
||||||
Http.Proxy = _settings.Proxy;
|
Http.Proxy = _settings.Proxy;
|
||||||
|
|
||||||
await PluginManager.InitializePluginsAsync(API);
|
await PluginManager.InitializePluginsAsync();
|
||||||
|
|
||||||
|
// Change language after all plugins are initialized because we need to update plugin title based on their api
|
||||||
|
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
|
||||||
|
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
|
||||||
|
|
||||||
await imageLoadertask;
|
await imageLoadertask;
|
||||||
|
|
||||||
var window = new MainWindow(_settings, _mainVM);
|
_mainWindow = new MainWindow();
|
||||||
|
|
||||||
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
||||||
|
|
||||||
Current.MainWindow = window;
|
Current.MainWindow = _mainWindow;
|
||||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||||
|
|
||||||
// todo temp fix for instance code logic
|
|
||||||
// load plugin before change language, because plugin language also needs be changed
|
|
||||||
InternationalizationManager.Instance.Settings = _settings;
|
|
||||||
InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
|
|
||||||
|
|
||||||
HotKeyMapper.Initialize(_mainVM);
|
|
||||||
|
|
||||||
// main windows needs initialized before theme change because of blur settings
|
// main windows needs initialized before theme change because of blur settings
|
||||||
ThemeManager.Instance.Settings = _settings;
|
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
|
||||||
ThemeManager.Instance.ChangeTheme(_settings.Theme);
|
|
||||||
|
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
|
|
@ -108,11 +190,12 @@ namespace Flow.Launcher
|
||||||
AutoUpdates();
|
AutoUpdates();
|
||||||
|
|
||||||
API.SaveAppAllSettings();
|
API.SaveAppAllSettings();
|
||||||
Log.Info(
|
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
|
||||||
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||||
|
|
||||||
private void AutoStartup()
|
private void AutoStartup()
|
||||||
{
|
{
|
||||||
// we try to enable auto-startup on first launch, or reenable if it was removed
|
// we try to enable auto-startup on first launch, or reenable if it was removed
|
||||||
|
|
@ -121,20 +204,25 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Helper.AutoStartup.Enable();
|
if (_settings.UseLogonTaskForStartup)
|
||||||
|
{
|
||||||
|
Helper.AutoStartup.EnableViaLogonTask();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Helper.AutoStartup.EnableViaRegistry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
// but if it fails (permissions, etc) then don't keep retrying
|
// but if it fails (permissions, etc) then don't keep retrying
|
||||||
// this also gives the user a visual indication in the Settings widget
|
// this also gives the user a visual indication in the Settings widget
|
||||||
_settings.StartFlowLauncherOnSystemStartup = false;
|
_settings.StartFlowLauncherOnSystemStartup = false;
|
||||||
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
|
API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
|
||||||
e.Message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//[Conditional("RELEASE")]
|
|
||||||
private void AutoUpdates()
|
private void AutoUpdates()
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
|
|
@ -143,20 +231,38 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
// check update every 5 hours
|
// check update every 5 hours
|
||||||
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
|
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
|
||||||
await _updater.UpdateAppAsync(API);
|
await Ioc.Default.GetRequiredService<Updater>().UpdateAppAsync();
|
||||||
|
|
||||||
while (await timer.WaitForNextTickAsync())
|
while (await timer.WaitForNextTickAsync())
|
||||||
// check updates on startup
|
// check updates on startup
|
||||||
await _updater.UpdateAppAsync(API);
|
await Ioc.Default.GetRequiredService<Updater>().UpdateAppAsync();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Register Events
|
||||||
|
|
||||||
private void RegisterExitEvents()
|
private void RegisterExitEvents()
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
|
AppDomain.CurrentDomain.ProcessExit += (s, e) =>
|
||||||
Current.Exit += (s, e) => Dispose();
|
{
|
||||||
Current.SessionEnding += (s, e) => Dispose();
|
Log.Info("|App.RegisterExitEvents|Process Exit");
|
||||||
|
Dispose();
|
||||||
|
};
|
||||||
|
|
||||||
|
Current.Exit += (s, e) =>
|
||||||
|
{
|
||||||
|
Log.Info("|App.RegisterExitEvents|Application Exit");
|
||||||
|
Dispose();
|
||||||
|
};
|
||||||
|
|
||||||
|
Current.SessionEnding += (s, e) =>
|
||||||
|
{
|
||||||
|
Log.Info("|App.RegisterExitEvents|Session Ending");
|
||||||
|
Dispose();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -177,20 +283,68 @@ namespace Flow.Launcher
|
||||||
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
|
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
#endregion
|
||||||
|
|
||||||
|
#region IDisposable
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
// if sessionending is called, exit proverbially be called when log off / shutdown
|
// Prevent two disposes at the same time.
|
||||||
// but if sessionending is not called, exit won't be called when log off / shutdown
|
lock (_disposingLock)
|
||||||
if (!_disposed)
|
|
||||||
{
|
{
|
||||||
API.SaveAppAllSettings();
|
if (!disposing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close()
|
||||||
|
// Accessing _mainWindow?.Dispatcher will cause the application stuck
|
||||||
|
// So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher
|
||||||
|
if (!_mainWindow.CanClose)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stopwatch.Normal("|App.Dispose|Dispose cost", () =>
|
||||||
|
{
|
||||||
|
Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------");
|
||||||
|
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
// Dispose needs to be called on the main Windows thread,
|
||||||
|
// since some resources owned by the thread need to be disposed.
|
||||||
|
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
|
||||||
|
_mainVM?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ISingleInstanceApp
|
||||||
|
|
||||||
public void OnSecondAppStarted()
|
public void OnSecondAppStarted()
|
||||||
{
|
{
|
||||||
_mainVM.Show();
|
Ioc.Default.GetRequiredService<MainViewModel>().Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,11 @@
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Button
|
<Button
|
||||||
Grid.Column="4"
|
Grid.Column="1"
|
||||||
Click="BtnCancel_OnClick"
|
Click="BtnCancel_OnClick"
|
||||||
Style="{StaticResource TitleBarCloseButtonStyle}">
|
Style="{StaticResource TitleBarCloseButtonStyle}">
|
||||||
<Path
|
<Path
|
||||||
|
|
@ -61,9 +58,9 @@
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Margin="26,0,26,0">
|
<StackPanel Margin="26 0 26 0">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="0,0,0,12"
|
Margin="0 0 0 12"
|
||||||
FontSize="20"
|
FontSize="20"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Text="{DynamicResource customeQueryHotkeyTitle}"
|
Text="{DynamicResource customeQueryHotkeyTitle}"
|
||||||
|
|
@ -75,10 +72,10 @@
|
||||||
TextWrapping="WrapWithOverflow" />
|
TextWrapping="WrapWithOverflow" />
|
||||||
<Image
|
<Image
|
||||||
Width="478"
|
Width="478"
|
||||||
Margin="0,20,0,0"
|
Margin="0 20 0 0"
|
||||||
Source="/Images/illustration_01.png" />
|
Source="/Images/illustration_01.png" />
|
||||||
|
|
||||||
<Grid Width="478" Margin="0,20,0,0">
|
<Grid Width="478" Margin="0 20 0 0">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition />
|
<RowDefinition />
|
||||||
<RowDefinition />
|
<RowDefinition />
|
||||||
|
|
@ -102,12 +99,12 @@
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
Margin="10,0,10,0"
|
Margin="10 0 10 0"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
HorizontalContentAlignment="Left"
|
HorizontalContentAlignment="Left"
|
||||||
HotkeySettings="{Binding Settings}"
|
DefaultHotkey=""
|
||||||
DefaultHotkey="" />
|
Type="CustomQueryHotkey" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
|
|
@ -127,8 +124,8 @@
|
||||||
x:Name="btnTestActionKeyword"
|
x:Name="btnTestActionKeyword"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="0,0,10,0"
|
Margin="0 0 10 0"
|
||||||
Padding="10,5,10,5"
|
Padding="10 5 10 5"
|
||||||
Click="BtnTestActionKeyword_OnClick"
|
Click="BtnTestActionKeyword_OnClick"
|
||||||
Content="{DynamicResource preview}" />
|
Content="{DynamicResource preview}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -136,21 +133,21 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Border
|
<Border
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Margin="0,14,0,0"
|
Margin="0 14 0 0"
|
||||||
Background="{DynamicResource PopupButtonAreaBGColor}"
|
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||||
BorderThickness="0,1,0,0">
|
BorderThickness="0 1 0 0">
|
||||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnCancel"
|
x:Name="btnCancel"
|
||||||
MinWidth="140"
|
MinWidth="140"
|
||||||
Margin="10,0,5,0"
|
Margin="10 0 5 0"
|
||||||
Click="BtnCancel_OnClick"
|
Click="BtnCancel_OnClick"
|
||||||
Content="{DynamicResource cancel}" />
|
Content="{DynamicResource cancel}" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="btnAdd"
|
x:Name="btnAdd"
|
||||||
MinWidth="140"
|
MinWidth="140"
|
||||||
Margin="5,0,10,0"
|
Margin="5 0 10 0"
|
||||||
Click="btnAdd_OnClick"
|
Click="btnAdd_OnClick"
|
||||||
Style="{StaticResource AccentButtonStyle}">
|
Style="{StaticResource AccentButtonStyle}">
|
||||||
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,18 @@ using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using Flow.Launcher.ViewModel;
|
|
||||||
|
|
||||||
namespace Flow.Launcher
|
namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
public partial class CustomQueryHotkeySetting : Window
|
public partial class CustomQueryHotkeySetting : Window
|
||||||
{
|
{
|
||||||
private SettingWindow _settingWidow;
|
private readonly Settings _settings;
|
||||||
private bool update;
|
private bool update;
|
||||||
private CustomPluginHotkey updateCustomHotkey;
|
private CustomPluginHotkey updateCustomHotkey;
|
||||||
public Settings Settings { get; }
|
|
||||||
|
|
||||||
public CustomQueryHotkeySetting(SettingWindow settingWidow, Settings settings)
|
public CustomQueryHotkeySetting(Settings settings)
|
||||||
{
|
{
|
||||||
_settingWidow = settingWidow;
|
_settings = settings;
|
||||||
Settings = settings;
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,13 +30,13 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
if (!update)
|
if (!update)
|
||||||
{
|
{
|
||||||
Settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
|
_settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
|
||||||
|
|
||||||
var pluginHotkey = new CustomPluginHotkey
|
var pluginHotkey = new CustomPluginHotkey
|
||||||
{
|
{
|
||||||
Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text
|
Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text
|
||||||
};
|
};
|
||||||
Settings.CustomPluginHotkeys.Add(pluginHotkey);
|
_settings.CustomPluginHotkeys.Add(pluginHotkey);
|
||||||
|
|
||||||
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
|
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
|
||||||
}
|
}
|
||||||
|
|
@ -59,11 +56,11 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
public void UpdateItem(CustomPluginHotkey item)
|
public void UpdateItem(CustomPluginHotkey item)
|
||||||
{
|
{
|
||||||
updateCustomHotkey = Settings.CustomPluginHotkeys.FirstOrDefault(o =>
|
updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o =>
|
||||||
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
|
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
|
||||||
if (updateCustomHotkey == null)
|
if (updateCustomHotkey == null)
|
||||||
{
|
{
|
||||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
|
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
|
||||||
Close();
|
Close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -77,8 +74,7 @@ namespace Flow.Launcher
|
||||||
private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e)
|
private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
App.API.ChangeQuery(tbAction.Text);
|
App.API.ChangeQuery(tbAction.Text);
|
||||||
Application.Current.MainWindow.Show();
|
App.API.ShowMainWindow();
|
||||||
Application.Current.MainWindow.Opacity = 1;
|
|
||||||
Application.Current.MainWindow.Focus();
|
Application.Current.MainWindow.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,11 @@
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Button
|
<Button
|
||||||
Grid.Column="4"
|
Grid.Column="1"
|
||||||
Click="BtnCancel_OnClick"
|
Click="BtnCancel_OnClick"
|
||||||
Style="{StaticResource TitleBarCloseButtonStyle}">
|
Style="{StaticResource TitleBarCloseButtonStyle}">
|
||||||
<Path
|
<Path
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,35 @@
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
using Flow.Launcher.ViewModel;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using Flow.Launcher.SettingPages.ViewModels;
|
||||||
|
using Flow.Launcher.Core;
|
||||||
|
|
||||||
namespace Flow.Launcher
|
namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
public partial class CustomShortcutSetting : Window
|
public partial class CustomShortcutSetting : Window
|
||||||
{
|
{
|
||||||
|
private readonly SettingsPaneHotkeyViewModel _hotkeyVm;
|
||||||
public string Key { get; set; } = String.Empty;
|
public string Key { get; set; } = String.Empty;
|
||||||
public string Value { get; set; } = String.Empty;
|
public string Value { get; set; } = String.Empty;
|
||||||
private string originalKey { get; init; } = null;
|
private string originalKey { get; } = null;
|
||||||
private string originalValue { get; init; } = null;
|
private string originalValue { get; } = null;
|
||||||
private bool update { get; init; } = false;
|
private bool update { get; } = false;
|
||||||
|
|
||||||
public CustomShortcutSetting(SettingWindowViewModel vm)
|
public CustomShortcutSetting(SettingsPaneHotkeyViewModel vm)
|
||||||
{
|
{
|
||||||
|
_hotkeyVm = vm;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CustomShortcutSetting(string key, string value)
|
public CustomShortcutSetting(string key, string value, SettingsPaneHotkeyViewModel vm)
|
||||||
{
|
{
|
||||||
Key = key;
|
Key = key;
|
||||||
Value = value;
|
Value = value;
|
||||||
originalKey = key;
|
originalKey = key;
|
||||||
originalValue = value;
|
originalValue = value;
|
||||||
update = true;
|
update = true;
|
||||||
|
_hotkeyVm = vm;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,13 +43,13 @@ namespace Flow.Launcher
|
||||||
{
|
{
|
||||||
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
|
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
|
||||||
{
|
{
|
||||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
|
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check if key is modified or adding a new one
|
// Check if key is modified or adding a new one
|
||||||
if ((update && originalKey != Key) || !update)
|
if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key))
|
||||||
{
|
{
|
||||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
|
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DialogResult = !update || originalKey != Key || originalValue != Value;
|
DialogResult = !update || originalKey != Key || originalValue != Value;
|
||||||
|
|
@ -61,8 +65,7 @@ namespace Flow.Launcher
|
||||||
private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e)
|
private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
App.API.ChangeQuery(tbExpand.Text);
|
App.API.ChangeQuery(tbExpand.Text);
|
||||||
Application.Current.MainWindow.Show();
|
App.API.ShowMainWindow();
|
||||||
Application.Current.MainWindow.Opacity = 1;
|
|
||||||
Application.Current.MainWindow.Focus();
|
Application.Current.MainWindow.Focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
|
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>false</UseWindowsForms>
|
||||||
<StartupObject>Flow.Launcher.App</StartupObject>
|
<StartupObject>Flow.Launcher.App</StartupObject>
|
||||||
<ApplicationIcon>Resources\app.ico</ApplicationIcon>
|
<ApplicationIcon>Resources\app.ico</ApplicationIcon>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
|
@ -83,20 +83,25 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
<PackageReference Include="ChefKeys" Version="0.1.2" />
|
||||||
<PackageReference Include="Fody" Version="6.5.4">
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
|
<PackageReference Include="Fody" Version="6.5.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="InputSimulator" Version="1.0.4" />
|
<PackageReference Include="InputSimulator" Version="1.0.4" />
|
||||||
|
<!-- Do not upgrade Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Hosting since we are .Net7.0 -->
|
||||||
|
<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.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||||
<!-- ModernWpfUI v0.9.5 introduced WinRT changes that causes Notification platform unavailable error on some machines -->
|
<!-- 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 -->
|
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
|
||||||
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
|
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
|
||||||
<PackageReference Include="NHotkey.Wpf" Version="2.1.1" />
|
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||||
<PackageReference Include="SharpVectors" Version="1.8.2" />
|
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
|
||||||
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.8" />
|
<PackageReference Include="TaskScheduler" Version="2.12.1" />
|
||||||
|
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,31 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Principal;
|
||||||
using Flow.Launcher.Infrastructure;
|
using Flow.Launcher.Infrastructure;
|
||||||
using Flow.Launcher.Infrastructure.Logger;
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
using Microsoft.Win32.TaskScheduler;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper;
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
public class AutoStartup
|
public class AutoStartup
|
||||||
{
|
{
|
||||||
private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
|
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 bool IsEnabled
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
// Check if logon task is enabled
|
||||||
|
if (CheckLogonTask())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if registry is enabled
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||||
|
|
@ -28,12 +41,74 @@ public class AutoStartup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Disable()
|
private static bool CheckLogonTask()
|
||||||
|
{
|
||||||
|
using var taskService = new TaskService();
|
||||||
|
var task = taskService.RootFolder.AllTasks.FirstOrDefault(t => t.Name == LogonTaskName);
|
||||||
|
if (task != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Check if the action is the same as the current executable path
|
||||||
|
var action = task.Definition.Actions.FirstOrDefault()!.ToString().Trim();
|
||||||
|
if (!Constant.ExecutablePath.Equals(action, StringComparison.OrdinalIgnoreCase) && !File.Exists(action))
|
||||||
|
{
|
||||||
|
UnscheduleLogonTask();
|
||||||
|
ScheduleLogonTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error("AutoStartup", $"Failed to check logon task: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DisableViaLogonTaskAndRegistry()
|
||||||
|
{
|
||||||
|
Disable(true);
|
||||||
|
Disable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EnableViaLogonTask()
|
||||||
|
{
|
||||||
|
Enable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EnableViaRegistry()
|
||||||
|
{
|
||||||
|
Enable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ChangeToViaLogonTask()
|
||||||
|
{
|
||||||
|
Disable(false);
|
||||||
|
Enable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ChangeToViaRegistry()
|
||||||
|
{
|
||||||
|
Disable(true);
|
||||||
|
Enable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Disable(bool logonTask)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
if (logonTask)
|
||||||
key?.DeleteValue(Constant.FlowLauncher, false);
|
{
|
||||||
|
UnscheduleLogonTask();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||||
|
key?.DeleteValue(Constant.FlowLauncher, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -42,12 +117,19 @@ public class AutoStartup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Enable()
|
private static void Enable(bool logonTask)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
if (logonTask)
|
||||||
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
{
|
||||||
|
ScheduleLogonTask();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||||
|
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -55,4 +137,54 @@ public class AutoStartup
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ScheduleLogonTask()
|
||||||
|
{
|
||||||
|
using var td = TaskService.Instance.NewTask();
|
||||||
|
td.RegistrationInfo.Description = LogonTaskDesc;
|
||||||
|
td.Triggers.Add(new LogonTrigger { UserId = WindowsIdentity.GetCurrent().Name, Delay = TimeSpan.FromSeconds(2) });
|
||||||
|
td.Actions.Add(Constant.ExecutablePath);
|
||||||
|
|
||||||
|
if (IsCurrentUserIsAdmin())
|
||||||
|
{
|
||||||
|
td.Principal.RunLevel = TaskRunLevel.Highest;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.Settings.StopIfGoingOnBatteries = false;
|
||||||
|
td.Settings.DisallowStartIfOnBatteries = false;
|
||||||
|
td.Settings.ExecutionTimeLimit = TimeSpan.Zero;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TaskService.Instance.RootFolder.RegisterTaskDefinition(LogonTaskName, td);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error("AutoStartup", $"Failed to schedule logon task: {e}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool UnscheduleLogonTask()
|
||||||
|
{
|
||||||
|
using var taskService = new TaskService();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
taskService.RootFolder.DeleteTask(LogonTaskName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error("AutoStartup", $"Failed to unschedule logon task: {e}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCurrentUserIsAdmin()
|
||||||
|
{
|
||||||
|
var identity = WindowsIdentity.GetCurrent();
|
||||||
|
var principal = new WindowsPrincipal(identity);
|
||||||
|
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Drawing.Printing;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Interop;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper;
|
|
||||||
|
|
||||||
public class DwmDropShadow
|
|
||||||
{
|
|
||||||
|
|
||||||
[DllImport("dwmapi.dll", PreserveSig = true)]
|
|
||||||
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
|
||||||
|
|
||||||
[DllImport("dwmapi.dll")]
|
|
||||||
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset);
|
|
||||||
|
|
||||||
/// <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 bool DropShadow(Window window)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
WindowInteropHelper helper = new WindowInteropHelper(window);
|
|
||||||
int val = 2;
|
|
||||||
int ret1 = DwmSetWindowAttribute(helper.Handle, 2, ref val, 4);
|
|
||||||
|
|
||||||
if (ret1 == 0)
|
|
||||||
{
|
|
||||||
Margins m = new Margins { Bottom = 0, Left = 0, Right = 0, Top = 0 };
|
|
||||||
int ret2 = DwmExtendFrameIntoClientArea(helper.Handle, ref m);
|
|
||||||
return ret2 == 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// Probably dwmapi.dll not found (incompatible OS)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -4,8 +4,10 @@ using System;
|
||||||
using NHotkey;
|
using NHotkey;
|
||||||
using NHotkey.Wpf;
|
using NHotkey.Wpf;
|
||||||
using Flow.Launcher.Core.Resource;
|
using Flow.Launcher.Core.Resource;
|
||||||
using System.Windows;
|
|
||||||
using Flow.Launcher.ViewModel;
|
using Flow.Launcher.ViewModel;
|
||||||
|
using ChefKeys;
|
||||||
|
using Flow.Launcher.Infrastructure.Logger;
|
||||||
|
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||||
|
|
||||||
namespace Flow.Launcher.Helper;
|
namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
|
|
@ -14,10 +16,10 @@ internal static class HotKeyMapper
|
||||||
private static Settings _settings;
|
private static Settings _settings;
|
||||||
private static MainViewModel _mainViewModel;
|
private static MainViewModel _mainViewModel;
|
||||||
|
|
||||||
internal static void Initialize(MainViewModel mainVM)
|
internal static void Initialize()
|
||||||
{
|
{
|
||||||
_mainViewModel = mainVM;
|
_mainViewModel = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||||
_settings = _mainViewModel.Settings;
|
_settings = Ioc.Default.GetService<Settings>();
|
||||||
|
|
||||||
SetHotkey(_settings.Hotkey, OnToggleHotkey);
|
SetHotkey(_settings.Hotkey, OnToggleHotkey);
|
||||||
LoadCustomPluginHotkey();
|
LoadCustomPluginHotkey();
|
||||||
|
|
@ -29,33 +31,92 @@ internal static class HotKeyMapper
|
||||||
_mainViewModel.ToggleFlowLauncher();
|
_mainViewModel.ToggleFlowLauncher();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void OnToggleHotkeyWithChefKeys()
|
||||||
|
{
|
||||||
|
if (!_mainViewModel.ShouldIgnoreHotkeys())
|
||||||
|
_mainViewModel.ToggleFlowLauncher();
|
||||||
|
}
|
||||||
|
|
||||||
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
||||||
{
|
{
|
||||||
var hotkey = new HotkeyModel(hotkeyStr);
|
var hotkey = new HotkeyModel(hotkeyStr);
|
||||||
SetHotkey(hotkey, action);
|
SetHotkey(hotkey, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SetWithChefKeys(string hotkeyStr)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, OnToggleHotkeyWithChefKeys);
|
||||||
|
ChefKeysManager.Start();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
|
||||||
|
e.Message,
|
||||||
|
e.StackTrace));
|
||||||
|
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
||||||
|
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
||||||
|
MessageBoxEx.Show(errorMsg, errorMsgTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
|
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
|
||||||
{
|
{
|
||||||
string hotkeyStr = hotkey.ToString();
|
string hotkeyStr = hotkey.ToString();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (hotkeyStr == "LWin" || hotkeyStr == "RWin")
|
||||||
|
{
|
||||||
|
SetWithChefKeys(hotkeyStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
|
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Error(
|
||||||
|
string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}",
|
||||||
|
e.Message,
|
||||||
|
e.StackTrace,
|
||||||
|
hotkeyStr));
|
||||||
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
||||||
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
||||||
MessageBox.Show(errorMsg,errorMsgTitle);
|
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void RemoveHotkey(string hotkeyStr)
|
internal static void RemoveHotkey(string hotkeyStr)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(hotkeyStr))
|
try
|
||||||
{
|
{
|
||||||
HotkeyManager.Current.Remove(hotkeyStr);
|
if (hotkeyStr == "LWin" || hotkeyStr == "RWin")
|
||||||
|
{
|
||||||
|
RemoveWithChefKeys(hotkeyStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(hotkeyStr))
|
||||||
|
HotkeyManager.Current.Remove(hotkeyStr);
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}",
|
||||||
|
e.Message,
|
||||||
|
e.StackTrace));
|
||||||
|
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("unregisterHotkeyFailed"), hotkeyStr);
|
||||||
|
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
||||||
|
MessageBoxEx.Show(errorMsg, errorMsgTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveWithChefKeys(string hotkeyStr)
|
||||||
|
{
|
||||||
|
ChefKeysManager.UnregisterHotkey(hotkeyStr);
|
||||||
|
ChefKeysManager.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void LoadCustomPluginHotkey()
|
internal static void LoadCustomPluginHotkey()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.IO.Pipes;
|
using System.IO.Pipes;
|
||||||
using System.Security;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
@ -14,177 +8,11 @@ using System.Windows;
|
||||||
// modified to allow single instace restart
|
// modified to allow single instace restart
|
||||||
namespace Flow.Launcher.Helper
|
namespace Flow.Launcher.Helper
|
||||||
{
|
{
|
||||||
internal enum WM
|
public interface ISingleInstanceApp
|
||||||
{
|
{
|
||||||
NULL = 0x0000,
|
void OnSecondAppStarted();
|
||||||
CREATE = 0x0001,
|
|
||||||
DESTROY = 0x0002,
|
|
||||||
MOVE = 0x0003,
|
|
||||||
SIZE = 0x0005,
|
|
||||||
ACTIVATE = 0x0006,
|
|
||||||
SETFOCUS = 0x0007,
|
|
||||||
KILLFOCUS = 0x0008,
|
|
||||||
ENABLE = 0x000A,
|
|
||||||
SETREDRAW = 0x000B,
|
|
||||||
SETTEXT = 0x000C,
|
|
||||||
GETTEXT = 0x000D,
|
|
||||||
GETTEXTLENGTH = 0x000E,
|
|
||||||
PAINT = 0x000F,
|
|
||||||
CLOSE = 0x0010,
|
|
||||||
QUERYENDSESSION = 0x0011,
|
|
||||||
QUIT = 0x0012,
|
|
||||||
QUERYOPEN = 0x0013,
|
|
||||||
ERASEBKGND = 0x0014,
|
|
||||||
SYSCOLORCHANGE = 0x0015,
|
|
||||||
SHOWWINDOW = 0x0018,
|
|
||||||
ACTIVATEAPP = 0x001C,
|
|
||||||
SETCURSOR = 0x0020,
|
|
||||||
MOUSEACTIVATE = 0x0021,
|
|
||||||
CHILDACTIVATE = 0x0022,
|
|
||||||
QUEUESYNC = 0x0023,
|
|
||||||
GETMINMAXINFO = 0x0024,
|
|
||||||
|
|
||||||
WINDOWPOSCHANGING = 0x0046,
|
|
||||||
WINDOWPOSCHANGED = 0x0047,
|
|
||||||
|
|
||||||
CONTEXTMENU = 0x007B,
|
|
||||||
STYLECHANGING = 0x007C,
|
|
||||||
STYLECHANGED = 0x007D,
|
|
||||||
DISPLAYCHANGE = 0x007E,
|
|
||||||
GETICON = 0x007F,
|
|
||||||
SETICON = 0x0080,
|
|
||||||
NCCREATE = 0x0081,
|
|
||||||
NCDESTROY = 0x0082,
|
|
||||||
NCCALCSIZE = 0x0083,
|
|
||||||
NCHITTEST = 0x0084,
|
|
||||||
NCPAINT = 0x0085,
|
|
||||||
NCACTIVATE = 0x0086,
|
|
||||||
GETDLGCODE = 0x0087,
|
|
||||||
SYNCPAINT = 0x0088,
|
|
||||||
NCMOUSEMOVE = 0x00A0,
|
|
||||||
NCLBUTTONDOWN = 0x00A1,
|
|
||||||
NCLBUTTONUP = 0x00A2,
|
|
||||||
NCLBUTTONDBLCLK = 0x00A3,
|
|
||||||
NCRBUTTONDOWN = 0x00A4,
|
|
||||||
NCRBUTTONUP = 0x00A5,
|
|
||||||
NCRBUTTONDBLCLK = 0x00A6,
|
|
||||||
NCMBUTTONDOWN = 0x00A7,
|
|
||||||
NCMBUTTONUP = 0x00A8,
|
|
||||||
NCMBUTTONDBLCLK = 0x00A9,
|
|
||||||
|
|
||||||
SYSKEYDOWN = 0x0104,
|
|
||||||
SYSKEYUP = 0x0105,
|
|
||||||
SYSCHAR = 0x0106,
|
|
||||||
SYSDEADCHAR = 0x0107,
|
|
||||||
COMMAND = 0x0111,
|
|
||||||
SYSCOMMAND = 0x0112,
|
|
||||||
|
|
||||||
MOUSEMOVE = 0x0200,
|
|
||||||
LBUTTONDOWN = 0x0201,
|
|
||||||
LBUTTONUP = 0x0202,
|
|
||||||
LBUTTONDBLCLK = 0x0203,
|
|
||||||
RBUTTONDOWN = 0x0204,
|
|
||||||
RBUTTONUP = 0x0205,
|
|
||||||
RBUTTONDBLCLK = 0x0206,
|
|
||||||
MBUTTONDOWN = 0x0207,
|
|
||||||
MBUTTONUP = 0x0208,
|
|
||||||
MBUTTONDBLCLK = 0x0209,
|
|
||||||
MOUSEWHEEL = 0x020A,
|
|
||||||
XBUTTONDOWN = 0x020B,
|
|
||||||
XBUTTONUP = 0x020C,
|
|
||||||
XBUTTONDBLCLK = 0x020D,
|
|
||||||
MOUSEHWHEEL = 0x020E,
|
|
||||||
|
|
||||||
|
|
||||||
CAPTURECHANGED = 0x0215,
|
|
||||||
|
|
||||||
ENTERSIZEMOVE = 0x0231,
|
|
||||||
EXITSIZEMOVE = 0x0232,
|
|
||||||
|
|
||||||
IME_SETCONTEXT = 0x0281,
|
|
||||||
IME_NOTIFY = 0x0282,
|
|
||||||
IME_CONTROL = 0x0283,
|
|
||||||
IME_COMPOSITIONFULL = 0x0284,
|
|
||||||
IME_SELECT = 0x0285,
|
|
||||||
IME_CHAR = 0x0286,
|
|
||||||
IME_REQUEST = 0x0288,
|
|
||||||
IME_KEYDOWN = 0x0290,
|
|
||||||
IME_KEYUP = 0x0291,
|
|
||||||
|
|
||||||
NCMOUSELEAVE = 0x02A2,
|
|
||||||
|
|
||||||
DWMCOMPOSITIONCHANGED = 0x031E,
|
|
||||||
DWMNCRENDERINGCHANGED = 0x031F,
|
|
||||||
DWMCOLORIZATIONCOLORCHANGED = 0x0320,
|
|
||||||
DWMWINDOWMAXIMIZEDCHANGE = 0x0321,
|
|
||||||
|
|
||||||
#region Windows 7
|
|
||||||
DWMSENDICONICTHUMBNAIL = 0x0323,
|
|
||||||
DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326,
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
USER = 0x0400,
|
|
||||||
|
|
||||||
// This is the hard-coded message value used by WinForms for Shell_NotifyIcon.
|
|
||||||
// It's relatively safe to reuse.
|
|
||||||
TRAYMOUSEMESSAGE = 0x800, //WM_USER + 1024
|
|
||||||
APP = 0x8000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressUnmanagedCodeSecurity]
|
|
||||||
internal static class NativeMethods
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Delegate declaration that matches WndProc signatures.
|
|
||||||
/// </summary>
|
|
||||||
public delegate IntPtr MessageHandler(WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled);
|
|
||||||
|
|
||||||
[DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)]
|
|
||||||
private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs);
|
|
||||||
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)]
|
|
||||||
private static extern IntPtr _LocalFree(IntPtr hMem);
|
|
||||||
|
|
||||||
|
|
||||||
public static string[] CommandLineToArgvW(string cmdLine)
|
|
||||||
{
|
|
||||||
IntPtr argv = IntPtr.Zero;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int numArgs = 0;
|
|
||||||
|
|
||||||
argv = _CommandLineToArgvW(cmdLine, out numArgs);
|
|
||||||
if (argv == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
throw new Win32Exception();
|
|
||||||
}
|
|
||||||
var result = new string[numArgs];
|
|
||||||
|
|
||||||
for (int i = 0; i < numArgs; i++)
|
|
||||||
{
|
|
||||||
IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr)));
|
|
||||||
result[i] = Marshal.PtrToStringUni(currArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
|
|
||||||
IntPtr p = _LocalFree(argv);
|
|
||||||
// Otherwise LocalFree failed.
|
|
||||||
// Assert.AreEqual(IntPtr.Zero, p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ISingleInstanceApp
|
|
||||||
{
|
|
||||||
void OnSecondAppStarted();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This class checks to make sure that only one instance of
|
/// This class checks to make sure that only one instance of
|
||||||
/// this application is running at a time.
|
/// this application is running at a time.
|
||||||
|
|
@ -196,9 +24,7 @@ namespace Flow.Launcher.Helper
|
||||||
/// running as Administrator, can activate it with command line arguments.
|
/// running as Administrator, can activate it with command line arguments.
|
||||||
/// For most apps, this will not be much of an issue.
|
/// For most apps, this will not be much of an issue.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static class SingleInstance<TApplication>
|
public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp
|
||||||
where TApplication: Application , ISingleInstanceApp
|
|
||||||
|
|
||||||
{
|
{
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
|
|
||||||
|
|
@ -211,15 +37,12 @@ namespace Flow.Launcher.Helper
|
||||||
/// Suffix to the channel name.
|
/// Suffix to the channel name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
|
private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
|
||||||
|
private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Application mutex.
|
/// Application mutex.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static Mutex singleInstanceMutex;
|
internal static Mutex SingleInstanceMutex { get; set; }
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Properties
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
@ -230,24 +53,23 @@ namespace Flow.Launcher.Helper
|
||||||
/// If not, activates the first instance.
|
/// If not, activates the first instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>True if this is the first instance of the application.</returns>
|
/// <returns>True if this is the first instance of the application.</returns>
|
||||||
public static bool InitializeAsFirstInstance( string uniqueName )
|
public static bool InitializeAsFirstInstance()
|
||||||
{
|
{
|
||||||
// Build unique application Id and the IPC channel name.
|
// Build unique application Id and the IPC channel name.
|
||||||
string applicationIdentifier = uniqueName + Environment.UserName;
|
string applicationIdentifier = InstanceMutexName + Environment.UserName;
|
||||||
|
|
||||||
string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
|
string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
|
||||||
|
|
||||||
// Create mutex based on unique application Id to check if this is the first instance of the application.
|
// Create mutex based on unique application Id to check if this is the first instance of the application.
|
||||||
bool firstInstance;
|
SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance);
|
||||||
singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance);
|
|
||||||
if (firstInstance)
|
if (firstInstance)
|
||||||
{
|
{
|
||||||
_ = CreateRemoteService(channelName);
|
_ = CreateRemoteServiceAsync(channelName);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_ = SignalFirstInstance(channelName);
|
_ = SignalFirstInstanceAsync(channelName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -257,84 +79,31 @@ namespace Flow.Launcher.Helper
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Cleanup()
|
public static void Cleanup()
|
||||||
{
|
{
|
||||||
singleInstanceMutex?.ReleaseMutex();
|
SingleInstanceMutex?.ReleaseMutex();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Private Methods
|
#region Private Methods
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>List of command line arg strings.</returns>
|
|
||||||
private static IList<string> GetCommandLineArgs( string uniqueApplicationName )
|
|
||||||
{
|
|
||||||
string[] args = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// The application was not clickonce deployed, get args from standard API's
|
|
||||||
args = Environment.GetCommandLineArgs();
|
|
||||||
}
|
|
||||||
catch (NotSupportedException)
|
|
||||||
{
|
|
||||||
|
|
||||||
// The application was clickonce deployed
|
|
||||||
// Clickonce deployed apps cannot recieve traditional commandline arguments
|
|
||||||
// As a workaround commandline arguments can be written to a shared location before
|
|
||||||
// the app is launched and the app can obtain its commandline arguments from the
|
|
||||||
// shared location
|
|
||||||
string appFolderPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName);
|
|
||||||
|
|
||||||
string cmdLinePath = Path.Combine(appFolderPath, "cmdline.txt");
|
|
||||||
if (File.Exists(cmdLinePath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (TextReader reader = new StreamReader(cmdLinePath, Encoding.Unicode))
|
|
||||||
{
|
|
||||||
args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd());
|
|
||||||
}
|
|
||||||
|
|
||||||
File.Delete(cmdLinePath);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args == null)
|
|
||||||
{
|
|
||||||
args = new string[] { };
|
|
||||||
}
|
|
||||||
|
|
||||||
return new List<string>(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a remote server pipe for communication.
|
/// Creates a remote server pipe for communication.
|
||||||
/// Once receives signal from client, will activate first instance.
|
/// Once receives signal from client, will activate first instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channelName">Application's IPC channel name.</param>
|
/// <param name="channelName">Application's IPC channel name.</param>
|
||||||
private static async Task CreateRemoteService(string channelName)
|
private static async Task CreateRemoteServiceAsync(string channelName)
|
||||||
{
|
{
|
||||||
using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In))
|
using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In);
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
while(true)
|
// Wait for connection to the pipe
|
||||||
{
|
await pipeServer.WaitForConnectionAsync();
|
||||||
// Wait for connection to the pipe
|
|
||||||
await pipeServer.WaitForConnectionAsync();
|
// Do an asynchronous call to ActivateFirstInstance function
|
||||||
if (Application.Current != null)
|
Application.Current?.Dispatcher.Invoke(ActivateFirstInstance);
|
||||||
{
|
|
||||||
// Do an asynchronous call to ActivateFirstInstance function
|
// Disconect client
|
||||||
Application.Current.Dispatcher.Invoke(ActivateFirstInstance);
|
pipeServer.Disconnect();
|
||||||
}
|
|
||||||
// Disconect client
|
|
||||||
pipeServer.Disconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,25 +114,13 @@ namespace Flow.Launcher.Helper
|
||||||
/// <param name="args">
|
/// <param name="args">
|
||||||
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
|
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
|
||||||
/// </param>
|
/// </param>
|
||||||
private static async Task SignalFirstInstance(string channelName)
|
private static async Task SignalFirstInstanceAsync(string channelName)
|
||||||
{
|
{
|
||||||
// Create a client pipe connected to server
|
// Create a client pipe connected to server
|
||||||
using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out))
|
using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out);
|
||||||
{
|
|
||||||
// Connect to the available pipe
|
|
||||||
await pipeClient.ConnectAsync(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
// Connect to the available pipe
|
||||||
/// Callback for activating first instance of the application.
|
await pipeClient.ConnectAsync(0);
|
||||||
/// </summary>
|
|
||||||
/// <param name="arg">Callback argument.</param>
|
|
||||||
/// <returns>Always null.</returns>
|
|
||||||
private static object ActivateFirstInstanceCallback(object o)
|
|
||||||
{
|
|
||||||
ActivateFirstInstance();
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,29 @@ public static class SingletonWindowOpener
|
||||||
{
|
{
|
||||||
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
||||||
?? (T)Activator.CreateInstance(typeof(T), args);
|
?? (T)Activator.CreateInstance(typeof(T), args);
|
||||||
|
|
||||||
// Fix UI bug
|
// Fix UI bug
|
||||||
// Add `window.WindowState = WindowState.Normal`
|
// Add `window.WindowState = WindowState.Normal`
|
||||||
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
|
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
|
||||||
// Not sure why this works tho
|
// Not sure why this works tho
|
||||||
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
|
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
|
||||||
// https://stackoverflow.com/a/59719760/4230390
|
// https://stackoverflow.com/a/59719760/4230390
|
||||||
window.WindowState = WindowState.Normal;
|
// Ensure the window is not minimized before showing it
|
||||||
window.Show();
|
if (window.WindowState == WindowState.Minimized)
|
||||||
|
{
|
||||||
|
window.WindowState = WindowState.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the window is visible
|
||||||
|
if (!window.IsVisible)
|
||||||
|
{
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
window.Activate(); // Bring the window to the foreground if already open
|
||||||
|
}
|
||||||
|
|
||||||
window.Focus();
|
window.Focus();
|
||||||
|
|
||||||
return (T)window;
|
return (T)window;
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue