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)
|
||||
Italiano
|
||||
Slovenský
|
||||
quicklook
|
||||
Tiếng Việt
|
||||
Droplex
|
||||
Preinstalled
|
||||
|
|
|
|||
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
|
|
@ -8,7 +8,8 @@ updates:
|
|||
- package-ecosystem: "nuget" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 3
|
||||
ignore:
|
||||
- dependency-name: "squirrel-windows"
|
||||
reviewers:
|
||||
|
|
|
|||
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:
|
||||
push:
|
||||
branches: ['dev']
|
||||
branches: ['master']
|
||||
paths: ['Plugins/**']
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
@ -369,4 +369,4 @@ jobs:
|
|||
tag_name: "v${{steps.updated-version-windowssettings.outputs.prop}}"
|
||||
body: Visit Flow's [release notes](https://github.com/Flow-Launcher/Flow.Launcher/releases) for changes.
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PUBLISH_PLUGINS }}
|
||||
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.Plugin.SharedCommands;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Configuration
|
||||
{
|
||||
public class Portable : IPortable
|
||||
{
|
||||
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private UpdateManager NewUpdateManager()
|
||||
private static UpdateManager NewUpdateManager()
|
||||
{
|
||||
var applicationFolderName = Constant.ApplicationDirectory
|
||||
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
|
||||
|
|
@ -40,7 +44,7 @@ namespace Flow.Launcher.Core.Configuration
|
|||
#endif
|
||||
IndicateDeletion(DataLocation.PortableDataPath);
|
||||
|
||||
MessageBox.Show("Flow Launcher needs to restart to finish disabling portable mode, " +
|
||||
API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " +
|
||||
"after the restart your portable data profile will be deleted and roaming data profile kept");
|
||||
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
|
|
@ -64,7 +68,7 @@ namespace Flow.Launcher.Core.Configuration
|
|||
#endif
|
||||
IndicateDeletion(DataLocation.RoamingDataPath);
|
||||
|
||||
MessageBox.Show("Flow Launcher needs to restart to finish enabling portable mode, " +
|
||||
API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " +
|
||||
"after the restart your roaming data profile will be deleted and portable data profile kept");
|
||||
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
|
|
@ -77,41 +81,35 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
public void RemoveShortcuts()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
|
||||
}
|
||||
|
||||
public void RemoveUninstallerEntry()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.RemoveUninstallerRegistryEntry();
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.RemoveUninstallerRegistryEntry();
|
||||
}
|
||||
|
||||
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
||||
{
|
||||
FilesFolders.CopyAll(fromLocation, toLocation);
|
||||
FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||
VerifyUserDataAfterMove(fromLocation, toLocation);
|
||||
}
|
||||
|
||||
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
|
||||
{
|
||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation);
|
||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||
}
|
||||
|
||||
public void CreateShortcuts()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
|
||||
}
|
||||
|
||||
public void CreateUninstallerEntry()
|
||||
|
|
@ -125,18 +123,14 @@ namespace Flow.Launcher.Core.Configuration
|
|||
subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String);
|
||||
}
|
||||
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||
}
|
||||
|
||||
internal void IndicateDeletion(string filePathTodelete)
|
||||
private static void IndicateDeletion(string filePathTodelete)
|
||||
{
|
||||
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);
|
||||
using (var _ = File.CreateText(deleteFilePath))
|
||||
{
|
||||
}
|
||||
using var _ = File.CreateText(deleteFilePath);
|
||||
}
|
||||
|
||||
///<summary>
|
||||
|
|
@ -157,13 +151,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
// delete it and prompt the user to pick the portable data location
|
||||
if (File.Exists(roamingDataDeleteFilePath))
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(roamingDataDir);
|
||||
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
|
||||
|
||||
if (MessageBox.Show("Flow Launcher has detected you enabled portable mode, " +
|
||||
if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " +
|
||||
"would you like to move it to a different location?", string.Empty,
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
FilesFolders.OpenPath(Constant.RootDirectory);
|
||||
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
|
@ -172,9 +166,9 @@ namespace Flow.Launcher.Core.Configuration
|
|||
// delete it and notify the user about it.
|
||||
else if (File.Exists(portableDataDeleteFilePath))
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(portableDataDir);
|
||||
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
|
||||
|
||||
MessageBox.Show("Flow Launcher has detected you disabled portable mode, " +
|
||||
API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " +
|
||||
"the relevant shortcuts and uninstaller entry have been created");
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +180,7 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
if (roamingLocationExists && portableLocationExists)
|
||||
{
|
||||
MessageBox.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " +
|
||||
API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " +
|
||||
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
|
||||
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
||||
{
|
||||
public abstract class AbstractPluginEnvironment
|
||||
{
|
||||
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
internal abstract string Language { get; }
|
||||
|
||||
internal abstract string EnvName { get; }
|
||||
|
|
@ -24,7 +28,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
internal virtual string FileDialogFilter => string.Empty;
|
||||
|
||||
internal abstract string PluginsSettingsFilePath { get; set; }
|
||||
internal abstract string PluginsSettingsFilePath { get; set; }
|
||||
|
||||
internal List<PluginMetadata> PluginMetadataList;
|
||||
|
||||
|
|
@ -38,8 +42,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
internal IEnumerable<PluginPair> Setup()
|
||||
{
|
||||
// If no plugin is using the language, return empty list
|
||||
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new List<PluginPair>();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
|
||||
{
|
||||
|
|
@ -50,24 +57,56 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
||||
}
|
||||
|
||||
if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " +
|
||||
$"will require {EnvName} to run. Would you like to download {EnvName}? " +
|
||||
Environment.NewLine + Environment.NewLine +
|
||||
"Click no if it's already installed, " +
|
||||
$"and you will be prompted to select the folder that contains the {EnvName} executable",
|
||||
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
|
||||
var noRuntimeMessage = string.Format(
|
||||
API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
{
|
||||
var msg = $"Please select the {EnvName} executable";
|
||||
string selectedFile;
|
||||
var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
|
||||
|
||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedFile))
|
||||
{
|
||||
PluginsSettingsFilePath = selectedFile;
|
||||
|
||||
}
|
||||
// Nothing selected because user pressed cancel from the file dialog window
|
||||
if (string.IsNullOrEmpty(selectedFile))
|
||||
InstallEnvironment();
|
||||
else
|
||||
{
|
||||
var forceDownloadMessage = string.Format(
|
||||
API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
|
||||
// Let users select valid path or choose to download
|
||||
while (string.IsNullOrEmpty(selectedFile))
|
||||
{
|
||||
if (API.ShowMsgBox(forceDownloadMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
// Continue select file
|
||||
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
}
|
||||
else
|
||||
{
|
||||
// User selected no, break the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedFile))
|
||||
{
|
||||
PluginsSettingsFilePath = selectedFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
InstallEnvironment();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -80,8 +119,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom).");
|
||||
API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
|
||||
Log.Error("PluginsLoader",
|
||||
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
||||
$"{Language}Environment");
|
||||
|
|
@ -94,13 +132,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
|
||||
{
|
||||
if (expectedPath == currentPath)
|
||||
return;
|
||||
if (expectedPath == currentPath) return;
|
||||
|
||||
FilesFolders.RemoveFolderIfExists(installedDirPath);
|
||||
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
|
||||
|
||||
InstallEnvironment();
|
||||
|
||||
}
|
||||
|
||||
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
|
||||
|
|
@ -112,13 +148,16 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
foreach (var metadata in PluginMetadataList)
|
||||
{
|
||||
if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
metadata.AssemblyName = string.Empty;
|
||||
pluginPairs.Add(CreatePluginPair(filePath, metadata));
|
||||
}
|
||||
}
|
||||
|
||||
return pluginPairs;
|
||||
}
|
||||
|
||||
private string GetFileFromDialog(string title, string filter = "")
|
||||
private static string GetFileFromDialog(string title, string filter = "")
|
||||
{
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
|
|
@ -132,7 +171,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
var result = dlg.ShowDialog();
|
||||
return result == DialogResult.OK ? dlg.FileName : string.Empty;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -175,31 +213,33 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
else
|
||||
{
|
||||
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
|
||||
{
|
||||
settings.PluginSettings.PythonExecutablePath
|
||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
|
||||
}
|
||||
|
||||
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
|
||||
{
|
||||
settings.PluginSettings.NodeExecutablePath
|
||||
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(filePath)) return false;
|
||||
|
||||
// DataLocation.PortableDataPath returns the current portable path, this determines if an out
|
||||
// of date path is also a portable path.
|
||||
var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}";
|
||||
var portableAppEnvLocation = Path.Combine("UserData", DataLocation.PluginEnvironments, pluginEnvironmentName);
|
||||
|
||||
return filePath.Contains(portableAppEnvLocation);
|
||||
}
|
||||
|
||||
private static bool IsUsingRoamingPath(string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(filePath)) return false;
|
||||
|
||||
return filePath.StartsWith(DataLocation.RoamingDataPath);
|
||||
}
|
||||
|
|
@ -209,8 +249,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
|
||||
|
||||
// get the substring after "Environments" because we can not determine it dynamically
|
||||
var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count());
|
||||
return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}";
|
||||
var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
|
||||
return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
using Droplex;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
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 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 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
|
||||
// uses Python plugin they need to custom install and use v3.8.9
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.IO;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
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 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 override void InstallEnvironment()
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(InstallPath);
|
||||
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||
|
||||
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.IO;
|
||||
using Droplex;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
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 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 override void InstallEnvironment()
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(InstallPath);
|
||||
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||
|
||||
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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
|
|
@ -17,11 +18,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
||||
|
||||
private static DateTime lastFetchedAt = DateTime.MinValue;
|
||||
private static TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
|
||||
private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
public static List<UserPlugin> UserPlugins { get; private set; }
|
||||
|
||||
public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
|
||||
public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -31,18 +32,26 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
{
|
||||
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
|
||||
|
||||
UserPlugins = results;
|
||||
lastFetchedAt = DateTime.Now;
|
||||
// If the results are empty, we shouldn't update the manifest because the results are invalid.
|
||||
if (results.Count != 0)
|
||||
{
|
||||
UserPlugins = results;
|
||||
lastFetchedAt = DateTime.Now;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e);
|
||||
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(nameof(PluginsManifest), "Http request failed", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
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>
|
||||
<PackageReference Include="Droplex" Version="1.7.0" />
|
||||
<PackageReference Include="FSharp.Core" Version="7.0.401" />
|
||||
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.2.1" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||
<PackageReference Include="FSharp.Core" Version="9.0.201" />
|
||||
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,16 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Microsoft.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using CheckBox = System.Windows.Controls.CheckBox;
|
||||
using Control = System.Windows.Controls.Control;
|
||||
using Orientation = System.Windows.Controls.Orientation;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
using System.Windows.Documents;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -42,7 +30,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
private int RequestId { get; set; }
|
||||
|
||||
private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json");
|
||||
private string SettingPath => Path.Combine(Context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "Settings.json");
|
||||
|
||||
public override List<Result> LoadContextMenus(Result selectedResult)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,32 +1,15 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Microsoft.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using CheckBox = System.Windows.Controls.CheckBox;
|
||||
using Control = System.Windows.Controls.Control;
|
||||
using Orientation = System.Windows.Controls.Orientation;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
using System.Windows.Documents;
|
||||
using static System.Windows.Forms.LinkLabel;
|
||||
using Droplex;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -34,7 +17,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// Represent the plugin that using JsonPRC
|
||||
/// every JsonRPC plugin should has its own plugin instance
|
||||
/// </summary>
|
||||
internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||
public abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable
|
||||
{
|
||||
protected PluginInitContext Context;
|
||||
public const string JsonRPC = "JsonRPC";
|
||||
|
|
@ -44,8 +27,9 @@ namespace Flow.Launcher.Core.Plugin
|
|||
private string SettingConfigurationPath =>
|
||||
Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml");
|
||||
|
||||
private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory,
|
||||
Context.CurrentPluginMetadata.Name, "Settings.json");
|
||||
private string SettingDirectory => Context.CurrentPluginMetadata.PluginSettingsDirectoryPath;
|
||||
|
||||
private string SettingPath => Path.Combine(SettingDirectory, "Settings.json");
|
||||
|
||||
public abstract List<Result> LoadContextMenus(Result selectedResult);
|
||||
|
||||
|
|
@ -155,6 +139,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Settings?.Save();
|
||||
}
|
||||
|
||||
public bool NeedCreateSettingPanel()
|
||||
{
|
||||
return Settings.NeedCreateSettingPanel();
|
||||
}
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
{
|
||||
return Settings.CreateSettingPanel();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
|
@ -7,6 +8,8 @@ using System.Windows.Documents;
|
|||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
public class JsonRPCPluginSettings
|
||||
|
|
@ -16,51 +19,74 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public required string SettingPath { get; init; }
|
||||
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
|
||||
|
||||
public IReadOnlyDictionary<string, object> Inner => Settings;
|
||||
protected ConcurrentDictionary<string, object> Settings { get; set; }
|
||||
public IReadOnlyDictionary<string, object?> Inner => Settings;
|
||||
protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!;
|
||||
public required IPublicAPI API { get; init; }
|
||||
|
||||
private JsonStorage<ConcurrentDictionary<string, object>> _storage;
|
||||
private static readonly string ClassName = nameof(JsonRPCPluginSettings);
|
||||
|
||||
// maybe move to resource?
|
||||
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
|
||||
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
|
||||
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
|
||||
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
|
||||
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
|
||||
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
|
||||
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
|
||||
|
||||
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
|
||||
private static readonly Thickness SettingPanelItemLeftMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftMargin");
|
||||
private static readonly Thickness SettingPanelItemTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemTopBottomMargin");
|
||||
private static readonly Thickness SettingPanelItemLeftTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftTopBottomMargin");
|
||||
private static readonly double SettingPanelTextBoxMinWidth = (double)Application.Current.FindResource("SettingPanelTextBoxMinWidth");
|
||||
private static readonly double SettingPanelPathTextBoxWidth = (double)Application.Current.FindResource("SettingPanelPathTextBoxWidth");
|
||||
private static readonly double SettingPanelAreaTextBoxMinHeight = (double)Application.Current.FindResource("SettingPanelAreaTextBoxMinHeight");
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_storage = new JsonStorage<ConcurrentDictionary<string, object>>(SettingPath);
|
||||
Settings = await _storage.LoadAsync();
|
||||
|
||||
if (Configuration == null)
|
||||
if (Settings == null)
|
||||
{
|
||||
return;
|
||||
_storage = new JsonStorage<ConcurrentDictionary<string, object?>>(SettingPath);
|
||||
Settings = await _storage.LoadAsync();
|
||||
|
||||
// Because value type of settings dictionary is object which causes them to be JsonElement when loading from json files,
|
||||
// we need to convert it to the correct type
|
||||
foreach (var (key, value) in Settings)
|
||||
{
|
||||
if (value is not JsonElement jsonElement) continue;
|
||||
|
||||
Settings[key] = jsonElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => jsonElement.GetString() ?? value,
|
||||
JsonValueKind.True => jsonElement.GetBoolean(),
|
||||
JsonValueKind.False => jsonElement.GetBoolean(),
|
||||
JsonValueKind.Null => null,
|
||||
_ => value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (Configuration == null) return;
|
||||
|
||||
foreach (var (type, attributes) in Configuration.Body)
|
||||
{
|
||||
if (attributes.Name == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Skip if the setting does not have attributes or name
|
||||
if (attributes?.Name == null) continue;
|
||||
|
||||
if (!Settings.ContainsKey(attributes.Name))
|
||||
// Skip if the setting does not have attributes or name
|
||||
if (!NeedSaveInSettings(type)) continue;
|
||||
|
||||
// If need save in settings, we need to make sure the setting exists in the settings file
|
||||
if (Settings.ContainsKey(attributes.Name)) continue;
|
||||
|
||||
if (type == "checkbox")
|
||||
{
|
||||
// If can parse the default value to bool, use it, otherwise use false
|
||||
Settings[attributes.Name] = bool.TryParse(attributes.DefaultValue, out var value) && value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings[attributes.Name] = attributes.DefaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void UpdateSettings(IReadOnlyDictionary<string, object> settings)
|
||||
{
|
||||
if (settings == null || settings.Count == 0)
|
||||
return;
|
||||
if (settings == null || settings.Count == 0) return;
|
||||
|
||||
foreach (var (key, value) in settings)
|
||||
{
|
||||
|
|
@ -71,19 +97,23 @@ namespace Flow.Launcher.Core.Plugin
|
|||
switch (control)
|
||||
{
|
||||
case TextBox textBox:
|
||||
textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty);
|
||||
var text = value as string ?? string.Empty;
|
||||
textBox.Dispatcher.Invoke(() => textBox.Text = text);
|
||||
break;
|
||||
case PasswordBox passwordBox:
|
||||
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty);
|
||||
var password = value as string ?? string.Empty;
|
||||
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = password);
|
||||
break;
|
||||
case ComboBox comboBox:
|
||||
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
|
||||
break;
|
||||
case CheckBox checkBox:
|
||||
checkBox.Dispatcher.Invoke(() =>
|
||||
checkBox.IsChecked = value is bool isChecked
|
||||
? isChecked
|
||||
: bool.Parse(value as string ?? string.Empty));
|
||||
var isChecked = value is bool boolValue
|
||||
? boolValue
|
||||
// If can parse the default value to bool, use it, otherwise use false
|
||||
: value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString)
|
||||
&& boolValueFromString;
|
||||
checkBox.Dispatcher.Invoke(() =>checkBox.IsChecked = isChecked);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,322 +124,381 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
await _storage.SaveAsync();
|
||||
try
|
||||
{
|
||||
await _storage.SaveAsync();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_storage.Save();
|
||||
try
|
||||
{
|
||||
_storage.Save();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public bool NeedCreateSettingPanel()
|
||||
{
|
||||
// If there are no settings or the settings configuration is empty, return null
|
||||
return Settings != null && Configuration != null && Configuration.Body.Count != 0;
|
||||
}
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
{
|
||||
if (Settings == null || Settings.Count == 0)
|
||||
return new();
|
||||
// No need to check if NeedCreateSettingPanel is true because CreateSettingPanel will only be called if it's true
|
||||
// if (!NeedCreateSettingPanel()) return null;
|
||||
|
||||
var settingWindow = new UserControl();
|
||||
var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
||||
|
||||
ColumnDefinition gridCol1 = new ColumnDefinition();
|
||||
ColumnDefinition gridCol2 = new ColumnDefinition();
|
||||
|
||||
gridCol1.Width = new GridLength(70, GridUnitType.Star);
|
||||
gridCol2.Width = new GridLength(30, GridUnitType.Star);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol1);
|
||||
mainPanel.ColumnDefinitions.Add(gridCol2);
|
||||
settingWindow.Content = mainPanel;
|
||||
int rowCount = 0;
|
||||
|
||||
foreach (var (type, attribute) in Configuration.Body)
|
||||
// Create main grid with two columns (Column 1: Auto, Column 2: *)
|
||||
var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
||||
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
|
||||
{
|
||||
Separator sep = new Separator();
|
||||
sep.VerticalAlignment = VerticalAlignment.Top;
|
||||
sep.Margin = settingSepMargin;
|
||||
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
|
||||
var panel = new StackPanel
|
||||
Width = new GridLength(0, GridUnitType.Auto)
|
||||
});
|
||||
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
|
||||
{
|
||||
Width = new GridLength(1, GridUnitType.Star)
|
||||
});
|
||||
|
||||
// Iterate over each setting and create one row for it
|
||||
var rowCount = 0;
|
||||
foreach (var (type, attributes) in Configuration!.Body)
|
||||
{
|
||||
// Skip if the setting does not have attributes or name
|
||||
if (attributes?.Name == null) continue;
|
||||
|
||||
// Add a new row to the main grid
|
||||
mainPanel.RowDefinitions.Add(new RowDefinition()
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelPanelMargin
|
||||
};
|
||||
|
||||
RowDefinition gridRow = new RowDefinition();
|
||||
mainPanel.RowDefinitions.Add(gridRow);
|
||||
var name = new TextBlock()
|
||||
{
|
||||
Text = attribute.Label,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
var desc = new TextBlock()
|
||||
{
|
||||
Text = attribute.Description,
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingDescMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
|
||||
|
||||
if (attribute.Description == null) /* if no description, hide */
|
||||
desc.Visibility = Visibility.Collapsed;
|
||||
|
||||
|
||||
if (type != "textBlock") /* if textBlock, hide desc */
|
||||
{
|
||||
panel.Children.Add(name);
|
||||
panel.Children.Add(desc);
|
||||
}
|
||||
|
||||
|
||||
Grid.SetColumn(panel, 0);
|
||||
Grid.SetRow(panel, rowCount);
|
||||
Height = new GridLength(0, GridUnitType.Auto)
|
||||
});
|
||||
|
||||
// State controls for column 0 and 1
|
||||
StackPanel? panel = null;
|
||||
FrameworkElement contentControl;
|
||||
|
||||
// If the type is textBlock, separator, or checkbox, we do not need to create a panel
|
||||
if (type != "textBlock" && type != "separator" && type != "checkbox")
|
||||
{
|
||||
// Create a panel to hold the label and description
|
||||
panel = new StackPanel
|
||||
{
|
||||
Margin = SettingPanelItemTopBottomMargin,
|
||||
Orientation = Orientation.Vertical,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
// Create a text block for name
|
||||
var name = new TextBlock()
|
||||
{
|
||||
Text = attributes.Label,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
// Create a text block for description
|
||||
TextBlock? desc = null;
|
||||
if (attributes.Description != null)
|
||||
{
|
||||
desc = new TextBlock()
|
||||
{
|
||||
Text = attributes.Description,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
|
||||
desc.SetResourceReference(TextBlock.StyleProperty, "SettingPanelTextBlockDescriptionStyle"); // for theme change
|
||||
}
|
||||
|
||||
// Add the name and description to the panel
|
||||
panel.Children.Add(name);
|
||||
if (desc != null) panel.Children.Add(desc);
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "textBlock":
|
||||
{
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||
Margin = settingTextBlockMargin,
|
||||
Padding = new Thickness(0, 0, 0, 0),
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
contentControl = new TextBlock
|
||||
{
|
||||
Text = attributes.Description?.Replace("\\r\\n", "\r\n") ?? string.Empty,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemTopBottomMargin,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "input":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
MinWidth = SettingPanelTextBoxMinWidth,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = textBox.Text;
|
||||
};
|
||||
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = textBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "inputWithFileBtn":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
case "inputWithFolderBtn":
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0),
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Width = SettingPanelPathTextBoxWidth,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftMargin,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = textBox.Text;
|
||||
};
|
||||
|
||||
var Btn = new System.Windows.Controls.Button()
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
|
||||
};
|
||||
var Btn = new Button()
|
||||
{
|
||||
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);
|
||||
dockPanel.Children.Add(Btn);
|
||||
dockPanel.Children.Add(textBox);
|
||||
contentControl = dockPanel;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
var path = dialog switch
|
||||
{
|
||||
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":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
Height = 120,
|
||||
Margin = settingControlMargin,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var textBox = new TextBox()
|
||||
{
|
||||
MinHeight = SettingPanelAreaTextBoxMinHeight,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
AcceptsReturn = true,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
textBox.TextChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = textBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "passwordBox":
|
||||
{
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
Margin = settingControlMargin,
|
||||
Password = Settings[attribute.Name] as string ?? string.Empty,
|
||||
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var passwordBox = new PasswordBox()
|
||||
{
|
||||
MinWidth = SettingPanelTextBoxMinWidth,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
Password = Settings[attributes.Name] as string ?? string.Empty,
|
||||
PasswordChar = attributes.passwordChar == default ? '*' : attributes.passwordChar,
|
||||
ToolTip = attributes.Description,
|
||||
};
|
||||
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
passwordBox.PasswordChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
|
||||
contentControl = passwordBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = passwordBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "dropdown":
|
||||
{
|
||||
var comboBox = new System.Windows.Controls.ComboBox()
|
||||
{
|
||||
ItemsSource = attribute.Options,
|
||||
SelectedItem = Settings[attribute.Name],
|
||||
Margin = settingControlMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
var comboBox = new ComboBox()
|
||||
{
|
||||
ItemsSource = attributes.Options,
|
||||
SelectedItem = Settings[attributes.Name],
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
|
||||
};
|
||||
comboBox.SelectionChanged += (sender, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = (string)((ComboBox)sender).SelectedItem;
|
||||
};
|
||||
|
||||
contentControl = comboBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = comboBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "checkbox":
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
IsChecked =
|
||||
Settings[attribute.Name] is bool isChecked
|
||||
// If can parse the default value to bool, use it, otherwise use false
|
||||
var defaultValue = bool.TryParse(attributes.DefaultValue, out var value) && value;
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
IsChecked =
|
||||
Settings[attributes.Name] is bool isChecked
|
||||
? isChecked
|
||||
: bool.Parse(attribute.DefaultValue),
|
||||
Margin = settingCheckboxMargin,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
: defaultValue,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemTopBottomMargin,
|
||||
Content = attributes.Label,
|
||||
ToolTip = attributes.Description
|
||||
};
|
||||
|
||||
checkBox.Click += (sender, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
|
||||
};
|
||||
checkBox.Click += (sender, _) =>
|
||||
{
|
||||
Settings[attributes.Name] = ((CheckBox)sender).IsChecked ?? defaultValue;
|
||||
};
|
||||
|
||||
contentControl = checkBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
contentControl = checkBox;
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
case "hyperlink":
|
||||
var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url };
|
||||
|
||||
var linkbtn = new System.Windows.Controls.Button
|
||||
{
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
Margin = settingControlMargin
|
||||
};
|
||||
var hyperlink = new Hyperlink
|
||||
{
|
||||
ToolTip = attributes.Description,
|
||||
NavigateUri = attributes.url
|
||||
};
|
||||
|
||||
linkbtn.Content = attribute.urlLabel;
|
||||
hyperlink.Inlines.Add(attributes.urlLabel);
|
||||
hyperlink.RequestNavigate += (sender, e) =>
|
||||
{
|
||||
API.OpenUrl(e.Uri);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
contentControl = linkbtn;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
var textBlock = new TextBlock()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
textBlock.Inlines.Add(hyperlink);
|
||||
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
contentControl = textBlock;
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
case "separator":
|
||||
{
|
||||
var sep = new Separator();
|
||||
|
||||
sep.SetResourceReference(Separator.StyleProperty, "SettingPanelSeparatorStyle");
|
||||
|
||||
contentControl = sep;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type != "textBlock")
|
||||
SettingControls[attribute.Name] = contentControl;
|
||||
// If type is textBlock or separator, we just add the content control to the main grid
|
||||
if (panel == null)
|
||||
{
|
||||
// Add the content control to the column 0, row rowCount and columnSpan 2 of the main grid
|
||||
mainPanel.Children.Add(contentControl);
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the panel to the column 0 and row rowCount of the main grid
|
||||
mainPanel.Children.Add(panel);
|
||||
Grid.SetColumn(panel, 0);
|
||||
Grid.SetRow(panel, rowCount);
|
||||
|
||||
// Add the content control to the column 1 and row rowCount of the main grid
|
||||
mainPanel.Children.Add(contentControl);
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
}
|
||||
|
||||
// Add into SettingControls for settings storage if need
|
||||
if (NeedSaveInSettings(type)) SettingControls[attributes.Name] = contentControl;
|
||||
|
||||
mainPanel.Children.Add(panel);
|
||||
mainPanel.Children.Add(contentControl);
|
||||
rowCount++;
|
||||
}
|
||||
|
||||
return settingWindow;
|
||||
// Wrap the main grid in a user control
|
||||
return new UserControl()
|
||||
{
|
||||
Content = mainPanel
|
||||
};
|
||||
}
|
||||
|
||||
private static bool NeedSaveInSettings(string type)
|
||||
{
|
||||
return type != "textBlock" && type != "separator" && type != "hyperlink";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,54 +26,33 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
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;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return res.Hide;
|
||||
}
|
||||
|
||||
private JoinableTaskFactory JTF { get; } = new JoinableTaskFactory(new JoinableTaskContext());
|
||||
|
||||
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;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<Result>();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public override async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("query",
|
||||
new object[] { query, Settings.Inner },
|
||||
token);
|
||||
var res = await RPC.InvokeWithCancellationAsync<JsonRPCQueryResponseModel>("query",
|
||||
new object[] { query, Settings.Inner },
|
||||
token);
|
||||
|
||||
var results = ParseResults(res);
|
||||
var results = ParseResults(res);
|
||||
|
||||
return results;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<Result>();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -133,10 +112,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
RPC.StartListening();
|
||||
}
|
||||
|
||||
public virtual Task ReloadDataAsync()
|
||||
public virtual async Task ReloadDataAsync()
|
||||
{
|
||||
SetupJsonRPC();
|
||||
return Task.CompletedTask;
|
||||
try
|
||||
{
|
||||
await RPC.InvokeAsync("reload_data", Context);
|
||||
}
|
||||
catch (RemoteMethodNotFoundException e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async ValueTask DisposeAsync()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -121,10 +120,10 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
|||
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)
|
||||
{
|
||||
return _api.HttpDownloadAsync(url, filePath, token);
|
||||
return _api.HttpDownloadAsync(url, filePath, reportProgress, token);
|
||||
}
|
||||
|
||||
public void AddActionKeyword(string pluginId, string newActionKeyword)
|
||||
|
|
@ -162,16 +161,29 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
|
|||
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
|
||||
}
|
||||
|
||||
|
||||
public void OpenUrl(string url, bool? inPrivate = null)
|
||||
{
|
||||
_api.OpenUrl(url, inPrivate);
|
||||
}
|
||||
|
||||
|
||||
public void OpenAppUri(string appUri)
|
||||
{
|
||||
_api.OpenAppUri(appUri);
|
||||
}
|
||||
|
||||
public void BackToQueryResults()
|
||||
{
|
||||
_api.BackToQueryResults();
|
||||
}
|
||||
|
||||
public void StartLoadingBar()
|
||||
{
|
||||
_api.StartLoadingBar();
|
||||
}
|
||||
|
||||
public void StopLoadingBar()
|
||||
{
|
||||
_api.StopLoadingBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
|
|
@ -9,7 +9,6 @@ using System.Text.Json;
|
|||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
||||
internal abstract class PluginConfig
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -112,7 +111,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
metadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(configPath));
|
||||
metadata.PluginDirectory = pluginDirectory;
|
||||
// for plugins which doesn't has ActionKeywords key
|
||||
metadata.ActionKeywords = metadata.ActionKeywords ?? new List<string> { metadata.ActionKeyword };
|
||||
metadata.ActionKeywords ??= new List<string> { metadata.ActionKeyword };
|
||||
// for plugin still use old ActionKeyword
|
||||
metadata.ActionKeyword = metadata.ActionKeywords?[0];
|
||||
}
|
||||
|
|
@ -137,4 +136,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System.Text.Json;
|
||||
using ISavable = Flow.Launcher.Plugin.ISavable;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -27,11 +28,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
public static readonly HashSet<PluginPair> GlobalPlugins = 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 List<PluginMetadata> _metadatas;
|
||||
private static List<string> _modifiedPlugins = new List<string>();
|
||||
private static List<string> _modifiedPlugins = new();
|
||||
|
||||
/// <summary>
|
||||
/// Directories that will hold Flow Launcher plugin directory
|
||||
|
|
@ -51,7 +54,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save json and ISavable
|
||||
/// Save json and ISavable
|
||||
/// </summary>
|
||||
public static void Save()
|
||||
{
|
||||
|
|
@ -68,15 +71,20 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
foreach (var pluginPair in AllPlugins)
|
||||
{
|
||||
switch (pluginPair.Plugin)
|
||||
{
|
||||
case IDisposable disposable:
|
||||
disposable.Dispose();
|
||||
break;
|
||||
case IAsyncDisposable asyncDisposable:
|
||||
await asyncDisposable.DisposeAsync();
|
||||
break;
|
||||
}
|
||||
await DisposePluginAsync(pluginPair);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DisposePluginAsync(PluginPair pluginPair)
|
||||
{
|
||||
switch (pluginPair.Plugin)
|
||||
{
|
||||
case IDisposable disposable:
|
||||
disposable.Dispose();
|
||||
break;
|
||||
case IAsyncDisposable asyncDisposable:
|
||||
await asyncDisposable.DisposeAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +98,48 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}).ToArray());
|
||||
}
|
||||
|
||||
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public static async Task CloseExternalPreviewAsync()
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.ClosePreviewAsync(),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
public static bool UseExternalPreview()
|
||||
{
|
||||
return GetPluginsForInterface<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()
|
||||
{
|
||||
// validate user directory
|
||||
|
|
@ -109,15 +159,33 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Settings = settings;
|
||||
Settings.UpdatePluginSettings(_metadatas);
|
||||
AllPlugins = PluginsLoader.Plugins(_metadatas, Settings);
|
||||
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
|
||||
UpdatePluginDirectory(_metadatas);
|
||||
}
|
||||
|
||||
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
|
||||
{
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
if (AllowedLanguage.IsDotNet(metadata.Language))
|
||||
{
|
||||
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>
|
||||
/// Call initialize for all plugins
|
||||
/// </summary>
|
||||
/// <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 InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
|
||||
|
|
@ -163,9 +231,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (failedPlugins.Any())
|
||||
{
|
||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||
API.ShowMsg($"Fail to Init Plugins",
|
||||
$"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help",
|
||||
"", false);
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("failedToInitializePluginsTitle"),
|
||||
string.Format(
|
||||
API.GetTranslation("failedToInitializePluginsMessage"),
|
||||
failed
|
||||
),
|
||||
"",
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,12 +247,10 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
if (query is null)
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
|
||||
|
||||
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
|
||||
return GlobalPlugins;
|
||||
|
||||
|
||||
var plugin = NonGlobalPlugins[query.ActionKeyword];
|
||||
|
||||
return new List<PluginPair>
|
||||
{
|
||||
plugin
|
||||
|
|
@ -229,7 +301,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
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)
|
||||
{
|
||||
|
|
@ -237,8 +309,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
r.PluginID = metadata.ID;
|
||||
r.OriginQuery = query;
|
||||
|
||||
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
|
||||
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
|
||||
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
|
||||
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
|
||||
if (metadata.ActionKeywords.Count == 1)
|
||||
r.ActionKeywordAssigned = query.ActionKeyword;
|
||||
}
|
||||
|
|
@ -256,7 +328,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
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)
|
||||
|
|
@ -312,7 +385,16 @@ namespace Flow.Launcher.Core.Plugin
|
|||
NonGlobalPlugins[newActionKeyword] = plugin;
|
||||
}
|
||||
|
||||
// Update action keywords and action keyword in plugin metadata
|
||||
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
|
||||
if (plugin.Metadata.ActionKeywords.Count > 0)
|
||||
{
|
||||
plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
plugin.Metadata.ActionKeyword = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -333,16 +415,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
|
||||
NonGlobalPlugins.Remove(oldActionkeyword);
|
||||
|
||||
|
||||
// Update action keywords and action keyword in plugin metadata
|
||||
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
|
||||
}
|
||||
|
||||
public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword)
|
||||
{
|
||||
if (oldActionKeyword != newActionKeyword)
|
||||
if (plugin.Metadata.ActionKeywords.Count > 0)
|
||||
{
|
||||
AddActionKeyword(id, newActionKeyword);
|
||||
RemoveActionKeyword(id, oldActionKeyword);
|
||||
plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
plugin.Metadata.ActionKeyword = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -373,37 +454,26 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
#region Public functions
|
||||
|
||||
public static bool PluginModified(string uuid)
|
||||
public static bool PluginModified(string id)
|
||||
{
|
||||
return _modifiedPlugins.Contains(uuid);
|
||||
return _modifiedPlugins.Contains(id);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
|
||||
/// unless it's a local path installation
|
||||
/// </summary>
|
||||
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||
public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(newVersion, zipFilePath, checkModified:false);
|
||||
UninstallPlugin(existingVersion, removeSettings:false, checkModified:false);
|
||||
await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
|
||||
_modifiedPlugins.Add(existingVersion.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
|
||||
/// </summary>
|
||||
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(plugin, zipFilePath, checkModified: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstall a plugin.
|
||||
/// </summary>
|
||||
public static void UninstallPlugin(PluginMetadata plugin, bool removeSettings = true)
|
||||
public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
|
||||
{
|
||||
UninstallPlugin(plugin, removeSettings, true);
|
||||
await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -421,7 +491,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
// Unzip plugin files to temp folder
|
||||
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
|
||||
|
||||
|
||||
if(!plugin.IsFromLocalInstallPath)
|
||||
File.Delete(zipFilePath);
|
||||
|
||||
|
|
@ -444,20 +514,20 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
|
||||
|
||||
var defaultPluginIDs = new List<string>
|
||||
{
|
||||
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
|
||||
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
|
||||
"572be03c74c642baae319fc283e561a8", // Explorer
|
||||
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
|
||||
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
|
||||
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
|
||||
"791FC278BA414111B8D1886DFE447410", // Program
|
||||
"D409510CD0D2481F853690A07E6DC426", // Shell
|
||||
"CEA08895D2544B019B2E9C5009600DF4", // Sys
|
||||
"0308FD86DE0A4DEE8D62B9B535370992", // URL
|
||||
"565B73353DBF4806919830B9202EE3BF", // WebSearch
|
||||
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
|
||||
};
|
||||
{
|
||||
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
|
||||
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
|
||||
"572be03c74c642baae319fc283e561a8", // Explorer
|
||||
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
|
||||
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
|
||||
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
|
||||
"791FC278BA414111B8D1886DFE447410", // Program
|
||||
"D409510CD0D2481F853690A07E6DC426", // Shell
|
||||
"CEA08895D2544B019B2E9C5009600DF4", // Sys
|
||||
"0308FD86DE0A4DEE8D62B9B535370992", // URL
|
||||
"565B73353DBF4806919830B9202EE3BF", // WebSearch
|
||||
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
|
||||
};
|
||||
|
||||
// Treat default plugin differently, it needs to be removable along with each flow release
|
||||
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
|
||||
|
|
@ -466,9 +536,17 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
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)
|
||||
{
|
||||
|
|
@ -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))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
||||
#pragma warning disable IDE0005
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
|
@ -49,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
{
|
||||
var erroredPlugins = new List<string>();
|
||||
|
||||
|
|
@ -73,9 +74,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
typeof(IAsyncPlugin));
|
||||
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
|
||||
metadata.AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
|
@ -111,7 +114,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (erroredPlugins.Count > 0)
|
||||
{
|
||||
var errorPluginString = String.Join(Environment.NewLine, erroredPlugins);
|
||||
var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
|
||||
|
||||
var errorMessage = "The following "
|
||||
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
|
||||
|
|
@ -119,33 +122,41 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"Please refer to the logs for more information", "",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
});
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
private static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(metadata => new PluginPair
|
||||
.Select(metadata =>
|
||||
{
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata
|
||||
return new PluginPair
|
||||
{
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static IEnumerable<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source)
|
||||
private static IEnumerable<PluginPair> ExecutableV2Plugins(IEnumerable<PluginMetadata> source)
|
||||
{
|
||||
return source
|
||||
.Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(metadata => new PluginPair
|
||||
.Select(metadata =>
|
||||
{
|
||||
Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata
|
||||
return new PluginPair
|
||||
{
|
||||
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
|
||||
Metadata = metadata
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,10 +82,10 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
ClientProcess.Kill(true);
|
||||
await ClientProcess.WaitForExitAsync();
|
||||
ClientProcess.Dispose();
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
|
@ -25,14 +26,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
|
||||
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||
// Prevent Python from writing .py[co] files.
|
||||
// Because .pyc contains location infos which will prevent python portable.
|
||||
_startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
|
||||
|
||||
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
|
||||
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
|
||||
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
|
||||
|
||||
|
||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
||||
_startInfo.ArgumentList.Add("-B");
|
||||
}
|
||||
|
||||
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
|
||||
|
|
@ -50,10 +50,53 @@ namespace Flow.Launcher.Core.Plugin
|
|||
// TODO: Async Action
|
||||
return Execute(_startInfo);
|
||||
}
|
||||
|
||||
public override async Task InitAsync(PluginInitContext context)
|
||||
{
|
||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
_startInfo.ArgumentList.Add("");
|
||||
// Run .py files via `-c <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");
|
||||
|
||||
// This makes it easier for plugin authors to import their own modules.
|
||||
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
|
||||
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
|
||||
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
|
||||
_startInfo.ArgumentList.Add("-c");
|
||||
_startInfo.ArgumentList.Add(
|
||||
$"""
|
||||
import sys
|
||||
sys.path.append(r'{rootDirectory}')
|
||||
sys.path.append(r'{libDirectory}')
|
||||
sys.path.append(r'{libPyWin32LibDirectory}')
|
||||
sys.path.append(r'{libPyWin32Directory}')
|
||||
sys.path.append(r'{pluginDirectory}')
|
||||
|
||||
import runpy
|
||||
runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__')
|
||||
"""
|
||||
);
|
||||
// Plugins always expect the JSON data to be in the third argument
|
||||
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
|
||||
_startInfo.ArgumentList.Add("");
|
||||
}
|
||||
// Run .pyz files as is
|
||||
else
|
||||
{
|
||||
// No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now,
|
||||
// but the plugins still expect data to be sent as the third argument, so we're keeping
|
||||
// the flag here, even though it's not necessary anymore.
|
||||
_startInfo.ArgumentList.Add("-B");
|
||||
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
|
||||
// Plugins always expect the JSON data to be in the third argument
|
||||
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
|
||||
_startInfo.ArgumentList.Add("");
|
||||
}
|
||||
|
||||
await base.InitAsync(context);
|
||||
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,14 +26,45 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
var path = Path.Combine(Constant.ProgramDirectory, JsonRpc);
|
||||
StartInfo.EnvironmentVariables["PYTHONPATH"] = path;
|
||||
|
||||
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
|
||||
StartInfo.ArgumentList.Add("-B");
|
||||
StartInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
public static Language Czech = new Language("cs", "čeština");
|
||||
public static Language Arabic = new Language("ar", "اللغة العربية");
|
||||
public static Language Vietnamese = new Language("vi-vn", "Tiếng Việt");
|
||||
|
||||
public static Language Hebrew = new Language("he", "עברית");
|
||||
|
||||
public static List<Language> GetAvailableLanguages()
|
||||
{
|
||||
|
|
@ -57,9 +57,43 @@ namespace Flow.Launcher.Core.Resource
|
|||
Turkish,
|
||||
Czech,
|
||||
Arabic,
|
||||
Vietnamese
|
||||
Vietnamese,
|
||||
Hebrew
|
||||
};
|
||||
return languages;
|
||||
}
|
||||
|
||||
public static string GetSystemTranslation(string languageCode)
|
||||
{
|
||||
return languageCode switch
|
||||
{
|
||||
"en" => "System",
|
||||
"zh-cn" => "系统",
|
||||
"zh-tw" => "系統",
|
||||
"uk-UA" => "Система",
|
||||
"ru" => "Система",
|
||||
"fr" => "Système",
|
||||
"ja" => "システム",
|
||||
"nl" => "Systeem",
|
||||
"pl" => "System",
|
||||
"da" => "System",
|
||||
"de" => "System",
|
||||
"ko" => "시스템",
|
||||
"sr" => "Систем",
|
||||
"pt-pt" => "Sistema",
|
||||
"pt-br" => "Sistema",
|
||||
"es" => "Sistema",
|
||||
"es-419" => "Sistema",
|
||||
"it" => "Sistema",
|
||||
"nb-NO" => "System",
|
||||
"sk" => "Systém",
|
||||
"tr" => "Sistem",
|
||||
"cs" => "Systém",
|
||||
"ar" => "النظام",
|
||||
"vi-vn" => "Hệ thống",
|
||||
"he" => "מערכת",
|
||||
_ => "System",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,34 +11,61 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
|||
using Flow.Launcher.Plugin;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class Internationalization
|
||||
{
|
||||
public Settings Settings { get; set; }
|
||||
private const string Folder = "Languages";
|
||||
private const string DefaultLanguageCode = "en";
|
||||
private const string DefaultFile = "en.xaml";
|
||||
private const string Extension = ".xaml";
|
||||
private readonly Settings _settings;
|
||||
private readonly List<string> _languageDirectories = new List<string>();
|
||||
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
|
||||
private readonly string SystemLanguageCode;
|
||||
|
||||
public Internationalization()
|
||||
public Internationalization(Settings settings)
|
||||
{
|
||||
AddPluginLanguageDirectories();
|
||||
LoadDefaultLanguage();
|
||||
// we don't want to load /Languages/en.xaml twice
|
||||
// so add flowlauncher language directory after load plugin language files
|
||||
_settings = settings;
|
||||
AddFlowLauncherLanguageDirectory();
|
||||
SystemLanguageCode = GetSystemLanguageCodeAtStartup();
|
||||
}
|
||||
|
||||
|
||||
private void AddFlowLauncherLanguageDirectory()
|
||||
{
|
||||
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
_languageDirectories.Add(directory);
|
||||
}
|
||||
|
||||
private static string GetSystemLanguageCodeAtStartup()
|
||||
{
|
||||
var availableLanguages = AvailableLanguages.GetAvailableLanguages();
|
||||
|
||||
// Retrieve the language identifiers for the current culture.
|
||||
// ChangeLanguage method overrides the CultureInfo.CurrentCulture, so this needs to
|
||||
// be called at startup in order to get the correct lang code of system.
|
||||
var currentCulture = CultureInfo.CurrentCulture;
|
||||
var twoLetterCode = currentCulture.TwoLetterISOLanguageName;
|
||||
var threeLetterCode = currentCulture.ThreeLetterISOLanguageName;
|
||||
var fullName = currentCulture.Name;
|
||||
|
||||
// Try to find a match in the available languages list
|
||||
foreach (var language in availableLanguages)
|
||||
{
|
||||
var languageCode = language.LanguageCode;
|
||||
|
||||
if (string.Equals(languageCode, twoLetterCode, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return languageCode;
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultLanguageCode;
|
||||
}
|
||||
|
||||
private void AddPluginLanguageDirectories()
|
||||
{
|
||||
|
|
@ -56,19 +83,65 @@ namespace Flow.Launcher.Core.Resource
|
|||
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
|
||||
}
|
||||
}
|
||||
|
||||
LoadDefaultLanguage();
|
||||
}
|
||||
|
||||
private void LoadDefaultLanguage()
|
||||
{
|
||||
// Removes language files loaded before any plugins were loaded.
|
||||
// Prevents the language Flow started in from overwriting English if the user switches back to English
|
||||
RemoveOldLanguageFiles();
|
||||
LoadLanguage(AvailableLanguages.English);
|
||||
_oldResources.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize language. Will change app language and plugin language based on settings.
|
||||
/// </summary>
|
||||
public async Task InitializeLanguageAsync()
|
||||
{
|
||||
// Get actual language
|
||||
var languageCode = _settings.Language;
|
||||
if (languageCode == Constant.SystemLanguageCode)
|
||||
{
|
||||
languageCode = SystemLanguageCode;
|
||||
}
|
||||
|
||||
// Get language by language code and change language
|
||||
var language = GetLanguageByLanguageCode(languageCode);
|
||||
|
||||
// Add plugin language directories first so that we can load language files from plugins
|
||||
AddPluginLanguageDirectories();
|
||||
|
||||
// Change language
|
||||
await ChangeLanguageAsync(language);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change language during runtime. Will change app language and plugin language & save settings.
|
||||
/// </summary>
|
||||
/// <param name="languageCode"></param>
|
||||
public void ChangeLanguage(string languageCode)
|
||||
{
|
||||
languageCode = languageCode.NonNull();
|
||||
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)
|
||||
|
|
@ -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();
|
||||
if (language != AvailableLanguages.English)
|
||||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
|
||||
// Culture of main thread
|
||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||
|
||||
// Raise event after culture is set
|
||||
Settings.Language = language.LanguageCode;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
UpdatePluginMetadataTranslations();
|
||||
});
|
||||
// Raise event for plugins after culture is set
|
||||
await Task.Run(UpdatePluginMetadataTranslations);
|
||||
}
|
||||
|
||||
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
||||
{
|
||||
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
|
||||
|
||||
if (Settings.ShouldUsePinyin)
|
||||
if (_settings.ShouldUsePinyin)
|
||||
return false;
|
||||
|
||||
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
|
||||
|
|
@ -123,7 +191,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
// "Do you want to search with pinyin?"
|
||||
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
|
||||
|
||||
if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
if (Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
|
@ -140,11 +208,14 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
private void LoadLanguage(Language language)
|
||||
{
|
||||
var flowEnglishFile = Path.Combine(Constant.ProgramDirectory, Folder, DefaultFile);
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
var filename = $"{language.LanguageCode}{Extension}";
|
||||
var files = _languageDirectories
|
||||
.Select(d => LanguageFile(d, filename))
|
||||
.Where(f => !string.IsNullOrEmpty(f))
|
||||
// Exclude Flow's English language file since it's built into the binary, and there's no need to load
|
||||
// it again from the file system.
|
||||
.Where(f => !string.IsNullOrEmpty(f) && f != flowEnglishFile)
|
||||
.ToArray();
|
||||
|
||||
if (files.Length > 0)
|
||||
|
|
@ -163,7 +234,9 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
private static Internationalization instance;
|
||||
private static object syncObject = new object();
|
||||
|
||||
public static Internationalization Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
lock (syncObject)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = new Internationalization();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
=> Ioc.Default.GetRequiredService<Internationalization>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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.Sockets;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using JetBrains.Annotations;
|
||||
using Squirrel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
|
|
@ -15,30 +17,33 @@ using Flow.Launcher.Infrastructure.Http;
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Squirrel;
|
||||
|
||||
namespace Flow.Launcher.Core
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1);
|
||||
|
||||
public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
|
||||
public async Task UpdateAppAsync(bool silentUpdate = true)
|
||||
{
|
||||
await UpdateLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!silentUpdate)
|
||||
api.ShowMsg(api.GetTranslation("pleaseWait"),
|
||||
api.GetTranslation("update_flowlauncher_update_check"));
|
||||
_api.ShowMsg(_api.GetTranslation("pleaseWait"),
|
||||
_api.GetTranslation("update_flowlauncher_update_check"));
|
||||
|
||||
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -48,18 +53,18 @@ namespace Flow.Launcher.Core
|
|||
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
|
||||
var currentVersion = Version.Parse(Constant.Version);
|
||||
|
||||
Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>");
|
||||
Log.Info($"|Updater.UpdateApp|Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
|
||||
|
||||
if (newReleaseVersion <= currentVersion)
|
||||
{
|
||||
if (!silentUpdate)
|
||||
MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest"));
|
||||
_api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silentUpdate)
|
||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"),
|
||||
api.GetTranslation("update_flowlauncher_updating"));
|
||||
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
|
||||
_api.GetTranslation("update_flowlauncher_updating"));
|
||||
|
||||
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -67,10 +72,10 @@ namespace Flow.Launcher.Core
|
|||
|
||||
if (DataLocation.PortableDataLocationInUse())
|
||||
{
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
|
||||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination);
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination))
|
||||
MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
|
||||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||
DataLocation.PortableDataPath,
|
||||
targetDestination));
|
||||
}
|
||||
|
|
@ -83,7 +88,7 @@ namespace Flow.Launcher.Core
|
|||
|
||||
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
|
||||
|
||||
if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
|
|
@ -96,8 +101,8 @@ namespace Flow.Launcher.Core
|
|||
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
|
||||
|
||||
if (!silentUpdate)
|
||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
|
||||
api.GetTranslation("update_flowlauncher_check_connection"));
|
||||
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
|
||||
_api.GetTranslation("update_flowlauncher_check_connection"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -119,7 +124,7 @@ namespace Flow.Launcher.Core
|
|||
}
|
||||
|
||||
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||
private static async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||
{
|
||||
var uri = new Uri(repository);
|
||||
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
|
||||
|
|
@ -141,12 +146,22 @@ namespace Flow.Launcher.Core
|
|||
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);
|
||||
|
||||
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 MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
||||
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
||||
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
|
||||
public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png");
|
||||
|
||||
public static string PythonPath;
|
||||
public static string NodePath;
|
||||
|
|
@ -46,10 +48,13 @@ namespace Flow.Launcher.Infrastructure
|
|||
public const string Themes = "Themes";
|
||||
public const string Settings = "Settings";
|
||||
public const string Logs = "Logs";
|
||||
public const string Cache = "Cache";
|
||||
|
||||
public const string Website = "https://flowlauncher.com";
|
||||
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
|
||||
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
|
||||
public const string Docs = "https://flowlauncher.com/docs";
|
||||
|
||||
public const string SystemLanguageCode = "system";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
|
|
@ -15,7 +15,20 @@ namespace Flow.Launcher.Infrastructure
|
|||
{
|
||||
var explorerWindow = GetActiveExplorer();
|
||||
string locationUrl = explorerWindow?.LocationURL;
|
||||
return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null;
|
||||
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get directory path from a file path
|
||||
/// </summary>
|
||||
private static string GetDirectoryPath(string path)
|
||||
{
|
||||
if (!path.EndsWith("\\"))
|
||||
{
|
||||
return path + "\\";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -54,12 +67,6 @@ namespace Flow.Launcher.Infrastructure
|
|||
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
|
||||
/// </summary>
|
||||
|
|
@ -70,9 +77,9 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
var index = 0;
|
||||
var numRemaining = hWnds.Count;
|
||||
EnumWindows((wnd, _) =>
|
||||
PInvoke.EnumWindows((wnd, _) =>
|
||||
{
|
||||
var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32());
|
||||
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
|
||||
if (searchIndex != -1)
|
||||
{
|
||||
z[searchIndex] = index;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@
|
|||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="NativeMethods.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SolutionAssemblyInfo.cs" Link="Properties\SolutionAssemblyInfo.cs" />
|
||||
<None Include="FodyWeavers.xml" />
|
||||
|
|
@ -49,13 +53,18 @@
|
|||
|
||||
<ItemGroup>
|
||||
<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">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MemoryPack" Version="1.21.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.7.30" />
|
||||
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||
<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="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
public static class Helper
|
||||
{
|
||||
static Helper()
|
||||
{
|
||||
jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
|
||||
/// </summary>
|
||||
|
|
@ -36,55 +28,5 @@ namespace Flow.Launcher.Infrastructure
|
|||
throw new NullReferenceException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
|
||||
{
|
||||
if (!Directory.Exists(dataDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
}
|
||||
|
||||
foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
|
||||
{
|
||||
var data = Path.GetFileName(bundledDataPath);
|
||||
var dataPath = Path.Combine(dataDirectory, data.NonNull());
|
||||
if (!File.Exists(dataPath))
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
|
||||
var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
|
||||
if (time1 != time2)
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
public static string Formatted<T>(this T t)
|
||||
{
|
||||
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||
{
|
||||
|
|
@ -10,44 +15,45 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
/// </summary>
|
||||
public unsafe class GlobalHotkey : IDisposable
|
||||
{
|
||||
private static readonly IntPtr hookId;
|
||||
|
||||
|
||||
|
||||
private static readonly HOOKPROC _procKeyboard = HookKeyboardCallback;
|
||||
private static readonly UnhookWindowsHookExSafeHandle hookId;
|
||||
|
||||
public delegate bool KeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state);
|
||||
internal static Func<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()
|
||||
{
|
||||
// Set the hook
|
||||
hookId = InterceptKeys.SetHook(& LowLevelKeyboardProc);
|
||||
hookId = SetHook(_procKeyboard, WINDOWS_HOOK_ID.WH_KEYBOARD_LL);
|
||||
}
|
||||
|
||||
private static UnhookWindowsHookExSafeHandle SetHook(HOOKPROC proc, WINDOWS_HOOK_ID hookId)
|
||||
{
|
||||
using var curProcess = Process.GetCurrentProcess();
|
||||
using var curModule = curProcess.MainModule;
|
||||
return PInvoke.SetWindowsHookEx(hookId, proc, PInvoke.GetModuleHandle(curModule.ModuleName), 0);
|
||||
}
|
||||
|
||||
public static SpecialKeyState CheckModifiers()
|
||||
{
|
||||
SpecialKeyState state = new SpecialKeyState();
|
||||
if ((InterceptKeys.GetKeyState(VK_SHIFT) & 0x8000) != 0)
|
||||
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) & 0x8000) != 0)
|
||||
{
|
||||
//SHIFT is pressed
|
||||
state.ShiftPressed = true;
|
||||
}
|
||||
if ((InterceptKeys.GetKeyState(VK_CONTROL) & 0x8000) != 0)
|
||||
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) & 0x8000) != 0)
|
||||
{
|
||||
//CONTROL is pressed
|
||||
state.CtrlPressed = true;
|
||||
}
|
||||
if ((InterceptKeys.GetKeyState(VK_ALT) & 0x8000) != 0)
|
||||
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) & 0x8000) != 0)
|
||||
{
|
||||
//ALT is pressed
|
||||
state.AltPressed = true;
|
||||
}
|
||||
if ((InterceptKeys.GetKeyState(VK_WIN) & 0x8000) != 0)
|
||||
if ((PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_LWIN) & 0x8000) != 0 ||
|
||||
(PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_RWIN) & 0x8000) != 0)
|
||||
{
|
||||
//WIN is pressed
|
||||
state.WinPressed = true;
|
||||
|
|
@ -56,33 +62,33 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
return state;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam)
|
||||
private static LRESULT HookKeyboardCallback(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
bool continues = true;
|
||||
|
||||
if (nCode >= 0)
|
||||
{
|
||||
if (wParam.ToUInt32() == (int)KeyEvent.WM_KEYDOWN ||
|
||||
wParam.ToUInt32() == (int)KeyEvent.WM_KEYUP ||
|
||||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYDOWN ||
|
||||
wParam.ToUInt32() == (int)KeyEvent.WM_SYSKEYUP)
|
||||
if (wParam.Value == (int)KeyEvent.WM_KEYDOWN ||
|
||||
wParam.Value == (int)KeyEvent.WM_KEYUP ||
|
||||
wParam.Value == (int)KeyEvent.WM_SYSKEYDOWN ||
|
||||
wParam.Value == (int)KeyEvent.WM_SYSKEYUP)
|
||||
{
|
||||
if (hookedKeyboardCallback != null)
|
||||
continues = hookedKeyboardCallback((KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), CheckModifiers());
|
||||
continues = hookedKeyboardCallback((KeyEvent)wParam.Value, Marshal.ReadInt32(lParam), CheckModifiers());
|
||||
}
|
||||
}
|
||||
|
||||
if (continues)
|
||||
{
|
||||
return InterceptKeys.CallNextHookEx(hookId, nCode, wParam, lParam);
|
||||
return PInvoke.CallNextHookEx(hookId, nCode, wParam, lParam);
|
||||
}
|
||||
return (IntPtr)(-1);
|
||||
|
||||
return new LRESULT(1);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
InterceptKeys.UnhookWindowsHookEx(hookId);
|
||||
hookId.Dispose();
|
||||
}
|
||||
|
||||
~GlobalHotkey()
|
||||
|
|
@ -90,4 +96,4 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
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
|
||||
{
|
||||
public enum KeyEvent
|
||||
|
|
@ -5,21 +7,21 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
/// <summary>
|
||||
/// Key down
|
||||
/// </summary>
|
||||
WM_KEYDOWN = 256,
|
||||
WM_KEYDOWN = (int)PInvoke.WM_KEYDOWN,
|
||||
|
||||
/// <summary>
|
||||
/// Key up
|
||||
/// </summary>
|
||||
WM_KEYUP = 257,
|
||||
WM_KEYUP = (int)PInvoke.WM_KEYUP,
|
||||
|
||||
/// <summary>
|
||||
/// System key up
|
||||
/// </summary>
|
||||
WM_SYSKEYUP = 261,
|
||||
WM_SYSKEYUP = (int)PInvoke.WM_SYSKEYUP,
|
||||
|
||||
/// <summary>
|
||||
/// System key down
|
||||
/// </summary>
|
||||
WM_SYSKEYDOWN = 260
|
||||
WM_SYSKEYDOWN = (int)PInvoke.WM_SYSKEYDOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
|||
using System;
|
||||
using System.Threading;
|
||||
using Flow.Launcher.Plugin;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Http
|
||||
{
|
||||
|
|
@ -17,8 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
|
||||
private static HttpClient client = new HttpClient();
|
||||
|
||||
public static IPublicAPI API { get; set; }
|
||||
|
||||
static Http()
|
||||
{
|
||||
// need to be added so it would work on a win10 machine
|
||||
|
|
@ -78,20 +77,55 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||
await response.Content.CopyToAsync(fileStream, token);
|
||||
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
||||
var canReportProgress = totalBytes != -1;
|
||||
|
||||
if (canReportProgress && reportProgress != null)
|
||||
{
|
||||
await using var contentStream = await response.Content.ReadAsStreamAsync(token);
|
||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true);
|
||||
|
||||
var buffer = new byte[8192];
|
||||
long totalRead = 0;
|
||||
int read;
|
||||
double progressValue = 0;
|
||||
|
||||
reportProgress(0);
|
||||
|
||||
while ((read = await contentStream.ReadAsync(buffer, token)) > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer.AsMemory(0, read), token);
|
||||
totalRead += read;
|
||||
|
||||
progressValue = totalRead * 100.0 / totalBytes;
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
else
|
||||
reportProgress(progressValue);
|
||||
}
|
||||
|
||||
if (progressValue < 100)
|
||||
reportProgress(100);
|
||||
}
|
||||
else
|
||||
{
|
||||
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
|
||||
await response.Content.CopyToAsync(fileStream, token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using static Flow.Launcher.Infrastructure.Http.Http;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
|
|
@ -22,12 +20,12 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
|
||||
private static IImageHashGenerator _hashGenerator;
|
||||
private static readonly bool EnableImageHash = true;
|
||||
public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon));
|
||||
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
||||
public const int SmallIconSize = 64;
|
||||
public const int FullIconSize = 256;
|
||||
|
||||
|
||||
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
||||
|
||||
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();
|
||||
|
||||
|
|
@ -70,12 +68,22 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
.Select(x => x.Key)
|
||||
.ToList());
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception($"|ImageLoader.SaveAsync|Failed to save image cache to file", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WaitSaveAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
storageLock.Release();
|
||||
}
|
||||
|
||||
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
|
|
@ -139,7 +147,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return new ImageResult(image, ImageType.ImageFile);
|
||||
}
|
||||
|
||||
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
if (path.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var imageSource = new BitmapImage(new Uri(path));
|
||||
imageSource.Freeze();
|
||||
|
|
@ -172,7 +180,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
||||
{
|
||||
// Download image from url
|
||||
await using var resp = await GetStreamAsync(uriResult);
|
||||
await using var resp = await Http.Http.GetStreamAsync(uriResult);
|
||||
await using var buffer = new MemoryStream();
|
||||
await resp.CopyToAsync(buffer);
|
||||
buffer.Seek(0, SeekOrigin.Begin);
|
||||
|
|
@ -215,8 +223,16 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
type = ImageType.ImageFile;
|
||||
if (loadFullImage)
|
||||
{
|
||||
image = LoadFullImage(path);
|
||||
type = ImageType.FullImageFile;
|
||||
try
|
||||
{
|
||||
image = LoadFullImage(path);
|
||||
type = ImageType.FullImageFile;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
image = Image;
|
||||
type = ImageType.Error;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.Graphics.Gdi;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
/// <summary>
|
||||
/// Subclass of <see cref="Windows.Win32.UI.Shell.SIIGBF"/>
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ThumbnailOptions
|
||||
{
|
||||
|
|
@ -22,91 +29,13 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
{
|
||||
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
|
||||
|
||||
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern int SHCreateItemFromParsingName(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string path,
|
||||
IntPtr pbc,
|
||||
ref Guid riid,
|
||||
[MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool DeleteObject(IntPtr hObject);
|
||||
|
||||
[ComImport]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
|
||||
internal interface IShellItem
|
||||
{
|
||||
void BindToHandler(IntPtr pbc,
|
||||
[MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
|
||||
[MarshalAs(UnmanagedType.LPStruct)]Guid riid,
|
||||
out IntPtr ppv);
|
||||
|
||||
void GetParent(out IShellItem ppsi);
|
||||
void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
|
||||
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
||||
void Compare(IShellItem psi, uint hint, out int piOrder);
|
||||
};
|
||||
|
||||
internal enum SIGDN : uint
|
||||
{
|
||||
NORMALDISPLAY = 0,
|
||||
PARENTRELATIVEPARSING = 0x80018001,
|
||||
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
|
||||
DESKTOPABSOLUTEPARSING = 0x80028000,
|
||||
PARENTRELATIVEEDITING = 0x80031001,
|
||||
DESKTOPABSOLUTEEDITING = 0x8004c000,
|
||||
FILESYSPATH = 0x80058000,
|
||||
URL = 0x80068000
|
||||
}
|
||||
|
||||
internal enum HResult
|
||||
{
|
||||
Ok = 0x0000,
|
||||
False = 0x0001,
|
||||
InvalidArguments = unchecked((int)0x80070057),
|
||||
OutOfMemory = unchecked((int)0x8007000E),
|
||||
NoInterface = unchecked((int)0x80004002),
|
||||
Fail = unchecked((int)0x80004005),
|
||||
ExtractionFailed = unchecked((int)0x8004B200),
|
||||
ElementNotFound = unchecked((int)0x80070490),
|
||||
TypeElementNotFound = unchecked((int)0x8002802B),
|
||||
NoObject = unchecked((int)0x800401E5),
|
||||
Win32ErrorCanceled = 1223,
|
||||
Canceled = unchecked((int)0x800704C7),
|
||||
ResourceInUse = unchecked((int)0x800700AA),
|
||||
AccessDenied = unchecked((int)0x80030005)
|
||||
}
|
||||
|
||||
[ComImportAttribute()]
|
||||
[GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
|
||||
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IShellItemImageFactory
|
||||
{
|
||||
[PreserveSig]
|
||||
HResult GetImage(
|
||||
[In, MarshalAs(UnmanagedType.Struct)] NativeSize size,
|
||||
[In] ThumbnailOptions flags,
|
||||
[Out] out IntPtr phbm);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct NativeSize
|
||||
{
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
public int Width { set { width = value; } }
|
||||
public int Height { set { height = value; } }
|
||||
};
|
||||
private static readonly Guid GUID_IShellItem = typeof(IShellItem).GUID;
|
||||
|
||||
private static readonly HRESULT S_ExtractionFailed = (HRESULT)0x8004B200;
|
||||
|
||||
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -115,39 +44,61 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
finally
|
||||
{
|
||||
// delete HBitmap to avoid memory leaks
|
||||
DeleteObject(hBitmap);
|
||||
PInvoke.DeleteObject(hBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
IShellItem nativeShellItem;
|
||||
Guid shellItem2Guid = new Guid(IShellItem2Guid);
|
||||
int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
|
||||
|
||||
if (retCode != 0)
|
||||
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
var retCode = PInvoke.SHCreateItemFromParsingName(
|
||||
fileName,
|
||||
null,
|
||||
GUID_IShellItem,
|
||||
out var nativeShellItem);
|
||||
|
||||
if (retCode != HRESULT.S_OK)
|
||||
throw Marshal.GetExceptionForHR(retCode);
|
||||
|
||||
NativeSize nativeSize = new NativeSize
|
||||
if (nativeShellItem is not IShellItemImageFactory imageFactory)
|
||||
{
|
||||
Width = width,
|
||||
Height = height
|
||||
};
|
||||
|
||||
IntPtr hBitmap;
|
||||
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap);
|
||||
|
||||
// if extracting image thumbnail and failed, extract shell icon
|
||||
if (options == ThumbnailOptions.ThumbnailOnly && hr == HResult.ExtractionFailed)
|
||||
{
|
||||
hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, out hBitmap);
|
||||
Marshal.ReleaseComObject(nativeShellItem);
|
||||
nativeShellItem = null;
|
||||
throw new InvalidOperationException("Failed to get IShellItemImageFactory");
|
||||
}
|
||||
|
||||
Marshal.ReleaseComObject(nativeShellItem);
|
||||
SIZE size = new SIZE
|
||||
{
|
||||
cx = width,
|
||||
cy = height
|
||||
};
|
||||
|
||||
if (hr == HResult.Ok) return hBitmap;
|
||||
HBITMAP hBitmap = default;
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
imageFactory.GetImage(size, (SIIGBF)options, &hBitmap);
|
||||
}
|
||||
catch (COMException ex) when (ex.HResult == S_ExtractionFailed && options == ThumbnailOptions.ThumbnailOnly)
|
||||
{
|
||||
// Fallback to IconOnly if ThumbnailOnly fails
|
||||
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
|
||||
}
|
||||
catch (FileNotFoundException) when (options == ThumbnailOptions.ThumbnailOnly)
|
||||
{
|
||||
// Fallback to IconOnly if files cannot be found
|
||||
imageFactory.GetImage(size, (SIIGBF)ThumbnailOptions.IconOnly, &hBitmap);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (nativeShellItem != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(nativeShellItem);
|
||||
}
|
||||
}
|
||||
|
||||
throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr));
|
||||
return hBitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
{
|
||||
public static class Log
|
||||
{
|
||||
public const string DirectoryName = "Logs";
|
||||
public const string DirectoryName = Constant.Logs;
|
||||
|
||||
public static string CurrentLogDirectory { get; }
|
||||
|
||||
static Log()
|
||||
{
|
||||
CurrentLogDirectory = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Version);
|
||||
CurrentLogDirectory = DataLocation.VersionLogDirectory;
|
||||
if (!Directory.Exists(CurrentLogDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(CurrentLogDirectory);
|
||||
|
|
@ -48,17 +48,45 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
configuration.AddTarget("file", fileTargetASyncWrapper);
|
||||
configuration.AddTarget("debug", debugTarget);
|
||||
|
||||
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper)
|
||||
{
|
||||
RuleName = "file"
|
||||
};
|
||||
#if DEBUG
|
||||
var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper);
|
||||
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget);
|
||||
var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget)
|
||||
{
|
||||
RuleName = "debug"
|
||||
};
|
||||
configuration.LoggingRules.Add(debugRule);
|
||||
#else
|
||||
var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
|
||||
#endif
|
||||
configuration.LoggingRules.Add(fileRule);
|
||||
LogManager.Configuration = configuration;
|
||||
}
|
||||
|
||||
public static void SetLogLevel(LOGLEVEL level)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case LOGLEVEL.DEBUG:
|
||||
UseDebugLogLevel();
|
||||
break;
|
||||
default:
|
||||
UseInfoLogLevel();
|
||||
break;
|
||||
}
|
||||
Info(nameof(Logger), $"Using log level: {level}.");
|
||||
}
|
||||
|
||||
private static void UseDebugLogLevel()
|
||||
{
|
||||
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
|
||||
}
|
||||
|
||||
private static void UseInfoLogLevel()
|
||||
{
|
||||
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
|
||||
}
|
||||
|
||||
private static void LogFaultyFormat(string message)
|
||||
{
|
||||
var logger = LogManager.GetLogger("FaultyLogger");
|
||||
|
|
@ -206,4 +234,10 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
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 System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
|
|
@ -103,13 +104,21 @@ namespace Flow.Launcher.Infrastructure
|
|||
{"Ou", "ou"}
|
||||
});
|
||||
|
||||
|
||||
private static readonly ReadOnlyDictionary<string, string> first = new(new Dictionary<string, string>(){
|
||||
{"Ch", "i"},
|
||||
{"Sh", "u"},
|
||||
{"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>()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using MemoryPack;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
|
|
@ -16,19 +12,17 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
/// Normally, it has better performance, but not readable
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable
|
||||
/// https://github.com/Cysharp/MemoryPack
|
||||
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable <see href="https://github.com/Cysharp/MemoryPack"/>
|
||||
/// </remarks>
|
||||
public class BinaryStorage<T>
|
||||
{
|
||||
const string DirectoryName = "Cache";
|
||||
public const string FileSuffix = ".cache";
|
||||
|
||||
const string FileSuffix = ".cache";
|
||||
|
||||
public BinaryStorage(string filename)
|
||||
// Let the derived class to set the file path
|
||||
public BinaryStorage(string filename, string directoryPath = null)
|
||||
{
|
||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||
Helper.ValidateDirectory(directoryPath);
|
||||
directoryPath ??= DataLocation.CacheDirectory;
|
||||
FilesFolders.ValidateDirectory(directoryPath);
|
||||
|
||||
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
|
||||
{
|
||||
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
|
||||
return t;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
catch (System.Exception)
|
||||
{
|
||||
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
|
||||
return defaultData;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,51 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
public class FlowLauncherJsonStorage<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()
|
||||
{
|
||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||
Helper.ValidateDirectory(directoryPath);
|
||||
FilesFolders.ValidateDirectory(directoryPath);
|
||||
|
||||
var filename = typeof(T).Name;
|
||||
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.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
|
|
@ -16,7 +17,7 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
protected T? Data;
|
||||
|
||||
// need a new directory name
|
||||
public const string DirectoryName = "Settings";
|
||||
public const string DirectoryName = Constant.Settings;
|
||||
public const string FileSuffix = ".json";
|
||||
|
||||
protected string FilePath { get; init; } = null!;
|
||||
|
|
@ -31,12 +32,13 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
protected JsonStorage()
|
||||
{
|
||||
}
|
||||
|
||||
public JsonStorage(string filePath)
|
||||
{
|
||||
FilePath = filePath;
|
||||
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
|
||||
|
||||
Helper.ValidateDirectory(DirectoryPath);
|
||||
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
}
|
||||
|
||||
public async Task<T> LoadAsync()
|
||||
|
|
@ -97,6 +99,7 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreBackup()
|
||||
{
|
||||
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
|
||||
|
|
@ -179,25 +182,21 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
public void Save()
|
||||
{
|
||||
string serialized = JsonSerializer.Serialize(Data,
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
File.WriteAllText(TempFilePath, serialized);
|
||||
|
||||
AtomicWriteSetting();
|
||||
}
|
||||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
var tempOutput = File.OpenWrite(TempFilePath);
|
||||
await using var tempOutput = File.OpenWrite(TempFilePath);
|
||||
await JsonSerializer.SerializeAsync(tempOutput, Data,
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
new JsonSerializerOptions { WriteIndented = true });
|
||||
AtomicWriteSetting();
|
||||
}
|
||||
|
||||
private void AtomicWriteSetting()
|
||||
{
|
||||
if (!File.Exists(FilePath))
|
||||
|
|
@ -206,9 +205,9 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
}
|
||||
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.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
public class PluginJsonStorage<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()
|
||||
{
|
||||
// C# related, add python related below
|
||||
var dataType = typeof(T);
|
||||
var assemblyName = dataType.Assembly.GetName().Name;
|
||||
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName);
|
||||
Helper.ValidateDirectory(DirectoryPath);
|
||||
AssemblyName = dataType.Assembly.GetName().Name;
|
||||
DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName);
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
||||
}
|
||||
|
|
@ -20,6 +33,29 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
{
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
public class StringMatcher
|
||||
{
|
||||
private readonly MatchOption _defaultMatchOption = new MatchOption();
|
||||
private readonly MatchOption _defaultMatchOption = new();
|
||||
|
||||
public SearchPrecisionScore UserSettingSearchPrecision { get; set; }
|
||||
|
||||
private readonly IAlphabet _alphabet;
|
||||
|
||||
public StringMatcher(IAlphabet alphabet = null)
|
||||
public StringMatcher(IAlphabet alphabet, Settings settings)
|
||||
{
|
||||
_alphabet = alphabet;
|
||||
UserSettingSearchPrecision = settings.QuerySearchPrecision;
|
||||
}
|
||||
|
||||
// This is a workaround to allow unit tests to set the instance
|
||||
public StringMatcher(IAlphabet alphabet)
|
||||
{
|
||||
_alphabet = alphabet;
|
||||
}
|
||||
|
||||
public static StringMatcher Instance { get; internal set; }
|
||||
|
||||
public static MatchResult FuzzySearch(string query, string stringToCompare)
|
||||
{
|
||||
return Instance.FuzzyMatch(query, stringToCompare);
|
||||
return Ioc.Default.GetRequiredService<StringMatcher>().FuzzyMatch(query, stringToCompare);
|
||||
}
|
||||
|
||||
public MatchResult FuzzyMatch(string query, string stringToCompare)
|
||||
|
|
@ -241,16 +248,16 @@ namespace Flow.Launcher.Infrastructure
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool IsAcronymChar(string stringToCompare, int compareStringIndex)
|
||||
private static bool IsAcronymChar(string stringToCompare, int compareStringIndex)
|
||||
=> char.IsUpper(stringToCompare[compareStringIndex]) ||
|
||||
compareStringIndex == 0 || // 0 index means char is the start of the compare string, which is an acronym
|
||||
char.IsWhiteSpace(stringToCompare[compareStringIndex - 1]);
|
||||
|
||||
private bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
|
||||
private static bool IsAcronymNumber(string stringToCompare, int compareStringIndex)
|
||||
=> stringToCompare[compareStringIndex] >= 0 && stringToCompare[compareStringIndex] <= 9;
|
||||
|
||||
// To get the index of the closest space which preceeds the first matching index
|
||||
private int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
||||
private static int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstMatchIndex)
|
||||
{
|
||||
var closestSpaceIndex = -1;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
return false;
|
||||
}
|
||||
|
||||
public static string VersionLogDirectory => Path.Combine(LogDirectory, Constant.Version);
|
||||
public static string LogDirectory => Path.Combine(DataDirectory(), Constant.Logs);
|
||||
|
||||
public static readonly string CacheDirectory = Path.Combine(DataDirectory(), Constant.Cache);
|
||||
public static readonly string SettingsDirectory = Path.Combine(DataDirectory(), Constant.Settings);
|
||||
public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins);
|
||||
public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins);
|
||||
public static readonly string ThemesDirectory = Path.Combine(DataDirectory(), Constant.Themes);
|
||||
|
||||
public static readonly string PluginSettingsDirectory = Path.Combine(SettingsDirectory, Constant.Plugins);
|
||||
public static readonly string PluginCacheDirectory = Path.Combine(DataDirectory(), Constant.Cache, Constant.Plugins);
|
||||
|
||||
public const string PythonEnvironmentName = "Python";
|
||||
public const string NodeEnvironmentName = "Node.js";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
|
|
@ -6,8 +7,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public class PluginsSettings : BaseModel
|
||||
{
|
||||
private string pythonExecutablePath = string.Empty;
|
||||
public string PythonExecutablePath {
|
||||
get { return pythonExecutablePath; }
|
||||
public string PythonExecutablePath
|
||||
{
|
||||
get => pythonExecutablePath;
|
||||
set
|
||||
{
|
||||
pythonExecutablePath = value;
|
||||
|
|
@ -18,7 +20,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
private string nodeExecutablePath = string.Empty;
|
||||
public string NodeExecutablePath
|
||||
{
|
||||
get { return nodeExecutablePath; }
|
||||
get => nodeExecutablePath;
|
||||
set
|
||||
{
|
||||
nodeExecutablePath = value;
|
||||
|
|
@ -26,19 +28,32 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, Plugin> Plugins { get; set; } = new Dictionary<string, Plugin>();
|
||||
/// <summary>
|
||||
/// Only used for serialization
|
||||
/// </summary>
|
||||
public Dictionary<string, Plugin> Plugins { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Update plugin settings with metadata.
|
||||
/// FL will get default values from metadata first and then load settings to metadata
|
||||
/// </summary>
|
||||
/// <param name="metadatas">Parsed plugin metadatas</param>
|
||||
public void UpdatePluginSettings(List<PluginMetadata> metadatas)
|
||||
{
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
if (Plugins.ContainsKey(metadata.ID))
|
||||
if (Plugins.TryGetValue(metadata.ID, out var settings))
|
||||
{
|
||||
var settings = Plugins[metadata.ID];
|
||||
|
||||
// If settings exist, update settings & metadata value
|
||||
// update settings values with metadata
|
||||
if (string.IsNullOrEmpty(settings.Version))
|
||||
{
|
||||
settings.Version = metadata.Version;
|
||||
}
|
||||
settings.DefaultActionKeywords = metadata.ActionKeywords; // metadata provides default values
|
||||
settings.DefaultSearchDelayTime = metadata.SearchDelayTime; // metadata provides default values
|
||||
|
||||
// update metadata values with settings
|
||||
if (settings.ActionKeywords?.Count > 0)
|
||||
{
|
||||
metadata.ActionKeywords = settings.ActionKeywords;
|
||||
|
|
@ -51,30 +66,65 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
metadata.Disabled = settings.Disabled;
|
||||
metadata.Priority = settings.Priority;
|
||||
metadata.SearchDelayTime = settings.SearchDelayTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If settings does not exist, create a new one
|
||||
Plugins[metadata.ID] = new Plugin
|
||||
{
|
||||
ID = metadata.ID,
|
||||
Name = metadata.Name,
|
||||
Version = metadata.Version,
|
||||
ActionKeywords = metadata.ActionKeywords,
|
||||
DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values
|
||||
ActionKeywords = metadata.ActionKeywords, // use default value
|
||||
Disabled = metadata.Disabled,
|
||||
Priority = metadata.Priority
|
||||
Priority = metadata.Priority,
|
||||
DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values
|
||||
SearchDelayTime = metadata.SearchDelayTime, // use default value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Plugin GetPluginSettings(string id)
|
||||
{
|
||||
if (Plugins.TryGetValue(id, out var plugin))
|
||||
{
|
||||
return plugin;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Plugin RemovePluginSettings(string id)
|
||||
{
|
||||
Plugins.Remove(id, out var plugin);
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
|
||||
public class Plugin
|
||||
{
|
||||
public string ID { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Version { get; set; }
|
||||
public List<string> ActionKeywords { get; set; } // a reference of the action keywords from plugin manager
|
||||
|
||||
[JsonIgnore]
|
||||
public List<string> DefaultActionKeywords { get; set; }
|
||||
|
||||
// a reference of the action keywords from plugin manager
|
||||
public List<string> ActionKeywords { get; set; }
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public SearchDelayTime? DefaultSearchDelayTime { get; set; }
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchDelayTime? SearchDelayTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used only to save the state of the plugin in settings
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Drawing;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
|
@ -13,7 +15,25 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
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;
|
||||
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
|
||||
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
|
||||
|
|
@ -48,21 +68,23 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
get => _theme;
|
||||
set
|
||||
{
|
||||
if (value == _theme)
|
||||
return;
|
||||
_theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(MaxResultsToShow));
|
||||
if (value != _theme)
|
||||
{
|
||||
_theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(MaxResultsToShow));
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool UseDropShadowEffect { get; set; } = false;
|
||||
public bool UseDropShadowEffect { get; set; } = true;
|
||||
public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None;
|
||||
|
||||
/* Appearance Settings. It should be separated from the setting later.*/
|
||||
public double WindowHeightSize { get; set; } = 42;
|
||||
public double ItemHeightSize { get; set; } = 58;
|
||||
public double QueryBoxFontSize { get; set; } = 20;
|
||||
public double ResultItemFontSize { get; set; } = 16;
|
||||
public double ResultSubItemFontSize { get; set; } = 13;
|
||||
public double ResultSubItemFontSize { get; set; } = 13;
|
||||
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name;
|
||||
public string QueryBoxFontStyle { get; set; }
|
||||
public string QueryBoxFontWeight { get; set; }
|
||||
|
|
@ -90,7 +112,34 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public double SettingWindowHeight { get; set; } = 700;
|
||||
public double? SettingWindowTop { get; set; } = null;
|
||||
public double? SettingWindowLeft { get; set; } = null;
|
||||
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
|
||||
public WindowState SettingWindowState { get; set; } = WindowState.Normal;
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -180,6 +229,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
};
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO;
|
||||
|
||||
/// <summary>
|
||||
/// 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 AlwaysPreview { get; set; } = false;
|
||||
|
||||
public bool AlwaysStartEn { get; set; } = false;
|
||||
|
||||
private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular;
|
||||
|
|
@ -199,8 +251,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
set
|
||||
{
|
||||
_querySearchPrecision = value;
|
||||
if (StringMatcher.Instance != null)
|
||||
StringMatcher.Instance.UserSettingSearchPrecision = value;
|
||||
if (_stringMatcher != null)
|
||||
_stringMatcher.UserSettingSearchPrecision = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,10 +271,26 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
/// </summary>
|
||||
public double CustomWindowTop { get; set; } = 0;
|
||||
|
||||
public bool KeepMaxResults { get; set; } = false;
|
||||
public int MaxResultsToShow { get; set; } = 5;
|
||||
public int ActivateTimes { get; set; }
|
||||
/// <summary>
|
||||
/// Fixed window size
|
||||
/// </summary>
|
||||
private bool _keepMaxResults { get; set; } = false;
|
||||
public bool KeepMaxResults
|
||||
{
|
||||
get => _keepMaxResults;
|
||||
set
|
||||
{
|
||||
if (_keepMaxResults != value)
|
||||
{
|
||||
_keepMaxResults = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int MaxResultsToShow { get; set; } = 5;
|
||||
|
||||
public int ActivateTimes { get; set; }
|
||||
|
||||
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
|
||||
|
||||
|
|
@ -239,11 +307,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public bool EnableUpdateLog { get; set; }
|
||||
|
||||
public bool StartFlowLauncherOnSystemStartup { get; set; } = false;
|
||||
public bool UseLogonTaskForStartup { get; set; } = false;
|
||||
public bool HideOnStartup { get; set; } = true;
|
||||
bool _hideNotifyIcon { get; set; }
|
||||
public bool HideNotifyIcon
|
||||
{
|
||||
get { return _hideNotifyIcon; }
|
||||
get => _hideNotifyIcon;
|
||||
set
|
||||
{
|
||||
_hideNotifyIcon = value;
|
||||
|
|
@ -253,6 +322,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public bool LeaveCmdOpen { get; set; }
|
||||
public bool HideWhenDeactivated { get; set; } = true;
|
||||
|
||||
public bool SearchQueryResultsWithDelay { get; set; }
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchDelayTime SearchDelayTime { get; set; } = SearchDelayTime.Normal;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.Cursor;
|
||||
|
||||
|
|
@ -275,7 +349,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
[JsonIgnore]
|
||||
public bool WMPInstalled { get; set; } = true;
|
||||
|
||||
|
||||
// This needs to be loaded last by staying at the bottom
|
||||
public PluginsSettings PluginSettings { get; set; } = new PluginsSettings();
|
||||
|
||||
|
|
@ -371,7 +444,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
Selected,
|
||||
Empty,
|
||||
Preserved
|
||||
Preserved,
|
||||
ActionKeywordPreserved,
|
||||
ActionKeywordSelected
|
||||
}
|
||||
|
||||
public enum ColorSchemes
|
||||
|
|
@ -406,4 +481,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
Fast,
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="SpecialKeyState"/> object with all keys not pressed.
|
||||
/// </summary>
|
||||
public static readonly SpecialKeyState Default = new () {
|
||||
CtrlPressed = false,
|
||||
ShiftPressed = false,
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>4.3.0</Version>
|
||||
<PackageVersion>4.3.0</PackageVersion>
|
||||
<AssemblyVersion>4.3.0</AssemblyVersion>
|
||||
<FileVersion>4.3.0</FileVersion>
|
||||
<Version>4.4.0</Version>
|
||||
<PackageVersion>4.4.0</PackageVersion>
|
||||
<AssemblyVersion>4.4.0</AssemblyVersion>
|
||||
<FileVersion>4.4.0</FileVersion>
|
||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||
<Authors>Flow-Launcher</Authors>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
|
@ -57,17 +57,25 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fody" Version="6.5.4">
|
||||
<PackageReference Include="Fody" Version="6.5.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<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" />
|
||||
</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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
|
|
@ -16,7 +17,8 @@ namespace Flow.Launcher.Plugin
|
|||
public interface IPublicAPI
|
||||
{
|
||||
/// <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>
|
||||
/// <param name="query">query text</param>
|
||||
/// <param name="requery">
|
||||
|
|
@ -150,7 +152,6 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="callback"></param>
|
||||
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
|
||||
/// </summary>
|
||||
|
|
@ -180,19 +181,24 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
/// <param name="url">URL to download 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>
|
||||
/// <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>
|
||||
/// Add ActionKeyword for specific plugin
|
||||
/// Add ActionKeyword and update action keyword metadata for specific plugin
|
||||
/// Before adding, please check if action keyword is already assigned by <see cref="ActionKeywordAssigned"/>
|
||||
/// </summary>
|
||||
/// <param name="pluginId">ID for plugin that needs to add action keyword</param>
|
||||
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
|
||||
void AddActionKeyword(string pluginId, string newActionKeyword);
|
||||
|
||||
/// <summary>
|
||||
/// Remove ActionKeyword for specific plugin
|
||||
/// Remove ActionKeyword and update action keyword metadata for specific plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
|
||||
/// <param name="oldActionKeyword">The actionkeyword that is supposed to be removed</param>
|
||||
|
|
@ -294,9 +300,106 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <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);
|
||||
|
||||
/// <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
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for plugins that want to manually update their results
|
||||
/// </summary>
|
||||
public interface IResultUpdated : IFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Event that is triggered when the results are updated
|
||||
/// </summary>
|
||||
event ResultUpdatedEventHandler ResultsUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for the ResultsUpdated event
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for the ResultsUpdated event
|
||||
/// </summary>
|
||||
public class ResultUpdatedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// List of results that should be displayed
|
||||
/// </summary>
|
||||
public List<Result> Results;
|
||||
|
||||
/// <summary>
|
||||
/// Query that triggered the update
|
||||
/// </summary>
|
||||
public Query Query;
|
||||
|
||||
/// <summary>
|
||||
/// Token that can be used to cancel the update
|
||||
/// </summary>
|
||||
public CancellationToken Token { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,15 @@
|
|||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface is used to create settings panel for .Net plugins
|
||||
/// </summary>
|
||||
public interface ISettingProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Create settings panel control for .Net plugins
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Control CreateSettingPanel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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>
|
||||
public class PluginInitContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Default constructor.
|
||||
/// </summary>
|
||||
public PluginInitContext()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="currentPluginMetadata"></param>
|
||||
/// <param name="api"></param>
|
||||
public PluginInitContext(PluginMetadata currentPluginMetadata, IPublicAPI api)
|
||||
{
|
||||
CurrentPluginMetadata = currentPluginMetadata;
|
||||
|
|
|
|||
|
|
@ -4,24 +4,77 @@ using System.Text.Json.Serialization;
|
|||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin metadata
|
||||
/// </summary>
|
||||
public class PluginMetadata : BaseModel
|
||||
{
|
||||
private string _pluginDirectory;
|
||||
/// <summary>
|
||||
/// Plugin ID.
|
||||
/// </summary>
|
||||
public string ID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Website { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
public string ExecuteFilePath { get; private set;}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin author.
|
||||
/// </summary>
|
||||
public string Author { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin version.
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin language.
|
||||
/// See <see cref="AllowedLanguage"/>
|
||||
/// </summary>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin description.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin website.
|
||||
/// </summary>
|
||||
public string Website { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether plugin is disabled.
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin execute file path.
|
||||
/// </summary>
|
||||
public string ExecuteFilePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin execute file name.
|
||||
/// </summary>
|
||||
public string ExecuteFileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin assembly name.
|
||||
/// Only available for .Net plugins.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string AssemblyName { get; internal set; }
|
||||
|
||||
private string _pluginDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin source directory.
|
||||
/// </summary>
|
||||
public string PluginDirectory
|
||||
{
|
||||
get { return _pluginDirectory; }
|
||||
get => _pluginDirectory;
|
||||
internal set
|
||||
{
|
||||
_pluginDirectory = value;
|
||||
|
|
@ -30,28 +83,78 @@ namespace Flow.Launcher.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The first action keyword of plugin.
|
||||
/// </summary>
|
||||
public string ActionKeyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All action keywords of plugin.
|
||||
/// </summary>
|
||||
public List<string> ActionKeywords { get; set; }
|
||||
|
||||
public string IcoPath { get; set;}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
/// <summary>
|
||||
/// Hide plugin keyword setting panel.
|
||||
/// </summary>
|
||||
public bool HideActionKeywordPanel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin search delay time. 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]
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Init time include both plugin load time and init time
|
||||
/// Init time include both plugin load time and init time.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public long InitTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Average query time.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public long AvgQueryTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Query count.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int QueryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the plugin settings directory which is not validated.
|
||||
/// It is used to store plugin settings files and data files.
|
||||
/// When plugin is deleted, FL will ask users whether to keep its settings.
|
||||
/// If users do not want to keep, this directory will be deleted.
|
||||
/// </summary>
|
||||
public string PluginSettingsDirectoryPath { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the plugin cache directory which is not validated.
|
||||
/// It is used to store cache files.
|
||||
/// When plugin is deleted, this directory will be deleted as well.
|
||||
/// </summary>
|
||||
public string PluginCacheDirectoryPath { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert <see cref="PluginMetadata"/> to string.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,37 @@
|
|||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin instance and plugin metadata
|
||||
/// </summary>
|
||||
public class PluginPair
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin instance
|
||||
/// </summary>
|
||||
public IAsyncPlugin Plugin { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin metadata
|
||||
/// </summary>
|
||||
public PluginMetadata Metadata { get; internal set; }
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Convert to string
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Metadata.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare by plugin metadata ID
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
PluginPair r = obj as PluginPair;
|
||||
if (r != null)
|
||||
if (obj is PluginPair r)
|
||||
{
|
||||
return string.Equals(r.Metadata.ID, Metadata.ID);
|
||||
}
|
||||
|
|
@ -25,6 +41,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get hash code
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a query that is sent to a plugin.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Raw query, this includes action keyword if it has
|
||||
/// 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 = " ";
|
||||
|
||||
/// <summary>
|
||||
/// User can set multiple action keywords seperated by ';'
|
||||
/// User can set multiple action keywords seperated by whitespace
|
||||
/// </summary>
|
||||
public const string ActionKeywordSeparator = ";";
|
||||
|
||||
public const string ActionKeywordSeparator = TermSeparator;
|
||||
|
||||
/// <summary>
|
||||
/// Wildcard action keyword. Plugins using this value will be queried on every search.
|
||||
|
|
@ -67,13 +55,13 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public string ActionKeyword { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
/// <summary>
|
||||
/// Splits <see cref="SearchTerms"/> by spaces and returns the first item.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// returns an empty string when <see cref="SearchTerms"/> does not have enough items.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string FirstSearch => SplitSearch(0);
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Runtime;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -13,7 +12,6 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public class Result
|
||||
{
|
||||
|
||||
private string _pluginDirectory;
|
||||
|
||||
private string _icoPath;
|
||||
|
|
@ -70,7 +68,8 @@ namespace Flow.Launcher.Plugin
|
|||
&& !string.IsNullOrEmpty(PluginDirectory)
|
||||
&& !Path.IsPathRooted(value)
|
||||
&& !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_icoPath = Path.Combine(PluginDirectory, value);
|
||||
}
|
||||
|
|
@ -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 />
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
@ -205,6 +183,16 @@ namespace Flow.Launcher.Plugin
|
|||
TitleHighlightData = TitleHighlightData,
|
||||
OriginQuery = OriginQuery,
|
||||
PluginDirectory = PluginDirectory,
|
||||
ContextData = ContextData,
|
||||
PluginID = PluginID,
|
||||
TitleToolTip = TitleToolTip,
|
||||
SubTitleToolTip = SubTitleToolTip,
|
||||
PreviewPanel = PreviewPanel,
|
||||
ProgressBar = ProgressBar,
|
||||
ProgressBarColor = ProgressBarColor,
|
||||
Preview = Preview,
|
||||
AddSelectedCount = AddSelectedCount,
|
||||
RecordKey = RecordKey
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -262,6 +250,24 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
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>
|
||||
/// Info of the preview section of a <see cref="Result"/>
|
||||
/// </summary>
|
||||
|
|
@ -270,12 +276,12 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Full image used for preview panel
|
||||
/// </summary>
|
||||
public string PreviewImagePath { get; set; }
|
||||
public string PreviewImagePath { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the preview image should occupy the full width of the preview panel.
|
||||
/// </summary>
|
||||
public bool IsMedia { get; set; }
|
||||
public bool IsMedia { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Result description text that is shown at the bottom of the preview panel.
|
||||
|
|
@ -283,12 +289,17 @@ namespace Flow.Launcher.Plugin
|
|||
/// <remarks>
|
||||
/// When a value is not set, the <see cref="SubTitle"/> will be used.
|
||||
/// </remarks>
|
||||
public string Description { get; set; }
|
||||
public string Description { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate to get the preview panel's image
|
||||
/// </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>
|
||||
/// Default instance of <see cref="PreviewInfo"/>
|
||||
|
|
@ -299,6 +310,7 @@ namespace Flow.Launcher.Plugin
|
|||
Description = null,
|
||||
IsMedia = false,
|
||||
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>
|
||||
/// <param name="sourcePath"></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.
|
||||
DirectoryInfo dir = new DirectoryInfo(sourcePath);
|
||||
|
|
@ -54,7 +55,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
foreach (DirectoryInfo subdir in dirs)
|
||||
{
|
||||
string temppath = Path.Combine(targetPath, subdir.Name);
|
||||
CopyAll(subdir.FullName, temppath);
|
||||
CopyAll(subdir.FullName, temppath, messageBoxExShow);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
@ -62,8 +63,9 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
#if DEBUG
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath));
|
||||
RemoveFolderIfExists(targetPath);
|
||||
messageBoxExShow ??= MessageBox.Show;
|
||||
messageBoxExShow(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath));
|
||||
RemoveFolderIfExists(targetPath, messageBoxExShow);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +77,9 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
/// </summary>
|
||||
/// <param name="fromPath"></param>
|
||||
/// <param name="toPath"></param>
|
||||
/// <param name="messageBoxExShow"></param>
|
||||
/// <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
|
||||
{
|
||||
|
|
@ -96,7 +99,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
#if DEBUG
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath));
|
||||
messageBoxExShow ??= MessageBox.Show;
|
||||
messageBoxExShow(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath));
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
|
@ -107,7 +111,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
/// Deletes a folder if it exists
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
|
|
@ -119,7 +124,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
#if DEBUG
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path));
|
||||
messageBoxExShow ??= MessageBox.Show;
|
||||
messageBoxExShow(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -148,7 +154,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
/// Open a directory window (using the OS's default handler, usually explorer)
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
|
|
@ -166,7 +173,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
#if DEBUG
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath));
|
||||
messageBoxExShow ??= MessageBox.Show;
|
||||
messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -177,7 +185,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
/// <param name="filePath">File path</param>
|
||||
/// <param name="workingDir">Working directory</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
|
||||
{
|
||||
|
|
@ -196,7 +205,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
#if DEBUG
|
||||
throw;
|
||||
#else
|
||||
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", filePath));
|
||||
messageBoxExShow ??= MessageBox.Show;
|
||||
messageBoxExShow(string.Format("Unable to open the path {0}, please check if it exists", filePath));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -308,5 +318,51 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
{
|
||||
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a directory, creating it if it doesn't exist
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
public static void ValidateDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a data directory, synchronizing it by ensuring all files from a bundled source directory exist in it.
|
||||
/// If files are missing or outdated, they are copied from the bundled directory to the data directory.
|
||||
/// </summary>
|
||||
/// <param name="bundledDataDirectory"></param>
|
||||
/// <param name="dataDirectory"></param>
|
||||
public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
|
||||
{
|
||||
if (!Directory.Exists(dataDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
}
|
||||
|
||||
foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
|
||||
{
|
||||
var data = Path.GetFileName(bundledDataPath);
|
||||
if (data == null) continue;
|
||||
var dataPath = Path.Combine(dataDirectory, data);
|
||||
if (!File.Exists(dataPath))
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
|
||||
var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
|
||||
if (time1 != time2)
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ using System.Linq;
|
|||
|
||||
namespace Flow.Launcher.Plugin.SharedCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains methods to open a search in a new browser window or tab.
|
||||
/// </summary>
|
||||
public static class SearchWeb
|
||||
{
|
||||
private static string GetDefaultBrowserPath()
|
||||
|
|
@ -106,4 +109,4 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,32 @@
|
|||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
namespace Flow.Launcher.Plugin.SharedCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains methods for running shell commands
|
||||
/// </summary>
|
||||
public static class ShellCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for EnumThreadWindows
|
||||
/// </summary>
|
||||
/// <param name="hwnd"></param>
|
||||
/// <param name="lParam"></param>
|
||||
/// <returns></returns>
|
||||
public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam);
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Runs a windows command using the provided ProcessStartInfo
|
||||
/// </summary>
|
||||
/// <param name="processStartInfo"></param>
|
||||
/// <returns></returns>
|
||||
public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo)
|
||||
{
|
||||
processStartInfo.Verb = "RunAsUser";
|
||||
|
|
@ -28,6 +39,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
CheckSecurityWindow();
|
||||
Thread.Sleep(25);
|
||||
}
|
||||
|
||||
while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open
|
||||
{
|
||||
containsSecurityWindow = false;
|
||||
|
|
@ -42,24 +54,42 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
{
|
||||
ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads;
|
||||
for (int i = 0; i < ptc.Count; i++)
|
||||
EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero);
|
||||
PInvoke.EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero);
|
||||
}
|
||||
|
||||
private static bool CheckSecurityThread(IntPtr hwnd, IntPtr lParam)
|
||||
private static BOOL CheckSecurityThread(HWND hwnd, LPARAM lParam)
|
||||
{
|
||||
if (GetWindowTitle(hwnd) == "Windows Security")
|
||||
containsSecurityWindow = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string GetWindowTitle(IntPtr hwnd)
|
||||
private static unsafe string GetWindowTitle(HWND hwnd)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(GetWindowTextLength(hwnd) + 1);
|
||||
GetWindowText(hwnd, sb, sb.Capacity);
|
||||
return sb.ToString();
|
||||
var capacity = PInvoke.GetWindowTextLength(hwnd) + 1;
|
||||
int length;
|
||||
Span<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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,14 +2,29 @@
|
|||
|
||||
namespace Flow.Launcher.Plugin.SharedModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of a match operation.
|
||||
/// </summary>
|
||||
public class MatchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MatchResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="success"></param>
|
||||
/// <param name="searchPrecision"></param>
|
||||
public MatchResult(bool success, SearchPrecisionScore searchPrecision)
|
||||
{
|
||||
Success = success;
|
||||
SearchPrecision = searchPrecision;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MatchResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="success"></param>
|
||||
/// <param name="searchPrecision"></param>
|
||||
/// <param name="matchData"></param>
|
||||
/// <param name="rawScore"></param>
|
||||
public MatchResult(bool success, SearchPrecisionScore searchPrecision, List<int> matchData, int rawScore)
|
||||
{
|
||||
Success = success;
|
||||
|
|
@ -18,6 +33,9 @@ namespace Flow.Launcher.Plugin.SharedModels
|
|||
RawScore = rawScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the match operation was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -30,6 +48,9 @@ namespace Flow.Launcher.Plugin.SharedModels
|
|||
/// </summary>
|
||||
private int _rawScore;
|
||||
|
||||
/// <summary>
|
||||
/// The raw calculated search score without any search precision filtering applied.
|
||||
/// </summary>
|
||||
public int RawScore
|
||||
{
|
||||
get { return _rawScore; }
|
||||
|
|
@ -45,8 +66,15 @@ namespace Flow.Launcher.Plugin.SharedModels
|
|||
/// </summary>
|
||||
public List<int> MatchData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The search precision score used to filter the search results.
|
||||
/// </summary>
|
||||
public SearchPrecisionScore SearchPrecision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the search precision score is met.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsSearchPrecisionScoreMet()
|
||||
{
|
||||
return IsSearchPrecisionScoreMet(_rawScore);
|
||||
|
|
@ -63,10 +91,24 @@ namespace Flow.Launcher.Plugin.SharedModels
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the search precision score used to filter search results.
|
||||
/// </summary>
|
||||
public enum SearchPrecisionScore
|
||||
{
|
||||
/// <summary>
|
||||
/// The highest search precision score.
|
||||
/// </summary>
|
||||
Regular = 50,
|
||||
|
||||
/// <summary>
|
||||
/// The medium search precision score.
|
||||
/// </summary>
|
||||
Low = 20,
|
||||
|
||||
/// <summary>
|
||||
/// The lowest search precision score.
|
||||
/// </summary>
|
||||
None = 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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 NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace Flow.Launcher.Test
|
||||
{
|
||||
|
|
@ -35,7 +36,7 @@ namespace Flow.Launcher.Test
|
|||
[TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)]
|
||||
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
|
||||
|
|
@ -47,7 +48,7 @@ namespace Flow.Launcher.Test
|
|||
[TestCase(@"c:\foo", @"c:\foo\", true)]
|
||||
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>
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
|
||||
<PackageReference Include="nunit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
|
|
@ -21,6 +22,8 @@ namespace Flow.Launcher.Test
|
|||
private const string MicrosoftSqlServerManagementStudio = "Microsoft SQL Server Management Studio";
|
||||
private const string VisualStudioCode = "Visual Studio Code";
|
||||
|
||||
private readonly IAlphabet alphabet = null;
|
||||
|
||||
public List<string> GetSearchStrings()
|
||||
=> new List<string>
|
||||
{
|
||||
|
|
@ -34,7 +37,7 @@ namespace Flow.Launcher.Test
|
|||
OneOneOneOne
|
||||
};
|
||||
|
||||
public List<int> GetPrecisionScores()
|
||||
public static List<int> GetPrecisionScores()
|
||||
{
|
||||
var listToReturn = new List<int>();
|
||||
|
||||
|
|
@ -59,7 +62,7 @@ namespace Flow.Launcher.Test
|
|||
};
|
||||
|
||||
var results = new List<Result>();
|
||||
var matcher = new StringMatcher();
|
||||
var matcher = new StringMatcher(alphabet);
|
||||
foreach (var str in sources)
|
||||
{
|
||||
results.Add(new Result
|
||||
|
|
@ -71,20 +74,20 @@ namespace Flow.Launcher.Test
|
|||
|
||||
results = results.Where(x => x.Score > 0).OrderByDescending(x => x.Score).ToList();
|
||||
|
||||
Assert.IsTrue(results.Count == 3);
|
||||
Assert.IsTrue(results[0].Title == "Inste");
|
||||
Assert.IsTrue(results[1].Title == "Install Package");
|
||||
Assert.IsTrue(results[2].Title == "file open in browser-test");
|
||||
ClassicAssert.IsTrue(results.Count == 3);
|
||||
ClassicAssert.IsTrue(results[0].Title == "Inste");
|
||||
ClassicAssert.IsTrue(results[1].Title == "Install Package");
|
||||
ClassicAssert.IsTrue(results[2].Title == "file open in browser-test");
|
||||
}
|
||||
|
||||
[TestCase("Chrome")]
|
||||
public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString)
|
||||
{
|
||||
var compareString = "Can have rum only in my glass";
|
||||
var matcher = new StringMatcher();
|
||||
var matcher = new StringMatcher(alphabet);
|
||||
var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore;
|
||||
|
||||
Assert.True(scoreResult == 0);
|
||||
ClassicAssert.True(scoreResult == 0);
|
||||
}
|
||||
|
||||
[TestCase("chr")]
|
||||
|
|
@ -97,7 +100,7 @@ namespace Flow.Launcher.Test
|
|||
string searchTerm)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
var matcher = new StringMatcher();
|
||||
var matcher = new StringMatcher(alphabet);
|
||||
foreach (var str in GetSearchStrings())
|
||||
{
|
||||
results.Add(new Result
|
||||
|
|
@ -125,7 +128,7 @@ namespace Flow.Launcher.Test
|
|||
Debug.WriteLine("###############################################");
|
||||
Debug.WriteLine("");
|
||||
|
||||
Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore));
|
||||
ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,11 +150,11 @@ namespace Flow.Launcher.Test
|
|||
string queryString, string compareString, int expectedScore)
|
||||
{
|
||||
// When, Given
|
||||
var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
|
||||
var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
|
||||
var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore;
|
||||
|
||||
// Should
|
||||
Assert.AreEqual(expectedScore, rawScore,
|
||||
ClassicAssert.AreEqual(expectedScore, rawScore,
|
||||
$"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}");
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +184,7 @@ namespace Flow.Launcher.Test
|
|||
bool expectedPrecisionResult)
|
||||
{
|
||||
// When
|
||||
var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore};
|
||||
var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore};
|
||||
|
||||
// Given
|
||||
var matchResult = matcher.FuzzyMatch(queryString, compareString);
|
||||
|
|
@ -190,12 +193,12 @@ namespace Flow.Launcher.Test
|
|||
Debug.WriteLine("###############################################");
|
||||
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
|
||||
Debug.WriteLine(
|
||||
$"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
|
||||
$"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
|
||||
Debug.WriteLine("###############################################");
|
||||
Debug.WriteLine("");
|
||||
|
||||
// Should
|
||||
Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
|
||||
ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
|
||||
$"Query: {queryString}{Environment.NewLine} " +
|
||||
$"Compare: {compareString}{Environment.NewLine}" +
|
||||
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
|
||||
|
|
@ -232,7 +235,7 @@ namespace Flow.Launcher.Test
|
|||
bool expectedPrecisionResult)
|
||||
{
|
||||
// When
|
||||
var matcher = new StringMatcher {UserSettingSearchPrecision = expectedPrecisionScore};
|
||||
var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore};
|
||||
|
||||
// Given
|
||||
var matchResult = matcher.FuzzyMatch(queryString, compareString);
|
||||
|
|
@ -241,12 +244,12 @@ namespace Flow.Launcher.Test
|
|||
Debug.WriteLine("###############################################");
|
||||
Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}");
|
||||
Debug.WriteLine(
|
||||
$"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
|
||||
$"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})");
|
||||
Debug.WriteLine("###############################################");
|
||||
Debug.WriteLine("");
|
||||
|
||||
// Should
|
||||
Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
|
||||
ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(),
|
||||
$"Query:{queryString}{Environment.NewLine} " +
|
||||
$"Compare:{compareString}{Environment.NewLine}" +
|
||||
$"Raw Score: {matchResult.RawScore}{Environment.NewLine}" +
|
||||
|
|
@ -260,7 +263,7 @@ namespace Flow.Launcher.Test
|
|||
string queryString, string compareString1, string compareString2)
|
||||
{
|
||||
// When
|
||||
var matcher = new StringMatcher {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
|
||||
var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular};
|
||||
|
||||
// Given
|
||||
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
|
||||
|
|
@ -277,7 +280,7 @@ namespace Flow.Launcher.Test
|
|||
Debug.WriteLine("");
|
||||
|
||||
// Should
|
||||
Assert.True(compareString1Result.Score > compareString2Result.Score,
|
||||
ClassicAssert.True(compareString1Result.Score > compareString2Result.Score,
|
||||
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
||||
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
|
||||
$"Should be greater than{Environment.NewLine}" +
|
||||
|
|
@ -293,7 +296,7 @@ namespace Flow.Launcher.Test
|
|||
string queryString, string compareString1, string compareString2)
|
||||
{
|
||||
// When
|
||||
var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
|
||||
var matcher = new StringMatcher(alphabet) { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
|
||||
|
||||
// Given
|
||||
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
|
||||
|
|
@ -310,7 +313,7 @@ namespace Flow.Launcher.Test
|
|||
Debug.WriteLine("");
|
||||
|
||||
// Should
|
||||
Assert.True(compareString1Result.Score > compareString2Result.Score,
|
||||
ClassicAssert.True(compareString1Result.Score > compareString2Result.Score,
|
||||
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
||||
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
|
||||
$"Should be greater than{Environment.NewLine}" +
|
||||
|
|
@ -323,7 +326,7 @@ namespace Flow.Launcher.Test
|
|||
string secondName, string secondDescription, string secondExecutableName)
|
||||
{
|
||||
// Act
|
||||
var matcher = new StringMatcher();
|
||||
var matcher = new StringMatcher(alphabet);
|
||||
var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore;
|
||||
var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore;
|
||||
var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore;
|
||||
|
|
@ -336,7 +339,7 @@ namespace Flow.Launcher.Test
|
|||
var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max();
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(firstScore > secondScore,
|
||||
ClassicAssert.IsTrue(firstScore > secondScore,
|
||||
$"Query: \"{queryString}\"{Environment.NewLine} " +
|
||||
$"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" +
|
||||
$"Should be greater than{Environment.NewLine}" +
|
||||
|
|
@ -358,9 +361,9 @@ namespace Flow.Launcher.Test
|
|||
public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString,
|
||||
int desiredScore)
|
||||
{
|
||||
var matcher = new StringMatcher();
|
||||
var matcher = new StringMatcher(alphabet);
|
||||
var score = matcher.FuzzyMatch(queryString, compareString).Score;
|
||||
Assert.IsTrue(score == desiredScore,
|
||||
ClassicAssert.IsTrue(score == desiredScore,
|
||||
$@"Query: ""{queryString}""
|
||||
CompareString: ""{compareString}""
|
||||
Score: {score}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using NUnit.Framework;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Infrastructure.Http;
|
||||
|
|
@ -16,16 +17,16 @@ namespace Flow.Launcher.Test
|
|||
|
||||
proxy.Enabled = true;
|
||||
proxy.Server = "127.0.0.1";
|
||||
Assert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}"));
|
||||
Assert.IsNull(Http.WebProxy.Credentials);
|
||||
ClassicAssert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}"));
|
||||
ClassicAssert.IsNull(Http.WebProxy.Credentials);
|
||||
|
||||
proxy.UserName = "test";
|
||||
Assert.NotNull(Http.WebProxy.Credentials);
|
||||
Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName);
|
||||
Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, "");
|
||||
ClassicAssert.NotNull(Http.WebProxy.Credentials);
|
||||
ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName);
|
||||
ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, "");
|
||||
|
||||
proxy.Password = "test password";
|
||||
Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password);
|
||||
ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -15,37 +16,37 @@ namespace Flow.Launcher.Test
|
|||
// Given
|
||||
var duplicateList = new List<PluginMetadata>
|
||||
{
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||
Version = "1.0.1"
|
||||
},
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||
Version = "1.0.2"
|
||||
},
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "CEA0TYUC6D3B4085823D60DC76F28855",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "ABC0TYUC6D3B7855823D60DC76F28855",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "ABC0TYUC6D3B7855823D60DC76F28855",
|
||||
Version = "1.0.0"
|
||||
|
|
@ -56,11 +57,11 @@ namespace Flow.Launcher.Test
|
|||
(var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList);
|
||||
|
||||
// Then
|
||||
Assert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2");
|
||||
Assert.True(unique.Count() == 1);
|
||||
ClassicAssert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2");
|
||||
ClassicAssert.True(unique.Count == 1);
|
||||
|
||||
Assert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855"));
|
||||
Assert.True(duplicates.Count() == 6);
|
||||
ClassicAssert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855"));
|
||||
ClassicAssert.True(duplicates.Count == 6);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -69,12 +70,12 @@ namespace Flow.Launcher.Test
|
|||
// Given
|
||||
var duplicateList = new List<PluginMetadata>
|
||||
{
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "CEA0TYUC6D3B7855823D60DC76F28855",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
new PluginMetadata
|
||||
new()
|
||||
{
|
||||
ID = "CEA0TYUC6D3B7855823D60DC76F28855",
|
||||
Version = "1.0.0"
|
||||
|
|
@ -85,8 +86,8 @@ namespace Flow.Launcher.Test
|
|||
(var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList);
|
||||
|
||||
// Then
|
||||
Assert.True(unique.Count() == 0);
|
||||
Assert.True(duplicates.Count() == 2);
|
||||
ClassicAssert.True(unique.Count == 0);
|
||||
ClassicAssert.True(duplicates.Count == 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
|
|||
using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static Flow.Launcher.Plugin.Explorer.Search.SearchManager;
|
||||
|
||||
namespace Flow.Launcher.Test.Plugins
|
||||
|
|
@ -22,28 +20,6 @@ namespace Flow.Launcher.Test.Plugins
|
|||
[TestFixture]
|
||||
public class ExplorerTest
|
||||
{
|
||||
#pragma warning disable CS1998 // async method with no await (more readable to leave it async to match the tested signature)
|
||||
private async Task<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 PreviousLocationNotExistReturnsFalse(string dummyString) => false;
|
||||
|
|
@ -57,7 +33,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(result == expectedString,
|
||||
ClassicAssert.IsTrue(result == expectedString,
|
||||
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual: {result}{Environment.NewLine}");
|
||||
}
|
||||
|
|
@ -74,7 +50,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var queryString = queryConstructor.Directory(folderPath);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
|
||||
ClassicAssert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
|
||||
$"Expected string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {queryString}{Environment.NewLine}");
|
||||
}
|
||||
|
|
@ -94,7 +70,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var queryString = queryConstructor.Directory(folderPath, userSearchString);
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedString, queryString);
|
||||
ClassicAssert.AreEqual(expectedString, queryString);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
|
|
@ -105,7 +81,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch;
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedString, resultString);
|
||||
ClassicAssert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
|
|
@ -128,7 +104,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var resultString = queryConstructor.FilesAndFolders(userSearchString);
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedString, resultString);
|
||||
ClassicAssert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -138,13 +114,13 @@ namespace Flow.Launcher.Test.Plugins
|
|||
string querySearchString, string expectedString)
|
||||
{
|
||||
// Given
|
||||
var queryConstructor = new QueryConstructor(new Settings());
|
||||
_ = new QueryConstructor(new Settings());
|
||||
|
||||
//When
|
||||
var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(resultString == expectedString,
|
||||
ClassicAssert.IsTrue(resultString == expectedString,
|
||||
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||
}
|
||||
|
|
@ -162,12 +138,12 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var resultString = queryConstructor.FileContent(userSearchString);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(resultString == expectedString,
|
||||
ClassicAssert.IsTrue(resultString == expectedString,
|
||||
$"Expected query string: {expectedString}{Environment.NewLine} " +
|
||||
$"Actual string was: {resultString}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
|
||||
public static void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
|
||||
{
|
||||
// Given
|
||||
var query = new Query
|
||||
|
|
@ -181,7 +157,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(result,
|
||||
ClassicAssert.IsTrue(result,
|
||||
$"Expected True for file content search. {Environment.NewLine} " +
|
||||
$"Actual result was: {result}{Environment.NewLine}");
|
||||
}
|
||||
|
|
@ -206,7 +182,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var result = FilesFolders.IsLocationPathString(querySearchString);
|
||||
|
||||
//Then
|
||||
Assert.IsTrue(result == expectedResult,
|
||||
ClassicAssert.IsTrue(result == expectedResult,
|
||||
$"Expected query search string check result is: {expectedResult} {Environment.NewLine} " +
|
||||
$"Actual check result is {result} {Environment.NewLine}");
|
||||
|
||||
|
|
@ -233,7 +209,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path);
|
||||
|
||||
//Then
|
||||
Assert.IsTrue(previousDirectoryPath == expectedString,
|
||||
ClassicAssert.IsTrue(previousDirectoryPath == expectedString,
|
||||
$"Expected path string: {expectedString} {Environment.NewLine} " +
|
||||
$"Actual path string is {previousDirectoryPath} {Environment.NewLine}");
|
||||
}
|
||||
|
|
@ -246,7 +222,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
|
||||
|
||||
//Then
|
||||
Assert.IsTrue(returnedPath == expectedString,
|
||||
ClassicAssert.IsTrue(returnedPath == expectedString,
|
||||
$"Expected path string: {expectedString} {Environment.NewLine} " +
|
||||
$"Actual path string is {returnedPath} {Environment.NewLine}");
|
||||
}
|
||||
|
|
@ -260,7 +236,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var resultString = QueryConstructor.RecursiveDirectoryConstraint(path);
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedString, resultString);
|
||||
ClassicAssert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
|
|
@ -274,7 +250,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(expectedString, resultString);
|
||||
ClassicAssert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
|
||||
[TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")]
|
||||
|
|
@ -305,7 +281,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(result, expectedResult);
|
||||
ClassicAssert.AreEqual(result, expectedResult);
|
||||
}
|
||||
|
||||
[TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")]
|
||||
|
|
@ -334,7 +310,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(result, expectedResult);
|
||||
ClassicAssert.AreEqual(result, expectedResult);
|
||||
}
|
||||
|
||||
[TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")]
|
||||
|
|
@ -366,7 +342,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(result, expectedResult);
|
||||
ClassicAssert.AreEqual(result, expectedResult);
|
||||
}
|
||||
|
||||
[TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")]
|
||||
|
|
@ -398,7 +374,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
|
||||
|
||||
// Then
|
||||
Assert.AreEqual(result, expectedResult);
|
||||
ClassicAssert.AreEqual(result, expectedResult);
|
||||
}
|
||||
|
||||
[TestCase(@"c:\foo", @"c:\foo", true)]
|
||||
|
|
@ -420,7 +396,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
};
|
||||
|
||||
// When, Then
|
||||
Assert.AreEqual(expectedResult, comparator.Equals(result1, result2));
|
||||
ClassicAssert.AreEqual(expectedResult, comparator.Equals(result1, result2));
|
||||
}
|
||||
|
||||
[TestCase(@"c:\foo\", @"c:\foo\")]
|
||||
|
|
@ -444,7 +420,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var hash2 = comparator.GetHashCode(result2);
|
||||
|
||||
// When, Then
|
||||
Assert.IsTrue(hash1 == hash2);
|
||||
ClassicAssert.IsTrue(hash1 == hash2);
|
||||
}
|
||||
|
||||
[TestCase(@"%appdata%", true)]
|
||||
|
|
@ -461,7 +437,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var result = EnvironmentVariables.HasEnvironmentVar(path);
|
||||
|
||||
// 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.Plugin;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.Test.Plugins
|
||||
|
|
@ -40,13 +39,13 @@ namespace Flow.Launcher.Test.Plugins
|
|||
Search = resultText
|
||||
}, default);
|
||||
|
||||
Assert.IsNotNull(results);
|
||||
ClassicAssert.IsNotNull(results);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsNotNull(result.AsyncAction);
|
||||
Assert.IsNotNull(result.Title);
|
||||
ClassicAssert.IsNotNull(result);
|
||||
ClassicAssert.IsNotNull(result.AsyncAction);
|
||||
ClassicAssert.IsNotNull(result.Title);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -56,35 +55,11 @@ namespace Flow.Launcher.Test.Plugins
|
|||
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>()),
|
||||
new JsonRPCQueryResponseModel(0, new List<JsonRPCResult>
|
||||
{
|
||||
new JsonRPCResult
|
||||
new()
|
||||
{
|
||||
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.Legacy;
|
||||
using Flow.Launcher.Plugin.Url;
|
||||
|
||||
namespace Flow.Launcher.Test
|
||||
namespace Flow.Launcher.Test.Plugins
|
||||
{
|
||||
[TestFixture]
|
||||
public class UrlPluginTest
|
||||
|
|
@ -10,23 +11,23 @@ namespace Flow.Launcher.Test
|
|||
public void URLMatchTest()
|
||||
{
|
||||
var plugin = new Main();
|
||||
Assert.IsTrue(plugin.IsURL("http://www.google.com"));
|
||||
Assert.IsTrue(plugin.IsURL("https://www.google.com"));
|
||||
Assert.IsTrue(plugin.IsURL("http://google.com"));
|
||||
Assert.IsTrue(plugin.IsURL("www.google.com"));
|
||||
Assert.IsTrue(plugin.IsURL("google.com"));
|
||||
Assert.IsTrue(plugin.IsURL("http://localhost"));
|
||||
Assert.IsTrue(plugin.IsURL("https://localhost"));
|
||||
Assert.IsTrue(plugin.IsURL("http://localhost:80"));
|
||||
Assert.IsTrue(plugin.IsURL("https://localhost:80"));
|
||||
Assert.IsTrue(plugin.IsURL("http://110.10.10.10"));
|
||||
Assert.IsTrue(plugin.IsURL("110.10.10.10"));
|
||||
Assert.IsTrue(plugin.IsURL("ftp://110.10.10.10"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("http://www.google.com"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("https://www.google.com"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("http://google.com"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("www.google.com"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("google.com"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("http://localhost"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("https://localhost"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("http://localhost:80"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("https://localhost:80"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("http://110.10.10.10"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("110.10.10.10"));
|
||||
ClassicAssert.IsTrue(plugin.IsURL("ftp://110.10.10.10"));
|
||||
|
||||
|
||||
Assert.IsFalse(plugin.IsURL("wwww"));
|
||||
Assert.IsFalse(plugin.IsURL("wwww.c"));
|
||||
Assert.IsFalse(plugin.IsURL("wwww.c"));
|
||||
ClassicAssert.IsFalse(plugin.IsURL("wwww"));
|
||||
ClassicAssert.IsFalse(plugin.IsURL("wwww.c"));
|
||||
ClassicAssert.IsFalse(plugin.IsURL("wwww.c"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
|
|
@ -17,17 +18,17 @@ namespace Flow.Launcher.Test
|
|||
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
|
||||
Assert.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.");
|
||||
Assert.AreEqual(">", q.ActionKeyword);
|
||||
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);
|
||||
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
|
||||
ClassicAssert.AreEqual(">", q.ActionKeyword);
|
||||
|
||||
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);
|
||||
Assert.AreEqual("google.com", q.SecondSearch);
|
||||
Assert.AreEqual("-n", q.ThirdSearch);
|
||||
ClassicAssert.AreEqual("ping", q.FirstSearch);
|
||||
ClassicAssert.AreEqual("google.com", q.SecondSearch);
|
||||
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]
|
||||
|
|
@ -40,11 +41,11 @@ namespace Flow.Launcher.Test
|
|||
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
|
||||
Assert.AreEqual("> ping google.com -n 20 -6", q.Search);
|
||||
Assert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
|
||||
Assert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
|
||||
Assert.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.Search);
|
||||
ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
|
||||
ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
|
||||
ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
|
||||
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -52,13 +53,13 @@ namespace Flow.Launcher.Test
|
|||
{
|
||||
Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary<string, PluginPair>());
|
||||
|
||||
Assert.AreEqual("file.txt file2 file3", q.Search);
|
||||
Assert.AreEqual("", q.ActionKeyword);
|
||||
ClassicAssert.AreEqual("file.txt file2 file3", q.Search);
|
||||
ClassicAssert.AreEqual("", q.ActionKeyword);
|
||||
|
||||
Assert.AreEqual("file.txt", q.FirstSearch);
|
||||
Assert.AreEqual("file2", q.SecondSearch);
|
||||
Assert.AreEqual("file3", q.ThirdSearch);
|
||||
Assert.AreEqual("file2 file3", q.SecondToEndSearch);
|
||||
ClassicAssert.AreEqual("file.txt", q.FirstSearch);
|
||||
ClassicAssert.AreEqual("file2", q.SecondSearch);
|
||||
ClassicAssert.AreEqual("file3", q.ThirdSearch);
|
||||
ClassicAssert.AreEqual("file2 file3", q.SecondToEndSearch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using Flow.Launcher.Core;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
|
|
@ -31,20 +34,59 @@ namespace Flow.Launcher
|
|||
|
||||
private void btnDone_OnClick(object sender, RoutedEventArgs _)
|
||||
{
|
||||
var oldActionKeyword = plugin.Metadata.ActionKeywords[0];
|
||||
var newActionKeyword = tbAction.Text.Trim();
|
||||
newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*";
|
||||
|
||||
if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword))
|
||||
var oldActionKeywords = plugin.Metadata.ActionKeywords;
|
||||
|
||||
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList();
|
||||
newActionKeywords.RemoveAll(string.IsNullOrEmpty);
|
||||
newActionKeywords = newActionKeywords.Distinct().ToList();
|
||||
|
||||
newActionKeywords = newActionKeywords.Count > 0 ? newActionKeywords : new() { Query.GlobalPluginWildcardSign };
|
||||
|
||||
var addedActionKeywords = newActionKeywords.Except(oldActionKeywords).ToList();
|
||||
var removedActionKeywords = oldActionKeywords.Except(newActionKeywords).ToList();
|
||||
if (!addedActionKeywords.Any(App.API.ActionKeywordAssigned))
|
||||
{
|
||||
pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword);
|
||||
Close();
|
||||
if (oldActionKeywords.Count != newActionKeywords.Count)
|
||||
{
|
||||
ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||
return;
|
||||
}
|
||||
|
||||
var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList();
|
||||
var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList();
|
||||
|
||||
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
|
||||
{
|
||||
// User just changes the sequence of action keywords
|
||||
var msg = translater.GetTranslation("newActionKeywordsSameAsOld");
|
||||
MessageBoxEx.Show(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceActionKeyword(plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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:ui="http://schemas.modernwpf.com/2019"
|
||||
ShutdownMode="OnMainWindowClose"
|
||||
Startup="OnStartupAsync">
|
||||
Startup="OnStartup">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
|
|
@ -20,12 +20,17 @@
|
|||
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</ui:ThemeResources.ThemeDictionaries>
|
||||
</ui:ThemeResources>
|
||||
<ui:XamlControlsResources />
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/CustomControlTemplate.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.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core;
|
||||
using Flow.Launcher.Core.Configuration;
|
||||
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
||||
|
|
@ -14,91 +15,172 @@ using Flow.Launcher.Infrastructure;
|
|||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Infrastructure.Image;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class App : IDisposable, ISingleInstanceApp
|
||||
{
|
||||
public static PublicAPIInstance API { get; private set; }
|
||||
private const string Unique = "Flow.Launcher_Unique_Application_Mutex";
|
||||
#region Public Properties
|
||||
|
||||
public static IPublicAPI API { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private static bool _disposed;
|
||||
private Settings _settings;
|
||||
private MainViewModel _mainVM;
|
||||
private SettingWindowViewModel _settingsVM;
|
||||
private readonly Updater _updater = new Updater(Flow.Launcher.Properties.Settings.Default.GithubRepo);
|
||||
private readonly Portable _portable = new Portable();
|
||||
private readonly PinyinAlphabet _alphabet = new PinyinAlphabet();
|
||||
private StringMatcher _stringMatcher;
|
||||
private MainWindow _mainWindow;
|
||||
private readonly MainViewModel _mainVM;
|
||||
private readonly Settings _settings;
|
||||
|
||||
// To prevent two disposals running at the same time.
|
||||
private static readonly object _disposingLock = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public App()
|
||||
{
|
||||
// Initialize settings
|
||||
try
|
||||
{
|
||||
var storage = new FlowLauncherJsonStorage<Settings>();
|
||||
_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]
|
||||
public static void Main()
|
||||
{
|
||||
if (SingleInstance<App>.InitializeAsFirstInstance(Unique))
|
||||
if (SingleInstance<App>.InitializeAsFirstInstance())
|
||||
{
|
||||
using (var application = new App())
|
||||
{
|
||||
application.InitializeComponent();
|
||||
application.Run();
|
||||
}
|
||||
using var application = new App();
|
||||
application.InitializeComponent();
|
||||
application.Run();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnStartupAsync(object sender, StartupEventArgs e)
|
||||
#endregion
|
||||
|
||||
#region App Events
|
||||
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
|
||||
private async void OnStartup(object sender, StartupEventArgs e)
|
||||
{
|
||||
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
|
||||
{
|
||||
_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(
|
||||
"|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
||||
Log.SetLogLevel(_settings.LogLevel);
|
||||
|
||||
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
|
||||
|
||||
Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
|
||||
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
|
||||
|
||||
RegisterAppDomainExceptions();
|
||||
RegisterDispatcherUnhandledException();
|
||||
|
||||
var imageLoadertask = ImageLoader.InitializeAsync();
|
||||
|
||||
_settingsVM = new SettingWindowViewModel(_updater, _portable);
|
||||
_settings = _settingsVM.Settings;
|
||||
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
_alphabet.Initialize(_settings);
|
||||
_stringMatcher = new StringMatcher(_alphabet);
|
||||
StringMatcher.Instance = _stringMatcher;
|
||||
_stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
var window = new MainWindow(_settings, _mainVM);
|
||||
_mainWindow = new MainWindow();
|
||||
|
||||
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
||||
|
||||
Current.MainWindow = window;
|
||||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
// 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
|
||||
ThemeManager.Instance.Settings = _settings;
|
||||
ThemeManager.Instance.ChangeTheme(_settings.Theme);
|
||||
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
|
|
@ -108,11 +190,12 @@ namespace Flow.Launcher
|
|||
AutoUpdates();
|
||||
|
||||
API.SaveAppAllSettings();
|
||||
Log.Info(
|
||||
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
||||
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
|
||||
});
|
||||
}
|
||||
|
||||
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||
|
||||
private void AutoStartup()
|
||||
{
|
||||
// we try to enable auto-startup on first launch, or reenable if it was removed
|
||||
|
|
@ -121,20 +204,25 @@ namespace Flow.Launcher
|
|||
{
|
||||
try
|
||||
{
|
||||
Helper.AutoStartup.Enable();
|
||||
if (_settings.UseLogonTaskForStartup)
|
||||
{
|
||||
Helper.AutoStartup.EnableViaLogonTask();
|
||||
}
|
||||
else
|
||||
{
|
||||
Helper.AutoStartup.EnableViaRegistry();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// but if it fails (permissions, etc) then don't keep retrying
|
||||
// this also gives the user a visual indication in the Settings widget
|
||||
_settings.StartFlowLauncherOnSystemStartup = false;
|
||||
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
|
||||
e.Message);
|
||||
API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//[Conditional("RELEASE")]
|
||||
private void AutoUpdates()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
|
|
@ -143,20 +231,38 @@ namespace Flow.Launcher
|
|||
{
|
||||
// check update every 5 hours
|
||||
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
|
||||
await _updater.UpdateAppAsync(API);
|
||||
await Ioc.Default.GetRequiredService<Updater>().UpdateAppAsync();
|
||||
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
// check updates on startup
|
||||
await _updater.UpdateAppAsync(API);
|
||||
await Ioc.Default.GetRequiredService<Updater>().UpdateAppAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Register Events
|
||||
|
||||
private void RegisterExitEvents()
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
|
||||
Current.Exit += (s, e) => Dispose();
|
||||
Current.SessionEnding += (s, e) => Dispose();
|
||||
AppDomain.CurrentDomain.ProcessExit += (s, e) =>
|
||||
{
|
||||
Log.Info("|App.RegisterExitEvents|Process Exit");
|
||||
Dispose();
|
||||
};
|
||||
|
||||
Current.Exit += (s, e) =>
|
||||
{
|
||||
Log.Info("|App.RegisterExitEvents|Application Exit");
|
||||
Dispose();
|
||||
};
|
||||
|
||||
Current.SessionEnding += (s, e) =>
|
||||
{
|
||||
Log.Info("|App.RegisterExitEvents|Session Ending");
|
||||
Dispose();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -177,20 +283,68 @@ namespace Flow.Launcher
|
|||
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
|
||||
// but if sessionending is not called, exit won't be called when log off / shutdown
|
||||
if (!_disposed)
|
||||
// Prevent two disposes at the same time.
|
||||
lock (_disposingLock)
|
||||
{
|
||||
API.SaveAppAllSettings();
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close()
|
||||
// Accessing _mainWindow?.Dispatcher will cause the application stuck
|
||||
// So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher
|
||||
if (!_mainWindow.CanClose)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
_mainVM.Show();
|
||||
Ioc.Default.GetRequiredService<MainViewModel>().Show();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,14 +32,11 @@
|
|||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="4"
|
||||
Grid.Column="1"
|
||||
Click="BtnCancel_OnClick"
|
||||
Style="{StaticResource TitleBarCloseButtonStyle}">
|
||||
<Path
|
||||
|
|
@ -61,9 +58,9 @@
|
|||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="26,0,26,0">
|
||||
<StackPanel Margin="26 0 26 0">
|
||||
<TextBlock
|
||||
Margin="0,0,0,12"
|
||||
Margin="0 0 0 12"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource customeQueryHotkeyTitle}"
|
||||
|
|
@ -75,10 +72,10 @@
|
|||
TextWrapping="WrapWithOverflow" />
|
||||
<Image
|
||||
Width="478"
|
||||
Margin="0,20,0,0"
|
||||
Margin="0 20 0 0"
|
||||
Source="/Images/illustration_01.png" />
|
||||
|
||||
<Grid Width="478" Margin="0,20,0,0">
|
||||
<Grid Width="478" Margin="0 20 0 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
|
|
@ -102,12 +99,12 @@
|
|||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="10,0,10,0"
|
||||
Margin="10 0 10 0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
HotkeySettings="{Binding Settings}"
|
||||
DefaultHotkey="" />
|
||||
DefaultHotkey=""
|
||||
Type="CustomQueryHotkey" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
|
|
@ -127,8 +124,8 @@
|
|||
x:Name="btnTestActionKeyword"
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,10,0"
|
||||
Padding="10,5,10,5"
|
||||
Margin="0 0 10 0"
|
||||
Padding="10 5 10 5"
|
||||
Click="BtnTestActionKeyword_OnClick"
|
||||
Content="{DynamicResource preview}" />
|
||||
</Grid>
|
||||
|
|
@ -136,21 +133,21 @@
|
|||
</StackPanel>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="0,14,0,0"
|
||||
Margin="0 14 0 0"
|
||||
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0,1,0,0">
|
||||
BorderThickness="0 1 0 0">
|
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="btnCancel"
|
||||
MinWidth="140"
|
||||
Margin="10,0,5,0"
|
||||
Margin="10 0 5 0"
|
||||
Click="BtnCancel_OnClick"
|
||||
Content="{DynamicResource cancel}" />
|
||||
<Button
|
||||
x:Name="btnAdd"
|
||||
MinWidth="140"
|
||||
Margin="5,0,10,0"
|
||||
Margin="5 0 10 0"
|
||||
Click="btnAdd_OnClick"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
||||
|
|
|
|||
|
|
@ -6,21 +6,18 @@ using System.Linq;
|
|||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Controls;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class CustomQueryHotkeySetting : Window
|
||||
{
|
||||
private SettingWindow _settingWidow;
|
||||
private readonly Settings _settings;
|
||||
private bool update;
|
||||
private CustomPluginHotkey updateCustomHotkey;
|
||||
public Settings Settings { get; }
|
||||
|
||||
public CustomQueryHotkeySetting(SettingWindow settingWidow, Settings settings)
|
||||
public CustomQueryHotkeySetting(Settings settings)
|
||||
{
|
||||
_settingWidow = settingWidow;
|
||||
Settings = settings;
|
||||
_settings = settings;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
|
|
@ -33,13 +30,13 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (!update)
|
||||
{
|
||||
Settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
|
||||
_settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
|
||||
|
||||
var pluginHotkey = new CustomPluginHotkey
|
||||
{
|
||||
Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text
|
||||
};
|
||||
Settings.CustomPluginHotkeys.Add(pluginHotkey);
|
||||
_settings.CustomPluginHotkeys.Add(pluginHotkey);
|
||||
|
||||
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
|
||||
}
|
||||
|
|
@ -59,11 +56,11 @@ namespace Flow.Launcher
|
|||
|
||||
public void UpdateItem(CustomPluginHotkey item)
|
||||
{
|
||||
updateCustomHotkey = Settings.CustomPluginHotkeys.FirstOrDefault(o =>
|
||||
updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o =>
|
||||
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
|
||||
if (updateCustomHotkey == null)
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
|
||||
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("invalidPluginHotkey"));
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
|
@ -77,8 +74,7 @@ namespace Flow.Launcher
|
|||
private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
App.API.ChangeQuery(tbAction.Text);
|
||||
Application.Current.MainWindow.Show();
|
||||
Application.Current.MainWindow.Opacity = 1;
|
||||
App.API.ShowMainWindow();
|
||||
Application.Current.MainWindow.Focus();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,14 +30,11 @@
|
|||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="4"
|
||||
Grid.Column="1"
|
||||
Click="BtnCancel_OnClick"
|
||||
Style="{StaticResource TitleBarCloseButtonStyle}">
|
||||
<Path
|
||||
|
|
|
|||
|
|
@ -1,31 +1,35 @@
|
|||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Flow.Launcher.SettingPages.ViewModels;
|
||||
using Flow.Launcher.Core;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class CustomShortcutSetting : Window
|
||||
{
|
||||
private readonly SettingsPaneHotkeyViewModel _hotkeyVm;
|
||||
public string Key { get; set; } = String.Empty;
|
||||
public string Value { get; set; } = String.Empty;
|
||||
private string originalKey { get; init; } = null;
|
||||
private string originalValue { get; init; } = null;
|
||||
private bool update { get; init; } = false;
|
||||
private string originalKey { get; } = null;
|
||||
private string originalValue { get; } = null;
|
||||
private bool update { get; } = false;
|
||||
|
||||
public CustomShortcutSetting(SettingWindowViewModel vm)
|
||||
public CustomShortcutSetting(SettingsPaneHotkeyViewModel vm)
|
||||
{
|
||||
_hotkeyVm = vm;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public CustomShortcutSetting(string key, string value)
|
||||
public CustomShortcutSetting(string key, string value, SettingsPaneHotkeyViewModel vm)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
originalKey = key;
|
||||
originalValue = value;
|
||||
update = true;
|
||||
_hotkeyVm = vm;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
|
|
@ -39,13 +43,13 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
|
||||
{
|
||||
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
|
||||
App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
DialogResult = !update || originalKey != Key || originalValue != Value;
|
||||
|
|
@ -61,8 +65,7 @@ namespace Flow.Launcher
|
|||
private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
App.API.ChangeQuery(tbExpand.Text);
|
||||
Application.Current.MainWindow.Show();
|
||||
Application.Current.MainWindow.Opacity = 1;
|
||||
App.API.ShowMainWindow();
|
||||
Application.Current.MainWindow.Focus();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<UseWindowsForms>false</UseWindowsForms>
|
||||
<StartupObject>Flow.Launcher.App</StartupObject>
|
||||
<ApplicationIcon>Resources\app.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
|
|
@ -83,20 +83,25 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageReference Include="Fody" Version="6.5.4">
|
||||
<PackageReference Include="ChefKeys" Version="0.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Fody" Version="6.5.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<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" />
|
||||
<!-- ModernWpfUI v0.9.5 introduced WinRT changes that causes Notification platform unavailable error on some machines -->
|
||||
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
|
||||
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
|
||||
<PackageReference Include="NHotkey.Wpf" Version="2.1.1" />
|
||||
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="SharpVectors" Version="1.8.2" />
|
||||
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.8" />
|
||||
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
|
||||
<PackageReference Include="TaskScheduler" Version="2.12.1" />
|
||||
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,31 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32.TaskScheduler;
|
||||
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public class AutoStartup
|
||||
{
|
||||
private const string StartupPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
|
||||
private const string LogonTaskName = $"{Constant.FlowLauncher} Startup";
|
||||
private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup";
|
||||
|
||||
public static bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
// Check if logon task is enabled
|
||||
if (CheckLogonTask())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if registry is enabled
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
|
|
@ -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
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.DeleteValue(Constant.FlowLauncher, false);
|
||||
if (logonTask)
|
||||
{
|
||||
UnscheduleLogonTask();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.DeleteValue(Constant.FlowLauncher, false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -42,12 +117,19 @@ public class AutoStartup
|
|||
}
|
||||
}
|
||||
|
||||
internal static void Enable()
|
||||
private static void Enable(bool logonTask)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
||||
if (logonTask)
|
||||
{
|
||||
ScheduleLogonTask();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true);
|
||||
key?.SetValue(Constant.FlowLauncher, $"\"{Constant.ExecutablePath}\"");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -55,4 +137,54 @@ public class AutoStartup
|
|||
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.Wpf;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using System.Windows;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using ChefKeys;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
|
|
@ -14,10 +16,10 @@ internal static class HotKeyMapper
|
|||
private static Settings _settings;
|
||||
private static MainViewModel _mainViewModel;
|
||||
|
||||
internal static void Initialize(MainViewModel mainVM)
|
||||
internal static void Initialize()
|
||||
{
|
||||
_mainViewModel = mainVM;
|
||||
_settings = _mainViewModel.Settings;
|
||||
_mainViewModel = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||
_settings = Ioc.Default.GetService<Settings>();
|
||||
|
||||
SetHotkey(_settings.Hotkey, OnToggleHotkey);
|
||||
LoadCustomPluginHotkey();
|
||||
|
|
@ -29,33 +31,92 @@ internal static class HotKeyMapper
|
|||
_mainViewModel.ToggleFlowLauncher();
|
||||
}
|
||||
|
||||
internal static void OnToggleHotkeyWithChefKeys()
|
||||
{
|
||||
if (!_mainViewModel.ShouldIgnoreHotkeys())
|
||||
_mainViewModel.ToggleFlowLauncher();
|
||||
}
|
||||
|
||||
private static void SetHotkey(string hotkeyStr, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
var hotkey = new HotkeyModel(hotkeyStr);
|
||||
SetHotkey(hotkey, action);
|
||||
}
|
||||
|
||||
private static void SetWithChefKeys(string hotkeyStr)
|
||||
{
|
||||
try
|
||||
{
|
||||
ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, OnToggleHotkeyWithChefKeys);
|
||||
ChefKeysManager.Start();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
|
||||
e.Message,
|
||||
e.StackTrace));
|
||||
string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
||||
string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
||||
MessageBoxEx.Show(errorMsg, errorMsgTitle);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> action)
|
||||
{
|
||||
string hotkeyStr = hotkey.ToString();
|
||||
try
|
||||
{
|
||||
if (hotkeyStr == "LWin" || hotkeyStr == "RWin")
|
||||
{
|
||||
SetWithChefKeys(hotkeyStr);
|
||||
return;
|
||||
}
|
||||
|
||||
HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
|
||||
}
|
||||
catch (Exception)
|
||||
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 errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle");
|
||||
MessageBox.Show(errorMsg,errorMsgTitle);
|
||||
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO.Pipes;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
|
@ -14,177 +8,11 @@ using System.Windows;
|
|||
// modified to allow single instace restart
|
||||
namespace Flow.Launcher.Helper
|
||||
{
|
||||
internal enum WM
|
||||
public interface ISingleInstanceApp
|
||||
{
|
||||
NULL = 0x0000,
|
||||
CREATE = 0x0001,
|
||||
DESTROY = 0x0002,
|
||||
MOVE = 0x0003,
|
||||
SIZE = 0x0005,
|
||||
ACTIVATE = 0x0006,
|
||||
SETFOCUS = 0x0007,
|
||||
KILLFOCUS = 0x0008,
|
||||
ENABLE = 0x000A,
|
||||
SETREDRAW = 0x000B,
|
||||
SETTEXT = 0x000C,
|
||||
GETTEXT = 0x000D,
|
||||
GETTEXTLENGTH = 0x000E,
|
||||
PAINT = 0x000F,
|
||||
CLOSE = 0x0010,
|
||||
QUERYENDSESSION = 0x0011,
|
||||
QUIT = 0x0012,
|
||||
QUERYOPEN = 0x0013,
|
||||
ERASEBKGND = 0x0014,
|
||||
SYSCOLORCHANGE = 0x0015,
|
||||
SHOWWINDOW = 0x0018,
|
||||
ACTIVATEAPP = 0x001C,
|
||||
SETCURSOR = 0x0020,
|
||||
MOUSEACTIVATE = 0x0021,
|
||||
CHILDACTIVATE = 0x0022,
|
||||
QUEUESYNC = 0x0023,
|
||||
GETMINMAXINFO = 0x0024,
|
||||
|
||||
WINDOWPOSCHANGING = 0x0046,
|
||||
WINDOWPOSCHANGED = 0x0047,
|
||||
|
||||
CONTEXTMENU = 0x007B,
|
||||
STYLECHANGING = 0x007C,
|
||||
STYLECHANGED = 0x007D,
|
||||
DISPLAYCHANGE = 0x007E,
|
||||
GETICON = 0x007F,
|
||||
SETICON = 0x0080,
|
||||
NCCREATE = 0x0081,
|
||||
NCDESTROY = 0x0082,
|
||||
NCCALCSIZE = 0x0083,
|
||||
NCHITTEST = 0x0084,
|
||||
NCPAINT = 0x0085,
|
||||
NCACTIVATE = 0x0086,
|
||||
GETDLGCODE = 0x0087,
|
||||
SYNCPAINT = 0x0088,
|
||||
NCMOUSEMOVE = 0x00A0,
|
||||
NCLBUTTONDOWN = 0x00A1,
|
||||
NCLBUTTONUP = 0x00A2,
|
||||
NCLBUTTONDBLCLK = 0x00A3,
|
||||
NCRBUTTONDOWN = 0x00A4,
|
||||
NCRBUTTONUP = 0x00A5,
|
||||
NCRBUTTONDBLCLK = 0x00A6,
|
||||
NCMBUTTONDOWN = 0x00A7,
|
||||
NCMBUTTONUP = 0x00A8,
|
||||
NCMBUTTONDBLCLK = 0x00A9,
|
||||
|
||||
SYSKEYDOWN = 0x0104,
|
||||
SYSKEYUP = 0x0105,
|
||||
SYSCHAR = 0x0106,
|
||||
SYSDEADCHAR = 0x0107,
|
||||
COMMAND = 0x0111,
|
||||
SYSCOMMAND = 0x0112,
|
||||
|
||||
MOUSEMOVE = 0x0200,
|
||||
LBUTTONDOWN = 0x0201,
|
||||
LBUTTONUP = 0x0202,
|
||||
LBUTTONDBLCLK = 0x0203,
|
||||
RBUTTONDOWN = 0x0204,
|
||||
RBUTTONUP = 0x0205,
|
||||
RBUTTONDBLCLK = 0x0206,
|
||||
MBUTTONDOWN = 0x0207,
|
||||
MBUTTONUP = 0x0208,
|
||||
MBUTTONDBLCLK = 0x0209,
|
||||
MOUSEWHEEL = 0x020A,
|
||||
XBUTTONDOWN = 0x020B,
|
||||
XBUTTONUP = 0x020C,
|
||||
XBUTTONDBLCLK = 0x020D,
|
||||
MOUSEHWHEEL = 0x020E,
|
||||
|
||||
|
||||
CAPTURECHANGED = 0x0215,
|
||||
|
||||
ENTERSIZEMOVE = 0x0231,
|
||||
EXITSIZEMOVE = 0x0232,
|
||||
|
||||
IME_SETCONTEXT = 0x0281,
|
||||
IME_NOTIFY = 0x0282,
|
||||
IME_CONTROL = 0x0283,
|
||||
IME_COMPOSITIONFULL = 0x0284,
|
||||
IME_SELECT = 0x0285,
|
||||
IME_CHAR = 0x0286,
|
||||
IME_REQUEST = 0x0288,
|
||||
IME_KEYDOWN = 0x0290,
|
||||
IME_KEYUP = 0x0291,
|
||||
|
||||
NCMOUSELEAVE = 0x02A2,
|
||||
|
||||
DWMCOMPOSITIONCHANGED = 0x031E,
|
||||
DWMNCRENDERINGCHANGED = 0x031F,
|
||||
DWMCOLORIZATIONCOLORCHANGED = 0x0320,
|
||||
DWMWINDOWMAXIMIZEDCHANGE = 0x0321,
|
||||
|
||||
#region Windows 7
|
||||
DWMSENDICONICTHUMBNAIL = 0x0323,
|
||||
DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326,
|
||||
#endregion
|
||||
|
||||
USER = 0x0400,
|
||||
|
||||
// This is the hard-coded message value used by WinForms for Shell_NotifyIcon.
|
||||
// It's relatively safe to reuse.
|
||||
TRAYMOUSEMESSAGE = 0x800, //WM_USER + 1024
|
||||
APP = 0x8000
|
||||
void OnSecondAppStarted();
|
||||
}
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
internal static class NativeMethods
|
||||
{
|
||||
/// <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>
|
||||
/// This class checks to make sure that only one instance of
|
||||
/// 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.
|
||||
/// For most apps, this will not be much of an issue.
|
||||
/// </remarks>
|
||||
public static class SingleInstance<TApplication>
|
||||
where TApplication: Application , ISingleInstanceApp
|
||||
|
||||
public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
|
|
@ -211,15 +37,12 @@ namespace Flow.Launcher.Helper
|
|||
/// Suffix to the channel name.
|
||||
/// </summary>
|
||||
private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
|
||||
private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex";
|
||||
|
||||
/// <summary>
|
||||
/// Application mutex.
|
||||
/// </summary>
|
||||
internal static Mutex singleInstanceMutex;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Properties
|
||||
internal static Mutex SingleInstanceMutex { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -230,24 +53,23 @@ namespace Flow.Launcher.Helper
|
|||
/// If not, activates the first instance.
|
||||
/// </summary>
|
||||
/// <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.
|
||||
string applicationIdentifier = uniqueName + Environment.UserName;
|
||||
string applicationIdentifier = InstanceMutexName + Environment.UserName;
|
||||
|
||||
string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
|
||||
string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
|
||||
|
||||
// Create mutex based on unique application Id to check if this is the first instance of the application.
|
||||
bool firstInstance;
|
||||
singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance);
|
||||
SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance);
|
||||
if (firstInstance)
|
||||
{
|
||||
_ = CreateRemoteService(channelName);
|
||||
_ = CreateRemoteServiceAsync(channelName);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = SignalFirstInstance(channelName);
|
||||
_ = SignalFirstInstanceAsync(channelName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -257,84 +79,31 @@ namespace Flow.Launcher.Helper
|
|||
/// </summary>
|
||||
public static void Cleanup()
|
||||
{
|
||||
singleInstanceMutex?.ReleaseMutex();
|
||||
SingleInstanceMutex?.ReleaseMutex();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#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>
|
||||
/// Creates a remote server pipe for communication.
|
||||
/// Once receives signal from client, will activate first instance.
|
||||
/// </summary>
|
||||
/// <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();
|
||||
if (Application.Current != null)
|
||||
{
|
||||
// Do an asynchronous call to ActivateFirstInstance function
|
||||
Application.Current.Dispatcher.Invoke(ActivateFirstInstance);
|
||||
}
|
||||
// Disconect client
|
||||
pipeServer.Disconnect();
|
||||
}
|
||||
// Wait for connection to the pipe
|
||||
await pipeServer.WaitForConnectionAsync();
|
||||
|
||||
// Do an asynchronous call to ActivateFirstInstance function
|
||||
Application.Current?.Dispatcher.Invoke(ActivateFirstInstance);
|
||||
|
||||
// Disconect client
|
||||
pipeServer.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -345,25 +114,13 @@ namespace Flow.Launcher.Helper
|
|||
/// <param name="args">
|
||||
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
|
||||
/// </param>
|
||||
private static async Task SignalFirstInstance(string channelName)
|
||||
private static async Task SignalFirstInstanceAsync(string channelName)
|
||||
{
|
||||
// Create a client pipe connected to server
|
||||
using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out))
|
||||
{
|
||||
// Connect to the available pipe
|
||||
await pipeClient.ConnectAsync(0);
|
||||
}
|
||||
}
|
||||
using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out);
|
||||
|
||||
/// <summary>
|
||||
/// Callback for activating first instance of the application.
|
||||
/// </summary>
|
||||
/// <param name="arg">Callback argument.</param>
|
||||
/// <returns>Always null.</returns>
|
||||
private static object ActivateFirstInstanceCallback(object o)
|
||||
{
|
||||
ActivateFirstInstance();
|
||||
return null;
|
||||
// Connect to the available pipe
|
||||
await pipeClient.ConnectAsync(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -10,16 +10,29 @@ public static class SingletonWindowOpener
|
|||
{
|
||||
var window = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.GetType() == typeof(T))
|
||||
?? (T)Activator.CreateInstance(typeof(T), args);
|
||||
|
||||
|
||||
// Fix UI bug
|
||||
// Add `window.WindowState = WindowState.Normal`
|
||||
// If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
|
||||
// Not sure why this works tho
|
||||
// Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
|
||||
// https://stackoverflow.com/a/59719760/4230390
|
||||
window.WindowState = WindowState.Normal;
|
||||
window.Show();
|
||||
|
||||
// Ensure the window is not minimized before showing it
|
||||
if (window.WindowState == WindowState.Minimized)
|
||||
{
|
||||
window.WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
// Ensure the window is visible
|
||||
if (!window.IsVisible)
|
||||
{
|
||||
window.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
window.Activate(); // Bring the window to the foreground if already open
|
||||
}
|
||||
|
||||
window.Focus();
|
||||
|
||||
return (T)window;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue