mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into administrator_mode
This commit is contained in:
commit
2c1f1617cd
72 changed files with 3536 additions and 1253 deletions
2
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
|
|
@ -16,6 +16,8 @@ body:
|
|||
I have checked that this issue has not already been reported.
|
||||
- label: >
|
||||
I am using the latest version of Flow Launcher.
|
||||
- label: >
|
||||
I am using the prerelease version of Flow Launcher.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
|
|
|||
35
.github/update_release_pr.py
vendored
35
.github/update_release_pr.py
vendored
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
85
.github/workflows/default_plugins.yml
vendored
85
.github/workflows/default_plugins.yml
vendored
|
|
@ -3,11 +3,10 @@ name: Publish Default Plugins
|
|||
on:
|
||||
push:
|
||||
branches: ['master']
|
||||
paths: ['Plugins/**']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
publish:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
|
|
@ -17,39 +16,24 @@ jobs:
|
|||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Determine New Plugin Updates
|
||||
uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
browserbookmark:
|
||||
- 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json'
|
||||
calculator:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Calculator/plugin.json'
|
||||
explorer:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Explorer/plugin.json'
|
||||
pluginindicator:
|
||||
- 'Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json'
|
||||
pluginsmanager:
|
||||
- 'Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json'
|
||||
processkiller:
|
||||
- 'Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json'
|
||||
program:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Program/plugin.json'
|
||||
shell:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Shell/plugin.json'
|
||||
sys:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Sys/plugin.json'
|
||||
url:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Url/plugin.json'
|
||||
websearch:
|
||||
- 'Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json'
|
||||
windowssettings:
|
||||
- 'Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json'
|
||||
base: 'master'
|
||||
- name: Update Plugins To Production Version
|
||||
run: |
|
||||
$version = "1.0.0"
|
||||
Get-Content appveyor.yml | ForEach-Object {
|
||||
if ($_ -match "version:\s*'(\d+\.\d+\.\d+)\.") {
|
||||
$version = $matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
$jsonFiles = Get-ChildItem -Path ".\Plugins\*\plugin.json"
|
||||
foreach ($file in $jsonFiles) {
|
||||
$plugin_old_ver = Get-Content $file.FullName -Raw | ConvertFrom-Json
|
||||
(Get-Content $file) -replace '"Version"\s*:\s*".*?"', "`"Version`": `"$version`"" | Set-Content $file
|
||||
$plugin_new_ver = Get-Content $file.FullName -Raw | ConvertFrom-Json
|
||||
Write-Host "Updated" $plugin_old_ver.Name "version from" $plugin_old_ver.Version "to" $plugin_new_ver.Version
|
||||
}
|
||||
|
||||
- name: Get BrowserBookmark Version
|
||||
if: steps.changes.outputs.browserbookmark == 'true'
|
||||
id: updated-version-browserbookmark
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -57,14 +41,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build BrowserBookmark
|
||||
if: steps.changes.outputs.browserbookmark == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
|
||||
7z a -tzip "Flow.Launcher.Plugin.BrowserBookmark.zip" "./Flow.Launcher.Plugin.BrowserBookmark/*"
|
||||
rm -r "Flow.Launcher.Plugin.BrowserBookmark"
|
||||
|
||||
- name: Publish BrowserBookmark
|
||||
if: steps.changes.outputs.browserbookmark == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.BrowserBookmark"
|
||||
|
|
@ -76,7 +58,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Calculator Version
|
||||
if: steps.changes.outputs.calculator == 'true'
|
||||
id: updated-version-calculator
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -84,14 +65,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Calculator
|
||||
if: steps.changes.outputs.calculator == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Calculator.zip" "./Flow.Launcher.Plugin.Calculator/*"
|
||||
rm -r "Flow.Launcher.Plugin.Calculator"
|
||||
|
||||
- name: Publish Calculator
|
||||
if: steps.changes.outputs.calculator == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Calculator"
|
||||
|
|
@ -103,7 +82,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Explorer Version
|
||||
if: steps.changes.outputs.explorer == 'true'
|
||||
id: updated-version-explorer
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -111,14 +89,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Explorer
|
||||
if: steps.changes.outputs.explorer == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Explorer.zip" "./Flow.Launcher.Plugin.Explorer/*"
|
||||
rm -r "Flow.Launcher.Plugin.Explorer"
|
||||
|
||||
- name: Publish Explorer
|
||||
if: steps.changes.outputs.explorer == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Explorer"
|
||||
|
|
@ -130,7 +106,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get PluginIndicator Version
|
||||
if: steps.changes.outputs.pluginindicator == 'true'
|
||||
id: updated-version-pluginindicator
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -138,14 +113,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build PluginIndicator
|
||||
if: steps.changes.outputs.pluginindicator == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
|
||||
7z a -tzip "Flow.Launcher.Plugin.PluginIndicator.zip" "./Flow.Launcher.Plugin.PluginIndicator/*"
|
||||
rm -r "Flow.Launcher.Plugin.PluginIndicator"
|
||||
|
||||
- name: Publish PluginIndicator
|
||||
if: steps.changes.outputs.pluginindicator == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginIndicator"
|
||||
|
|
@ -157,7 +130,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get PluginsManager Version
|
||||
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||
id: updated-version-pluginsmanager
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -165,14 +137,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build PluginsManager
|
||||
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
|
||||
7z a -tzip "Flow.Launcher.Plugin.PluginsManager.zip" "./Flow.Launcher.Plugin.PluginsManager/*"
|
||||
rm -r "Flow.Launcher.Plugin.PluginsManager"
|
||||
|
||||
- name: Publish PluginsManager
|
||||
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginsManager"
|
||||
|
|
@ -184,7 +154,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get ProcessKiller Version
|
||||
if: steps.changes.outputs.processkiller == 'true'
|
||||
id: updated-version-processkiller
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -192,14 +161,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build ProcessKiller
|
||||
if: steps.changes.outputs.processkiller == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
|
||||
7z a -tzip "Flow.Launcher.Plugin.ProcessKiller.zip" "./Flow.Launcher.Plugin.ProcessKiller/*"
|
||||
rm -r "Flow.Launcher.Plugin.ProcessKiller"
|
||||
|
||||
- name: Publish ProcessKiller
|
||||
if: steps.changes.outputs.processkiller == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller"
|
||||
|
|
@ -211,7 +178,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Program Version
|
||||
if: steps.changes.outputs.program == 'true'
|
||||
id: updated-version-program
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -219,14 +185,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Program
|
||||
if: steps.changes.outputs.program == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net7.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Program.zip" "./Flow.Launcher.Plugin.Program/*"
|
||||
rm -r "Flow.Launcher.Plugin.Program"
|
||||
|
||||
- name: Publish Program
|
||||
if: steps.changes.outputs.program == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Program"
|
||||
|
|
@ -238,7 +202,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Shell Version
|
||||
if: steps.changes.outputs.shell == 'true'
|
||||
id: updated-version-shell
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -246,14 +209,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Shell
|
||||
if: steps.changes.outputs.shell == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Shell.zip" "./Flow.Launcher.Plugin.Shell/*"
|
||||
rm -r "Flow.Launcher.Plugin.Shell"
|
||||
|
||||
- name: Publish Shell
|
||||
if: steps.changes.outputs.shell == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Shell"
|
||||
|
|
@ -265,7 +226,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Sys Version
|
||||
if: steps.changes.outputs.sys == 'true'
|
||||
id: updated-version-sys
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -273,14 +233,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Sys
|
||||
if: steps.changes.outputs.sys == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Sys.zip" "./Flow.Launcher.Plugin.Sys/*"
|
||||
rm -r "Flow.Launcher.Plugin.Sys"
|
||||
|
||||
- name: Publish Sys
|
||||
if: steps.changes.outputs.sys == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Sys"
|
||||
|
|
@ -292,7 +250,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Url Version
|
||||
if: steps.changes.outputs.url == 'true'
|
||||
id: updated-version-url
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -300,14 +257,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Url
|
||||
if: steps.changes.outputs.url == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Url.zip" "./Flow.Launcher.Plugin.Url/*"
|
||||
rm -r "Flow.Launcher.Plugin.Url"
|
||||
|
||||
- name: Publish Url
|
||||
if: steps.changes.outputs.url == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Url"
|
||||
|
|
@ -319,7 +274,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get WebSearch Version
|
||||
if: steps.changes.outputs.websearch == 'true'
|
||||
id: updated-version-websearch
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -327,14 +281,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build WebSearch
|
||||
if: steps.changes.outputs.websearch == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
|
||||
7z a -tzip "Flow.Launcher.Plugin.WebSearch.zip" "./Flow.Launcher.Plugin.WebSearch/*"
|
||||
rm -r "Flow.Launcher.Plugin.WebSearch"
|
||||
|
||||
- name: Publish WebSearch
|
||||
if: steps.changes.outputs.websearch == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.WebSearch"
|
||||
|
|
@ -346,7 +298,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get WindowsSettings Version
|
||||
if: steps.changes.outputs.windowssettings == 'true'
|
||||
id: updated-version-windowssettings
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -354,14 +305,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build WindowsSettings
|
||||
if: steps.changes.outputs.windowssettings == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
|
||||
7z a -tzip "Flow.Launcher.Plugin.WindowsSettings.zip" "./Flow.Launcher.Plugin.WindowsSettings/*"
|
||||
rm -r "Flow.Launcher.Plugin.WindowsSettings"
|
||||
|
||||
- name: Publish WindowsSettings
|
||||
if: steps.changes.outputs.windowssettings == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.WindowsSettings"
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ MONITORINFOEXW
|
|||
|
||||
WM_ENTERSIZEMOVE
|
||||
WM_EXITSIZEMOVE
|
||||
WM_NCLBUTTONDBLCLK
|
||||
WM_SYSCOMMAND
|
||||
|
||||
SC_MAXIMIZE
|
||||
SC_MINIMIZE
|
||||
|
||||
OleInitialize
|
||||
OleUninitialize
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ----------------------------------------------------");
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
84
Flow.Launcher/MsgWithButton.xaml
Normal file
84
Flow.Launcher/MsgWithButton.xaml
Normal 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>
|
||||
95
Flow.Launcher/MsgWithButton.xaml.cs
Normal file
95
Flow.Launcher/MsgWithButton.xaml.cs
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
227
Flow.Launcher/ReleaseNotesWindow.xaml
Normal file
227
Flow.Launcher/ReleaseNotesWindow.xaml
Normal 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>
|
||||
255
Flow.Launcher/ReleaseNotesWindow.xaml.cs
Normal file
255
Flow.Launcher/ReleaseNotesWindow.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -314,4 +314,11 @@ public partial class SettingsPaneAboutViewModel : BaseModel
|
|||
{
|
||||
SettingWindowFont = Win32Helper.GetSystemDefaultFont(false);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenReleaseNotes()
|
||||
{
|
||||
var releaseNotesWindow = new ReleaseNotesWindow();
|
||||
releaseNotesWindow.Show();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
</cc:Card>
|
||||
|
||||
<cc:Card Title="{DynamicResource releaseNotes}" Icon="">
|
||||
<cc:HyperLink Text="{DynamicResource releaseNotes}" Uri="{Binding ReleaseNotes}" />
|
||||
<Button Command="{Binding OpenReleaseNotesCommand}" Content="{DynamicResource releaseNotes}" />
|
||||
</cc:Card>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,17 @@
|
|||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
|
||||
<cc:Card
|
||||
Title="{DynamicResource showAtTopmost}"
|
||||
Margin="0 14 0 0"
|
||||
Icon=""
|
||||
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="">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
|
|
@ -165,7 +176,7 @@
|
|||
Title="{DynamicResource AlwaysPreview}"
|
||||
Margin="0 14 0 0"
|
||||
Icon=""
|
||||
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}"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
21
Plugins/Flow.Launcher.Plugin.Explorer/Helper/PathHelper.cs
Normal file
21
Plugins/Flow.Launcher.Plugin.Explorer/Helper/PathHelper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = "")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue