Merge branch 'dev' into ChannelSelect

This commit is contained in:
Jack Ye 2025-10-06 10:37:42 +08:00 committed by GitHub
commit fe9669721f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
504 changed files with 23588 additions and 14778 deletions

View file

@ -6,3 +6,6 @@ runcount
Firefox
Português
Português (Brasil)
favicons
moz
workaround

View file

@ -1,3 +1,5 @@
# This file should contain names of products, companies, or individuals that aren't in a standard dictionary (e.g., GitHub, Keptn, VSCode).
crowdin
DWM
workflows
@ -34,7 +36,6 @@ mscorlib
pythonw
dotnet
winget
jjw24
wolframalpha
gmail
duckduckgo
@ -49,7 +50,6 @@ srchadmin
EWX
dlgtext
CMD
appref-ms
appref
TSource
runas
@ -57,7 +57,6 @@ dpi
popup
ptr
pluginindicator
TobiasSekan
img
resx
bak
@ -68,9 +67,6 @@ dlg
ddd
dddd
clearlogfolder
ACCENT_ENABLE_TRANSPARENTGRADIENT
ACCENT_ENABLE_BLURBEHIND
WCA_ACCENT_POLICY
HGlobal
dopusrt
firefox
@ -91,22 +87,24 @@ keyevent
KListener
requery
vkcode
čeština
Polski
Srpski
Português
Português (Brasil)
Italiano
Slovenský
quicklook
Tiếng Việt
Droplex
Preinstalled
errormetadatafile
noresult
pluginsmanager
alreadyexists
JsonRPC
JsonRPCV2
Softpedia
img
Reloadable
metadatas
WMP
VSTHRD
CJK
Msix
dummyprofile
browserbookmark
copyurl

View file

@ -1,4 +1,6 @@
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
# This file should contain strings that contain a mix of letters and numbers, or specific symbols
# Questionably acceptable forms of `in to`
# Personally, I prefer `log into`, but people object
@ -121,3 +123,28 @@
# version suffix <word>v#
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
# Non-English
[a-zA-Z]*[ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*
\bjjw24\b
\bappref-ms\b
\bTobiasSekan\b
\bJsonRPC\b
\bJsonRPCV2\b
\bTiếng Việt\b
\bPortuguês (Brasil)\b
\bčeština\b
\bPortuguês\b
\bIoc\b
\bXiao\s*He\b
\bZi\s*Ran\s*Ma\b
\bWei\s*Ruan\b
\bZhi\s*Neng\s*ABC\b
\bZi\s*Guang\s*Pin\s*Yin\b
\bPin\s*Yin\s*Jia\s*Jia\b
\bXing\s*Kong\s*Jian\s*Dao\b
\bDa\s*Niu\b
\bXiao\s*Lang\b
\b[Ss]ettings [Ss]ettings\b

View file

@ -1,11 +1,12 @@
from os import getenv
from typing import Optional
import requests
def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: str = "all") -> list[dict]:
"""
Fetches pull requests from a GitHub repository that match a given milestone and label.
Fetches pull requests from a GitHub repository that match a given label and state.
Args:
token (str): GitHub token.
@ -23,39 +24,10 @@ def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: st
"Accept": "application/vnd.github.v3+json",
}
milestone_id = None
milestone_url = f"https://api.github.com/repos/{owner}/{repo}/milestones"
params = {"state": "open"}
try:
response = requests.get(milestone_url, headers=headers, params=params)
response.raise_for_status()
milestones = response.json()
if len(milestones) > 2:
print("More than two milestones found, unable to determine the milestone required.")
exit(1)
# milestones.pop()
for ms in milestones:
if ms["title"] != "Future":
milestone_id = ms["number"]
print(f"Gathering PRs with milestone {ms['title']}...")
break
if not milestone_id:
print(f"No suitable milestone found in repository '{owner}/{repo}'.")
exit(1)
except requests.exceptions.RequestException as e:
print(f"Error fetching milestones: {e}")
exit(1)
# This endpoint allows filtering by milestone and label. A PR in GH's perspective is a type of issue.
# This endpoint allows filtering by label(and milestone). A PR in GH's perspective is a type of issue.
prs_url = f"https://api.github.com/repos/{owner}/{repo}/issues"
params = {
"state": state,
"milestone": milestone_id,
"labels": label,
"per_page": 100,
}
@ -83,7 +55,9 @@ def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: st
return all_prs
def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all") -> list[dict]:
def get_prs(
pull_request_items: list[dict], label: str = "", state: str = "all", milestone_title: Optional[str] = None
) -> list[dict]:
"""
Returns a list of pull requests after applying the label and state filters.
@ -91,6 +65,8 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all")
pull_request_items (list[dict]): List of PR items.
label (str): The label name. Filter is not applied when empty string.
state (str): State of PR, e.g. open, closed, all
milestone_title (Optional[str]): The milestone title to filter by. This is the milestone number you created
in GitHub, e.g. '1.20.0'. If None, no milestone filtering is applied.
Returns:
list: A list of dictionaries, where each dictionary represents a pull request.
@ -99,22 +75,32 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all")
pr_list = []
count = 0
for pr in pull_request_items:
if state in [pr["state"], "all"] and (not label or [item for item in pr["labels"] if item["name"] == label]):
pr_list.append(pr)
count += 1
if state not in [pr["state"], "all"]:
continue
print(f"Found {count} PRs with {label if label else 'no filter on'} label and state as {state}")
if label and not [item for item in pr["labels"] if item["name"] == label]:
continue
if milestone_title:
if pr["milestone"] is None or pr["milestone"]["title"] != milestone_title:
continue
pr_list.append(pr)
count += 1
print(
f"Found {count} PRs with {label if label else 'no filter on'} label, state as {state}, and milestone {milestone_title if milestone_title else "any"}"
)
return pr_list
def get_prs_assignees(pull_request_items: list[dict], label: str = "", state: str = "all") -> list[str]:
def get_prs_assignees(pull_request_items: list[dict]) -> list[str]:
"""
Returns a list of pull request assignees after applying the label and state filters, excludes jjw24.
Returns a list of pull request assignees, excludes jjw24.
Args:
pull_request_items (list[dict]): List of PR items.
label (str): The label name. Filter is not applied when empty string.
state (str): State of PR, e.g. open, closed, all
pull_request_items (list[dict]): List of PR items to get the assignees from.
Returns:
list: A list of strs, where each string is an assignee name. List is not distinct, so can contain
@ -123,13 +109,13 @@ def get_prs_assignees(pull_request_items: list[dict], label: str = "", state: st
"""
assignee_list = []
for pr in pull_request_items:
if state in [pr["state"], "all"] and (not label or [item for item in pr["labels"] if item["name"] == label]):
[assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24" ]
[assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24"]
print(f"Found {len(assignee_list)} assignees with {label if label else 'no filter on'} label and state as {state}")
print(f"Found {len(assignee_list)} assignees")
return assignee_list
def get_pr_descriptions(pull_request_items: list[dict]) -> str:
"""
Returns the concatenated string of pr title and number in the format of
@ -207,15 +193,16 @@ if __name__ == "__main__":
print(f"Fetching {state} PRs for {repository_owner}/{repository_name} ...")
pull_requests = get_github_prs(github_token, repository_owner, repository_name)
# First, get all PRs to find the release PR and determine the milestone
all_pull_requests = get_github_prs(github_token, repository_owner, repository_name)
if not pull_requests:
print("No matching pull requests found")
if not all_pull_requests:
print("No pull requests found")
exit(1)
print(f"\nFound total of {len(pull_requests)} pull requests")
print(f"\nFound total of {len(all_pull_requests)} pull requests")
release_pr = get_prs(pull_requests, "release", "open")
release_pr = get_prs(all_pull_requests, "release", "open")
if len(release_pr) != 1:
print(f"Unable to find the exact release PR. Returned result: {release_pr}")
@ -223,14 +210,25 @@ if __name__ == "__main__":
print(f"Found release PR: {release_pr[0]['title']}")
enhancement_prs = get_prs(pull_requests, "enhancement", "closed")
bug_fix_prs = get_prs(pull_requests, "bug", "closed")
release_milestone_title = release_pr[0].get("milestone", {}).get("title", None)
if not release_milestone_title:
print("Release PR does not have a milestone assigned.")
exit(1)
print(f"Using milestone number: {release_milestone_title}")
enhancement_prs = get_prs(all_pull_requests, "enhancement", "closed", release_milestone_title)
bug_fix_prs = get_prs(all_pull_requests, "bug", "closed", release_milestone_title)
if len(enhancement_prs) == 0 and len(bug_fix_prs) == 0:
print(f"No PRs with {release_milestone_title} milestone were found")
description_content = "# Release notes\n"
description_content += f"## Features\n{get_pr_descriptions(enhancement_prs)}" if enhancement_prs else ""
description_content += f"## Bug fixes\n{get_pr_descriptions(bug_fix_prs)}" if bug_fix_prs else ""
assignees = list(set(get_prs_assignees(pull_requests, "enhancement", "closed") + get_prs_assignees(pull_requests, "bug", "closed")))
assignees = list(set(get_prs_assignees(enhancement_prs) + get_prs_assignees(bug_fix_prs)))
assignees.sort(key=str.lower)
description_content += f"### Authors:\n{', '.join(assignees)}"

View file

@ -10,11 +10,11 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: 7.0.x
dotnet-version: 9.0.x
- name: Update Plugins To Production Version
run: |
@ -42,7 +42,7 @@ jobs:
- name: Build BrowserBookmark
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
7z a -tzip "Flow.Launcher.Plugin.BrowserBookmark.zip" "./Flow.Launcher.Plugin.BrowserBookmark/*"
rm -r "Flow.Launcher.Plugin.BrowserBookmark"
@ -66,7 +66,7 @@ jobs:
- name: Build Calculator
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
7z a -tzip "Flow.Launcher.Plugin.Calculator.zip" "./Flow.Launcher.Plugin.Calculator/*"
rm -r "Flow.Launcher.Plugin.Calculator"
@ -90,7 +90,7 @@ jobs:
- name: Build Explorer
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
7z a -tzip "Flow.Launcher.Plugin.Explorer.zip" "./Flow.Launcher.Plugin.Explorer/*"
rm -r "Flow.Launcher.Plugin.Explorer"
@ -114,7 +114,7 @@ jobs:
- name: Build PluginIndicator
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
7z a -tzip "Flow.Launcher.Plugin.PluginIndicator.zip" "./Flow.Launcher.Plugin.PluginIndicator/*"
rm -r "Flow.Launcher.Plugin.PluginIndicator"
@ -138,7 +138,7 @@ jobs:
- name: Build PluginsManager
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
7z a -tzip "Flow.Launcher.Plugin.PluginsManager.zip" "./Flow.Launcher.Plugin.PluginsManager/*"
rm -r "Flow.Launcher.Plugin.PluginsManager"
@ -162,7 +162,7 @@ jobs:
- name: Build ProcessKiller
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
7z a -tzip "Flow.Launcher.Plugin.ProcessKiller.zip" "./Flow.Launcher.Plugin.ProcessKiller/*"
rm -r "Flow.Launcher.Plugin.ProcessKiller"
@ -186,7 +186,7 @@ jobs:
- name: Build Program
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net7.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net9.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
7z a -tzip "Flow.Launcher.Plugin.Program.zip" "./Flow.Launcher.Plugin.Program/*"
rm -r "Flow.Launcher.Plugin.Program"
@ -210,7 +210,7 @@ jobs:
- name: Build Shell
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
7z a -tzip "Flow.Launcher.Plugin.Shell.zip" "./Flow.Launcher.Plugin.Shell/*"
rm -r "Flow.Launcher.Plugin.Shell"
@ -234,7 +234,7 @@ jobs:
- name: Build Sys
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
7z a -tzip "Flow.Launcher.Plugin.Sys.zip" "./Flow.Launcher.Plugin.Sys/*"
rm -r "Flow.Launcher.Plugin.Sys"
@ -258,7 +258,7 @@ jobs:
- name: Build Url
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
7z a -tzip "Flow.Launcher.Plugin.Url.zip" "./Flow.Launcher.Plugin.Url/*"
rm -r "Flow.Launcher.Plugin.Url"
@ -282,7 +282,7 @@ jobs:
- name: Build WebSearch
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
7z a -tzip "Flow.Launcher.Plugin.WebSearch.zip" "./Flow.Launcher.Plugin.WebSearch/*"
rm -r "Flow.Launcher.Plugin.WebSearch"
@ -306,7 +306,7 @@ jobs:
- name: Build WindowsSettings
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
7z a -tzip "Flow.Launcher.Plugin.WindowsSettings.zip" "./Flow.Launcher.Plugin.WindowsSettings/*"
rm -r "Flow.Launcher.Plugin.WindowsSettings"

View file

@ -16,11 +16,11 @@ jobs:
runs-on: windows-latest
env:
FlowVersion: 1.19.5
FlowVersion: 1.20.2
NUGET_CERT_REVOCATION_MODE: offline
BUILD_NUMBER: ${{ github.run_number }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set Flow.Launcher.csproj version
id: update
uses: vers-one/dotnet-project-version-updater@v1.7
@ -29,9 +29,9 @@ jobs:
"**/SolutionAssemblyInfo.cs"
version: ${{ env.FlowVersion }}.${{ env.BUILD_NUMBER }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: 7.0.x
dotnet-version: 9.0.x
# cache: true
# cache-dependency-path: |
# Flow.Launcher/packages.lock.json

View file

@ -11,9 +11,9 @@ jobs:
update-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.x"

View file

@ -72,7 +72,7 @@ jobs:
steps:
- name: check-spelling
id: spelling
uses: check-spelling/check-spelling@prerelease
uses: check-spelling/check-spelling@v0.0.25
with:
suppress_push_for_open_pull_request: 1
checkout: true
@ -128,7 +128,7 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
uses: check-spelling/check-spelling@prerelease
uses: check-spelling/check-spelling@v0.0.25
with:
checkout: true
spell_check_this: check-spelling/spell-check-this@main

View file

@ -18,7 +18,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
stale-issue-message: 'This issue is stale because it has been open ${{ env.days-before-stale }} days with no activity. Remove stale label or comment or this will be closed in ${{ env.days-before-stale }} days.\n\nAlternatively this issue can be kept open by adding one of the following labels:\n${{ env.exempt-issue-labels }}'
days-before-stale: ${{ env.days-before-stale }}

View file

@ -1,5 +1,7 @@
<Project>
<PropertyGroup>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<!-- Work around https://github.com/dotnet/runtime/issues/109682 -->
<CETCompat>false</CETCompat>
</PropertyGroup>
</Project>

View file

@ -3,10 +3,8 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.Win32;
using Squirrel;
@ -17,8 +15,6 @@ namespace Flow.Launcher.Core.Configuration
{
private static readonly string ClassName = nameof(Portable);
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
/// <summary>
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
/// </summary>
@ -45,14 +41,13 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.PortableDataPath);
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");
PublicApi.Instance.ShowMsgBox(Localize.restartToDisablePortableMode());
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
catch (Exception e)
{
API.LogException(ClassName, "Error occurred while disabling portable mode", e);
PublicApi.Instance.LogException(ClassName, "Error occurred while disabling portable mode", e);
}
}
@ -69,14 +64,13 @@ namespace Flow.Launcher.Core.Configuration
#endif
IndicateDeletion(DataLocation.RoamingDataPath);
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");
PublicApi.Instance.ShowMsgBox(Localize.restartToEnablePortableMode());
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
catch (Exception e)
{
API.LogException(ClassName, "Error occurred while enabling portable mode", e);
PublicApi.Instance.LogException(ClassName, "Error occurred while enabling portable mode", e);
}
}
@ -96,13 +90,13 @@ namespace Flow.Launcher.Core.Configuration
public void MoveUserDataFolder(string fromLocation, string toLocation)
{
FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
FilesFolders.CopyAll(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s));
VerifyUserDataAfterMove(fromLocation, toLocation);
}
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
{
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s));
}
public void CreateShortcuts()
@ -152,13 +146,12 @@ namespace Flow.Launcher.Core.Configuration
// delete it and prompt the user to pick the portable data location
if (File.Exists(roamingDataDeleteFilePath))
{
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => PublicApi.Instance.ShowMsgBox(s));
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)
if (PublicApi.Instance.ShowMsgBox(Localize.moveToDifferentLocation(),
string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
FilesFolders.OpenPath(Constant.RootDirectory, (s) => PublicApi.Instance.ShowMsgBox(s));
Environment.Exit(0);
}
@ -167,10 +160,9 @@ namespace Flow.Launcher.Core.Configuration
// delete it and notify the user about it.
else if (File.Exists(portableDataDeleteFilePath))
{
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => PublicApi.Instance.ShowMsgBox(s));
API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " +
"the relevant shortcuts and uninstaller entry have been created");
PublicApi.Instance.ShowMsgBox(Localize.shortcutsUninstallerCreated());
}
}
@ -181,9 +173,7 @@ namespace Flow.Launcher.Core.Configuration
if (roamingLocationExists && portableLocationExists)
{
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));
PublicApi.Instance.ShowMsgBox(Localize.userDataDuplicated(DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
return false;
}

View file

@ -8,7 +8,6 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Plugin;
@ -18,13 +17,9 @@ namespace Flow.Launcher.Core.ExternalPlugins
{
private static readonly string ClassName = nameof(CommunityPluginSource);
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
private string latestEtag = "";
private List<UserPlugin> plugins = new();
private List<UserPlugin> plugins = [];
private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new()
{
@ -41,7 +36,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
/// </remarks>
public async Task<List<UserPlugin>> FetchAsync(CancellationToken token)
{
API.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
PublicApi.Instance.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl);
@ -59,29 +54,40 @@ namespace Flow.Launcher.Core.ExternalPlugins
.ConfigureAwait(false);
latestEtag = response.Headers.ETag?.Tag;
API.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
PublicApi.Instance.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
return plugins;
}
else if (response.StatusCode == HttpStatusCode.NotModified)
{
API.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
PublicApi.Instance.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
return plugins;
}
else
{
API.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
PublicApi.Instance.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
return null;
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
PublicApi.Instance.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller.");
return null;
}
catch (TaskCanceledException)
{
// Likely an HttpClient timeout or external cancellation not requested by our token
PublicApi.Instance.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out.");
return null;
}
catch (Exception e)
{
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
{
API.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
PublicApi.Instance.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
}
else
{
API.LogException(ClassName, "Error Occurred", e);
PublicApi.Instance.LogException(ClassName, "Error Occurred", e);
}
return null;
}

View file

@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -15,7 +14,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
private static readonly string ClassName = nameof(AbstractPluginEnvironment);
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
protected readonly IPublicAPI API = PublicApi.Instance;
internal abstract string Language { get; }
@ -58,15 +57,10 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
}
var noRuntimeMessage = string.Format(
API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
Language,
EnvName,
Environment.NewLine
);
var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine);
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{
var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
var msg = Localize.runtimePluginChooseRuntimeExecutable(EnvName);
var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
@ -77,12 +71,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
// Nothing selected because user pressed cancel from the file dialog window
else
{
var forceDownloadMessage = string.Format(
API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
Language,
EnvName,
Environment.NewLine
);
var forceDownloadMessage = Localize.runtimeExecutableInvalidChooseDownload(Language, EnvName, Environment.NewLine);
// Let users select valid path or choose to download
while (string.IsNullOrEmpty(selectedFile))
@ -120,7 +109,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
}
else
{
API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
API.ShowMsgBox(Localize.runtimePluginUnableToSetExecutablePath(Language));
API.LogError(ClassName,
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment");
@ -248,7 +237,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
private static string GetUpdatedEnvironmentPath(string filePath)
{
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
// get the substring after "Environments" because we can not determine it dynamically
var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";

View file

@ -11,6 +11,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class PythonEnvironment : AbstractPluginEnvironment
{
private static readonly string ClassName = nameof(PythonEnvironment);
internal override string Language => AllowedLanguage.Python;
internal override string EnvName => DataLocation.PythonEnvironmentName;
@ -39,9 +41,20 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
// 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
JTF.Run(() => DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath));
JTF.Run(async () =>
{
try
{
await DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath);
PluginsSettingsFilePath = ExecutablePath;
PluginsSettingsFilePath = ExecutablePath;
}
catch (System.Exception e)
{
API.ShowMsgError(Localize.failToInstallPythonEnv());
API.LogException(ClassName, "Failed to install Python environment", e);
}
});
}
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)

View file

@ -11,6 +11,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class TypeScriptEnvironment : AbstractPluginEnvironment
{
private static readonly string ClassName = nameof(TypeScriptEnvironment);
internal override string Language => AllowedLanguage.TypeScript;
internal override string EnvName => DataLocation.NodeEnvironmentName;
@ -34,9 +36,20 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath));
JTF.Run(async () =>
{
try
{
await DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath);
PluginsSettingsFilePath = ExecutablePath;
PluginsSettingsFilePath = ExecutablePath;
}
catch (System.Exception e)
{
API.ShowMsgError(Localize.failToInstallTypeScriptEnv());
API.LogException(ClassName, "Failed to install TypeScript environment", e);
}
});
}
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)

View file

@ -11,6 +11,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class TypeScriptV2Environment : AbstractPluginEnvironment
{
private static readonly string ClassName = nameof(TypeScriptV2Environment);
internal override string Language => AllowedLanguage.TypeScriptV2;
internal override string EnvName => DataLocation.NodeEnvironmentName;
@ -34,9 +36,20 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath));
JTF.Run(async () =>
{
try
{
await DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath);
PluginsSettingsFilePath = ExecutablePath;
PluginsSettingsFilePath = ExecutablePath;
}
catch (System.Exception e)
{
API.ShowMsgError(Localize.failToInstallTypeScriptEnv());
API.LogException(ClassName, "Failed to install TypeScript environment", e);
}
});
}
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)

View file

@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
using Flow.Launcher.Infrastructure;
namespace Flow.Launcher.Core.ExternalPlugins
{
@ -12,10 +12,10 @@ namespace Flow.Launcher.Core.ExternalPlugins
private static readonly string ClassName = nameof(PluginsManifest);
private static readonly CommunityPluginStore mainPluginStore =
new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json",
"https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json",
"https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json",
"https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json");
new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json",
"https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json",
"https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json",
"https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json");
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
@ -35,18 +35,28 @@ namespace Flow.Launcher.Core.ExternalPlugins
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
// If the results are empty, we shouldn't update the manifest because the results are invalid.
if (results.Count != 0)
{
UserPlugins = results;
lastFetchedAt = DateTime.Now;
if (results.Count == 0)
return false;
return true;
var updatedPluginResults = new List<UserPlugin>();
var appVersion = SemanticVersioning.Version.Parse(Constant.Version);
for (int i = 0; i < results.Count; i++)
{
if (IsMinimumAppVersionSatisfied(results[i], appVersion))
updatedPluginResults.Add(results[i]);
}
UserPlugins = updatedPluginResults;
lastFetchedAt = DateTime.Now;
return true;
}
}
catch (Exception e)
{
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(ClassName, "Http request failed", e);
PublicApi.Instance.LogException(ClassName, "Http request failed", e);
}
finally
{
@ -55,5 +65,28 @@ namespace Flow.Launcher.Core.ExternalPlugins
return false;
}
private static bool IsMinimumAppVersionSatisfied(UserPlugin plugin, SemanticVersioning.Version appVersion)
{
if (string.IsNullOrEmpty(plugin.MinimumAppVersion))
return true;
try
{
if (appVersion >= SemanticVersioning.Version.Parse(plugin.MinimumAppVersion))
return true;
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. "
+ "Plugin excluded from manifest", e);
return false;
}
PublicApi.Instance.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, "
+ $"but current version is {Constant.Version}. Plugin excluded from manifest.");
return false;
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<OutputType>Library</OutputType>
@ -12,6 +12,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -33,6 +34,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -54,11 +56,24 @@
<ItemGroup>
<PackageReference Include="Droplex" Version="1.7.0" />
<PackageReference Include="FSharp.Core" Version="9.0.201" />
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.0" />
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
<PackageReference Include="FSharp.Core" Version="9.0.303" />
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.4" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
<PackageReference Include="StreamJsonRpc" Version="2.21.10" />
<PackageReference Include="StreamJsonRpc" Version="2.22.11" />
</ItemGroup>
<PropertyGroup>
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Remove="Languages\en.xaml" />
<AdditionalFiles Include="..\Flow.Launcher\Languages\en.xaml">
<Link>Languages\en.xaml</Link>
</AdditionalFiles>
</ItemGroup>
<ItemGroup>

View file

@ -27,6 +27,7 @@ namespace Flow.Launcher.Core.Plugin
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
private static readonly double MainGridColumn0MaxWidthRatio = 0.6;
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");
@ -156,7 +157,7 @@ namespace Flow.Launcher.Core.Plugin
{
if (!NeedCreateSettingPanel()) return null!;
// Create main grid with two columns (Column 1: Auto, Column 2: *)
// Create main grid with two columns (Column 0: Auto, Column 1: *)
var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
{
@ -200,7 +201,7 @@ namespace Flow.Launcher.Core.Plugin
{
Text = attributes.Label,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.WrapWithOverflow
TextWrapping = TextWrapping.Wrap
};
// Create a text block for description
@ -211,7 +212,7 @@ namespace Flow.Launcher.Core.Plugin
{
Text = attributes.Description,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.WrapWithOverflow
TextWrapping = TextWrapping.Wrap
};
desc.SetResourceReference(TextBlock.StyleProperty, "SettingPanelTextBlockDescriptionStyle"); // for theme change
@ -247,7 +248,8 @@ namespace Flow.Launcher.Core.Plugin
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftTopBottomMargin,
Text = Settings[attributes.Name] as string ?? string.Empty,
ToolTip = attributes.Description
ToolTip = attributes.Description,
TextWrapping = TextWrapping.Wrap
};
textBox.TextChanged += (_, _) =>
@ -269,7 +271,8 @@ namespace Flow.Launcher.Core.Plugin
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftMargin,
Text = Settings[attributes.Name] as string ?? string.Empty,
ToolTip = attributes.Description
ToolTip = attributes.Description,
TextWrapping = TextWrapping.Wrap
};
textBox.TextChanged += (_, _) =>
@ -282,7 +285,7 @@ namespace Flow.Launcher.Core.Plugin
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftMargin,
Content = API.GetTranslation("select")
Content = Localize.select()
};
Btn.Click += (_, _) =>
@ -333,7 +336,7 @@ namespace Flow.Launcher.Core.Plugin
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center,
Margin = SettingPanelItemLeftTopBottomMargin,
TextWrapping = TextWrapping.WrapWithOverflow,
TextWrapping = TextWrapping.Wrap,
AcceptsReturn = true,
Text = Settings[attributes.Name] as string ?? string.Empty,
ToolTip = attributes.Description
@ -488,6 +491,8 @@ namespace Flow.Launcher.Core.Plugin
rowCount++;
}
mainPanel.SizeChanged += MainPanel_SizeChanged;
// Wrap the main grid in a user control
return new UserControl()
{
@ -495,6 +500,28 @@ namespace Flow.Launcher.Core.Plugin
};
}
private void MainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (sender is not Grid grid) return;
var workingWidth = grid.ActualWidth;
if (workingWidth <= 0) return;
var constrainedWidth = MainGridColumn0MaxWidthRatio * workingWidth;
// Set MaxWidth of column 0 and its children
// We must set MaxWidth of its children to make text wrapping work correctly
grid.ColumnDefinitions[0].MaxWidth = constrainedWidth;
foreach (var child in grid.Children)
{
if (child is FrameworkElement element && Grid.GetColumn(element) == 0 && Grid.GetColumnSpan(element) == 1)
{
element.MaxWidth = constrainedWidth;
}
}
}
private static bool NeedSaveInSettings(string type)
{
return type != "textBlock" && type != "separator" && type != "hyperlink";

View file

@ -5,7 +5,6 @@ using System.IO;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;
using System.Text.Json;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Plugin
{
@ -13,10 +12,6 @@ namespace Flow.Launcher.Core.Plugin
{
private static readonly string ClassName = nameof(PluginConfig);
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
/// <summary>
/// Parse plugin metadata in the given directories
/// </summary>
@ -38,7 +33,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Can't delete <{directory}>", e);
PublicApi.Instance.LogException(ClassName, $"Can't delete <{directory}>", e);
}
}
else
@ -55,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin
duplicateList
.ForEach(
x => API.LogWarn(ClassName,
x => PublicApi.Instance.LogWarn(ClassName,
string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " +
"not loaded due to version not the highest of the duplicates",
x.Name, x.ID, x.Version),
@ -107,7 +102,7 @@ namespace Flow.Launcher.Core.Plugin
string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName);
if (!File.Exists(configPath))
{
API.LogError(ClassName, $"Didn't find config file <{configPath}>");
PublicApi.Instance.LogError(ClassName, $"Didn't find config file <{configPath}>");
return null;
}
@ -123,19 +118,19 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
PublicApi.Instance.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
return null;
}
if (!AllowedLanguage.IsAllowed(metadata.Language))
{
API.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
PublicApi.Instance.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
return null;
}
if (!File.Exists(metadata.ExecuteFilePath))
{
API.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
PublicApi.Instance.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
return null;
}

View file

@ -0,0 +1,462 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin;
/// <summary>
/// Class for installing, updating, and uninstalling plugins.
/// </summary>
public static class PluginInstaller
{
private static readonly string ClassName = nameof(PluginInstaller);
private static readonly Settings Settings = Ioc.Default.GetRequiredService<Settings>();
/// <summary>
/// Installs a plugin and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
/// </summary>
/// <param name="newPlugin">The plugin to install.</param>
/// <returns>A Task representing the asynchronous install operation.</returns>
public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin)
{
if (PublicApi.Instance.PluginModified(newPlugin.ID))
{
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(newPlugin.Name),
Localize.pluginModifiedAlreadyMessage());
return;
}
if (PublicApi.Instance.ShowMsgBox(
Localize.InstallPromptSubtitle(newPlugin.Name, newPlugin.Author, Environment.NewLine),
Localize.InstallPromptTitle(),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
try
{
// at minimum should provide a name, but handle plugin that is not downloaded from plugins manifest and is a url download
var downloadFilename = string.IsNullOrEmpty(newPlugin.Version)
? $"{newPlugin.Name}-{Guid.NewGuid()}.zip"
: $"{newPlugin.Name}-{newPlugin.Version}.zip";
var filePath = Path.Combine(Path.GetTempPath(), downloadFilename);
using var cts = new CancellationTokenSource();
if (!newPlugin.IsFromLocalInstallPath)
{
await DownloadFileAsync(
$"{Localize.DownloadingPlugin()} {newPlugin.Name}",
newPlugin.UrlDownload, filePath, cts);
}
else
{
filePath = newPlugin.LocalInstallPath;
}
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath);
}
if (!PublicApi.Instance.InstallPlugin(newPlugin, filePath))
{
return;
}
if (!newPlugin.IsFromLocalInstallPath)
{
File.Delete(filePath);
}
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, "Failed to install plugin", e);
PublicApi.Instance.ShowMsgError(Localize.ErrorInstallingPlugin());
return; // do not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
PublicApi.Instance.RestartApp();
}
else
{
PublicApi.Instance.ShowMsg(
Localize.installbtn(),
Localize.InstallSuccessNoRestart(newPlugin.Name));
}
}
/// <summary>
/// Installs a plugin from a local zip file and restarts the application if required by settings. Validates the zip and prompts user for confirmation.
/// </summary>
/// <param name="filePath">The path to the plugin zip file.</param>
/// <returns>A Task representing the asynchronous install operation.</returns>
public static async Task InstallPluginAndCheckRestartAsync(string filePath)
{
UserPlugin plugin;
try
{
using ZipArchive archive = ZipFile.OpenRead(filePath);
var pluginJsonEntry = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ??
throw new FileNotFoundException("The zip file does not contain a plugin.json file.");
using Stream stream = pluginJsonEntry.Open();
plugin = JsonSerializer.Deserialize<UserPlugin>(stream);
plugin.IcoPath = "Images\\zipfolder.png";
plugin.LocalInstallPath = filePath;
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, "Failed to validate zip file", e);
PublicApi.Instance.ShowMsgError(Localize.ZipFileNotHavePluginJson());
return;
}
if (PublicApi.Instance.PluginModified(plugin.ID))
{
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
Localize.pluginModifiedAlreadyMessage());
return;
}
if (Settings.ShowUnknownSourceWarning)
{
if (!InstallSourceKnown(plugin.Website)
&& PublicApi.Instance.ShowMsgBox(Localize.InstallFromUnknownSourceSubtitle(Environment.NewLine),
Localize.InstallFromUnknownSourceTitle(),
MessageBoxButton.YesNo) == MessageBoxResult.No)
return;
}
await InstallPluginAndCheckRestartAsync(plugin);
}
/// <summary>
/// Uninstalls a plugin and restarts the application if required by settings. Prompts user for confirmation and whether to keep plugin settings.
/// </summary>
/// <param name="oldPlugin">The plugin metadata to uninstall.</param>
/// <returns>A Task representing the asynchronous uninstall operation.</returns>
public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin)
{
if (PublicApi.Instance.PluginModified(oldPlugin.ID))
{
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(oldPlugin.Name),
Localize.pluginModifiedAlreadyMessage());
return;
}
if (PublicApi.Instance.ShowMsgBox(
Localize.UninstallPromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
Localize.UninstallPromptTitle(),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
var removePluginSettings = PublicApi.Instance.ShowMsgBox(
Localize.KeepPluginSettingsSubtitle(),
Localize.KeepPluginSettingsTitle(),
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
try
{
if (!await PublicApi.Instance.UninstallPluginAsync(oldPlugin, removePluginSettings))
{
return;
}
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, "Failed to uninstall plugin", e);
PublicApi.Instance.ShowMsgError(Localize.ErrorUninstallingPlugin());
return; // don not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
PublicApi.Instance.RestartApp();
}
else
{
PublicApi.Instance.ShowMsg(
Localize.uninstallbtn(),
Localize.UninstallSuccessNoRestart(oldPlugin.Name));
}
}
/// <summary>
/// Updates a plugin to a new version and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
/// </summary>
/// <param name="newPlugin">The new plugin version to install.</param>
/// <param name="oldPlugin">The existing plugin metadata to update.</param>
/// <returns>A Task representing the asynchronous update operation.</returns>
public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin)
{
if (PublicApi.Instance.ShowMsgBox(
Localize.UpdatePromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
Localize.UpdatePromptTitle(),
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
try
{
var filePath = Path.Combine(Path.GetTempPath(), $"{newPlugin.Name}-{newPlugin.Version}.zip");
using var cts = new CancellationTokenSource();
if (!newPlugin.IsFromLocalInstallPath)
{
await DownloadFileAsync(
$"{Localize.DownloadingPlugin()} {newPlugin.Name}",
newPlugin.UrlDownload, filePath, cts);
}
else
{
filePath = newPlugin.LocalInstallPath;
}
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
if (!await PublicApi.Instance.UpdatePluginAsync(oldPlugin, newPlugin, filePath))
{
return;
}
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e);
PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin());
return; // do not restart on failure
}
if (Settings.AutoRestartAfterChanging)
{
PublicApi.Instance.RestartApp();
}
else
{
PublicApi.Instance.ShowMsg(
Localize.updatebtn(),
Localize.UpdateSuccessNoRestart(newPlugin.Name));
}
}
/// <summary>
/// Updates the plugin to the latest version available from its source.
/// </summary>
/// <param name="updateAllPlugins">Action to execute when the user chooses to update all plugins.</param>
/// <param name="silentUpdate">If true, do not show any messages when there is no update available.</param>
/// <param name="usePrimaryUrlOnly">If true, only use the primary URL for updates.</param>
/// <param name="token">Cancellation token to cancel the update operation.</param>
/// <returns></returns>
public static async Task CheckForPluginUpdatesAsync(Action<List<PluginUpdateInfo>> updateAllPlugins, bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default)
{
// Update the plugin manifest
await PublicApi.Instance.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
// Get all plugins that can be updated
var resultsForUpdate = (
from existingPlugin in PublicApi.Instance.GetAllPlugins()
join pluginUpdateSource in PublicApi.Instance.GetPluginManifest()
on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
StringComparison.InvariantCulture) <
0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
&& !PublicApi.Instance.PluginModified(existingPlugin.Metadata.ID)
select
new PluginUpdateInfo()
{
ID = existingPlugin.Metadata.ID,
Name = existingPlugin.Metadata.Name,
Author = existingPlugin.Metadata.Author,
CurrentVersion = existingPlugin.Metadata.Version,
NewVersion = pluginUpdateSource.Version,
IcoPath = existingPlugin.Metadata.IcoPath,
PluginExistingMetadata = existingPlugin.Metadata,
PluginNewUserPlugin = pluginUpdateSource
}).ToList();
// No updates
if (resultsForUpdate.Count == 0)
{
if (!silentUpdate)
{
PublicApi.Instance.ShowMsg(Localize.updateNoResultTitle(), Localize.updateNoResultSubtitle());
}
return;
}
// If all plugins are modified, just return
if (resultsForUpdate.All(x => PublicApi.Instance.PluginModified(x.ID)))
{
return;
}
// Show message box with button to update all plugins
PublicApi.Instance.ShowMsgWithButton(
Localize.updateAllPluginsTitle(),
Localize.updateAllPluginsButtonContent(),
() =>
{
updateAllPlugins(resultsForUpdate);
},
string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name)));
}
/// <summary>
/// Updates all plugins that have available updates.
/// </summary>
/// <param name="resultsForUpdate"></param>
/// <param name="restart"></param>
public static async Task UpdateAllPluginsAsync(IEnumerable<PluginUpdateInfo> resultsForUpdate, bool restart)
{
var anyPluginSuccess = false;
await Task.WhenAll(resultsForUpdate.Select(async plugin =>
{
var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip");
try
{
using var cts = new CancellationTokenSource();
await DownloadFileAsync(
$"{Localize.DownloadingPlugin()} {plugin.PluginNewUserPlugin.Name}",
plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
if (!await PublicApi.Instance.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
{
return;
}
anyPluginSuccess = true;
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e);
PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin());
}
}));
if (!anyPluginSuccess) return;
if (restart)
{
PublicApi.Instance.RestartApp();
}
else
{
PublicApi.Instance.ShowMsg(
Localize.updatebtn(),
Localize.PluginsUpdateSuccessNoRestart());
}
}
/// <summary>
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
/// </summary>
/// <param name="progressBoxTitle">The title for the progress box.</param>
/// <param name="downloadUrl">The URL to download from.</param>
/// <param name="filePath">The local file path to save to.</param>
/// <param name="cts">Cancellation token source for cancelling the download.</param>
/// <param name="deleteFile">Whether to delete the file if it already exists.</param>
/// <param name="showProgress">Whether to show a progress box during download.</param>
/// <returns>A Task representing the asynchronous download operation.</returns>
private static async Task DownloadFileAsync(string progressBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
{
if (deleteFile && File.Exists(filePath))
File.Delete(filePath);
if (showProgress)
{
var exceptionHappened = false;
await PublicApi.Instance.ShowProgressBoxAsync(progressBoxTitle,
async (reportProgress) =>
{
if (reportProgress == null)
{
// when reportProgress is null, it means there is exception with the progress box
// so we record it with exceptionHappened and return so that progress box will close instantly
exceptionHappened = true;
return;
}
else
{
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
}
}, cts.Cancel);
// if exception happened while downloading and user does not cancel downloading,
// we need to redownload the plugin
if (exceptionHappened && (!cts.IsCancellationRequested))
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
else
{
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
}
/// <summary>
/// Determines if the plugin install source is a known/approved source (e.g., GitHub and matches an existing plugin author).
/// </summary>
/// <param name="url">The URL to check.</param>
/// <returns>True if the source is known, otherwise false.</returns>
private static bool InstallSourceKnown(string url)
{
if (string.IsNullOrEmpty(url))
return false;
var pieces = url.Split('/');
if (pieces.Length < 4)
return false;
var author = pieces[3];
var acceptedHost = "github.com";
var acceptedSource = "https://github.com";
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost)
return false;
return PublicApi.Instance.GetAllPlugins().Any(x =>
!string.IsNullOrEmpty(x.Metadata.Website) &&
x.Metadata.Website.StartsWith(constructedUrlPart)
);
}
}
public record PluginUpdateInfo
{
public string ID { get; init; }
public string Name { get; init; }
public string Author { get; init; }
public string CurrentVersion { get; init; }
public string NewVersion { get; init; }
public string IcoPath { get; init; }
public PluginMetadata PluginExistingMetadata { get; init; }
public UserPlugin PluginNewUserPlugin { get; init; }
}

View file

@ -6,9 +6,9 @@ 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.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
@ -18,31 +18,31 @@ using ISavable = Flow.Launcher.Plugin.ISavable;
namespace Flow.Launcher.Core.Plugin
{
/// <summary>
/// The entry for managing Flow Launcher plugins
/// Class for co-ordinating and managing all plugin lifecycle.
/// </summary>
public static class PluginManager
{
private static readonly string ClassName = nameof(PluginManager);
private static IEnumerable<PluginPair> _contextMenuPlugins;
private static IEnumerable<PluginPair> _homePlugins;
public static List<PluginPair> AllPlugins { get; private set; }
public static readonly HashSet<PluginPair> GlobalPlugins = new();
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
// 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 readonly List<string> _modifiedPlugins = new();
private static readonly ConcurrentBag<string> ModifiedPlugins = new();
private static IEnumerable<PluginPair> _contextMenuPlugins;
private static IEnumerable<PluginPair> _homePlugins;
private static IEnumerable<PluginPair> _resultUpdatePlugin;
private static IEnumerable<PluginPair> _translationPlugins;
private static readonly List<DialogJumpExplorerPair> _dialogJumpExplorerPlugins = new();
private static readonly List<DialogJumpDialogPair> _dialogJumpDialogPlugins = new();
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
private static readonly string[] Directories =
public static readonly string[] Directories =
{
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
};
@ -70,12 +70,12 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
}
}
API.SavePluginSettings();
API.SavePluginCaches();
PublicApi.Instance.SavePluginSettings();
PublicApi.Instance.SavePluginCaches();
}
public static async ValueTask DisposePluginsAsync()
@ -102,7 +102,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
}
}
@ -173,12 +173,36 @@ namespace Flow.Launcher.Core.Plugin
/// <param name="settings"></param>
public static void LoadPlugins(PluginsSettings settings)
{
_metadatas = PluginConfig.Parse(Directories);
var metadatas = PluginConfig.Parse(Directories);
Settings = settings;
Settings.UpdatePluginSettings(_metadatas);
AllPlugins = PluginsLoader.Plugins(_metadatas, 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);
UpdatePluginDirectory(metadatas);
// Initialize plugin enumerable after all plugins are initialized
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
_resultUpdatePlugin = GetPluginsForInterface<IResultUpdated>();
_translationPlugins = GetPluginsForInterface<IPluginI18n>();
// Initialize Dialog Jump plugin pairs
foreach (var pair in GetPluginsForInterface<IDialogJumpExplorer>())
{
_dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair
{
Plugin = (IDialogJumpExplorer)pair.Plugin,
Metadata = pair.Metadata
});
}
foreach (var pair in GetPluginsForInterface<IDialogJumpDialog>())
{
_dialogJumpDialogPlugins.Add(new DialogJumpDialogPair
{
Plugin = (IDialogJumpDialog)pair.Plugin,
Metadata = pair.Metadata
});
}
}
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
@ -189,7 +213,7 @@ namespace Flow.Launcher.Core.Plugin
{
if (string.IsNullOrEmpty(metadata.AssemblyName))
{
API.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}");
PublicApi.Instance.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}");
continue; // Skip if AssemblyName is not set, which can happen for erroneous plugins
}
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName);
@ -199,7 +223,7 @@ namespace Flow.Launcher.Core.Plugin
{
if (string.IsNullOrEmpty(metadata.Name))
{
API.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}");
PublicApi.Instance.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}");
continue; // Skip if Name is not set, which can happen for erroneous plugins
}
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name);
@ -220,37 +244,34 @@ namespace Flow.Launcher.Core.Plugin
{
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, PublicApi.Instance)));
pair.Metadata.InitTime += milliseconds;
API.LogInfo(ClassName,
PublicApi.Instance.LogInfo(ClassName,
$"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
}
catch (Exception e)
{
API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
if (pair.Metadata.Disabled && pair.Metadata.HomeDisabled)
{
// If this plugin is already disabled, do not show error message again
// Or else it will be shown every time
API.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error");
PublicApi.Instance.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error");
}
else
{
pair.Metadata.Disabled = true;
pair.Metadata.HomeDisabled = true;
failedPlugins.Enqueue(pair);
API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
PublicApi.Instance.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
}
}
}));
await Task.WhenAll(InitTasks);
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
foreach (var plugin in AllPlugins)
{
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
@ -269,28 +290,36 @@ namespace Flow.Launcher.Core.Plugin
}
}
if (failedPlugins.Any())
if (!failedPlugins.IsEmpty)
{
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
API.ShowMsg(
API.GetTranslation("failedToInitializePluginsTitle"),
string.Format(
API.GetTranslation("failedToInitializePluginsMessage"),
failed
),
PublicApi.Instance.ShowMsg(
Localize.failedToInitializePluginsTitle(),
Localize.failedToInitializePluginsMessage(failed),
"",
false
);
}
}
public static ICollection<PluginPair> ValidPluginsForQuery(Query query)
public static ICollection<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump)
{
if (query is null)
return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
return GlobalPlugins;
{
if (dialogJump)
return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList();
else
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
return Array.Empty<PluginPair>();
if (PublicApi.Instance.PluginModified(plugin.Metadata.ID))
return Array.Empty<PluginPair>();
return new List<PluginPair>
{
@ -300,7 +329,7 @@ namespace Flow.Launcher.Core.Plugin
public static ICollection<PluginPair> ValidPluginsForHomeQuery()
{
return _homePlugins.ToList();
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
@ -310,7 +339,7 @@ namespace Flow.Launcher.Core.Plugin
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
@ -354,7 +383,7 @@ namespace Flow.Launcher.Core.Plugin
try
{
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await ((IAsyncHomeQuery)pair.Plugin).HomeQueryAsync(token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
@ -371,7 +400,37 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e);
return null;
}
return results;
}
public static async Task<List<DialogJumpResult>> QueryDialogJumpForPluginAsync(PluginPair pair, Query query, CancellationToken token)
{
var results = new List<DialogJumpResult>();
var metadata = pair.Metadata;
try
{
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
if (results == null)
return null;
UpdatePluginMetadata(results, metadata, query);
token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException)
{
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null;
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e);
return null;
}
return results;
@ -402,16 +461,26 @@ namespace Flow.Launcher.Core.Plugin
return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id);
}
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
private static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
{
// 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 IList<PluginPair> GetResultUpdatePlugin()
{
return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static IList<PluginPair> GetTranslationPlugins()
{
return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static List<Result> GetContextMenusForPlugin(Result result)
{
var results = new List<Result>();
var pluginPair = _contextMenuPlugins.FirstOrDefault(o => o.Metadata.ID == result.PluginID);
var pluginPair = _contextMenuPlugins.Where(p => !PluginModified(p.Metadata.ID)).FirstOrDefault(o => o.Metadata.ID == result.PluginID);
if (pluginPair != null)
{
var plugin = (IContextMenu)pluginPair.Plugin;
@ -428,7 +497,7 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName,
PublicApi.Instance.LogException(ClassName,
$"Can't load context menus for plugin <{pluginPair.Metadata.Name}>",
e);
}
@ -439,7 +508,17 @@ namespace Flow.Launcher.Core.Plugin
public static bool IsHomePlugin(string id)
{
return _homePlugins.Any(p => p.Metadata.ID == id);
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
}
public static IList<DialogJumpExplorerPair> GetDialogJumpExplorers()
{
return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static IList<DialogJumpDialogPair> GetDialogJumpDialogs()
{
return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static bool ActionKeywordRegistered(string actionKeyword)
@ -529,44 +608,62 @@ namespace Flow.Launcher.Core.Plugin
private static bool SameOrLesserPluginVersionExists(string metadataPath)
{
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
if (!Version.TryParse(newMetadata.Version, out var newVersion))
return true; // If version is not valid, we assume it is lesser than any existing version
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
&& newMetadata.Version.CompareTo(x.Metadata.Version) <= 0);
&& Version.TryParse(x.Metadata.Version, out var version)
&& newVersion <= version);
}
#region Public functions
public static bool PluginModified(string id)
{
return _modifiedPlugins.Contains(id);
return ModifiedPlugins.Contains(id);
}
public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
public static async Task<bool> UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
{
InstallPlugin(newVersion, zipFilePath, checkModified:false);
await UninstallPluginAsync(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false);
_modifiedPlugins.Add(existingVersion.ID);
if (PluginModified(existingVersion.ID))
{
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(existingVersion.Name),
Localize.pluginModifiedAlreadyMessage());
return false;
}
var installSuccess = InstallPlugin(newVersion, zipFilePath, checkModified: false);
if (!installSuccess) return false;
var uninstallSuccess = await UninstallPluginAsync(existingVersion, removePluginFromSettings: false, removePluginSettings: false, checkModified: false);
if (!uninstallSuccess) return false;
ModifiedPlugins.Add(existingVersion.ID);
return true;
}
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
public static bool InstallPlugin(UserPlugin plugin, string zipFilePath)
{
InstallPlugin(plugin, zipFilePath, checkModified: true);
return InstallPlugin(plugin, zipFilePath, checkModified: true);
}
public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
public static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginSettings = false)
{
await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true);
return await UninstallPluginAsync(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings, checkModified: true);
}
#endregion
#region Internal functions
internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
{
if (checkModified && PluginModified(plugin.ID))
{
// Distinguish exception from installing same or less version
throw new ArgumentException($"Plugin {plugin.Name} {plugin.ID} has been modified.", nameof(plugin));
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
Localize.pluginModifiedAlreadyMessage());
return false;
}
// Unzip plugin files to temp folder
@ -584,12 +681,16 @@ namespace Flow.Launcher.Core.Plugin
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
{
throw new FileNotFoundException($"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist");
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.fileNotFoundMessage(pluginFolderPath));
return false;
}
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
{
throw new InvalidOperationException($"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}");
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
Localize.pluginExistAlreadyMessage());
return false;
}
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
@ -617,7 +718,7 @@ namespace Flow.Launcher.Core.Plugin
var newPluginPath = Path.Combine(installDirectory, folderName);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s));
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s));
try
{
@ -626,20 +727,24 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
}
if (checkModified)
{
_modifiedPlugins.Add(plugin.ID);
ModifiedPlugins.Add(plugin.ID);
}
return true;
}
internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
internal static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
{
if (checkModified && PluginModified(plugin.ID))
{
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
Localize.pluginModifiedAlreadyMessage());
return false;
}
if (removePluginSettings || removePluginFromSettings)
@ -657,7 +762,7 @@ namespace Flow.Launcher.Core.Plugin
if (removePluginSettings)
{
// For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances
if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable)
if (AllowedLanguage.IsDotNet(plugin.Language) && PublicApi.Instance is IRemovable removable)
{
removable.RemovePluginSettings(plugin.AssemblyName);
removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath);
@ -671,9 +776,9 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"),
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginSettingsTitle(),
Localize.failedToRemovePluginSettingsMessage(plugin.Name));
}
}
@ -687,12 +792,18 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"),
string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name));
PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginCacheTitle(),
Localize.failedToRemovePluginCacheMessage(plugin.Name));
}
Settings.RemovePluginSettings(plugin.ID);
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID);
var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
foreach (var key in keysToRemove)
{
NonGlobalPlugins.Remove(key);
}
}
// Marked for deletion. Will be deleted on next start up
@ -700,8 +811,10 @@ namespace Flow.Launcher.Core.Plugin
if (checkModified)
{
_modifiedPlugins.Add(plugin.ID);
ModifiedPlugins.Add(plugin.ID);
}
return true;
}
#endregion

View file

@ -2,9 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins.Environments;
#pragma warning disable IDE0005
using Flow.Launcher.Infrastructure.Logger;
@ -18,10 +15,6 @@ namespace Flow.Launcher.Core.Plugin
{
private static readonly string ClassName = nameof(PluginsLoader);
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
{
var dotnetPlugins = DotNetPlugins(metadatas);
@ -64,7 +57,7 @@ namespace Flow.Launcher.Core.Plugin
foreach (var metadata in metadatas)
{
var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
{
Assembly assembly = null;
IAsyncPlugin plugin = null;
@ -89,19 +82,19 @@ namespace Flow.Launcher.Core.Plugin
#else
catch (Exception e) when (assembly == null)
{
Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
}
catch (InvalidOperationException e)
{
Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
}
catch (ReflectionTypeLoadException e)
{
Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
}
catch (Exception e)
{
Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
#endif
@ -120,17 +113,13 @@ namespace Flow.Launcher.Core.Plugin
{
var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
var errorMessage = "The following "
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
+ "errored and cannot be loaded:";
var errorMessage = erroredPlugins.Count > 1 ?
Localize.pluginsHaveErrored():
Localize.pluginHasErrored();
_ = Task.Run(() =>
{
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
$"Please refer to the logs for more information", "",
MessageBoxButton.OK, MessageBoxImage.Warning);
});
PublicApi.Instance.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
Localize.referToLogs());
}
return plugins;

View file

@ -17,6 +17,7 @@ namespace Flow.Launcher.Core.Resource
public static Language German = new Language("de", "Deutsch");
public static Language Korean = new Language("ko", "한국어");
public static Language Serbian = new Language("sr", "Srpski");
public static Language Serbian_Cyrillic = new Language("sr-Cyrl-RS", "Српски");
public static Language Portuguese_Portugal = new Language("pt-pt", "Português");
public static Language Portuguese_Brazil = new Language("pt-br", "Português (Brasil)");
public static Language Spanish = new Language("es", "Spanish");
@ -47,6 +48,7 @@ namespace Flow.Launcher.Core.Resource
German,
Korean,
Serbian,
Serbian_Cyrillic,
Portuguese_Portugal,
Portuguese_Brazil,
Spanish,
@ -79,7 +81,8 @@ namespace Flow.Launcher.Core.Resource
"da" => "System",
"de" => "System",
"ko" => "시스템",
"sr" => "Систем",
"sr" => "Sistem",
"sr-Cyrl-RS" => "Систем",
"pt-pt" => "Sistema",
"pt-br" => "Sistema",
"es" => "Sistema",

View file

@ -1,50 +1,43 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
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 class Internationalization : IDisposable
{
private static readonly string ClassName = nameof(Internationalization);
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
private const string Folder = "Languages";
private const string DefaultLanguageCode = "en";
private const string DefaultFile = "en.xaml";
private const string Extension = ".xaml";
private readonly Settings _settings;
private readonly List<string> _languageDirectories = new();
private readonly List<ResourceDictionary> _oldResources = new();
private readonly string SystemLanguageCode;
private readonly List<string> _languageDirectories = [];
private readonly List<ResourceDictionary> _oldResources = [];
private static string SystemLanguageCode;
private readonly SemaphoreSlim _langChangeLock = new(1, 1);
public Internationalization(Settings settings)
{
_settings = settings;
AddFlowLauncherLanguageDirectory();
SystemLanguageCode = GetSystemLanguageCodeAtStartup();
}
private void AddFlowLauncherLanguageDirectory()
{
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
_languageDirectories.Add(directory);
}
#region Initialization
private static string GetSystemLanguageCodeAtStartup()
/// <summary>
/// Initialize the system language code based on the current culture.
/// </summary>
public static void InitSystemLanguageCode()
{
var availableLanguages = AvailableLanguages.GetAvailableLanguages();
@ -65,40 +58,11 @@ namespace Flow.Launcher.Core.Resource
string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) ||
string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase))
{
return languageCode;
SystemLanguageCode = languageCode;
}
}
return DefaultLanguageCode;
}
private void AddPluginLanguageDirectories()
{
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
{
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
var dir = Path.GetDirectoryName(location);
if (dir != null)
{
var pluginThemeDirectory = Path.Combine(dir, Folder);
_languageDirectories.Add(pluginThemeDirectory);
}
else
{
API.LogError(ClassName, $"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();
SystemLanguageCode = DefaultLanguageCode;
}
/// <summary>
@ -116,13 +80,64 @@ namespace Flow.Launcher.Core.Resource
// Get language by language code and change language
var language = GetLanguageByLanguageCode(languageCode);
// Add Flow Launcher language directory
AddFlowLauncherLanguageDirectory();
// Add plugin language directories first so that we can load language files from plugins
AddPluginLanguageDirectories();
// Load default language resources
LoadDefaultLanguage();
// Change language
await ChangeLanguageAsync(language);
await ChangeLanguageAsync(language, false);
}
private void AddFlowLauncherLanguageDirectory()
{
// Check if Flow Launcher language directory exists
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
if (!Directory.Exists(directory))
{
PublicApi.Instance.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>");
return;
}
_languageDirectories.Add(directory);
}
private void AddPluginLanguageDirectories()
{
foreach (var pluginsDir in PluginManager.Directories)
{
if (!Directory.Exists(pluginsDir)) continue;
// Enumerate all top directories in the plugin directory
foreach (var dir in Directory.GetDirectories(pluginsDir))
{
// Check if the directory contains a language folder
var pluginLanguageDir = Path.Combine(dir, Folder);
if (!Directory.Exists(pluginLanguageDir)) continue;
// Check if the language directory contains default language file since it will be checked later
_languageDirectories.Add(pluginLanguageDir);
}
}
}
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();
}
#endregion
#region Change Language
/// <summary>
/// Change language during runtime. Will change app language and plugin language & save settings.
/// </summary>
@ -151,11 +166,11 @@ namespace Flow.Launcher.Core.Resource
private static Language GetLanguageByLanguageCode(string languageCode)
{
var lowercase = languageCode.ToLower();
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
var language = AvailableLanguages.GetAvailableLanguages().
FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase));
if (language == null)
{
API.LogError(ClassName, $"Language code can't be found <{languageCode}>");
PublicApi.Instance.LogError(ClassName, $"Language code can't be found <{languageCode}>");
return AvailableLanguages.English;
}
else
@ -164,24 +179,62 @@ namespace Flow.Launcher.Core.Resource
}
}
private async Task ChangeLanguageAsync(Language language)
private async Task ChangeLanguageAsync(Language language, bool updateMetadata = true)
{
// Remove old language files and load language
RemoveOldLanguageFiles();
if (language != AvailableLanguages.English)
{
LoadLanguage(language);
}
await _langChangeLock.WaitAsync();
try
{
// Remove old language files and load language
RemoveOldLanguageFiles();
if (language != AvailableLanguages.English)
{
LoadLanguage(language);
}
// Change culture info
ChangeCultureInfo(language.LanguageCode);
if (updateMetadata)
{
// Raise event for plugins after culture is set
await Task.Run(UpdatePluginMetadataTranslations);
}
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e);
}
finally
{
_langChangeLock.Release();
}
}
public static void ChangeCultureInfo(string languageCode)
{
// 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 for plugins after culture is set
await Task.Run(UpdatePluginMetadataTranslations);
CultureInfo currentCulture;
try
{
currentCulture = CultureInfo.CreateSpecificCulture(languageCode);
}
catch (CultureNotFoundException)
{
currentCulture = CultureInfo.CreateSpecificCulture(SystemLanguageCode);
}
CultureInfo.CurrentCulture = currentCulture;
CultureInfo.CurrentUICulture = currentCulture;
var thread = Thread.CurrentThread;
thread.CurrentCulture = currentCulture;
thread.CurrentUICulture = currentCulture;
}
#endregion
#region Prompt Pinyin
public bool PromptShouldUsePinyin(string languageCodeToSet)
{
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
@ -194,14 +247,18 @@ namespace Flow.Launcher.Core.Resource
// No other languages should show the following text so just make it hard-coded
// "Do you want to search with pinyin?"
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?";
if (Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
if (PublicApi.Instance.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
return false;
return true;
}
#endregion
#region Language Resources Management
private void RemoveOldLanguageFiles()
{
var dicts = Application.Current.Resources.MergedDictionaries;
@ -209,6 +266,7 @@ namespace Flow.Launcher.Core.Resource
{
dicts.Remove(r);
}
_oldResources.Clear();
}
private void LoadLanguage(Language language)
@ -237,45 +295,6 @@ namespace Flow.Launcher.Core.Resource
}
}
public List<Language> LoadAvailableLanguages()
{
var list = AvailableLanguages.GetAvailableLanguages();
list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
return list;
}
public static string GetTranslation(string key)
{
var translation = Application.Current.TryFindResource(key);
if (translation is string)
{
return translation.ToString();
}
else
{
API.LogError(ClassName, $"No Translation for key {key}");
return $"No Translation for key {key}";
}
}
private void UpdatePluginMetadataTranslations()
{
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
{
if (p.Plugin is not IPluginI18n pluginI18N) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
}
private static string LanguageFile(string folder, string language)
{
if (Directory.Exists(folder))
@ -287,7 +306,7 @@ namespace Flow.Launcher.Core.Resource
}
else
{
API.LogError(ClassName, $"Language path can't be found <{path}>");
PublicApi.Instance.LogError(ClassName, $"Language path can't be found <{path}>");
var english = Path.Combine(folder, DefaultFile);
if (File.Exists(english))
{
@ -295,7 +314,7 @@ namespace Flow.Launcher.Core.Resource
}
else
{
API.LogError(ClassName, $"Default English Language path can't be found <{path}>");
PublicApi.Instance.LogError(ClassName, $"Default English Language path can't be found <{path}>");
return string.Empty;
}
}
@ -305,5 +324,69 @@ namespace Flow.Launcher.Core.Resource
return string.Empty;
}
}
#endregion
#region Available Languages
public List<Language> LoadAvailableLanguages()
{
var list = AvailableLanguages.GetAvailableLanguages();
list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
return list;
}
#endregion
#region Get Translations
public static string GetTranslation(string key)
{
var translation = Application.Current.TryFindResource(key);
if (translation is string)
{
return translation.ToString();
}
else
{
PublicApi.Instance.LogError(ClassName, $"No Translation for key {key}");
return $"No Translation for key {key}";
}
}
#endregion
#region Update Metadata
public static void UpdatePluginMetadataTranslations()
{
// Update plugin metadata name & description
foreach (var p in PluginManager.GetTranslationPlugins())
{
if (p.Plugin is not IPluginI18n pluginI18N) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)
{
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
}
#endregion
#region IDisposable
public void Dispose()
{
RemoveOldLanguageFiles();
_langChangeLock.Dispose();
}
#endregion
}
}

View file

@ -1,38 +0,0 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Windows.Data;
namespace Flow.Launcher.Core.Resource
{
[Obsolete("LocalizationConverter is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
public class LocalizationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType == typeof(string) && value != null)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
if (fi != null)
{
string localizedDescription = string.Empty;
var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if ((attributes.Length > 0) && (!String.IsNullOrEmpty(attributes[0].Description)))
{
localizedDescription = attributes[0].Description;
}
return (!String.IsNullOrEmpty(localizedDescription)) ? localizedDescription : value.ToString();
}
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View file

@ -1,30 +0,0 @@
using System.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Resource
{
public class LocalizedDescriptionAttribute : DescriptionAttribute
{
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
private readonly string _resourceKey;
public LocalizedDescriptionAttribute(string resourceKey)
{
_resourceKey = resourceKey;
}
public override string Description
{
get
{
string description = API.GetTranslation(_resourceKey);
return string.IsNullOrWhiteSpace(description) ?
string.Format("[[{0}]]", _resourceKey) : description;
}
}
}
}

View file

@ -444,17 +444,27 @@ namespace Flow.Launcher.Core.Resource
_api.LogError(ClassName, $"Theme <{theme}> path can't be found");
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme));
_api.ShowMsgBox(Localize.theme_load_failure_path_not_exists(theme));
ChangeTheme(Constant.DefaultTheme);
}
return false;
}
catch (XamlParseException)
catch (XamlParseException e)
{
_api.LogError(ClassName, $"Theme <{theme}> fail to parse");
_api.LogException(ClassName, $"Theme <{theme}> fail to parse xaml", e);
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));
_api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme));
ChangeTheme(Constant.DefaultTheme);
}
return false;
}
catch (Exception e)
{
_api.LogException(ClassName, $"Theme <{theme}> fail to load", e);
if (theme != Constant.DefaultTheme)
{
_api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme));
ChangeTheme(Constant.DefaultTheme);
}
return false;

View file

@ -49,29 +49,30 @@ namespace Flow.Launcher.Core
try
{
if (!silentUpdate)
_api.ShowMsg(_api.GetTranslation("pleaseWait"),
_api.GetTranslation("update_flowlauncher_update_check"));
_api.ShowMsg(Localize.pleaseWait(),
Localize.update_flowlauncher_update_check());
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
// UpdateApp CheckForUpdate will return value only if the app is squirrel installed
var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false);
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = Version.Parse(Constant.Version);
var newReleaseVersion =
SemanticVersioning.Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = SemanticVersioning.Version.Parse(Constant.Version);
_api.LogInfo(ClassName, $"Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
if (newReleaseVersion <= currentVersion)
{
if (!silentUpdate)
_api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
_api.ShowMsgBox(Localize.update_flowlauncher_already_on_latest());
return;
}
if (!silentUpdate)
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
_api.GetTranslation("update_flowlauncher_updating"));
_api.ShowMsg(Localize.update_flowlauncher_update_found(),
Localize.update_flowlauncher_updating());
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
@ -79,12 +80,12 @@ namespace Flow.Launcher.Core
if (DataLocation.PortableDataLocationInUse())
{
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
var targetDestination = updateManager.RootAppDirectory +
$"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
DataLocation.PortableDataPath,
targetDestination));
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination,
(s) => _api.ShowMsgBox(s)))
_api.ShowMsgBox(Localize.update_flowlauncher_fail_moving_portable_user_profile_data(DataLocation.PortableDataPath, targetDestination));
}
else
{
@ -95,16 +96,19 @@ namespace Flow.Launcher.Core
_api.LogInfo(ClassName, $"Update success:{newVersionTips}");
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
if (_api.ShowMsgBox(newVersionTips, Localize.update_flowlauncher_new_update(),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
}
catch (Exception e)
{
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
if (e is HttpRequestException or WebException or SocketException ||
e.InnerException is TimeoutException)
{
_api.LogException(ClassName, $"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
_api.LogException(ClassName,
$"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
}
else
{
@ -112,8 +116,8 @@ namespace Flow.Launcher.Core
}
if (!silentUpdate)
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
_api.GetTranslation("update_flowlauncher_check_connection"));
_api.ShowMsgError(Localize.update_flowlauncher_fail(),
Localize.update_flowlauncher_check_connection());
}
finally
{
@ -124,14 +128,11 @@ namespace Flow.Launcher.Core
[UsedImplicitly]
private class GithubRelease
{
[JsonPropertyName("prerelease")]
public bool Prerelease { get; [UsedImplicitly] set; }
[JsonPropertyName("prerelease")] public bool Prerelease { get; [UsedImplicitly] set; }
[JsonPropertyName("published_at")]
public DateTime PublishedAt { get; [UsedImplicitly] set; }
[JsonPropertyName("published_at")] public DateTime PublishedAt { get; [UsedImplicitly] set; }
[JsonPropertyName("html_url")]
public string HtmlUrl { get; [UsedImplicitly] set; }
[JsonPropertyName("html_url")] public string HtmlUrl { get; [UsedImplicitly] set; }
}
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
@ -146,10 +147,7 @@ namespace Flow.Launcher.Core
var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First();
var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/");
var client = new WebClient
{
Proxy = Http.WebProxy
};
var client = new WebClient { Proxy = Http.WebProxy };
var downloader = new FileDownloader(client);
var manager = new UpdateManager(latestUrl, urlDownloader: downloader);
@ -157,19 +155,16 @@ namespace Flow.Launcher.Core
return manager;
}
private string NewVersionTips(string version)
private static string NewVersionTips(string version)
{
var tips = string.Format(_api.GetTranslation("newVersionTips"), version);
var tips = Localize.newVersionTips(version);
return tips;
}
private static string Formatted<T>(T t)
{
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
{
WriteIndented = true
});
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions { WriteIndented = true });
return formatted;
}

View file

@ -0,0 +1,290 @@
{
"version": 1,
"dependencies": {
"net9.0-windows7.0": {
"Droplex": {
"type": "Direct",
"requested": "[1.7.0, )",
"resolved": "1.7.0",
"contentHash": "wutfIus/Ufw/9TDsp86R1ycnIH+wWrj4UhcmrzAHWjsdyC2iM07WEQ9+APTB7pQynsDnYH1r2i58XgAJ3lxUXA==",
"dependencies": {
"YamlDotNet": "9.1.0"
}
},
"Flow.Launcher.Localization": {
"type": "Direct",
"requested": "[0.0.6, )",
"resolved": "0.0.6",
"contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw=="
},
"FSharp.Core": {
"type": "Direct",
"requested": "[9.0.303, )",
"resolved": "9.0.303",
"contentHash": "6JlV8aD8qQvcmfoe/PMOxCHXc0uX4lR23u0fAyQtnVQxYULLoTZgwgZHSnRcuUHOvS3wULFWcwdnP1iwslH60g=="
},
"Meziantou.Framework.Win32.Jobs": {
"type": "Direct",
"requested": "[3.4.4, )",
"resolved": "3.4.4",
"contentHash": "AivBzH5wM1NHBLehclim+o37SmireP7JxCRUoTilsc/h7LH9+YCPjb6Ig6y0khnQhFcO1P8RHYw4oiR15TGHUg=="
},
"Microsoft.IO.RecyclableMemoryStream": {
"type": "Direct",
"requested": "[3.0.1, )",
"resolved": "3.0.1",
"contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g=="
},
"SemanticVersioning": {
"type": "Direct",
"requested": "[3.0.0, )",
"resolved": "3.0.0",
"contentHash": "RR+8GbPQ/gjDqov/1QN1OPoUlbUruNwcL3WjWCeLw+MY7+od/ENhnkYxCfAC6rQLIu3QifaJt3kPYyP3RumqMQ=="
},
"squirrel.windows": {
"type": "Direct",
"requested": "[1.5.2, )",
"resolved": "1.5.2",
"contentHash": "89Y/CFxWm7SEOjvuV2stVa8p+SNM9GOLk4tUNm2nUF792nfkimAgwRA/umVsdyd/OXBH8byXSh4V1qck88ZAyQ==",
"dependencies": {
"DeltaCompressionDotNet": "[1.0.0, 2.0.0)",
"Mono.Cecil": "0.9.6.1",
"Splat": "1.6.2"
}
},
"StreamJsonRpc": {
"type": "Direct",
"requested": "[2.22.11, )",
"resolved": "2.22.11",
"contentHash": "TQcqBFswLNpdSJANjhxZmIIe0Yl0kGqzjZ+uHLdhrkxntofvNu6C53XPEEYQ3Wkj8AorKumkuv/VMvTH4BHOZw==",
"dependencies": {
"MessagePack": "2.5.192",
"Microsoft.VisualStudio.Threading.Only": "17.13.61",
"Microsoft.VisualStudio.Validation": "17.8.8",
"Nerdbank.Streams": "2.12.87",
"Newtonsoft.Json": "13.0.3",
"System.IO.Pipelines": "8.0.0"
}
},
"Ben.Demystifier": {
"type": "Transitive",
"resolved": "0.4.1",
"contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==",
"dependencies": {
"System.Reflection.Metadata": "5.0.0"
}
},
"BitFaster.Caching": {
"type": "Transitive",
"resolved": "2.5.4",
"contentHash": "1QroTY1PVCZOSG9FnkkCrmCKk/+bZCgI/YXq376HnYwUDJ4Ho0EaV4YaA/5v5WYLnwIwIO7RZkdWbg9pxIpueQ=="
},
"CommunityToolkit.Mvvm": {
"type": "Transitive",
"resolved": "8.4.0",
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
},
"DeltaCompressionDotNet": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "nwbZAYd+DblXAIzlnwDSnl0CiCm8jWLfHSYnoN4wYhtIav6AegB3+T/vKzLbU2IZlPB8Bvl8U3NXpx3eaz+N5w=="
},
"ini-parser": {
"type": "Transitive",
"resolved": "2.5.2",
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
},
"InputSimulator": {
"type": "Transitive",
"resolved": "1.0.4",
"contentHash": "D0LvRCPQMX6/FJHBjng+RO+wRDuHTJrfo7IAc7rmkPvRqchdVGJWg3y70peOtDy3OLNK+HSOwVkH4GiuLnkKgA=="
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2025.2.2",
"contentHash": "0X56ZRizuHdrnPpgXjWV7f2tQO1FlQg5O1967OGKnI/4ZRNOK642J8L7brM1nYvrxTTU5TP1yRyXLRLaXLPQ8A=="
},
"MemoryPack": {
"type": "Transitive",
"resolved": "1.21.4",
"contentHash": "wy3JTBNBsO8LfQcBvvYsWr3lm2Oakolrfu0UQ3oSJSEiD+7ye0GUhYTaXuYYBowqsXBXWD9gf2218ae0JRiYVQ==",
"dependencies": {
"MemoryPack.Core": "1.21.4",
"MemoryPack.Generator": "1.21.4"
}
},
"MemoryPack.Core": {
"type": "Transitive",
"resolved": "1.21.4",
"contentHash": "6RszGorZ0ejNmp37ZcboPBMvvPCuNW2jlrdQfcs/lMzE5b3pmPF6hsm/laDc34hRlbAST1ZxaX/DvYu2DF5sBQ=="
},
"MemoryPack.Generator": {
"type": "Transitive",
"resolved": "1.21.4",
"contentHash": "g14EsSS85yn0lHTi0J9ivqlZMf09A2iI51fmI+0KkzIzyCbWOBWPi5mdaY7YWmXprk12aYh9u/qfWHQUYthlwg=="
},
"MessagePack": {
"type": "Transitive",
"resolved": "2.5.192",
"contentHash": "Jtle5MaFeIFkdXtxQeL9Tu2Y3HsAQGoSntOzrn6Br/jrl6c8QmG22GEioT5HBtZJR0zw0s46OnKU8ei2M3QifA==",
"dependencies": {
"MessagePack.Annotations": "2.5.192",
"Microsoft.NET.StringTools": "17.6.3"
}
},
"MessagePack.Annotations": {
"type": "Transitive",
"resolved": "2.5.192",
"contentHash": "jaJuwcgovWIZ8Zysdyf3b7b34/BrADw4v82GaEZymUhDd3ScMPrYd/cttekeDteJJPXseJxp04yTIcxiVUjTWg=="
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.6.3",
"contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA=="
},
"Microsoft.VisualStudio.Threading": {
"type": "Transitive",
"resolved": "17.14.15",
"contentHash": "1DrCusT3xNLSlaJg77BsUSAzrhjdZBAvvsS0PMzyPM+fGais6SnISOhqdZQop8VVMIBLsYm2gyF9W7THjgavwA==",
"dependencies": {
"Microsoft.VisualStudio.Threading.Analyzers": "17.14.15",
"Microsoft.VisualStudio.Threading.Only": "17.14.15",
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Transitive",
"resolved": "17.14.15",
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
},
"Microsoft.VisualStudio.Threading.Only": {
"type": "Transitive",
"resolved": "17.14.15",
"contentHash": "NqONyw1RXyj9P3k5e1uU2k9kc1ptwuU5NJQzG+MPq7vQVHUzBY8HLuJf/N2Rw5H/myD96CVxziDxmjawPuzntw==",
"dependencies": {
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Microsoft.VisualStudio.Validation": {
"type": "Transitive",
"resolved": "17.8.8",
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
},
"Microsoft.Win32.SystemEvents": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ=="
},
"Mono.Cecil": {
"type": "Transitive",
"resolved": "0.9.6.1",
"contentHash": "yMsurNaOxxKIjyW9pEB+tRrR1S3DFnN1+iBgKvYvXG8kW0Y6yknJeMAe/tl3+P78/2C6304TgF7aVqpqXgEQ9Q=="
},
"Nerdbank.Streams": {
"type": "Transitive",
"resolved": "2.12.87",
"contentHash": "oDKOeKZ865I5X8qmU3IXMyrAnssYEiYWTobPGdrqubN3RtTzEHIv+D6fwhdcfrdhPJzHjCkK/ORztR/IsnmA6g==",
"dependencies": {
"Microsoft.VisualStudio.Threading.Only": "17.13.61",
"Microsoft.VisualStudio.Validation": "17.8.8",
"System.IO.Pipelines": "8.0.0"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"NHotkey": {
"type": "Transitive",
"resolved": "3.0.0",
"contentHash": "IEghs0QqWsQYH0uUmvIl0Ye6RaebWRh38eB6ToOkDnQucTYRGFOgtig0gSxlwCszTilYFz3n1ZuY762x+kDR3A=="
},
"NHotkey.Wpf": {
"type": "Transitive",
"resolved": "3.0.0",
"contentHash": "BIUKlhTG5KtFf9OQzWvkmVmktt5/FFj6AOEgag8Uf0R2YdZt5ajUzs3sVskcJcT2TztWlEHKQr1jFj3KQ0D9Nw==",
"dependencies": {
"NHotkey": "3.0.0"
}
},
"NLog": {
"type": "Transitive",
"resolved": "6.0.4",
"contentHash": "Xr+lIk1ZlTTFXEqnxQVLxrDqZlt2tm5X+/AhJbaY2emb/dVtGDiU5QuEtj3gHtwV/SWlP/rJ922I/BPuOJXlRw=="
},
"NLog.OutputDebugString": {
"type": "Transitive",
"resolved": "6.0.4",
"contentHash": "TOP2Ap9BbE98B/l/TglnguowOD0rXo8B/20xAgvj9shO/kf6IJ5M4QMhVxq72mrneJ/ANhHY7Jcd+xJbzuI5PA==",
"dependencies": {
"NLog": "6.0.4"
}
},
"SharpVectors.Wpf": {
"type": "Transitive",
"resolved": "1.8.5",
"contentHash": "WURdBDq5AE8RjKV9pFS7lNkJe81gxja9SaMGE4URq9GJUZ6M+5DGUL0Lm3B0iYW2/Meyowaz4ffGsyW+RBSTtg=="
},
"Splat": {
"type": "Transitive",
"resolved": "1.6.2",
"contentHash": "DeH0MxPU+D4JchkIDPYG4vUT+hsWs9S41cFle0/4K5EJMXWurx5DzAkj2366DfK14/XKNhsu6tCl4dZXJ3CD4w=="
},
"System.Drawing.Common": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==",
"dependencies": {
"Microsoft.Win32.SystemEvents": "7.0.0"
}
},
"System.IO.Pipelines": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ=="
},
"ToolGood.Words.Pinyin": {
"type": "Transitive",
"resolved": "3.1.0.3",
"contentHash": "VKcf8sUq/+LyY99WgLhOu7Q32ROEyR30/2xCCj9ADRi45wVC7kpXrYCf9vH1qirkmrIfpL8inoxAbrqAlfXxsQ=="
},
"YamlDotNet": {
"type": "Transitive",
"resolved": "9.1.0",
"contentHash": "fuvGXU4Ec5HrsmEc+BiFTNPCRf1cGBI2kh/3RzMWgddM2M4ALhbSPoI3X3mhXZUD1qqQd9oSkFAtWjpz8z9eRg=="
},
"flow.launcher.infrastructure": {
"type": "Project",
"dependencies": {
"Ben.Demystifier": "[0.4.1, )",
"BitFaster.Caching": "[2.5.4, )",
"CommunityToolkit.Mvvm": "[8.4.0, )",
"Flow.Launcher.Localization": "[0.0.6, )",
"Flow.Launcher.Plugin": "[5.0.0, )",
"InputSimulator": "[1.0.4, )",
"MemoryPack": "[1.21.4, )",
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
"NHotkey.Wpf": "[3.0.0, )",
"NLog": "[6.0.4, )",
"NLog.OutputDebugString": "[6.0.4, )",
"SharpVectors.Wpf": "[1.8.5, )",
"System.Drawing.Common": "[7.0.0, )",
"ToolGood.Words.Pinyin": "[3.1.0.3, )",
"ini-parser": "[2.5.2, )"
}
},
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"JetBrains.Annotations": "[2025.2.2, )"
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.DialogJump;
public class DialogJumpExplorerPair
{
public IDialogJumpExplorer Plugin { get; init; }
public PluginMetadata Metadata { get; init; }
public override string ToString()
{
return Metadata.Name;
}
public override bool Equals(object obj)
{
if (obj is DialogJumpExplorerPair r)
{
return string.Equals(r.Metadata.ID, Metadata.ID);
}
else
{
return false;
}
}
public override int GetHashCode()
{
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
return hashcode;
}
}
public class DialogJumpDialogPair
{
public IDialogJumpDialog Plugin { get; init; }
public PluginMetadata Metadata { get; init; }
public override string ToString()
{
return Metadata.Name;
}
public override bool Equals(object obj)
{
if (obj is DialogJumpDialogPair r)
{
return string.Equals(r.Metadata.ID, Metadata.ID);
}
else
{
return false;
}
}
public override int GetHashCode()
{
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
return hashcode;
}
}

View file

@ -0,0 +1,345 @@
using System;
using System.Threading;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using WindowsInput;
using WindowsInput.Native;
namespace Flow.Launcher.Infrastructure.DialogJump.Models
{
/// <summary>
/// Class for handling Windows File Dialog instances in DialogJump.
/// </summary>
public class WindowsDialog : IDialogJumpDialog
{
private const string WindowsDialogClassName = "#32770";
public IDialogJumpDialogWindow CheckDialogWindow(IntPtr hwnd)
{
// Is it a Win32 dialog box?
if (GetClassName(new(hwnd)) == WindowsDialogClassName)
{
// Is it a windows file dialog?
var dialogType = GetFileDialogType(new(hwnd));
if (dialogType != DialogType.Others)
{
return new WindowsDialogWindow(hwnd, dialogType);
}
}
return null;
}
public void Dispose()
{
}
#region Help Methods
private static unsafe string GetClassName(HWND handle)
{
fixed (char* buf = new char[256])
{
return PInvoke.GetClassName(handle, buf, 256) switch
{
0 => string.Empty,
_ => new string(buf),
};
}
}
private static DialogType GetFileDialogType(HWND handle)
{
// Is it a Windows Open file dialog?
var fileEditor = PInvoke.GetDlgItem(handle, 0x047C);
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open;
// Is it a Windows Save or Save As file dialog?
fileEditor = PInvoke.GetDlgItem(handle, 0x0000);
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs;
return DialogType.Others;
}
#endregion
}
public class WindowsDialogWindow : IDialogJumpDialogWindow
{
public IntPtr Handle { get; private set; } = IntPtr.Zero;
// After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore
// So we need to cache the current tab and use the original handle
private IDialogJumpDialogWindowTab _currentTab { get; set; } = null;
private readonly DialogType _dialogType;
internal WindowsDialogWindow(IntPtr handle, DialogType dialogType)
{
Handle = handle;
_dialogType = dialogType;
}
public IDialogJumpDialogWindowTab GetCurrentTab()
{
return _currentTab ??= new WindowsDialogTab(Handle, _dialogType);
}
public void Dispose()
{
}
}
public class WindowsDialogTab : IDialogJumpDialogWindowTab
{
#region Public Properties
public IntPtr Handle { get; private set; } = IntPtr.Zero;
#endregion
#region Private Fields
private static readonly string ClassName = nameof(WindowsDialogTab);
private static readonly InputSimulator _inputSimulator = new();
private readonly DialogType _dialogType;
private bool _legacy { get; set; } = false;
private HWND _pathControl { get; set; } = HWND.Null;
private HWND _pathEditor { get; set; } = HWND.Null;
private HWND _fileEditor { get; set; } = HWND.Null;
private HWND _openButton { get; set; } = HWND.Null;
#endregion
#region Constructor
internal WindowsDialogTab(IntPtr handle, DialogType dialogType)
{
Handle = handle;
_dialogType = dialogType;
Log.Debug(ClassName, $"File dialog type: {dialogType}");
}
#endregion
#region Public Methods
public string GetCurrentFolder()
{
if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty;
return GetWindowText(_pathEditor);
}
public string GetCurrentFile()
{
if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty;
return GetWindowText(_fileEditor);
}
public bool JumpFolder(string path, bool auto)
{
if (auto)
{
// Use legacy jump folder method for auto Dialog Jump because file editor is default value.
// After setting path using file editor, we do not need to revert its value.
return JumpFolderWithFileEditor(path, false);
}
// Alt-D or Ctrl-L to focus on the path input box
// "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus
_inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D);
// _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L);
if (_pathControl.IsNull && !GetPathControlEditor())
{
// https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1
// The dialog is a legacy one, so we can only edit file editor directly.
Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
var timeOut = !SpinWait.SpinUntil(() =>
{
var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0;
}, 1000);
if (timeOut)
{
// Path control is not visible, so we can only edit file editor directly.
Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
if (_pathEditor.IsNull)
{
// Path editor cannot be found, so we can only edit file editor directly.
Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method");
return JumpFolderWithFileEditor(path, true);
}
SetWindowText(_pathEditor, path);
_inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN);
return true;
}
public bool JumpFile(string path)
{
if (_fileEditor.IsNull && !GetFileEditor()) return false;
SetWindowText(_fileEditor, path);
return true;
}
public bool Open()
{
if (_openButton.IsNull && !GetOpenButton()) return false;
PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
return true;
}
public void Dispose()
{
}
#endregion
#region Helper Methods
#region Get Handles
private bool GetPathControlEditor()
{
// Get the handle of the path editor
// Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control
_pathControl = PInvoke.FindWindowEx(new(Handle), HWND.Null, "WorkerW", null); // 0x0000
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205
if (_pathControl == HWND.Null)
{
_pathEditor = HWND.Null;
_legacy = true;
Log.Info(ClassName, "Legacy dialog");
}
else
{
_pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox
_pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit
if (_pathEditor == HWND.Null)
{
_legacy = true;
Log.Error(ClassName, "Failed to find path editor handle");
}
}
return !_legacy;
}
private bool GetFileEditor()
{
if (_dialogType == DialogType.Open)
{
// Get the handle of the file name editor of Open file dialog
_fileEditor = PInvoke.GetDlgItem(new(Handle), 0x047C); // ComboBoxEx32
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit
}
else
{
// Get the handle of the file name editor of Save / SaveAs file dialog
_fileEditor = PInvoke.GetDlgItem(new(Handle), 0x0000); // DUIViewWndClassName
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit
}
if (_fileEditor == HWND.Null)
{
Log.Error(ClassName, "Failed to find file name editor handle");
return false;
}
return true;
}
private bool GetOpenButton()
{
// Get the handle of the open button
_openButton = PInvoke.GetDlgItem(new(Handle), 0x0001); // Open/Save/SaveAs Button
if (_openButton == HWND.Null)
{
Log.Error(ClassName, "Failed to find open button handle");
return false;
}
return true;
}
#endregion
#region Windows Text
private static unsafe string GetWindowText(HWND handle)
{
int length;
Span<char> buffer = stackalloc char[1000];
fixed (char* pBuffer = buffer)
{
// If the control has no title bar or text, or if the control handle is invalid, the return value is zero.
length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer);
}
return buffer[..length].ToString();
}
private static unsafe nint SetWindowText(HWND handle, string text)
{
fixed (char* textPtr = text + '\0')
{
return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value;
}
}
#endregion
#region Legacy Jump Folder
private bool JumpFolderWithFileEditor(string path, bool resetFocus)
{
// For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors.
if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false;
if (_fileEditor.IsNull && !GetFileEditor()) return false;
SetWindowText(_fileEditor, path);
if (_openButton.IsNull && !GetOpenButton()) return false;
PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
return true;
}
#endregion
#endregion
}
internal enum DialogType
{
Others,
Open,
SaveOrSaveAs
}
}

View file

@ -0,0 +1,260 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Flow.Launcher.Plugin;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
namespace Flow.Launcher.Infrastructure.DialogJump.Models
{
/// <summary>
/// Class for handling Windows Explorer instances in DialogJump.
/// </summary>
public class WindowsExplorer : IDialogJumpExplorer
{
public IDialogJumpExplorerWindow CheckExplorerWindow(IntPtr hwnd)
{
IDialogJumpExplorerWindow explorerWindow = null;
// Is it from Explorer?
var processName = Win32Helper.GetProcessNameFromHwnd(new(hwnd));
if (processName.Equals("explorer.exe", StringComparison.OrdinalIgnoreCase))
{
EnumerateShellWindows((shellWindow) =>
{
try
{
if (shellWindow is not IWebBrowser2 explorer) return true;
if (explorer.HWND != hwnd) return true;
explorerWindow = new WindowsExplorerWindow(hwnd);
return false;
}
catch
{
// Ignored
}
return true;
});
}
return explorerWindow;
}
internal static unsafe void EnumerateShellWindows(Func<object, bool> action)
{
// Create an instance of ShellWindows
var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass
var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows
var result = PInvoke.CoCreateInstance(
&clsidShellWindows,
null,
CLSCTX.CLSCTX_ALL,
&iidIShellWindows,
out var shellWindowsObj);
if (result.Failed) return;
var shellWindows = (IShellWindows)shellWindowsObj;
// Enumerate the shell windows
var count = shellWindows.Count;
for (var i = 0; i < count; i++)
{
if (!action(shellWindows.Item(i)))
{
return;
}
}
}
public void Dispose()
{
}
}
public class WindowsExplorerWindow : IDialogJumpExplorerWindow
{
public IntPtr Handle { get; }
private static Guid _shellBrowserGuid = typeof(IShellBrowser).GUID;
internal WindowsExplorerWindow(IntPtr handle)
{
Handle = handle;
}
public string GetExplorerPath()
{
if (Handle == IntPtr.Zero) return null;
var activeTabHandle = GetActiveTabHandle(new(Handle));
if (activeTabHandle.IsNull) return null;
var window = GetExplorerByTabHandle(activeTabHandle);
if (window == null) return null;
var path = GetLocation(window);
return path;
}
public void Dispose()
{
}
#region Helper Methods
// Inspired by: https://github.com/w4po/ExplorerTabUtility
private static HWND GetActiveTabHandle(HWND windowHandle)
{
// Active tab always at the top of the z-index, so it is the first child of the ShellTabWindowClass.
var activeTab = PInvoke.FindWindowEx(windowHandle, HWND.Null, "ShellTabWindowClass", null);
return activeTab;
}
private static IWebBrowser2 GetExplorerByTabHandle(HWND tabHandle)
{
if (tabHandle.IsNull) return null;
IWebBrowser2 window = null;
WindowsExplorer.EnumerateShellWindows((shellWindow) =>
{
try
{
return StartSTAThread(() =>
{
if (shellWindow is not IWebBrowser2 explorer) return true;
if (explorer is not IServiceProvider sp) return true;
sp.QueryService(ref _shellBrowserGuid, ref _shellBrowserGuid, out var shellBrowser);
if (shellBrowser == null) return true;
try
{
shellBrowser.GetWindow(out var hWnd); // Must execute in STA thread to get this hWnd
if (hWnd == tabHandle)
{
window = explorer;
return false;
}
}
catch
{
// Ignored
}
finally
{
Marshal.ReleaseComObject(shellBrowser);
}
return true;
}) ?? true;
}
catch
{
// Ignored
}
return true;
});
return window;
}
private static bool? StartSTAThread(Func<bool> action)
{
bool? result = null;
var thread = new Thread(() =>
{
result = action();
})
{
IsBackground = true
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return result;
}
private static string GetLocation(IWebBrowser2 window)
{
var path = window.LocationURL.ToString();
if (!string.IsNullOrWhiteSpace(path)) return NormalizeLocation(path);
// Recycle Bin, This PC, etc
if (window.Document is not IShellFolderViewDual folderView) return null;
// Attempt to get the path from the folder view
try
{
// CSWin32 Folder does not have Self, so we need to use dynamic type here
// Use dynamic to bypass static typing
dynamic folder = folderView.Folder;
// Access the Self property via dynamic binding
dynamic folderItem = folder.Self;
// Get path from the folder item
path = folderItem.Path;
}
catch
{
return null;
}
return NormalizeLocation(path);
}
private static string NormalizeLocation(string location)
{
if (location.IndexOf('%') > -1)
location = Environment.ExpandEnvironmentVariables(location);
if (location.StartsWith("::", StringComparison.Ordinal))
location = $"shell:{location}";
else if (location.StartsWith("{", StringComparison.Ordinal))
location = $"shell:::{location}";
location = location.Trim(' ', '/', '\\', '\n', '\'', '"');
return location.Replace('/', '\\');
}
#endregion
}
#region COM Interfaces
// Inspired by: https://github.com/w4po/ExplorerTabUtility
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("6d5140c1-7436-11ce-8034-00aa006009fa")]
[ComImport]
public interface IServiceProvider
{
[PreserveSig]
int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellBrowser ppvObject);
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214E2-0000-0000-C000-000000000046")]
[ComImport]
public interface IShellBrowser
{
[PreserveSig]
int GetWindow(out nint handle);
}
#endregion
}

View file

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Windows.Win32;
namespace Flow.Launcher.Infrastructure
{
@ -13,9 +9,10 @@ namespace Flow.Launcher.Infrastructure
/// </summary>
public static string GetActiveExplorerPath()
{
var explorerWindow = GetActiveExplorer();
string locationUrl = explorerWindow?.LocationURL;
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
var explorerPath = DialogJump.DialogJump.GetActiveExplorerPath();
return !string.IsNullOrEmpty(explorerPath) ?
GetDirectoryPath(new Uri(explorerPath).LocalPath) :
null;
}
/// <summary>
@ -23,74 +20,12 @@ namespace Flow.Launcher.Infrastructure
/// </summary>
private static string GetDirectoryPath(string path)
{
if (!path.EndsWith("\\"))
if (!path.EndsWith('\\'))
{
return path + "\\";
}
return path;
}
/// <summary>
/// Gets the file explorer that is currently in the foreground
/// </summary>
private static dynamic GetActiveExplorer()
{
Type type = Type.GetTypeFromProgID("Shell.Application");
if (type == null) return null;
dynamic shell = Activator.CreateInstance(type);
if (shell == null)
{
return null;
}
var explorerWindows = new List<dynamic>();
var openWindows = shell.Windows();
for (int i = 0; i < openWindows.Count; i++)
{
var window = openWindows.Item(i);
if (window == null) continue;
// find the desired window and make sure that it is indeed a file explorer
// we don't want the Internet Explorer or the classic control panel
// ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE"
if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe")
{
explorerWindows.Add(window);
}
}
if (explorerWindows.Count == 0) return null;
var zOrders = GetZOrder(explorerWindows);
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
}
/// <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>
private static IEnumerable<int> GetZOrder(List<dynamic> hWnds)
{
var z = new int[hWnds.Count];
for (var i = 0; i < hWnds.Count; i++) z[i] = -1;
var index = 0;
var numRemaining = hWnds.Count;
PInvoke.EnumWindows((wnd, _) =>
{
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
if (searchIndex != -1)
{
z[searchIndex] = index;
numRemaining--;
if (numRemaining == 0) return false;
}
index++;
return true;
}, IntPtr.Zero);
return z;
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
<OutputType>Library</OutputType>
<UseWpf>true</UseWpf>
@ -12,6 +12,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -33,6 +34,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -53,28 +55,43 @@
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="BitFaster.Caching" Version="2.5.3" />
<PackageReference Include="BitFaster.Caching" Version="2.5.4" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Fody" Version="6.5.5">
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
<PackageReference Include="Fody" Version="6.9.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="InputSimulator" Version="1.0.4" />
<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">
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.14.15" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.205">
<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="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="NLog" Version="6.0.4" />
<PackageReference Include="NLog.OutputDebugString" Version="6.0.4" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="SharpVectors.Wpf" Version="1.8.4.2" />
<PackageReference Include="SharpVectors.Wpf" Version="1.8.5" />
<!-- Do not upgrade this to higher version since it can cause this issue on WinForm platform: -->
<!-- PlatformNotSupportedException: SystemEvents is not supported on this platform. -->
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
</ItemGroup>
<PropertyGroup>
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Remove="Languages\en.xaml" />
<AdditionalFiles Include="..\Flow.Launcher\Languages\en.xaml">
<Link>Languages\en.xaml</Link>
</AdditionalFiles>
</ItemGroup>
</Project>

View file

@ -4,10 +4,8 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using JetBrains.Annotations;
namespace Flow.Launcher.Infrastructure.Http
@ -78,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Http
}
catch (UriFormatException e)
{
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsg("Please try again", "Unable to parse Http Proxy");
PublicApi.Instance.ShowMsgError(Localize.pleaseTryAgain(), Localize.parseProxyFailed());
Log.Exception(ClassName, "Unable to parse Uri", e);
}
}

View file

@ -0,0 +1,22 @@
namespace Flow.Launcher.Infrastructure
{
/// <summary>
/// Translate a language to English letters using a given rule.
/// </summary>
public interface IAlphabet
{
/// <summary>
/// Translate a string to English letters, using a given rule.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public (string translation, TranslationMapping map) Translate(string stringToTranslate);
/// <summary>
/// Determine if a string should be translated to English letter with this Alphabet.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public bool ShouldTranslate(string stringToTranslate);
}
}

View file

@ -19,37 +19,42 @@ namespace Flow.Launcher.Infrastructure.Image
private static readonly string ClassName = nameof(ImageLoader);
private static readonly ImageCache ImageCache = new();
private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
private static Lock storageLock { get; } = new();
private static BinaryStorage<List<(string, bool)>> _storage;
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
private static IImageHashGenerator _hashGenerator;
private static ImageHashGenerator _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 static ImageSource Image => ImageCache[Constant.ImageIcon, false];
public static ImageSource MissingImage => ImageCache[Constant.MissingImgIcon, false];
public static ImageSource LoadingImage => ImageCache[Constant.LoadingImgIcon, false];
public const int SmallIconSize = 64;
public const int FullIconSize = 256;
public const int FullImageSize = 320;
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
private static readonly string[] ImageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"];
private static readonly string SvgExtension = ".svg";
public static async Task InitializeAsync()
{
_storage = new BinaryStorage<List<(string, bool)>>("Image");
_hashGenerator = new ImageHashGenerator();
var usage = await LoadStorageToConcurrentDictionaryAsync();
_storage.ClearData();
ImageCache.Initialize(usage);
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
var usage = await Task.Run(() =>
{
ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze();
ImageCache[icon, false] = img;
}
_storage = new BinaryStorage<List<(string, bool)>>("Image");
_hashGenerator = new ImageHashGenerator();
var usage = LoadStorageToConcurrentDictionary();
_storage.ClearData();
ImageCache.Initialize(usage);
foreach (var icon in new[] { Constant.DefaultIcon, Constant.ImageIcon, Constant.MissingImgIcon, Constant.LoadingImgIcon })
{
ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze();
ImageCache[icon, false] = img;
}
return usage;
});
_ = Task.Run(async () =>
{
@ -64,42 +69,26 @@ namespace Flow.Launcher.Infrastructure.Image
});
}
public static async Task SaveAsync()
public static void Save()
{
await storageLock.WaitAsync();
try
lock (storageLock)
{
await _storage.SaveAsync(ImageCache.EnumerateEntries()
.Select(x => x.Key)
.ToList());
}
catch (System.Exception e)
{
Log.Exception(ClassName, "Failed to save image cache to file", e);
}
finally
{
storageLock.Release();
try
{
_storage.Save([.. ImageCache.EnumerateEntries().Select(x => x.Key)]);
}
catch (System.Exception e)
{
Log.Exception(ClassName, "Failed to save image cache to file", e);
}
}
}
public static async Task WaitSaveAsync()
private static List<(string, bool)> LoadStorageToConcurrentDictionary()
{
await storageLock.WaitAsync();
storageLock.Release();
}
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
{
await storageLock.WaitAsync();
try
lock (storageLock)
{
return await _storage.TryLoadAsync(new List<(string, bool)>());
}
finally
{
storageLock.Release();
return _storage.TryLoad([]);
}
}
@ -174,7 +163,7 @@ namespace Flow.Launcher.Infrastructure.Image
Log.Exception(ClassName, $"Failed to get thumbnail for {path} on first try", e);
Log.Exception(ClassName, $"Failed to get thumbnail for {path} on second try", e2);
ImageSource image = ImageCache[Constant.MissingImgIcon, false];
ImageSource image = MissingImage;
ImageCache[path, false] = image;
imageResult = new ImageResult(image, ImageType.Error);
}
@ -273,7 +262,7 @@ namespace Flow.Launcher.Infrastructure.Image
}
else
{
image = ImageCache[Constant.MissingImgIcon, false];
image = MissingImage;
path = Constant.MissingImgIcon;
}
@ -338,7 +327,7 @@ namespace Flow.Launcher.Infrastructure.Image
return img;
}
private static ImageSource LoadFullImage(string path)
private static BitmapImage LoadFullImage(string path)
{
BitmapImage image = new BitmapImage();
image.BeginInit();
@ -375,7 +364,7 @@ namespace Flow.Launcher.Infrastructure.Image
return image;
}
private static ImageSource LoadSvgImage(string path, bool loadFullImage = false)
private static RenderTargetBitmap LoadSvgImage(string path, bool loadFullImage = false)
{
// Set up drawing settings
var desiredHeight = loadFullImage ? FullImageSize : SmallIconSize;

View file

@ -1,13 +1,14 @@
using System;
using System.Runtime.InteropServices;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using IniParser;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.Shell;
namespace Flow.Launcher.Infrastructure.Image
{
@ -35,9 +36,32 @@ namespace Flow.Launcher.Infrastructure.Image
private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205;
private const string UrlExtension = ".url";
/// <summary>
/// Obtains a BitmapSource thumbnail for the specified file.
/// </summary>
/// <remarks>
/// If the file is a Windows URL shortcut (".url"), the method attempts to resolve the shortcut's icon and use that for the thumbnail; otherwise it requests a thumbnail for the file path. The native HBITMAP used to create the BitmapSource is always released to avoid native memory leaks.
/// </remarks>
/// <param name="fileName">Path to the file (can be a regular file or a ".url" shortcut).</param>
/// <param name="width">Requested thumbnail width in pixels.</param>
/// <param name="height">Requested thumbnail height in pixels.</param>
/// <param name="options">Thumbnail extraction options (flags) controlling fallback and caching behavior.</param>
/// <returns>A BitmapSource representing the requested thumbnail.</returns>
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
{
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
HBITMAP hBitmap;
var extension = Path.GetExtension(fileName);
if (string.Equals(extension, UrlExtension, StringComparison.OrdinalIgnoreCase))
{
hBitmap = GetHBitmapForUrlFile(fileName, width, height, options);
}
else
{
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
}
try
{
@ -50,6 +74,21 @@ namespace Flow.Launcher.Infrastructure.Image
}
}
/// <summary>
/// Obtains a native HBITMAP for the specified file at the requested size using the Windows Shell image factory.
/// </summary>
/// <remarks>
/// If <paramref name="options"/> is <see cref="ThumbnailOptions.ThumbnailOnly"/> and thumbnail extraction fails
/// due to extraction errors or a missing path, the method falls back to requesting an icon (<see cref="ThumbnailOptions.IconOnly"/>).
/// The returned HBITMAP is a raw GDI handle; the caller is responsible for releasing it (e.g., via DeleteObject) to avoid native memory leaks.
/// </remarks>
/// <param name="fileName">Path to the file to thumbnail.</param>
/// <param name="width">Requested thumbnail width in pixels.</param>
/// <param name="height">Requested thumbnail height in pixels.</param>
/// <param name="options">Thumbnail request flags that control behavior (e.g., ThumbnailOnly, IconOnly).</param>
/// <returns>An HBITMAP handle containing the image. Caller must free the handle when finished.</returns>
/// <exception cref="COMException">If creating the shell item fails (HRESULT returned by SHCreateItemFromParsingName).</exception>
/// <exception cref="InvalidOperationException">If the shell item does not expose IShellItemImageFactory or if an unexpected error occurs while obtaining the image.</exception>
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
{
var retCode = PInvoke.SHCreateItemFromParsingName(
@ -108,5 +147,44 @@ namespace Flow.Launcher.Infrastructure.Image
return hBitmap;
}
/// <summary>
/// Obtains an HBITMAP for a Windows .url shortcut by resolving its IconFile entry and delegating to GetHBitmap.
/// </summary>
/// <remarks>
/// The method parses the .url file as an INI, looks in the "InternetShortcut" section for the "IconFile" entry,
/// and requests a bitmap for that icon path. If no IconFile is present or any error occurs while reading or
/// resolving the icon, it falls back to requesting a thumbnail for the .url file itself.
/// </remarks>
/// <param name="fileName">Path to the .url shortcut file.</param>
/// <param name="width">Requested thumbnail width (pixels).</param>
/// <param name="height">Requested thumbnail height (pixels).</param>
/// <param name="options">ThumbnailOptions flags controlling extraction behavior.</param>
/// <returns>An HBITMAP containing the requested image; callers are responsible for freeing the native handle.</returns>
private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, int height, ThumbnailOptions options)
{
HBITMAP hBitmap;
try
{
var parser = new FileIniDataParser();
var data = parser.ReadFile(fileName);
var urlSection = data["InternetShortcut"];
var iconPath = urlSection?["IconFile"];
if (!File.Exists(iconPath))
{
// If the IconFile is missing, throw exception to fallback to the default icon
throw new FileNotFoundException("Icon file not specified in Internet shortcut (.url) file.");
}
hBitmap = GetHBitmap(Path.GetFullPath(iconPath), width, height, options);
}
catch
{
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
}
return hBitmap;
}
}
}

View file

@ -34,7 +34,7 @@ namespace Flow.Launcher.Infrastructure.Logger
var fileTarget = new FileTarget
{
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt",
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/Flow.Launcher.${date:format=yyyy-MM-dd}.log",
Layout = layout
};
@ -65,26 +65,22 @@ namespace Flow.Launcher.Infrastructure.Logger
public static void SetLogLevel(LOGLEVEL level)
{
switch (level)
var rule = LogManager.Configuration.FindRuleByName("file");
var nlogLevel = level switch
{
case LOGLEVEL.DEBUG:
UseDebugLogLevel();
break;
default:
UseInfoLogLevel();
break;
}
Info(nameof(Logger), $"Using log level: {level}.");
}
LOGLEVEL.NONE => LogLevel.Off,
LOGLEVEL.ERROR => LogLevel.Error,
LOGLEVEL.DEBUG => LogLevel.Debug,
_ => LogLevel.Info
};
private static void UseDebugLogLevel()
{
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
}
rule.SetLoggingLevels(nlogLevel, LogLevel.Fatal);
private static void UseInfoLogLevel()
{
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
LogManager.ReconfigExistingLoggers();
// We can't log Info when level is set to Error or None, so we use Debug
Debug(nameof(Logger), $"Using log level: {level}.");
}
private static void LogFaultyFormat(string message)
@ -169,7 +165,9 @@ namespace Flow.Launcher.Infrastructure.Logger
public enum LOGLEVEL
{
DEBUG,
INFO
NONE,
ERROR,
INFO,
DEBUG
}
}

View file

@ -34,12 +34,6 @@ WINDOW_STYLE
SetLastError
WINDOW_EX_STYLE
GetSystemMetrics
EnumDisplayMonitors
MonitorFromWindow
GetMonitorInfo
MONITORINFOEXW
WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE
WM_NCLBUTTONDBLCLK
@ -66,3 +60,35 @@ LOCALE_TRANSIENT_KEYBOARD4
SHParseDisplayName
SHOpenFolderAndSelectItems
CoTaskMemFree
SetWinEventHook
UnhookWinEvent
SendMessage
EVENT_SYSTEM_FOREGROUND
WINEVENT_OUTOFCONTEXT
WM_SETTEXT
IShellFolderViewDual2
CoCreateInstance
CLSCTX
IShellWindows
IWebBrowser2
EVENT_OBJECT_DESTROY
EVENT_OBJECT_LOCATIONCHANGE
EVENT_SYSTEM_MOVESIZESTART
EVENT_SYSTEM_MOVESIZEEND
GetDlgItem
PostMessage
BM_CLICK
WM_GETTEXT
OpenProcess
QueryFullProcessImageName
EVENT_OBJECT_HIDE
EVENT_SYSTEM_DIALOGEND
DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
WM_POWERBROADCAST
PBT_APMRESUMEAUTOMATIC
PBT_APMRESUMESUSPEND
PowerRegisterSuspendResumeNotification
PowerUnregisterSuspendResumeNotification
DeviceNotifyCallbackRoutine

View file

@ -1,209 +1,193 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
using JetBrains.Annotations;
using System.Text.Json;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings;
using ToolGood.Words.Pinyin;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher.Infrastructure
{
public class TranslationMapping
{
private bool constructed;
private List<int> originalIndexs = new List<int>();
private List<int> translatedIndexs = new List<int>();
private int translatedLength = 0;
public string key { get; private set; }
public void setKey(string key)
{
this.key = key;
}
public void AddNewIndex(int originalIndex, int translatedIndex, int length)
{
if (constructed)
throw new InvalidOperationException("Mapping shouldn't be changed after constructed");
originalIndexs.Add(originalIndex);
translatedIndexs.Add(translatedIndex);
translatedIndexs.Add(translatedIndex + length);
translatedLength += length - 1;
}
public int MapToOriginalIndex(int translatedIndex)
{
if (translatedIndex > translatedIndexs.Last())
return translatedIndex - translatedLength - 1;
int lowerBound = 0;
int upperBound = originalIndexs.Count - 1;
int count = 0;
// Corner case handle
if (translatedIndex < translatedIndexs[0])
return translatedIndex;
if (translatedIndex > translatedIndexs.Last())
{
int indexDef = 0;
for (int k = 0; k < originalIndexs.Count; k++)
{
indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2];
}
return translatedIndex - indexDef - 1;
}
// Binary Search with Range
for (int i = originalIndexs.Count / 2;; count++)
{
if (translatedIndex < translatedIndexs[i * 2])
{
// move to lower middle
upperBound = i;
i = (i + lowerBound) / 2;
}
else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1)
{
lowerBound = i;
// move to upper middle
// due to floor of integer division, move one up on corner case
i = (i + upperBound + 1) / 2;
}
else
return originalIndexs[i];
if (upperBound - lowerBound <= 1 &&
translatedIndex > translatedIndexs[lowerBound * 2 + 1] &&
translatedIndex < translatedIndexs[upperBound * 2])
{
int indexDef = 0;
for (int j = 0; j < upperBound; j++)
{
indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2];
}
return translatedIndex - indexDef - 1;
}
}
}
public void endConstruct()
{
if (constructed)
throw new InvalidOperationException("Mapping has already been constructed");
constructed = true;
}
}
/// <summary>
/// Translate a language to English letters using a given rule.
/// </summary>
public interface IAlphabet
{
/// <summary>
/// Translate a string to English letters, using a given rule.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public (string translation, TranslationMapping map) Translate(string stringToTranslate);
/// <summary>
/// Determine if a string can be translated to English letter with this Alphabet.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public bool CanBeTranslated(string stringToTranslate);
}
public class PinyinAlphabet : IAlphabet
{
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
new ConcurrentDictionary<string, (string translation, TranslationMapping map)>();
private Settings _settings;
private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = new();
private readonly Settings _settings;
private ReadOnlyDictionary<string, string> currentDoublePinyinTable;
public PinyinAlphabet()
{
Initialize(Ioc.Default.GetRequiredService<Settings>());
_settings = Ioc.Default.GetRequiredService<Settings>();
LoadDoublePinyinTable();
_settings.PropertyChanged += (sender, e) =>
{
switch (e.PropertyName)
{
case nameof (Settings.ShouldUsePinyin):
if (_settings.ShouldUsePinyin)
{
Reload();
}
break;
case nameof(Settings.UseDoublePinyin):
case nameof(Settings.DoublePinyinSchema):
if (_settings.UseDoublePinyin)
{
Reload();
}
break;
}
};
}
private void Initialize([NotNull] Settings settings)
public void Reload()
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
LoadDoublePinyinTable();
_pinyinCache.Clear();
}
public bool CanBeTranslated(string stringToTranslate)
private void CreateDoublePinyinTableFromStream(Stream jsonStream)
{
return WordsHelper.HasChinese(stringToTranslate);
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null");
var schemaKey = _settings.DoublePinyinSchema.ToString();
if (!table.TryGetValue(schemaKey, out var schemaDict))
{
throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken.");
}
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(schemaDict);
}
private void LoadDoublePinyinTable()
{
if (!_settings.UseDoublePinyin)
{
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
return;
}
var tablePath = Path.Combine(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
try
{
using var fs = File.OpenRead(tablePath);
CreateDoublePinyinTableFromStream(fs);
}
catch (FileNotFoundException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Double pinyin table file not found: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (DirectoryNotFoundException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (UnauthorizedAccessException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (System.Exception e)
{
Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
}
public bool ShouldTranslate(string stringToTranslate)
{
// If the query (stringToTranslate) does NOT contain Chinese characters,
// we should translate the target string to pinyin for matching
return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate);
}
public (string translation, TranslationMapping map) Translate(string content)
{
if (_settings.ShouldUsePinyin)
{
if (!_pinyinCache.ContainsKey(content))
{
return BuildCacheFromContent(content);
}
else
{
return _pinyinCache[content];
}
}
return (content, null);
if (!_settings.ShouldUsePinyin || !ContainsChinese(content))
return (content, null);
return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content);
}
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
{
if (WordsHelper.HasChinese(content))
var resultList = WordsHelper.GetPinyinList(content);
var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity
var map = new TranslationMapping();
var previousIsChinese = false;
for (var i = 0; i < resultList.Length; i++)
{
var resultList = WordsHelper.GetPinyinList(content);
StringBuilder resultBuilder = new StringBuilder();
TranslationMapping map = new TranslationMapping();
bool pre = false;
for (int i = 0; i < resultList.Length; i++)
if (IsChineseCharacter(content[i]))
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i];
if (i > 0)
{
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
resultBuilder.Append(' ');
resultBuilder.Append(resultList[i]);
pre = true;
}
else
{
if (pre)
{
pre = false;
resultBuilder.Append(' ');
}
resultBuilder.Append(resultList[i]);
}
map.AddNewIndex(resultBuilder.Length, translated.Length);
resultBuilder.Append(translated);
previousIsChinese = true;
}
else
{
// Add space after Chinese characters before non-Chinese characters
if (previousIsChinese)
{
previousIsChinese = false;
resultBuilder.Append(' ');
}
map.AddNewIndex(resultBuilder.Length, resultList[i].Length);
resultBuilder.Append(resultList[i]);
}
map.endConstruct();
var key = resultBuilder.ToString();
map.setKey(key);
return _pinyinCache[content] = (key, map);
}
else
map.EndConstruct();
var translation = resultBuilder.ToString();
var result = (translation, map);
return _pinyinCache[content] = result;
}
/// <summary>
/// Optimized Chinese character detection using the comprehensive CJK Unicode ranges
/// </summary>
private static bool ContainsChinese(ReadOnlySpan<char> text)
{
foreach (var c in text)
{
return (content, null);
if (IsChineseCharacter(c))
return true;
}
return false;
}
/// <summary>
/// Check if a character is a Chinese character using comprehensive Unicode ranges
/// Covers CJK Unified Ideographs, Extension A
/// </summary>
private static bool IsChineseCharacter(char c)
{
return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs
(c >= 0x3400 && c <= 0x4DBF); // CJK Extension A
}
private string ToDoublePinyin(string fullPinyin)
{
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
? doublePinyinValue
: fullPinyin;
}
}
}

View file

@ -12,7 +12,7 @@ using MemoryPack;
namespace Flow.Launcher.Infrastructure.Storage
{
/// <summary>
/// Stroage object using binary data
/// Storage object using binary data
/// Normally, it has better performance, but not readable
/// </summary>
/// <remarks>
@ -53,6 +53,45 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
public T TryLoad(T defaultData)
{
if (Data != null) return Data;
if (File.Exists(FilePath))
{
if (new FileInfo(FilePath).Length == 0)
{
Log.Error(ClassName, $"Zero length cache file <{FilePath}>");
Data = defaultData;
Save();
}
var bytes = File.ReadAllBytes(FilePath);
Data = Deserialize(bytes, defaultData);
}
else
{
Log.Info(ClassName, "Cache file not exist, load default data");
Data = defaultData;
Save();
}
return Data;
}
private T Deserialize(ReadOnlySpan<byte> bytes, T defaultData)
{
try
{
var t = MemoryPackSerializer.Deserialize<T>(bytes);
return t ?? defaultData;
}
catch (System.Exception e)
{
Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e);
return defaultData;
}
}
public async ValueTask<T> TryLoadAsync(T defaultData)
{
if (Data != null) return Data;
@ -79,26 +118,31 @@ namespace Flow.Launcher.Infrastructure.Storage
return Data;
}
private static async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
private async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
{
try
{
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
return t ?? defaultData;
}
catch (System.Exception)
catch (System.Exception e)
{
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e);
return defaultData;
}
}
public void Save()
{
Save(Data.NonNull());
}
public void Save(T data)
{
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
var serialized = MemoryPackSerializer.Serialize(Data);
var serialized = MemoryPackSerializer.Serialize(data);
File.WriteAllBytes(FilePath, serialized);
}
@ -107,15 +151,6 @@ namespace Flow.Launcher.Infrastructure.Storage
await SaveAsync(Data.NonNull());
}
// ImageCache need to convert data into concurrent dictionary for usage,
// so we would better to clear the data
public void ClearData()
{
Data = default;
}
// ImageCache storages data in its class,
// so we need to pass it to SaveAsync
public async ValueTask SaveAsync(T data)
{
// User may delete the directory, so we need to check it
@ -124,5 +159,12 @@ namespace Flow.Launcher.Infrastructure.Storage
await using var stream = new FileStream(FilePath, FileMode.Create);
await MemoryPackSerializer.SerializeAsync(stream, data);
}
// ImageCache need to convert data into concurrent dictionary for usage,
// so we would better to clear the data
public void ClearData()
{
Data = default;
}
}
}

View file

@ -68,7 +68,7 @@ namespace Flow.Launcher.Infrastructure
query = query.Trim();
TranslationMapping translationMapping = null;
if (_alphabet is not null && !_alphabet.CanBeTranslated(query))
if (_alphabet is not null && _alphabet.ShouldTranslate(query))
{
// We assume that if a query can be translated (containing characters of a language, like Chinese)
// it actually means user doesn't want it to be translated to English letters.
@ -228,7 +228,7 @@ namespace Flow.Launcher.Infrastructure
return new MatchResult(false, UserSettingSearchPrecision);
}
private bool IsAcronym(string stringToCompare, int compareStringIndex)
private static bool IsAcronym(string stringToCompare, int compareStringIndex)
{
if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex))
return true;
@ -237,7 +237,7 @@ namespace Flow.Launcher.Infrastructure
}
// When counting acronyms, treat a set of numbers as one acronym ie. Visual 2019 as 2 acronyms instead of 5
private bool IsAcronymCount(string stringToCompare, int compareStringIndex)
private static bool IsAcronymCount(string stringToCompare, int compareStringIndex)
{
if (IsAcronymChar(stringToCompare, compareStringIndex))
return true;

View file

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
namespace Flow.Launcher.Infrastructure
{
public class TranslationMapping
{
private bool _isConstructed;
// Assuming one original item maps to multi translated items
// list[i] is the last translated index + 1 of original index i
private readonly List<int> _originalToTranslated = new();
public void AddNewIndex(int translatedIndex, int length)
{
if (_isConstructed)
throw new InvalidOperationException("Mapping shouldn't be changed after construction");
_originalToTranslated.Add(translatedIndex + length);
}
public int MapToOriginalIndex(int translatedIndex)
{
var searchResult = _originalToTranslated.BinarySearch(translatedIndex);
return searchResult >= 0 ? searchResult : ~searchResult;
}
public void EndConstruct()
{
if (_isConstructed)
throw new InvalidOperationException("Mapping has already been constructed");
_isConstructed = true;
}
}
}

View file

@ -1,58 +0,0 @@
using System;
using System.Windows.Markup;
namespace Flow.Launcher.Infrastructure.UI
{
[Obsolete("EnumBindingSourceExtension is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
public class EnumBindingSourceExtension : MarkupExtension
{
private Type _enumType;
public Type EnumType
{
get { return _enumType; }
set
{
if (value != _enumType)
{
if (value != null)
{
Type enumType = Nullable.GetUnderlyingType(value) ?? value;
if (!enumType.IsEnum)
{
throw new ArgumentException("Type must represent an enum.");
}
}
_enumType = value;
}
}
}
public EnumBindingSourceExtension() { }
public EnumBindingSourceExtension(Type enumType)
{
EnumType = enumType;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (_enumType == null)
{
throw new InvalidOperationException("The EnumType must be specified.");
}
Type actualEnumType = Nullable.GetUnderlyingType(_enumType) ?? _enumType;
Array enumValues = Enum.GetValues(actualEnumType);
if (actualEnumType == _enumType)
{
return enumValues;
}
Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1);
enumValues.CopyTo(tempArray, 1);
return tempArray;
}
}
}

View file

@ -1,11 +1,13 @@
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Infrastructure.UserSettings
{
public class CustomBrowserViewModel : BaseModel
{
public string Name { get; set; }
[JsonIgnore]
public string DisplayName => Name == "Default" ? Localize.defaultBrowser_default() : Name;
public string Path { get; set; }
public string PrivateArg { get; set; }
public bool EnablePrivate { get; set; }
@ -26,8 +28,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings
Editable = Editable
};
}
public void OnDisplayNameChanged()
{
OnPropertyChanged(nameof(DisplayName));
}
}
}

View file

@ -1,10 +1,13 @@
using Flow.Launcher.Plugin;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.ViewModel
namespace Flow.Launcher.Infrastructure.UserSettings
{
public class CustomExplorerViewModel : BaseModel
{
public string Name { get; set; }
[JsonIgnore]
public string DisplayName => Name == "Explorer" ? Localize.fileManagerExplorer() : Name;
public string Path { get; set; }
public string FileArgument { get; set; } = "\"%d\"";
public string DirectoryArgument { get; set; } = "\"%d\"";
@ -21,5 +24,10 @@ namespace Flow.Launcher.ViewModel
Editable = Editable
};
}
public void OnDisplayNameChanged()
{
OnPropertyChanged(nameof(DisplayName));
}
}
}

View file

@ -1,8 +1,6 @@
using System;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings
{
@ -55,11 +53,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
public string Description { get; set; }
public string LocalizedDescription => API.GetTranslation(Description);
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
public string LocalizedDescription => PublicApi.Instance.GetTranslation(Description);
public BaseBuiltinShortcutModel(string key, string description)
{

View file

@ -7,8 +7,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
public const string PortableFolderName = "UserData";
public const string DeletionIndicatorFile = ".dead";
public static string PortableDataPath = Path.Combine(Constant.ProgramDirectory, PortableFolderName);
public static string RoamingDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher");
public static readonly string PortableDataPath = Path.Combine(Constant.ProgramDirectory, PortableFolderName);
public static readonly string RoamingDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher");
public static string DataDirectory()
{
if (PortableDataLocationInUse())
@ -19,7 +19,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public static bool PortableDataLocationInUse()
{
if (Directory.Exists(PortableDataPath) && !File.Exists(DeletionIndicatorFile))
if (Directory.Exists(PortableDataPath) &&
!File.Exists(Path.Combine(PortableDataPath, DeletionIndicatorFile)))
return true;
return false;

View file

@ -1,4 +1,5 @@
using Flow.Launcher.Plugin;
using System;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings
{
@ -6,5 +7,26 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
public string Hotkey { get; set; }
public string ActionKeyword { get; set; }
public CustomPluginHotkey(string hotkey, string actionKeyword)
{
Hotkey = hotkey;
ActionKeyword = actionKeyword;
}
public override bool Equals(object other)
{
if (other is CustomPluginHotkey otherHotkey)
{
return Hotkey == otherHotkey.Hotkey && ActionKeyword == otherHotkey.ActionKeyword;
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(Hotkey, ActionKeyword);
}
}
}

View file

@ -9,7 +9,6 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.ViewModel;
namespace Flow.Launcher.Infrastructure.UserSettings
{
@ -25,7 +24,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public void Initialize()
{
// Initialize dependency injection instances after Ioc.Default is created
_stringMatcher = Ioc.Default.GetRequiredService<StringMatcher>();
// Initialize application resources after application is created
var settingWindowFont = new FontFamily(SettingWindowFont);
Application.Current.Resources["SettingWindowFont"] = settingWindowFont;
Application.Current.Resources["ContentControlThemeFontFamily"] = settingWindowFont;
}
public void Save()
@ -34,9 +39,37 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
private string _openResultModifiers = KeyConstant.Alt;
public string OpenResultModifiers
{
get => _openResultModifiers;
set
{
if (_openResultModifiers != value)
{
_openResultModifiers = value;
OnPropertyChanged();
}
}
}
public string ColorScheme { get; set; } = "System";
public bool ShowOpenResultHotkey { get; set; } = true;
private bool _showOpenResultHotkey = true;
public bool ShowOpenResultHotkey
{
get => _showOpenResultHotkey;
set
{
if (_showOpenResultHotkey != value)
{
_showOpenResultHotkey = value;
OnPropertyChanged();
}
}
}
public double WindowSize { get; set; } = 580;
public string PreviewHotkey { get; set; } = $"F1";
public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab";
@ -52,6 +85,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public string OpenHistoryHotkey { get; set; } = $"Ctrl+H";
public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up";
public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down";
public string DialogJumpHotkey { get; set; } = $"{KeyConstant.Alt} + G";
private string _language = Constant.SystemLanguageCode;
public string Language
@ -59,8 +93,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
get => _language;
set
{
_language = value;
OnPropertyChanged();
if (_language != value)
{
_language = value;
OnPropertyChanged();
}
}
}
private string _theme = Constant.DefaultTheme;
@ -69,7 +106,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
get => _theme;
set
{
if (value != _theme)
if (_theme != value)
{
_theme = value;
OnPropertyChanged();
@ -78,7 +115,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
}
public bool UseDropShadowEffect { get; set; } = true;
public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None;
public BackdropTypes BackdropType { get; set; } = BackdropTypes.None;
public string ReleaseNotesVersion { get; set; } = string.Empty;
/* Appearance Settings. It should be separated from the setting later.*/
@ -116,8 +153,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
_settingWindowFont = value;
OnPropertyChanged();
Application.Current.Resources["SettingWindowFont"] = new FontFamily(value);
Application.Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(value);
if (Application.Current != null)
{
Application.Current.Resources["SettingWindowFont"] = new FontFamily(value);
Application.Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(value);
}
}
}
}
@ -190,9 +230,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
}
}
public int MaxHistoryResultsToShowForHomePage { get; set; } = 5;
public bool AutoRestartAfterChanging { get; set; } = false;
public bool ShowUnknownSourceWarning { get; set; } = true;
public bool AutoUpdatePlugins { get; set; } = true;
public int CustomExplorerIndex { get; set; } = 0;
[JsonIgnore]
@ -281,13 +325,70 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
};
public bool EnableDialogJump { get; set; } = true;
public bool AutoDialogJump { get; set; } = false;
public bool ShowDialogJumpWindow { get; set; } = false;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpWindowPositions DialogJumpWindowPosition { get; set; } = DialogJumpWindowPositions.UnderDialog;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpResultBehaviours DialogJumpResultBehaviour { get; set; } = DialogJumpResultBehaviours.LeftClick;
[JsonConverter(typeof(JsonStringEnumConverter))]
public DialogJumpFileResultBehaviours DialogJumpFileResultBehaviour { get; set; } = DialogJumpFileResultBehaviours.FullPath;
[JsonConverter(typeof(JsonStringEnumConverter))]
public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO;
/// <summary>
/// when false Alphabet static service will always return empty results
/// </summary>
public bool ShouldUsePinyin { get; set; } = false;
private bool _useAlphabet = true;
public bool ShouldUsePinyin
{
get => _useAlphabet;
set
{
if (_useAlphabet != value)
{
_useAlphabet = value;
OnPropertyChanged();
}
}
}
private bool _useDoublePinyin = false;
public bool UseDoublePinyin
{
get => _useDoublePinyin;
set
{
if (_useDoublePinyin != value)
{
_useDoublePinyin = value;
OnPropertyChanged();
}
}
}
private DoublePinyinSchemas _doublePinyinSchema = DoublePinyinSchemas.XiaoHe;
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
public DoublePinyinSchemas DoublePinyinSchema
{
get => _doublePinyinSchema;
set
{
if (_doublePinyinSchema != value)
{
_doublePinyinSchema = value;
OnPropertyChanged();
}
}
}
public bool AlwaysPreview { get; set; } = false;
@ -300,9 +401,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
get => _querySearchPrecision;
set
{
_querySearchPrecision = value;
if (_stringMatcher != null)
_stringMatcher.UserSettingSearchPrecision = value;
if (_querySearchPrecision != value)
{
_querySearchPrecision = value;
if (_stringMatcher != null)
_stringMatcher.UserSettingSearchPrecision = value;
}
}
}
@ -369,13 +473,30 @@ namespace Flow.Launcher.Infrastructure.UserSettings
get => _hideNotifyIcon;
set
{
_hideNotifyIcon = value;
OnPropertyChanged();
if (_hideNotifyIcon != value)
{
_hideNotifyIcon = value;
OnPropertyChanged();
}
}
}
public bool LeaveCmdOpen { get; set; }
public bool HideWhenDeactivated { get; set; } = true;
private bool _showAtTopmost = false;
public bool ShowAtTopmost
{
get => _showAtTopmost;
set
{
if (_showAtTopmost != value)
{
_showAtTopmost = value;
OnPropertyChanged();
}
}
}
public bool SearchQueryResultsWithDelay { get; set; }
public int SearchDelayTime { get; set; } = 150;
@ -411,7 +532,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
var list = FixedHotkeys();
// Customizeable hotkeys
// Customizable hotkeys
if (!string.IsNullOrEmpty(Hotkey))
list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = ""));
if (!string.IsNullOrEmpty(PreviewHotkey))
@ -431,7 +552,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
if (!string.IsNullOrEmpty(SettingWindowHotkey))
list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = ""));
if (!string.IsNullOrEmpty(OpenHistoryHotkey))
list.Add(new(OpenHistoryHotkey, "OpenHistoryHotkey", () => OpenHistoryHotkey = ""));
list.Add(new(OpenHistoryHotkey, "OpenHistoryHotkey", () => OpenHistoryHotkey = ""));
if (!string.IsNullOrEmpty(OpenContextMenuHotkey))
list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = ""));
if (!string.IsNullOrEmpty(SelectNextPageHotkey))
@ -442,6 +563,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = ""));
if (!string.IsNullOrEmpty(CycleHistoryDownHotkey))
list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = ""));
if (!string.IsNullOrEmpty(DialogJumpHotkey))
list.Add(new(DialogJumpHotkey, "dialogJumpHotkey", () => DialogJumpHotkey = ""));
// Custom Query Hotkeys
foreach (var customPluginHotkey in CustomPluginHotkeys)
@ -537,9 +660,41 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public enum BackdropTypes
{
None,
None,
Acrylic,
Mica,
MicaAlt
}
public enum DoublePinyinSchemas
{
XiaoHe,
ZiRanMa,
WeiRuan,
ZhiNengABC,
ZiGuangPinYin,
PinYinJiaJia,
XingKongJianDao,
DaNiu,
XiaoLang
}
public enum DialogJumpWindowPositions
{
UnderDialog,
FollowDefault
}
public enum DialogJumpResultBehaviours
{
LeftClick,
RightClick
}
public enum DialogJumpFileResultBehaviours
{
FullPath,
FullPathOpen,
Directory
}
}

View file

@ -13,10 +13,14 @@ using System.Windows.Interop;
using System.Windows.Markup;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedModels;
using Microsoft.Win32;
using Microsoft.Win32.SafeHandles;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.System.Power;
using Windows.Win32.System.Threading;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.Shell.Common;
using Windows.Win32.UI.WindowsAndMessaging;
@ -118,9 +122,9 @@ namespace Flow.Launcher.Infrastructure
#region Window Foreground
public static nint GetForegroundWindow()
public static unsafe nint GetForegroundWindow()
{
return PInvoke.GetForegroundWindow().Value;
return (nint)PInvoke.GetForegroundWindow().Value;
}
public static bool SetForegroundWindow(Window window)
@ -138,6 +142,11 @@ namespace Flow.Launcher.Infrastructure
return IsForegroundWindow(GetWindowHandle(window));
}
public static bool IsForegroundWindow(nint handle)
{
return IsForegroundWindow(new HWND(handle));
}
internal static bool IsForegroundWindow(HWND handle)
{
return handle.Equals(PInvoke.GetForegroundWindow());
@ -279,15 +288,15 @@ namespace Flow.Launcher.Infrastructure
{
var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null);
hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView");
if (hWndDesktop.Value != IntPtr.Zero)
if (hWndDesktop != HWND.Null)
{
return false;
}
}
var monitorInfo = MonitorInfo.GetNearestDisplayMonitor(hWnd);
return (appBounds.bottom - appBounds.top) == monitorInfo.RectMonitor.Height &&
(appBounds.right - appBounds.left) == monitorInfo.RectMonitor.Width;
return (appBounds.bottom - appBounds.top) == monitorInfo.Bounds.Height &&
(appBounds.right - appBounds.left) == monitorInfo.Bounds.Width;
}
#endregion
@ -344,6 +353,16 @@ namespace Flow.Launcher.Infrastructure
return new(windowHelper.Handle);
}
internal static HWND GetMainWindowHandle()
{
// When application is exiting, the Application.Current will be null
if (Application.Current == null) return HWND.Null;
// Get the FL main window
var hwnd = GetWindowHandle(Application.Current.MainWindow, true);
return hwnd;
}
#endregion
#region STA Thread
@ -480,7 +499,7 @@ namespace Flow.Launcher.Infrastructure
/// 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()
public unsafe static void RestorePreviousKeyboardLayout()
{
if (_previousLayout == HKL.Null) return;
@ -491,7 +510,7 @@ namespace Flow.Launcher.Infrastructure
hwnd,
PInvoke.WM_INPUTLANGCHANGEREQUEST,
PInvoke.INPUTLANGCHANGE_FORWARD,
_previousLayout.Value
new LPARAM((nint)_previousLayout.Value)
);
_previousLayout = HKL.Null;
@ -761,6 +780,62 @@ namespace Flow.Launcher.Infrastructure
#endregion
#region Window Rect
public static unsafe bool GetWindowRect(nint handle, out Rect outRect)
{
var rect = new RECT();
var result = PInvoke.GetWindowRect(new(handle), &rect);
if (!result)
{
outRect = new Rect();
return false;
}
// Convert RECT to Rect
outRect = new Rect(
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top
);
return true;
}
#endregion
#region Window Process
internal static unsafe string GetProcessNameFromHwnd(HWND hWnd)
{
return Path.GetFileName(GetProcessPathFromHwnd(hWnd));
}
internal static unsafe string GetProcessPathFromHwnd(HWND hWnd)
{
uint pid;
var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid);
if (threadId == 0) return string.Empty;
var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (process != HWND.Null)
{
using var safeHandle = new SafeProcessHandle((nint)process.Value, true);
uint capacity = 2000;
Span<char> buffer = new char[capacity];
if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, buffer, ref capacity))
{
return string.Empty;
}
return buffer[..(int)capacity].ToString();
}
return string.Empty;
}
#endregion
#region Explorer
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems
@ -791,5 +866,155 @@ namespace Flow.Launcher.Infrastructure
}
#endregion
#region Win32 Dark Mode
/*
* Inspired by https://github.com/ysc3839/win32-darkmode
*/
[DllImport("uxtheme.dll", EntryPoint = "#135", SetLastError = true)]
private static extern int SetPreferredAppMode(int appMode);
public static void EnableWin32DarkMode(string colorScheme)
{
try
{
// Undocumented API from Windows 10 1809
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
Environment.OSVersion.Version.Build >= 17763)
{
var flag = colorScheme switch
{
Constant.Light => 3, // ForceLight
Constant.Dark => 2, // ForceDark
Constant.System => 1, // AllowDark
_ => 0 // Default
};
_ = SetPreferredAppMode(flag);
}
}
catch
{
// Ignore errors on unsupported OS
}
}
#endregion
#region File / Folder Dialog
public static string SelectFile()
{
var dlg = new OpenFileDialog();
var result = dlg.ShowDialog();
if (result == true)
return dlg.FileName;
return string.Empty;
}
#endregion
#region Sleep Mode Listener
private static Action _func;
private static PDEVICE_NOTIFY_CALLBACK_ROUTINE _callback = null;
private static DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS _recipient;
private static SafeHandle _recipientHandle;
private static HPOWERNOTIFY _handle = HPOWERNOTIFY.Null;
/// <summary>
/// Registers a listener for sleep mode events.
/// Inspired from: https://github.com/XKaguya/LenovoLegionToolkit
/// https://blog.csdn.net/mochounv/article/details/114668594
/// </summary>
/// <param name="func"></param>
/// <exception cref="Win32Exception"></exception>
public static unsafe void RegisterSleepModeListener(Action func)
{
if (_callback != null)
{
// Only register if not already registered
return;
}
_func = func;
_callback = new PDEVICE_NOTIFY_CALLBACK_ROUTINE(DeviceNotifyCallback);
_recipient = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS()
{
Callback = _callback,
Context = null
};
_recipientHandle = new StructSafeHandle<DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS>(_recipient);
_handle = PInvoke.PowerRegisterSuspendResumeNotification(
REGISTER_NOTIFICATION_FLAGS.DEVICE_NOTIFY_CALLBACK,
_recipientHandle,
out var handle) == WIN32_ERROR.ERROR_SUCCESS ?
new HPOWERNOTIFY(new IntPtr(handle)) :
HPOWERNOTIFY.Null;
if (_handle.IsNull)
{
throw new Win32Exception("Error registering for power notifications: " + Marshal.GetLastWin32Error());
}
}
/// <summary>
/// Unregisters the sleep mode listener.
/// </summary>
public static void UnregisterSleepModeListener()
{
if (!_handle.IsNull)
{
PInvoke.PowerUnregisterSuspendResumeNotification(_handle);
_handle = HPOWERNOTIFY.Null;
_func = null;
_callback = null;
_recipientHandle = null;
}
}
private static unsafe uint DeviceNotifyCallback(void* context, uint type, void* setting)
{
switch (type)
{
case PInvoke.PBT_APMRESUMEAUTOMATIC:
// Operation is resuming automatically from a low-power state.This message is sent every time the system resumes
_func?.Invoke();
break;
case PInvoke.PBT_APMRESUMESUSPEND:
// Operation is resuming from a low-power state.This message is sent after PBT_APMRESUMEAUTOMATIC if the resume is triggered by user input, such as pressing a key
_func?.Invoke();
break;
}
return 0;
}
private sealed class StructSafeHandle<T> : SafeHandle where T : struct
{
private readonly nint _ptr = nint.Zero;
public StructSafeHandle(T recipient) : base(nint.Zero, true)
{
var pRecipient = Marshal.AllocHGlobal(Marshal.SizeOf<T>());
Marshal.StructureToPtr(recipient, pRecipient, false);
SetHandle(pRecipient);
_ptr = pRecipient;
}
public override bool IsInvalid => handle == nint.Zero;
protected override bool ReleaseHandle()
{
Marshal.FreeHGlobal(_ptr);
return true;
}
}
#endregion
}
}

View file

@ -0,0 +1,210 @@
{
"version": 1,
"dependencies": {
"net9.0-windows7.0": {
"Ben.Demystifier": {
"type": "Direct",
"requested": "[0.4.1, )",
"resolved": "0.4.1",
"contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==",
"dependencies": {
"System.Reflection.Metadata": "5.0.0"
}
},
"BitFaster.Caching": {
"type": "Direct",
"requested": "[2.5.4, )",
"resolved": "2.5.4",
"contentHash": "1QroTY1PVCZOSG9FnkkCrmCKk/+bZCgI/YXq376HnYwUDJ4Ho0EaV4YaA/5v5WYLnwIwIO7RZkdWbg9pxIpueQ=="
},
"CommunityToolkit.Mvvm": {
"type": "Direct",
"requested": "[8.4.0, )",
"resolved": "8.4.0",
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
},
"Flow.Launcher.Localization": {
"type": "Direct",
"requested": "[0.0.6, )",
"resolved": "0.0.6",
"contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw=="
},
"Fody": {
"type": "Direct",
"requested": "[6.9.3, )",
"resolved": "6.9.3",
"contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA=="
},
"ini-parser": {
"type": "Direct",
"requested": "[2.5.2, )",
"resolved": "2.5.2",
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
},
"InputSimulator": {
"type": "Direct",
"requested": "[1.0.4, )",
"resolved": "1.0.4",
"contentHash": "D0LvRCPQMX6/FJHBjng+RO+wRDuHTJrfo7IAc7rmkPvRqchdVGJWg3y70peOtDy3OLNK+HSOwVkH4GiuLnkKgA=="
},
"MemoryPack": {
"type": "Direct",
"requested": "[1.21.4, )",
"resolved": "1.21.4",
"contentHash": "wy3JTBNBsO8LfQcBvvYsWr3lm2Oakolrfu0UQ3oSJSEiD+7ye0GUhYTaXuYYBowqsXBXWD9gf2218ae0JRiYVQ==",
"dependencies": {
"MemoryPack.Core": "1.21.4",
"MemoryPack.Generator": "1.21.4"
}
},
"Microsoft.VisualStudio.Threading": {
"type": "Direct",
"requested": "[17.14.15, )",
"resolved": "17.14.15",
"contentHash": "1DrCusT3xNLSlaJg77BsUSAzrhjdZBAvvsS0PMzyPM+fGais6SnISOhqdZQop8VVMIBLsYm2gyF9W7THjgavwA==",
"dependencies": {
"Microsoft.VisualStudio.Threading.Analyzers": "17.14.15",
"Microsoft.VisualStudio.Threading.Only": "17.14.15",
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Microsoft.Windows.CsWin32": {
"type": "Direct",
"requested": "[0.3.205, )",
"resolved": "0.3.205",
"contentHash": "U5wGAnyKd7/I2YMd43nogm81VMtjiKzZ9dsLMVI4eAB7jtv5IEj0gprj0q/F3iRmAIaGv5omOf8iSYx2+nE6BQ==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
"Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview",
"Microsoft.Windows.WDK.Win32Metadata": "0.12.8-experimental"
}
},
"NHotkey.Wpf": {
"type": "Direct",
"requested": "[3.0.0, )",
"resolved": "3.0.0",
"contentHash": "BIUKlhTG5KtFf9OQzWvkmVmktt5/FFj6AOEgag8Uf0R2YdZt5ajUzs3sVskcJcT2TztWlEHKQr1jFj3KQ0D9Nw==",
"dependencies": {
"NHotkey": "3.0.0"
}
},
"NLog": {
"type": "Direct",
"requested": "[6.0.4, )",
"resolved": "6.0.4",
"contentHash": "Xr+lIk1ZlTTFXEqnxQVLxrDqZlt2tm5X+/AhJbaY2emb/dVtGDiU5QuEtj3gHtwV/SWlP/rJ922I/BPuOJXlRw=="
},
"NLog.OutputDebugString": {
"type": "Direct",
"requested": "[6.0.4, )",
"resolved": "6.0.4",
"contentHash": "TOP2Ap9BbE98B/l/TglnguowOD0rXo8B/20xAgvj9shO/kf6IJ5M4QMhVxq72mrneJ/ANhHY7Jcd+xJbzuI5PA==",
"dependencies": {
"NLog": "6.0.4"
}
},
"PropertyChanged.Fody": {
"type": "Direct",
"requested": "[4.1.0, )",
"resolved": "4.1.0",
"contentHash": "6v+f9cI8YjnZH2WBHuOqWEAo8DFFNGFIdU8xD3AsL6fhV6Y8oAmVWd7XKk49t8DpeUBwhR/X+97+6Epvek0Y3A==",
"dependencies": {
"Fody": "6.6.4"
}
},
"SharpVectors.Wpf": {
"type": "Direct",
"requested": "[1.8.5, )",
"resolved": "1.8.5",
"contentHash": "WURdBDq5AE8RjKV9pFS7lNkJe81gxja9SaMGE4URq9GJUZ6M+5DGUL0Lm3B0iYW2/Meyowaz4ffGsyW+RBSTtg=="
},
"System.Drawing.Common": {
"type": "Direct",
"requested": "[7.0.0, )",
"resolved": "7.0.0",
"contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==",
"dependencies": {
"Microsoft.Win32.SystemEvents": "7.0.0"
}
},
"ToolGood.Words.Pinyin": {
"type": "Direct",
"requested": "[3.1.0.3, )",
"resolved": "3.1.0.3",
"contentHash": "VKcf8sUq/+LyY99WgLhOu7Q32ROEyR30/2xCCj9ADRi45wVC7kpXrYCf9vH1qirkmrIfpL8inoxAbrqAlfXxsQ=="
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2025.2.2",
"contentHash": "0X56ZRizuHdrnPpgXjWV7f2tQO1FlQg5O1967OGKnI/4ZRNOK642J8L7brM1nYvrxTTU5TP1yRyXLRLaXLPQ8A=="
},
"MemoryPack.Core": {
"type": "Transitive",
"resolved": "1.21.4",
"contentHash": "6RszGorZ0ejNmp37ZcboPBMvvPCuNW2jlrdQfcs/lMzE5b3pmPF6hsm/laDc34hRlbAST1ZxaX/DvYu2DF5sBQ=="
},
"MemoryPack.Generator": {
"type": "Transitive",
"resolved": "1.21.4",
"contentHash": "g14EsSS85yn0lHTi0J9ivqlZMf09A2iI51fmI+0KkzIzyCbWOBWPi5mdaY7YWmXprk12aYh9u/qfWHQUYthlwg=="
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Transitive",
"resolved": "17.14.15",
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
},
"Microsoft.VisualStudio.Threading.Only": {
"type": "Transitive",
"resolved": "17.14.15",
"contentHash": "NqONyw1RXyj9P3k5e1uU2k9kc1ptwuU5NJQzG+MPq7vQVHUzBY8HLuJf/N2Rw5H/myD96CVxziDxmjawPuzntw==",
"dependencies": {
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Microsoft.VisualStudio.Validation": {
"type": "Transitive",
"resolved": "17.8.8",
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
},
"Microsoft.Win32.SystemEvents": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ=="
},
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",
"resolved": "0.1.42-alpha",
"contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA=="
},
"Microsoft.Windows.SDK.Win32Metadata": {
"type": "Transitive",
"resolved": "61.0.15-preview",
"contentHash": "cysex3dazKtCPALCluC2XX3f5Aedy9H2pw5jb+TW5uas2rkem1Z7FRnbUrg2vKx0pk0Qz+4EJNr37HdYTEcvEQ=="
},
"Microsoft.Windows.WDK.Win32Metadata": {
"type": "Transitive",
"resolved": "0.12.8-experimental",
"contentHash": "3n8R44/Z96Ly+ty4eYVJfESqbzvpw96lRLs3zOzyDmr1x1Kw7FNn5CyE416q+bZQV3e1HRuMUvyegMeRE/WedA==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview"
}
},
"NHotkey": {
"type": "Transitive",
"resolved": "3.0.0",
"contentHash": "IEghs0QqWsQYH0uUmvIl0Ye6RaebWRh38eB6ToOkDnQucTYRGFOgtig0gSxlwCszTilYFz3n1ZuY762x+kDR3A=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ=="
},
"flow.launcher.plugin": {
"type": "Project",
"dependencies": {
"JetBrains.Annotations": "[2025.2.2, )"
}
}
}
}
}

View file

@ -0,0 +1,92 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Describes a result of a <see cref="Query"/> executed by a plugin in Dialog Jump window
/// </summary>
public class DialogJumpResult : Result
{
/// <summary>
/// This holds the path which can be provided by plugin to be navigated to the
/// file dialog when records in Dialog Jump window is right clicked on a result.
/// </summary>
public required string DialogJumpPath { get; init; }
/// <summary>
/// Clones the current Dialog Jump result
/// </summary>
public new DialogJumpResult Clone()
{
return new DialogJumpResult
{
Title = Title,
SubTitle = SubTitle,
ActionKeywordAssigned = ActionKeywordAssigned,
CopyText = CopyText,
AutoCompleteText = AutoCompleteText,
IcoPath = IcoPath,
BadgeIcoPath = BadgeIcoPath,
RoundedIcon = RoundedIcon,
Icon = Icon,
BadgeIcon = BadgeIcon,
Glyph = Glyph,
Action = Action,
AsyncAction = AsyncAction,
Score = Score,
TitleHighlightData = TitleHighlightData,
OriginQuery = OriginQuery,
PluginDirectory = PluginDirectory,
ContextData = ContextData,
PluginID = PluginID,
TitleToolTip = TitleToolTip,
SubTitleToolTip = SubTitleToolTip,
PreviewPanel = PreviewPanel,
ProgressBar = ProgressBar,
ProgressBarColor = ProgressBarColor,
Preview = Preview,
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey,
ShowBadge = ShowBadge,
DialogJumpPath = DialogJumpPath
};
}
/// <summary>
/// Convert <see cref="Result"/> to <see cref="DialogJumpResult"/>.
/// </summary>
public static DialogJumpResult From(Result result, string dialogJumpPath)
{
return new DialogJumpResult
{
Title = result.Title,
SubTitle = result.SubTitle,
ActionKeywordAssigned = result.ActionKeywordAssigned,
CopyText = result.CopyText,
AutoCompleteText = result.AutoCompleteText,
IcoPath = result.IcoPath,
BadgeIcoPath = result.BadgeIcoPath,
RoundedIcon = result.RoundedIcon,
Icon = result.Icon,
BadgeIcon = result.BadgeIcon,
Glyph = result.Glyph,
Action = result.Action,
AsyncAction = result.AsyncAction,
Score = result.Score,
TitleHighlightData = result.TitleHighlightData,
OriginQuery = result.OriginQuery,
PluginDirectory = result.PluginDirectory,
ContextData = result.ContextData,
PluginID = result.PluginID,
TitleToolTip = result.TitleToolTip,
SubTitleToolTip = result.SubTitleToolTip,
PreviewPanel = result.PreviewPanel,
ProgressBar = result.ProgressBar,
ProgressBarColor = result.ProgressBarColor,
Preview = result.Preview,
AddSelectedCount = result.AddSelectedCount,
RecordKey = result.RecordKey,
ShowBadge = result.ShowBadge,
DialogJumpPath = dialogJumpPath
};
}
}
}

View file

@ -39,7 +39,14 @@ namespace Flow.Launcher.Plugin
/// <param name="sender"></param>
/// <param name="args"></param>
public delegate void VisibilityChangedEventHandler(object sender, VisibilityChangedEventArgs args);
/// <summary>
/// A delegate for when the actual application theme is changed
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public delegate void ActualApplicationThemeChangedEventHandler(object sender, ActualApplicationThemeChangedEventArgs args);
/// <summary>
/// The event args for <see cref="VisibilityChangedEventHandler"/>
/// </summary>
@ -77,4 +84,15 @@ namespace Flow.Launcher.Plugin
/// </summary>
public Query Query { get; set; }
}
/// <summary>
/// The event args for <see cref="ActualApplicationThemeChangedEventHandler"/>
/// </summary>
public class ActualApplicationThemeChangedEventArgs : EventArgs
{
/// <summary>
/// <see langword="true"/> if the application has changed actual theme
/// </summary>
public bool IsDark { get; init; }
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ProjectGuid>{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}</ProjectGuid>
<UseWPF>true</UseWPF>
<OutputType>Library</OutputType>
@ -11,13 +11,14 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup>
<Version>4.5.0</Version>
<PackageVersion>4.5.0</PackageVersion>
<AssemblyVersion>4.5.0</AssemblyVersion>
<FileVersion>4.5.0</FileVersion>
<Version>5.0.0</Version>
<PackageVersion>5.0.0</PackageVersion>
<AssemblyVersion>5.0.0</AssemblyVersion>
<FileVersion>5.0.0</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -27,6 +28,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageReadmeFile>Readme.md</PackageReadmeFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(APPVEYOR)' == 'True'">
@ -66,17 +68,17 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="6.5.5">
<PackageReference Include="Fody" Version="6.9.3">
<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="2024.3.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.205">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Asynchronous Dialog Jump Model
/// </summary>
public interface IAsyncDialogJump : IFeatures
{
/// <summary>
/// Asynchronous querying for Dialog Jump window
/// </summary>
/// <para>
/// If the Querying method requires high IO transmission
/// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncDialogJump interface
/// </para>
/// <param name="query">Query to search</param>
/// <param name="token">Cancel when querying job is obsolete</param>
/// <returns></returns>
Task<List<DialogJumpResult>> QueryDialogJumpAsync(Query query, CancellationToken token);
}
}

View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Synchronous Dialog Jump Model
/// <para>
/// If the Querying method requires high IO transmission
/// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncDialogJump interface
/// </para>
/// </summary>
public interface IDialogJump : IAsyncDialogJump
{
/// <summary>
/// Querying for Dialog Jump window
/// <para>
/// This method will be called within a Task.Run,
/// so please avoid synchrously wait for long.
/// </para>
/// </summary>
/// <param name="query">Query to search</param>
/// <returns></returns>
List<DialogJumpResult> QueryDialogJump(Query query);
Task<List<DialogJumpResult>> IAsyncDialogJump.QueryDialogJumpAsync(Query query, CancellationToken token) => Task.Run(() => QueryDialogJump(query), token);
}
}

View file

@ -0,0 +1,96 @@
using System;
#nullable enable
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Interface for handling file dialog instances in DialogJump.
/// </summary>
public interface IDialogJumpDialog : IFeatures, IDisposable
{
/// <summary>
/// Check if the foreground window is a file dialog instance.
/// </summary>
/// <param name="hwnd">
/// The handle of the foreground window to check.
/// </param>
/// <returns>
/// The window if the foreground window is a file dialog instance. Null if it is not.
/// </returns>
IDialogJumpDialogWindow? CheckDialogWindow(IntPtr hwnd);
}
/// <summary>
/// Interface for handling a specific file dialog window in DialogJump.
/// </summary>
public interface IDialogJumpDialogWindow : IDisposable
{
/// <summary>
/// The handle of the dialog window.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current tab of the dialog window.
/// </summary>
/// <returns></returns>
IDialogJumpDialogWindowTab GetCurrentTab();
}
/// <summary>
/// Interface for handling a specific tab in a file dialog window in DialogJump.
/// </summary>
public interface IDialogJumpDialogWindowTab : IDisposable
{
/// <summary>
/// The handle of the dialog tab.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current folder path of the dialog tab.
/// </summary>
/// <returns></returns>
string GetCurrentFolder();
/// <summary>
/// Get the current file of the dialog tab.
/// </summary>
/// <returns></returns>
string GetCurrentFile();
/// <summary>
/// Jump to a folder in the dialog tab.
/// </summary>
/// <param name="path">
/// The path to the folder to jump to.
/// </param>
/// <param name="auto">
/// Whether folder jump is under automatical mode.
/// </param>
/// <returns>
/// True if the jump was successful, false otherwise.
/// </returns>
bool JumpFolder(string path, bool auto);
/// <summary>
/// Jump to a file in the dialog tab.
/// </summary>
/// <param name="path">
/// The path to the file to jump to.
/// </param>
/// <returns>
/// True if the jump was successful, false otherwise.
/// </returns>
bool JumpFile(string path);
/// <summary>
/// Open the file in the dialog tab.
/// </summary>
/// <returns>
/// True if the file was opened successfully, false otherwise.
/// </returns>
bool Open();
}
}

View file

@ -0,0 +1,40 @@
using System;
#nullable enable
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Interface for handling file explorer instances in DialogJump.
/// </summary>
public interface IDialogJumpExplorer : IFeatures, IDisposable
{
/// <summary>
/// Check if the foreground window is a Windows Explorer instance.
/// </summary>
/// <param name="hwnd">
/// The handle of the foreground window to check.
/// </param>
/// <returns>
/// The window if the foreground window is a file explorer instance. Null if it is not.
/// </returns>
IDialogJumpExplorerWindow? CheckExplorerWindow(IntPtr hwnd);
}
/// <summary>
/// Interface for handling a specific file explorer window in DialogJump.
/// </summary>
public interface IDialogJumpExplorerWindow : IDisposable
{
/// <summary>
/// The handle of the explorer window.
/// </summary>
IntPtr Handle { get; }
/// <summary>
/// Get the current folder path of the explorer window.
/// </summary>
/// <returns></returns>
string? GetExplorerPath();
}
}

View file

@ -32,6 +32,6 @@ namespace Flow.Launcher.Plugin
Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context));
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query));
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query), token);
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
@ -23,8 +23,8 @@ namespace Flow.Launcher.Plugin
/// </summary>
/// <param name="query">query text</param>
/// <param name="requery">
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
/// Set this to <see langword="true"/> to force Flow Launcher requerying
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
/// Set this to <see langword="true"/> to force Flow Launcher re-querying
/// </param>
void ChangeQuery(string query, bool requery = false);
@ -49,7 +49,7 @@ namespace Flow.Launcher.Plugin
/// </summary>
/// <param name="text">Text to save on clipboard</param>
/// <param name="directCopy">When true it will directly copy the file/folder from the path specified in text</param>
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
/// It will show file/folder/text is copied successfully.
/// Turn this off to show your own notification after copy is done.</param>>
public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true);
@ -65,7 +65,7 @@ namespace Flow.Launcher.Plugin
void SavePluginSettings();
/// <summary>
/// Reloads any Plugins that have the
/// Reloads any Plugins that have the
/// IReloadable implemented. It refeshes
/// Plugin's in memory data with new content
/// added by user.
@ -97,7 +97,7 @@ namespace Flow.Launcher.Plugin
/// Show the MainWindow when hiding
/// </summary>
void ShowMainWindow();
/// <summary>
/// Focus the query text box in the main window
/// </summary>
@ -115,7 +115,7 @@ namespace Flow.Launcher.Plugin
bool IsMainWindowVisible();
/// <summary>
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
/// </summary>
event VisibilityChangedEventHandler VisibilityChanged;
@ -171,7 +171,7 @@ namespace Flow.Launcher.Plugin
string GetTranslation(string key);
/// <summary>
/// Get all loaded plugins
/// Get all loaded plugins
/// </summary>
/// <returns></returns>
List<PluginPair> GetAllPlugins();
@ -229,7 +229,7 @@ namespace Flow.Launcher.Plugin
MatchResult FuzzySearch(string query, string stringToCompare);
/// <summary>
/// Http download the spefic url and return as string
/// Http download the specific url and return as string
/// </summary>
/// <param name="url">URL to call Http Get</param>
/// <param name="token">Cancellation Token</param>
@ -237,7 +237,7 @@ namespace Flow.Launcher.Plugin
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
/// <summary>
/// Http download the spefic url and return as stream
/// Http download the specific url and return as stream
/// </summary>
/// <param name="url">URL to call Http Get</param>
/// <param name="token">Cancellation Token</param>
@ -305,8 +305,8 @@ namespace Flow.Launcher.Plugin
void LogError(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log an Exception. Will throw if in debug mode so developer will be aware,
/// otherwise logs the eror message. This is the primary logging method used for Flow
/// Log an Exception. Will throw if in debug mode so developer will be aware,
/// otherwise logs the eror message. This is the primary logging method used for Flow
/// </summary>
void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "");
@ -336,13 +336,28 @@ namespace Flow.Launcher.Plugin
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null);
/// <summary>
/// Opens the URL with the given Uri object.
/// Opens the URL using the browser with the given Uri object, even if the URL is a local file.
/// The browser and mode used is based on what's configured in Flow's default browser settings.
/// </summary>
public void OpenWebUrl(Uri url, bool? inPrivate = null);
/// <summary>
/// Opens the URL using the browser with the given string, even if the URL is a local file.
/// The browser and mode used is based on what's configured in Flow's default browser settings.
/// Non-C# plugins should use this method.
/// </summary>
public void OpenWebUrl(string url, bool? inPrivate = null);
/// <summary>
/// Opens the URL with the given Uri object in browser if scheme is Http or Https.
/// If the URL is a local file, it will instead be opened with the default application for that file type.
/// The browser and mode used is based on what's configured in Flow's default browser settings.
/// </summary>
public void OpenUrl(Uri url, bool? inPrivate = null);
/// <summary>
/// Opens the URL with the given string.
/// Opens the URL with the given string in browser if scheme is Http or Https.
/// If the URL is a local file, it will instead be opened with the default application for that file type.
/// The browser and mode used is based on what's configured in Flow's default browser settings.
/// Non-C# plugins should use this method.
/// </summary>
@ -378,7 +393,7 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Reloads the query.
/// When current results are from context menu or history, it will go back to query results before requerying.
/// When current results are from context menu or history, it will go back to query results before re-querying.
/// </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);
@ -532,8 +547,10 @@ namespace Flow.Launcher.Plugin
/// <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);
/// <returns>
/// True if the plugin is updated successfully, false otherwise.
/// </returns>
public Task<bool> UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
/// <summary>
/// Install a plugin. By default will remove the zip file if installation is from url,
@ -543,7 +560,10 @@ namespace Flow.Launcher.Plugin
/// <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);
/// <returns>
/// True if the plugin is installed successfully, false otherwise.
/// </returns>
public bool InstallPlugin(UserPlugin plugin, string zipFilePath);
/// <summary>
/// Uninstall a plugin
@ -552,8 +572,10 @@ namespace Flow.Launcher.Plugin
/// <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);
/// <returns>
/// True if the plugin is updated successfully, false otherwise.
/// </returns>
public Task<bool> UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
/// <summary>
/// Log debug message of the time taken to execute a method
@ -580,5 +602,28 @@ namespace Flow.Launcher.Plugin
/// </summary>
/// <returns>The time taken to execute the method in milliseconds</returns>
public Task<long> StopwatchLogInfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "");
/// <summary>
/// Representing whether the application is using a dark theme
/// </summary>
/// <returns></returns>
bool IsApplicationDarkTheme();
/// <summary>
/// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
/// </summary>
event ActualApplicationThemeChangedEventHandler ActualApplicationThemeChanged;
/// <summary>
/// Get the user data directory of Flow Launcher.
/// </summary>
/// <returns></returns>
string GetDataDirectory();
/// <summary>
/// Get the log directory of Flow Launcher.
/// </summary>
/// <returns></returns>
string GetLogDirectory();
}
}

View file

@ -5,4 +5,12 @@ GetWindowTextLength
WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
WM_SYSKEYUP
GetSystemMetrics
EnumDisplayMonitors
MonitorFromWindow
GetMonitorInfo
MONITORINFOEXW
GetCursorPos
MonitorFromPoint

View file

@ -57,7 +57,10 @@ namespace Flow.Launcher.Plugin
/// for user on the plugin result. If autocomplete action for example is tab, pressing tab will have
/// the default constructed autocomplete text (result's Title), or the text provided here if not empty.
/// </summary>
/// <remarks>When a value is not set, the <see cref="Title"/> will be used.</remarks>
/// <remarks>
/// When a value is not set, the <see cref="Title"/> will be used.
/// Please include the action keyword prefix when necessary because Flow does not prepend it automatically.
/// </remarks>
public string AutoCompleteText { get; set; }
/// <summary>
@ -257,6 +260,17 @@ namespace Flow.Launcher.Plugin
/// </summary>
public bool ShowBadge { get; set; } = false;
/// <summary>
/// This holds the text which can be shown as a query suggestion.
/// </summary>
/// <remarks>
/// When a value is not set, the <see cref="Title"/> will be used.
/// Do not include the action keyword prefix because Flow prepends it automatically.
/// If the it does not start with the query text, it will not be shown as a suggestion.
/// So make sure to set this value to start with the query text.
/// </remarks>
public string QuerySuggestionText { get; set; }
/// <summary>
/// Run this result, asynchronously
/// </summary>
@ -308,6 +322,7 @@ namespace Flow.Launcher.Plugin
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey,
ShowBadge = ShowBadge,
QuerySuggestionText = QuerySuggestionText
};
}

View file

@ -150,6 +150,16 @@ namespace Flow.Launcher.Plugin.SharedCommands
return File.Exists(filePath);
}
/// <summary>
/// Checks if a file or directory exists
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static bool FileOrLocationExists(this string path)
{
return LocationExists(path) || FileExists(path);
}
/// <summary>
/// Open a directory window (using the OS's default handler, usually explorer)
/// </summary>

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System;
using System.Runtime.InteropServices;
using System.Windows;
using Windows.Win32;
@ -7,13 +6,16 @@ using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Flow.Launcher.Infrastructure;
namespace Flow.Launcher.Plugin.SharedModels;
/// <summary>
/// Contains full information about a display monitor.
/// Codes are edited from: <see href="https://github.com/Jack251970/DesktopWidgets3">.
/// Inspired from: https://github.com/Jack251970/DesktopWidgets3.
/// </summary>
internal class MonitorInfo
/// <remarks>
/// Use this class to replace the System.Windows.Forms.Screen class which can cause possible System.PlatformNotSupportedException.
/// </remarks>
public class MonitorInfo
{
/// <summary>
/// Gets the display monitors (including invisible pseudo-monitors associated with the mirroring drivers).
@ -23,14 +25,14 @@ internal class MonitorInfo
{
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) =>
var callback = new MONITORENUMPROC((monitor, deviceContext, rect, 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);
bool ok = PInvoke.EnumDisplayMonitors(hdc, null, callback, dwData);
if (!ok)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
@ -43,11 +45,11 @@ internal class MonitorInfo
/// </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)
public static unsafe MonitorInfo GetNearestDisplayMonitor(nint hwnd)
{
var nearestMonitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
var nearestMonitor = PInvoke.MonitorFromWindow(new(hwnd), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
MonitorInfo nearestMonitorInfo = null;
var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
var callback = new MONITORENUMPROC((monitor, deviceContext, rect, data) =>
{
if (monitor == nearestMonitor)
{
@ -58,7 +60,7 @@ internal class MonitorInfo
});
var dwData = new LPARAM();
var hdc = new HDC();
bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
bool ok = PInvoke.EnumDisplayMonitors(hdc, null, callback, dwData);
if (!ok)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
@ -66,17 +68,75 @@ internal class MonitorInfo
return nearestMonitorInfo;
}
/// <summary>
/// Gets the primary display monitor (the one that contains the taskbar).
/// </summary>
/// <returns>The primary display monitor, or null if no monitor is found.</returns>
public static unsafe MonitorInfo GetPrimaryDisplayMonitor()
{
var primaryMonitor = PInvoke.MonitorFromWindow(new HWND(nint.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
MonitorInfo primaryMonitorInfo = null;
var callback = new MONITORENUMPROC((monitor, deviceContext, rect, data) =>
{
if (monitor == primaryMonitor)
{
primaryMonitorInfo = new MonitorInfo(monitor, rect);
return false;
}
return true;
});
var dwData = new LPARAM();
var hdc = new HDC();
bool ok = PInvoke.EnumDisplayMonitors(hdc, null, callback, dwData);
if (!ok)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
}
return primaryMonitorInfo;
}
/// <summary>
/// Gets the display monitor that contains the cursor.
/// </summary>
/// <returns>The display monitor that contains the cursor, or null if no monitor is found.</returns>
public static unsafe MonitorInfo GetCursorDisplayMonitor()
{
if (!PInvoke.GetCursorPos(out var pt))
{
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
}
var cursorMonitor = PInvoke.MonitorFromPoint(pt, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
MonitorInfo cursorMonitorInfo = null;
var callback = new MONITORENUMPROC((monitor, deviceContext, rect, data) =>
{
if (monitor == cursorMonitor)
{
cursorMonitorInfo = new MonitorInfo(monitor, rect);
return false;
}
return true;
});
var dwData = new LPARAM();
var hdc = new HDC();
bool ok = PInvoke.EnumDisplayMonitors(hdc, null, callback, dwData);
if (!ok)
{
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
}
return cursorMonitorInfo;
}
private readonly HMONITOR _monitor;
internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect)
{
RectMonitor =
Bounds =
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 =
WorkingArea =
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();
@ -93,7 +153,7 @@ internal class MonitorInfo
/// <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; }
public Rect Bounds { get; }
/// <summary>
/// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates.
@ -101,15 +161,15 @@ internal class MonitorInfo
/// <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; }
public Rect WorkingArea { get; }
/// <summary>
/// Gets if the monitor is the the primary display monitor.
/// Gets if the monitor is the primary display monitor.
/// </summary>
public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(IntPtr.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(nint.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
/// <inheritdoc />
public override string ToString() => $"{Name} {RectMonitor.Width}x{RectMonitor.Height}";
public override string ToString() => $"{Name} {Bounds.Width}x{Bounds.Height}";
private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi)
{

View file

@ -76,5 +76,10 @@ namespace Flow.Launcher.Plugin
/// Indicates whether the plugin is installed from a local path
/// </summary>
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
/// <summary>
/// The minimum Flow Launcher version required for this plugin. Default is "".
/// </summary>
public string MinimumAppVersion { get; set; } = string.Empty;
}
}

View file

@ -0,0 +1,77 @@
{
"version": 1,
"dependencies": {
"net9.0-windows7.0": {
"Fody": {
"type": "Direct",
"requested": "[6.9.3, )",
"resolved": "6.9.3",
"contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA=="
},
"JetBrains.Annotations": {
"type": "Direct",
"requested": "[2025.2.2, )",
"resolved": "2025.2.2",
"contentHash": "0X56ZRizuHdrnPpgXjWV7f2tQO1FlQg5O1967OGKnI/4ZRNOK642J8L7brM1nYvrxTTU5TP1yRyXLRLaXLPQ8A=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"Microsoft.Windows.CsWin32": {
"type": "Direct",
"requested": "[0.3.205, )",
"resolved": "0.3.205",
"contentHash": "U5wGAnyKd7/I2YMd43nogm81VMtjiKzZ9dsLMVI4eAB7jtv5IEj0gprj0q/F3iRmAIaGv5omOf8iSYx2+nE6BQ==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
"Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview",
"Microsoft.Windows.WDK.Win32Metadata": "0.12.8-experimental"
}
},
"PropertyChanged.Fody": {
"type": "Direct",
"requested": "[4.1.0, )",
"resolved": "4.1.0",
"contentHash": "6v+f9cI8YjnZH2WBHuOqWEAo8DFFNGFIdU8xD3AsL6fhV6Y8oAmVWd7XKk49t8DpeUBwhR/X+97+6Epvek0Y3A==",
"dependencies": {
"Fody": "6.6.4"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",
"resolved": "0.1.42-alpha",
"contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA=="
},
"Microsoft.Windows.SDK.Win32Metadata": {
"type": "Transitive",
"resolved": "61.0.15-preview",
"contentHash": "cysex3dazKtCPALCluC2XX3f5Aedy9H2pw5jb+TW5uas2rkem1Z7FRnbUrg2vKx0pk0Qz+4EJNr37HdYTEcvEQ=="
},
"Microsoft.Windows.WDK.Win32Metadata": {
"type": "Transitive",
"resolved": "0.12.8-experimental",
"contentHash": "3n8R44/Z96Ly+ty4eYVJfESqbzvpw96lRLs3zOzyDmr1x1Kw7FNn5CyE416q+bZQV3e1HRuMUvyegMeRE/WedA==",
"dependencies": {
"Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview"
}
}
}
}
}

View file

@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using Flow.Launcher.Infrastructure;
using ToolGood.Words.Pinyin;
namespace Flow.Launcher.Test
{
/// <summary>
/// Performance test comparing ContainsChinese() vs WordsHelper.HasChinese()
///
/// This test verifies:
/// 1. Both methods produce identical results (correctness)
/// 2. Performance characteristics of both implementations
/// 3. Memory allocation patterns
///
/// The ContainsChinese() method uses optimized Unicode range checking with ReadOnlySpan
/// while WordsHelper.HasChinese() uses the ToolGood.Words library implementation.
/// </summary>
[TestFixture]
public class ChineseDetectionPerformanceTest
{
private readonly List<string> _testStrings = new()
{
// Pure English - should return false
"Hello World",
"Visual Studio Code",
"Microsoft Office 2023",
"Adobe Photoshop Creative Suite",
"Google Chrome Browser Application",
// Pure Chinese - should return true
"你好世界",
"微软办公软件",
"谷歌浏览器",
"北京大学计算机科学与技术学院",
"中华人民共和国国家发展和改革委员会",
// Mixed content - should return true
"Hello 世界",
"Visual Studio 代码编辑器",
"QQ音乐 Music Player",
"Windows 10 操作系统",
"GitHub 代码仓库管理平台",
// Edge cases
"",
" ",
"123456",
"!@#$%^&*()",
"café résumé naïve", // Accented characters (not Chinese)
// Long strings for performance testing
"This is a very long English string that contains no Chinese characters but is designed to test performance with longer text content that might appear in file names or application descriptions",
"这是一个非常长的中文字符串,包含了很多汉字,用来测试在处理较长中文文本时的性能表现,比如可能出现在文件名或应用程序描述中的文本内容",
"This is a mixed 混合内容的字符串 that contains both English and Chinese characters 中英文混合 to test performance with 复杂的文本内容 in real-world scenarios 真实场景中的应用"
};
[Test]
public void ContainsChinese_CorrectnessTest()
{
// Verify ContainsChinese works correctly for known cases
ClassicAssert.IsFalse(ContainsChinese("Hello World"), "Pure English should return false");
ClassicAssert.IsTrue(ContainsChinese("你好世界"), "Pure Chinese should return true");
ClassicAssert.IsTrue(ContainsChinese("Hello 世界"), "Mixed content should return true");
ClassicAssert.IsFalse(ContainsChinese(""), "Empty string should return false");
ClassicAssert.IsFalse(ContainsChinese("123456"), "Numbers should return false");
ClassicAssert.IsFalse(ContainsChinese("café résumé"), "Accented characters should return false");
}
[Test]
public void WordsHelper_CorrectnessTest()
{
// Verify WordsHelper.HasChinese works correctly for known cases
ClassicAssert.IsFalse(WordsHelper.HasChinese("Hello World"), "Pure English should return false");
ClassicAssert.IsTrue(WordsHelper.HasChinese("你好世界"), "Pure Chinese should return true");
ClassicAssert.IsTrue(WordsHelper.HasChinese("Hello 世界"), "Mixed content should return true");
ClassicAssert.IsFalse(WordsHelper.HasChinese(""), "Empty string should return false");
ClassicAssert.IsFalse(WordsHelper.HasChinese("123456"), "Numbers should return false");
ClassicAssert.IsFalse(WordsHelper.HasChinese("café résumé"), "Accented characters should return false");
}
[Test]
public void BothMethods_ShouldProduceSameResults()
{
// Critical test: verify both methods produce identical results for all test cases
foreach (var testString in _testStrings)
{
var wordsHelperResult = WordsHelper.HasChinese(testString);
var containsChineseResult = ContainsChinese(testString);
ClassicAssert.AreEqual(wordsHelperResult, containsChineseResult,
$"Results differ for string: '{testString}'. WordsHelper: {wordsHelperResult}, ContainsChinese: {containsChineseResult}");
}
Console.WriteLine($"✓ Both methods produce identical results for all {_testStrings.Count} test cases");
}
[Test]
public void PerformanceComparison_BasicBenchmark()
{
const int iterations = 1000000;
Console.WriteLine("=== CHINESE CHARACTER DETECTION PERFORMANCE TEST ===");
Console.WriteLine($"Test iterations: {iterations:N0}");
Console.WriteLine($"Test strings: {_testStrings.Count}");
Console.WriteLine($"Total operations: {iterations * _testStrings.Count:N0}");
Console.WriteLine();
// Warmup to ensure JIT compilation
Console.WriteLine("Warming up...");
for (int i = 0; i < 1000; i++)
{
foreach (var testString in _testStrings)
{
_ = ContainsChinese(testString);
_ = WordsHelper.HasChinese(testString);
}
}
// Benchmark ContainsChinese method
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var sw1 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
foreach (var testString in _testStrings)
{
_ = ContainsChinese(testString);
}
}
sw1.Stop();
// Benchmark WordsHelper.HasChinese method
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var sw2 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
foreach (var testString in _testStrings)
{
_ = WordsHelper.HasChinese(testString);
}
}
sw2.Stop();
// Calculate and display results
var containsChineseMs = sw1.Elapsed.TotalMilliseconds;
var wordsHelperMs = sw2.Elapsed.TotalMilliseconds;
var speedRatio = wordsHelperMs / containsChineseMs;
var timeDifference = wordsHelperMs - containsChineseMs;
Console.WriteLine("RESULTS:");
Console.WriteLine($"ContainsChinese(): {containsChineseMs:F3} ms");
Console.WriteLine($"WordsHelper.HasChinese(): {wordsHelperMs:F3} ms");
Console.WriteLine($"Time difference: {timeDifference:F3} ms");
Console.WriteLine($"Speed improvement: {speedRatio:F2}x");
Console.WriteLine($"Performance gain: {((speedRatio - 1) * 100):F1}%");
Console.WriteLine();
if (speedRatio > 1.0)
{
Console.WriteLine($"✓ ContainsChinese() is {speedRatio:F2}x faster than WordsHelper.HasChinese()");
}
else
{
Console.WriteLine($"⚠ WordsHelper.HasChinese() is {(1/speedRatio):F2}x faster than ContainsChinese()");
}
// Test always passes - this is a measurement test
ClassicAssert.IsTrue(true);
}
[Test]
public void PerformanceComparison_ByStringType()
{
Console.WriteLine("=== PERFORMANCE BY STRING TYPE ===");
var categories = new Dictionary<string, List<string>>
{
["Pure English"] = _testStrings.Where(s => !ContainsChinese(s) && s.All(c => c <= 127)).ToList(),
["Pure Chinese"] = _testStrings.Where(s => ContainsChinese(s) && s.All(c => IsChineseCharacter(c) || char.IsWhiteSpace(c))).ToList(),
["Mixed Content"] = _testStrings.Where(s => ContainsChinese(s) && s.Any(c => c <= 127 && char.IsLetter(c))).ToList(),
["Edge Cases"] = _testStrings.Where(s => string.IsNullOrWhiteSpace(s) || s.All(c => !char.IsLetter(c))).ToList()
};
foreach (var category in categories)
{
if (category.Value.Count == 0) continue;
Console.WriteLine($"\n{category.Key} ({category.Value.Count} strings):");
var sample = category.Value.First();
var displayText = sample.Length > 40 ? sample.Substring(0, 40) + "..." : sample;
Console.WriteLine($" Sample: '{displayText}'");
const int categoryIterations = 5000;
// Test each method
var sw1 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < categoryIterations; i++)
{
foreach (var str in category.Value)
{
_ = ContainsChinese(str);
}
}
sw1.Stop();
var sw2 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < categoryIterations; i++)
{
foreach (var str in category.Value)
{
_ = WordsHelper.HasChinese(str);
}
}
sw2.Stop();
var ratio = (double)sw2.ElapsedTicks / sw1.ElapsedTicks;
Console.WriteLine($" Performance: ContainsChinese is {ratio:F2}x faster");
}
ClassicAssert.IsTrue(true);
}
/// <summary>
/// Optimized Chinese character detection using comprehensive CJK Unicode ranges
/// This method uses ReadOnlySpan for better performance and covers all CJK character ranges
/// </summary>
private static bool ContainsChinese(ReadOnlySpan<char> text)
{
foreach (var c in text)
{
if (IsChineseCharacter(c))
return true;
}
return false;
}
/// <summary>
/// Check if a character is a Chinese character using comprehensive Unicode ranges
/// Covers CJK Unified Ideographs and all extension blocks
/// </summary>
private static bool IsChineseCharacter(char c)
{
return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs (most common Chinese characters)
(c >= 0x3400 && c <= 0x4DBF) || // CJK Extension A
(c >= 0x20000 && c <= 0x2A6DF) || // CJK Extension B
(c >= 0x2A700 && c <= 0x2B73F) || // CJK Extension C
(c >= 0x2B740 && c <= 0x2B81F) || // CJK Extension D
(c >= 0x2B820 && c <= 0x2CEAF) || // CJK Extension E
(c >= 0x2CEB0 && c <= 0x2EBEF) || // CJK Extension F
(c >= 0xF900 && c <= 0xFAFF) || // CJK Compatibility Ideographs
(c >= 0x2F800 && c <= 0x2FA1F); // CJK Compatibility Supplement
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<ProjectGuid>{FF742965-9A80-41A5-B042-D6C7D3A21708}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
@ -39,6 +39,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Calculator\Flow.Launcher.Plugin.Calculator.csproj" />
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Explorer\Flow.Launcher.Plugin.Explorer.csproj" />
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Program\Flow.Launcher.Plugin.Program.csproj" />
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Url\Flow.Launcher.Plugin.Url.csproj" />
@ -48,13 +49,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="nunit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0">
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="nunit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Flow.Launcher.Plugin.Calculator;
using Mages.Core;
using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace Flow.Launcher.Test.Plugins
{
[TestFixture]
public class CalculatorPluginTest
{
private readonly Main _plugin;
private readonly Settings _settings = new()
{
DecimalSeparator = DecimalSeparator.UseSystemLocale,
MaxDecimalPlaces = 10,
ShowErrorMessage = false // Make sure we return the empty results when error occurs
};
private readonly Engine _engine = new(new Configuration
{
Scope = new Dictionary<string, object>
{
{ "e", Math.E }, // e is not contained in the default mages engine
}
});
public CalculatorPluginTest()
{
_plugin = new Main();
var settingField = typeof(Main).GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance);
if (settingField == null)
Assert.Fail("Could not find field '_settings' on Flow.Launcher.Plugin.Calculator.Main");
settingField.SetValue(_plugin, _settings);
var engineField = typeof(Main).GetField("MagesEngine", BindingFlags.NonPublic | BindingFlags.Static);
if (engineField == null)
Assert.Fail("Could not find static field 'MagesEngine' on Flow.Launcher.Plugin.Calculator.Main");
engineField.SetValue(null, _engine);
}
// Basic operations
[TestCase(@"1+1", "2")]
[TestCase(@"2-1", "1")]
[TestCase(@"2*2", "4")]
[TestCase(@"4/2", "2")]
[TestCase(@"2^3", "8")]
// Decimal places
[TestCase(@"10/3", "3.3333333333")]
// Parentheses
[TestCase(@"(1+2)*3", "9")]
[TestCase(@"2^(1+2)", "8")]
// Functions
[TestCase(@"pow(2,3)", "8")]
[TestCase(@"min(1,-1,-2)", "-2")]
[TestCase(@"max(1,-1,-2)", "1")]
[TestCase(@"sqrt(16)", "4")]
[TestCase(@"sin(pi)", "0.0000000000")]
[TestCase(@"cos(0)", "1")]
[TestCase(@"tan(0)", "0")]
[TestCase(@"log10(100)", "2")]
[TestCase(@"log(100)", "2")]
[TestCase(@"log2(8)", "3")]
[TestCase(@"ln(e)", "1")]
[TestCase(@"abs(-5)", "5")]
// Constants
[TestCase(@"pi", "3.1415926536")]
// Complex expressions
[TestCase(@"(2+3)*sqrt(16)-log(100)/ln(e)", "18")]
[TestCase(@"sin(pi/2)+cos(0)+tan(0)", "2")]
// Error handling (should return empty result)
[TestCase(@"10/0", "")]
[TestCase(@"sqrt(-1)", "")]
[TestCase(@"log(0)", "")]
[TestCase(@"invalid_expression", "")]
public void CalculatorTest(string expression, string result)
{
ClassicAssert.AreEqual(GetCalculationResult(expression), result);
}
private string GetCalculationResult(string expression)
{
var results = _plugin.Query(new Plugin.Query()
{
Search = expression
});
return results.Count > 0 ? results[0].Title : string.Empty;
}
}
}

View file

@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Reflection;
using Flow.Launcher.Infrastructure;
using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace Flow.Launcher.Test
{
[TestFixture]
public class TranslationMappingTest
{
[Test]
public void AddNewIndex_ShouldAddTranslatedIndexPlusLength()
{
var mapping = new TranslationMapping();
mapping.AddNewIndex(5, 3);
mapping.AddNewIndex(8, 2);
// 5+3=8, 8+2=10
ClassicAssert.AreEqual(2, GetOriginalToTranslatedCount(mapping));
ClassicAssert.AreEqual(8, GetOriginalToTranslatedAt(mapping, 0));
ClassicAssert.AreEqual(10, GetOriginalToTranslatedAt(mapping, 1));
}
[TestCase(0, 0)]
[TestCase(2, 1)]
[TestCase(3, 1)]
[TestCase(5, 2)]
[TestCase(6, 2)]
public void MapToOriginalIndex_ShouldReturnExpectedIndex(int translatedIndex, int expectedOriginalIndex)
{
var mapping = new TranslationMapping();
// a测试
// a Ce Shi
mapping.AddNewIndex(0, 1);
mapping.AddNewIndex(2, 2);
mapping.AddNewIndex(5, 3);
var result = mapping.MapToOriginalIndex(translatedIndex);
ClassicAssert.AreEqual(expectedOriginalIndex, result);
}
private static int GetOriginalToTranslatedCount(TranslationMapping mapping)
{
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
var list = (List<int>)field.GetValue(mapping);
return list.Count;
}
private static int GetOriginalToTranslatedAt(TranslationMapping mapping, int index)
{
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
var list = (List<int>)field.GetValue(mapping);
return list[index];
}
}
}

View file

@ -14,102 +14,102 @@
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition Height="80" />
</Grid.RowDefinitions>
<Grid>
<StackPanel Grid.Row="0">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="4"
Click="BtnCancel_OnClick"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
</Grid>
</StackPanel>
<StackPanel Margin="26 12 26 0">
<StackPanel Grid.Row="0" Margin="0 0 0 12">
<TextBlock
Grid.Column="0"
Margin="0 0 0 0"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource actionKeywordsTitle}"
TextAlignment="Left" />
</StackPanel>
<StackPanel>
<TextBlock
FontSize="14"
Text="{DynamicResource actionkeyword_tips}"
TextAlignment="Left"
TextWrapping="WrapWithOverflow" />
</StackPanel>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Margin="0 18 0 0" Orientation="Horizontal">
<TextBlock
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource currentActionKeywords}" />
<TextBlock
x:Name="tbOldActionKeyword"
Grid.Row="0"
Grid.Column="1"
Margin="14 10 10 10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource Color05B}" />
</StackPanel>
<StackPanel Margin="0 0 0 10" Orientation="Horizontal">
<TextBlock
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource newActionKeyword}" />
<TextBox
x:Name="tbAction"
Width="105"
Margin="10 10 15 10"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
<Border
<Button
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Right"
Click="BtnCancel_OnClick"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="26 12 26 12"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource actionKeywordsTitle}"
TextAlignment="Left" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="26 0 26 10"
FontSize="14"
Text="{DynamicResource actionkeyword_tips}"
TextAlignment="Left"
TextWrapping="WrapWithOverflow" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="26 0 10 0"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource currentActionKeywords}" />
<TextBox
x:Name="tbOldActionKeyword"
Grid.Row="3"
Grid.Column="1"
Margin="10 10 26 6"
VerticalAlignment="Center"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource Color05B}"
IsReadOnly="True"
Text="List of old keyword(s)" />
<TextBlock
Grid.Row="4"
Grid.Column="0"
Margin="26 6 10 12"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource newActionKeyword}" />
<TextBox
x:Name="tbAction"
Grid.Row="4"
Grid.Column="1"
Margin="10 6 26 10"
VerticalAlignment="Center" />
<Border
Grid.Row="5"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0 10 0 0"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0 1 0 0">
@ -117,15 +117,15 @@
<Button
x:Name="btnCancel"
Width="145"
Height="30"
Margin="10 0 5 0"
Height="38"
Margin="10 0 10 0"
Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" />
<Button
x:Name="btnDone"
Width="145"
Height="30"
Margin="5 0 10 0"
Height="38"
Margin="10 0 10 0"
Click="btnDone_OnClick"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />

View file

@ -20,7 +20,9 @@ namespace Flow.Launcher
private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e)
{
tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, _plugin.Metadata.ActionKeywords.ToArray());
tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, _plugin.Metadata.ActionKeywords);
tbAction.Text = tbOldActionKeyword.Text;
tbAction.SelectAll();
tbAction.Focus();
}
@ -33,38 +35,39 @@ namespace Flow.Launcher
{
var oldActionKeywords = _plugin.Metadata.ActionKeywords;
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList();
newActionKeywords.RemoveAll(string.IsNullOrEmpty);
newActionKeywords = newActionKeywords.Distinct().ToList();
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator)
.Where(s => !string.IsNullOrEmpty(s))
.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))
if (addedActionKeywords.Any(App.API.ActionKeywordAssigned))
{
if (oldActionKeywords.Count != newActionKeywords.Count)
{
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
return;
}
App.API.ShowMsgBox(Localize.newActionKeywordsHasBeenAssigned());
return;
}
var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList();
var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList();
if (oldActionKeywords.Count != newActionKeywords.Count)
{
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
return;
}
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
{
// User just changes the sequence of action keywords
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld"));
}
else
{
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
}
var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList();
var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList();
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
{
// User just changes the sequence of action keywords
App.API.ShowMsgBox(Localize.newActionKeywordsSameAsOld());
}
else
{
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
}
}

View file

@ -2,7 +2,8 @@
x:Class="Flow.Launcher.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
ShutdownMode="OnMainWindowClose"
Startup="OnStartup">
<Application.Resources>
@ -10,17 +11,17 @@
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources>
<ui:ThemeResources.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<ResourceDictionary x:Key="Light" ui:ThemeDictionary.Key="Light">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Light.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<ResourceDictionary x:Key="Dark" ui:ThemeDictionary.Key="Dark">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<ResourceDictionary x:Key="HighContrast" ui:ThemeDictionary.Key="HighContrast">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
</ResourceDictionary.MergedDictionaries>
@ -33,6 +34,15 @@
<ResourceDictionary Source="pack://application:,,,/Themes/Win11Light.xaml" />
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Override styles in UI.Modern.WPF -->
<Thickness x:Key="ListViewItemCompactSelectedBorderThemeThickness">2</Thickness>
<sys:Double x:Key="CheckBoxMinWidth">0</sys:Double>
<sys:Double x:Key="GridViewItemMinWidth">0</sys:Double>
<sys:Double x:Key="GridViewItemMinHeight">40</sys:Double>
<sys:Double x:Key="ListViewItemMinWidth">0</sys:Double>
<sys:Double x:Key="ListViewItemMinHeight">36</sys:Double>
<SolidColorBrush x:Key="NavigationViewSelectionIndicatorForeground" Color="#FF0063B1" />
</ResourceDictionary>
</Application.Resources>
</Application>

View file

@ -16,11 +16,13 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.SettingPages.ViewModels;
using Flow.Launcher.ViewModel;
using iNKORE.UI.WPF.Modern.Common;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.VisualStudio.Threading;
@ -41,9 +43,10 @@ namespace Flow.Launcher
private static readonly string ClassName = nameof(App);
private static bool _disposed;
private static Settings _settings;
private static MainWindow _mainWindow;
private readonly MainViewModel _mainVM;
private readonly Settings _settings;
private readonly Internationalization _internationalization;
// To prevent two disposals running at the same time.
private static readonly object _disposingLock = new();
@ -54,19 +57,11 @@ namespace Flow.Launcher
public App()
{
// Do not use bitmap cache since it can cause WPF second window freezing issue
ShadowAssist.UseBitmapCache = false;
// 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;
}
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
// Configure the dependency injection container
try
@ -121,22 +116,13 @@ namespace Flow.Launcher
API = Ioc.Default.GetRequiredService<IPublicAPI>();
_settings.Initialize();
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
_internationalization = Ioc.Default.GetRequiredService<Internationalization>();
}
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
@ -146,6 +132,29 @@ namespace Flow.Launcher
[STAThread]
public static void Main()
{
// Initialize settings so that we can get language code
try
{
var storage = new FlowLauncherJsonStorage<Settings>();
_settings = storage.Load();
_settings.SetStorage(storage);
}
catch (Exception e)
{
ShowErrorMsgBoxAndFailFast("Cannot load setting storage, please check local data directory", e);
return;
}
// Initialize system language before changing culture info
Internationalization.InitSystemLanguageCode();
// Change culture info before application creation to localize WinForm windows
if (_settings.Language != Constant.SystemLanguageCode)
{
Internationalization.ChangeCultureInfo(_settings.Language);
}
// Start the application as a single instance
if (SingleInstance<App>.InitializeAsFirstInstance())
{
using var application = new App();
@ -156,6 +165,19 @@ namespace Flow.Launcher
#endregion
#region Fail Fast
private static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
{
// Firstly show users the message
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
Environment.FailFast(message, e);
}
#endregion
#region App Events
#pragma warning disable VSTHRD100 // Avoid async void methods
@ -177,6 +199,12 @@ namespace Flow.Launcher
Notification.Install();
// Enable Win32 dark mode if the system is in dark mode before creating all windows
Win32Helper.EnableWin32DarkMode(_settings.ColorScheme);
// Initialize language before portable clean up since it needs translations
await _internationalization.InitializeLanguageAsync();
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
@ -197,10 +225,13 @@ namespace Flow.Launcher
Http.Proxy = _settings.Proxy;
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
await API.UpdatePluginManifestAsync();
await PluginManager.InitializePluginsAsync();
// Change language after all plugins are initialized because we need to update plugin title based on their api
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
// Update plugin titles after plugins are initialized with their api instances
Internationalization.UpdatePluginMetadataTranslations();
await imageLoadertask;
@ -216,12 +247,16 @@ namespace Flow.Launcher
// Initialize theme for main window
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs());
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
RegisterExitEvents();
AutoStartup();
AutoUpdates();
AutoPluginUpdates();
API.SaveAppAllSettings();
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
@ -234,7 +269,7 @@ namespace Flow.Launcher
/// Check startup only for Release
/// </summary>
[Conditional("RELEASE")]
private void AutoStartup()
private static void AutoStartup()
{
// we try to enable auto-startup on first launch, or reenable if it was removed
// but the user still has the setting set
@ -249,13 +284,13 @@ namespace Flow.Launcher
// 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;
API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
API.ShowMsgError(Localize.setAutoStartFailed(), e.Message);
}
}
}
[Conditional("RELEASE")]
private void AutoUpdates()
private static void AutoUpdates()
{
_ = Task.Run(async () =>
{
@ -272,6 +307,38 @@ namespace Flow.Launcher
});
}
[Conditional("RELEASE")]
private static void AutoPluginUpdates()
{
_ = Task.Run(async () =>
{
if (_settings.AutoUpdatePlugins)
{
// check plugin updates every 5 hour
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Current.Dispatcher.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
});
});
while (await timer.WaitForNextTickAsync())
// check updates on startup
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Current.Dispatcher.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
});
});
}
});
}
#endregion
#region Register Events
@ -363,6 +430,8 @@ namespace Flow.Launcher
// since some resources owned by the thread need to be disposed.
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
_mainVM?.Dispose();
DialogJump.Dispose();
_internationalization.Dispose();
}
API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------");

View file

@ -5,7 +5,7 @@ using System.Windows.Input;
namespace Flow.Launcher.Converters;
internal class BoolToIMEConversionModeConverter : IValueConverter
public class BoolToIMEConversionModeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
@ -22,7 +22,7 @@ internal class BoolToIMEConversionModeConverter : IValueConverter
}
}
internal class BoolToIMEStateConverter : IValueConverter
public class BoolToIMEStateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{

View file

@ -0,0 +1,91 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Flow.Launcher.Converters;
public class CornerRadiusFilterConverter : DependencyObject, IValueConverter
{
public CornerRadiusFilterKind Filter { get; set; }
public double Scale { get; set; } = 1.0;
public static CornerRadius Convert(CornerRadius radius, CornerRadiusFilterKind filterKind)
{
CornerRadius result = radius;
switch (filterKind)
{
case CornerRadiusFilterKind.Top:
result.BottomLeft = 0;
result.BottomRight = 0;
break;
case CornerRadiusFilterKind.Right:
result.TopLeft = 0;
result.BottomLeft = 0;
break;
case CornerRadiusFilterKind.Bottom:
result.TopLeft = 0;
result.TopRight = 0;
break;
case CornerRadiusFilterKind.Left:
result.TopRight = 0;
result.BottomRight = 0;
break;
}
return result;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var cornerRadius = (CornerRadius)value;
var scale = Scale;
if (!double.IsNaN(scale))
{
cornerRadius.TopLeft *= scale;
cornerRadius.TopRight *= scale;
cornerRadius.BottomRight *= scale;
cornerRadius.BottomLeft *= scale;
}
var filterType = Filter;
if (filterType == CornerRadiusFilterKind.TopLeftValue ||
filterType == CornerRadiusFilterKind.BottomRightValue)
{
return GetDoubleValue(cornerRadius, filterType);
}
return Convert(cornerRadius, filterType);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private static double GetDoubleValue(CornerRadius radius, CornerRadiusFilterKind filterKind)
{
switch (filterKind)
{
case CornerRadiusFilterKind.TopLeftValue:
return radius.TopLeft;
case CornerRadiusFilterKind.BottomRightValue:
return radius.BottomRight;
}
return 0;
}
}
public enum CornerRadiusFilterKind
{
None,
Top,
Right,
Bottom,
Left,
TopLeftValue,
BottomRightValue
}

View file

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Flow.Launcher.Converters;
public class PlacementRectangleConverter : IMultiValueConverter
{
public Thickness Margin { get; set; }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 &&
values[0] is double width &&
values[1] is double height)
{
var margin = Margin;
var topLeft = new Point(margin.Left, margin.Top);
var bottomRight = new Point(width - margin.Right, height - margin.Bottom);
var rect = new Rect(topLeft, bottomRight);
return rect;
}
return Rect.Empty;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View file

@ -33,15 +33,39 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter
{
var selectedResult = selectedItem.Result;
var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " ";
var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title;
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
string selectedResultPossibleSuggestion = null;
// Firstly check if the result has QuerySuggestionText
if (!string.IsNullOrEmpty(selectedResult.QuerySuggestionText))
{
selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.QuerySuggestionText;
// If this QuerySuggestionText does not start with the queryText, set it to null
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
{
selectedResultPossibleSuggestion = null;
}
}
// Then check Title as suggestion
if (string.IsNullOrEmpty(selectedResultPossibleSuggestion))
{
selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title;
// If this QuerySuggestionText does not start with the queryText, set it to null
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
{
selectedResultPossibleSuggestion = null;
}
}
if (string.IsNullOrEmpty(selectedResultPossibleSuggestion))
return string.Empty;
// For AutocompleteQueryCommand.
// When user typed lower case and result title is uppercase, we still want to display suggestion
selectedItem.QuerySuggestionText = queryText + selectedResultPossibleSuggestion.Substring(queryText.Length);
selectedItem.QuerySuggestionText = string.Concat(queryText, selectedResultPossibleSuggestion.AsSpan(queryText.Length));
// Check if Text will be larger than our QueryTextBox
Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch);

View file

@ -1,20 +1,19 @@
using Flow.Launcher.Plugin.Everything.Everything;
using Flow.Launcher.Plugin.Explorer.Helper;
using System;
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Flow.Launcher.Plugin.Explorer.Views.Converters;
namespace Flow.Launcher.Converters;
public class EnumNameConverter : IValueConverter
public class SharedSizeGroupConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is SortOption option ? option.GetTranslatedName() : value;
return (Visibility)value != Visibility.Collapsed ? (string)parameter : null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View file

@ -5,7 +5,7 @@ using System.Windows.Input;
namespace Flow.Launcher.Converters;
class StringToKeyBindingConverter : IValueConverter
public class StringToKeyBindingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{

View file

@ -119,7 +119,8 @@
Grid.Column="1"
Margin="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Center" />
VerticalAlignment="Center"
Text="{Binding ActionKeyword}" />
<Button
x:Name="btnTestActionKeyword"
Grid.Row="1"
@ -150,7 +151,20 @@
Margin="5 0 10 0"
Click="btnAdd_OnClick"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBlock
x:Name="tbAdd"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{DynamicResource done}"
Visibility="Collapsed" />
<TextBlock
x:Name="tbUpdate"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{DynamicResource update}"
Visibility="Collapsed" />
</Grid>
</Button>
</StackPanel>
</Border>

View file

@ -1,73 +1,52 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows;
using System.Windows.Controls;
using Flow.Launcher.Helper;
using System.Windows.Input;
using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher
{
public partial class CustomQueryHotkeySetting : Window
{
private readonly Settings _settings;
public string Hotkey { get; set; } = string.Empty;
public string ActionKeyword { get; set; } = string.Empty;
private bool update;
private CustomPluginHotkey updateCustomHotkey;
private readonly bool update;
private readonly CustomPluginHotkey originalCustomHotkey;
public CustomQueryHotkeySetting(Settings settings)
public CustomQueryHotkeySetting()
{
_settings = settings;
InitializeComponent();
tbAdd.Visibility = Visibility.Visible;
}
public CustomQueryHotkeySetting(CustomPluginHotkey hotkey)
{
originalCustomHotkey = hotkey;
update = true;
ActionKeyword = originalCustomHotkey.ActionKeyword;
InitializeComponent();
tbUpdate.Visibility = Visibility.Visible;
HotkeyControl.SetHotkey(originalCustomHotkey.Hotkey, false);
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void btnAdd_OnClick(object sender, RoutedEventArgs e)
{
if (!update)
Hotkey = HotkeyControl.CurrentHotkey.ToString();
if (string.IsNullOrEmpty(Hotkey) && string.IsNullOrEmpty(ActionKeyword))
{
_settings.CustomPluginHotkeys ??= new ObservableCollection<CustomPluginHotkey>();
var pluginHotkey = new CustomPluginHotkey
{
Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text
};
_settings.CustomPluginHotkeys.Add(pluginHotkey);
HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
}
else
{
var oldHotkey = updateCustomHotkey.Hotkey;
updateCustomHotkey.ActionKeyword = tbAction.Text;
updateCustomHotkey.Hotkey = HotkeyControl.CurrentHotkey.ToString();
//remove origin hotkey
HotKeyMapper.RemoveHotkey(oldHotkey);
HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey);
}
Close();
}
public void UpdateItem(CustomPluginHotkey item)
{
updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o =>
o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey);
if (updateCustomHotkey == null)
{
App.API.ShowMsgBox(App.API.GetTranslation("invalidPluginHotkey"));
Close();
App.API.ShowMsgBox(Localize.emptyPluginHotkey());
return;
}
tbAction.Text = updateCustomHotkey.ActionKeyword;
HotkeyControl.SetHotkey(updateCustomHotkey.Hotkey, false);
update = true;
lblAdd.Text = App.API.GetTranslation("update");
DialogResult = !update || originalCustomHotkey.Hotkey != Hotkey || originalCustomHotkey.ActionKeyword != ActionKeyword;
Close();
}
private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e)
@ -79,6 +58,7 @@ namespace Flow.Launcher
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
{
DialogResult = false;
Close();
}

View file

@ -118,24 +118,26 @@
FontSize="14"
Text="{DynamicResource customShortcutExpansion}" />
<DockPanel
Grid.Row="1"
Grid.Column="1"
LastChildFill="True">
<Button
x:Name="btnTestShortcut"
Margin="0 0 10 0"
Padding="10 5 10 5"
Click="BtnTestShortcut_OnClick"
Content="{DynamicResource preview}"
DockPanel.Dock="Right" />
<Grid Grid.Row="1" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
x:Name="tbExpand"
Grid.Column="0"
Margin="10 0 10 0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Value}" />
</DockPanel>
<Button
x:Name="btnTestShortcut"
Grid.Column="1"
Margin="0 0 10 0"
Padding="10 5 10 5"
Click="BtnTestShortcut_OnClick"
Content="{DynamicResource preview}" />
</Grid>
</Grid>
</StackPanel>
</StackPanel>

View file

@ -40,15 +40,17 @@ namespace Flow.Launcher
{
if (string.IsNullOrEmpty(Key) || string.IsNullOrEmpty(Value))
{
App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut"));
App.API.ShowMsgBox(Localize.emptyShortcut());
return;
}
// Check if key is modified or adding a new one
if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key))
{
App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut"));
App.API.ShowMsgBox(Localize.duplicateShortcut());
return;
}
DialogResult = !update || originalKey != Key || originalValue != Value;
Close();
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>false</UseWindowsForms>
<StartupObject>Flow.Launcher.App</StartupObject>
@ -12,6 +12,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -36,8 +37,55 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
</PropertyGroup>
<Target Name="RemoveUnnecessaryRuntimesAfterBuild" AfterTargets="Build">
<RemoveDir Directories="$(OutputPath)runtimes\browser-wasm;
$(OutputPath)runtimes\linux-arm;
$(OutputPath)runtimes\linux-arm64;
$(OutputPath)runtimes\linux-armel;
$(OutputPath)runtimes\linux-mips64;
$(OutputPath)runtimes\linux-musl-arm;
$(OutputPath)runtimes\linux-musl-arm64;
$(OutputPath)runtimes\linux-musl-x64;
$(OutputPath)runtimes\linux-musl-s390x;
$(OutputPath)runtimes\linux-ppc64le;
$(OutputPath)runtimes\linux-s390x;
$(OutputPath)runtimes\linux-x64;
$(OutputPath)runtimes\linux-x86;
$(OutputPath)runtimes\maccatalyst-arm64;
$(OutputPath)runtimes\maccatalyst-x64;
$(OutputPath)runtimes\osx;
$(OutputPath)runtimes\osx-arm64;
$(OutputPath)runtimes\osx-x64;
$(OutputPath)runtimes\win-arm;
$(OutputPath)runtimes\win-arm64;"/>
</Target>
<Target Name="RemoveUnnecessaryRuntimesAfterPublish" AfterTargets="Publish">
<RemoveDir Directories="$(PublishDir)runtimes\browser-wasm;
$(PublishDir)runtimes\linux-arm;
$(PublishDir)runtimes\linux-arm64;
$(PublishDir)runtimes\linux-armel;
$(PublishDir)runtimes\linux-mips64;
$(PublishDir)runtimes\linux-musl-arm;
$(PublishDir)runtimes\linux-musl-arm64;
$(PublishDir)runtimes\linux-musl-x64;
$(PublishDir)runtimes\linux-musl-s390x;
$(PublishDir)runtimes\linux-ppc64le;
$(PublishDir)runtimes\linux-s390x;
$(PublishDir)runtimes\linux-x64;
$(PublishDir)runtimes\linux-x86;
$(PublishDir)runtimes\maccatalyst-arm64;
$(PublishDir)runtimes\maccatalyst-x64;
$(PublishDir)runtimes\osx;
$(PublishDir)runtimes\osx-arm64;
$(PublishDir)runtimes\osx-x64;
$(PublishDir)runtimes\win-arm;
$(PublishDir)runtimes\win-arm64;"/>
</Target>
<ItemGroup>
<ApplicationDefinition Remove="App.xaml" />
</ItemGroup>
@ -77,7 +125,7 @@
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Segoe Fluent Icons.ttf">
<Content Include="Resources\SegoeFluentIcons.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
@ -85,30 +133,26 @@
<ItemGroup>
<PackageReference Include="ChefKeys" Version="0.1.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Fody" Version="6.5.5">
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
<PackageReference Include="Fody" Version="6.9.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.1" />
<PackageReference Include="MdXaml" Version="1.27.0" />
<PackageReference Include="MdXaml.AnimatedGif" Version="1.27.0" />
<PackageReference Include="MdXaml.Html" Version="1.27.0" />
<PackageReference Include="MdXaml.Plugins" Version="1.27.0" />
<PackageReference Include="MdXaml.Svg" Version="1.27.0" />
<!-- 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.Extensions.DependencyInjection" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<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="3.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
<PackageReference Include="TaskScheduler" Version="2.12.1" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" />
<PackageReference Include="TaskScheduler" Version="2.12.2" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
@ -117,6 +161,10 @@
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
<PropertyGroup>
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
</PropertyGroup>
<ItemGroup>
<Content Include="Resources\open.wav">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
@ -139,6 +187,9 @@
<None Update="Resources\dev.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\double_pinyin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
@ -147,7 +198,6 @@
<Target Name="RemoveDuplicateAnalyzers" BeforeTargets="CoreCompile">
<!-- Work around https://github.com/dotnet/wpf/issues/6792 -->
<ItemGroup>
<FilteredAnalyzer Include="@(Analyzer-&gt;Distinct())" />
<Analyzer Remove="@(Analyzer)" />

View file

@ -0,0 +1,33 @@
using System.Windows;
using System.Windows.Controls;
namespace Flow.Launcher.Helper;
public static class BorderHelper
{
#region Child
public static readonly DependencyProperty ChildProperty =
DependencyProperty.RegisterAttached(
"Child",
typeof(UIElement),
typeof(BorderHelper),
new PropertyMetadata(default(UIElement), OnChildChanged));
public static UIElement GetChild(Border border)
{
return (UIElement)border.GetValue(ChildProperty);
}
public static void SetChild(Border border, UIElement value)
{
border.SetValue(ChildProperty, value);
}
private static void OnChildChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Border)d).Child = (UIElement)e.NewValue;
}
#endregion
}

View file

@ -1,11 +1,12 @@
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using NHotkey;
using NHotkey.Wpf;
using Flow.Launcher.ViewModel;
using System;
using ChefKeys;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.ViewModel;
using NHotkey;
using NHotkey.Wpf;
namespace Flow.Launcher.Helper;
@ -22,6 +23,10 @@ internal static class HotKeyMapper
_settings = Ioc.Default.GetService<Settings>();
SetHotkey(_settings.Hotkey, OnToggleHotkey);
if (_settings.EnableDialogJump)
{
SetHotkey(_settings.DialogJumpHotkey, DialogJump.OnToggleHotkey);
}
LoadCustomPluginHotkey();
}
@ -56,8 +61,8 @@ internal static class HotKeyMapper
string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace));
string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
string errorMsg = Localize.registerHotkeyFailed(hotkeyStr);
string errorMsgTitle = Localize.MessageBoxTitle();
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
@ -82,8 +87,8 @@ internal static class HotKeyMapper
e.Message,
e.StackTrace,
hotkeyStr));
string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
string errorMsg = Localize.registerHotkeyFailed(hotkeyStr);
string errorMsgTitle = Localize.MessageBoxTitle();
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}
@ -107,8 +112,8 @@ internal static class HotKeyMapper
string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}",
e.Message,
e.StackTrace));
string errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr);
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
string errorMsg = Localize.unregisterHotkeyFailed(hotkeyStr);
string errorMsgTitle = Localize.MessageBoxTitle();
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
@ -16,7 +17,7 @@ public static class WallpaperPathRetrieval
private const int MaxCacheSize = 3;
private static readonly Dictionary<(string, DateTime), ImageBrush> WallpaperCache = new();
private static readonly object CacheLock = new();
private static readonly Lock CacheLock = new();
public static Brush GetWallpaperBrush()
{
@ -31,7 +32,7 @@ public static class WallpaperPathRetrieval
var wallpaperPath = Win32Helper.GetWallpaperPath();
if (string.IsNullOrEmpty(wallpaperPath) || !File.Exists(wallpaperPath))
{
App.API.LogInfo(ClassName, $"Wallpaper path is invalid: {wallpaperPath}");
App.API.LogError(ClassName, $"Wallpaper path is invalid: {wallpaperPath}");
var wallpaperColor = GetWallpaperColor();
return new SolidColorBrush(wallpaperColor);
}
@ -47,17 +48,22 @@ public static class WallpaperPathRetrieval
return cachedWallpaper;
}
}
using var fileStream = File.OpenRead(wallpaperPath);
var decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);
var frame = decoder.Frames[0];
var originalWidth = frame.PixelWidth;
var originalHeight = frame.PixelHeight;
int originalWidth, originalHeight;
// Use `using ()` instead of `using var` sentence here to ensure the wallpaper file is not locked
using (var fileStream = File.OpenRead(wallpaperPath))
{
var decoder = BitmapDecoder.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);
var frame = decoder.Frames[0];
originalWidth = frame.PixelWidth;
originalHeight = frame.PixelHeight;
}
if (originalWidth == 0 || originalHeight == 0)
{
App.API.LogInfo(ClassName, $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}");
return new SolidColorBrush(Colors.Transparent);
App.API.LogError(ClassName, $"Failed to load bitmap: Width={originalWidth}, Height={originalHeight}");
var wallpaperColor = GetWallpaperColor();
return new SolidColorBrush(wallpaperColor);
}
// Calculate the scaling factor to fit the image within 800x600 while preserving aspect ratio
@ -70,7 +76,9 @@ public static class WallpaperPathRetrieval
// Set DecodePixelWidth and DecodePixelHeight to resize the image while preserving aspect ratio
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad; // Use OnLoad to ensure the wallpaper file is not locked
bitmap.UriSource = new Uri(wallpaperPath);
bitmap.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
bitmap.DecodePixelWidth = decodedPixelWidth;
bitmap.DecodePixelHeight = decodedPixelHeight;
bitmap.EndInit();
@ -104,13 +112,13 @@ public static class WallpaperPathRetrieval
private static Color GetWallpaperColor()
{
RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", false);
using var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", false);
var result = key?.GetValue("Background", null);
if (result is string strResult)
{
try
{
var parts = strResult.Trim().Split(new[] { ' ' }, 3).Select(byte.Parse).ToList();
var parts = strResult.Trim().Split([' '], 3).Select(byte.Parse).ToList();
return Color.FromRgb(parts[0], parts[1], parts[2]);
}
catch (Exception ex)

View file

@ -1,11 +1,23 @@
using Microsoft.Win32;
using System;
using Microsoft.Win32;
namespace Flow.Launcher.Helper;
internal static class WindowsMediaPlayerHelper
{
private static readonly string ClassName = nameof(WindowsMediaPlayerHelper);
internal static bool IsWindowsMediaPlayerInstalled()
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MediaPlayer");
return key?.GetValue("Installation Directory") != null;
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MediaPlayer");
return key?.GetValue("Installation Directory") != null;
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to check if Windows Media Player is installed", e);
return false;
}
}
}

View file

@ -110,7 +110,8 @@ namespace Flow.Launcher
SelectPrevItemHotkey,
SelectPrevItemHotkey2,
SelectNextItemHotkey,
SelectNextItemHotkey2
SelectNextItemHotkey2,
DialogJumpHotkey,
}
// We can initialize settings in static field because it has been constructed in App constuctor
@ -142,6 +143,7 @@ namespace Flow.Launcher
HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2,
HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey,
HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2,
HotkeyType.DialogJumpHotkey => _settings.DialogJumpHotkey,
_ => throw new System.NotImplementedException("Hotkey type not set")
};
}
@ -201,6 +203,9 @@ namespace Flow.Launcher
case HotkeyType.SelectNextItemHotkey2:
_settings.SelectNextItemHotkey2 = value;
break;
case HotkeyType.DialogJumpHotkey:
_settings.DialogJumpHotkey = value;
break;
default:
throw new System.NotImplementedException("Hotkey type not set");
}
@ -229,7 +234,7 @@ namespace Flow.Launcher
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
public string EmptyHotkey => App.API.GetTranslation("none");
public string EmptyHotkey => Localize.none();
public ObservableCollection<string> KeysToDisplay { get; set; } = new();

View file

@ -2,7 +2,7 @@
x:Class="Flow.Launcher.HotkeyControlDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
Background="{DynamicResource PopuBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0 1 0 0"

View file

@ -9,7 +9,7 @@ using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using ModernWpf.Controls;
using iNKORE.UI.WPF.Modern.Controls;
namespace Flow.Launcher;
@ -33,7 +33,7 @@ public partial class HotkeyControlDialog : ContentDialog
public EResultType ResultType { get; private set; } = EResultType.Cancel;
public string ResultValue { get; private set; } = string.Empty;
public static string EmptyHotkey => App.API.GetTranslation("none");
public static string EmptyHotkey => Localize.none();
private static bool isOpenFlowHotkey;
@ -41,7 +41,7 @@ public partial class HotkeyControlDialog : ContentDialog
{
WindowTitle = windowTitle switch
{
"" or null => App.API.GetTranslation("hotkeyRegTitle"),
"" or null => Localize.hotkeyRegTitle(),
_ => windowTitle
};
DefaultHotkey = defaultHotkey;
@ -146,10 +146,7 @@ public partial class HotkeyControlDialog : ContentDialog
Alert.Visibility = Visibility.Visible;
if (registeredHotkeyData.RemoveHotkey is not null)
{
tbMsg.Text = string.Format(
App.API.GetTranslation("hotkeyUnavailableEditable"),
description
);
tbMsg.Text = Localize.hotkeyUnavailableEditable(description);
SaveBtn.IsEnabled = false;
SaveBtn.Visibility = Visibility.Collapsed;
OverwriteBtn.IsEnabled = true;
@ -158,10 +155,7 @@ public partial class HotkeyControlDialog : ContentDialog
}
else
{
tbMsg.Text = string.Format(
App.API.GetTranslation("hotkeyUnavailableUneditable"),
description
);
tbMsg.Text = Localize.hotkeyUnavailableUneditable(description);
SaveBtn.IsEnabled = false;
SaveBtn.Visibility = Visibility.Visible;
OverwriteBtn.IsEnabled = false;
@ -175,7 +169,7 @@ public partial class HotkeyControlDialog : ContentDialog
if (!CheckHotkeyAvailability(hotkey.Value, true))
{
tbMsg.Text = App.API.GetTranslation("hotkeyUnavailable");
tbMsg.Text = Localize.hotkeyUnavailable();
Alert.Visibility = Visibility.Visible;
SaveBtn.IsEnabled = false;
SaveBtn.Visibility = Visibility.Visible;

View file

@ -10,12 +10,32 @@
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
Click yes if you would like select the {0} executable again. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">تعذر تعيين مسار الملف التنفيذي لـ {0}، يرجى المحاولة من إعدادات Flow (قم بالتمرير إلى الأسفل).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">فشل في تهيئة الإضافات</system:String>
<system:String x:Key="failedToInitializePluginsMessage">الإضافات: {0} - فشل في التحميل وسيتم تعطيلها، يرجى الاتصال بمطور الإضافة للحصول على المساعدة</system:String>
<!-- Portable -->
<system:String x:Key="restartToDisablePortableMode">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</system:String>
<system:String x:Key="restartToEnablePortableMode">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</system:String>
<system:String x:Key="moveToDifferentLocation">Flow Launcher has detected you enabled portable mode, would you like to move it to a different location?</system:String>
<system:String x:Key="shortcutsUninstallerCreated">Flow Launcher has detected you disabled portable mode, the relevant shortcuts and uninstaller entry have been created</system:String>
<system:String x:Key="userDataDuplicated">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.</system:String>
<!-- Plugin Loader -->
<system:String x:Key="pluginHasErrored">The following plugin has errored and cannot be loaded:</system:String>
<system:String x:Key="pluginsHaveErrored">The following plugins have errored and cannot be loaded:</system:String>
<system:String x:Key="referToLogs">Please refer to the logs for more information</system:String>
<!-- Http -->
<system:String x:Key="pleaseTryAgain">Please try again</system:String>
<system:String x:Key="parseProxyFailed">Unable to parse Http Proxy</system:String>
<!-- AbstractPluginEnvironment -->
<system:String x:Key="failToInstallTypeScriptEnv">Failed to install TypeScript environment. Please try again later</system:String>
<system:String x:Key="failToInstallPythonEnv">Failed to install Python environment. Please try again later.</system:String>
<!-- MainWindow -->
<system:String x:Key="registerHotkeyFailed">فشل في تسجيل مفتاح التشغيل السريع &quot;{0}&quot;. قد يكون المفتاح مستخدمًا من قبل برنامج آخر. قم بتغيير المفتاح، أو قم بإغلاق البرنامج الآخر.</system:String>
<system:String x:Key="unregisterHotkeyFailed">Failed to unregister hotkey &quot;{0}&quot;. Please try again or see log for details</system:String>
@ -91,6 +111,7 @@
<system:String x:Key="typingStartEn">دائمًا ابدأ الكتابة بوضع الإنجليزية</system:String>
<system:String x:Key="typingStartEnTooltip">تغيير مؤقت لطريقة الإدخال إلى وضع الإنجليزية عند تنشيط Flow.</system:String>
<system:String x:Key="autoUpdates">التحديث التلقائي</system:String>
<system:String x:Key="autoUpdatesTooltip">Automatically check and update the app when available</system:String>
<system:String x:Key="select">اختر</system:String>
<system:String x:Key="hideOnStartup">إخفاء Flow Launcher عند بدء التشغيل</system:String>
<system:String x:Key="hideOnStartupToolTip">يتم إخفاء نافذة بحث Flow Launcher في العلبة بعد بدء التشغيل.</system:String>
@ -102,7 +123,20 @@
<system:String x:Key="SearchPrecisionLow">منخفضة</system:String>
<system:String x:Key="SearchPrecisionRegular">عادية</system:String>
<system:String x:Key="ShouldUsePinyin">البحث باستخدام Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">السماح باستخدام Pinyin للبحث. Pinyin هو نظام التهجئة الروماني القياسي لترجمة الصينية.</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Pinyin is the standard system of romanized spelling for translating Chinese. Please note, enabling this can significantly increase memory usage during search.</system:String>
<system:String x:Key="ShouldUseDoublePinyin">Use Double Pinyin</system:String>
<system:String x:Key="ShouldUseDoublePinyinToolTip">Use Double Pinyin instead of Full Pinyin to search.</system:String>
<system:String x:Key="DoublePinyinSchema">Double Pinyin Schema</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoHe">Xiao He</system:String>
<system:String x:Key="DoublePinyinSchemasZiRanMa">Zi Ran Ma</system:String>
<system:String x:Key="DoublePinyinSchemasWeiRuan">Wei Ruan</system:String>
<system:String x:Key="DoublePinyinSchemasZhiNengABC">Zhi Neng ABC</system:String>
<system:String x:Key="DoublePinyinSchemasZiGuangPinYin">Zi Guang Pin Yin</system:String>
<system:String x:Key="DoublePinyinSchemasPinYinJiaJia">Pin Yin Jia Jia</system:String>
<system:String x:Key="DoublePinyinSchemasXingKongJianDao">Xing Kong Jian Dao</system:String>
<system:String x:Key="DoublePinyinSchemasDaNiu">Da Niu</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoLang">Xiao Lang</system:String>
<system:String x:Key="AlwaysPreview">دائمًا معاينة</system:String>
<system:String x:Key="AlwaysPreviewToolTip">فتح لوحة المعاينة دائمًا عند تنشيط Flow. اضغط على {0} للتبديل بين المعاينة وعدمها.</system:String>
<system:String x:Key="shadowEffectNotAllowed">تأثير الظل غير مسموح به بينما يتم تمكين تأثير التمويه في السمة الحالية</system:String>
@ -131,11 +165,21 @@
<system:String x:Key="KoreanImeOpenLinkButton">فتح</system:String>
<system:String x:Key="KoreanImeRegistry">Use Previous Korean IME</system:String>
<system:String x:Key="KoreanImeRegistryTooltip">You can change the Previous Korean IME settings directly from here</system:String>
<system:String x:Key="KoreanImeSettingChangeFailTitle">Failed to change Korean IME setting</system:String>
<system:String x:Key="KoreanImeSettingChangeFailSubTitle">Please check your system registry access or contact support.</system:String>
<system:String x:Key="homePage">Home Page</system:String>
<system:String x:Key="homePageToolTip">Show home page results when query text is empty.</system:String>
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
<system:String x:Key="showAtTopmost">Show Search Window at Foremost</system:String>
<system:String x:Key="showAtTopmostToolTip">Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.</system:String>
<system:String x:Key="autoRestartAfterChanging">Restart after modifying plugin via Plugin Store</system:String>
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
<system:String x:Key="showUnknownSourceWarning">Show unknown source warning</system:String>
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
<system:String x:Key="autoUpdatePlugins">Auto update plugins</system:String>
<system:String x:Key="autoUpdatePluginsToolTip">Automatically check plugin updates and notify if there are any updates available</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">البحث عن إضافة</system:String>
@ -174,6 +218,13 @@
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="pluginModifiedAlreadyTitle">{0} modified already</system:String>
<system:String x:Key="pluginModifiedAlreadyMessage">Please restart Flow before making any further changes</system:String>
<system:String x:Key="failedToInstallPluginTitle">Fail to install {0}</system:String>
<system:String x:Key="failedToUninstallPluginTitle">Fail to uninstall {0}</system:String>
<system:String x:Key="fileNotFoundMessage">Unable to find plugin.json from the extracted zip file, or this path {0} does not exist</system:String>
<system:String x:Key="pluginExistAlreadyMessage">A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin</system:String>
<system:String x:Key="errorCreatingSettingPanel">Error creating setting panel for plugin {0}:{1}{2}</system:String>
<!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">متجر الإضافات</system:String>
@ -189,6 +240,34 @@
<system:String x:Key="LabelNew">إصدار جديد</system:String>
<system:String x:Key="LabelNewToolTip">تم تحديث هذه الإضافة في آخر 7 أيام</system:String>
<system:String x:Key="LabelUpdateToolTip">يتوفر تحديث جديد</system:String>
<system:String x:Key="ErrorInstallingPlugin">خطأ في تثبيت الإضاف</system:String>
<system:String x:Key="ErrorUninstallingPlugin">خطأ في إلغاء تثبيت الإضافة</system:String>
<system:String x:Key="ErrorUpdatingPlugin">Error updating plugin</system:String>
<system:String x:Key="KeepPluginSettingsTitle">Keep plugin settings</system:String>
<system:String x:Key="KeepPluginSettingsSubtitle">Do you want to keep the settings of the plugin for the next usage?</system:String>
<system:String x:Key="InstallSuccessNoRestart">تم تثبيت الإضافة {0} بنجاح. يرجى إعادة تشغيل Flow.</system:String>
<system:String x:Key="UninstallSuccessNoRestart">تم إلغاء تثبيت الإضافة {0} بنجاح. يرجى إعادة تشغيل Flow.</system:String>
<system:String x:Key="UpdateSuccessNoRestart">تم تحديث الإضافة {0} بنجاح. يرجى إعادة تشغيل Flow.</system:String>
<system:String x:Key="InstallPromptTitle">Plugin install</system:String>
<system:String x:Key="InstallPromptSubtitle">{0} بواسطة {1} {2}{2}هل ترغب في تثبيت هذه الإضافة؟</system:String>
<system:String x:Key="UninstallPromptTitle">Plugin uninstall</system:String>
<system:String x:Key="UninstallPromptSubtitle">{0} بواسطة {1} {2}{2}هل ترغب في إلغاء تثبيت هذه الإضافة؟</system:String>
<system:String x:Key="UpdatePromptTitle">Plugin update</system:String>
<system:String x:Key="UpdatePromptSubtitle">{0} بواسطة {1} {2}{2}هل ترغب في تحديث هذه الإضافة؟</system:String>
<system:String x:Key="DownloadingPlugin">تحميل الإضاف</system:String>
<system:String x:Key="AutoRestartAfterChange">Automatically restart after installing/uninstalling/updating plugins in plugin store</system:String>
<system:String x:Key="ZipFileNotHavePluginJson">Zip file does not have a valid plugin.json configuration</system:String>
<system:String x:Key="InstallFromUnknownSourceTitle">التثبيت من مصدر غير معرو</system:String>
<system:String x:Key="InstallFromUnknownSourceSubtitle">This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window)</system:String>
<system:String x:Key="ZipFiles">Zip files</system:String>
<system:String x:Key="SelectZipFile">Please select zip file</system:String>
<system:String x:Key="installLocalPluginTooltip">Install plugin from local path</system:String>
<system:String x:Key="updateNoResultTitle">لا توجد تحديثات متاح</system:String>
<system:String x:Key="updateNoResultSubtitle">جميع الإضافات محدث</system:String>
<system:String x:Key="updateAllPluginsTitle">Plugin updates available</system:String>
<system:String x:Key="updateAllPluginsButtonContent">Update plugins</system:String>
<system:String x:Key="checkPluginUpdatesTooltip">Check plugin updates</system:String>
<system:String x:Key="PluginsUpdateSuccessNoRestart">Plugins are successfully updated. Please restart Flow.</system:String>
<!-- Setting Theme -->
<system:String x:Key="theme">السمة</system:String>
@ -254,6 +333,7 @@
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<system:String x:Key="MaxShowResultsCannotWorkWithAlwaysPreview">Since Always Preview is on, maximum results shown may not take effect because preview panel requires a certain minimum height</system:String>
<!-- Setting Hotkey -->
<system:String x:Key="hotkey">مفتاح الاختصار</system:String>
@ -316,6 +396,28 @@
<system:String x:Key="showBadges">Show Result Badges</system:String>
<system:String x:Key="showBadgesToolTip">For supported plugins, badges are displayed to help distinguish them more easily.</system:String>
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
<system:String x:Key="showBadgesGlobalOnlyToolTip">Show badges for global query results only</system:String>
<system:String x:Key="dialogJumpHotkey">Dialog Jump</system:String>
<system:String x:Key="dialogJumpHotkeyToolTip">Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager.</system:String>
<system:String x:Key="dialogJump">Dialog Jump</system:String>
<system:String x:Key="dialogJumpToolTip">When Open/Save As dialog window opens, quickly navigate to the current path of the file manager.</system:String>
<system:String x:Key="autoDialogJump">Dialog Jump Automatically</system:String>
<system:String x:Key="autoDialogJumpToolTip">When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental)</system:String>
<system:String x:Key="showDialogJumpWindow">Show Dialog Jump Window</system:String>
<system:String x:Key="showDialogJumpWindowToolTip">Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations.</system:String>
<system:String x:Key="dialogJumpWindowPosition">Dialog Jump Window Position</system:String>
<system:String x:Key="dialogJumpWindowPositionToolTip">Select position for the Dialog Jump search window</system:String>
<system:String x:Key="DialogJumpWindowPositionUnderDialog">Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed</system:String>
<system:String x:Key="DialogJumpWindowPositionFollowDefault">Default search window position. Displayed when triggered by search window hotkey</system:String>
<system:String x:Key="dialogJumpResultBehaviour">Dialog Jump Result Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window to the selected result path</system:String>
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Left click or Enter key</system:String>
<system:String x:Key="DialogJumpResultBehaviourRightClick">Right click</system:String>
<system:String x:Key="dialogJumpFileResultBehaviour">Dialog Jump File Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpFileResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window when the result is a file path</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Fill full path in file name box</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Fill full path in file name box and open</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Fill directory in path box</system:String>
<!-- Setting Proxy -->
<system:String x:Key="proxy">بروكسي HTTP</system:String>
@ -366,15 +468,24 @@
<system:String x:Key="userdatapathButton">فتح المجلد</system:String>
<system:String x:Key="advanced">Advanced</system:String>
<system:String x:Key="logLevel">Log Level</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="LogLevelNONE">Silent</system:String>
<system:String x:Key="LogLevelERROR">خطأ</system:String>
<system:String x:Key="LogLevelINFO">Info</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="settingWindowFontTitle">Setting Window Font</system:String>
<!-- Release Notes Window -->
<system:String x:Key="seeMoreReleaseNotes">See more release notes on GitHub</system:String>
<system:String x:Key="checkNetworkConnectionTitle">Failed to fetch release notes</system:String>
<system:String x:Key="checkNetworkConnectionSubTitle">Please check your network connection or ensure GitHub is accessible</system:String>
<system:String x:Key="appUpdateTitle">Flow Launcher has been updated to {0}</system:String>
<system:String x:Key="appUpdateButtonContent">Click here to view the release notes</system:String>
<!-- FileManager Setting Dialog -->
<system:String x:Key="fileManagerWindow">اختر مدير الملفات</system:String>
<system:String x:Key="fileManager_learnMore">Learn more</system:String>
<system:String x:Key="fileManager_tips">يرجى تحديد موقع ملف مدير الملفات الذي تستخدمه وإضافة الحجج حسب الحاجة. يمثل &quot;%d&quot; مسار الدليل المفتوح، ويستخدمه الحقل &quot;الحجة للمجلد&quot; للأوامر التي تفتح أدلة محددة. يمثل &quot;%f&quot; مسار الملف المفتوح، ويستخدمه الحقل &quot;الحجة للملف&quot; للأوامر التي تفتح ملفات محددة.</system:String>
<system:String x:Key="fileManager_tips2">على سبيل المثال، إذا كان مدير الملفات يستخدم أمرًا مثل &quot;totalcmd.exe /A c:\windows&quot; لفتح دليل c:\windows، فإن مسار مدير الملفات سيكون totalcmd.exe، وحجة المجلد ستكون /A &quot;%d&quot;. قد تحتاج بعض مديري الملفات مثل QTTabBar فقط إلى توفير مسار، في هذه الحالة استخدم &quot;%d&quot; كمسار مدير الملفات واترك باقي الحقول فارغة.</system:String>
<system:String x:Key="fileManager_tips2">For example, if the file manager uses a command such as &quot;totalcmd.exe /A c:\windows&quot; to open the c:\windows directory, the File Manager Path will be totalcmd.exe, and the Arg For Folder will be /A &quot;%d&quot;. Certain file managers like QTTabBar may just require a path to be supplied, in this instance use &quot;%d&quot; as the File Manager Path and leave the rest of the fields blank.</system:String>
<system:String x:Key="fileManager_name">مدير الملفات</system:String>
<system:String x:Key="fileManager_profile_name">اسم الملف الشخصي</system:String>
<system:String x:Key="fileManager_path">مسار مدير الملفات</system:String>
@ -382,6 +493,7 @@
<system:String x:Key="fileManager_file_arg">حجة للملف</system:String>
<system:String x:Key="fileManagerPathNotFound">The file manager '{0}' could not be located at '{1}'. Would you like to continue?</system:String>
<system:String x:Key="fileManagerPathError">File Manager Path Error</system:String>
<system:String x:Key="fileManagerExplorer">File Explorer</system:String>
<!-- DefaultBrowser Setting Dialog -->
<system:String x:Key="defaultBrowserTitle">متصفح الويب الافتراضي</system:String>
@ -392,6 +504,8 @@
<system:String x:Key="defaultBrowser_newWindow">نافذة جديدة</system:String>
<system:String x:Key="defaultBrowser_newTab">تبويب جديد</system:String>
<system:String x:Key="defaultBrowser_parameter">الوضع الخاص</system:String>
<system:String x:Key="defaultBrowser_default">Default</system:String>
<system:String x:Key="defaultBrowser_new_profile">New Profile</system:String>
<!-- Priority Setting Dialog -->
<system:String x:Key="changePriorityWindow">تغيير الأولوية</system:String>
@ -425,13 +539,14 @@
<system:String x:Key="customeQueryHotkeyTips">اضغط على مفتاح اختصار مخصص لفتح Flow Launcher وإدخال الاستعلام المحدد تلقائيًا.</system:String>
<system:String x:Key="preview">معاينة</system:String>
<system:String x:Key="hotkeyIsNotUnavailable">مفتاح الاختصار غير متاح، يرجى اختيار مفتاح اختصار جديد</system:String>
<system:String x:Key="invalidPluginHotkey">مفتاح اختصار غير صالح للإضافة</system:String>
<system:String x:Key="invalidPluginHotkey">Hotkey is invalid</system:String>
<system:String x:Key="update">تحديث</system:String>
<system:String x:Key="hotkeyRegTitle">ربط مفتاح الاختصار</system:String>
<system:String x:Key="hotkeyUnavailable">مفتاح الاختصار الحالي غير متاح.</system:String>
<system:String x:Key="hotkeyUnavailableUneditable">تم حجز هذا المفتاح لـ &quot;{0}&quot; ولا يمكن استخدامه. يرجى اختيار مفتاح اختصار آخر.</system:String>
<system:String x:Key="hotkeyUnavailableEditable">يتم استخدام هذا المفتاح بالفعل من قبل &quot;{0}&quot;. إذا ضغطت على &quot;استبدال&quot;، سيتم إزالته من &quot;{0}&quot;.</system:String>
<system:String x:Key="hotkeyRegGuide">اضغط على المفاتيح التي تريد استخدامها لهذه الوظيفة.</system:String>
<system:String x:Key="emptyPluginHotkey">Hotkey and action keyword are empty</system:String>
<!-- Custom Query Shortcut Dialog -->
<system:String x:Key="customeQueryShortcutTitle">اختصار الاستعلام المخصص</system:String>
@ -442,6 +557,7 @@
</system:String>
<system:String x:Key="duplicateShortcut">الاختصار موجود بالفعل، يرجى إدخال اختصار جديد أو تعديل الموجود.</system:String>
<system:String x:Key="emptyShortcut">الاختصار و/أو توسيعه فارغ.</system:String>
<system:String x:Key="invalidShortcut">Shortcut is invalid</system:String>
<!-- Common Action -->
<system:String x:Key="commonSave">حفظ</system:String>
@ -480,6 +596,7 @@
</system:String>
<system:String x:Key="errorTitle">خطأ</system:String>
<system:String x:Key="folderOpenError">An error occurred while opening the folder. {0}</system:String>
<system:String x:Key="browserOpenError">An error occurred while opening the URL in the browser. Please check your Default Web Browser configuration in the General section of the settings window</system:String>
<!-- General Notice -->
<system:String x:Key="pleaseWait">يرجى الانتظار...</system:String>
@ -505,6 +622,11 @@
<system:String x:Key="update_flowlauncher_update_files">تحديث الملفات</system:String>
<system:String x:Key="update_flowlauncher_update_update_description">وصف التحديث</system:String>
<!-- Plugin Update Window -->
<system:String x:Key="restartAfterUpdating">Restart Flow Launcher after updating plugins</system:String>
<system:String x:Key="updatePluginCheckboxContent">{0}: Update from v{1} to v{2}</system:String>
<system:String x:Key="updatePluginNoSelected">No plugin selected</system:String>
<!-- Welcome Window -->
<system:String x:Key="Skip">تخطي</system:String>
<system:String x:Key="Welcome_Page1_Title">مرحبًا بك في Flow Launcher</system:String>

View file

@ -10,12 +10,32 @@
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
Click yes if you would like select the {0} executable again. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String>
<!-- Portable -->
<system:String x:Key="restartToDisablePortableMode">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</system:String>
<system:String x:Key="restartToEnablePortableMode">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</system:String>
<system:String x:Key="moveToDifferentLocation">Flow Launcher has detected you enabled portable mode, would you like to move it to a different location?</system:String>
<system:String x:Key="shortcutsUninstallerCreated">Flow Launcher has detected you disabled portable mode, the relevant shortcuts and uninstaller entry have been created</system:String>
<system:String x:Key="userDataDuplicated">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.</system:String>
<!-- Plugin Loader -->
<system:String x:Key="pluginHasErrored">The following plugin has errored and cannot be loaded:</system:String>
<system:String x:Key="pluginsHaveErrored">The following plugins have errored and cannot be loaded:</system:String>
<system:String x:Key="referToLogs">Please refer to the logs for more information</system:String>
<!-- Http -->
<system:String x:Key="pleaseTryAgain">Please try again</system:String>
<system:String x:Key="parseProxyFailed">Unable to parse Http Proxy</system:String>
<!-- AbstractPluginEnvironment -->
<system:String x:Key="failToInstallTypeScriptEnv">Failed to install TypeScript environment. Please try again later</system:String>
<system:String x:Key="failToInstallPythonEnv">Failed to install Python environment. Please try again later.</system:String>
<!-- MainWindow -->
<system:String x:Key="registerHotkeyFailed">Nepodařilo se zaregistrovat hotkey &quot;{0}&quot;. Klávesová zkratka může být používána jiným programem. Změňte na jinou klávesu nebo ukončíte jiný program.</system:String>
<system:String x:Key="unregisterHotkeyFailed">Failed to unregister hotkey &quot;{0}&quot;. Please try again or see log for details</system:String>
@ -91,6 +111,7 @@
<system:String x:Key="typingStartEn">Vždy spouštět psaní v anglickém rozvržení klávesnice</system:String>
<system:String x:Key="typingStartEnTooltip">Dočasně změní metodu vstupu do angličtiny při zobrazení Flow Launcher.</system:String>
<system:String x:Key="autoUpdates">Automatické aktualizace</system:String>
<system:String x:Key="autoUpdatesTooltip">Automatically check and update the app when available</system:String>
<system:String x:Key="select">Vybrat</system:String>
<system:String x:Key="hideOnStartup">Skrýt Flow Launcher při spuštění</system:String>
<system:String x:Key="hideOnStartupToolTip">Flow Launcher search window is hidden in the tray after starting up.</system:String>
@ -102,7 +123,20 @@
<system:String x:Key="SearchPrecisionLow">Low</system:String>
<system:String x:Key="SearchPrecisionRegular">Regular</system:String>
<system:String x:Key="ShouldUsePinyin">Vyhledávání pomocí pchin-jin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Umožňuje vyhledávání pomocí pchin-jin. Pchin-jin je systém zápisu čínského jazyka pomocí písmen latinky.</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Pinyin is the standard system of romanized spelling for translating Chinese. Please note, enabling this can significantly increase memory usage during search.</system:String>
<system:String x:Key="ShouldUseDoublePinyin">Use Double Pinyin</system:String>
<system:String x:Key="ShouldUseDoublePinyinToolTip">Use Double Pinyin instead of Full Pinyin to search.</system:String>
<system:String x:Key="DoublePinyinSchema">Double Pinyin Schema</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoHe">Xiao He</system:String>
<system:String x:Key="DoublePinyinSchemasZiRanMa">Zi Ran Ma</system:String>
<system:String x:Key="DoublePinyinSchemasWeiRuan">Wei Ruan</system:String>
<system:String x:Key="DoublePinyinSchemasZhiNengABC">Zhi Neng ABC</system:String>
<system:String x:Key="DoublePinyinSchemasZiGuangPinYin">Zi Guang Pin Yin</system:String>
<system:String x:Key="DoublePinyinSchemasPinYinJiaJia">Pin Yin Jia Jia</system:String>
<system:String x:Key="DoublePinyinSchemasXingKongJianDao">Xing Kong Jian Dao</system:String>
<system:String x:Key="DoublePinyinSchemasDaNiu">Da Niu</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoLang">Xiao Lang</system:String>
<system:String x:Key="AlwaysPreview">Vždy zobrazit náhled</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Při aktivaci služby Flow vždy otevřete panel náhledu. Stisknutím klávesy {0} přepnete náhled.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Stínový efekt není povolen, pokud je aktivní efekt rozostření</system:String>
@ -131,11 +165,21 @@
<system:String x:Key="KoreanImeOpenLinkButton">Otevřít</system:String>
<system:String x:Key="KoreanImeRegistry">Use Previous Korean IME</system:String>
<system:String x:Key="KoreanImeRegistryTooltip">You can change the Previous Korean IME settings directly from here</system:String>
<system:String x:Key="KoreanImeSettingChangeFailTitle">Failed to change Korean IME setting</system:String>
<system:String x:Key="KoreanImeSettingChangeFailSubTitle">Please check your system registry access or contact support.</system:String>
<system:String x:Key="homePage">Home Page</system:String>
<system:String x:Key="homePageToolTip">Show home page results when query text is empty.</system:String>
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
<system:String x:Key="showAtTopmost">Show Search Window at Foremost</system:String>
<system:String x:Key="showAtTopmostToolTip">Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.</system:String>
<system:String x:Key="autoRestartAfterChanging">Restart after modifying plugin via Plugin Store</system:String>
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
<system:String x:Key="showUnknownSourceWarning">Show unknown source warning</system:String>
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
<system:String x:Key="autoUpdatePlugins">Auto update plugins</system:String>
<system:String x:Key="autoUpdatePluginsToolTip">Automatically check plugin updates and notify if there are any updates available</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Vyhledat plugin</system:String>
@ -174,6 +218,13 @@
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="pluginModifiedAlreadyTitle">{0} modified already</system:String>
<system:String x:Key="pluginModifiedAlreadyMessage">Please restart Flow before making any further changes</system:String>
<system:String x:Key="failedToInstallPluginTitle">Fail to install {0}</system:String>
<system:String x:Key="failedToUninstallPluginTitle">Fail to uninstall {0}</system:String>
<system:String x:Key="fileNotFoundMessage">Unable to find plugin.json from the extracted zip file, or this path {0} does not exist</system:String>
<system:String x:Key="pluginExistAlreadyMessage">A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin</system:String>
<system:String x:Key="errorCreatingSettingPanel">Error creating setting panel for plugin {0}:{1}{2}</system:String>
<!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Obchod s pluginy</system:String>
@ -189,6 +240,34 @@
<system:String x:Key="LabelNew">Nová verze</system:String>
<system:String x:Key="LabelNewToolTip">Tento plugin byl aktualizován během posledních 7 dní</system:String>
<system:String x:Key="LabelUpdateToolTip">Nová aktualizace je k dispozici</system:String>
<system:String x:Key="ErrorInstallingPlugin">Chyba instalace pluginu</system:String>
<system:String x:Key="ErrorUninstallingPlugin">Error uninstalling plugin</system:String>
<system:String x:Key="ErrorUpdatingPlugin">Error updating plugin</system:String>
<system:String x:Key="KeepPluginSettingsTitle">Keep plugin settings</system:String>
<system:String x:Key="KeepPluginSettingsSubtitle">Do you want to keep the settings of the plugin for the next usage?</system:String>
<system:String x:Key="InstallSuccessNoRestart">Plugin {0} successfully installed. Please restart Flow.</system:String>
<system:String x:Key="UninstallSuccessNoRestart">Plugin {0} successfully uninstalled. Please restart Flow.</system:String>
<system:String x:Key="UpdateSuccessNoRestart">Plugin {0} successfully updated. Please restart Flow.</system:String>
<system:String x:Key="InstallPromptTitle">Plugin install</system:String>
<system:String x:Key="InstallPromptSubtitle">{0} by {1} {2}{2}Would you like to install this plugin?</system:String>
<system:String x:Key="UninstallPromptTitle">Plugin uninstall</system:String>
<system:String x:Key="UninstallPromptSubtitle">{0} by {1} {2}{2}Would you like to uninstall this plugin?</system:String>
<system:String x:Key="UpdatePromptTitle">Plugin update</system:String>
<system:String x:Key="UpdatePromptSubtitle">{0} by {1} {2}{2}Would you like to update this plugin?</system:String>
<system:String x:Key="DownloadingPlugin">Stahování pluginu</system:String>
<system:String x:Key="AutoRestartAfterChange">Automatically restart after installing/uninstalling/updating plugins in plugin store</system:String>
<system:String x:Key="ZipFileNotHavePluginJson">Zip file does not have a valid plugin.json configuration</system:String>
<system:String x:Key="InstallFromUnknownSourceTitle">Instalace z neznámého zdroje</system:String>
<system:String x:Key="InstallFromUnknownSourceSubtitle">This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window)</system:String>
<system:String x:Key="ZipFiles">Zip files</system:String>
<system:String x:Key="SelectZipFile">Please select zip file</system:String>
<system:String x:Key="installLocalPluginTooltip">Install plugin from local path</system:String>
<system:String x:Key="updateNoResultTitle">Nejsou dostupné žádné aktualizace</system:String>
<system:String x:Key="updateNoResultSubtitle">Všechny pluginy jsou aktuální</system:String>
<system:String x:Key="updateAllPluginsTitle">Plugin updates available</system:String>
<system:String x:Key="updateAllPluginsButtonContent">Update plugins</system:String>
<system:String x:Key="checkPluginUpdatesTooltip">Check plugin updates</system:String>
<system:String x:Key="PluginsUpdateSuccessNoRestart">Plugins are successfully updated. Please restart Flow.</system:String>
<!-- Setting Theme -->
<system:String x:Key="theme">Motiv</system:String>
@ -254,6 +333,7 @@
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<system:String x:Key="MaxShowResultsCannotWorkWithAlwaysPreview">Since Always Preview is on, maximum results shown may not take effect because preview panel requires a certain minimum height</system:String>
<!-- Setting Hotkey -->
<system:String x:Key="hotkey">Klávesová zkratka</system:String>
@ -316,6 +396,28 @@
<system:String x:Key="showBadges">Show Result Badges</system:String>
<system:String x:Key="showBadgesToolTip">For supported plugins, badges are displayed to help distinguish them more easily.</system:String>
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
<system:String x:Key="showBadgesGlobalOnlyToolTip">Show badges for global query results only</system:String>
<system:String x:Key="dialogJumpHotkey">Dialog Jump</system:String>
<system:String x:Key="dialogJumpHotkeyToolTip">Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager.</system:String>
<system:String x:Key="dialogJump">Dialog Jump</system:String>
<system:String x:Key="dialogJumpToolTip">When Open/Save As dialog window opens, quickly navigate to the current path of the file manager.</system:String>
<system:String x:Key="autoDialogJump">Dialog Jump Automatically</system:String>
<system:String x:Key="autoDialogJumpToolTip">When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental)</system:String>
<system:String x:Key="showDialogJumpWindow">Show Dialog Jump Window</system:String>
<system:String x:Key="showDialogJumpWindowToolTip">Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations.</system:String>
<system:String x:Key="dialogJumpWindowPosition">Dialog Jump Window Position</system:String>
<system:String x:Key="dialogJumpWindowPositionToolTip">Select position for the Dialog Jump search window</system:String>
<system:String x:Key="DialogJumpWindowPositionUnderDialog">Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed</system:String>
<system:String x:Key="DialogJumpWindowPositionFollowDefault">Default search window position. Displayed when triggered by search window hotkey</system:String>
<system:String x:Key="dialogJumpResultBehaviour">Dialog Jump Result Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window to the selected result path</system:String>
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Left click or Enter key</system:String>
<system:String x:Key="DialogJumpResultBehaviourRightClick">Right click</system:String>
<system:String x:Key="dialogJumpFileResultBehaviour">Dialog Jump File Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpFileResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window when the result is a file path</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Fill full path in file name box</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Fill full path in file name box and open</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Fill directory in path box</system:String>
<!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP Proxy</system:String>
@ -366,15 +468,24 @@
<system:String x:Key="userdatapathButton">Open Folder</system:String>
<system:String x:Key="advanced">Advanced</system:String>
<system:String x:Key="logLevel">Log Level</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="LogLevelNONE">Silent</system:String>
<system:String x:Key="LogLevelERROR">Chyba</system:String>
<system:String x:Key="LogLevelINFO">Info</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="settingWindowFontTitle">Setting Window Font</system:String>
<!-- Release Notes Window -->
<system:String x:Key="seeMoreReleaseNotes">See more release notes on GitHub</system:String>
<system:String x:Key="checkNetworkConnectionTitle">Failed to fetch release notes</system:String>
<system:String x:Key="checkNetworkConnectionSubTitle">Please check your network connection or ensure GitHub is accessible</system:String>
<system:String x:Key="appUpdateTitle">Flow Launcher has been updated to {0}</system:String>
<system:String x:Key="appUpdateButtonContent">Click here to view the release notes</system:String>
<!-- FileManager Setting Dialog -->
<system:String x:Key="fileManagerWindow">Vybrat správce souborů</system:String>
<system:String x:Key="fileManager_learnMore">Learn more</system:String>
<system:String x:Key="fileManager_tips">Please specify the file location of the file manager you using and add arguments as required. The &quot;%d&quot; represents the directory path to open for, used by the Arg for Folder field and for commands opening specific directories. The &quot;%f&quot; represents the file path to open for, used by the Arg for File field and for commands opening specific files.</system:String>
<system:String x:Key="fileManager_tips2">For example, if the file manager uses a command such as &quot;totalcmd.exe /A c:\windows&quot; to open the c:\windows directory, the File Manager Path will be totalcmd.exe, and the Arg For Folder will be /A &quot;%d&quot;. Certain file managers like QTTabBar may just require a path to be supplied, in this instance use &quot;%d&quot; as the File Manager Path and leave the rest of the fileds blank.</system:String>
<system:String x:Key="fileManager_tips2">For example, if the file manager uses a command such as &quot;totalcmd.exe /A c:\windows&quot; to open the c:\windows directory, the File Manager Path will be totalcmd.exe, and the Arg For Folder will be /A &quot;%d&quot;. Certain file managers like QTTabBar may just require a path to be supplied, in this instance use &quot;%d&quot; as the File Manager Path and leave the rest of the fields blank.</system:String>
<system:String x:Key="fileManager_name">Správce souborů</system:String>
<system:String x:Key="fileManager_profile_name">Jméno profilu</system:String>
<system:String x:Key="fileManager_path">Cesta k správci souborů</system:String>
@ -382,6 +493,7 @@
<system:String x:Key="fileManager_file_arg">Argumenty pro Soubor</system:String>
<system:String x:Key="fileManagerPathNotFound">The file manager '{0}' could not be located at '{1}'. Would you like to continue?</system:String>
<system:String x:Key="fileManagerPathError">File Manager Path Error</system:String>
<system:String x:Key="fileManagerExplorer">File Explorer</system:String>
<!-- DefaultBrowser Setting Dialog -->
<system:String x:Key="defaultBrowserTitle">Výchozí prohlížeč</system:String>
@ -392,6 +504,8 @@
<system:String x:Key="defaultBrowser_newWindow">Nové okno</system:String>
<system:String x:Key="defaultBrowser_newTab">Nová karta</system:String>
<system:String x:Key="defaultBrowser_parameter">Soukromý režim</system:String>
<system:String x:Key="defaultBrowser_default">Default</system:String>
<system:String x:Key="defaultBrowser_new_profile">New Profile</system:String>
<!-- Priority Setting Dialog -->
<system:String x:Key="changePriorityWindow">Změnit prioritu</system:String>
@ -425,13 +539,14 @@
<system:String x:Key="customeQueryHotkeyTips">Stisknutím vlastní klávesové zkratky otevřete nástroj Flow Launcher a automaticky zadejte dotaz.</system:String>
<system:String x:Key="preview">Náhled</system:String>
<system:String x:Key="hotkeyIsNotUnavailable">Klávesová zkratka je nedostupná, zadejte prosím novou zkratku</system:String>
<system:String x:Key="invalidPluginHotkey">Neplatná klávesová zkratka pluginu</system:String>
<system:String x:Key="invalidPluginHotkey">Hotkey is invalid</system:String>
<system:String x:Key="update">Aktualizovat</system:String>
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for &quot;{0}&quot; and can't be used. Please choose another hotkey.</system:String>
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by &quot;{0}&quot;. If you press &quot;Overwrite&quot;, it will be removed from &quot;{0}&quot;.</system:String>
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
<system:String x:Key="emptyPluginHotkey">Hotkey and action keyword are empty</system:String>
<!-- Custom Query Shortcut Dialog -->
<system:String x:Key="customeQueryShortcutTitle">Vlastní klávesová zkratka pro zadávání dotazů</system:String>
@ -442,6 +557,7 @@ Pokud před zkratku při zadávání přidáte znak &quot;@&quot;, bude odpovíd
</system:String>
<system:String x:Key="duplicateShortcut">Zkratka již existuje, zadejte novou zkratku nebo upravte stávající.</system:String>
<system:String x:Key="emptyShortcut">Zkratka a/nebo její plné znění je prázdné.</system:String>
<system:String x:Key="invalidShortcut">Shortcut is invalid</system:String>
<!-- Common Action -->
<system:String x:Key="commonSave">Uložit</system:String>
@ -480,6 +596,7 @@ Pokud před zkratku při zadávání přidáte znak &quot;@&quot;, bude odpovíd
</system:String>
<system:String x:Key="errorTitle">Chyba</system:String>
<system:String x:Key="folderOpenError">An error occurred while opening the folder. {0}</system:String>
<system:String x:Key="browserOpenError">An error occurred while opening the URL in the browser. Please check your Default Web Browser configuration in the General section of the settings window</system:String>
<!-- General Notice -->
<system:String x:Key="pleaseWait">Počkejte prosím...</system:String>
@ -505,6 +622,11 @@ Pokud před zkratku při zadávání přidáte znak &quot;@&quot;, bude odpovíd
<system:String x:Key="update_flowlauncher_update_files">Aktualizovat soubory</system:String>
<system:String x:Key="update_flowlauncher_update_update_description">Aktualizovat popis</system:String>
<!-- Plugin Update Window -->
<system:String x:Key="restartAfterUpdating">Restart Flow Launcher after updating plugins</system:String>
<system:String x:Key="updatePluginCheckboxContent">{0}: Update from v{1} to v{2}</system:String>
<system:String x:Key="updatePluginNoSelected">No plugin selected</system:String>
<!-- Welcome Window -->
<system:String x:Key="Skip">Přeskočit</system:String>
<system:String x:Key="Welcome_Page1_Title">Vítejte v Flow Launcheru</system:String>

View file

@ -10,12 +10,32 @@
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
Click yes if you would like select the {0} executable again. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String>
<!-- Portable -->
<system:String x:Key="restartToDisablePortableMode">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</system:String>
<system:String x:Key="restartToEnablePortableMode">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</system:String>
<system:String x:Key="moveToDifferentLocation">Flow Launcher has detected you enabled portable mode, would you like to move it to a different location?</system:String>
<system:String x:Key="shortcutsUninstallerCreated">Flow Launcher has detected you disabled portable mode, the relevant shortcuts and uninstaller entry have been created</system:String>
<system:String x:Key="userDataDuplicated">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.</system:String>
<!-- Plugin Loader -->
<system:String x:Key="pluginHasErrored">The following plugin has errored and cannot be loaded:</system:String>
<system:String x:Key="pluginsHaveErrored">The following plugins have errored and cannot be loaded:</system:String>
<system:String x:Key="referToLogs">Please refer to the logs for more information</system:String>
<!-- Http -->
<system:String x:Key="pleaseTryAgain">Please try again</system:String>
<system:String x:Key="parseProxyFailed">Unable to parse Http Proxy</system:String>
<!-- AbstractPluginEnvironment -->
<system:String x:Key="failToInstallTypeScriptEnv">Failed to install TypeScript environment. Please try again later</system:String>
<system:String x:Key="failToInstallPythonEnv">Failed to install Python environment. Please try again later.</system:String>
<!-- MainWindow -->
<system:String x:Key="registerHotkeyFailed">Failed to register hotkey &quot;{0}&quot;. The hotkey may be in use by another program. Change to a different hotkey, or exit another program.</system:String>
<system:String x:Key="unregisterHotkeyFailed">Failed to unregister hotkey &quot;{0}&quot;. Please try again or see log for details</system:String>
@ -91,10 +111,11 @@
<system:String x:Key="typingStartEn">Always Start Typing in English Mode</system:String>
<system:String x:Key="typingStartEnTooltip">Temporarily change your input method to English mode when activating Flow.</system:String>
<system:String x:Key="autoUpdates">Autoopdatering</system:String>
<system:String x:Key="autoUpdatesTooltip">Automatically check and update the app when available</system:String>
<system:String x:Key="select">Vælg</system:String>
<system:String x:Key="hideOnStartup">Skjul Flow Launcher ved opstart</system:String>
<system:String x:Key="hideOnStartupToolTip">Flow Launcher search window is hidden in the tray after starting up.</system:String>
<system:String x:Key="hideNotifyIcon">Hide tray icon</system:String>
<system:String x:Key="hideNotifyIcon"></system:String>
<system:String x:Key="hideNotifyIconToolTip">When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window.</system:String>
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String>
<system:String x:Key="querySearchPrecisionToolTip">Changes minimum match score required for results.</system:String>
@ -102,7 +123,20 @@
<system:String x:Key="SearchPrecisionLow">Low</system:String>
<system:String x:Key="SearchPrecisionRegular">Regular</system:String>
<system:String x:Key="ShouldUsePinyin">Search with Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese.</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Pinyin is the standard system of romanized spelling for translating Chinese. Please note, enabling this can significantly increase memory usage during search.</system:String>
<system:String x:Key="ShouldUseDoublePinyin">Use Double Pinyin</system:String>
<system:String x:Key="ShouldUseDoublePinyinToolTip">Use Double Pinyin instead of Full Pinyin to search.</system:String>
<system:String x:Key="DoublePinyinSchema">Double Pinyin Schema</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoHe">Xiao He</system:String>
<system:String x:Key="DoublePinyinSchemasZiRanMa">Zi Ran Ma</system:String>
<system:String x:Key="DoublePinyinSchemasWeiRuan">Wei Ruan</system:String>
<system:String x:Key="DoublePinyinSchemasZhiNengABC">Zhi Neng ABC</system:String>
<system:String x:Key="DoublePinyinSchemasZiGuangPinYin">Zi Guang Pin Yin</system:String>
<system:String x:Key="DoublePinyinSchemasPinYinJiaJia">Pin Yin Jia Jia</system:String>
<system:String x:Key="DoublePinyinSchemasXingKongJianDao">Xing Kong Jian Dao</system:String>
<system:String x:Key="DoublePinyinSchemasDaNiu">Da Niu</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoLang">Xiao Lang</system:String>
<system:String x:Key="AlwaysPreview">Always Preview</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Always open preview panel when Flow activates. Press {0} to toggle preview.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Skyggeeffekt er ikke tilladt, når det aktuelle tema har sløringseffekt aktiveret</system:String>
@ -131,11 +165,21 @@
<system:String x:Key="KoreanImeOpenLinkButton">Åben</system:String>
<system:String x:Key="KoreanImeRegistry">Use Previous Korean IME</system:String>
<system:String x:Key="KoreanImeRegistryTooltip">You can change the Previous Korean IME settings directly from here</system:String>
<system:String x:Key="KoreanImeSettingChangeFailTitle">Failed to change Korean IME setting</system:String>
<system:String x:Key="KoreanImeSettingChangeFailSubTitle">Please check your system registry access or contact support.</system:String>
<system:String x:Key="homePage">Home Page</system:String>
<system:String x:Key="homePageToolTip">Show home page results when query text is empty.</system:String>
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
<system:String x:Key="showAtTopmost">Show Search Window at Foremost</system:String>
<system:String x:Key="showAtTopmostToolTip">Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.</system:String>
<system:String x:Key="autoRestartAfterChanging">Restart after modifying plugin via Plugin Store</system:String>
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
<system:String x:Key="showUnknownSourceWarning">Show unknown source warning</system:String>
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
<system:String x:Key="autoUpdatePlugins">Auto update plugins</system:String>
<system:String x:Key="autoUpdatePluginsToolTip">Automatically check plugin updates and notify if there are any updates available</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String>
@ -174,6 +218,13 @@
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="pluginModifiedAlreadyTitle">{0} modified already</system:String>
<system:String x:Key="pluginModifiedAlreadyMessage">Please restart Flow before making any further changes</system:String>
<system:String x:Key="failedToInstallPluginTitle">Fail to install {0}</system:String>
<system:String x:Key="failedToUninstallPluginTitle">Fail to uninstall {0}</system:String>
<system:String x:Key="fileNotFoundMessage">Unable to find plugin.json from the extracted zip file, or this path {0} does not exist</system:String>
<system:String x:Key="pluginExistAlreadyMessage">A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin</system:String>
<system:String x:Key="errorCreatingSettingPanel">Error creating setting panel for plugin {0}:{1}{2}</system:String>
<!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plugin-butik</system:String>
@ -189,6 +240,34 @@
<system:String x:Key="LabelNew">New Version</system:String>
<system:String x:Key="LabelNewToolTip">This plugin has been updated within the last 7 days</system:String>
<system:String x:Key="LabelUpdateToolTip">New Update is Available</system:String>
<system:String x:Key="ErrorInstallingPlugin">Error installing plugin</system:String>
<system:String x:Key="ErrorUninstallingPlugin">Error uninstalling plugin</system:String>
<system:String x:Key="ErrorUpdatingPlugin">Error updating plugin</system:String>
<system:String x:Key="KeepPluginSettingsTitle">Keep plugin settings</system:String>
<system:String x:Key="KeepPluginSettingsSubtitle">Do you want to keep the settings of the plugin for the next usage?</system:String>
<system:String x:Key="InstallSuccessNoRestart">Plugin {0} successfully installed. Please restart Flow.</system:String>
<system:String x:Key="UninstallSuccessNoRestart">Plugin {0} successfully uninstalled. Please restart Flow.</system:String>
<system:String x:Key="UpdateSuccessNoRestart">Plugin {0} successfully updated. Please restart Flow.</system:String>
<system:String x:Key="InstallPromptTitle">Plugin install</system:String>
<system:String x:Key="InstallPromptSubtitle">{0} by {1} {2}{2}Would you like to install this plugin?</system:String>
<system:String x:Key="UninstallPromptTitle">Plugin uninstall</system:String>
<system:String x:Key="UninstallPromptSubtitle">{0} by {1} {2}{2}Would you like to uninstall this plugin?</system:String>
<system:String x:Key="UpdatePromptTitle">Plugin update</system:String>
<system:String x:Key="UpdatePromptSubtitle">{0} by {1} {2}{2}Would you like to update this plugin?</system:String>
<system:String x:Key="DownloadingPlugin">Downloading plugin</system:String>
<system:String x:Key="AutoRestartAfterChange">Automatically restart after installing/uninstalling/updating plugins in plugin store</system:String>
<system:String x:Key="ZipFileNotHavePluginJson">Zip file does not have a valid plugin.json configuration</system:String>
<system:String x:Key="InstallFromUnknownSourceTitle">Installing from an unknown source</system:String>
<system:String x:Key="InstallFromUnknownSourceSubtitle">This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window)</system:String>
<system:String x:Key="ZipFiles">Zip files</system:String>
<system:String x:Key="SelectZipFile">Please select zip file</system:String>
<system:String x:Key="installLocalPluginTooltip">Install plugin from local path</system:String>
<system:String x:Key="updateNoResultTitle">No update available</system:String>
<system:String x:Key="updateNoResultSubtitle">All plugins are up to date</system:String>
<system:String x:Key="updateAllPluginsTitle">Plugin updates available</system:String>
<system:String x:Key="updateAllPluginsButtonContent">Update plugins</system:String>
<system:String x:Key="checkPluginUpdatesTooltip">Check plugin updates</system:String>
<system:String x:Key="PluginsUpdateSuccessNoRestart">Plugins are successfully updated. Please restart Flow.</system:String>
<!-- Setting Theme -->
<system:String x:Key="theme">Tema</system:String>
@ -254,6 +333,7 @@
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="KeepMaxResults">Fixed Window Size</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<system:String x:Key="MaxShowResultsCannotWorkWithAlwaysPreview">Since Always Preview is on, maximum results shown may not take effect because preview panel requires a certain minimum height</system:String>
<!-- Setting Hotkey -->
<system:String x:Key="hotkey">Genvejstast</system:String>
@ -316,6 +396,28 @@
<system:String x:Key="showBadges">Show Result Badges</system:String>
<system:String x:Key="showBadgesToolTip">For supported plugins, badges are displayed to help distinguish them more easily.</system:String>
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
<system:String x:Key="showBadgesGlobalOnlyToolTip">Show badges for global query results only</system:String>
<system:String x:Key="dialogJumpHotkey">Dialog Jump</system:String>
<system:String x:Key="dialogJumpHotkeyToolTip">Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager.</system:String>
<system:String x:Key="dialogJump">Dialog Jump</system:String>
<system:String x:Key="dialogJumpToolTip">When Open/Save As dialog window opens, quickly navigate to the current path of the file manager.</system:String>
<system:String x:Key="autoDialogJump">Dialog Jump Automatically</system:String>
<system:String x:Key="autoDialogJumpToolTip">When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental)</system:String>
<system:String x:Key="showDialogJumpWindow">Show Dialog Jump Window</system:String>
<system:String x:Key="showDialogJumpWindowToolTip">Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations.</system:String>
<system:String x:Key="dialogJumpWindowPosition">Dialog Jump Window Position</system:String>
<system:String x:Key="dialogJumpWindowPositionToolTip">Select position for the Dialog Jump search window</system:String>
<system:String x:Key="DialogJumpWindowPositionUnderDialog">Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed</system:String>
<system:String x:Key="DialogJumpWindowPositionFollowDefault">Default search window position. Displayed when triggered by search window hotkey</system:String>
<system:String x:Key="dialogJumpResultBehaviour">Dialog Jump Result Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window to the selected result path</system:String>
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Left click or Enter key</system:String>
<system:String x:Key="DialogJumpResultBehaviourRightClick">Right click</system:String>
<system:String x:Key="dialogJumpFileResultBehaviour">Dialog Jump File Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpFileResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window when the result is a file path</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Fill full path in file name box</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Fill full path in file name box and open</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Fill directory in path box</system:String>
<!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP Proxy</system:String>
@ -366,15 +468,24 @@
<system:String x:Key="userdatapathButton">Open Folder</system:String>
<system:String x:Key="advanced">Advanced</system:String>
<system:String x:Key="logLevel">Log Level</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="LogLevelNONE">Silent</system:String>
<system:String x:Key="LogLevelERROR">Error</system:String>
<system:String x:Key="LogLevelINFO">Info</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="settingWindowFontTitle">Setting Window Font</system:String>
<!-- Release Notes Window -->
<system:String x:Key="seeMoreReleaseNotes">See more release notes on GitHub</system:String>
<system:String x:Key="checkNetworkConnectionTitle">Failed to fetch release notes</system:String>
<system:String x:Key="checkNetworkConnectionSubTitle">Please check your network connection or ensure GitHub is accessible</system:String>
<system:String x:Key="appUpdateTitle">Flow Launcher has been updated to {0}</system:String>
<system:String x:Key="appUpdateButtonContent">Click here to view the release notes</system:String>
<!-- FileManager Setting Dialog -->
<system:String x:Key="fileManagerWindow">Select File Manager</system:String>
<system:String x:Key="fileManager_learnMore">Learn more</system:String>
<system:String x:Key="fileManager_tips">Please specify the file location of the file manager you using and add arguments as required. The &quot;%d&quot; represents the directory path to open for, used by the Arg for Folder field and for commands opening specific directories. The &quot;%f&quot; represents the file path to open for, used by the Arg for File field and for commands opening specific files.</system:String>
<system:String x:Key="fileManager_tips2">For example, if the file manager uses a command such as &quot;totalcmd.exe /A c:\windows&quot; to open the c:\windows directory, the File Manager Path will be totalcmd.exe, and the Arg For Folder will be /A &quot;%d&quot;. Certain file managers like QTTabBar may just require a path to be supplied, in this instance use &quot;%d&quot; as the File Manager Path and leave the rest of the fileds blank.</system:String>
<system:String x:Key="fileManager_tips2">For example, if the file manager uses a command such as &quot;totalcmd.exe /A c:\windows&quot; to open the c:\windows directory, the File Manager Path will be totalcmd.exe, and the Arg For Folder will be /A &quot;%d&quot;. Certain file managers like QTTabBar may just require a path to be supplied, in this instance use &quot;%d&quot; as the File Manager Path and leave the rest of the fields blank.</system:String>
<system:String x:Key="fileManager_name">Filhåndtering</system:String>
<system:String x:Key="fileManager_profile_name">Profilnavn</system:String>
<system:String x:Key="fileManager_path">Sti til filhåndtering</system:String>
@ -382,6 +493,7 @@
<system:String x:Key="fileManager_file_arg">Arg for fil</system:String>
<system:String x:Key="fileManagerPathNotFound">The file manager '{0}' could not be located at '{1}'. Would you like to continue?</system:String>
<system:String x:Key="fileManagerPathError">File Manager Path Error</system:String>
<system:String x:Key="fileManagerExplorer">File Explorer</system:String>
<!-- DefaultBrowser Setting Dialog -->
<system:String x:Key="defaultBrowserTitle">Default Web Browser</system:String>
@ -392,6 +504,8 @@
<system:String x:Key="defaultBrowser_newWindow">New Window</system:String>
<system:String x:Key="defaultBrowser_newTab">New Tab</system:String>
<system:String x:Key="defaultBrowser_parameter">Privattilstand</system:String>
<system:String x:Key="defaultBrowser_default">Default</system:String>
<system:String x:Key="defaultBrowser_new_profile">New Profile</system:String>
<!-- Priority Setting Dialog -->
<system:String x:Key="changePriorityWindow">Skift prioritet</system:String>
@ -425,13 +539,14 @@
<system:String x:Key="customeQueryHotkeyTips">Press a custom hotkey to open Flow Launcher and input the specified query automatically.</system:String>
<system:String x:Key="preview">Vis</system:String>
<system:String x:Key="hotkeyIsNotUnavailable">Genvejstast er utilgængelig, vælg venligst en ny genvejstast</system:String>
<system:String x:Key="invalidPluginHotkey">Ugyldig plugin genvejstast</system:String>
<system:String x:Key="invalidPluginHotkey">Hotkey is invalid</system:String>
<system:String x:Key="update">Opdater</system:String>
<system:String x:Key="hotkeyRegTitle">Binding Hotkey</system:String>
<system:String x:Key="hotkeyUnavailable">Current hotkey is unavailable.</system:String>
<system:String x:Key="hotkeyUnavailableUneditable">This hotkey is reserved for &quot;{0}&quot; and can't be used. Please choose another hotkey.</system:String>
<system:String x:Key="hotkeyUnavailableEditable">This hotkey is already in use by &quot;{0}&quot;. If you press &quot;Overwrite&quot;, it will be removed from &quot;{0}&quot;.</system:String>
<system:String x:Key="hotkeyRegGuide">Press the keys you want to use for this function.</system:String>
<system:String x:Key="emptyPluginHotkey">Hotkey and action keyword are empty</system:String>
<!-- Custom Query Shortcut Dialog -->
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
@ -442,6 +557,7 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
</system:String>
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
<system:String x:Key="invalidShortcut">Shortcut is invalid</system:String>
<!-- Common Action -->
<system:String x:Key="commonSave">Gem</system:String>
@ -480,6 +596,7 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
</system:String>
<system:String x:Key="errorTitle">Error</system:String>
<system:String x:Key="folderOpenError">An error occurred while opening the folder. {0}</system:String>
<system:String x:Key="browserOpenError">An error occurred while opening the URL in the browser. Please check your Default Web Browser configuration in the General section of the settings window</system:String>
<!-- General Notice -->
<system:String x:Key="pleaseWait">Please wait...</system:String>
@ -505,6 +622,11 @@ If you add an '@' prefix while inputting a shortcut, it matches any position in
<system:String x:Key="update_flowlauncher_update_files">Opdatereringsfiler</system:String>
<system:String x:Key="update_flowlauncher_update_update_description">Opdateringsbeskrivelse</system:String>
<!-- Plugin Update Window -->
<system:String x:Key="restartAfterUpdating">Restart Flow Launcher after updating plugins</system:String>
<system:String x:Key="updatePluginCheckboxContent">{0}: Update from v{1} to v{2}</system:String>
<system:String x:Key="updatePluginNoSelected">No plugin selected</system:String>
<!-- Welcome Window -->
<system:String x:Key="Skip">Skip</system:String>
<system:String x:Key="Welcome_Page1_Title">Welcome to Flow Launcher</system:String>

View file

@ -10,11 +10,31 @@
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
Click yes if you would like select the {0} executable again. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Der Pfad zur ausführbaren Datei {0} kann nicht festgelegt werden. Bitte versuchen Sie es in den Einstellungen von Flow (scrollen Sie nach unten).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Plug-ins können nicht initialisiert werden</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plug-ins: {0} - nicht geladen werden und wird deaktiviert, bitte kontaktieren Sie den Ersteller des Plug-ins für Hilfe</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plug-ins: {0} - können nicht geladen werden und wird deaktiviert, bitte kontaktieren Sie den Ersteller des Plug-ins für Hilfe</system:String>
<!-- Portable -->
<system:String x:Key="restartToDisablePortableMode">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</system:String>
<system:String x:Key="restartToEnablePortableMode">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</system:String>
<system:String x:Key="moveToDifferentLocation">Flow Launcher has detected you enabled portable mode, would you like to move it to a different location?</system:String>
<system:String x:Key="shortcutsUninstallerCreated">Flow Launcher has detected you disabled portable mode, the relevant shortcuts and uninstaller entry have been created</system:String>
<system:String x:Key="userDataDuplicated">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.</system:String>
<!-- Plugin Loader -->
<system:String x:Key="pluginHasErrored">The following plugin has errored and cannot be loaded:</system:String>
<system:String x:Key="pluginsHaveErrored">The following plugins have errored and cannot be loaded:</system:String>
<system:String x:Key="referToLogs">Please refer to the logs for more information</system:String>
<!-- Http -->
<system:String x:Key="pleaseTryAgain">Bitte versuchen Sie es erneut</system:String>
<system:String x:Key="parseProxyFailed">Unable to parse Http Proxy</system:String>
<!-- AbstractPluginEnvironment -->
<system:String x:Key="failToInstallTypeScriptEnv">Failed to install TypeScript environment. Please try again later</system:String>
<system:String x:Key="failToInstallPythonEnv">Failed to install Python environment. Please try again later.</system:String>
<!-- MainWindow -->
<system:String x:Key="registerHotkeyFailed">Hotkey &quot;{0}&quot; konnte nicht registriert werden. Der Hotkey ist möglicherweise von einem anderen Programm in Verwendung. Wechseln Sie zu einem anderen Hotkey oder beenden Sie das andere Programm.</system:String>
@ -41,7 +61,7 @@
<system:String x:Key="textTitle">Text</system:String>
<system:String x:Key="GameMode">Spielmodus</system:String>
<system:String x:Key="GameModeToolTip">Aussetzen der Verwendung von Hotkeys.</system:String>
<system:String x:Key="PositionReset">Position zurücksetzen</system:String>
<system:String x:Key="PositionReset">Zurücksetzen der Position</system:String>
<system:String x:Key="PositionResetToolTip">Position des Suchfensters zurücksetzen</system:String>
<system:String x:Key="queryTextBoxPlaceholder">Zum Suchen hier tippen</system:String>
@ -56,7 +76,7 @@
<system:String x:Key="setAutoStartFailed">Fehler bei Einstellungsstart beim Start</system:String>
<system:String x:Key="hideFlowLauncherWhenLoseFocus">Flow Launcher ausblenden, wenn Fokus verloren geht</system:String>
<system:String x:Key="dontPromptUpdateMsg">Versionsbenachrichtigungen nicht zeigen</system:String>
<system:String x:Key="SearchWindowPosition">Search Window Location</system:String>
<system:String x:Key="SearchWindowPosition">Ort des Suchfensters</system:String>
<system:String x:Key="SearchWindowScreenRememberLastLaunchLocation">Letzte Position merken</system:String>
<system:String x:Key="SearchWindowScreenCursor">Monitor mit Mauscursor</system:String>
<system:String x:Key="SearchWindowScreenFocus">Monitor mit fokussiertem Fenster</system:String>
@ -65,8 +85,8 @@
<system:String x:Key="SearchWindowAlign">Position des Suchfensters auf Monitor</system:String>
<system:String x:Key="SearchWindowAlignCenter">Zentriert</system:String>
<system:String x:Key="SearchWindowAlignCenterTop">Oben zentriert</system:String>
<system:String x:Key="SearchWindowAlignLeftTop">Links oben</system:String>
<system:String x:Key="SearchWindowAlignRightTop">Rechts oben</system:String>
<system:String x:Key="SearchWindowAlignLeftTop">Oben links</system:String>
<system:String x:Key="SearchWindowAlignRightTop">Oben rechts</system:String>
<system:String x:Key="SearchWindowAlignCustom">Benutzerdefinierte Position</system:String>
<system:String x:Key="language">Sprache</system:String>
<system:String x:Key="lastQueryMode">Letzter Abfragestil</system:String>
@ -91,6 +111,7 @@
<system:String x:Key="typingStartEn">Tippen immer im englischen Modus starten</system:String>
<system:String x:Key="typingStartEnTooltip">Ändern Sie Ihre Eingabemethode temporär in den englischen Modus, wenn Sie Flow aktivieren.</system:String>
<system:String x:Key="autoUpdates">Auto-Update</system:String>
<system:String x:Key="autoUpdatesTooltip">Automatically check and update the app when available</system:String>
<system:String x:Key="select">Auswählen</system:String>
<system:String x:Key="hideOnStartup">Flow Launcher beim Start ausblenden</system:String>
<system:String x:Key="hideOnStartupToolTip">Flow Launcher-Suchfenster wird nach dem Start in der Taskleiste ausgeblendet.</system:String>
@ -102,40 +123,63 @@
<system:String x:Key="SearchPrecisionLow">Gering</system:String>
<system:String x:Key="SearchPrecisionRegular">Regelmäßig</system:String>
<system:String x:Key="ShouldUsePinyin">Suche mit Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Ermöglicht die Verwendung von Pinyin für die Suche. Pinyin ist das Standardsystem der romanisierten Schreibweise für die Übersetzung von chinesischen Texten.</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Pinyin is the standard system of romanized spelling for translating Chinese. Please note, enabling this can significantly increase memory usage during search.</system:String>
<system:String x:Key="ShouldUseDoublePinyin">Double Pinyin verwenden</system:String>
<system:String x:Key="ShouldUseDoublePinyinToolTip">Use Double Pinyin instead of Full Pinyin to search.</system:String>
<system:String x:Key="DoublePinyinSchema">Double Pinyin-Schema</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoHe">Xiao He</system:String>
<system:String x:Key="DoublePinyinSchemasZiRanMa">Zi Ran Ma</system:String>
<system:String x:Key="DoublePinyinSchemasWeiRuan">Wei Ruan</system:String>
<system:String x:Key="DoublePinyinSchemasZhiNengABC">Zhi Neng ABC</system:String>
<system:String x:Key="DoublePinyinSchemasZiGuangPinYin">Zi Guang Pin Yin</system:String>
<system:String x:Key="DoublePinyinSchemasPinYinJiaJia">Pin Yin Jia Jia</system:String>
<system:String x:Key="DoublePinyinSchemasXingKongJianDao">Xing Kong Jian Dao</system:String>
<system:String x:Key="DoublePinyinSchemasDaNiu">Da Niu</system:String>
<system:String x:Key="DoublePinyinSchemasXiaoLang">Xiao Lang</system:String>
<system:String x:Key="AlwaysPreview">Immer Vorschau</system:String>
<system:String x:Key="AlwaysPreviewToolTip">Vorschau-Panel immer öffnen, wenn Flow aktiviert ist. Drücken Sie {0}, um Vorschau umzuschalten.</system:String>
<system:String x:Key="shadowEffectNotAllowed">Schatteneffekt ist nicht erlaubt, während das aktuelle Theme den Unschärfe-Effekt aktiviert hat</system:String>
<system:String x:Key="searchDelay">Search Delay</system:String>
<system:String x:Key="searchDelayToolTip">Adds a short delay while typing to reduce UI flicker and result load. Recommended if your typing speed is average.</system:String>
<system:String x:Key="searchDelayNumberBoxToolTip">Enter the wait time (in ms) until input is considered complete. This can only be edited if Search Delay is enabled.</system:String>
<system:String x:Key="searchDelayTime">Default Search Delay Time</system:String>
<system:String x:Key="searchDelayTimeToolTip">Wait time before showing results after typing stops. Higher values wait longer. (ms)</system:String>
<system:String x:Key="KoreanImeTitle">Information for Korean IME user</system:String>
<system:String x:Key="searchDelay">Suchverzögerung</system:String>
<system:String x:Key="searchDelayToolTip">Fügt eine kurze Verzögerung beim Tippen hinzu, um das Flackern der Benutzeroberfläche und die Ergebnislast zu verringern. Empfohlen, wenn Ihre Tippgeschwindigkeit durchschnittlich ist.</system:String>
<system:String x:Key="searchDelayNumberBoxToolTip">Geben Sie die Wartezeit (in ms) ein, bis die Eingabe als abgeschlossen gilt. Dies kann nur bearbeitet werden, wenn die Suchverzögerung aktiviert ist.</system:String>
<system:String x:Key="searchDelayTime">Suchverzögerungszeit per Default</system:String>
<system:String x:Key="searchDelayTimeToolTip">Wartezeit, bevor die Ergebnisse nach Tippstopps angezeigt werden. Bei höheren Werten wird länger gewartet. (ms)</system:String>
<system:String x:Key="KoreanImeTitle">Informationen für koreanischen IME-Benutzer</system:String>
<system:String x:Key="KoreanImeGuide">
The Korean input method used in Windows 11 may cause some issues in Flow Launcher.
Die in Windows 11 verwendete koreanische Eingabemethode kann einige Probleme im Flow Launcher verursachen.
If you experience any problems, you may need to enable &quot;Use previous version of Korean IME&quot;.
Wenn Sie irgendwelche Probleme haben, müssen Sie unter Umständen &quot;Vorherige Version der koreanischen IME&quot; aktivieren.
Open Setting in Windows 11 and go to:
Öffnen Sie die Einstellung in Windows 11 und gehen Sie zu:
Time &amp; Language &gt; Language &amp; Region &gt; Korean &gt; Language Options &gt; Keyboard - Microsoft IME &gt; Compatibility,
Time &amp; Language &gt; Language &amp; Region &gt; Koreanisch &gt; Sprachoptionen &gt; Tastatur - Microsoft IME &gt; Kompatibilität,
and enable &quot;Use previous version of Microsoft IME&quot;.
und aktivieren Sie &quot;Vorherige Version von Microsoft IME&quot;.
</system:String>
<system:String x:Key="KoreanImeOpenLink">Sprach- und Regionen-Systemeinstellungen öffnen</system:String>
<system:String x:Key="KoreanImeOpenLinkToolTip">Opens the Korean IME setting location. Go to Korean &gt; Language Options &gt; Keyboard - Microsoft IME &gt; Compatibility</system:String>
<system:String x:Key="KoreanImeOpenLinkToolTip">Öffnet den Ort für koreanische IME-Einstellung. Gehen Sie zu Koreanisch &gt; Sprachoptionen &gt; Tastatur - Microsoft IME &gt; Kompatibilität</system:String>
<system:String x:Key="KoreanImeOpenLinkButton">Öffnen</system:String>
<system:String x:Key="KoreanImeRegistry">Vorherige koreanische IME verwenden</system:String>
<system:String x:Key="KoreanImeRegistryTooltip">You can change the Previous Korean IME settings directly from here</system:String>
<system:String x:Key="KoreanImeRegistryTooltip">Sie können die Einstellungen des vorherigen koreanischen IME direkt von hier aus ändern</system:String>
<system:String x:Key="KoreanImeSettingChangeFailTitle">Koreanische IME-Einstellung konnte nicht geändert werden</system:String>
<system:String x:Key="KoreanImeSettingChangeFailSubTitle">Please check your system registry access or contact support.</system:String>
<system:String x:Key="homePage">Homepage</system:String>
<system:String x:Key="homePageToolTip">Show home page results when query text is empty.</system:String>
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
<system:String x:Key="homePageToolTip">Ergebnisse der Homepage zeigen, wenn Abfragetext leer ist.</system:String>
<system:String x:Key="historyResultsForHomePage">Historie-Ergebnisse auf Homepage zeigen</system:String>
<system:String x:Key="historyResultsCountForHomePage">Maximal gezeigte Historie-Ergebnisse auf Homepage</system:String>
<system:String x:Key="homeToggleBoxToolTip">Dies kann nur bearbeitet werden, wenn das Plug-in das Home-Feature unterstützt und die Homepage aktiviert ist.</system:String>
<system:String x:Key="showAtTopmost">Suchfenster an vorderster zeigen</system:String>
<system:String x:Key="showAtTopmostToolTip">Setzt die Einstellung 'Immer im Vordergrund' anderer Programme außer Kraft und zeigt Flow in der vordersten Position an.</system:String>
<system:String x:Key="autoRestartAfterChanging">Restart after modifying plugin via Plugin Store</system:String>
<system:String x:Key="autoRestartAfterChangingToolTip">Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store</system:String>
<system:String x:Key="showUnknownSourceWarning">Unbekannte Quellwarnung zeigen</system:String>
<system:String x:Key="showUnknownSourceWarningToolTip">Show warning when installing plugins from unknown sources</system:String>
<system:String x:Key="autoUpdatePlugins">Plug-ins automatisch aktualisieren</system:String>
<system:String x:Key="autoUpdatePluginsToolTip">Automatically check plugin updates and notify if there are any updates available</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Plug-in suchen</system:String>
@ -152,12 +196,12 @@
<system:String x:Key="currentActionKeywords">Aktuelles Action-Schlüsselwort</system:String>
<system:String x:Key="newActionKeyword">Neues Aktions-Schlüsselwort</system:String>
<system:String x:Key="actionKeywordsTooltip">Aktions-Schlüsselwörter ändern</system:String>
<system:String x:Key="pluginSearchDelayTime">Plugin search delay time</system:String>
<system:String x:Key="pluginSearchDelayTimeTooltip">Change Plugin Search Delay Time</system:String>
<system:String x:Key="pluginSearchDelayTime">Suchverzögerungszeit für Plug-in</system:String>
<system:String x:Key="pluginSearchDelayTimeTooltip">Suchverzögerungszeit für Plug-in ändern</system:String>
<system:String x:Key="FilterComboboxLabel">Erweiterte Einstellungen</system:String>
<system:String x:Key="DisplayModeOnOff">Aktiviert</system:String>
<system:String x:Key="DisplayModePriority">Priorität</system:String>
<system:String x:Key="DisplayModeSearchDelay">Search Delay</system:String>
<system:String x:Key="DisplayModeSearchDelay">Suchverzögerung</system:String>
<system:String x:Key="DisplayModeHomeOnOff">Homepage</system:String>
<system:String x:Key="currentPriority">Aktuelle Priorität</system:String>
<system:String x:Key="newPriority">Neue Priorität</system:String>
@ -172,8 +216,15 @@
<system:String x:Key="plugin_uninstall">Deinstallieren</system:String>
<system:String x:Key="failedToRemovePluginSettingsTitle">Plug-in-Einstellungen können nicht entfernt werden</system:String>
<system:String x:Key="failedToRemovePluginSettingsMessage">Plug-ins: {0} - Plug-in-Einstellungsdateien können nicht entfernt werden, bitte entfernen Sie diese manuell</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plugins: {0} - Fail to remove plugin cache files, please remove them manually</system:String>
<system:String x:Key="failedToRemovePluginCacheTitle">Plug-in-Cache kann nicht entfernt werden</system:String>
<system:String x:Key="failedToRemovePluginCacheMessage">Plug-ins: {0} - Plug-in-Cache-Dateien können nicht entfernt werden, bitte entfernen Sie diese manuell</system:String>
<system:String x:Key="pluginModifiedAlreadyTitle">{0} bereits modifiziert</system:String>
<system:String x:Key="pluginModifiedAlreadyMessage">Please restart Flow before making any further changes</system:String>
<system:String x:Key="failedToInstallPluginTitle">Fail to install {0}</system:String>
<system:String x:Key="failedToUninstallPluginTitle">Fail to uninstall {0}</system:String>
<system:String x:Key="fileNotFoundMessage">Unable to find plugin.json from the extracted zip file, or this path {0} does not exist</system:String>
<system:String x:Key="pluginExistAlreadyMessage">A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin</system:String>
<system:String x:Key="errorCreatingSettingPanel">Error creating setting panel for plugin {0}:{1}{2}</system:String>
<!-- Setting Plugin Store -->
<system:String x:Key="pluginStore">Plug-in-Store</system:String>
@ -189,6 +240,34 @@
<system:String x:Key="LabelNew">Neue Version</system:String>
<system:String x:Key="LabelNewToolTip">Dieses Plug-in ist innerhalb der letzten 7 Tage aktualisiert worden</system:String>
<system:String x:Key="LabelUpdateToolTip">Neues Update ist verfügbar</system:String>
<system:String x:Key="ErrorInstallingPlugin">Fehler bei Installation des Plug-ins</system:String>
<system:String x:Key="ErrorUninstallingPlugin">Fehler bei Deinstallation des Plug-ins</system:String>
<system:String x:Key="ErrorUpdatingPlugin">Fehler bei Aktualisierung des Plug-ins</system:String>
<system:String x:Key="KeepPluginSettingsTitle">Plug-in-Einstellungen beibehalten</system:String>
<system:String x:Key="KeepPluginSettingsSubtitle">Möchten Sie die Einstellungen des Plug-ins für die nächste Nutzung beibehalten?</system:String>
<system:String x:Key="InstallSuccessNoRestart">Plug-in {0} erfolgreich installiert. Bitte starten Sie Flow neu.</system:String>
<system:String x:Key="UninstallSuccessNoRestart">Plug-in {0} erfolgreich deinstalliert. Bitte starten Sie Flow neu.</system:String>
<system:String x:Key="UpdateSuccessNoRestart">Plug-in {0} erfolgreich aktualisiert. Bitte starten Sie Flow neu.</system:String>
<system:String x:Key="InstallPromptTitle">Plug-in-Installation</system:String>
<system:String x:Key="InstallPromptSubtitle">{0} von {1} {2}{2}Möchten Sie dieses Plug-in installieren?</system:String>
<system:String x:Key="UninstallPromptTitle">Plug-in-Deinstallation</system:String>
<system:String x:Key="UninstallPromptSubtitle">{0} von {1} {2}{2}Möchten Sie dieses Plug-in deinstallieren?</system:String>
<system:String x:Key="UpdatePromptTitle">Plug-in-Update</system:String>
<system:String x:Key="UpdatePromptSubtitle">{0} von {1} {2}{2}Möchten Sie dieses Plugin aktualisieren?</system:String>
<system:String x:Key="DownloadingPlugin">Plug-in wird heruntergeladen</system:String>
<system:String x:Key="AutoRestartAfterChange">Automatically restart after installing/uninstalling/updating plugins in plugin store</system:String>
<system:String x:Key="ZipFileNotHavePluginJson">Zip file does not have a valid plugin.json configuration</system:String>
<system:String x:Key="InstallFromUnknownSourceTitle">Installation aus unbekannter Quelle</system:String>
<system:String x:Key="InstallFromUnknownSourceSubtitle">This plugin is from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning in general section of setting window)</system:String>
<system:String x:Key="ZipFiles">Zip-Dateien</system:String>
<system:String x:Key="SelectZipFile">Bitte wählen Sie eine Zip-Datei aus</system:String>
<system:String x:Key="installLocalPluginTooltip">Plug-in aus lokalem Pfad installieren</system:String>
<system:String x:Key="updateNoResultTitle">Kein Update verfügbar</system:String>
<system:String x:Key="updateNoResultSubtitle">Alle Plug-ins sind auf dem neuesten Stand</system:String>
<system:String x:Key="updateAllPluginsTitle">Plug-in-Updates verfügbar</system:String>
<system:String x:Key="updateAllPluginsButtonContent">Plug-ins aktualisieren</system:String>
<system:String x:Key="checkPluginUpdatesTooltip">Plug-in-Updates überprüfen</system:String>
<system:String x:Key="PluginsUpdateSuccessNoRestart">Plug-ins sind erfolgreich aktualisiert. Bitte starten Sie Flow neu.</system:String>
<!-- Setting Theme -->
<system:String x:Key="theme">Theme</system:String>
@ -210,9 +289,9 @@
<system:String x:Key="resultItemFont">Schriftart des Ergebnistitels</system:String>
<system:String x:Key="resultSubItemFont">Schriftart des Ergebnis-Untertitels</system:String>
<system:String x:Key="resetCustomize">Zurücksetzen</system:String>
<system:String x:Key="resetCustomizeToolTip">Reset to the recommended font and size settings.</system:String>
<system:String x:Key="ImportThemeSize">Import Theme Size</system:String>
<system:String x:Key="ImportThemeSizeToolTip">If a size value intended by the theme designer is available, it will be retrieved and applied.</system:String>
<system:String x:Key="resetCustomizeToolTip">Auf die empfohlenen Schriftart- und Größeneinstellungen zurücksetzen.</system:String>
<system:String x:Key="ImportThemeSize">Theme-Größe importieren</system:String>
<system:String x:Key="ImportThemeSizeToolTip">Wenn ein vom Theme-Designer vorgesehener Größenwert verfügbar ist, wird dieser abgerufen und angewendet.</system:String>
<system:String x:Key="CustomizeToolTip">Individuell anpassen</system:String>
<system:String x:Key="windowMode">Fenstermodus</system:String>
<system:String x:Key="opacity">Opazität</system:String>
@ -240,8 +319,8 @@
<system:String x:Key="Clock">Uhr</system:String>
<system:String x:Key="Date">Datum</system:String>
<system:String x:Key="BackdropType">Backdrop-Typ</system:String>
<system:String x:Key="BackdropInfo">The backdrop effect is not applied in the preview.</system:String>
<system:String x:Key="BackdropTypeDisabledToolTip">Backdrop supported starting from Windows 11 build 22000 and above</system:String>
<system:String x:Key="BackdropInfo">Der Backdrop-Effekt wird in der Vorschau nicht angewendet.</system:String>
<system:String x:Key="BackdropTypeDisabledToolTip">Backdrop wird ab Windows 11 Build 22000 und darüber unterstützt</system:String>
<system:String x:Key="BackdropTypesNone">Keine</system:String>
<system:String x:Key="BackdropTypesAcrylic">Acrylic</system:String>
<system:String x:Key="BackdropTypesMica">Mica</system:String>
@ -249,11 +328,12 @@
<system:String x:Key="TypeIsDarkToolTip">Dieses Theme unterstützt zwei Modi (hell/dunkel).</system:String>
<system:String x:Key="TypeHasBlurToolTip">Dieses Theme unterstützt Unschärfe und transparenten Hintergrund.</system:String>
<system:String x:Key="ShowPlaceholder">Platzhalter zeigen</system:String>
<system:String x:Key="ShowPlaceholderTip">Display placeholder when query is empty</system:String>
<system:String x:Key="ShowPlaceholderTip">Platzhalter anzeigen, wenn Abfrage leer ist</system:String>
<system:String x:Key="PlaceholderText">Platzhaltertext</system:String>
<system:String x:Key="PlaceholderTextTip">Change placeholder text. Input empty will use: {0}</system:String>
<system:String x:Key="PlaceholderTextTip">Platzhaltertext ändern. Eingabe leer wird verwendet: {0}</system:String>
<system:String x:Key="KeepMaxResults">Festgelegte Fenstergröße</system:String>
<system:String x:Key="KeepMaxResultsToolTip">The window size is not adjustable by dragging.</system:String>
<system:String x:Key="KeepMaxResultsToolTip">Die Fenstergröße ist durch Ziehen nicht anpassbar.</system:String>
<system:String x:Key="MaxShowResultsCannotWorkWithAlwaysPreview">Since Always Preview is on, maximum results shown may not take effect because preview panel requires a certain minimum height</system:String>
<!-- Setting Hotkey -->
<system:String x:Key="hotkey">Hotkey</system:String>
@ -314,8 +394,30 @@
<system:String x:Key="useGlyphUIEffect">Segoe Fluent-Icons für Abfrageergebnisse verwenden, wo unterstützt</system:String>
<system:String x:Key="flowlauncherPressHotkey">Taste drücken</system:String>
<system:String x:Key="showBadges">Ergebnis-Badges zeigen</system:String>
<system:String x:Key="showBadgesToolTip">For supported plugins, badges are displayed to help distinguish them more easily.</system:String>
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
<system:String x:Key="showBadgesToolTip">Für unterstützte Plug-ins werden Badges zur besseren Unterscheidung angezeigt.</system:String>
<system:String x:Key="showBadgesGlobalOnly">Ergebnis-Badges nur für globale Abfrage zeigen</system:String>
<system:String x:Key="showBadgesGlobalOnlyToolTip">Badges nur für globale Abfrageergebnisse zeigen</system:String>
<system:String x:Key="dialogJumpHotkey">Dialog Jump</system:String>
<system:String x:Key="dialogJumpHotkeyToolTip">Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager.</system:String>
<system:String x:Key="dialogJump">Dialog Jump</system:String>
<system:String x:Key="dialogJumpToolTip">When Open/Save As dialog window opens, quickly navigate to the current path of the file manager.</system:String>
<system:String x:Key="autoDialogJump">Dialog Jump Automatically</system:String>
<system:String x:Key="autoDialogJumpToolTip">When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental)</system:String>
<system:String x:Key="showDialogJumpWindow">Show Dialog Jump Window</system:String>
<system:String x:Key="showDialogJumpWindowToolTip">Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations.</system:String>
<system:String x:Key="dialogJumpWindowPosition">Dialog Jump Window Position</system:String>
<system:String x:Key="dialogJumpWindowPositionToolTip">Select position for the Dialog Jump search window</system:String>
<system:String x:Key="DialogJumpWindowPositionUnderDialog">Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed</system:String>
<system:String x:Key="DialogJumpWindowPositionFollowDefault">Default search window position. Displayed when triggered by search window hotkey</system:String>
<system:String x:Key="dialogJumpResultBehaviour">Dialog Jump Result Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window to the selected result path</system:String>
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Linksklick oder Enter-Taste</system:String>
<system:String x:Key="DialogJumpResultBehaviourRightClick">Rechtsklick</system:String>
<system:String x:Key="dialogJumpFileResultBehaviour">Dialog Jump File Navigation Behaviour</system:String>
<system:String x:Key="dialogJumpFileResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window when the result is a file path</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Vollständigen Pfad in Dateinamensbox ausfüllen</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Vollständigen Pfad in Dateinamensbox ausfüllen und öffnen</system:String>
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Verzeichnis in Pfadbox ausfüllen</system:String>
<!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP-Proxy</system:String>
@ -358,40 +460,52 @@
<system:String x:Key="clearlogfolderMessage">Sind Sie sicher, dass Sie alle Logs löschen wollen?</system:String>
<system:String x:Key="cachefolder">Cache-Ordner</system:String>
<system:String x:Key="clearcachefolder">Cache leeren</system:String>
<system:String x:Key="clearcachefolderMessage">Are you sure you want to delete all caches?</system:String>
<system:String x:Key="clearfolderfailMessage">Failed to clear part of folders and files. Please see log file for more information</system:String>
<system:String x:Key="clearcachefolderMessage">Sind Sie sicher, dass Sie alle Caches löschen wollen?</system:String>
<system:String x:Key="clearfolderfailMessage">Ein Teil der Ordner und Dateien konnte nicht gelöscht werden. Weitere Informationen entnehmen Sie bitte der Logdatei</system:String>
<system:String x:Key="welcomewindow">Assistent</system:String>
<system:String x:Key="userdatapath">Speicherort für Benutzerdaten</system:String>
<system:String x:Key="userdatapathToolTip">Benutzereinstellungen und installierte Plug-ins werden im Ordner für Benutzerdaten gespeichert. Dieser Speicherort kann variieren, je nachdem, ob sich das Programm im portablen Modus befindet oder nicht.</system:String>
<system:String x:Key="userdatapathButton">Ordner öffnen</system:String>
<system:String x:Key="advanced">Advanced</system:String>
<system:String x:Key="advanced">Erweitert</system:String>
<system:String x:Key="logLevel">Log-Ebene</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="LogLevelNONE">Silent</system:String>
<system:String x:Key="LogLevelERROR">Fehler</system:String>
<system:String x:Key="LogLevelINFO">Info</system:String>
<system:String x:Key="settingWindowFontTitle">Setting Window Font</system:String>
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
<system:String x:Key="settingWindowFontTitle">Einstellung der Fensterschriftart</system:String>
<!-- Release Notes Window -->
<system:String x:Key="seeMoreReleaseNotes">Weitere Versionshinweise finden Sie auf GitHub</system:String>
<system:String x:Key="checkNetworkConnectionTitle">Versionshinweise konnten nicht abgerufen werden</system:String>
<system:String x:Key="checkNetworkConnectionSubTitle">Bitte überprüfen Sie Ihre Netzwerkverbindung oder stellen Sie sicher, dass GitHub erreichbar ist</system:String>
<system:String x:Key="appUpdateTitle">Flow Launcher ist aktualisiert worden auf {0}</system:String>
<system:String x:Key="appUpdateButtonContent">Klicken Sie hier, um die Versionshinweise anzusehen</system:String>
<!-- FileManager Setting Dialog -->
<system:String x:Key="fileManagerWindow">Dateimanager auswählen</system:String>
<system:String x:Key="fileManager_learnMore">Mehr erfahren</system:String>
<system:String x:Key="fileManager_tips">Bitte geben Sie den Dateiort des von Ihnen verwendeten Dateimanagers an und fügen Sie bei Bedarf Argumente hinzu. Das „%d“ repräsentiert den dafür zu öffnenden Verzeichnispfad, der vom Feld Arg for Folder und für Befehle zum Öffnen bestimmter Verzeichnisse verwendet wird. Das „%f“ repräsentiert den dafür zu öffnenden Dateipfad, der vom Feld Arg for File und für Befehle zum Öffnen bestimmter Dateien verwendet wird.</system:String>
<system:String x:Key="fileManager_tips2">Zum Beispiel, wenn der Dateimanager einen Befehl wie „totalcmd.exe /A c:\windows“ verwendet, um das Verzeichnis c:\windows zu öffnen, lautet der Dateimanager-Pfad „totalcmd.exe“ und der Arg for Folder „/A %d“. Bestimmte Dateimanager wie QTTabBar kann nur die Angabe eines Pfades erfordern, in diesem Fall verwenden Sie „%d“ als den Dateimanager-Pfad und lassen den Rest der Felder blank.</system:String>
<system:String x:Key="fileManager_tips2">For example, if the file manager uses a command such as &quot;totalcmd.exe /A c:\windows&quot; to open the c:\windows directory, the File Manager Path will be totalcmd.exe, and the Arg For Folder will be /A &quot;%d&quot;. Certain file managers like QTTabBar may just require a path to be supplied, in this instance use &quot;%d&quot; as the File Manager Path and leave the rest of the fields blank.</system:String>
<system:String x:Key="fileManager_name">Dateimanager</system:String>
<system:String x:Key="fileManager_profile_name">Profilname</system:String>
<system:String x:Key="fileManager_path">Dateimanager-Pfad</system:String>
<system:String x:Key="fileManager_directory_arg">Arg For Folder</system:String>
<system:String x:Key="fileManager_file_arg">Arg For File</system:String>
<system:String x:Key="fileManagerPathNotFound">The file manager '{0}' could not be located at '{1}'. Would you like to continue?</system:String>
<system:String x:Key="fileManagerPathError">File Manager Path Error</system:String>
<system:String x:Key="fileManagerPathNotFound">Der Dateimanager '{0}' konnte nicht unter '{1}' gefunden werden. Möchten Sie fortfahren?</system:String>
<system:String x:Key="fileManagerPathError">Pfadfehler bei Dateimanager</system:String>
<system:String x:Key="fileManagerExplorer">File Explorer</system:String>
<!-- DefaultBrowser Setting Dialog -->
<system:String x:Key="defaultBrowserTitle">Webbrowser per Default</system:String>
<system:String x:Key="defaultBrowser_tips">Die Defaulteinstellung folgt der Default-Browsereinstellung des Betriebssystems. Wenn separat angegeben, verwendet Flow diesen Browser.</system:String>
<system:String x:Key="defaultBrowser_tips">Die Defaulteinstellung folgt der Default-Browsereinstellung des Betriebssystems (OS). Wenn separat spezifiziert, verwendet Flow diesen Browser.</system:String>
<system:String x:Key="defaultBrowser_name">Browser</system:String>
<system:String x:Key="defaultBrowser_profile_name">Browser-Name</system:String>
<system:String x:Key="defaultBrowser_path">Browser-Pfad</system:String>
<system:String x:Key="defaultBrowser_newWindow">Neues Fenster</system:String>
<system:String x:Key="defaultBrowser_newTab">Neuer Tab</system:String>
<system:String x:Key="defaultBrowser_parameter">Privater Modus</system:String>
<system:String x:Key="defaultBrowser_default">Default</system:String>
<system:String x:Key="defaultBrowser_new_profile">New Profile</system:String>
<!-- Priority Setting Dialog -->
<system:String x:Key="changePriorityWindow">Priorität ändern</system:String>
@ -403,45 +517,47 @@
<system:String x:Key="newActionKeywords">Neues Aktions-Schlüsselwort</system:String>
<system:String x:Key="cancel">Abbrechen</system:String>
<system:String x:Key="done">Fertig</system:String>
<system:String x:Key="cannotFindSpecifiedPlugin">Das angegebene Plug-in kann nicht gefunden werden</system:String>
<system:String x:Key="cannotFindSpecifiedPlugin">Das spezifizierte Plug-in kann nicht gefunden werden</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">Neues Aktions-Schlüsselwort darf nicht leer sein</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">Dieses neue Aktions-Schlüsselwort ist bereits einem anderen Plug-in zugewiesen, bitte wählen Sie ein anderes</system:String>
<system:String x:Key="newActionKeywordsSameAsOld">Dieses neue Aktions-Schlüsselwort ist dasselbe wie das alte, bitte wählen Sie ein anderes</system:String>
<system:String x:Key="success">Erfolg</system:String>
<system:String x:Key="completedSuccessfully">Erfolgreich abgeschlossen</system:String>
<system:String x:Key="failedToCopy">Failed to copy</system:String>
<system:String x:Key="failedToCopy">Kopieren nicht möglich</system:String>
<system:String x:Key="actionkeyword_tips">Geben Sie die Aktions-Schlüsselwörter ein, die Sie zum Starten des Plug-ins verwenden möchten, und trennen Sie sie durch Leerzeichen voneinander ab. Verwenden Sie *, wenn Sie keine spezifizieren möchten, und das Plug-in wird ohne jegliche Aktions-Schlüsselwörter ausgelöst.</system:String>
<!-- Search Delay Settings Dialog -->
<system:String x:Key="searchDelayTimeTitle">Search Delay Time Setting</system:String>
<system:String x:Key="searchDelayTimeTips">Input the search delay time in ms you like to use for the plugin. Input empty if you don't want to specify any, and the plugin will use default search delay time.</system:String>
<system:String x:Key="searchDelayTimeTitle">Einstellung der Suchverzögerungszeit</system:String>
<system:String x:Key="searchDelayTimeTips">Geben Sie die Suchverzögerungszeit in ms ein, die Sie für das Plug-in verwenden möchten. Eingabe leer, wenn Sie keine Angaben machen wollen, und das Plug-in wird die Default-Suchverzögerungszeit verwenden.</system:String>
<!-- Search Delay Settings Dialog -->
<system:String x:Key="homeTitle">Homepage</system:String>
<system:String x:Key="homeTips">Enable the plugin home page state if you like to show the plugin results when query is empty.</system:String>
<system:String x:Key="homeTips">Aktivieren Sie den Zustand der Plug-in-Homepage, wenn Sie die Plug-in-Ergebnisse anzeigen möchten, wenn Abfrage leer ist.</system:String>
<!-- Custom Query Hotkey Dialog -->
<system:String x:Key="customeQueryHotkeyTitle">Benutzerdefinierter Abfrage-Hotkey</system:String>
<system:String x:Key="customeQueryHotkeyTips">Drücken Sie einen benutzerdefinierten Hotkey, um Flow Launcher zu öffnen und die angegebene Abfrage automatisch einzugeben.</system:String>
<system:String x:Key="customeQueryHotkeyTips">Drücken Sie einen benutzerdefinierten Hotkey, um Flow Launcher zu öffnen und die spezifizierte Abfrage automatisch einzugeben.</system:String>
<system:String x:Key="preview">Vorschau</system:String>
<system:String x:Key="hotkeyIsNotUnavailable">Hotkey ist nicht verfügbar, bitte wählen Sie einen neuen Hotkey aus</system:String>
<system:String x:Key="invalidPluginHotkey">Plug-in-Hotkey ungültig</system:String>
<system:String x:Key="invalidPluginHotkey">Hotkey ist ungültig</system:String>
<system:String x:Key="update">Aktualisieren</system:String>
<system:String x:Key="hotkeyRegTitle">Bindung Hotkey</system:String>
<system:String x:Key="hotkeyUnavailable">Aktueller Hotkey ist nicht verfügbar.</system:String>
<system:String x:Key="hotkeyUnavailableUneditable">Dieser Hotkey ist für &quot;{0}&quot; reserviert und kann nicht verwendet werden. Bitte wählen Sie einen anderen Hotkey.</system:String>
<system:String x:Key="hotkeyUnavailableEditable">Dieser Hotkey ist bereits in Verwendung von &quot;{0}&quot;. Wenn Sie &quot;Überschreiben&quot; drücken, wird dieser aus &quot;{0}&quot; entfernt.</system:String>
<system:String x:Key="hotkeyRegGuide">Drücken Sie die Tasten, die Sie für diese Funktion verwenden möchten.</system:String>
<system:String x:Key="emptyPluginHotkey">Hotkey und Aktions-Schlüsselwort sind leer</system:String>
<!-- Custom Query Shortcut Dialog -->
<system:String x:Key="customeQueryShortcutTitle">Benutzerdefinierter Abfrage-Shortcut</system:String>
<system:String x:Key="customeQueryShortcutTips">Geben Sie einen Shortcut ein, der sich automatisch auf die angegebene Abfrage erweitert.</system:String>
<system:String x:Key="customeQueryShortcutTips">Geben Sie einen Shortcut ein, der sich automatisch auf die spezifizierte Abfrage erweitert.</system:String>
<system:String x:Key="customeQueryShortcutGuide" xml:space="preserve">Ein Shortcut wird erweitert, wenn dieser genau mit der Abfrage übereinstimmt.
Wenn Sie bei der Eingabe eines Shortcuts ein '@'-Präfix hinzufügen, stimmt dieser mit jeder beliebigen Position in der Abfrage überein. Integrierte Shortcuts stimmen mit jeder Position in einer Abfrage überein.
</system:String>
<system:String x:Key="duplicateShortcut">Shortcut ist bereits vorhanden, bitte geben Sie einen neuen Shortcut ein oder bearbeiten Sie den vorhandenen.</system:String>
<system:String x:Key="emptyShortcut">Shortcut und/oder dessen Erweiterung ist leer.</system:String>
<system:String x:Key="invalidShortcut">Shortcut ist ungültig</system:String>
<!-- Common Action -->
<system:String x:Key="commonSave">Speichern</system:String>
@ -474,12 +590,13 @@ Wenn Sie bei der Eingabe eines Shortcuts ein '@'-Präfix hinzufügen, stimmt die
<system:String x:Key="reportWindow_copy_below">2. Kopieren Sie die Ausnahmemeldung unterhalb</system:String>
<!-- File Open Error -->
<system:String x:Key="fileManagerNotFoundTitle">File Manager Error</system:String>
<system:String x:Key="fileManagerNotFoundTitle">Fehler bei Dateimanager</system:String>
<system:String x:Key="fileManagerNotFound">
The specified file manager could not be found. Please check the Custom File Manager setting under Settings &gt; General.
Der spezifizierte Dateimanager konnte nicht gefunden werden. Bitte überprüfen Sie die Einstellung des benutzerdefinierten Dateimanagers unter Einstellungen &gt; Allgemein.
</system:String>
<system:String x:Key="errorTitle">Fehler</system:String>
<system:String x:Key="folderOpenError">An error occurred while opening the folder. {0}</system:String>
<system:String x:Key="folderOpenError">Beim Öffnen des Ordners ist ein Fehler aufgetreten. {0}</system:String>
<system:String x:Key="browserOpenError">Beim Öffnen der URL im Browser ist ein Fehler aufgetreten. Bitte überprüfen Sie die Konfiguration Ihres Default-Webbrowsers im Abschnitt „Allgemein“ des Einstellungsfensters</system:String>
<!-- General Notice -->
<system:String x:Key="pleaseWait">Bitte warten Sie ...</system:String>
@ -505,6 +622,11 @@ Wenn Sie bei der Eingabe eines Shortcuts ein '@'-Präfix hinzufügen, stimmt die
<system:String x:Key="update_flowlauncher_update_files">Daten aktualisieren</system:String>
<system:String x:Key="update_flowlauncher_update_update_description">Update-Beschreibung</system:String>
<!-- Plugin Update Window -->
<system:String x:Key="restartAfterUpdating">Flow Launcher nach Aktualisierung der Plug-ins neu starten</system:String>
<system:String x:Key="updatePluginCheckboxContent">{0}: Update von v{1} auf v{2}</system:String>
<system:String x:Key="updatePluginNoSelected">Kein Plug-In ausgewählt</system:String>
<!-- Welcome Window -->
<system:String x:Key="Skip">Überspringen</system:String>
<system:String x:Key="Welcome_Page1_Title">Willkommen bei Flow Launcher</system:String>

Some files were not shown because too many files have changed in this diff Show more