Merge branch 'dev' into administrator_mode

This commit is contained in:
Jack Ye 2025-06-10 19:42:41 +08:00 committed by GitHub
commit 2c1f1617cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 3536 additions and 1253 deletions

View file

@ -16,6 +16,8 @@ body:
I have checked that this issue has not already been reported.
- label: >
I am using the latest version of Flow Launcher.
- label: >
I am using the prerelease version of Flow Launcher.
- type: textarea
attributes:

View file

@ -11,7 +11,7 @@ def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: st
token (str): GitHub token.
owner (str): The owner of the repository.
repo (str): The name of the repository.
label (str): The label name.
label (str): The label name. Filter is not applied when empty string.
state (str): State of PR, e.g. open, closed, all
Returns:
@ -89,7 +89,7 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all")
Args:
pull_request_items (list[dict]): List of PR items.
label (str): The label name.
label (str): The label name. Filter is not applied when empty string.
state (str): State of PR, e.g. open, closed, all
Returns:
@ -99,14 +99,36 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all")
pr_list = []
count = 0
for pr in pull_request_items:
if pr["state"] == state and [item for item in pr["labels"] if item["name"] == label]:
if state in [pr["state"], "all"] and (not label or [item for item in pr["labels"] if item["name"] == label]):
pr_list.append(pr)
count += 1
print(f"Found {count} PRs with {label if label else 'no'} label and state as {state}")
print(f"Found {count} PRs with {label if label else 'no filter on'} label and state as {state}")
return pr_list
def get_prs_assignees(pull_request_items: list[dict], label: str = "", state: str = "all") -> list[str]:
"""
Returns a list of pull request assignees after applying the label and state filters, excludes jjw24.
Args:
pull_request_items (list[dict]): List of PR items.
label (str): The label name. Filter is not applied when empty string.
state (str): State of PR, e.g. open, closed, all
Returns:
list: A list of strs, where each string is an assignee name. List is not distinct, so can contain
duplicate names.
Returns an empty list if none are found.
"""
assignee_list = []
for pr in pull_request_items:
if state in [pr["state"], "all"] and (not label or [item for item in pr["labels"] if item["name"] == label]):
[assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24" ]
print(f"Found {len(assignee_list)} assignees with {label if label else 'no filter on'} label and state as {state}")
return assignee_list
def get_pr_descriptions(pull_request_items: list[dict]) -> str:
"""
@ -208,6 +230,11 @@ if __name__ == "__main__":
description_content += f"## Features\n{get_pr_descriptions(enhancement_prs)}" if enhancement_prs else ""
description_content += f"## Bug fixes\n{get_pr_descriptions(bug_fix_prs)}" if bug_fix_prs else ""
assignees = list(set(get_prs_assignees(pull_requests, "enhancement", "closed") + get_prs_assignees(pull_requests, "bug", "closed")))
assignees.sort(key=str.lower)
description_content += f"### Authors:\n{', '.join(assignees)}"
update_pull_request_description(
github_token, repository_owner, repository_name, release_pr[0]["number"], description_content
)

View file

@ -3,11 +3,10 @@ name: Publish Default Plugins
on:
push:
branches: ['master']
paths: ['Plugins/**']
workflow_dispatch:
jobs:
build:
publish:
runs-on: windows-latest
steps:
@ -17,39 +16,24 @@ jobs:
with:
dotnet-version: 7.0.x
- name: Determine New Plugin Updates
uses: dorny/paths-filter@v3
id: changes
with:
filters: |
browserbookmark:
- 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json'
calculator:
- 'Plugins/Flow.Launcher.Plugin.Calculator/plugin.json'
explorer:
- 'Plugins/Flow.Launcher.Plugin.Explorer/plugin.json'
pluginindicator:
- 'Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json'
pluginsmanager:
- 'Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json'
processkiller:
- 'Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json'
program:
- 'Plugins/Flow.Launcher.Plugin.Program/plugin.json'
shell:
- 'Plugins/Flow.Launcher.Plugin.Shell/plugin.json'
sys:
- 'Plugins/Flow.Launcher.Plugin.Sys/plugin.json'
url:
- 'Plugins/Flow.Launcher.Plugin.Url/plugin.json'
websearch:
- 'Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json'
windowssettings:
- 'Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json'
base: 'master'
- name: Update Plugins To Production Version
run: |
$version = "1.0.0"
Get-Content appveyor.yml | ForEach-Object {
if ($_ -match "version:\s*'(\d+\.\d+\.\d+)\.") {
$version = $matches[1]
}
}
$jsonFiles = Get-ChildItem -Path ".\Plugins\*\plugin.json"
foreach ($file in $jsonFiles) {
$plugin_old_ver = Get-Content $file.FullName -Raw | ConvertFrom-Json
(Get-Content $file) -replace '"Version"\s*:\s*".*?"', "`"Version`": `"$version`"" | Set-Content $file
$plugin_new_ver = Get-Content $file.FullName -Raw | ConvertFrom-Json
Write-Host "Updated" $plugin_old_ver.Name "version from" $plugin_old_ver.Version "to" $plugin_new_ver.Version
}
- name: Get BrowserBookmark Version
if: steps.changes.outputs.browserbookmark == 'true'
id: updated-version-browserbookmark
uses: notiz-dev/github-action-json-property@release
with:
@ -57,14 +41,12 @@ jobs:
prop_path: 'Version'
- name: Build BrowserBookmark
if: steps.changes.outputs.browserbookmark == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
7z a -tzip "Flow.Launcher.Plugin.BrowserBookmark.zip" "./Flow.Launcher.Plugin.BrowserBookmark/*"
rm -r "Flow.Launcher.Plugin.BrowserBookmark"
- name: Publish BrowserBookmark
if: steps.changes.outputs.browserbookmark == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.BrowserBookmark"
@ -76,7 +58,6 @@ jobs:
- name: Get Calculator Version
if: steps.changes.outputs.calculator == 'true'
id: updated-version-calculator
uses: notiz-dev/github-action-json-property@release
with:
@ -84,14 +65,12 @@ jobs:
prop_path: 'Version'
- name: Build Calculator
if: steps.changes.outputs.calculator == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
7z a -tzip "Flow.Launcher.Plugin.Calculator.zip" "./Flow.Launcher.Plugin.Calculator/*"
rm -r "Flow.Launcher.Plugin.Calculator"
- name: Publish Calculator
if: steps.changes.outputs.calculator == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.Calculator"
@ -103,7 +82,6 @@ jobs:
- name: Get Explorer Version
if: steps.changes.outputs.explorer == 'true'
id: updated-version-explorer
uses: notiz-dev/github-action-json-property@release
with:
@ -111,14 +89,12 @@ jobs:
prop_path: 'Version'
- name: Build Explorer
if: steps.changes.outputs.explorer == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
7z a -tzip "Flow.Launcher.Plugin.Explorer.zip" "./Flow.Launcher.Plugin.Explorer/*"
rm -r "Flow.Launcher.Plugin.Explorer"
- name: Publish Explorer
if: steps.changes.outputs.explorer == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.Explorer"
@ -130,7 +106,6 @@ jobs:
- name: Get PluginIndicator Version
if: steps.changes.outputs.pluginindicator == 'true'
id: updated-version-pluginindicator
uses: notiz-dev/github-action-json-property@release
with:
@ -138,14 +113,12 @@ jobs:
prop_path: 'Version'
- name: Build PluginIndicator
if: steps.changes.outputs.pluginindicator == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
7z a -tzip "Flow.Launcher.Plugin.PluginIndicator.zip" "./Flow.Launcher.Plugin.PluginIndicator/*"
rm -r "Flow.Launcher.Plugin.PluginIndicator"
- name: Publish PluginIndicator
if: steps.changes.outputs.pluginindicator == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginIndicator"
@ -157,7 +130,6 @@ jobs:
- name: Get PluginsManager Version
if: steps.changes.outputs.pluginsmanager == 'true'
id: updated-version-pluginsmanager
uses: notiz-dev/github-action-json-property@release
with:
@ -165,14 +137,12 @@ jobs:
prop_path: 'Version'
- name: Build PluginsManager
if: steps.changes.outputs.pluginsmanager == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
7z a -tzip "Flow.Launcher.Plugin.PluginsManager.zip" "./Flow.Launcher.Plugin.PluginsManager/*"
rm -r "Flow.Launcher.Plugin.PluginsManager"
- name: Publish PluginsManager
if: steps.changes.outputs.pluginsmanager == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginsManager"
@ -184,7 +154,6 @@ jobs:
- name: Get ProcessKiller Version
if: steps.changes.outputs.processkiller == 'true'
id: updated-version-processkiller
uses: notiz-dev/github-action-json-property@release
with:
@ -192,14 +161,12 @@ jobs:
prop_path: 'Version'
- name: Build ProcessKiller
if: steps.changes.outputs.processkiller == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
7z a -tzip "Flow.Launcher.Plugin.ProcessKiller.zip" "./Flow.Launcher.Plugin.ProcessKiller/*"
rm -r "Flow.Launcher.Plugin.ProcessKiller"
- name: Publish ProcessKiller
if: steps.changes.outputs.processkiller == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller"
@ -211,7 +178,6 @@ jobs:
- name: Get Program Version
if: steps.changes.outputs.program == 'true'
id: updated-version-program
uses: notiz-dev/github-action-json-property@release
with:
@ -219,14 +185,12 @@ jobs:
prop_path: 'Version'
- name: Build Program
if: steps.changes.outputs.program == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net7.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
7z a -tzip "Flow.Launcher.Plugin.Program.zip" "./Flow.Launcher.Plugin.Program/*"
rm -r "Flow.Launcher.Plugin.Program"
- name: Publish Program
if: steps.changes.outputs.program == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.Program"
@ -238,7 +202,6 @@ jobs:
- name: Get Shell Version
if: steps.changes.outputs.shell == 'true'
id: updated-version-shell
uses: notiz-dev/github-action-json-property@release
with:
@ -246,14 +209,12 @@ jobs:
prop_path: 'Version'
- name: Build Shell
if: steps.changes.outputs.shell == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
7z a -tzip "Flow.Launcher.Plugin.Shell.zip" "./Flow.Launcher.Plugin.Shell/*"
rm -r "Flow.Launcher.Plugin.Shell"
- name: Publish Shell
if: steps.changes.outputs.shell == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.Shell"
@ -265,7 +226,6 @@ jobs:
- name: Get Sys Version
if: steps.changes.outputs.sys == 'true'
id: updated-version-sys
uses: notiz-dev/github-action-json-property@release
with:
@ -273,14 +233,12 @@ jobs:
prop_path: 'Version'
- name: Build Sys
if: steps.changes.outputs.sys == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
7z a -tzip "Flow.Launcher.Plugin.Sys.zip" "./Flow.Launcher.Plugin.Sys/*"
rm -r "Flow.Launcher.Plugin.Sys"
- name: Publish Sys
if: steps.changes.outputs.sys == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.Sys"
@ -292,7 +250,6 @@ jobs:
- name: Get Url Version
if: steps.changes.outputs.url == 'true'
id: updated-version-url
uses: notiz-dev/github-action-json-property@release
with:
@ -300,14 +257,12 @@ jobs:
prop_path: 'Version'
- name: Build Url
if: steps.changes.outputs.url == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
7z a -tzip "Flow.Launcher.Plugin.Url.zip" "./Flow.Launcher.Plugin.Url/*"
rm -r "Flow.Launcher.Plugin.Url"
- name: Publish Url
if: steps.changes.outputs.url == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.Url"
@ -319,7 +274,6 @@ jobs:
- name: Get WebSearch Version
if: steps.changes.outputs.websearch == 'true'
id: updated-version-websearch
uses: notiz-dev/github-action-json-property@release
with:
@ -327,14 +281,12 @@ jobs:
prop_path: 'Version'
- name: Build WebSearch
if: steps.changes.outputs.websearch == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
7z a -tzip "Flow.Launcher.Plugin.WebSearch.zip" "./Flow.Launcher.Plugin.WebSearch/*"
rm -r "Flow.Launcher.Plugin.WebSearch"
- name: Publish WebSearch
if: steps.changes.outputs.websearch == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.WebSearch"
@ -346,7 +298,6 @@ jobs:
- name: Get WindowsSettings Version
if: steps.changes.outputs.windowssettings == 'true'
id: updated-version-windowssettings
uses: notiz-dev/github-action-json-property@release
with:
@ -354,14 +305,12 @@ jobs:
prop_path: 'Version'
- name: Build WindowsSettings
if: steps.changes.outputs.windowssettings == 'true'
run: |
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
7z a -tzip "Flow.Launcher.Plugin.WindowsSettings.zip" "./Flow.Launcher.Plugin.WindowsSettings/*"
rm -r "Flow.Launcher.Plugin.WindowsSettings"
- name: Publish WindowsSettings
if: steps.changes.outputs.windowssettings == 'true'
uses: softprops/action-gh-release@v2
with:
repository: "Flow-Launcher/Flow.Launcher.Plugin.WindowsSettings"

View file

@ -12,7 +12,7 @@ using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin
{
public class JsonRPCPluginSettings
public class JsonRPCPluginSettings : ISavable
{
public required JsonRpcConfigurationModel? Configuration { get; init; }

View file

@ -671,7 +671,15 @@ namespace Flow.Launcher.Core.Resource
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property.Name == "Background"));
windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Transparent)));
}
// For themes with blur enabled, the window border is rendered by the system, so it's treated as a simple rectangle regardless of thickness.
//(This is to avoid issues when the window is forcibly changed to a rectangular shape during snap scenarios.)
var cornerRadiusSetter = windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property == Border.CornerRadiusProperty);
if (cornerRadiusSetter != null)
cornerRadiusSetter.Value = new CornerRadius(0);
else
windowBorderStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(0)));
// Apply the blur effect
Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType);
ColorizeWindow(theme, backdropType);

View file

@ -1,25 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Resource
{
public class TranslationConverter : IValueConverter
{
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null;
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var key = value.ToString();
if (string.IsNullOrEmpty(key)) return key;
return API.GetTranslation(key);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new InvalidOperationException();
}
}

View file

@ -220,5 +220,18 @@ namespace Flow.Launcher.Infrastructure.Http
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
public static async Task<string> GetStringAsync(string url, CancellationToken token = default)
{
try
{
Log.Debug(ClassName, $"Url <{url}>");
return await client.GetStringAsync(url, token);
}
catch (System.Exception e)
{
return string.Empty;
}
}
}
}

View file

@ -42,6 +42,11 @@ MONITORINFOEXW
WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE
WM_NCLBUTTONDBLCLK
WM_SYSCOMMAND
SC_MAXIMIZE
SC_MINIMIZE
OleInitialize
OleUninitialize

View file

@ -2,11 +2,13 @@
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
{
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
// Expose ISaveable interface in derived class to make sure we are calling the new version of Save method
public class FlowLauncherJsonStorage<T> : JsonStorage<T>, ISavable where T : new()
{
private static readonly string ClassName = "FlowLauncherJsonStorage";

View file

@ -1,11 +1,13 @@
using System.IO;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
{
public class PluginBinaryStorage<T> : BinaryStorage<T> where T : new()
// Expose ISaveable interface in derived class to make sure we are calling the new version of Save method
public class PluginBinaryStorage<T> : BinaryStorage<T>, ISavable where T : new()
{
private static readonly string ClassName = "PluginBinaryStorage";

View file

@ -2,11 +2,13 @@
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage
{
public class PluginJsonStorage<T> : JsonStorage<T> where T : new()
// Expose ISaveable interface in derived class to make sure we are calling the new version of Save method
public class PluginJsonStorage<T> : JsonStorage<T>, ISavable where T : new()
{
// Use assembly name to check which plugin is using this storage
public readonly string AssemblyName;

View file

@ -1,6 +1,8 @@
using System;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings
{
@ -53,6 +55,12 @@ 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 BaseBuiltinShortcutModel(string key, string description)
{
Key = key;

View file

@ -33,7 +33,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
_storage.Save();
}
private string _theme = Constant.DefaultTheme;
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
public string ColorScheme { get; set; } = "System";
@ -60,16 +59,20 @@ namespace Flow.Launcher.Infrastructure.UserSettings
get => _language;
set
{
_language = value;
OnPropertyChanged();
if (_language != value)
{
_language = value;
OnPropertyChanged();
}
}
}
private string _theme = Constant.DefaultTheme;
public string Theme
{
get => _theme;
set
{
if (value != _theme)
if (_theme != value)
{
_theme = value;
OnPropertyChanged();
@ -79,6 +82,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
public bool UseDropShadowEffect { get; set; } = true;
public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None;
public string ReleaseNotesVersion { get; set; } = string.Empty;
/* Appearance Settings. It should be separated from the setting later.*/
public double WindowHeightSize { get; set; } = 42;
@ -297,9 +301,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;
}
}
}
@ -366,8 +373,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
get => _hideNotifyIcon;
set
{
_hideNotifyIcon = value;
OnPropertyChanged();
if (_hideNotifyIcon != value)
{
_hideNotifyIcon = value;
OnPropertyChanged();
}
}
}
public bool LeaveCmdOpen { get; set; }
@ -375,6 +385,20 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool AlwaysRunAsAdministrator { get; set; } = false;
private bool _showAtTopmost = true;
public bool ShowAtTopmost
{
get => _showAtTopmost;
set
{
if (_showAtTopmost != value)
{
_showAtTopmost = value;
OnPropertyChanged();
}
}
}
public bool SearchQueryResultsWithDelay { get; set; }
public int SearchDelayTime { get; set; } = 150;

View file

@ -325,6 +325,11 @@ namespace Flow.Launcher.Infrastructure
public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE;
public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE;
public const int WM_NCLBUTTONDBLCLK = (int)PInvoke.WM_NCLBUTTONDBLCLK;
public const int WM_SYSCOMMAND = (int)PInvoke.WM_SYSCOMMAND;
public const int SC_MAXIMIZE = (int)PInvoke.SC_MAXIMIZE;
public const int SC_MINIMIZE = (int)PInvoke.SC_MINIMIZE;
#endregion

View file

@ -14,10 +14,10 @@
</PropertyGroup>
<PropertyGroup>
<Version>4.4.0</Version>
<PackageVersion>4.4.0</PackageVersion>
<AssemblyVersion>4.4.0</AssemblyVersion>
<FileVersion>4.4.0</FileVersion>
<Version>4.5.0</Version>
<PackageVersion>4.5.0</PackageVersion>
<AssemblyVersion>4.5.0</AssemblyVersion>
<FileVersion>4.5.0</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

View file

@ -84,6 +84,15 @@ namespace Flow.Launcher.Plugin
/// <param name="subTitle">Optional message subtitle</param>
void ShowMsgError(string title, string subTitle = "");
/// <summary>
/// Show the error message using Flow's standard error icon.
/// </summary>
/// <param name="title">Message title</param>
/// <param name="buttonText">Message button content</param>
/// <param name="buttonAction">Message button action</param>
/// <param name="subTitle">Optional message subtitle</param>
void ShowMsgErrorWithButton(string title, string buttonText, Action buttonAction, string subTitle = "");
/// <summary>
/// Show the MainWindow when hiding
/// </summary>
@ -127,6 +136,27 @@ namespace Flow.Launcher.Plugin
/// <param name="useMainWindowAsOwner">when true will use main windows as the owner</param>
void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true);
/// <summary>
/// Show message box with button
/// </summary>
/// <param name="title">Message title</param>
/// <param name="buttonText">Message button content</param>
/// <param name="buttonAction">Message button action</param>
/// <param name="subTitle">Message subtitle</param>
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle = "", string iconPath = "");
/// <summary>
/// Show message box with button
/// </summary>
/// <param name="title">Message title</param>
/// <param name="buttonText">Message button content</param>
/// <param name="buttonAction">Message button action</param>
/// <param name="subTitle">Message subtitle</param>
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
/// <param name="useMainWindowAsOwner">when true will use main windows as the owner</param>
void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath, bool useMainWindowAsOwner = true);
/// <summary>
/// Open setting dialog
/// </summary>

View file

@ -1,8 +1,9 @@
using Microsoft.Win32;
using System;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Win32;
namespace Flow.Launcher.Plugin.SharedCommands
{
@ -13,7 +14,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
{
private static string GetDefaultBrowserPath()
{
string name = string.Empty;
var name = string.Empty;
try
{
using var regDefault = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice", false);
@ -23,8 +24,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
name = regKey.GetValue(null).ToString().ToLower().Replace("\"", "");
if (!name.EndsWith("exe"))
name = name.Substring(0, name.LastIndexOf(".exe") + 4);
name = name[..(name.LastIndexOf(".exe") + 4)];
}
catch
{
@ -65,12 +65,21 @@ namespace Flow.Launcher.Plugin.SharedCommands
{
Process.Start(psi)?.Dispose();
}
catch (System.ComponentModel.Win32Exception)
// This error may be thrown if browser path is incorrect
catch (Win32Exception)
{
Process.Start(new ProcessStartInfo
try
{
FileName = url, UseShellExecute = true
});
Process.Start(new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
}
catch
{
throw; // Re-throw the exception if we cannot open the URL in the default browser
}
}
}
@ -100,12 +109,20 @@ namespace Flow.Launcher.Plugin.SharedCommands
Process.Start(psi)?.Dispose();
}
// This error may be thrown if browser path is incorrect
catch (System.ComponentModel.Win32Exception)
catch (Win32Exception)
{
Process.Start(new ProcessStartInfo
try
{
FileName = url, UseShellExecute = true
});
Process.Start(new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
}
catch
{
throw; // Re-throw the exception if we cannot open the URL in the default browser
}
}
}
}

View file

@ -174,6 +174,8 @@ namespace Flow.Launcher
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
Notification.Install();
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");

View file

@ -90,6 +90,11 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<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" />

View file

@ -1,20 +1,21 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Exception;
using Flow.Launcher.Infrastructure.Logger;
using NLog;
namespace Flow.Launcher.Helper;
public static class ErrorReporting
{
private static void Report(Exception e, [CallerMemberName] string methodName = "UnHandledException")
private static void Report(Exception e, bool silent = false, [CallerMemberName] string methodName = "UnHandledException")
{
var logger = LogManager.GetLogger(methodName);
logger.Fatal(ExceptionFormatter.FormatExcpetion(e));
if (silent) return;
var reportWindow = new ReportWindow(e);
reportWindow.Show();
}
@ -35,8 +36,9 @@ public static class ErrorReporting
public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
// handle unobserved task exceptions on UI thread
Application.Current.Dispatcher.Invoke(() => Report(e.Exception));
// log exception but do not handle unobserved task exceptions on UI thread
//Application.Current.Dispatcher.Invoke(() => Report(e.Exception, true));
Log.Exception(nameof(ErrorReporting), "Unobserved task exception occurred.", e.Exception);
// prevent application exit, so the user can copy the prompted error info
e.SetObserved();
}

View file

@ -125,12 +125,12 @@
BorderThickness="0 1 0 0"
CornerRadius="0 0 8 8">
<StackPanel
Margin="10"
Margin="10 9 10 10"
HorizontalAlignment="Center"
Orientation="Horizontal">
<Button
x:Name="OverwriteBtn"
Height="30"
MinHeight="36"
MinWidth="100"
Margin="0 0 4 0"
Click="Overwrite"
@ -139,26 +139,26 @@
Visibility="Collapsed" />
<Button
x:Name="SaveBtn"
Height="30"
MinHeight="36"
MinWidth="100"
Margin="0 0 4 0"
Click="Save"
Content="{DynamicResource commonSave}"
Style="{StaticResource AccentButtonStyle}" />
<Button
Height="30"
MinHeight="36"
MinWidth="100"
Margin="4 0 4 0"
Click="Reset"
Content="{DynamicResource commonReset}" />
<Button
Height="30"
MinHeight="36"
MinWidth="100"
Margin="4 0 4 0"
Click="Delete"
Content="{DynamicResource commonDelete}" />
<Button
Height="30"
MinHeight="36"
MinWidth="100"
Margin="4 0 0 0"
Click="Cancel"

View file

@ -132,6 +132,8 @@
<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 Topmost</system:String>
<system:String x:Key="showAtTopmostToolTip">Show search window above other windows</system:String>
<system:String x:Key="alwaysRunAsAdministrator">Always run as administrator</system:String>
<system:String x:Key="alwaysRunAsAdministratorToolTip">Run Flow Launcher as administrator on system startup</system:String>
<system:String x:Key="runAsAdministratorChange">Administrator Mode Change</system:String>
@ -370,6 +372,13 @@
<system:String x:Key="LogLevelINFO">Info</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>
@ -478,6 +487,7 @@
</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>

View file

@ -42,7 +42,7 @@
<system:String x:Key="GameMode">Chế độ trò chơi</system:String>
<system:String x:Key="GameModeToolTip">Tạm dừng sử dụng phím nóng.</system:String>
<system:String x:Key="PositionReset">Đặt lại vị trí</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
<system:String x:Key="PositionResetToolTip">Cài lại vị trí cửa sổ tìm kiếm</system:String>
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General -->

View file

@ -84,6 +84,8 @@ namespace Flow.Launcher
_viewModel = Ioc.Default.GetRequiredService<MainViewModel>();
DataContext = _viewModel;
Topmost = _settings.ShowAtTopmost;
InitializeComponent();
UpdatePosition();
@ -121,6 +123,9 @@ namespace Flow.Launcher
// Set First Launch to false
_settings.FirstLaunch = false;
// Update release notes version
_settings.ReleaseNotesVersion = Constant.Version;
// Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None
if (Win32Helper.IsBackdropSupported()) _settings.BackdropType = BackdropTypes.Acrylic;
@ -132,6 +137,25 @@ namespace Flow.Launcher
welcomeWindow.Show();
}
if (_settings.ReleaseNotesVersion != Constant.Version)
{
// Update release notes version
_settings.ReleaseNotesVersion = Constant.Version;
// Display message box with button
App.API.ShowMsgWithButton(
string.Format(App.API.GetTranslation("appUpdateTitle"), Constant.Version),
App.API.GetTranslation("appUpdateButtonContent"),
() =>
{
Application.Current.Dispatcher.Invoke(() =>
{
var releaseNotesWindow = new ReleaseNotesWindow();
releaseNotesWindow.Show();
});
});
}
// Initialize place holder
SetupPlaceholderText();
_viewModel.PlaceholderText = _settings.PlaceholderText;
@ -289,6 +313,9 @@ namespace Flow.Launcher
_viewModel.QueryResults();
}
break;
case nameof(Settings.ShowAtTopmost):
Topmost = _settings.ShowAtTopmost;
break;
}
};
@ -465,7 +492,55 @@ namespace Flow.Launcher
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left) DragMove();
// When the window is maximized via Snap,
// dragging attempts will first switch the window from Maximized to Normal state,
// and adjust the drag position accordingly.
if (e.ChangedButton == MouseButton.Left)
{
try
{
if (WindowState == WindowState.Maximized)
{
// Calculate ratio based on maximized window dimensions
double maxWidth = ActualWidth;
double maxHeight = ActualHeight;
var mousePos = e.GetPosition(this);
double xRatio = mousePos.X / maxWidth;
double yRatio = mousePos.Y / maxHeight;
// Current monitor information
var screen = Screen.FromHandle(new WindowInteropHelper(this).Handle);
var workingArea = screen.WorkingArea;
var screenLeftTop = Win32Helper.TransformPixelsToDIP(this, workingArea.X, workingArea.Y);
// Switch to Normal state
WindowState = WindowState.Normal;
Application.Current?.Dispatcher.Invoke(new Action(() =>
{
double normalWidth = Width;
double normalHeight = Height;
// Apply ratio based on the difference between maximized and normal window sizes
Left = screenLeftTop.X + (maxWidth - normalWidth) * xRatio;
Top = screenLeftTop.Y + (maxHeight - normalHeight) * yRatio;
if (Mouse.LeftButton == MouseButtonState.Pressed)
{
DragMove();
}
}), DispatcherPriority.ApplicationIdle);
}
else
{
DragMove();
}
}
catch (InvalidOperationException)
{
// Ignored - can occur if drag operation is already in progress
}
}
}
#endregion
@ -490,56 +565,76 @@ namespace Flow.Launcher
#region Window WndProc
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == Win32Helper.WM_ENTERSIZEMOVE)
switch (msg)
{
_initialWidth = (int)Width;
_initialHeight = (int)Height;
handled = true;
}
else if (msg == Win32Helper.WM_EXITSIZEMOVE)
{
if (_initialHeight != (int)Height)
{
if (!_settings.KeepMaxResults)
case Win32Helper.WM_ENTERSIZEMOVE:
_initialWidth = (int)Width;
_initialHeight = (int)Height;
handled = true;
break;
case Win32Helper.WM_EXITSIZEMOVE:
//Prevent updating the number of results when the window height is below the height of a single result item.
//This situation occurs not only when the user manually resizes the window, but also when the window is released from a side snap, as the OS automatically adjusts the window height.
//(Without this check, releasing from a snap can cause the window height to hit the minimum, resulting in only 2 results being shown.)
if (_initialHeight != (int)Height && Height > (_settings.WindowHeightSize + _settings.ItemHeightSize))
{
// Get shadow margin
var shadowMargin = 0;
var (_, useDropShadowEffect) = _theme.GetActualValue();
if (useDropShadowEffect)
if (!_settings.KeepMaxResults)
{
shadowMargin = 32;
// Get shadow margin
var shadowMargin = 0;
var (_, useDropShadowEffect) = _theme.GetActualValue();
if (useDropShadowEffect)
{
shadowMargin = 32;
}
// Calculate max results to show
var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize;
if (itemCount < 2)
{
_settings.MaxResultsToShow = 2;
}
else
{
_settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount));
}
}
// Calculate max results to show
var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize;
if (itemCount < 2)
{
_settings.MaxResultsToShow = 2;
}
else
{
_settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount));
}
SizeToContent = SizeToContent.Height;
}
else
{
// Update height when exiting maximized snap state.
SizeToContent = SizeToContent.Height;
}
SizeToContent = SizeToContent.Height;
}
if (_initialWidth != (int)Width)
{
if (!_settings.KeepMaxResults)
if (_initialWidth != (int)Width)
{
// Update width
_viewModel.MainWindowWidth = Width;
if (!_settings.KeepMaxResults)
{
// Update width
_viewModel.MainWindowWidth = Width;
}
SizeToContent = SizeToContent.Height;
}
handled = true;
break;
case Win32Helper.WM_NCLBUTTONDBLCLK: // Block the double click in frame
SizeToContent = SizeToContent.Height;
}
handled = true;
handled = true;
break;
case Win32Helper.WM_SYSCOMMAND: // Block Maximize/Minimize by Win+Up and Win+Down Arrow
var command = wParam.ToInt32() & 0xFFF0;
if (command == Win32Helper.SC_MAXIMIZE || command == Win32Helper.SC_MINIMIZE)
{
SizeToContent = SizeToContent.Height;
handled = true;
}
break;
}
return IntPtr.Zero;

View file

@ -1,41 +1,76 @@
<Window x:Class="Flow.Launcher.Msg"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="#ebebeb"
Topmost="True"
SizeToContent="Height"
ResizeMode="NoResize"
WindowStyle="None"
ShowInTaskbar="False"
Title="Msg" Height="60" Width="420">
<Window
x:Class="Flow.Launcher.Msg"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Msg"
Width="420"
Height="60"
Background="#ebebeb"
ResizeMode="NoResize"
ShowInTaskbar="False"
SizeToContent="Height"
Topmost="True"
WindowStyle="None">
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation x:Name="showAnimation" Duration="0:0:0.3" Storyboard.TargetProperty="Top"
AccelerationRatio="0.2" />
<DoubleAnimation
x:Name="showAnimation"
AccelerationRatio="0.2"
Storyboard.TargetProperty="Top"
Duration="0:0:0.3" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5">
<Grid
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition />
<ColumnDefinition Width="2.852" />
<ColumnDefinition Width="13.148"/>
<ColumnDefinition Width="13.148" />
</Grid.ColumnDefinitions>
<Image x:Name="imgIco" Width="32" Height="32" HorizontalAlignment="Left" Margin="0,9" />
<Grid HorizontalAlignment="Stretch" Margin="5 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
<Image
x:Name="imgIco"
Width="32"
Height="32"
Margin="0 9"
HorizontalAlignment="Left" />
<Grid
Grid.Column="1"
Margin="5 0 0 0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock x:Name="tbTitle" FontSize="16" Foreground="#37392c" FontWeight="Medium">Title</TextBlock>
<TextBlock Grid.Row="1" Foreground="#8e94a4" x:Name="tbSubTitle">sdfdsf</TextBlock>
<TextBlock
x:Name="tbTitle"
FontSize="16"
FontWeight="Medium"
Foreground="#37392c">
Title
</TextBlock>
<TextBlock
x:Name="tbSubTitle"
Grid.Row="1"
Foreground="#8e94a4">
sdfdsf
</TextBlock>
</Grid>
<Image x:Name="imgClose" Grid.Column="2" Cursor="Hand" Width="16" VerticalAlignment="Top"
HorizontalAlignment="Right" Grid.ColumnSpan="2" />
<Image
x:Name="imgClose"
Grid.Column="2"
Grid.ColumnSpan="2"
Width="16"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Cursor="Hand" />
</Grid>
</Window>

View file

@ -0,0 +1,84 @@
<Window
x:Class="Flow.Launcher.MsgWithButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Msg"
Width="420"
Height="60"
Background="#ebebeb"
ResizeMode="NoResize"
ShowInTaskbar="False"
SizeToContent="Height"
Topmost="True"
WindowStyle="None">
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
x:Name="showAnimation"
AccelerationRatio="0.2"
Storyboard.TargetProperty="Top"
Duration="0:0:0.3" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<StackPanel Orientation="Vertical">
<Grid
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition />
<ColumnDefinition Width="2.852" />
<ColumnDefinition Width="13.148" />
</Grid.ColumnDefinitions>
<Image
x:Name="imgIco"
Width="32"
Height="32"
Margin="0 9"
HorizontalAlignment="Left" />
<Grid
Grid.Column="1"
Margin="5 0 0 0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
x:Name="tbTitle"
FontSize="16"
FontWeight="Medium"
Foreground="#37392c">
Title
</TextBlock>
<TextBlock
x:Name="tbSubTitle"
Grid.Row="1"
Foreground="#8e94a4">
sdfdsf
</TextBlock>
</Grid>
<Image
x:Name="imgClose"
Grid.Column="2"
Grid.ColumnSpan="2"
Width="16"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Cursor="Hand" />
</Grid>
<Button
x:Name="btn"
Margin="5 0 5 5"
HorizontalAlignment="Stretch"
Content="fwafaw"
Foreground="#dcdcdc" />
</StackPanel>
</Window>

View file

@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media.Animation;
using Flow.Launcher.Infrastructure;
namespace Flow.Launcher
{
public partial class MsgWithButton : Window
{
private readonly Storyboard fadeOutStoryboard = new();
private bool closing;
public MsgWithButton()
{
InitializeComponent();
var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
var dipWorkingArea = Win32Helper.TransformPixelsToDIP(this,
screen.WorkingArea.Width,
screen.WorkingArea.Height);
Left = dipWorkingArea.X - Width;
Top = dipWorkingArea.Y;
showAnimation.From = dipWorkingArea.Y;
showAnimation.To = dipWorkingArea.Y - Height;
// Create the fade out storyboard
fadeOutStoryboard.Completed += fadeOutStoryboard_Completed;
DoubleAnimation fadeOutAnimation = new DoubleAnimation(dipWorkingArea.Y - Height, dipWorkingArea.Y, new Duration(TimeSpan.FromSeconds(5)))
{
AccelerationRatio = 0.2
};
Storyboard.SetTarget(fadeOutAnimation, this);
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(TopProperty));
fadeOutStoryboard.Children.Add(fadeOutAnimation);
_ = LoadImageAsync();
imgClose.MouseUp += imgClose_MouseUp;
}
private async System.Threading.Tasks.Task LoadImageAsync()
{
imgClose.Source = await App.API.LoadImageAsync(Path.Combine(Constant.ProgramDirectory, "Images\\close.png"));
}
void imgClose_MouseUp(object sender, MouseButtonEventArgs e)
{
if (!closing)
{
closing = true;
fadeOutStoryboard.Begin();
}
}
private void fadeOutStoryboard_Completed(object sender, EventArgs e)
{
Close();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
public async void Show(string title, string buttonText, Action buttonAction, string subTitle, string iconPath)
{
tbTitle.Text = title;
tbSubTitle.Text = subTitle;
btn.Content = buttonText;
btn.Click += (s, e) => buttonAction();
if (string.IsNullOrEmpty(subTitle))
{
tbSubTitle.Visibility = Visibility.Collapsed;
}
if (!File.Exists(iconPath))
{
imgIco.Source = await App.API.LoadImageAsync(Path.Combine(Constant.ProgramDirectory, "Images\\app.png"));
}
else
{
imgIco.Source = await App.API.LoadImageAsync(iconPath);
}
Show();
await Dispatcher.InvokeAsync(async () =>
{
if (!closing)
{
closing = true;
await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin);
}
});
}
}
}

View file

@ -1,8 +1,9 @@
using Flow.Launcher.Infrastructure;
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Windows;
using Flow.Launcher.Infrastructure;
using Microsoft.Toolkit.Uwp.Notifications;
namespace Flow.Launcher
{
@ -12,10 +13,30 @@ namespace Flow.Launcher
internal static bool legacy = !Win32Helper.IsNotificationSupported();
private static readonly ConcurrentDictionary<string, Action> _notificationActions = new();
internal static void Install()
{
if (!legacy)
{
ToastNotificationManagerCompat.OnActivated += toastArgs =>
{
var actionId = toastArgs.Argument; // Or use toastArgs.UserInput if using input
if (_notificationActions.TryGetValue(actionId, out var action))
{
action?.Invoke();
}
};
}
}
internal static void Uninstall()
{
if (!legacy)
{
_notificationActions.Clear();
ToastNotificationManagerCompat.Uninstall();
}
}
public static void Show(string title, string subTitle, string iconPath = null)
@ -67,5 +88,58 @@ namespace Flow.Launcher
var msg = new Msg();
msg.Show(title, subTitle, iconPath);
}
public static void ShowWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath = null)
{
Application.Current.Dispatcher.Invoke(() =>
{
ShowInternalWithButton(title, buttonText, buttonAction, subTitle, iconPath);
});
}
private static void ShowInternalWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath = null)
{
// Handle notification for win7/8/early win10
if (legacy)
{
LegacyShowWithButton(title, buttonText, buttonAction, subTitle, iconPath);
return;
}
// Using Windows Notification System
var Icon = !File.Exists(iconPath)
? Path.Combine(Constant.ProgramDirectory, "Images\\app.png")
: iconPath;
try
{
var guid = Guid.NewGuid().ToString();
new ToastContentBuilder()
.AddText(title, hintMaxLines: 1)
.AddText(subTitle)
.AddButton(buttonText, ToastActivationType.Background, guid)
.AddAppLogoOverride(new Uri(Icon))
.Show();
_notificationActions.AddOrUpdate(guid, buttonAction, (key, oldValue) => buttonAction);
}
catch (InvalidOperationException e)
{
// Temporary fix for the Windows 11 notification issue
// Possibly from 22621.1413 or 22621.1485, judging by post time of #2024
App.API.LogException(ClassName, "Notification InvalidOperationException Error", e);
LegacyShowWithButton(title, buttonText, buttonAction, subTitle, iconPath);
}
catch (Exception e)
{
App.API.LogException(ClassName, "Notification Error", e);
LegacyShowWithButton(title, buttonText, buttonAction, subTitle, iconPath);
}
}
private static void LegacyShowWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath)
{
var msg = new MsgWithButton();
msg.Show(title, buttonText, buttonAction, subTitle, iconPath);
}
}
}

View file

@ -122,6 +122,9 @@ namespace Flow.Launcher
public void ShowMsgError(string title, string subTitle = "") =>
ShowMsg(title, subTitle, Constant.ErrorIcon, true);
public void ShowMsgErrorWithButton(string title, string buttonText, Action buttonAction, string subTitle = "") =>
ShowMsgWithButton(title, buttonText, buttonAction, subTitle, Constant.ErrorIcon, true);
public void ShowMsg(string title, string subTitle = "", string iconPath = "") =>
ShowMsg(title, subTitle, iconPath, true);
@ -130,6 +133,14 @@ namespace Flow.Launcher
Notification.Show(title, subTitle, iconPath);
}
public void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle = "", string iconPath = "") =>
ShowMsgWithButton(title, buttonText, buttonAction, subTitle, iconPath, true);
public void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath, bool useMainWindowAsOwner = true)
{
Notification.ShowWithButton(title, buttonText, buttonAction, subTitle, iconPath);
}
public void OpenSettingDialog()
{
Application.Current.Dispatcher.Invoke(() =>
@ -250,7 +261,7 @@ namespace Flow.Launcher
Http.GetStreamAsync(url, token);
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null,
CancellationToken token = default) =>Http.DownloadAsync(url, filePath, reportProgress, token);
CancellationToken token = default) => Http.DownloadAsync(url, filePath, reportProgress, token);
public void AddActionKeyword(string pluginId, string newActionKeyword) =>
PluginManager.AddActionKeyword(pluginId, newActionKeyword);
@ -275,7 +286,7 @@ namespace Flow.Launcher
public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") =>
Log.Exception(className, message, e, methodName);
private readonly ConcurrentDictionary<Type, object> _pluginJsonStorages = new();
private readonly ConcurrentDictionary<Type, ISavable> _pluginJsonStorages = new();
public void RemovePluginSettings(string assemblyName)
{
@ -293,10 +304,9 @@ namespace Flow.Launcher
public void SavePluginSettings()
{
foreach (var value in _pluginJsonStorages.Values)
foreach (var savable in _pluginJsonStorages.Values)
{
var savable = value as ISavable;
savable?.Save();
savable.Save();
}
}
@ -398,13 +408,27 @@ namespace Flow.Launcher
var path = browserInfo.Path == "*" ? "" : browserInfo.Path;
if (browserInfo.OpenInTab)
try
{
uri.AbsoluteUri.OpenInBrowserTab(path, inPrivate ?? browserInfo.EnablePrivate, browserInfo.PrivateArg);
if (browserInfo.OpenInTab)
{
uri.AbsoluteUri.OpenInBrowserTab(path, inPrivate ?? browserInfo.EnablePrivate, browserInfo.PrivateArg);
}
else
{
uri.AbsoluteUri.OpenInBrowserWindow(path, inPrivate ?? browserInfo.EnablePrivate, browserInfo.PrivateArg);
}
}
else
catch (Exception e)
{
uri.AbsoluteUri.OpenInBrowserWindow(path, inPrivate ?? browserInfo.EnablePrivate, browserInfo.PrivateArg);
var tabOrWindow = browserInfo.OpenInTab ? "tab" : "window";
LogException(ClassName, $"Failed to open URL in browser {tabOrWindow}: {path}, {inPrivate ?? browserInfo.EnablePrivate}, {browserInfo.PrivateArg}", e);
ShowMsgBox(
GetTranslation("browserOpenError"),
GetTranslation("errorTitle"),
MessageBoxButton.OK,
MessageBoxImage.Error
);
}
}
else
@ -481,7 +505,7 @@ namespace Flow.Launcher
public bool SetCurrentTheme(ThemeData theme) =>
Theme.ChangeTheme(theme.FileNameWithoutExtension);
private readonly ConcurrentDictionary<(string, string, Type), object> _pluginBinaryStorages = new();
private readonly ConcurrentDictionary<(string, string, Type), ISavable> _pluginBinaryStorages = new();
public void RemovePluginCaches(string cacheDirectory)
{
@ -498,10 +522,9 @@ namespace Flow.Launcher
public void SavePluginCaches()
{
foreach (var value in _pluginBinaryStorages.Values)
foreach (var savable in _pluginBinaryStorages.Values)
{
var savable = value as ISavable;
savable?.Save();
savable.Save();
}
}

View file

@ -0,0 +1,227 @@
<Window
x:Class="Flow.Launcher.ReleaseNotesWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Flow.Launcher"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mdxam="clr-namespace:MdXaml;assembly=MdXaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
Title="{DynamicResource releaseNotes}"
Width="940"
Height="600"
MinWidth="940"
MinHeight="600"
Background="{DynamicResource PopuBGColor}"
Closed="Window_Closed"
Foreground="{DynamicResource PopupTextColor}"
Loaded="Window_Loaded"
ResizeMode="CanResize"
StateChanged="Window_StateChanged"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="Close" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Close" Executed="OnCloseExecuted" />
</Window.CommandBindings>
<Grid>
<Border Style="{StaticResource WindowMainPanelStyle}">
<Grid Background="{DynamicResource Color01B}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="24" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- TitleBar and Control -->
<Image
Grid.Row="0"
Grid.Column="0"
Width="16"
Height="16"
Margin="10 4 4 4"
RenderOptions.BitmapScalingMode="HighQuality"
Source="/Images/app.png" />
<TextBlock
Grid.Row="0"
Grid.Column="1"
Margin="4 0 0 0"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource Color05B}"
Text="{DynamicResource releaseNotes}" />
<Button
Grid.Row="0"
Grid.Column="2"
Click="OnMinimizeButtonClick"
RenderOptions.EdgeMode="Aliased"
Style="{DynamicResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,15 H 28"
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>
<Button
Name="MaximizeButton"
Grid.Row="0"
Grid.Column="3"
Click="OnMaximizeRestoreButtonClick"
Style="{StaticResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18.5,10.5 H 27.5 V 19.5 H 18.5 Z"
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>
<Button
Name="RestoreButton"
Grid.Row="0"
Grid.Column="3"
Click="OnMaximizeRestoreButtonClick"
Style="{StaticResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18.5,12.5 H 25.5 V 19.5 H 18.5 Z M 20.5,12.5 V 10.5 H 27.5 V 17.5 H 25.5"
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>
<Button
Grid.Row="0"
Grid.Column="4"
Click="OnCloseButtonClick"
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
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="5"
Margin="18 0 18 0">
<cc:HyperLink x:Name="SeeMore" Text="{DynamicResource seeMoreReleaseNotes}" />
</Grid>
<!-- Do not use scroll function of MarkdownViewer because it does not support smooth scroll -->
<ScrollViewer
x:Name="MarkdownScrollViewer"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="5"
Width="500"
Height="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<!-- This row is for bottom margin -->
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<mdxam:MarkdownScrollViewer
x:Name="MarkdownViewer"
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClickAction="SafetyDisplayWithRelativePath"
HorizontalScrollBarVisibility="Disabled"
Loaded="MarkdownViewer_Loaded"
MouseWheel="MarkdownViewer_MouseWheel"
Plugins="{StaticResource MdXamlPlugins}"
PreviewMouseWheel="MarkdownViewer_PreviewMouseWheel"
VerticalScrollBarVisibility="Disabled"
Visibility="Collapsed" />
</Grid>
</ScrollViewer>
<!-- This Grid is for display progress ring and refresh button. -->
<!-- And it is also for changing the size of the MarkdownViewer. -->
<!-- Because VerticalAlignment="Stretch" can cause size issue with MarkdownScrollViewer. -->
<Grid
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="5"
Margin="15 0 20 0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
SizeChanged="Grid_SizeChanged">
<ui:ProgressRing
x:Name="RefreshProgressRing"
Width="32"
Height="32"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsActive="True" />
<Button
x:Name="RefreshButton"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="RefreshButton_Click"
Content="{DynamicResource refresh}"
Visibility="Collapsed" />
</Grid>
</Grid>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.Http;
namespace Flow.Launcher
{
public partial class ReleaseNotesWindow : Window
{
private static readonly string ReleaseNotes = Properties.Settings.Default.GithubRepo + "/releases";
public ReleaseNotesWindow()
{
InitializeComponent();
SeeMore.Uri = ReleaseNotes;
ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged += ThemeManager_ActualApplicationThemeChanged;
}
#region Window Events
private void ThemeManager_ActualApplicationThemeChanged(ModernWpf.ThemeManager sender, object args)
{
Application.Current.Dispatcher.Invoke(() =>
{
if (ModernWpf.ThemeManager.Current.ActualApplicationTheme == ModernWpf.ApplicationTheme.Light)
{
MarkdownViewer.MarkdownStyle = (Style)Application.Current.Resources["DocumentStyleGithubLikeLight"];
MarkdownViewer.Foreground = Brushes.Black;
MarkdownViewer.Background = Brushes.White;
}
else
{
MarkdownViewer.MarkdownStyle = (Style)Application.Current.Resources["DocumentStyleGithubLikeDark"];
MarkdownViewer.Foreground = Brushes.White;
MarkdownViewer.Background = Brushes.Black;
}
});
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
RefreshMaximizeRestoreButton();
ThemeManager_ActualApplicationThemeChanged(null, null);
}
private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e)
{
Close();
}
private void Window_Closed(object sender, EventArgs e)
{
ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged;
}
#endregion
#region Window Custom TitleBar
private void OnMinimizeButtonClick(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}
private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)
{
WindowState = WindowState switch
{
WindowState.Maximized => WindowState.Normal,
_ => WindowState.Maximized
};
}
private void OnCloseButtonClick(object sender, RoutedEventArgs e)
{
Close();
}
private void RefreshMaximizeRestoreButton()
{
if (WindowState == WindowState.Maximized)
{
MaximizeButton.Visibility = Visibility.Hidden;
RestoreButton.Visibility = Visibility.Visible;
}
else
{
MaximizeButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Hidden;
}
}
private void Window_StateChanged(object sender, EventArgs e)
{
RefreshMaximizeRestoreButton();
}
#endregion
#region Control Events
private void MarkdownViewer_Loaded(object sender, RoutedEventArgs e)
{
RefreshMarkdownViewer();
}
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
RefreshButton.Visibility = Visibility.Collapsed;
RefreshProgressRing.Visibility = Visibility.Visible;
RefreshMarkdownViewer();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private async void RefreshMarkdownViewer()
{
var output = await GetReleaseNotesMarkdownAsync().ConfigureAwait(false);
Application.Current.Dispatcher.Invoke(() =>
{
RefreshProgressRing.Visibility = Visibility.Collapsed;
if (string.IsNullOrEmpty(output))
{
RefreshButton.Visibility = Visibility.Visible;
MarkdownViewer.Visibility = Visibility.Collapsed;
App.API.ShowMsgError(
App.API.GetTranslation("checkNetworkConnectionTitle"),
App.API.GetTranslation("checkNetworkConnectionSubTitle"));
}
else
{
RefreshButton.Visibility = Visibility.Collapsed;
MarkdownViewer.Markdown = output;
MarkdownViewer.Visibility = Visibility.Visible;
}
});
}
private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)
{
MarkdownScrollViewer.Height = e.NewSize.Height;
MarkdownScrollViewer.Width = e.NewSize.Width;
}
private void MarkdownViewer_MouseWheel(object sender, MouseWheelEventArgs e)
{
RaiseMouseWheelEvent(sender, e);
}
private void MarkdownViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
RaiseMouseWheelEvent(sender, e);
}
private void RaiseMouseWheelEvent(object sender, MouseWheelEventArgs e)
{
e.Handled = true; // Prevent the inner control from handling the event
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = sender
};
// Raise the event on the parent ScrollViewer
MarkdownScrollViewer.RaiseEvent(eventArg);
}
#endregion
#region Release Notes
private static async Task<string> GetReleaseNotesMarkdownAsync()
{
var releaseNotesJSON = await Http.GetStringAsync("https://api.github.com/repos/Flow-Launcher/Flow.Launcher/releases");
if (string.IsNullOrEmpty(releaseNotesJSON))
{
return string.Empty;
}
var releases = JsonSerializer.Deserialize<List<GitHubReleaseInfo>>(releaseNotesJSON);
// Get the latest releases
var latestReleases = releases.OrderByDescending(release => release.PublishedDate).Take(3);
// Build the release notes in Markdown format
var releaseNotesHtmlBuilder = new StringBuilder(string.Empty);
foreach (var release in latestReleases)
{
releaseNotesHtmlBuilder.AppendLine("# " + release.Name);
// Because MdXaml.Html package cannot correctly render images without units,
// We need to manually add unit for images
// E.g. Replace <img src="..." width="500"> with <img src="..." width="500px">
var notes = ImageUnitRegex().Replace(release.ReleaseNotes, m =>
{
var prefix = m.Groups[1].Value;
var widthValue = m.Groups[2].Value;
var quote = m.Groups[3].Value;
var suffix = m.Groups[4].Value;
// Only replace if width is number like 500 without units like 500px
if (IsNumber(widthValue))
return $"{prefix}{widthValue}px{quote}{suffix}";
return m.Value;
});
releaseNotesHtmlBuilder.AppendLine(notes);
releaseNotesHtmlBuilder.AppendLine();
}
return releaseNotesHtmlBuilder.ToString();
}
private static bool IsNumber(string input)
{
if (string.IsNullOrEmpty(input))
return false;
foreach (char c in input)
{
if (!char.IsDigit(c))
return false;
}
return true;
}
private sealed class GitHubReleaseInfo
{
[JsonPropertyName("published_at")]
public DateTimeOffset PublishedDate { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("tag_name")]
public string TagName { get; set; }
[JsonPropertyName("body")]
public string ReleaseNotes { get; set; }
}
[GeneratedRegex("(<img\\s+[^>]*width\\s*=\\s*[\"']?)(\\d+)([\"']?)([^>]*>)", RegexOptions.IgnoreCase, "en-GB")]
private static partial Regex ImageUnitRegex();
#endregion
}
}

View file

@ -36,7 +36,8 @@
Text="{DynamicResource actionKeywords}" />
<!-- Here Margin="0 -4.5 0 -4.5" is to remove redundant top bottom margin from Margin="{StaticResource SettingPanelMargin}" -->
<Button
Width="100"
Width="auto"
MinWidth="100"
Margin="0 -4.5 0 -4.5"
HorizontalAlignment="Right"
Command="{Binding SetActionKeywordsCommand}"

View file

@ -1,6 +1,10 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mdagif="clr-namespace:MdXaml.AnimatedGif;assembly=MdXaml.AnimatedGif"
xmlns:mdhtml="clr-namespace:MdXaml.Html;assembly=MdXaml.Html"
xmlns:mdplugins="clr-namespace:MdXaml.Plugins;assembly=MdXaml.Plugins"
xmlns:mdsvg="clr-namespace:MdXaml.Svg;assembly=MdXaml.Svg"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.modernwpf.com/2019">
@ -2407,6 +2411,84 @@
</Setter.Value>
</Setter>
</Style>
<!-- Explorer Plugin Expander -->
<Style x:Key="ExpanderHeaderRightArrowStyle" TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border
x:Name="RootBorder"
Padding="16 15 16 15"
Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter
Grid.Column="0"
Margin="8 0 0 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True"
SnapsToDevicePixels="True" />
<Grid
x:Name="ChevronGrid"
Grid.Column="1"
Width="20"
Height="20"
Margin="8 0 4 0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Background="Transparent"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<RotateTransform Angle="0" />
</Grid.RenderTransform>
<Ellipse
x:Name="circle"
Width="19"
Height="19"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stroke="Transparent" />
<Path
x:Name="arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 1,1.5 L 4.5,5 L 8,1.5"
SnapsToDevicePixels="False"
Stroke="#666"
StrokeThickness="1" />
</Grid>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="arrow" Property="Data" Value="M 1,4.5 L 4.5,1 L 8,4.5" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="RootBorder" Property="Background" Value="{DynamicResource CustomExpanderHover}" />
<Setter TargetName="circle" Property="Stroke" Value="Transparent" />
<Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color05B}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="circle" Property="Stroke" Value="Transparent" />
<Setter TargetName="circle" Property="StrokeThickness" Value="1.5" />
<Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color17B}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ExpanderStyle1" TargetType="{x:Type Expander}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="Background" Value="Transparent" />
@ -5681,4 +5763,340 @@
<Setter Property="Margin" Value="0 2 0 0" />
<Setter Property="Foreground" Value="{DynamicResource Color04B}" />
</Style>
<!-- Release Notes -->
<mdplugins:MdXamlPlugins x:Key="MdXamlPlugins">
<mdhtml:HtmlPluginSetup />
<mdsvg:SvgPluginSetup />
<mdagif:AnimatedGifPluginSetup />
</mdplugins:MdXamlPlugins>
<Style x:Key="DocumentStyleGithubLikeLight" TargetType="FlowDocument">
<Setter Property="FontFamily" Value="Calibri" />
<Setter Property="TextAlignment" Value="Left" />
<Setter Property="PagePadding" Value="0" />
<Setter Property="FontSize" Value="14" />
<Style.Resources>
<Style TargetType="Section">
<Style.Triggers>
<Trigger Property="Tag" Value="Blockquote">
<Setter Property="Padding" Value="10 5" />
<Setter Property="BorderBrush" Value="#DEDEDE" />
<Setter Property="BorderThickness" Value="5 0 0 0" />
</Trigger>
</Style.Triggers>
</Style>
<Style xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" TargetType="avalonEdit:TextEditor">
<Setter Property="Background" Value="#EEEEEE" />
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="Margin" Value="2 0 2 0" />
<Setter Property="Padding" Value="3" />
</Style>
<Style TargetType="Paragraph">
<Setter Property="Margin" Value="0 7 0 0" />
<Style.Triggers>
<Trigger Property="Tag" Value="Heading1">
<Setter Property="Margin" Value="0 0 15 0" />
<Setter Property="Foreground" Value="#ff000000" />
<Setter Property="FontSize" Value="28" />
<Setter Property="FontWeight" Value="UltraBold" />
</Trigger>
<Trigger Property="Tag" Value="Heading2">
<Setter Property="Margin" Value="0 0 15 0" />
<Setter Property="Foreground" Value="#ff000000" />
<Setter Property="FontSize" Value="21" />
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="Tag" Value="Heading3">
<Setter Property="Margin" Value="0 0 10 0" />
<Setter Property="Foreground" Value="#ff000000" />
<Setter Property="FontSize" Value="17.5" />
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="Tag" Value="Heading4">
<Setter Property="Margin" Value="0 0 5 0" />
<Setter Property="Foreground" Value="#ff000000" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="Tag" Value="CodeBlock">
<Setter Property="FontFamily" Value="Courier New" />
<Setter Property="FontSize" Value="11.9" />
<Setter Property="Background" Value="#12181F25" />
<Setter Property="Padding" Value="20 10" />
</Trigger>
<Trigger Property="Tag" Value="Note">
<Setter Property="Margin" Value="5 0 5 0" />
<Setter Property="Padding" Value="10 5" />
<Setter Property="BorderBrush" Value="#DEDEDE" />
<Setter Property="BorderThickness" Value="3 3 3 3" />
<Setter Property="Background" Value="#FAFAFA" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Run">
<Style.Triggers>
<Trigger Property="Tag" Value="CodeSpan">
<Setter Property="FontFamily" Value="Courier New" />
<Setter Property="FontSize" Value="11.9" />
<Setter Property="Background" Value="#12181F25" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Span">
<Style.Triggers>
<Trigger Property="Tag" Value="CodeSpan">
<Setter Property="FontFamily" Value="Courier New" />
<Setter Property="FontSize" Value="11.9" />
<Setter Property="Background" Value="#12181F25" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Hyperlink">
<Setter Property="TextDecorations" Value="None" />
</Style>
<Style TargetType="Image">
<Setter Property="RenderOptions.BitmapScalingMode" Value="NearestNeighbor" />
<Style.Triggers>
<Trigger Property="Tag" Value="imageright">
<Setter Property="Margin" Value="20 0 0 0" />
</Trigger>
</Style.Triggers>
</Style>
<!--
The Table's style don't seem to support border-collapse.
By making the ruled line width 0.5 and applying it to cell and table,
it looks like the ruled lines are not doubled.
-->
<Style TargetType="Table">
<Setter Property="CellSpacing" Value="0" />
<Setter Property="BorderThickness" Value="0.5" />
<Setter Property="BorderBrush" Value="#DFE2E5" />
<Style.Resources>
<Style TargetType="TableCell">
<Setter Property="BorderThickness" Value="0.5" />
<Setter Property="BorderBrush" Value="#DFE2E5" />
<Setter Property="Padding" Value="13 6" />
</Style>
</Style.Resources>
</Style>
<Style TargetType="TableRowGroup">
<Style.Triggers>
<Trigger Property="Tag" Value="TableHeader">
<Setter Property="FontWeight" Value="DemiBold" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Background" Value="#FFFFFF" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TableRow">
<Style.Triggers>
<Trigger Property="Tag" Value="EvenTableRow">
<Setter Property="Background" Value="#F6F8FA" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="BlockUIContainer">
<Style.Triggers>
<Trigger Property="Tag" Value="RuleSingle">
<Setter Property="Margin" Value="0 3" />
</Trigger>
<Trigger Property="Tag" Value="RuleDouble">
<Setter Property="Margin" Value="0 3" />
</Trigger>
<Trigger Property="Tag" Value="RuleBold">
<Setter Property="Margin" Value="0 3" />
</Trigger>
<Trigger Property="Tag" Value="RuleBoldWithSingle">
<Setter Property="Margin" Value="0 3" />
</Trigger>
</Style.Triggers>
</Style>
</Style.Resources>
</Style>
<Style x:Key="DocumentStyleGithubLikeDark" TargetType="FlowDocument">
<Setter Property="FontFamily" Value="Calibri" />
<Setter Property="TextAlignment" Value="Left" />
<Setter Property="PagePadding" Value="0" />
<Setter Property="FontSize" Value="14" />
<Style.Resources>
<Style TargetType="Section">
<Style.Triggers>
<Trigger Property="Tag" Value="Blockquote">
<Setter Property="Padding" Value="10 5" />
<Setter Property="BorderBrush" Value="#212121" />
<Setter Property="BorderThickness" Value="5 0 0 0" />
</Trigger>
</Style.Triggers>
</Style>
<Style xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit" TargetType="avalonEdit:TextEditor">
<Setter Property="Background" Value="#111111" />
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="Margin" Value="2 0 2 0" />
<Setter Property="Padding" Value="3" />
</Style>
<Style TargetType="Paragraph">
<Setter Property="Margin" Value="0 7 0 0" />
<Style.Triggers>
<Trigger Property="Tag" Value="Heading1">
<Setter Property="Margin" Value="0 0 15 0" />
<Setter Property="Foreground" Value="#FFFFFFFF" />
<Setter Property="FontSize" Value="28" />
<Setter Property="FontWeight" Value="UltraBold" />
</Trigger>
<Trigger Property="Tag" Value="Heading2">
<Setter Property="Margin" Value="0 0 15 0" />
<Setter Property="Foreground" Value="#FFFFFFFF" />
<Setter Property="FontSize" Value="21" />
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="Tag" Value="Heading3">
<Setter Property="Margin" Value="0 0 10 0" />
<Setter Property="Foreground" Value="#FFFFFFFF" />
<Setter Property="FontSize" Value="17.5" />
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="Tag" Value="Heading4">
<Setter Property="Margin" Value="0 0 5 0" />
<Setter Property="Foreground" Value="#FFFFFFFF" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="Tag" Value="CodeBlock">
<Setter Property="FontFamily" Value="Courier New" />
<Setter Property="FontSize" Value="11.9" />
<Setter Property="Background" Value="#E0E7DFDA" />
<Setter Property="Padding" Value="20 10" />
</Trigger>
<Trigger Property="Tag" Value="Note">
<Setter Property="Margin" Value="5 0 5 0" />
<Setter Property="Padding" Value="10 5" />
<Setter Property="BorderBrush" Value="#212121" />
<Setter Property="BorderThickness" Value="3 3 3 3" />
<Setter Property="Background" Value="#050505" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Run">
<Style.Triggers>
<Trigger Property="Tag" Value="CodeSpan">
<Setter Property="FontFamily" Value="Courier New" />
<Setter Property="FontSize" Value="11.9" />
<Setter Property="Background" Value="#E0E7DFDA" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Span">
<Style.Triggers>
<Trigger Property="Tag" Value="CodeSpan">
<Setter Property="FontFamily" Value="Courier New" />
<Setter Property="FontSize" Value="11.9" />
<Setter Property="Background" Value="#E0E7DFDA" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Hyperlink">
<Setter Property="TextDecorations" Value="None" />
</Style>
<Style TargetType="Image">
<Setter Property="RenderOptions.BitmapScalingMode" Value="NearestNeighbor" />
<Style.Triggers>
<Trigger Property="Tag" Value="imageright">
<Setter Property="Margin" Value="20 0 0 0" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Table">
<Setter Property="CellSpacing" Value="0" />
<Setter Property="BorderThickness" Value="0.5" />
<Setter Property="BorderBrush" Value="#202020" />
<Style.Resources>
<Style TargetType="TableCell">
<Setter Property="BorderThickness" Value="0.5" />
<Setter Property="BorderBrush" Value="#202020" />
<Setter Property="Padding" Value="13 6" />
</Style>
</Style.Resources>
</Style>
<Style TargetType="TableRowGroup">
<Style.Triggers>
<Trigger Property="Tag" Value="TableHeader">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Background" Value="#000000" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TableRow">
<Style.Triggers>
<Trigger Property="Tag" Value="EvenTableRow">
<Setter Property="Background" Value="#090708" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="BlockUIContainer">
<Style.Triggers>
<Trigger Property="Tag" Value="RuleSingle">
<Setter Property="Margin" Value="0 3" />
</Trigger>
<Trigger Property="Tag" Value="RuleDouble">
<Setter Property="Margin" Value="0 3" />
</Trigger>
<Trigger Property="Tag" Value="RuleBold">
<Setter Property="Margin" Value="0 3" />
</Trigger>
<Trigger Property="Tag" Value="RuleBoldWithSingle">
<Setter Property="Margin" Value="0 3" />
</Trigger>
</Style.Triggers>
</Style>
</Style.Resources>
</Style>
</ResourceDictionary>

View file

@ -103,7 +103,7 @@
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="250" />
<RowDefinition />
<RowDefinition Height="340"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" HorizontalAlignment="Stretch">
@ -140,7 +140,7 @@
</Border>
<Canvas Grid.Row="1" Height="288">
<Canvas Grid.Row="1" Height="338">
<Image
Name="wizard"
Canvas.Right="30"
@ -156,7 +156,7 @@
<TextBlock
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource Welcome_Page1_Title}" />
Text="{DynamicResource Welcome_Page1_Title}" TextWrapping="WrapWithOverflow"/>
<TextBlock
Margin="0 10 24 0"
FontSize="14"

View file

@ -38,7 +38,7 @@
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="250" />
<RowDefinition />
<RowDefinition Height="340"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" HorizontalAlignment="Stretch">
@ -89,12 +89,12 @@
</StackPanel>
</Border>
<StackPanel Grid.Row="1" Margin="24 20 24 20">
<StackPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Visible">
<StackPanel Margin="24 20 24 20">
<TextBlock
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource Welcome_Page2_Title}" />
Text="{DynamicResource Welcome_Page2_Title}" TextWrapping="WrapWithOverflow"/>
<TextBlock
Margin="0 10 0 0"
FontSize="14"
@ -119,7 +119,7 @@
WindowTitle="{DynamicResource flowlauncherHotkey}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</ScrollViewer>
</ui:Page>

View file

@ -93,7 +93,7 @@
<TextBlock
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource Welcome_Page4_Title}" />
Text="{DynamicResource Welcome_Page4_Title}" TextWrapping="WrapWithOverflow"/>
<TextBlock
Margin="0 10 0 10"
FontSize="14"

View file

@ -53,7 +53,7 @@
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="250" />
<RowDefinition />
<RowDefinition Height="340"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" HorizontalAlignment="Stretch">
@ -79,18 +79,18 @@
</StackPanel>
</Border>
<StackPanel Grid.Row="1" Margin="24 20 24 20">
<StackPanel Grid.Row="1" Margin="24 20 24 20" >
<StackPanel>
<TextBlock
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource Welcome_Page5_Title}" />
Text="{DynamicResource Welcome_Page5_Title}" TextWrapping="WrapWithOverflow"/>
<TextBlock
Margin="0 10 0 0"
FontSize="14"
Text="{DynamicResource Welcome_Page5_Text01}"
TextWrapping="WrapWithOverflow" />
<StackPanel Margin="0 30 0 0" Orientation="Horizontal">
<StackPanel Margin="0 20 0 0" Orientation="Horizontal">
<CheckBox
Checked="OnAutoStartupChecked"
Content="{DynamicResource startFlowLauncherOnSystemStartup}"
@ -109,7 +109,7 @@
<Button
Width="150"
Height="40"
Margin="0 60 0 0"
Margin="0 102 0 0"
HorizontalAlignment="Right"
Click="BtnCancel_OnClick"
Content="{DynamicResource done}"

View file

@ -6,7 +6,6 @@
<converters:BorderClipConverter x:Key="BorderClipConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:TextConverter x:Key="TextConverter" />
<core:TranslationConverter x:Key="TranslationConverter" />
<!-- Icon for Theme Type Label -->
<Geometry x:Key="circle_half_stroke_solid">F1 M512,512z M0,0z M448,256C448,150,362,64,256,64L256,448C362,448,448,362,448,256z M0,256A256,256,0,1,1,512,256A256,256,0,1,1,0,256z</Geometry>

View file

@ -314,4 +314,11 @@ public partial class SettingsPaneAboutViewModel : BaseModel
{
SettingWindowFont = Win32Helper.GetSystemDefaultFont(false);
}
[RelayCommand]
private void OpenReleaseNotes()
{
var releaseNotesWindow = new ReleaseNotesWindow();
releaseNotesWindow.Show();
}
}

View file

@ -253,6 +253,8 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
DropdownDataGeneric<SearchWindowAligns>.UpdateLabels(SearchWindowAligns);
DropdownDataGeneric<SearchPrecisionScore>.UpdateLabels(SearchPrecisionScores);
DropdownDataGeneric<LastQueryMode>.UpdateLabels(LastQueryModes);
// Since we are using Binding instead of DynamicResource, we need to manually trigger the update
OnPropertyChanged(nameof(AlwaysPreviewToolTip));
}
public string Language

View file

@ -55,7 +55,7 @@
</cc:Card>
<cc:Card Title="{DynamicResource releaseNotes}" Icon="&#xe8fd;">
<cc:HyperLink Text="{DynamicResource releaseNotes}" Uri="{Binding ReleaseNotes}" />
<Button Command="{Binding OpenReleaseNotesCommand}" Content="{DynamicResource releaseNotes}" />
</cc:Card>

View file

@ -88,6 +88,17 @@
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource showAtTopmost}"
Margin="0 14 0 0"
Icon="&#xE923;"
Sub="{DynamicResource showAtTopmostToolTip}">
<ui:ToggleSwitch
IsOn="{Binding Settings.ShowAtTopmost}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:CardGroup Margin="0 14 0 0">
<cc:Card Title="{DynamicResource SearchWindowPosition}" Icon="&#xe7f4;">
<StackPanel Orientation="Horizontal">
@ -165,7 +176,7 @@
Title="{DynamicResource AlwaysPreview}"
Margin="0 14 0 0"
Icon="&#xe8a1;"
Sub="{DynamicResource AlwaysPreviewToolTip}">
Sub="{Binding AlwaysPreviewToolTip}">
<ui:ToggleSwitch
IsOn="{Binding Settings.AlwaysPreview}"
OffContent="{DynamicResource disable}"
@ -363,7 +374,6 @@
Title="{DynamicResource KoreanImeTitle}"
Margin="0 14 0 0"
Closable="False"
DataContext="{Binding RelativeSource={RelativeSource AncestorType=Border}, Path=DataContext}"
IsIconVisible="True"
Length="Long"
Message="{DynamicResource KoreanImeGuide}"

View file

@ -427,7 +427,7 @@
<GridViewColumn Width="430" Header="{DynamicResource builtinShortcutDescription}">
<GridViewColumn.CellTemplate>
<DataTemplate DataType="{x:Type userSettings:BuiltinShortcutModel}">
<TextBlock Text="{Binding Description, Converter={StaticResource TranslationConverter}}" />
<TextBlock Text="{Binding LocalizedDescription}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>

View file

@ -108,6 +108,10 @@
<Style x:Key="BasePendingLineStyle" TargetType="{x:Type Line}">
<Setter Property="Stroke" Value="{StaticResource SystemAccentColorLight1Brush}" />
</Style>
<Style
x:Key="PendingLineStyle"
BasedOn="{StaticResource BasePendingLineStyle}"
TargetType="{x:Type Line}" />
<Style x:Key="BaseClockPanelPosition" TargetType="{x:Type Canvas}" />

View file

@ -10,11 +10,11 @@
Name="FlowWelcomeWindow"
Title="{DynamicResource Welcome_Page1_Title}"
Width="550"
Height="650"
Height="700"
MinWidth="550"
MinHeight="650"
MinHeight="700"
MaxWidth="550"
MaxHeight="650"
MaxHeight="700"
d:DataContext="{d:DesignInstance Type=vm:WelcomeViewModel}"
Activated="OnActivated"
Background="{DynamicResource Color00B}"

View file

@ -1,7 +1,9 @@
using System.Collections.Generic;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Microsoft.Data.Sqlite;
@ -43,16 +45,23 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to register bookmark file monitoring: {bookmarkPath}", ex);
continue;
}
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
var profileBookmarks = LoadBookmarksFromFile(bookmarkPath, source);
// Load favicons after loading bookmarks
var faviconDbPath = Path.Combine(profile, "Favicons");
if (File.Exists(faviconDbPath))
if (Main._settings.EnableFavicons)
{
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
var faviconDbPath = Path.Combine(profile, "Favicons");
if (File.Exists(faviconDbPath))
{
Main._context.API.StopwatchLogInfo(ClassName, $"Load {profileBookmarks.Count} favicons cost", () =>
{
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
});
}
}
bookmarks.AddRange(profileBookmarks);
@ -148,19 +157,24 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
try
{
using var connection = new SqliteConnection($"Data Source={tempDbPath}");
connection.Open();
// Since some bookmarks may have same favicon id, we need to record them to avoid duplicates
var savedPaths = new ConcurrentDictionary<string, bool>();
foreach (var bookmark in bookmarks)
// Get favicons based on bookmarks concurrently
Parallel.ForEach(bookmarks, bookmark =>
{
// Use read-only connection to avoid locking issues
var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly");
connection.Open();
try
{
var url = bookmark.Url;
if (string.IsNullOrEmpty(url)) continue;
if (string.IsNullOrEmpty(url)) return;
// Extract domain from URL
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
continue;
return;
var domain = uri.Host;
@ -178,16 +192,21 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
using var reader = cmd.ExecuteReader();
if (!reader.Read() || reader.IsDBNull(1))
continue;
return;
var iconId = reader.GetInt64(0).ToString();
var imageData = (byte[])reader["image_data"];
if (imageData is not { Length: > 0 })
continue;
return;
var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{domain}_{iconId}.png");
SaveBitmapData(imageData, faviconPath);
// Filter out duplicate favicons
if (savedPaths.TryAdd(faviconPath, true))
{
SaveBitmapData(imageData, faviconPath);
}
bookmark.FaviconPath = faviconPath;
}
@ -195,11 +214,14 @@ public abstract class ChromiumBookmarkLoader : IBookmarkLoader
{
Main._context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex);
}
}
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(connection);
connection.Close();
finally
{
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(connection);
connection.Close();
connection.Dispose();
}
});
}
catch (Exception ex)
{

View file

@ -1,7 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Microsoft.Data.Sqlite;
@ -30,8 +32,6 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
ORDER BY moz_places.visit_count DESC
""";
private const string DbPathFormat = "Data Source={0}";
protected List<Bookmark> GetBookmarksFromPath(string placesPath)
{
// Variable to store bookmark list
@ -41,30 +41,32 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath))
return bookmarks;
// Try to register file monitoring
try
{
Main.RegisterBookmarkFile(placesPath);
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to register Firefox bookmark file monitoring: {placesPath}", ex);
return bookmarks;
}
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempplaces_{Guid.NewGuid()}.sqlite");
try
{
// Try to register file monitoring
try
{
Main.RegisterBookmarkFile(placesPath);
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to register Firefox bookmark file monitoring: {placesPath}", ex);
}
// Use a copy to avoid lock issues with the original file
File.Copy(placesPath, tempDbPath, true);
// Connect to database and execute query
string dbPath = string.Format(DbPathFormat, tempDbPath);
using var dbConnection = new SqliteConnection(dbPath);
// Create the connection string and init the connection
using var dbConnection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly");
// Open connection to the database file and execute the query
dbConnection.Open();
var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader();
// Create bookmark list
// Get results in List<Bookmark> format
bookmarks = reader
.Select(
x => new Bookmark(
@ -75,12 +77,20 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
)
.ToList();
// Path to favicon database
var faviconDbPath = Path.Combine(Path.GetDirectoryName(placesPath), "favicons.sqlite");
if (File.Exists(faviconDbPath))
// Load favicons after loading bookmarks
if (Main._settings.EnableFavicons)
{
LoadFaviconsFromDb(faviconDbPath, bookmarks);
var faviconDbPath = Path.Combine(Path.GetDirectoryName(placesPath), "favicons.sqlite");
if (File.Exists(faviconDbPath))
{
Main._context.API.StopwatchLogInfo(ClassName, $"Load {bookmarks.Count} favicons cost", () =>
{
LoadFaviconsFromDb(faviconDbPath, bookmarks);
});
}
}
// Close the connection so that we can delete the temporary file
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(dbConnection);
dbConnection.Close();
@ -93,7 +103,10 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
// Delete temporary file
try
{
File.Delete(tempDbPath);
if (File.Exists(tempDbPath))
{
File.Delete(tempDbPath);
}
}
catch (Exception ex)
{
@ -103,34 +116,52 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
return bookmarks;
}
private void LoadFaviconsFromDb(string faviconDbPath, List<Bookmark> bookmarks)
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
{
// Use a copy to avoid lock issues with the original file
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.sqlite");
try
{
// Use a copy to avoid lock issues with the original file
File.Copy(faviconDbPath, tempDbPath, true);
var defaultIconPath = Path.Combine(
Path.GetDirectoryName(typeof(FirefoxBookmarkLoaderBase).Assembly.Location),
"bookmark.png");
string dbPath = string.Format(DbPathFormat, tempDbPath);
using var connection = new SqliteConnection(dbPath);
connection.Open();
// Get favicons based on bookmark URLs
foreach (var bookmark in bookmarks)
File.Copy(dbPath, tempDbPath, true);
}
catch (Exception ex)
{
try
{
if (File.Exists(tempDbPath))
{
File.Delete(tempDbPath);
}
}
catch (Exception ex1)
{
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex1);
}
Main._context.API.LogException(ClassName, $"Failed to copy favicon DB: {dbPath}", ex);
return;
}
try
{
// Since some bookmarks may have same favicon id, we need to record them to avoid duplicates
var savedPaths = new ConcurrentDictionary<string, bool>();
// Get favicons based on bookmarks concurrently
Parallel.ForEach(bookmarks, bookmark =>
{
// Use read-only connection to avoid locking issues
var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly");
connection.Open();
try
{
if (string.IsNullOrEmpty(bookmark.Url))
continue;
return;
// Extract domain from URL
if (!Uri.TryCreate(bookmark.Url, UriKind.Absolute, out Uri uri))
continue;
return;
var domain = uri.Host;
@ -150,12 +181,12 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
using var reader = cmd.ExecuteReader();
if (!reader.Read() || reader.IsDBNull(0))
continue;
return;
var imageData = (byte[])reader["data"];
if (imageData is not { Length: > 0 })
continue;
return;
string faviconPath;
if (IsSvgData(imageData))
@ -166,7 +197,12 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
{
faviconPath = Path.Combine(_faviconCacheDir, $"firefox_{domain}.png");
}
SaveBitmapData(imageData, faviconPath);
// Filter out duplicate favicons
if (savedPaths.TryAdd(faviconPath, true))
{
SaveBitmapData(imageData, faviconPath);
}
bookmark.FaviconPath = faviconPath;
}
@ -174,15 +210,18 @@ public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
{
Main._context.API.LogException(ClassName, $"Failed to extract Firefox favicon: {bookmark.Url}", ex);
}
}
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(connection);
connection.Close();
finally
{
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(connection);
connection.Close();
connection.Dispose();
}
});
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to load Firefox favicon DB: {faviconDbPath}", ex);
Main._context.API.LogException(ClassName, $"Failed to load Firefox favicon DB: {tempDbPath}", ex);
}
// Delete temporary file
@ -231,6 +270,7 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
/// <summary>
/// Path to places.sqlite
/// </summary>
/// <remarks></remarks>
private static string PlacesPath
{
get
@ -256,12 +296,50 @@ public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
var indexOfDefaultProfileAttributePath = lines.IndexOf("Path=" + defaultProfileFolderName);
/*
Current profiles.ini structure example as of Firefox version 69.0.1
[Install736426B0AF4A39CB]
Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile
Locked=1
[Profile2]
Name=newblahprofile
IsRelative=0
Path=C:\t6h2yuq8.newblahprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code.
[Profile1]
Name=default
IsRelative=1
Path=Profiles/cydum7q4.default
Default=1
[Profile0]
Name=default-release
IsRelative=1
Path=Profiles/7789f565.default-release
[General]
StartWithLastProfile=1
Version=2
*/
// Seen in the example above, the IsRelative attribute is always above the Path attribute
var relativePath = Path.Combine(defaultProfileFolderName, "places.sqlite");
var absoluePath = Path.Combine(profileFolderPath, relativePath);
// If the index is out of range, it means that the default profile is in a custom location or the file is malformed
// If the profile is in a custom location, we need to check
if (indexOfDefaultProfileAttributePath - 1 < 0 ||
indexOfDefaultProfileAttributePath - 1 >= lines.Count)
{
return Directory.Exists(absoluePath) ? absoluePath : relativePath;
}
var relativeAttribute = lines[indexOfDefaultProfileAttributePath - 1];
return relativeAttribute == "0" // See above, the profile is located in a custom location, path is not relative, so IsRelative=0
? defaultProfileFolderName + @"\places.sqlite"
: Path.Combine(profileFolderPath, defaultProfileFolderName) + @"\places.sqlite";
? relativePath : absoluePath;
}
}
}

View file

@ -27,4 +27,6 @@
<system:String x:Key="flowlauncher_plugin_browserbookmark_browserEngine">Browser Engine</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_guideMessage01">If you are not using Chrome, Firefox or Edge, or you are using their portable version, you need to add bookmarks data directory and select correct browser engine to make this plugin work.</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_guideMessage02">For example: Brave's engine is Chromium; and its default bookmarks data location is: "%LOCALAPPDATA%\BraveSoftware\Brave-Browser\UserData". For Firefox engine, the bookmarks directory is the userdata folder contains the places.sqlite file.</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_enable_favicons">Load favicons (can be time consuming during startup)</system:String>
</ResourceDictionary>

View file

@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading;
using System.Windows.Controls;
using Flow.Launcher.Plugin.BrowserBookmark.Commands;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Flow.Launcher.Plugin.BrowserBookmark.Views;
using System.IO;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.BrowserBookmark;
@ -21,9 +21,9 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
internal static PluginInitContext _context;
private static List<Bookmark> _cachedBookmarks = new();
internal static Settings _settings;
private static Settings _settings;
private static List<Bookmark> _cachedBookmarks = new();
private static bool _initialized = false;

View file

@ -8,6 +8,8 @@ public class Settings : BaseModel
public string BrowserPath { get; set; }
public bool EnableFavicons { get; set; } = false;
public bool LoadChromeBookmark { get; set; } = true;
public bool LoadFirefoxBookmark { get; set; } = true;
public bool LoadEdgeBookmark { get; set; } = true;

View file

@ -12,6 +12,7 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
@ -91,5 +92,12 @@
Content="{DynamicResource flowlauncher_plugin_browserbookmark_removeBrowserBookmark}" />
</StackPanel>
</StackPanel>
<CheckBox
Grid.Row="2"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_enable_favicons}"
IsChecked="{Binding Settings.EnableFavicons}" />
</Grid>
</UserControl>

View file

@ -75,7 +75,9 @@ namespace Flow.Launcher.Plugin.Explorer
{
Settings.QuickAccessLinks.Add(new AccessLink
{
Path = record.FullPath, Type = record.Type
Name = record.FullPath.GetPathName(),
Path = record.FullPath,
Type = record.Type
});
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess"),

View file

@ -46,6 +46,7 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Droplex" Version="1.7.0" />
<!-- Do not upgrade System.Data.OleDb since we are .Net7.0 -->
<PackageReference Include="System.Data.OleDb" Version="8.0.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
@ -53,8 +54,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>

View file

@ -0,0 +1,21 @@
using System;
using System.IO;
using System.Linq;
using Flow.Launcher.Plugin.Explorer.Search;
namespace Flow.Launcher.Plugin.Explorer.Helper;
public static class PathHelper
{
public static string GetPathName(this string selectedPath)
{
if (string.IsNullOrEmpty(selectedPath)) return string.Empty;
var path = selectedPath.EndsWith(Constants.DirectorySeparator) ? selectedPath[0..^1] : selectedPath;
if (path.EndsWith(':'))
return path[0..^1] + " Drive";
return path.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
.Last();
}
}

View file

@ -5,6 +5,8 @@
<!-- Dialogues -->
<system:String x:Key="plugin_explorer_make_selection_warning">Please make a selection first</system:String>
<system:String x:Key="plugin_explorer_quick_access_link_no_folder_selected">Please select a folder path.</system:String>
<system:String x:Key="plugin_explorer_quick_access_link_path_already_exists">Please choose a different name or folder path.</system:String>
<system:String x:Key="plugin_explorer_select_folder_link_warning">Please select a folder link</system:String>
<system:String x:Key="plugin_explorer_delete_folder_link">Are you sure you want to delete {0}?</system:String>
<system:String x:Key="plugin_explorer_deletefileconfirm">Are you sure you want to permanently delete this file?</system:String>
@ -27,6 +29,7 @@
<system:String x:Key="plugin_explorer_add">Add</system:String>
<system:String x:Key="plugin_explorer_generalsetting_header">General Setting</system:String>
<system:String x:Key="plugin_explorer_manageactionkeywords_header">Customise Action Keywords</system:String>
<system:String x:Key="plugin_explorer_manage_quick_access_links_header">Customise Quick Access</system:String>
<system:String x:Key="plugin_explorer_quickaccesslinks_header">Quick Access Links</system:String>
<system:String x:Key="plugin_explorer_everything_setting_header">Everything Setting</system:String>
<system:String x:Key="plugin_explorer_previewpanel_setting_header">Preview Panel</system:String>
@ -43,6 +46,7 @@
<system:String x:Key="plugin_explorer_shell_path">Shell Path</system:String>
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">Index Search Excluded Paths</system:String>
<system:String x:Key="plugin_explorer_use_location_as_working_dir">Use search result's location as the working directory of the executable</system:String>
<system:String x:Key="plugin_explorer_display_more_info_in_tooltip">Display more information like size and age in tooltips</system:String>
<system:String x:Key="plugin_explorer_default_open_in_file_manager">Hit Enter to open folder in Default File Manager</system:String>
<system:String x:Key="plugin_explorer_usewindowsindexfordirectorysearch">Use Index Search For Path Search</system:String>
<system:String x:Key="plugin_explorer_manageindexoptions">Indexing Options</system:String>
@ -79,6 +83,9 @@
<!-- Plugin Tooltip -->
<system:String x:Key="plugin_explorer_plugin_ToolTipOpenDirectory">Ctrl + Enter to open the directory</system:String>
<system:String x:Key="plugin_explorer_plugin_ToolTipOpenContainingFolder">Ctrl + Enter to open the containing folder</system:String>
<system:String x:Key="plugin_explorer_plugin_tooltip_more_info">{0}{4}Size: {1}{4}Date created: {2}{4}Date modified: {3}</system:String>
<system:String x:Key="plugin_explorer_plugin_tooltip_more_info_unknown">Unknown</system:String>
<system:String x:Key="plugin_explorer_plugin_tooltip_more_info_volume">{0}{3}Space free: {1}{3}Total size: {2}</system:String>
<!-- Context menu items -->
<system:String x:Key="plugin_explorer_copypath">Copy path</system:String>
@ -92,6 +99,7 @@
<system:String x:Key="plugin_explorer_deletefile_subtitle">Permanently delete current file</system:String>
<system:String x:Key="plugin_explorer_deletefolder_subtitle">Permanently delete current folder</system:String>
<system:String x:Key="plugin_explorer_path">Path:</system:String>
<system:String x:Key="plugin_explorer_name">Name:</system:String>
<system:String x:Key="plugin_explorer_deletefilefolder_subtitle">Delete the selected</system:String>
<system:String x:Key="plugin_explorer_runasdifferentuser">Run as different user</system:String>
<system:String x:Key="plugin_explorer_runasdifferentuser_subtitle">Run the selected using a different user account</system:String>
@ -162,6 +170,9 @@
<system:String x:Key="flowlauncher_plugin_everything_enable_content_search">Do you want to enable content search for Everything?</system:String>
<system:String x:Key="flowlauncher_plugin_everything_enable_content_search_tips">It can be very slow without index (which is only supported in Everything v1.5+)</system:String>
<system:String x:Key="flowlauncher_plugin_everything_not_found">Unable to find Everything.exe</system:String>
<system:String x:Key="flowlauncher_plugin_everything_install_issue">Failed to install Everything, please install it manually</system:String>
<!-- Native Context Menu -->
<system:String x:Key="plugin_explorer_native_context_menu_header">Native Context Menu</system:String>
<system:String x:Key="plugin_explorer_native_context_menu_display_context_menu">Display native context menu (experimental)</system:String>

View file

@ -35,6 +35,7 @@ namespace Flow.Launcher.Plugin.Explorer
Context = context;
Settings = context.API.LoadSettingJsonStorage<Settings>();
FillQuickAccessLinkNames();
viewModel = new SettingsViewModel(context, Settings);
@ -95,5 +96,17 @@ namespace Flow.Launcher.Plugin.Explorer
{
return Context.API.GetTranslation("plugin_explorer_plugin_description");
}
private void FillQuickAccessLinkNames()
{
// Legacy version does not have names for quick access links, so we fill them with the path name.
foreach (var link in Settings.QuickAccessLinks)
{
if (string.IsNullOrWhiteSpace(link.Name))
{
link.Name = link.Path.GetPathName();
}
}
}
}
}

View file

@ -11,6 +11,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
{
public class EverythingSearchManager : IIndexProvider, IContentIndexProvider, IPathIndexProvider
{
private static readonly string ClassName = nameof(EverythingSearchManager);
private Settings Settings { get; }
public EverythingSearchManager(Settings settings)
@ -42,19 +44,32 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
private async ValueTask<bool> ClickToInstallEverythingAsync(ActionContext _)
{
var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(Settings.EverythingInstalledPath, Main.Context.API);
if (installedPath == null)
try
{
Main.Context.API.ShowMsgError("Unable to find Everything.exe");
var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(Settings.EverythingInstalledPath, Main.Context.API);
if (installedPath == null)
{
Main.Context.API.ShowMsgError(Main.Context.API.GetTranslation("flowlauncher_plugin_everything_not_found"));
Main.Context.API.LogError(ClassName, "Unable to find Everything.exe");
return false;
}
Settings.EverythingInstalledPath = installedPath;
Process.Start(installedPath, "-startup");
return true;
}
// Sometimes Everything installation will fail because of permission issues or file not found issues
// Just let the user know that Everything is not installed properly and ask them to install it manually
catch (Exception e)
{
Main.Context.API.ShowMsgError(Main.Context.API.GetTranslation("flowlauncher_plugin_everything_install_issue"));
Main.Context.API.LogException(ClassName, "Failed to install Everything", e);
return false;
}
Settings.EverythingInstalledPath = installedPath;
Process.Start(installedPath, "-startup");
return true;
}
public async IAsyncEnumerable<SearchResult> SearchAsync(string search, [EnumeratorCancellation] CancellationToken token)

View file

@ -1,8 +1,4 @@
using System;
using System.Linq;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
{
public class AccessLink
{
@ -10,20 +6,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
public ResultType Type { get; set; } = ResultType.Folder;
[JsonIgnore]
public string Name
{
get
{
var path = Path.EndsWith(Constants.DirectorySeparator) ? Path[0..^1] : Path;
if (path.EndsWith(':'))
return path[0..^1] + " Drive";
return path.Split(new[] { System.IO.Path.DirectorySeparatorChar }, StringSplitOptions.None)
.Last();
}
}
public string Name { get; set; }
}
}

View file

@ -14,6 +14,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
public static class ResultManager
{
private static readonly string ClassName = nameof(ResultManager);
private static readonly string[] SizeUnits = { "B", "KB", "MB", "GB", "TB" };
private static PluginInitContext Context;
private static Settings Settings { get; set; }
@ -99,10 +101,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder),
TitleHighlightData = Context.API.FuzzySearch(query.Search, title).MatchData,
CopyText = path,
Preview = new Result.PreviewInfo
{
FilePath = path,
},
PreviewPanel = new Lazy<UserControl>(() => new PreviewPanel(Settings, path, ResultType.Folder)),
Action = c =>
{
if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt)
@ -163,7 +162,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
},
Score = score,
TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"),
SubTitleToolTip = path,
SubTitleToolTip = Settings.DisplayMoreInformationInToolTip ? GetFolderMoreInfoTooltip(path) : path,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed }
};
}
@ -184,6 +183,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
if (progressValue >= 90)
progressBarColor = "#da2626";
var tooltip = Settings.DisplayMoreInformationInToolTip
? GetVolumeMoreInfoTooltip(path, freespace, totalspace)
: path;
return new Result
{
Title = title,
@ -202,8 +205,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
OpenFolder(path);
return true;
},
TitleToolTip = path,
SubTitleToolTip = path,
TitleToolTip = tooltip,
SubTitleToolTip = tooltip,
ContextData = new SearchResult { Type = ResultType.Volume, FullPath = path, WindowsIndexed = windowsIndexed }
};
}
@ -269,7 +272,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search
bool isMedia = IsMedia(Path.GetExtension(filePath));
var title = Path.GetFileName(filePath);
/* Preview Detail */
var result = new Result
@ -287,7 +289,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
TitleHighlightData = Context.API.FuzzySearch(query.Search, title).MatchData,
Score = score,
CopyText = filePath,
PreviewPanel = new Lazy<UserControl>(() => new PreviewPanel(Settings, filePath)),
PreviewPanel = new Lazy<UserControl>(() => new PreviewPanel(Settings, filePath, ResultType.File)),
Action = c =>
{
if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt)
@ -318,7 +320,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return true;
},
TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"),
SubTitleToolTip = filePath,
SubTitleToolTip = Settings.DisplayMoreInformationInToolTip ? GetFileMoreInfoTooltip(filePath) : filePath,
ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed }
};
return result;
@ -349,7 +351,51 @@ namespace Flow.Launcher.Plugin.Explorer.Search
_ = Task.Run(() => EverythingApi.IncrementRunCounterAsync(fileOrFolder));
}
private static readonly string[] MediaExtensions = { ".jpg", ".png", ".avi", ".mkv", ".bmp", ".gif", ".wmv", ".mp3", ".flac", ".mp4" };
private static string GetFileMoreInfoTooltip(string filePath)
{
try
{
var fileSize = PreviewPanel.GetFileSize(filePath);
var fileCreatedAt = PreviewPanel.GetFileCreatedAt(filePath, Settings.PreviewPanelDateFormat, Settings.PreviewPanelTimeFormat, Settings.ShowFileAgeInPreviewPanel);
var fileModifiedAt = PreviewPanel.GetFileLastModifiedAt(filePath, Settings.PreviewPanelDateFormat, Settings.PreviewPanelTimeFormat, Settings.ShowFileAgeInPreviewPanel);
return string.Format(Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info"),
filePath, fileSize, fileCreatedAt, fileModifiedAt, Environment.NewLine);
}
catch (Exception e)
{
Context.API.LogException(ClassName, $"Failed to load tooltip for {filePath}", e);
return filePath;
}
}
private static string GetFolderMoreInfoTooltip(string folderPath)
{
try
{
var folderSize = PreviewPanel.GetFolderSize(folderPath);
var folderCreatedAt = PreviewPanel.GetFolderCreatedAt(folderPath, Settings.PreviewPanelDateFormat, Settings.PreviewPanelTimeFormat, Settings.ShowFileAgeInPreviewPanel);
var folderModifiedAt = PreviewPanel.GetFolderLastModifiedAt(folderPath, Settings.PreviewPanelDateFormat, Settings.PreviewPanelTimeFormat, Settings.ShowFileAgeInPreviewPanel);
return string.Format(Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info"),
folderPath, folderSize, folderCreatedAt, folderModifiedAt, Environment.NewLine);
}
catch (Exception e)
{
Context.API.LogException(ClassName, $"Failed to load tooltip for {folderPath}", e);
return folderPath;
}
}
private static string GetVolumeMoreInfoTooltip(string volumePath, string freespace, string totalspace)
{
return string.Format(Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_volume"),
volumePath, freespace, totalspace, Environment.NewLine);
}
private static readonly string[] MediaExtensions =
{
".jpg", ".png", ".avi", ".mkv", ".bmp", ".gif", ".wmv", ".mp3", ".flac", ".mp4",
".m4a", ".m4v", ".heic", ".mov", ".flv", ".webm"
};
}
public enum ResultType

View file

@ -37,6 +37,8 @@ namespace Flow.Launcher.Plugin.Explorer
public bool DefaultOpenFolderInFileManager { get; set; } = false;
public bool DisplayMoreInformationInToolTip { get; set; } = false;
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
public bool SearchActionKeywordEnabled { get; set; } = true;

View file

@ -24,6 +24,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
public string Description { get; private init; }
public string LocalizedDescription => Main.Context.API.GetTranslation(Description);
internal Settings.ActionKeyword KeywordProperty { get; }
private void OnPropertyChanged([CallerMemberName] string propertyName = "")

View file

@ -9,6 +9,7 @@ using System.Linq;
using System.Windows;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions;
@ -243,6 +244,21 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
"yyyy-MM-dd",
"yyyy-MM-dd ddd",
"yyyy-MM-dd, dddd",
"dd/MMM/yyyy",
"dd/MMM/yyyy ddd",
"dd/MMM/yyyy, dddd",
"dd-MMM-yyyy",
"dd-MMM-yyyy ddd",
"dd-MMM-yyyy, dddd",
"dd.MMM.yyyy",
"dd.MMM.yyyy ddd",
"dd.MMM.yyyy, dddd",
"MMM/dd/yyyy",
"MMM/dd/yyyy ddd",
"MMM/dd/yyyy, dddd",
"yyyy-MMM-dd",
"yyyy-MMM-dd ddd",
"yyyy-MMM-dd, dddd",
};
#endregion
@ -328,14 +344,10 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
}
[RelayCommand]
private void EditLink(object commandParameter)
private void EditIndexSearchExcludePaths()
{
var (selectedLink, collection) = commandParameter switch
{
"QuickAccessLink" => (SelectedQuickAccessLink, Settings.QuickAccessLinks),
"IndexSearchExcludedPaths" => (SelectedIndexSearchExcludedPath, Settings.IndexSearchExcludedSubdirectoryPaths),
_ => throw new ArgumentOutOfRangeException(nameof(commandParameter))
};
var selectedLink = SelectedIndexSearchExcludedPath;
var collection = Settings.IndexSearchExcludedSubdirectoryPaths;
if (selectedLink is null)
{
@ -354,28 +366,18 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
collection.Remove(selectedLink);
collection.Add(new AccessLink
{
Path = path, Type = selectedLink.Type,
Path = path, Type = selectedLink.Type, Name = path.GetPathName()
});
}
private void ShowUnselectedMessage()
{
var warning = Context.API.GetTranslation("plugin_explorer_make_selection_warning");
Context.API.ShowMsgBox(warning);
Save();
}
[RelayCommand]
private void AddLink(object commandParameter)
private void AddIndexSearchExcludePaths()
{
var container = commandParameter switch
{
"QuickAccessLink" => Settings.QuickAccessLinks,
"IndexSearchExcludedPaths" => Settings.IndexSearchExcludedSubdirectoryPaths,
_ => throw new ArgumentOutOfRangeException(nameof(commandParameter))
};
ArgumentNullException.ThrowIfNull(container);
var container = Settings.IndexSearchExcludedSubdirectoryPaths;
if (container is null) return;
var folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() != DialogResult.OK)
@ -383,16 +385,47 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
var newAccessLink = new AccessLink
{
Name = folderBrowserDialog.SelectedPath.GetPathName(),
Path = folderBrowserDialog.SelectedPath
};
container.Add(newAccessLink);
Save();
}
[RelayCommand]
private void RemoveLink(object obj)
private void EditQuickAccessLink()
{
if (obj is not string container) return;
var selectedLink = SelectedQuickAccessLink;
var collection = Settings.QuickAccessLinks;
if (selectedLink is null)
{
ShowUnselectedMessage();
return;
}
var quickAccessLinkSettings = new QuickAccessLinkSettings(collection, SelectedQuickAccessLink);
if (quickAccessLinkSettings.ShowDialog() == true)
{
Save();
}
}
[RelayCommand]
private void AddQuickAccessLink()
{
var quickAccessLinkSettings = new QuickAccessLinkSettings(Settings.QuickAccessLinks);
if (quickAccessLinkSettings.ShowDialog() == true)
{
Save();
}
}
[RelayCommand]
private void RemoveLink(object commandParameter)
{
if (commandParameter is not string container) return;
switch (container)
{
@ -407,10 +440,16 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
}
Save();
}
private void ShowUnselectedMessage()
{
var warning = Context.API.GetTranslation("plugin_explorer_make_selection_warning");
Context.API.ShowMsgBox(warning);
}
#endregion
private string? PromptUserSelectPath(ResultType type, string? initialDirectory = null)
private static string? PromptUserSelectPath(ResultType type, string? initialDirectory = null)
{
string? path = null;

View file

@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;
@ -11,28 +12,32 @@ using DragEventArgs = System.Windows.DragEventArgs;
namespace Flow.Launcher.Plugin.Explorer.Views
{
/// <summary>
/// Interaction logic for ExplorerSettings.xaml
/// </summary>
public partial class ExplorerSettings
{
private readonly SettingsViewModel viewModel;
private readonly SettingsViewModel _viewModel;
private readonly List<Expander> _expanders;
public ExplorerSettings(SettingsViewModel viewModel)
{
_viewModel = viewModel;
DataContext = viewModel;
InitializeComponent();
this.viewModel = viewModel;
DataContext = viewModel;
ActionKeywordModel.Init(viewModel.Settings);
lbxAccessLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
_expanders = new List<Expander>
{
GeneralSettingsExpander,
ContextMenuExpander,
PreviewPanelExpander,
EverythingExpander,
ActionKeywordsExpander,
QuickAccessExpander,
ExcludedPathsExpander
};
}
private void AccessLinkDragDrop(string containerName, DragEventArgs e)
@ -51,7 +56,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
Path = s
};
viewModel.AppendLink(containerName, newFolderLink);
_viewModel.AppendLink(containerName, newFolderLink);
}
}
}
@ -76,8 +81,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
if (tbFastSortWarning is not null)
{
tbFastSortWarning.Visibility = viewModel.FastSortWarningVisibility;
tbFastSortWarning.Text = viewModel.SortOptionWarningMessage;
tbFastSortWarning.Visibility = _viewModel.FastSortWarningVisibility;
tbFastSortWarning.Text = _viewModel.SortOptionWarningMessage;
}
}
private void LbxAccessLinks_OnDrop(object sender, DragEventArgs e)
@ -93,5 +98,49 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
e.Handled = e.Text.ToCharArray().Any(c => !char.IsDigit(c));
}
private void Expander_Expanded(object sender, RoutedEventArgs e)
{
if (sender is Expander expandedExpander)
{
// Ensure _expanders is not null and contains items
if (_expanders == null || !_expanders.Any()) return;
foreach (var expander in _expanders)
{
if (expander != null && expander != expandedExpander && expander.IsExpanded)
{
expander.IsExpanded = false;
}
}
}
}
private void lbxAccessLinks_Loaded(object sender, RoutedEventArgs e)
{
lbxAccessLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
}
private void lbxExcludedPaths_Loaded(object sender, RoutedEventArgs e)
{
lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
}
private void lbxAccessLinks_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (sender is not ListView listView) return;
if (listView.View is not GridView gView) return;
var workingWidth =
listView.ActualWidth - SystemParameters.VerticalScrollBarWidth; // take into account vertical scrollbar
if (workingWidth <= 0) return;
var col1 = 0.4;
var col2 = 0.6;
gView.Columns[0].Width = workingWidth * col1;
gView.Columns[1].Width = workingWidth * col2;
}
}
}

View file

@ -117,7 +117,7 @@
HorizontalAlignment="Right"
VerticalAlignment="Top"
Style="{DynamicResource PreviewItemSubTitleStyle}"
Text="{Binding FileSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
Text="{Binding FileSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Mode=OneWay}"
TextWrapping="Wrap"
Visibility="{Binding FileSizeVisibility, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />

View file

@ -2,7 +2,9 @@
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
@ -16,8 +18,10 @@ namespace Flow.Launcher.Plugin.Explorer.Views;
public partial class PreviewPanel : UserControl, INotifyPropertyChanged
{
private static readonly string ClassName = nameof(PreviewPanel);
private string FilePath { get; }
public string FileSize { get; } = "";
public string FileSize { get; private set; } = Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
public string CreatedAt { get; } = "";
public string LastModifiedAt { get; } = "";
private ImageSource _previewImage = new BitmapImage();
@ -50,7 +54,7 @@ public partial class PreviewPanel : UserControl, INotifyPropertyChanged
? Visibility.Visible
: Visibility.Collapsed;
public PreviewPanel(Settings settings, string filePath)
public PreviewPanel(Settings settings, string filePath, ResultType type)
{
InitializeComponent();
@ -60,33 +64,32 @@ public partial class PreviewPanel : UserControl, INotifyPropertyChanged
if (Settings.ShowFileSizeInPreviewPanel)
{
var fileSize = new FileInfo(filePath).Length;
FileSize = ResultManager.ToReadableSize(fileSize, 2);
if (type == ResultType.File)
{
FileSize = GetFileSize(filePath);
}
else
{
_ = Task.Run(() =>
{
FileSize = GetFolderSize(filePath);
OnPropertyChanged(nameof(FileSize));
}).ConfigureAwait(false);
}
}
if (Settings.ShowCreatedDateInPreviewPanel)
{
DateTime createdDate = File.GetCreationTime(filePath);
string formattedDate = createdDate.ToString(
$"{Settings.PreviewPanelDateFormat} {Settings.PreviewPanelTimeFormat}",
CultureInfo.CurrentCulture
);
string result = formattedDate;
if (Settings.ShowFileAgeInPreviewPanel) result = $"{GetFileAge(createdDate)} - {formattedDate}";
CreatedAt = result;
CreatedAt = type == ResultType.File ?
GetFileCreatedAt(filePath, Settings.PreviewPanelDateFormat, Settings.PreviewPanelTimeFormat, Settings.ShowFileAgeInPreviewPanel) :
GetFolderCreatedAt(filePath, Settings.PreviewPanelDateFormat, Settings.PreviewPanelTimeFormat, Settings.ShowFileAgeInPreviewPanel);
}
if (Settings.ShowModifiedDateInPreviewPanel)
{
DateTime lastModifiedDate = File.GetLastWriteTime(filePath);
string formattedDate = lastModifiedDate.ToString(
$"{Settings.PreviewPanelDateFormat} {Settings.PreviewPanelTimeFormat}",
CultureInfo.CurrentCulture
);
string result = formattedDate;
if (Settings.ShowFileAgeInPreviewPanel) result = $"{GetFileAge(lastModifiedDate)} - {formattedDate}";
LastModifiedAt = result;
LastModifiedAt = type == ResultType.File ?
GetFileLastModifiedAt(filePath, Settings.PreviewPanelDateFormat, Settings.PreviewPanelTimeFormat, Settings.ShowFileAgeInPreviewPanel) :
GetFolderLastModifiedAt(filePath, Settings.PreviewPanelDateFormat, Settings.PreviewPanelTimeFormat, Settings.ShowFileAgeInPreviewPanel);
}
_ = LoadImageAsync();
@ -96,7 +99,211 @@ public partial class PreviewPanel : UserControl, INotifyPropertyChanged
{
PreviewImage = await Main.Context.API.LoadImageAsync(FilePath, true).ConfigureAwait(false);
}
public static string GetFileSize(string filePath)
{
try
{
var fileInfo = new FileInfo(filePath);
return ResultManager.ToReadableSize(fileInfo.Length, 2);
}
catch (FileNotFoundException)
{
Main.Context.API.LogError(ClassName, $"File not found: {filePath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (UnauthorizedAccessException)
{
Main.Context.API.LogError(ClassName, $"Access denied to file: {filePath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (Exception e)
{
Main.Context.API.LogException(ClassName, $"Failed to get file size for {filePath}", e);
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
}
public static string GetFileCreatedAt(string filePath, string previewPanelDateFormat, string previewPanelTimeFormat, bool showFileAgeInPreviewPanel)
{
try
{
var createdDate = File.GetCreationTime(filePath);
var formattedDate = createdDate.ToString(
$"{previewPanelDateFormat} {previewPanelTimeFormat}",
CultureInfo.CurrentCulture
);
var result = formattedDate;
if (showFileAgeInPreviewPanel) result = $"{GetFileAge(createdDate)} - {formattedDate}";
return result;
}
catch (FileNotFoundException)
{
Main.Context.API.LogError(ClassName, $"File not found: {filePath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (UnauthorizedAccessException)
{
Main.Context.API.LogError(ClassName, $"Access denied to file: {filePath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (Exception e)
{
Main.Context.API.LogException(ClassName, $"Failed to get file created date for {filePath}", e);
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
}
public static string GetFileLastModifiedAt(string filePath, string previewPanelDateFormat, string previewPanelTimeFormat, bool showFileAgeInPreviewPanel)
{
try
{
var lastModifiedDate = File.GetLastWriteTime(filePath);
var formattedDate = lastModifiedDate.ToString(
$"{previewPanelDateFormat} {previewPanelTimeFormat}",
CultureInfo.CurrentCulture
);
var result = formattedDate;
if (showFileAgeInPreviewPanel) result = $"{GetFileAge(lastModifiedDate)} - {formattedDate}";
return result;
}
catch (FileNotFoundException)
{
Main.Context.API.LogError(ClassName, $"File not found: {filePath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (UnauthorizedAccessException)
{
Main.Context.API.LogError(ClassName, $"Access denied to file: {filePath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (Exception e)
{
Main.Context.API.LogException(ClassName, $"Failed to get file modified date for {filePath}", e);
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
}
public static string GetFolderSize(string folderPath)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
// Use parallel enumeration for better performance
var directoryInfo = new DirectoryInfo(folderPath);
long size = directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories)
.AsParallel()
.WithCancellation(timeoutCts.Token)
.Sum(file => file.Length);
return ResultManager.ToReadableSize(size, 2);
}
catch (FileNotFoundException)
{
Main.Context.API.LogError(ClassName, $"Folder not found: {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (UnauthorizedAccessException)
{
Main.Context.API.LogError(ClassName, $"Access denied to folder: {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (OperationCanceledException)
{
Main.Context.API.LogError(ClassName, $"Operation timed out while calculating folder size for {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
// For parallel operations, AggregateException may be thrown if any of the tasks fail
catch (AggregateException ae)
{
switch (ae.InnerException)
{
case FileNotFoundException:
Main.Context.API.LogError(ClassName, $"Folder not found: {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
case UnauthorizedAccessException:
Main.Context.API.LogError(ClassName, $"Access denied to folder: {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
case OperationCanceledException:
Main.Context.API.LogError(ClassName, $"Operation timed out while calculating folder size for {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
default:
Main.Context.API.LogException(ClassName, $"Failed to get folder size for {folderPath}", ae);
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
}
catch (Exception e)
{
Main.Context.API.LogException(ClassName, $"Failed to get folder size for {folderPath}", e);
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
}
public static string GetFolderCreatedAt(string folderPath, string previewPanelDateFormat, string previewPanelTimeFormat, bool showFileAgeInPreviewPanel)
{
try
{
var createdDate = Directory.GetCreationTime(folderPath);
var formattedDate = createdDate.ToString(
$"{previewPanelDateFormat} {previewPanelTimeFormat}",
CultureInfo.CurrentCulture
);
var result = formattedDate;
if (showFileAgeInPreviewPanel) result = $"{GetFileAge(createdDate)} - {formattedDate}";
return result;
}
catch (FileNotFoundException)
{
Main.Context.API.LogError(ClassName, $"Folder not found: {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (UnauthorizedAccessException)
{
Main.Context.API.LogError(ClassName, $"Access denied to folder: {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (Exception e)
{
Main.Context.API.LogException(ClassName, $"Failed to get folder created date for {folderPath}", e);
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
}
public static string GetFolderLastModifiedAt(string folderPath, string previewPanelDateFormat, string previewPanelTimeFormat, bool showFileAgeInPreviewPanel)
{
try
{
var lastModifiedDate = Directory.GetLastWriteTime(folderPath);
var formattedDate = lastModifiedDate.ToString(
$"{previewPanelDateFormat} {previewPanelTimeFormat}",
CultureInfo.CurrentCulture
);
var result = formattedDate;
if (showFileAgeInPreviewPanel) result = $"{GetFileAge(lastModifiedDate)} - {formattedDate}";
return result;
}
catch (FileNotFoundException)
{
Main.Context.API.LogError(ClassName, $"Folder not found: {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (UnauthorizedAccessException)
{
Main.Context.API.LogError(ClassName, $"Access denied to folder: {folderPath}");
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
catch (Exception e)
{
Main.Context.API.LogException(ClassName, $"Failed to get folder modified date for {folderPath}", e);
return Main.Context.API.GetTranslation("plugin_explorer_plugin_tooltip_more_info_unknown");
}
}
private static string GetFileAge(DateTime fileDateTime)
{
var now = DateTime.Now;

View file

@ -0,0 +1,137 @@
<Window x:Class="Flow.Launcher.Plugin.Explorer.Views.QuickAccessLinkSettings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{DynamicResource plugin_explorer_manage_quick_access_links_header}"
Width="Auto"
Height="255"
Background="{DynamicResource PopuBGColor}"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize"
SizeToContent="Width"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="80" />
</Grid.RowDefinitions>
<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 0 26 0">
<StackPanel Margin="0 0 0 12">
<TextBlock
Margin="0 0 0 0"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource plugin_explorer_manage_quick_access_links_header}"
TextAlignment="Left" />
</StackPanel>
<StackPanel Margin="0 10 0 0" Orientation="Horizontal">
<TextBlock
MinWidth="150"
Margin="0 10 15 10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource plugin_explorer_name}" />
<TextBox
Margin="10 0 0 0"
VerticalAlignment="Center"
FontSize="12"
Width="250"
Text="{Binding SelectedName, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="0 10 0 0" Orientation="Horizontal">
<TextBlock
MinWidth="150"
Margin="0 10 15 10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource plugin_explorer_path}" />
<TextBox
Margin="10 0 0 0"
VerticalAlignment="Center"
FontSize="12"
Width="250"
Text="{Binding SelectedPath, Mode=TwoWay}"
IsReadOnly="True" />
<Button
Width="80"
Height="Auto"
Margin="10 0 0 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{DynamicResource select}"
Click="SelectPath_OnClick" />
</StackPanel>
</StackPanel>
</StackPanel>
<Border
Grid.Row="1"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0 1 0 0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
x:Name="btnCancel"
Width="145"
Height="30"
Margin="0 0 5 0"
Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" />
<Button
Name="DownButton"
Width="145"
Height="30"
Margin="5 0 0 0"
Click="OnDoneButtonClick"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
</Button>
</StackPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,147 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Forms;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
namespace Flow.Launcher.Plugin.Explorer.Views;
public partial class QuickAccessLinkSettings : INotifyPropertyChanged
{
private string _selectedPath;
public string SelectedPath
{
get => _selectedPath;
set
{
if (_selectedPath != value)
{
_selectedPath = value;
OnPropertyChanged();
if (string.IsNullOrEmpty(_selectedName))
{
SelectedName = _selectedPath.GetPathName();
}
}
}
}
private string _selectedName;
public string SelectedName
{
get
{
return string.IsNullOrEmpty(_selectedName) ? _selectedPath.GetPathName() : _selectedName;
}
set
{
if (_selectedName != value)
{
_selectedName = value;
OnPropertyChanged();
}
}
}
private bool IsEdit { get; }
private AccessLink SelectedAccessLink { get; }
public ObservableCollection<AccessLink> QuickAccessLinks { get; }
public QuickAccessLinkSettings(ObservableCollection<AccessLink> quickAccessLinks)
{
IsEdit = false;
QuickAccessLinks = quickAccessLinks;
InitializeComponent();
}
public QuickAccessLinkSettings(ObservableCollection<AccessLink> quickAccessLinks, AccessLink selectedAccessLink)
{
IsEdit = true;
_selectedName = selectedAccessLink.Name;
_selectedPath = selectedAccessLink.Path;
SelectedAccessLink = selectedAccessLink;
QuickAccessLinks = quickAccessLinks;
InitializeComponent();
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void OnDoneButtonClick(object sender, RoutedEventArgs e)
{
// Validate the input before proceeding
if (string.IsNullOrEmpty(SelectedName) || string.IsNullOrEmpty(SelectedPath))
{
var warning = Main.Context.API.GetTranslation("plugin_explorer_quick_access_link_no_folder_selected");
Main.Context.API.ShowMsgBox(warning);
return;
}
// Check if the path already exists in the quick access links
if (QuickAccessLinks.Any(x =>
x.Path.Equals(SelectedPath, StringComparison.OrdinalIgnoreCase) &&
x.Name.Equals(SelectedName, StringComparison.OrdinalIgnoreCase)))
{
var warning = Main.Context.API.GetTranslation("plugin_explorer_quick_access_link_path_already_exists");
Main.Context.API.ShowMsgBox(warning);
return;
}
// If editing, update the existing link
if (IsEdit)
{
if (SelectedAccessLink == null) return;
var index = QuickAccessLinks.IndexOf(SelectedAccessLink);
if (index >= 0)
{
var updatedLink = new AccessLink
{
Name = SelectedName,
Type = SelectedAccessLink.Type,
Path = SelectedPath
};
QuickAccessLinks[index] = updatedLink;
}
DialogResult = true;
Close();
}
// Otherwise, add a new one
else
{
var newAccessLink = new AccessLink
{
Name = SelectedName,
Path = SelectedPath
};
QuickAccessLinks.Add(newAccessLink);
DialogResult = true;
Close();
}
}
private void SelectPath_OnClick(object commandParameter, RoutedEventArgs e)
{
var folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
return;
SelectedPath = folderBrowserDialog.SelectedPath;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View file

@ -316,61 +316,75 @@ namespace Flow.Launcher.Plugin.PluginsManager
var downloadToFilePath = Path.Combine(Path.GetTempPath(),
$"{x.Name}-{x.NewVersion}.zip");
_ = Task.Run(async delegate
_ = Task.Run(async () =>
{
using var cts = new CancellationTokenSource();
try
{
using var cts = new CancellationTokenSource();
if (!x.PluginNewUserPlugin.IsFromLocalInstallPath)
{
await DownloadFileAsync(
$"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {x.PluginNewUserPlugin.Name}",
x.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
}
else
{
downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath;
}
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
else
{
await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin,
downloadToFilePath);
if (Settings.AutoRestartAfterChanging)
if (!x.PluginNewUserPlugin.IsFromLocalInstallPath)
{
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation(
"plugin_pluginsmanager_update_success_restart"),
x.Name));
Context.API.RestartApp();
await DownloadFileAsync(
$"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {x.PluginNewUserPlugin.Name}",
x.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
}
else
{
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation(
"plugin_pluginsmanager_update_success_no_restart"),
x.Name));
downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath;
}
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
return;
}
else
{
await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin,
downloadToFilePath);
if (Settings.AutoRestartAfterChanging)
{
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation(
"plugin_pluginsmanager_update_success_restart"),
x.Name));
Context.API.RestartApp();
}
else
{
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation(
"plugin_pluginsmanager_update_success_no_restart"),
x.Name));
}
}
}
}).ContinueWith(t =>
{
Context.API.LogException(ClassName, $"Update failed for {x.Name}",
t.Exception.InnerException);
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
x.Name));
}, token, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
catch (HttpRequestException e)
{
// show error message
Context.API.ShowMsgError(
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), x.Name),
Context.API.GetTranslation("plugin_pluginsmanager_download_error"));
Context.API.LogException(ClassName, "An error occurred while downloading plugin", e);
return;
}
catch (Exception e)
{
// show error message
Context.API.LogException(ClassName, $"Update failed for {x.Name}", e);
Context.API.ShowMsgError(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
x.Name));
return;
}
});
return true;
},
@ -436,7 +450,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
catch (Exception ex)
{
Context.API.LogException(ClassName, $"Update failed for {plugin.Name}", ex.InnerException);
Context.API.ShowMsg(
Context.API.ShowMsgError(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),

View file

@ -8,9 +8,8 @@ using Windows.Win32.Storage.FileSystem;
namespace Flow.Launcher.Plugin.Program.Programs
{
class ShellLinkHelper
public class ShellLinkHelper
{
// Reference : http://www.pinvoke.net/default.aspx/Interfaces.IShellLinkW
[ComImport(), Guid("00021401-0000-0000-C000-000000000046")]
public class ShellLink
@ -28,7 +27,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
const int STGM_READ = 0;
((IPersistFile)link).Load(path, STGM_READ);
var hwnd = new HWND(IntPtr.Zero);
((IShellLinkW)link).Resolve(hwnd, 0);
// Use SLR_NO_UI to avoid showing any UI during resolution, like Problem with Shortcut dialogs
// https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinka-resolve
((IShellLinkW)link).Resolve(hwnd, (uint)SLR_FLAGS.SLR_NO_UI);
const int MAX_PATH = 260;
Span<char> buffer = stackalloc char[MAX_PATH];
@ -79,6 +80,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
Marshal.ReleaseComObject(link);
return target;
}
}
}
}

View file

@ -201,94 +201,101 @@ namespace Flow.Launcher.Plugin.Shell
switch (_settings.Shell)
{
case Shell.Cmd:
{
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("cmd");
}
else
{
info.FileName = "cmd.exe";
}
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("cmd");
}
else
{
info.FileName = "cmd.exe";
}
info.ArgumentList.Add($"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}");
break;
}
info.ArgumentList.Add($"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}");
break;
}
case Shell.Powershell:
{
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("powershell");
// Using just a ; doesn't work with wt, as it's used to create a new tab for the terminal window
// \\ must be escaped for it to work properly, or breaking it into multiple arguments
var addedCharacter = _settings.UseWindowsTerminal ? "\\" : "";
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("powershell");
}
else
{
info.FileName = "powershell.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
info.ArgumentList.Add(command);
}
else
{
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}{addedCharacter}; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'{addedCharacter}; [System.Console]::ReadKey(){addedCharacter}; exit" : "")}");
}
break;
}
else
{
info.FileName = "powershell.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
info.ArgumentList.Add(command);
}
else
{
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
}
break;
}
case Shell.Pwsh:
{
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("pwsh");
// Using just a ; doesn't work with wt, as it's used to create a new tab for the terminal window
// \\ must be escaped for it to work properly, or breaking it into multiple arguments
var addedCharacter = _settings.UseWindowsTerminal ? "\\" : "";
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("pwsh");
}
else
{
info.FileName = "pwsh.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
}
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}{addedCharacter}; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'{addedCharacter}; [System.Console]::ReadKey(){addedCharacter}; exit" : "")}");
break;
}
else
{
info.FileName = "pwsh.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
}
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{Context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
break;
}
case Shell.RunCommand:
{
var parts = command.Split(new[]
{
' '
}, 2);
if (parts.Length == 2)
{
var filename = parts[0];
if (ExistInPath(filename))
var parts = command.Split(new[]
{
var arguments = parts[1];
info.FileName = filename;
info.ArgumentList.Add(arguments);
' '
}, 2);
if (parts.Length == 2)
{
var filename = parts[0];
if (ExistInPath(filename))
{
var arguments = parts[1];
info.FileName = filename;
info.ArgumentList.Add(arguments);
}
else
{
info.FileName = command;
}
}
else
{
info.FileName = command;
}
}
else
{
info.FileName = command;
info.UseShellExecute = true;
break;
}
info.UseShellExecute = true;
break;
}
default:
throw new NotImplementedException();
}

View file

@ -375,6 +375,10 @@ Yes please, let us know in the [Q&A](https://github.com/Flow-Launcher/Flow.Launc
## Development
### Localization
Our project localization is based on [Crowdin](https://crowdin.com). If you would like to change them, please go to https://crowdin.com/project/flow-launcher.
### New changes
All changes to flow are captured via pull requests. Some new changes will have been merged but still pending release, this means whilst a change may not exist in the current release, it may very well have been accepted and merged into the dev branch and available as a pre-release download. It is therefore a good idea that before you start to make changes, search through the open and closed pull requests to make sure the change you intend to make is not already done.