mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into avalonia_migration
Some checks are pending
Build / build (push) Waiting to run
Some checks are pending
Build / build (push) Waiting to run
Resolved conflicts: - Calculator.csproj: Keep Mages 3.0.1 (from dev) + Avalonia packages - Explorer SettingsViewModel.cs: Use PromptUserSelectFileAsync for ShellPath - ExplorerSettings.xaml: Removed (deleted in HEAD) - packages.lock.json: Restored from dev
This commit is contained in:
commit
c51c029263
199 changed files with 5721 additions and 10458 deletions
2
.github/workflows/default_plugins.yml
vendored
2
.github/workflows/default_plugins.yml
vendored
|
|
@ -10,7 +10,7 @@ jobs:
|
|||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
|
|
|
|||
12
.github/workflows/dotnet.yml
vendored
12
.github/workflows/dotnet.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
NUGET_CERT_REVOCATION_MODE: offline
|
||||
BUILD_NUMBER: ${{ github.run_number }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set Flow.Launcher.csproj version
|
||||
id: update
|
||||
uses: vers-one/dotnet-project-version-updater@v1.7
|
||||
|
|
@ -56,28 +56,28 @@ jobs:
|
|||
shell: powershell
|
||||
run: .\Scripts\post_build.ps1
|
||||
- name: Upload Plugin Nupkg
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Plugin nupkg
|
||||
path: |
|
||||
Output\Release\Flow.Launcher.Plugin.*.nupkg
|
||||
compression-level: 0
|
||||
- name: Upload Setup
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Flow Installer
|
||||
path: |
|
||||
Output\Packages\Flow-Launcher-*.exe
|
||||
compression-level: 0
|
||||
- name: Upload Portable Version
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Portable Version
|
||||
path: |
|
||||
Output\Packages\Flow-Launcher-Portable.zip
|
||||
compression-level: 0
|
||||
- name: Upload Full Nupkg
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Full nupkg
|
||||
path: |
|
||||
|
|
@ -85,7 +85,7 @@ jobs:
|
|||
|
||||
compression-level: 0
|
||||
- name: Upload Release Information
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: RELEASES
|
||||
path: |
|
||||
|
|
|
|||
2
.github/workflows/pr_assignee.yml
vendored
2
.github/workflows/pr_assignee.yml
vendored
|
|
@ -14,4 +14,4 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assign PR to creator
|
||||
uses: toshimaru/auto-author-assign@v2.1.1
|
||||
uses: toshimaru/auto-author-assign@v3.0.1
|
||||
|
|
|
|||
2
.github/workflows/release_pr.yml
vendored
2
.github/workflows/release_pr.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
update-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||
<!-- Work around https://github.com/dotnet/runtime/issues/109682 -->
|
||||
<!-- Workaround https://github.com/dotnet/runtime/issues/109682 -->
|
||||
<CETCompat>false</CETCompat>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -3,10 +3,8 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Microsoft.Win32;
|
||||
using Squirrel;
|
||||
|
|
@ -17,8 +15,6 @@ namespace Flow.Launcher.Core.Configuration
|
|||
{
|
||||
private static readonly string ClassName = nameof(Portable);
|
||||
|
||||
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
||||
/// </summary>
|
||||
|
|
@ -45,13 +41,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
#endif
|
||||
IndicateDeletion(DataLocation.PortableDataPath);
|
||||
|
||||
API.ShowMsgBox(API.GetTranslation("restartToDisablePortableMode"));
|
||||
PublicApi.Instance.ShowMsgBox(Localize.restartToDisablePortableMode());
|
||||
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Error occurred while disabling portable mode", e);
|
||||
PublicApi.Instance.LogException(ClassName, "Error occurred while disabling portable mode", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,13 +64,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
#endif
|
||||
IndicateDeletion(DataLocation.RoamingDataPath);
|
||||
|
||||
API.ShowMsgBox(API.GetTranslation("restartToEnablePortableMode"));
|
||||
PublicApi.Instance.ShowMsgBox(Localize.restartToEnablePortableMode());
|
||||
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Error occurred while enabling portable mode", e);
|
||||
PublicApi.Instance.LogException(ClassName, "Error occurred while enabling portable mode", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,13 +90,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
||||
{
|
||||
FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.CopyAll(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
VerifyUserDataAfterMove(fromLocation, toLocation);
|
||||
}
|
||||
|
||||
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
|
||||
{
|
||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
}
|
||||
|
||||
public void CreateShortcuts()
|
||||
|
|
@ -150,12 +146,12 @@ namespace Flow.Launcher.Core.Configuration
|
|||
// delete it and prompt the user to pick the portable data location
|
||||
if (File.Exists(roamingDataDeleteFilePath))
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
|
||||
if (API.ShowMsgBox(API.GetTranslation("moveToDifferentLocation"),
|
||||
if (PublicApi.Instance.ShowMsgBox(Localize.moveToDifferentLocation(),
|
||||
string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.OpenPath(Constant.RootDirectory, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
|
@ -164,9 +160,9 @@ namespace Flow.Launcher.Core.Configuration
|
|||
// delete it and notify the user about it.
|
||||
else if (File.Exists(portableDataDeleteFilePath))
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
|
||||
API.ShowMsgBox(API.GetTranslation("shortcutsUninstallerCreated"));
|
||||
PublicApi.Instance.ShowMsgBox(Localize.shortcutsUninstallerCreated());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,8 +173,7 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
if (roamingLocationExists && portableLocationExists)
|
||||
{
|
||||
API.ShowMsgBox(string.Format(API.GetTranslation("userDataDuplicated"),
|
||||
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
||||
PublicApi.Instance.ShowMsgBox(Localize.userDataDuplicated(DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using System.Text.Json;
|
|||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
|
|
@ -18,13 +17,9 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
{
|
||||
private static readonly string ClassName = nameof(CommunityPluginSource);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private string latestEtag = "";
|
||||
|
||||
private List<UserPlugin> plugins = new();
|
||||
private List<UserPlugin> plugins = [];
|
||||
|
||||
private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new()
|
||||
{
|
||||
|
|
@ -41,7 +36,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
/// </remarks>
|
||||
public async Task<List<UserPlugin>> FetchAsync(CancellationToken token)
|
||||
{
|
||||
API.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
|
||||
PublicApi.Instance.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl);
|
||||
|
||||
|
|
@ -59,40 +54,40 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
.ConfigureAwait(false);
|
||||
latestEtag = response.Headers.ETag?.Tag;
|
||||
|
||||
API.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
|
||||
PublicApi.Instance.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
|
||||
return plugins;
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.NotModified)
|
||||
{
|
||||
API.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
|
||||
PublicApi.Instance.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
|
||||
return plugins;
|
||||
}
|
||||
else
|
||||
{
|
||||
API.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||
PublicApi.Instance.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
API.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller.");
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller.");
|
||||
return null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Likely an HttpClient timeout or external cancellation not requested by our token
|
||||
API.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out.");
|
||||
PublicApi.Instance.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out.");
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
|
||||
{
|
||||
API.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
|
||||
}
|
||||
else
|
||||
{
|
||||
API.LogException(ClassName, "Error Occurred", e);
|
||||
PublicApi.Instance.LogException(ClassName, "Error Occurred", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
|
@ -15,7 +14,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
{
|
||||
private static readonly string ClassName = nameof(AbstractPluginEnvironment);
|
||||
|
||||
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
protected readonly IPublicAPI API = PublicApi.Instance;
|
||||
|
||||
internal abstract string Language { get; }
|
||||
|
||||
|
|
@ -58,15 +57,10 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
||||
}
|
||||
|
||||
var noRuntimeMessage = string.Format(
|
||||
API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine);
|
||||
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
{
|
||||
var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
|
||||
var msg = Localize.runtimePluginChooseRuntimeExecutable(EnvName);
|
||||
|
||||
var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
|
||||
|
|
@ -77,12 +71,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
// Nothing selected because user pressed cancel from the file dialog window
|
||||
else
|
||||
{
|
||||
var forceDownloadMessage = string.Format(
|
||||
API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
var forceDownloadMessage = Localize.runtimeExecutableInvalidChooseDownload(Language, EnvName, Environment.NewLine);
|
||||
|
||||
// Let users select valid path or choose to download
|
||||
while (string.IsNullOrEmpty(selectedFile))
|
||||
|
|
@ -120,7 +109,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
|
||||
API.ShowMsgBox(Localize.runtimePluginUnableToSetExecutablePath(Language));
|
||||
API.LogError(ClassName,
|
||||
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
||||
$"{Language}Environment");
|
||||
|
|
@ -248,7 +237,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
private static string GetUpdatedEnvironmentPath(string filePath)
|
||||
{
|
||||
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
|
||||
|
||||
|
||||
// get the substring after "Environments" because we can not determine it dynamically
|
||||
var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
|
||||
return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.ShowMsgError(API.GetTranslation("failToInstallPythonEnv"));
|
||||
API.ShowMsgError(Localize.failToInstallPythonEnv());
|
||||
API.LogException(ClassName, "Failed to install Python environment", e);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.ShowMsgError(API.GetTranslation("failToInstallTypeScriptEnv"));
|
||||
API.ShowMsgError(Localize.failToInstallTypeScriptEnv());
|
||||
API.LogException(ClassName, "Failed to install TypeScript environment", e);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.ShowMsgError(API.GetTranslation("failToInstallTypeScriptEnv"));
|
||||
API.ShowMsgError(Localize.failToInstallTypeScriptEnv());
|
||||
API.LogException(ClassName, "Failed to install TypeScript environment", e);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
|
||||
|
|
@ -23,10 +22,6 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
private static DateTime lastFetchedAt = DateTime.MinValue;
|
||||
private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public static List<UserPlugin> UserPlugins { get; private set; }
|
||||
|
||||
public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
|
||||
|
|
@ -67,7 +62,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Http request failed", e);
|
||||
PublicApi.Instance.LogException(ClassName, "Http request failed", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -90,12 +85,12 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. "
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to parse the minimum app version {plugin.MinimumAppVersion} for plugin {plugin.Name}. "
|
||||
+ "Plugin excluded from manifest", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
API.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, "
|
||||
PublicApi.Instance.LogInfo(ClassName, $"Plugin {plugin.Name} requires minimum Flow Launcher version {plugin.MinimumAppVersion}, "
|
||||
+ $"but current version is {Constant.Version}. Plugin excluded from manifest.");
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -55,6 +56,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Droplex" Version="1.7.0" />
|
||||
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
||||
<PackageReference Include="FSharp.Core" Version="9.0.303" />
|
||||
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.5" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
|
|
@ -63,6 +65,17 @@
|
|||
<PackageReference Include="squirrel.windows" Version="1.9.0" NoWarn="NU1701" />
|
||||
<PackageReference Include="StreamJsonRpc" Version="2.22.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Remove="Languages\en.xaml" />
|
||||
<AdditionalFiles Include="..\Flow.Launcher\Languages\en.xaml">
|
||||
<Link>Languages\en.xaml</Link>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
|
||||
|
|
|
|||
12
Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs
Normal file
12
Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin;
|
||||
|
||||
public interface IResultUpdateRegister
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a plugin to receive results updated event.
|
||||
/// </summary>
|
||||
/// <param name="pair"></param>
|
||||
void RegisterResultsUpdatedEvent(PluginPair pair);
|
||||
}
|
||||
|
|
@ -285,7 +285,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftMargin,
|
||||
Content = API.GetTranslation("select")
|
||||
Content = Localize.select()
|
||||
};
|
||||
|
||||
Btn.Click += (_, _) =>
|
||||
|
|
|
|||
|
|
@ -100,11 +100,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API));
|
||||
|
||||
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((rawQuery, response) =>
|
||||
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((trimmedQuery, response) =>
|
||||
{
|
||||
var results = ParseResults(response);
|
||||
ResultsUpdated?.Invoke(this,
|
||||
new ResultUpdatedEventArgs { Query = new Query() { RawQuery = rawQuery }, Results = results });
|
||||
new ResultUpdatedEventArgs { Query = new Query() { TrimmedQuery = trimmedQuery }, Results = results });
|
||||
}));
|
||||
RPC.SynchronizationContext = null;
|
||||
RPC.StartListening();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
|
|
@ -6,7 +6,6 @@ using Flow.Launcher.Infrastructure;
|
|||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -14,10 +13,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
private static readonly string ClassName = nameof(PluginConfig);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// Parse plugin metadata in the given directories
|
||||
/// </summary>
|
||||
|
|
@ -39,7 +34,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Can't delete <{directory}>", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Can't delete <{directory}>", e);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -56,7 +51,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
duplicateList
|
||||
.ForEach(
|
||||
x => API.LogWarn(ClassName,
|
||||
x => PublicApi.Instance.LogWarn(ClassName,
|
||||
string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " +
|
||||
"not loaded due to version not the highest of the duplicates",
|
||||
x.Name, x.ID, x.Version),
|
||||
|
|
@ -108,7 +103,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName);
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
API.LogError(ClassName, $"Didn't find config file <{configPath}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Didn't find config file <{configPath}>");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -124,19 +119,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!AllowedLanguage.IsAllowed(metadata.Language))
|
||||
{
|
||||
API.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!File.Exists(metadata.ExecuteFilePath))
|
||||
{
|
||||
API.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
|
||||
PublicApi.Instance.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
|
@ -22,10 +22,6 @@ public static class PluginInstaller
|
|||
|
||||
private static readonly Settings Settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// Installs a plugin and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
|
||||
/// </summary>
|
||||
|
|
@ -33,18 +29,16 @@ public static class PluginInstaller
|
|||
/// <returns>A Task representing the asynchronous install operation.</returns>
|
||||
public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin)
|
||||
{
|
||||
if (API.PluginModified(newPlugin.ID))
|
||||
if (PublicApi.Instance.PluginModified(newPlugin.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), newPlugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(newPlugin.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (API.ShowMsgBox(
|
||||
string.Format(
|
||||
API.GetTranslation("InstallPromptSubtitle"),
|
||||
newPlugin.Name, newPlugin.Author, Environment.NewLine),
|
||||
API.GetTranslation("InstallPromptTitle"),
|
||||
if (PublicApi.Instance.ShowMsgBox(
|
||||
Localize.InstallPromptSubtitle(newPlugin.Name, newPlugin.Author, Environment.NewLine),
|
||||
Localize.InstallPromptTitle(),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
try
|
||||
|
|
@ -61,7 +55,7 @@ public static class PluginInstaller
|
|||
if (!newPlugin.IsFromLocalInstallPath)
|
||||
{
|
||||
await DownloadFileAsync(
|
||||
$"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}",
|
||||
$"{Localize.DownloadingPlugin()} {newPlugin.Name}",
|
||||
newPlugin.UrlDownload, filePath, cts);
|
||||
}
|
||||
else
|
||||
|
|
@ -80,7 +74,7 @@ public static class PluginInstaller
|
|||
throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath);
|
||||
}
|
||||
|
||||
if (!API.InstallPlugin(newPlugin, filePath))
|
||||
if (!PublicApi.Instance.InstallPlugin(newPlugin, filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -92,23 +86,20 @@ public static class PluginInstaller
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to install plugin", e);
|
||||
API.ShowMsgError(API.GetTranslation("ErrorInstallingPlugin"));
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to install plugin", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ErrorInstallingPlugin());
|
||||
return; // do not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
API.RestartApp();
|
||||
PublicApi.Instance.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("installbtn"),
|
||||
string.Format(
|
||||
API.GetTranslation(
|
||||
"InstallSuccessNoRestart"),
|
||||
newPlugin.Name));
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.installbtn(),
|
||||
Localize.InstallSuccessNoRestart(newPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,24 +124,23 @@ public static class PluginInstaller
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to validate zip file", e);
|
||||
API.ShowMsgError(API.GetTranslation("ZipFileNotHavePluginJson"));
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to validate zip file", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ZipFileNotHavePluginJson());
|
||||
return;
|
||||
}
|
||||
|
||||
if (API.PluginModified(plugin.ID))
|
||||
if (PublicApi.Instance.PluginModified(plugin.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.ShowUnknownSourceWarning)
|
||||
{
|
||||
if (!InstallSourceKnown(plugin.Website)
|
||||
&& API.ShowMsgBox(string.Format(
|
||||
API.GetTranslation("InstallFromUnknownSourceSubtitle"), Environment.NewLine),
|
||||
API.GetTranslation("InstallFromUnknownSourceTitle"),
|
||||
&& PublicApi.Instance.ShowMsgBox(Localize.InstallFromUnknownSourceSubtitle(Environment.NewLine),
|
||||
Localize.InstallFromUnknownSourceTitle(),
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
return;
|
||||
}
|
||||
|
|
@ -165,51 +155,46 @@ public static class PluginInstaller
|
|||
/// <returns>A Task representing the asynchronous uninstall operation.</returns>
|
||||
public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin)
|
||||
{
|
||||
if (API.PluginModified(oldPlugin.ID))
|
||||
if (PublicApi.Instance.PluginModified(oldPlugin.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), oldPlugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(oldPlugin.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (API.ShowMsgBox(
|
||||
string.Format(
|
||||
API.GetTranslation("UninstallPromptSubtitle"),
|
||||
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
|
||||
API.GetTranslation("UninstallPromptTitle"),
|
||||
if (PublicApi.Instance.ShowMsgBox(
|
||||
Localize.UninstallPromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
|
||||
Localize.UninstallPromptTitle(),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
var removePluginSettings = API.ShowMsgBox(
|
||||
API.GetTranslation("KeepPluginSettingsSubtitle"),
|
||||
API.GetTranslation("KeepPluginSettingsTitle"),
|
||||
var removePluginSettings = PublicApi.Instance.ShowMsgBox(
|
||||
Localize.KeepPluginSettingsSubtitle(),
|
||||
Localize.KeepPluginSettingsTitle(),
|
||||
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
|
||||
|
||||
try
|
||||
{
|
||||
if (!await API.UninstallPluginAsync(oldPlugin, removePluginSettings))
|
||||
if (!await PublicApi.Instance.UninstallPluginAsync(oldPlugin, removePluginSettings))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to uninstall plugin", e);
|
||||
API.ShowMsgError(API.GetTranslation("ErrorUninstallingPlugin"));
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to uninstall plugin", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ErrorUninstallingPlugin());
|
||||
return; // don not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
API.RestartApp();
|
||||
PublicApi.Instance.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("uninstallbtn"),
|
||||
string.Format(
|
||||
API.GetTranslation(
|
||||
"UninstallSuccessNoRestart"),
|
||||
oldPlugin.Name));
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.uninstallbtn(),
|
||||
Localize.UninstallSuccessNoRestart(oldPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -221,11 +206,9 @@ public static class PluginInstaller
|
|||
/// <returns>A Task representing the asynchronous update operation.</returns>
|
||||
public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin)
|
||||
{
|
||||
if (API.ShowMsgBox(
|
||||
string.Format(
|
||||
API.GetTranslation("UpdatePromptSubtitle"),
|
||||
oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
|
||||
API.GetTranslation("UpdatePromptTitle"),
|
||||
if (PublicApi.Instance.ShowMsgBox(
|
||||
Localize.UpdatePromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
|
||||
Localize.UpdatePromptTitle(),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
try
|
||||
|
|
@ -237,7 +220,7 @@ public static class PluginInstaller
|
|||
if (!newPlugin.IsFromLocalInstallPath)
|
||||
{
|
||||
await DownloadFileAsync(
|
||||
$"{API.GetTranslation("DownloadingPlugin")} {newPlugin.Name}",
|
||||
$"{Localize.DownloadingPlugin()} {newPlugin.Name}",
|
||||
newPlugin.UrlDownload, filePath, cts);
|
||||
}
|
||||
else
|
||||
|
|
@ -251,30 +234,27 @@ public static class PluginInstaller
|
|||
return;
|
||||
}
|
||||
|
||||
if (!await API.UpdatePluginAsync(oldPlugin, newPlugin, filePath))
|
||||
if (!await PublicApi.Instance.UpdatePluginAsync(oldPlugin, newPlugin, filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to update plugin", e);
|
||||
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin());
|
||||
return; // do not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
API.RestartApp();
|
||||
PublicApi.Instance.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("updatebtn"),
|
||||
string.Format(
|
||||
API.GetTranslation(
|
||||
"UpdateSuccessNoRestart"),
|
||||
newPlugin.Name));
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.updatebtn(),
|
||||
Localize.UpdateSuccessNoRestart(newPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,17 +269,17 @@ public static class PluginInstaller
|
|||
public static async Task CheckForPluginUpdatesAsync(Action<List<PluginUpdateInfo>> updateAllPlugins, bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default)
|
||||
{
|
||||
// Update the plugin manifest
|
||||
await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
|
||||
await PublicApi.Instance.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
|
||||
|
||||
// Get all plugins that can be updated
|
||||
var resultsForUpdate = (
|
||||
from existingPlugin in API.GetAllPlugins()
|
||||
join pluginUpdateSource in API.GetPluginManifest()
|
||||
from existingPlugin in PublicApi.Instance.GetAllPlugins()
|
||||
join pluginUpdateSource in PublicApi.Instance.GetPluginManifest()
|
||||
on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
|
||||
where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
|
||||
StringComparison.InvariantCulture) <
|
||||
0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
|
||||
&& !API.PluginModified(existingPlugin.Metadata.ID)
|
||||
&& !PublicApi.Instance.PluginModified(existingPlugin.Metadata.ID)
|
||||
select
|
||||
new PluginUpdateInfo()
|
||||
{
|
||||
|
|
@ -314,25 +294,25 @@ public static class PluginInstaller
|
|||
}).ToList();
|
||||
|
||||
// No updates
|
||||
if (!resultsForUpdate.Any())
|
||||
if (resultsForUpdate.Count == 0)
|
||||
{
|
||||
if (!silentUpdate)
|
||||
{
|
||||
API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle"));
|
||||
PublicApi.Instance.ShowMsg(Localize.updateNoResultTitle(), Localize.updateNoResultSubtitle());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If all plugins are modified, just return
|
||||
if (resultsForUpdate.All(x => API.PluginModified(x.ID)))
|
||||
if (resultsForUpdate.All(x => PublicApi.Instance.PluginModified(x.ID)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message box with button to update all plugins
|
||||
API.ShowMsgWithButton(
|
||||
API.GetTranslation("updateAllPluginsTitle"),
|
||||
API.GetTranslation("updateAllPluginsButtonContent"),
|
||||
PublicApi.Instance.ShowMsgWithButton(
|
||||
Localize.updateAllPluginsTitle(),
|
||||
Localize.updateAllPluginsButtonContent(),
|
||||
() =>
|
||||
{
|
||||
updateAllPlugins(resultsForUpdate);
|
||||
|
|
@ -357,7 +337,7 @@ public static class PluginInstaller
|
|||
using var cts = new CancellationTokenSource();
|
||||
|
||||
await DownloadFileAsync(
|
||||
$"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}",
|
||||
$"{Localize.DownloadingPlugin()} {plugin.PluginNewUserPlugin.Name}",
|
||||
plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
|
||||
|
||||
// check if user cancelled download before installing plugin
|
||||
|
|
@ -366,7 +346,7 @@ public static class PluginInstaller
|
|||
return;
|
||||
}
|
||||
|
||||
if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
|
||||
if (!await PublicApi.Instance.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -375,8 +355,8 @@ public static class PluginInstaller
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Failed to update plugin", e);
|
||||
API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin"));
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin());
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
@ -384,13 +364,13 @@ public static class PluginInstaller
|
|||
|
||||
if (restart)
|
||||
{
|
||||
API.RestartApp();
|
||||
PublicApi.Instance.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("updatebtn"),
|
||||
API.GetTranslation("PluginsUpdateSuccessNoRestart"));
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.updatebtn(),
|
||||
Localize.PluginsUpdateSuccessNoRestart());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +392,7 @@ public static class PluginInstaller
|
|||
if (showProgress)
|
||||
{
|
||||
var exceptionHappened = false;
|
||||
await API.ShowProgressBoxAsync(progressBoxTitle,
|
||||
await PublicApi.Instance.ShowProgressBoxAsync(progressBoxTitle,
|
||||
async (reportProgress) =>
|
||||
{
|
||||
if (reportProgress == null)
|
||||
|
|
@ -424,18 +404,18 @@ public static class PluginInstaller
|
|||
}
|
||||
else
|
||||
{
|
||||
await API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
|
||||
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}, cts.Cancel);
|
||||
|
||||
// if exception happened while downloading and user does not cancel downloading,
|
||||
// we need to redownload the plugin
|
||||
if (exceptionHappened && (!cts.IsCancellationRequested))
|
||||
await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
|
||||
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
|
||||
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -462,7 +442,7 @@ public static class PluginInstaller
|
|||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost)
|
||||
return false;
|
||||
|
||||
return API.GetAllPlugins().Any(x =>
|
||||
return PublicApi.Instance.GetAllPlugins().Any(x =>
|
||||
!string.IsNullOrEmpty(x.Metadata.Website) &&
|
||||
x.Metadata.Website.StartsWith(constructedUrlPart)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
|
@ -6,8 +6,8 @@ using System.Linq;
|
|||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.DialogJump;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
|
@ -25,48 +25,36 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
private static readonly string ClassName = nameof(PluginManager);
|
||||
|
||||
public static List<PluginPair> AllPlugins { get; private set; }
|
||||
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
private static readonly ConcurrentDictionary<string, PluginPair> _allLoadedPlugins = [];
|
||||
private static readonly ConcurrentDictionary<string, PluginPair> _allInitializedPlugins = [];
|
||||
private static readonly ConcurrentDictionary<string, PluginPair> _initFailedPlugins = [];
|
||||
private static readonly ConcurrentDictionary<string, PluginPair> _globalPlugins = [];
|
||||
private static readonly ConcurrentDictionary<string, PluginPair> _nonGlobalPlugins = [];
|
||||
|
||||
private static PluginsSettings Settings;
|
||||
private static readonly ConcurrentBag<string> ModifiedPlugins = new();
|
||||
private static readonly ConcurrentBag<string> ModifiedPlugins = [];
|
||||
|
||||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||
private static IEnumerable<PluginPair> _homePlugins;
|
||||
private static IEnumerable<PluginPair> _resultUpdatePlugin;
|
||||
private static IEnumerable<PluginPair> _translationPlugins;
|
||||
|
||||
private static readonly List<DialogJumpExplorerPair> _dialogJumpExplorerPlugins = new();
|
||||
private static readonly List<DialogJumpDialogPair> _dialogJumpDialogPlugins = new();
|
||||
private static readonly ConcurrentBag<PluginPair> _contextMenuPlugins = [];
|
||||
private static readonly ConcurrentBag<PluginPair> _homePlugins = [];
|
||||
private static readonly ConcurrentBag<PluginPair> _translationPlugins = [];
|
||||
private static readonly ConcurrentBag<PluginPair> _externalPreviewPlugins = [];
|
||||
|
||||
/// <summary>
|
||||
/// Directories that will hold Flow Launcher plugin directory
|
||||
/// </summary>
|
||||
public static readonly string[] Directories =
|
||||
{
|
||||
[
|
||||
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
|
||||
};
|
||||
];
|
||||
|
||||
private static void DeletePythonBinding()
|
||||
{
|
||||
const string binding = "flowlauncher.py";
|
||||
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
|
||||
{
|
||||
File.Delete(Path.Combine(subDirectory, binding));
|
||||
}
|
||||
}
|
||||
#region Save & Dispose & Reload Plugin
|
||||
|
||||
/// <summary>
|
||||
/// Save json and ISavable
|
||||
/// </summary>
|
||||
public static void Save()
|
||||
{
|
||||
foreach (var pluginPair in AllPlugins)
|
||||
foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: false))
|
||||
{
|
||||
var savable = pluginPair.Plugin as ISavable;
|
||||
try
|
||||
|
|
@ -75,17 +63,18 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
|
||||
}
|
||||
}
|
||||
|
||||
API.SavePluginSettings();
|
||||
API.SavePluginCaches();
|
||||
PublicApi.Instance.SavePluginSettings();
|
||||
PublicApi.Instance.SavePluginCaches();
|
||||
}
|
||||
|
||||
public static async ValueTask DisposePluginsAsync()
|
||||
{
|
||||
foreach (var pluginPair in AllPlugins)
|
||||
// Still call dispose for all plugins even if initialization failed, so that we can clean up resources
|
||||
foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: true))
|
||||
{
|
||||
await DisposePluginAsync(pluginPair);
|
||||
}
|
||||
|
|
@ -107,55 +96,59 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ReloadDataAsync()
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IReloadable p => Task.Run(p.ReloadData),
|
||||
IAsyncReloadable p => p.ReloadDataAsync(),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
})]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region External Preview
|
||||
|
||||
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
})]);
|
||||
}
|
||||
|
||||
public static async Task CloseExternalPreviewAsync()
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.ClosePreviewAsync(),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
})]);
|
||||
}
|
||||
|
||||
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
|
||||
await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
|
||||
{
|
||||
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
|
||||
_ => Task.CompletedTask,
|
||||
}).ToArray());
|
||||
})]);
|
||||
}
|
||||
|
||||
public static bool UseExternalPreview()
|
||||
{
|
||||
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled);
|
||||
return GetExternalPreviewPlugins().Any(x => !x.Metadata.Disabled);
|
||||
}
|
||||
|
||||
public static bool AllowAlwaysPreview()
|
||||
{
|
||||
var plugin = GetPluginsForInterface<IAsyncExternalPreview>().FirstOrDefault(x => !x.Metadata.Disabled);
|
||||
var plugin = GetExternalPreviewPlugins().FirstOrDefault(x => !x.Metadata.Disabled);
|
||||
|
||||
if (plugin is null)
|
||||
return false;
|
||||
|
|
@ -163,6 +156,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
|
||||
}
|
||||
|
||||
private static IList<PluginPair> GetExternalPreviewPlugins()
|
||||
{
|
||||
return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
static PluginManager()
|
||||
{
|
||||
// validate user directory
|
||||
|
|
@ -171,9 +173,28 @@ namespace Flow.Launcher.Core.Plugin
|
|||
DeletePythonBinding();
|
||||
}
|
||||
|
||||
private static void DeletePythonBinding()
|
||||
{
|
||||
const string binding = "flowlauncher.py";
|
||||
foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(Path.Combine(subDirectory, binding));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Failed to delete {binding} in {subDirectory}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load & Initialize Plugins
|
||||
|
||||
/// <summary>
|
||||
/// because InitializePlugins needs API, so LoadPlugins needs to be called first
|
||||
/// todo happlebao The API should be removed
|
||||
/// Load plugins from the directories specified in Directories.
|
||||
/// </summary>
|
||||
/// <param name="settings"></param>
|
||||
public static void LoadPlugins(PluginsSettings settings)
|
||||
|
|
@ -181,33 +202,22 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var metadatas = PluginConfig.Parse(Directories);
|
||||
Settings = settings;
|
||||
Settings.UpdatePluginSettings(metadatas);
|
||||
AllPlugins = PluginsLoader.Plugins(metadatas, Settings);
|
||||
|
||||
// Load plugins
|
||||
var allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings);
|
||||
foreach (var plugin in allLoadedPlugins)
|
||||
{
|
||||
if (plugin != null)
|
||||
{
|
||||
if (!_allLoadedPlugins.TryAdd(plugin.Metadata.ID, plugin))
|
||||
{
|
||||
PublicApi.Instance.LogError(ClassName, $"Plugin with ID {plugin.Metadata.ID} already loaded");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
|
||||
UpdatePluginDirectory(metadatas);
|
||||
|
||||
// Initialize plugin enumerable after all plugins are initialized
|
||||
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
|
||||
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
|
||||
_resultUpdatePlugin = GetPluginsForInterface<IResultUpdated>();
|
||||
_translationPlugins = GetPluginsForInterface<IPluginI18n>();
|
||||
|
||||
// Initialize Dialog Jump plugin pairs
|
||||
foreach (var pair in GetPluginsForInterface<IDialogJumpExplorer>())
|
||||
{
|
||||
_dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair
|
||||
{
|
||||
Plugin = (IDialogJumpExplorer)pair.Plugin,
|
||||
Metadata = pair.Metadata
|
||||
});
|
||||
}
|
||||
foreach (var pair in GetPluginsForInterface<IDialogJumpDialog>())
|
||||
{
|
||||
_dialogJumpDialogPlugins.Add(new DialogJumpDialogPair
|
||||
{
|
||||
Plugin = (IDialogJumpDialog)pair.Plugin,
|
||||
Metadata = pair.Metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
|
||||
|
|
@ -218,7 +228,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
if (string.IsNullOrEmpty(metadata.AssemblyName))
|
||||
{
|
||||
API.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}");
|
||||
PublicApi.Instance.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}");
|
||||
continue; // Skip if AssemblyName is not set, which can happen for erroneous plugins
|
||||
}
|
||||
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName);
|
||||
|
|
@ -228,7 +238,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
if (string.IsNullOrEmpty(metadata.Name))
|
||||
{
|
||||
API.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}");
|
||||
PublicApi.Instance.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}");
|
||||
continue; // Skip if Name is not set, which can happen for erroneous plugins
|
||||
}
|
||||
metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name);
|
||||
|
|
@ -238,106 +248,146 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call initialize for all plugins
|
||||
/// Initialize all plugins asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="register">The register to register results updated event for each plugin.</param>
|
||||
/// <returns>return the list of failed to init plugins or null for none</returns>
|
||||
public static async Task InitializePluginsAsync()
|
||||
public static async Task InitializePluginsAsync(IResultUpdateRegister register)
|
||||
{
|
||||
var failedPlugins = new ConcurrentQueue<PluginPair>();
|
||||
|
||||
var InitTasks = AllPlugins.Select(pair => Task.Run(async delegate
|
||||
var initTasks = _allLoadedPlugins.Select(x => Task.Run(async () =>
|
||||
{
|
||||
var pair = x.Value;
|
||||
|
||||
// Register plugin action keywords so that plugins can be queried in results
|
||||
RegisterPluginActionKeywords(pair);
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
|
||||
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
|
||||
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
|
||||
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, PublicApi.Instance)));
|
||||
|
||||
pair.Metadata.InitTime += milliseconds;
|
||||
API.LogInfo(ClassName,
|
||||
PublicApi.Instance.LogInfo(ClassName,
|
||||
$"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
|
||||
if (pair.Metadata.Disabled && pair.Metadata.HomeDisabled)
|
||||
{
|
||||
// If this plugin is already disabled, do not show error message again
|
||||
// Or else it will be shown every time
|
||||
API.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error");
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error");
|
||||
}
|
||||
else
|
||||
{
|
||||
pair.Metadata.Disabled = true;
|
||||
pair.Metadata.HomeDisabled = true;
|
||||
failedPlugins.Enqueue(pair);
|
||||
API.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
|
||||
}
|
||||
|
||||
// Even if the plugin cannot be initialized, we still need to add it in all plugin list so that
|
||||
// we can remove the plugin from Plugin or Store page or Plugin Manager plugin.
|
||||
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
|
||||
_initFailedPlugins.TryAdd(pair.Metadata.ID, pair);
|
||||
return;
|
||||
}
|
||||
|
||||
// Register ResultsUpdated event so that plugin query can use results updated interface
|
||||
register.RegisterResultsUpdatedEvent(pair);
|
||||
|
||||
// Update plugin metadata translation after the plugin is initialized with IPublicAPI instance
|
||||
Internationalization.UpdatePluginMetadataTranslation(pair);
|
||||
|
||||
// Add plugin to Dialog Jump plugin list after the plugin is initialized
|
||||
DialogJump.InitializeDialogJumpPlugin(pair);
|
||||
|
||||
// Add plugin to lists after the plugin is initialized
|
||||
AddPluginToLists(pair);
|
||||
}));
|
||||
|
||||
await Task.WhenAll(InitTasks);
|
||||
await Task.WhenAll(initTasks);
|
||||
|
||||
foreach (var plugin in AllPlugins)
|
||||
if (!_initFailedPlugins.IsEmpty)
|
||||
{
|
||||
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
|
||||
// has multiple global and action keywords because we will only add them here once.
|
||||
foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct())
|
||||
{
|
||||
switch (actionKeyword)
|
||||
{
|
||||
case Query.GlobalPluginWildcardSign:
|
||||
GlobalPlugins.Add(plugin);
|
||||
break;
|
||||
default:
|
||||
NonGlobalPlugins[actionKeyword] = plugin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failedPlugins.Any())
|
||||
{
|
||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||
API.ShowMsg(
|
||||
API.GetTranslation("failedToInitializePluginsTitle"),
|
||||
string.Format(
|
||||
API.GetTranslation("failedToInitializePluginsMessage"),
|
||||
failed
|
||||
),
|
||||
var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name));
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.failedToInitializePluginsTitle(),
|
||||
Localize.failedToInitializePluginsMessage(failed),
|
||||
"",
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterPluginActionKeywords(PluginPair pair)
|
||||
{
|
||||
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
|
||||
// has multiple global and action keywords because we will only add them here once.
|
||||
foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct())
|
||||
{
|
||||
switch (actionKeyword)
|
||||
{
|
||||
case Query.GlobalPluginWildcardSign:
|
||||
_globalPlugins.TryAdd(pair.Metadata.ID, pair);
|
||||
break;
|
||||
default:
|
||||
_nonGlobalPlugins.TryAdd(actionKeyword, pair);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddPluginToLists(PluginPair pair)
|
||||
{
|
||||
if (pair.Plugin is IContextMenu)
|
||||
{
|
||||
_contextMenuPlugins.Add(pair);
|
||||
}
|
||||
if (pair.Plugin is IAsyncHomeQuery)
|
||||
{
|
||||
_homePlugins.Add(pair);
|
||||
}
|
||||
if (pair.Plugin is IPluginI18n)
|
||||
{
|
||||
_translationPlugins.Add(pair);
|
||||
}
|
||||
if (pair.Plugin is IAsyncExternalPreview)
|
||||
{
|
||||
_externalPreviewPlugins.Add(pair);
|
||||
}
|
||||
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate & Query Plugins
|
||||
|
||||
public static ICollection<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump)
|
||||
{
|
||||
if (query is null)
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
|
||||
if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
|
||||
{
|
||||
if (dialogJump)
|
||||
return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList();
|
||||
return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))];
|
||||
else
|
||||
return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))];
|
||||
}
|
||||
|
||||
if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
if (API.PluginModified(plugin.Metadata.ID))
|
||||
if (PluginModified(plugin.Metadata.ID))
|
||||
return Array.Empty<PluginPair>();
|
||||
|
||||
return new List<PluginPair>
|
||||
{
|
||||
plugin
|
||||
};
|
||||
return [plugin];
|
||||
}
|
||||
|
||||
public static ICollection<PluginPair> ValidPluginsForHomeQuery()
|
||||
{
|
||||
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
return [.. _homePlugins.Where(p => !PluginModified(p.Metadata.ID))];
|
||||
}
|
||||
|
||||
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
|
||||
|
|
@ -345,9 +395,31 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var results = new List<Result>();
|
||||
var metadata = pair.Metadata;
|
||||
|
||||
if (IsPluginInitializing(metadata))
|
||||
{
|
||||
Result r = new()
|
||||
{
|
||||
Title = Localize.pluginStillInitializing(metadata.Name),
|
||||
SubTitle = Localize.pluginStillInitializingSubtitle(),
|
||||
AutoCompleteText = query.TrimmedQuery,
|
||||
IcoPath = metadata.IcoPath,
|
||||
PluginDirectory = metadata.PluginDirectory,
|
||||
ActionKeywordAssigned = query.ActionKeyword,
|
||||
PluginID = metadata.ID,
|
||||
OriginQuery = query,
|
||||
Action = _ =>
|
||||
{
|
||||
PublicApi.Instance.ReQuery();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
results.Add(r);
|
||||
return results;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
|
@ -369,15 +441,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
Result r = new()
|
||||
{
|
||||
Title = $"{metadata.Name}: Failed to respond!",
|
||||
SubTitle = "Select this result for more info",
|
||||
Title = Localize.pluginFailedToRespond(metadata.Name),
|
||||
SubTitle = Localize.pluginFailedToRespondSubtitle(),
|
||||
AutoCompleteText = query.TrimmedQuery,
|
||||
IcoPath = Constant.ErrorIcon,
|
||||
PluginDirectory = metadata.PluginDirectory,
|
||||
ActionKeywordAssigned = query.ActionKeyword,
|
||||
PluginID = metadata.ID,
|
||||
OriginQuery = query,
|
||||
Action = _ => { throw new FlowPluginException(metadata, e);},
|
||||
Score = -100
|
||||
Action = _ => { throw new FlowPluginException(metadata, e);}
|
||||
};
|
||||
results.Add(r);
|
||||
}
|
||||
|
|
@ -389,9 +461,31 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var results = new List<Result>();
|
||||
var metadata = pair.Metadata;
|
||||
|
||||
if (IsPluginInitializing(metadata))
|
||||
{
|
||||
Result r = new()
|
||||
{
|
||||
Title = Localize.pluginStillInitializing(metadata.Name),
|
||||
SubTitle = Localize.pluginStillInitializingSubtitle(),
|
||||
AutoCompleteText = query.TrimmedQuery,
|
||||
IcoPath = metadata.IcoPath,
|
||||
PluginDirectory = metadata.PluginDirectory,
|
||||
ActionKeywordAssigned = query.ActionKeyword,
|
||||
PluginID = metadata.ID,
|
||||
OriginQuery = query,
|
||||
Action = _ =>
|
||||
{
|
||||
PublicApi.Instance.ReQuery();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
results.Add(r);
|
||||
return results;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
async () => results = await ((IAsyncHomeQuery)pair.Plugin).HomeQueryAsync(token).ConfigureAwait(false));
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
|
@ -408,7 +502,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e);
|
||||
return null;
|
||||
}
|
||||
return results;
|
||||
|
|
@ -419,9 +513,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var results = new List<DialogJumpResult>();
|
||||
var metadata = pair.Metadata;
|
||||
|
||||
if (IsPluginInitializing(metadata))
|
||||
{
|
||||
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false));
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
|
@ -438,12 +538,58 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e);
|
||||
return null;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static bool IsPluginInitializing(PluginMetadata metadata)
|
||||
{
|
||||
return !_allInitializedPlugins.ContainsKey(metadata.ID);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Plugin List
|
||||
|
||||
public static List<PluginPair> GetAllLoadedPlugins()
|
||||
{
|
||||
return [.. _allLoadedPlugins.Values];
|
||||
}
|
||||
|
||||
public static List<PluginPair> GetAllInitializedPlugins(bool includeFailed)
|
||||
{
|
||||
if (includeFailed)
|
||||
{
|
||||
return [.. _allInitializedPlugins.Values];
|
||||
}
|
||||
else
|
||||
{
|
||||
return [.. _allInitializedPlugins.Values
|
||||
.Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))];
|
||||
}
|
||||
}
|
||||
|
||||
private static List<PluginPair> GetGlobalPlugins()
|
||||
{
|
||||
return [.. _globalPlugins.Values];
|
||||
}
|
||||
|
||||
public static Dictionary<string, PluginPair> GetNonGlobalPlugins()
|
||||
{
|
||||
return _nonGlobalPlugins.ToDictionary();
|
||||
}
|
||||
|
||||
public static List<PluginPair> GetTranslationPlugins()
|
||||
{
|
||||
return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update Metadata & Get Plugin
|
||||
|
||||
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
|
||||
{
|
||||
foreach (var r in results)
|
||||
|
|
@ -462,28 +608,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
/// <summary>
|
||||
/// get specified plugin, return null if not found
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Plugin may not be initialized, so do not use its plugin model to execute any commands
|
||||
/// </remarks>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
public static PluginPair GetPluginForId(string id)
|
||||
{
|
||||
return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id);
|
||||
return GetAllLoadedPlugins().FirstOrDefault(o => o.Metadata.ID == id);
|
||||
}
|
||||
|
||||
private static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
|
||||
{
|
||||
// Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup
|
||||
return AllPlugins?.Where(p => p.Plugin is T) ?? Array.Empty<PluginPair>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static IList<PluginPair> GetResultUpdatePlugin()
|
||||
{
|
||||
return _resultUpdatePlugin.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
}
|
||||
|
||||
public static IList<PluginPair> GetTranslationPlugins()
|
||||
{
|
||||
return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
}
|
||||
#region Get Context Menus
|
||||
|
||||
public static List<Result> GetContextMenusForPlugin(Result result)
|
||||
{
|
||||
|
|
@ -505,7 +642,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName,
|
||||
PublicApi.Instance.LogException(ClassName,
|
||||
$"Can't load context menus for plugin <{pluginPair.Metadata.Name}>",
|
||||
e);
|
||||
}
|
||||
|
|
@ -514,27 +651,82 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return results;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Check Home Plugin
|
||||
|
||||
public static bool IsHomePlugin(string id)
|
||||
{
|
||||
return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
|
||||
}
|
||||
|
||||
public static IList<DialogJumpExplorerPair> GetDialogJumpExplorers()
|
||||
#endregion
|
||||
|
||||
#region Check Initializing & Init Failed
|
||||
|
||||
public static bool IsInitializingOrInitFailed(string id)
|
||||
{
|
||||
return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
// Id does not exist in loaded plugins
|
||||
if (!_allLoadedPlugins.ContainsKey(id)) return false;
|
||||
|
||||
// Plugin initialized already
|
||||
if (_allInitializedPlugins.ContainsKey(id))
|
||||
{
|
||||
// Check if the plugin initialization failed
|
||||
return _initFailedPlugins.ContainsKey(id);
|
||||
}
|
||||
// Plugin is still initializing
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static IList<DialogJumpDialogPair> GetDialogJumpDialogs()
|
||||
public static bool IsInitializing(string id)
|
||||
{
|
||||
return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
|
||||
// Id does not exist in loaded plugins
|
||||
if (!_allLoadedPlugins.ContainsKey(id)) return false;
|
||||
|
||||
// Plugin initialized already
|
||||
if (_allInitializedPlugins.ContainsKey(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Plugin is still initializing
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsInitializationFailed(string id)
|
||||
{
|
||||
// Id does not exist in loaded plugins
|
||||
if (!_allLoadedPlugins.ContainsKey(id)) return false;
|
||||
|
||||
// Plugin initialized already
|
||||
if (_allInitializedPlugins.ContainsKey(id))
|
||||
{
|
||||
// Check if the plugin initialization failed
|
||||
return _initFailedPlugins.ContainsKey(id);
|
||||
}
|
||||
// Plugin is still initializing
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Plugin Action Keyword
|
||||
|
||||
public static bool ActionKeywordRegistered(string actionKeyword)
|
||||
{
|
||||
// this method is only checking for action keywords (defined as not '*') registration
|
||||
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
|
||||
return actionKeyword != Query.GlobalPluginWildcardSign
|
||||
&& NonGlobalPlugins.ContainsKey(actionKeyword);
|
||||
&& _nonGlobalPlugins.ContainsKey(actionKeyword);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -546,11 +738,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var plugin = GetPluginForId(id);
|
||||
if (newActionKeyword == Query.GlobalPluginWildcardSign)
|
||||
{
|
||||
GlobalPlugins.Add(plugin);
|
||||
_globalPlugins.TryAdd(id, plugin);
|
||||
}
|
||||
else
|
||||
{
|
||||
NonGlobalPlugins[newActionKeyword] = plugin;
|
||||
_nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin);
|
||||
}
|
||||
|
||||
// Update action keywords and action keyword in plugin metadata
|
||||
|
|
@ -577,11 +769,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
plugin.Metadata.ActionKeywords
|
||||
.Count(x => x == Query.GlobalPluginWildcardSign) == 1)
|
||||
{
|
||||
GlobalPlugins.Remove(plugin);
|
||||
_globalPlugins.TryRemove(id, out _);
|
||||
}
|
||||
|
||||
if (oldActionkeyword != Query.GlobalPluginWildcardSign)
|
||||
NonGlobalPlugins.Remove(oldActionkeyword);
|
||||
{
|
||||
_nonGlobalPlugins.TryRemove(oldActionkeyword, out _);
|
||||
}
|
||||
|
||||
// Update action keywords and action keyword in plugin metadata
|
||||
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
|
||||
|
|
@ -595,6 +789,12 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Plugin Install & Uninstall & Update
|
||||
|
||||
#region Private Functions
|
||||
|
||||
private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
|
||||
{
|
||||
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
|
||||
|
|
@ -620,12 +820,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (!Version.TryParse(newMetadata.Version, out var newVersion))
|
||||
return true; // If version is not valid, we assume it is lesser than any existing version
|
||||
|
||||
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
|
||||
&& Version.TryParse(x.Metadata.Version, out var version)
|
||||
&& newVersion <= version);
|
||||
// Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists
|
||||
return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == newMetadata.ID
|
||||
&& Version.TryParse(x.Metadata.Version, out var version)
|
||||
&& newVersion <= version);
|
||||
}
|
||||
|
||||
#region Public functions
|
||||
#endregion
|
||||
|
||||
#region Public Functions
|
||||
|
||||
public static bool PluginModified(string id)
|
||||
{
|
||||
|
|
@ -636,8 +839,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
if (PluginModified(existingVersion.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), existingVersion.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(existingVersion.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -663,14 +866,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
#endregion
|
||||
|
||||
#region Internal functions
|
||||
#region Internal Functions
|
||||
|
||||
internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
|
||||
{
|
||||
if (checkModified && PluginModified(plugin.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -689,15 +892,15 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name),
|
||||
string.Format(API.GetTranslation("fileNotFoundMessage"), pluginFolderPath));
|
||||
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
|
||||
Localize.fileNotFoundMessage(pluginFolderPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("failedToInstallPluginTitle"), plugin.Name),
|
||||
API.GetTranslation("pluginExistAlreadyMessage"));
|
||||
PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
|
||||
Localize.pluginExistAlreadyMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -726,7 +929,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
var newPluginPath = Path.Combine(installDirectory, folderName);
|
||||
|
||||
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -735,7 +938,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
|
||||
}
|
||||
|
||||
if (checkModified)
|
||||
|
|
@ -750,8 +953,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
if (checkModified && PluginModified(plugin.ID))
|
||||
{
|
||||
API.ShowMsgError(string.Format(API.GetTranslation("pluginModifiedAlreadyTitle"), plugin.Name),
|
||||
API.GetTranslation("pluginModifiedAlreadyMessage"));
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -760,7 +963,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
// If we want to remove plugin from AllPlugins,
|
||||
// we need to dispose them so that they can release file handles
|
||||
// which can help FL to delete the plugin settings & cache folders successfully
|
||||
var pluginPairs = AllPlugins.FindAll(p => p.Metadata.ID == plugin.ID);
|
||||
var pluginPairs = GetAllInitializedPlugins(includeFailed: true).Where(p => p.Metadata.ID == plugin.ID).ToList();
|
||||
foreach (var pluginPair in pluginPairs)
|
||||
{
|
||||
await DisposePluginAsync(pluginPair);
|
||||
|
|
@ -770,7 +973,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
if (removePluginSettings)
|
||||
{
|
||||
// For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances
|
||||
if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable)
|
||||
if (AllowedLanguage.IsDotNet(plugin.Language) && PublicApi.Instance is IRemovable removable)
|
||||
{
|
||||
removable.RemovePluginSettings(plugin.AssemblyName);
|
||||
removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath);
|
||||
|
|
@ -784,9 +987,9 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
|
||||
API.ShowMsgError(API.GetTranslation("failedToRemovePluginSettingsTitle"),
|
||||
string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name));
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginSettingsTitle(),
|
||||
Localize.failedToRemovePluginSettingsMessage(plugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -800,17 +1003,27 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
|
||||
API.ShowMsgError(API.GetTranslation("failedToRemovePluginCacheTitle"),
|
||||
string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name));
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginCacheTitle(),
|
||||
Localize.failedToRemovePluginCacheMessage(plugin.Name));
|
||||
}
|
||||
Settings.RemovePluginSettings(plugin.ID);
|
||||
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
|
||||
GlobalPlugins.RemoveWhere(p => p.Metadata.ID == plugin.ID);
|
||||
var keysToRemove = NonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
|
||||
{
|
||||
_allLoadedPlugins.TryRemove(plugin.ID, out var _);
|
||||
}
|
||||
{
|
||||
_allInitializedPlugins.TryRemove(plugin.ID, out var _);
|
||||
}
|
||||
{
|
||||
_initFailedPlugins.TryRemove(plugin.ID, out var _);
|
||||
}
|
||||
{
|
||||
_globalPlugins.TryRemove(plugin.ID, out var _);
|
||||
}
|
||||
var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
NonGlobalPlugins.Remove(key);
|
||||
_nonGlobalPlugins.TryRemove(key, out var _);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -826,5 +1039,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
||||
#pragma warning disable IDE0005
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
|
@ -18,10 +15,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
private static readonly string ClassName = nameof(PluginsLoader);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||
{
|
||||
var dotnetPlugins = DotNetPlugins(metadatas);
|
||||
|
|
@ -55,7 +48,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return plugins;
|
||||
}
|
||||
|
||||
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
private static List<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
{
|
||||
var erroredPlugins = new List<string>();
|
||||
|
||||
|
|
@ -64,56 +57,58 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
|
||||
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
|
||||
{
|
||||
Assembly assembly = null;
|
||||
IAsyncPlugin plugin = null;
|
||||
|
||||
try
|
||||
{
|
||||
Assembly assembly = null;
|
||||
IAsyncPlugin plugin = null;
|
||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||
assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||
assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||
typeof(IAsyncPlugin));
|
||||
|
||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||
typeof(IAsyncPlugin));
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
|
||||
metadata.AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
metadata.AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
catch (Exception e) when (assembly == null)
|
||||
{
|
||||
Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e) when (assembly == null)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
erroredPlugins.Add(metadata.Name);
|
||||
return;
|
||||
}
|
||||
if (plugin == null)
|
||||
{
|
||||
erroredPlugins.Add(metadata.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||
});
|
||||
|
||||
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||
});
|
||||
metadata.InitTime += milliseconds;
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
|
||||
}
|
||||
|
||||
if (erroredPlugins.Count > 0)
|
||||
|
|
@ -121,12 +116,12 @@ namespace Flow.Launcher.Core.Plugin
|
|||
var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
|
||||
|
||||
var errorMessage = erroredPlugins.Count > 1 ?
|
||||
API.GetTranslation("pluginsHaveErrored") :
|
||||
API.GetTranslation("pluginHasErrored");
|
||||
Localize.pluginsHaveErrored():
|
||||
Localize.pluginHasErrored();
|
||||
|
||||
API.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
PublicApi.Instance.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
||||
API.GetTranslation("referToLogs"));
|
||||
Localize.referToLogs());
|
||||
}
|
||||
|
||||
return plugins;
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
public static class QueryBuilder
|
||||
{
|
||||
public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalPlugins)
|
||||
public static Query Build(string originalQuery, string trimmedQuery, Dictionary<string, PluginPair> nonGlobalPlugins)
|
||||
{
|
||||
// home query
|
||||
if (string.IsNullOrEmpty(text))
|
||||
if (string.IsNullOrEmpty(trimmedQuery))
|
||||
{
|
||||
return new Query()
|
||||
{
|
||||
Search = string.Empty,
|
||||
RawQuery = string.Empty,
|
||||
OriginalQuery = string.Empty,
|
||||
TrimmedQuery = string.Empty,
|
||||
SearchTerms = Array.Empty<string>(),
|
||||
ActionKeyword = string.Empty,
|
||||
IsHomeQuery = true
|
||||
|
|
@ -22,14 +23,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
// replace multiple white spaces with one white space
|
||||
var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
var terms = trimmedQuery.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (terms.Length == 0)
|
||||
{
|
||||
// nothing was typed
|
||||
return null;
|
||||
}
|
||||
|
||||
var rawQuery = text;
|
||||
string actionKeyword, search;
|
||||
string possibleActionKeyword = terms[0];
|
||||
string[] searchTerms;
|
||||
|
|
@ -38,21 +38,22 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
// use non global plugin for query
|
||||
actionKeyword = possibleActionKeyword;
|
||||
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
|
||||
search = terms.Length > 1 ? trimmedQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
|
||||
searchTerms = terms[1..];
|
||||
}
|
||||
else
|
||||
{
|
||||
// non action keyword
|
||||
actionKeyword = string.Empty;
|
||||
search = rawQuery.TrimStart();
|
||||
search = trimmedQuery.TrimStart();
|
||||
searchTerms = terms;
|
||||
}
|
||||
|
||||
return new Query()
|
||||
{
|
||||
Search = search,
|
||||
RawQuery = rawQuery,
|
||||
OriginalQuery = originalQuery,
|
||||
TrimmedQuery = trimmedQuery,
|
||||
SearchTerms = searchTerms,
|
||||
ActionKeyword = actionKeyword,
|
||||
IsHomeQuery = false
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
|
|
@ -18,10 +17,6 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
private static readonly string ClassName = nameof(Internationalization);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private const string Folder = "Languages";
|
||||
private const string DefaultLanguageCode = "en";
|
||||
private const string DefaultFile = "en.xaml";
|
||||
|
|
@ -104,7 +99,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
API.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +170,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (language == null)
|
||||
{
|
||||
API.LogError(ClassName, $"Language code can't be found <{languageCode}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Language code can't be found <{languageCode}>");
|
||||
return AvailableLanguages.English;
|
||||
}
|
||||
else
|
||||
|
|
@ -208,7 +203,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -254,7 +249,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
// "Do you want to search with pinyin?"
|
||||
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?";
|
||||
|
||||
if (API.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
if (PublicApi.Instance.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
|
@ -311,7 +306,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
else
|
||||
{
|
||||
API.LogError(ClassName, $"Language path can't be found <{path}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Language path can't be found <{path}>");
|
||||
var english = Path.Combine(folder, DefaultFile);
|
||||
if (File.Exists(english))
|
||||
{
|
||||
|
|
@ -319,7 +314,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
else
|
||||
{
|
||||
API.LogError(ClassName, $"Default English Language path can't be found <{path}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Default English Language path can't be found <{path}>");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -354,7 +349,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
else
|
||||
{
|
||||
API.LogError(ClassName, $"No Translation for key {key}");
|
||||
PublicApi.Instance.LogError(ClassName, $"No Translation for key {key}");
|
||||
return $"No Translation for key {key}";
|
||||
}
|
||||
}
|
||||
|
|
@ -377,11 +372,27 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdatePluginMetadataTranslation(PluginPair p)
|
||||
{
|
||||
// Update plugin metadata name & description
|
||||
if (p.Plugin is not IPluginI18n pluginI18N) return;
|
||||
try
|
||||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class LocalizedDescriptionAttribute : DescriptionAttribute
|
||||
{
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private readonly string _resourceKey;
|
||||
|
||||
public LocalizedDescriptionAttribute(string resourceKey)
|
||||
{
|
||||
_resourceKey = resourceKey;
|
||||
}
|
||||
|
||||
public override string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
string description = API.GetTranslation(_resourceKey);
|
||||
return string.IsNullOrWhiteSpace(description) ?
|
||||
string.Format("[[{0}]]", _resourceKey) : description;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -444,17 +444,27 @@ namespace Flow.Launcher.Core.Resource
|
|||
_api.LogError(ClassName, $"Theme <{theme}> path can't be found");
|
||||
if (theme != Constant.DefaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme));
|
||||
_api.ShowMsgBox(Localize.theme_load_failure_path_not_exists(theme));
|
||||
ChangeTheme(Constant.DefaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (XamlParseException)
|
||||
catch (XamlParseException e)
|
||||
{
|
||||
_api.LogError(ClassName, $"Theme <{theme}> fail to parse");
|
||||
_api.LogException(ClassName, $"Theme <{theme}> fail to parse xaml", e);
|
||||
if (theme != Constant.DefaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));
|
||||
_api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme));
|
||||
ChangeTheme(Constant.DefaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_api.LogException(ClassName, $"Theme <{theme}> fail to load", e);
|
||||
if (theme != Constant.DefaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme));
|
||||
ChangeTheme(Constant.DefaultTheme);
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ namespace Flow.Launcher.Core
|
|||
try
|
||||
{
|
||||
if (!silentUpdate)
|
||||
_api.ShowMsg(_api.GetTranslation("pleaseWait"),
|
||||
_api.GetTranslation("update_flowlauncher_update_check"));
|
||||
_api.ShowMsg(Localize.pleaseWait(),
|
||||
Localize.update_flowlauncher_update_check());
|
||||
|
||||
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -58,13 +58,13 @@ namespace Flow.Launcher.Core
|
|||
if (newReleaseVersion <= currentVersion)
|
||||
{
|
||||
if (!silentUpdate)
|
||||
_api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
|
||||
_api.ShowMsgBox(Localize.update_flowlauncher_already_on_latest());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silentUpdate)
|
||||
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
|
||||
_api.GetTranslation("update_flowlauncher_updating"));
|
||||
_api.ShowMsg(Localize.update_flowlauncher_update_found(),
|
||||
Localize.update_flowlauncher_updating());
|
||||
|
||||
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -77,10 +77,7 @@ namespace Flow.Launcher.Core
|
|||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination,
|
||||
(s) => _api.ShowMsgBox(s)))
|
||||
_api.ShowMsgBox(string.Format(
|
||||
_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||
DataLocation.PortableDataPath,
|
||||
targetDestination));
|
||||
_api.ShowMsgBox(Localize.update_flowlauncher_fail_moving_portable_user_profile_data(DataLocation.PortableDataPath, targetDestination));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -91,7 +88,7 @@ namespace Flow.Launcher.Core
|
|||
|
||||
_api.LogInfo(ClassName, $"Update success:{newVersionTips}");
|
||||
|
||||
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"),
|
||||
if (_api.ShowMsgBox(newVersionTips, Localize.update_flowlauncher_new_update(),
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
|
|
@ -111,8 +108,8 @@ namespace Flow.Launcher.Core
|
|||
}
|
||||
|
||||
if (!silentUpdate)
|
||||
_api.ShowMsgError(_api.GetTranslation("update_flowlauncher_fail"),
|
||||
_api.GetTranslation("update_flowlauncher_check_connection"));
|
||||
_api.ShowMsgError(Localize.update_flowlauncher_fail(),
|
||||
Localize.update_flowlauncher_check_connection());
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -150,9 +147,9 @@ namespace Flow.Launcher.Core
|
|||
return manager;
|
||||
}
|
||||
|
||||
private string NewVersionTips(string version)
|
||||
private static string NewVersionTips(string version)
|
||||
{
|
||||
var tips = string.Format(_api.GetTranslation("newVersionTips"), version);
|
||||
var tips = Localize.newVersionTips(version);
|
||||
|
||||
return tips;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@
|
|||
"YamlDotNet": "9.1.0"
|
||||
}
|
||||
},
|
||||
"Flow.Launcher.Localization": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.0.6, )",
|
||||
"resolved": "0.0.6",
|
||||
"contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw=="
|
||||
},
|
||||
"FSharp.Core": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.303, )",
|
||||
|
|
@ -61,26 +67,6 @@
|
|||
"System.IO.Pipelines": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.2.3",
|
||||
"contentHash": "pD6woFAUfGcyEvMmrpctntU4jv4fT8752pfx1J5iRORVX3Ob0oQi8PWo0TXVaAJZiSfH0cdKTeKx0w0DzD0/mg==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "0.0.29",
|
||||
"Avalonia.Remote.Protocol": "11.2.3",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.0.29",
|
||||
"contentHash": "U4eJLQdoDNHXtEba7MZUCwrBErBTxFp6sUewXBOdAhU0Kwzwaa/EKFcYm8kpcysjzKtfB4S0S9n0uxKZFz/ikw=="
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.2.3",
|
||||
"contentHash": "6V0aNtld48WmO8tAlWwlRlUmXYcOWv+1eJUSl1ETF+1blUe5yhcSmuWarPprO0hDk8Ta6wGfdfcrnVl2gITYcA=="
|
||||
},
|
||||
"Ben.Demystifier": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.4.1",
|
||||
|
|
@ -104,6 +90,11 @@
|
|||
"resolved": "1.1.0",
|
||||
"contentHash": "j/zGAQ9hLbl7JDpeO40DaXvyyNxwQNDwnJEN7eCexn5F9Kid+VKya/Er0rfIv5Zod/32XarkqFP/V6WFHS/UpQ=="
|
||||
},
|
||||
"ini-parser": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.5.2",
|
||||
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
|
||||
},
|
||||
"InputSimulator": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.4",
|
||||
|
|
@ -147,11 +138,6 @@
|
|||
"resolved": "2.5.192",
|
||||
"contentHash": "jaJuwcgovWIZ8Zysdyf3b7b34/BrADw4v82GaEZymUhDd3ScMPrYd/cttekeDteJJPXseJxp04yTIcxiVUjTWg=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"Microsoft.NET.StringTools": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.6.3",
|
||||
|
|
@ -1186,7 +1172,8 @@
|
|||
"Ben.Demystifier": "[0.4.1, )",
|
||||
"BitFaster.Caching": "[2.5.4, )",
|
||||
"CommunityToolkit.Mvvm": "[8.4.0, )",
|
||||
"Flow.Launcher.Plugin": "[5.1.0, )",
|
||||
"Flow.Launcher.Localization": "[0.0.6, )",
|
||||
"Flow.Launcher.Plugin": "[5.0.0, )",
|
||||
"InputSimulator": "[1.0.4, )",
|
||||
"MemoryPack": "[1.21.4, )",
|
||||
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
|
||||
|
|
@ -1195,13 +1182,13 @@
|
|||
"NLog.OutputDebugString": "[6.0.4, )",
|
||||
"SharpVectors.Wpf": "[1.8.5, )",
|
||||
"System.Drawing.Common": "[7.0.0, )",
|
||||
"ToolGood.Words.Pinyin": "[3.1.0.3, )"
|
||||
"ToolGood.Words.Pinyin": "[3.1.0.3, )",
|
||||
"ini-parser": "[2.5.2, )"
|
||||
}
|
||||
},
|
||||
"flow.launcher.plugin": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Avalonia": "[11.2.3, )",
|
||||
"JetBrains.Annotations": "[2025.2.2, )"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using NHotkey;
|
|||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Accessibility;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.DialogJump
|
||||
{
|
||||
|
|
@ -58,21 +59,17 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
|
||||
private static readonly Settings _settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private static HWND _mainWindowHandle = HWND.Null;
|
||||
|
||||
private static readonly Dictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
|
||||
private static readonly ConcurrentDictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
|
||||
|
||||
private static DialogJumpExplorerPair _lastExplorer = null;
|
||||
private static readonly object _lastExplorerLock = new();
|
||||
private static readonly Lock _lastExplorerLock = new();
|
||||
|
||||
private static readonly Dictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
|
||||
private static readonly ConcurrentDictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
|
||||
|
||||
private static IDialogJumpDialogWindow _dialogWindow = null;
|
||||
private static readonly object _dialogWindowLock = new();
|
||||
private static readonly Lock _dialogWindowLock = new();
|
||||
|
||||
private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null;
|
||||
private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null;
|
||||
|
|
@ -89,8 +86,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
private static DispatcherTimer _dragMoveTimer = null;
|
||||
|
||||
// A list of all file dialog windows that are auto switched already
|
||||
private static readonly List<HWND> _autoSwitchedDialogs = new();
|
||||
private static readonly object _autoSwitchedDialogsLock = new();
|
||||
private static readonly List<HWND> _autoSwitchedDialogs = [];
|
||||
private static readonly Lock _autoSwitchedDialogsLock = new();
|
||||
|
||||
private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null;
|
||||
private static readonly WINEVENTPROC _moveProc = MoveSizeCallBack;
|
||||
|
|
@ -105,22 +102,13 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
|
||||
#region Initialize & Setup
|
||||
|
||||
public static void InitializeDialogJump(IList<DialogJumpExplorerPair> dialogJumpExplorers,
|
||||
IList<DialogJumpDialogPair> dialogJumpDialogs)
|
||||
public static void InitializeDialogJump()
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
// Initialize Dialog Jump explorers & dialogs
|
||||
_dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null);
|
||||
foreach (var explorer in dialogJumpExplorers)
|
||||
{
|
||||
_dialogJumpExplorers.Add(explorer, null);
|
||||
}
|
||||
_dialogJumpDialogs.Add(WindowsDialogJumpDialog, null);
|
||||
foreach (var dialog in dialogJumpDialogs)
|
||||
{
|
||||
_dialogJumpDialogs.Add(dialog, null);
|
||||
}
|
||||
// Initialize preinstalled Dialog Jump explorers & dialogs
|
||||
_dialogJumpExplorers.TryAdd(WindowsDialogJumpExplorer, null);
|
||||
_dialogJumpDialogs.TryAdd(WindowsDialogJumpDialog, null);
|
||||
|
||||
// Initialize main window handle
|
||||
_mainWindowHandle = Win32Helper.GetMainWindowHandle();
|
||||
|
|
@ -135,6 +123,29 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
_initialized = true;
|
||||
}
|
||||
|
||||
public static void InitializeDialogJumpPlugin(PluginPair pair)
|
||||
{
|
||||
// Add Dialog Jump explorers & dialogs
|
||||
if (pair.Plugin is IDialogJumpExplorer explorer)
|
||||
{
|
||||
var dialogJumpExplorer = new DialogJumpExplorerPair
|
||||
{
|
||||
Plugin = explorer,
|
||||
Metadata = pair.Metadata
|
||||
};
|
||||
_dialogJumpExplorers.TryAdd(dialogJumpExplorer, null);
|
||||
}
|
||||
if (pair.Plugin is IDialogJumpDialog dialog)
|
||||
{
|
||||
var dialogJumpDialog = new DialogJumpDialogPair
|
||||
{
|
||||
Plugin = dialog,
|
||||
Metadata = pair.Metadata
|
||||
};
|
||||
_dialogJumpDialogs.TryAdd(dialogJumpDialog, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetupDialogJump(bool enabled)
|
||||
{
|
||||
if (enabled == _enabled) return;
|
||||
|
|
@ -315,7 +326,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
{
|
||||
foreach (var explorer in _dialogJumpExplorers.Keys)
|
||||
{
|
||||
if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified
|
||||
if (PublicApi.Instance.PluginModified(explorer.Metadata.ID) || // Plugin is modified
|
||||
explorer.Metadata.Disabled) continue; // Plugin is disabled
|
||||
|
||||
var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd);
|
||||
|
|
@ -485,6 +496,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
uint dwmsEventTime
|
||||
)
|
||||
{
|
||||
if (hwnd.IsNull) return;
|
||||
|
||||
await _foregroundChangeLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
|
|
@ -493,7 +506,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
var dialogWindowChanged = false;
|
||||
foreach (var dialog in _dialogJumpDialogs.Keys)
|
||||
{
|
||||
if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified
|
||||
if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified
|
||||
dialog.Metadata.Disabled) continue; // Plugin is disabled
|
||||
|
||||
IDialogJumpDialogWindow dialogWindow;
|
||||
|
|
@ -596,7 +609,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
{
|
||||
foreach (var explorer in _dialogJumpExplorers.Keys)
|
||||
{
|
||||
if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified
|
||||
if (PublicApi.Instance.PluginModified(explorer.Metadata.ID) || // Plugin is modified
|
||||
explorer.Metadata.Disabled) continue; // Plugin is disabled
|
||||
|
||||
var explorerWindow = explorer.Plugin.CheckExplorerWindow(hwnd);
|
||||
|
|
@ -636,6 +649,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
uint dwmsEventTime
|
||||
)
|
||||
{
|
||||
if (hwnd.IsNull) return;
|
||||
|
||||
// If the dialog window is moved, update the Dialog Jump window position
|
||||
var dialogWindowExist = false;
|
||||
lock (_dialogWindowLock)
|
||||
|
|
@ -661,6 +676,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
uint dwmsEventTime
|
||||
)
|
||||
{
|
||||
if (hwnd.IsNull) return;
|
||||
|
||||
// If the dialog window is moved or resized, update the Dialog Jump window position
|
||||
if (_dragMoveTimer != null)
|
||||
{
|
||||
|
|
@ -686,6 +703,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
uint dwmsEventTime
|
||||
)
|
||||
{
|
||||
if (hwnd.IsNull) return;
|
||||
|
||||
// If the dialog window is destroyed, set _dialogWindowHandle to null
|
||||
var dialogWindowExist = false;
|
||||
lock (_dialogWindowLock)
|
||||
|
|
@ -717,6 +736,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
uint dwmsEventTime
|
||||
)
|
||||
{
|
||||
if (hwnd.IsNull) return;
|
||||
|
||||
// If the dialog window is hidden, set _dialogWindowHandle to null
|
||||
var dialogWindowExist = false;
|
||||
lock (_dialogWindowLock)
|
||||
|
|
@ -748,6 +769,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
uint dwmsEventTime
|
||||
)
|
||||
{
|
||||
if (hwnd.IsNull) return;
|
||||
|
||||
// If the dialog window is ended, set _dialogWindowHandle to null
|
||||
var dialogWindowExist = false;
|
||||
lock (_dialogWindowLock)
|
||||
|
|
@ -887,7 +910,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
// Then check all dialog windows
|
||||
foreach (var dialog in _dialogJumpDialogs.Keys)
|
||||
{
|
||||
if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified
|
||||
if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified
|
||||
dialog.Metadata.Disabled) continue; // Plugin is disabled
|
||||
|
||||
var dialogWindow = _dialogJumpDialogs[dialog];
|
||||
|
|
@ -900,7 +923,7 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
// Finally search for the dialog window again
|
||||
foreach (var dialog in _dialogJumpDialogs.Keys)
|
||||
{
|
||||
if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified
|
||||
if (PublicApi.Instance.PluginModified(dialog.Metadata.ID) || // Plugin is modified
|
||||
dialog.Metadata.Disabled) continue; // Plugin is disabled
|
||||
|
||||
IDialogJumpDialogWindow dialogWindow;
|
||||
|
|
@ -1083,11 +1106,8 @@ namespace Flow.Launcher.Infrastructure.DialogJump
|
|||
_navigationLock.Dispose();
|
||||
|
||||
// Stop drag move timer
|
||||
if (_dragMoveTimer != null)
|
||||
{
|
||||
_dragMoveTimer.Stop();
|
||||
_dragMoveTimer = null;
|
||||
}
|
||||
_dragMoveTimer?.Stop();
|
||||
_dragMoveTimer = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Windows.Win32;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
|
|
@ -13,9 +9,10 @@ namespace Flow.Launcher.Infrastructure
|
|||
/// </summary>
|
||||
public static string GetActiveExplorerPath()
|
||||
{
|
||||
var explorerWindow = GetActiveExplorer();
|
||||
string locationUrl = explorerWindow?.LocationURL;
|
||||
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
|
||||
var explorerPath = DialogJump.DialogJump.GetActiveExplorerPath();
|
||||
return !string.IsNullOrEmpty(explorerPath) ?
|
||||
GetDirectoryPath(new Uri(explorerPath).LocalPath) :
|
||||
null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -23,74 +20,12 @@ namespace Flow.Launcher.Infrastructure
|
|||
/// </summary>
|
||||
private static string GetDirectoryPath(string path)
|
||||
{
|
||||
if (!path.EndsWith("\\"))
|
||||
if (!path.EndsWith('\\'))
|
||||
{
|
||||
return path + "\\";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file explorer that is currently in the foreground
|
||||
/// </summary>
|
||||
private static dynamic GetActiveExplorer()
|
||||
{
|
||||
Type type = Type.GetTypeFromProgID("Shell.Application");
|
||||
if (type == null) return null;
|
||||
dynamic shell = Activator.CreateInstance(type);
|
||||
if (shell == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var explorerWindows = new List<dynamic>();
|
||||
var openWindows = shell.Windows();
|
||||
for (int i = 0; i < openWindows.Count; i++)
|
||||
{
|
||||
var window = openWindows.Item(i);
|
||||
if (window == null) continue;
|
||||
|
||||
// find the desired window and make sure that it is indeed a file explorer
|
||||
// we don't want the Internet Explorer or the classic control panel
|
||||
// ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE"
|
||||
if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe")
|
||||
{
|
||||
explorerWindows.Add(window);
|
||||
}
|
||||
}
|
||||
|
||||
if (explorerWindows.Count == 0) return null;
|
||||
|
||||
var zOrders = GetZOrder(explorerWindows);
|
||||
|
||||
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
|
||||
/// </summary>
|
||||
private static IEnumerable<int> GetZOrder(List<dynamic> hWnds)
|
||||
{
|
||||
var z = new int[hWnds.Count];
|
||||
for (var i = 0; i < hWnds.Count; i++) z[i] = -1;
|
||||
|
||||
var index = 0;
|
||||
var numRemaining = hWnds.Count;
|
||||
PInvoke.EnumWindows((wnd, _) =>
|
||||
{
|
||||
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
|
||||
if (searchIndex != -1)
|
||||
{
|
||||
z[searchIndex] = index;
|
||||
numRemaining--;
|
||||
if (numRemaining == 0) return false;
|
||||
}
|
||||
index++;
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
|
||||
return z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -56,10 +57,12 @@
|
|||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
<PackageReference Include="BitFaster.Caching" Version="2.5.4" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
||||
<PackageReference Include="Fody" Version="6.9.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ini-parser" Version="2.5.2" />
|
||||
<PackageReference Include="InputSimulator" Version="1.0.4" />
|
||||
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.14.15" />
|
||||
|
|
@ -80,4 +83,15 @@
|
|||
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Remove="Languages\en.xaml" />
|
||||
<AdditionalFiles Include="..\Flow.Launcher\Languages\en.xaml">
|
||||
<Link>Languages\en.xaml</Link>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -4,10 +4,8 @@ using System.Net;
|
|||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Http
|
||||
|
|
@ -20,10 +18,6 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
|
||||
private static readonly HttpClient client = new();
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
static Http()
|
||||
{
|
||||
// need to be added so it would work on a win10 machine
|
||||
|
|
@ -82,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
}
|
||||
catch (UriFormatException e)
|
||||
{
|
||||
API.ShowMsgError(API.GetTranslation("pleaseTryAgain"), API.GetTranslation("parseProxyFailed"));
|
||||
PublicApi.Instance.ShowMsgError(Localize.pleaseTryAgain(), Localize.parseProxyFailed());
|
||||
Log.Exception(ClassName, "Unable to parse Uri", e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media.Imaging;
|
||||
using IniParser;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.Graphics.Gdi;
|
||||
using Windows.Win32.UI.Shell;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
|
|
@ -35,9 +36,32 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205;
|
||||
|
||||
private const string UrlExtension = ".url";
|
||||
|
||||
/// <summary>
|
||||
/// Obtains a BitmapSource thumbnail for the specified file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the file is a Windows URL shortcut (".url"), the method attempts to resolve the shortcut's icon and use that for the thumbnail; otherwise it requests a thumbnail for the file path. The native HBITMAP used to create the BitmapSource is always released to avoid native memory leaks.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">Path to the file (can be a regular file or a ".url" shortcut).</param>
|
||||
/// <param name="width">Requested thumbnail width in pixels.</param>
|
||||
/// <param name="height">Requested thumbnail height in pixels.</param>
|
||||
/// <param name="options">Thumbnail extraction options (flags) controlling fallback and caching behavior.</param>
|
||||
/// <returns>A BitmapSource representing the requested thumbnail.</returns>
|
||||
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
HBITMAP hBitmap;
|
||||
|
||||
var extension = Path.GetExtension(fileName);
|
||||
if (string.Equals(extension, UrlExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hBitmap = GetHBitmapForUrlFile(fileName, width, height, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -50,6 +74,21 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains a native HBITMAP for the specified file at the requested size using the Windows Shell image factory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <paramref name="options"/> is <see cref="ThumbnailOptions.ThumbnailOnly"/> and thumbnail extraction fails
|
||||
/// due to extraction errors or a missing path, the method falls back to requesting an icon (<see cref="ThumbnailOptions.IconOnly"/>).
|
||||
/// The returned HBITMAP is a raw GDI handle; the caller is responsible for releasing it (e.g., via DeleteObject) to avoid native memory leaks.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">Path to the file to thumbnail.</param>
|
||||
/// <param name="width">Requested thumbnail width in pixels.</param>
|
||||
/// <param name="height">Requested thumbnail height in pixels.</param>
|
||||
/// <param name="options">Thumbnail request flags that control behavior (e.g., ThumbnailOnly, IconOnly).</param>
|
||||
/// <returns>An HBITMAP handle containing the image. Caller must free the handle when finished.</returns>
|
||||
/// <exception cref="COMException">If creating the shell item fails (HRESULT returned by SHCreateItemFromParsingName).</exception>
|
||||
/// <exception cref="InvalidOperationException">If the shell item does not expose IShellItemImageFactory or if an unexpected error occurs while obtaining the image.</exception>
|
||||
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
var retCode = PInvoke.SHCreateItemFromParsingName(
|
||||
|
|
@ -108,5 +147,44 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
return hBitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains an HBITMAP for a Windows .url shortcut by resolving its IconFile entry and delegating to GetHBitmap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The method parses the .url file as an INI, looks in the "InternetShortcut" section for the "IconFile" entry,
|
||||
/// and requests a bitmap for that icon path. If no IconFile is present or any error occurs while reading or
|
||||
/// resolving the icon, it falls back to requesting a thumbnail for the .url file itself.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">Path to the .url shortcut file.</param>
|
||||
/// <param name="width">Requested thumbnail width (pixels).</param>
|
||||
/// <param name="height">Requested thumbnail height (pixels).</param>
|
||||
/// <param name="options">ThumbnailOptions flags controlling extraction behavior.</param>
|
||||
/// <returns>An HBITMAP containing the requested image; callers are responsible for freeing the native handle.</returns>
|
||||
private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
HBITMAP hBitmap;
|
||||
|
||||
try
|
||||
{
|
||||
var parser = new FileIniDataParser();
|
||||
var data = parser.ReadFile(fileName);
|
||||
var urlSection = data["InternetShortcut"];
|
||||
|
||||
var iconPath = urlSection?["IconFile"];
|
||||
if (!File.Exists(iconPath))
|
||||
{
|
||||
// If the IconFile is missing, throw exception to fallback to the default icon
|
||||
throw new FileNotFoundException("Icon file not specified in Internet shortcut (.url) file.");
|
||||
}
|
||||
hBitmap = GetHBitmap(Path.GetFullPath(iconPath), width, height, options);
|
||||
}
|
||||
catch
|
||||
{
|
||||
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
}
|
||||
|
||||
return hBitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
|
||||
var fileTarget = new FileTarget
|
||||
{
|
||||
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt",
|
||||
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/Flow.Launcher.${date:format=yyyy-MM-dd}.log",
|
||||
Layout = layout
|
||||
};
|
||||
|
||||
|
|
@ -65,26 +65,22 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
|
||||
public static void SetLogLevel(LOGLEVEL level)
|
||||
{
|
||||
switch (level)
|
||||
var rule = LogManager.Configuration.FindRuleByName("file");
|
||||
|
||||
var nlogLevel = level switch
|
||||
{
|
||||
case LOGLEVEL.DEBUG:
|
||||
UseDebugLogLevel();
|
||||
break;
|
||||
default:
|
||||
UseInfoLogLevel();
|
||||
break;
|
||||
}
|
||||
Info(nameof(Logger), $"Using log level: {level}.");
|
||||
}
|
||||
LOGLEVEL.NONE => LogLevel.Off,
|
||||
LOGLEVEL.ERROR => LogLevel.Error,
|
||||
LOGLEVEL.DEBUG => LogLevel.Debug,
|
||||
_ => LogLevel.Info
|
||||
};
|
||||
|
||||
private static void UseDebugLogLevel()
|
||||
{
|
||||
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
|
||||
}
|
||||
rule.SetLoggingLevels(nlogLevel, LogLevel.Fatal);
|
||||
|
||||
private static void UseInfoLogLevel()
|
||||
{
|
||||
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
|
||||
// We can't log Info when level is set to Error or None, so we use Debug
|
||||
Debug(nameof(Logger), $"Using log level: {level}.");
|
||||
}
|
||||
|
||||
private static void LogFaultyFormat(string message)
|
||||
|
|
@ -172,7 +168,9 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
|
||||
public enum LOGLEVEL
|
||||
{
|
||||
DEBUG,
|
||||
INFO
|
||||
NONE,
|
||||
ERROR,
|
||||
INFO,
|
||||
DEBUG
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,4 +91,6 @@ PBT_APMRESUMEAUTOMATIC
|
|||
PBT_APMRESUMESUSPEND
|
||||
PowerRegisterSuspendResumeNotification
|
||||
PowerUnregisterSuspendResumeNotification
|
||||
DeviceNotifyCallbackRoutine
|
||||
DeviceNotifyCallbackRoutine
|
||||
|
||||
MonitorFromWindow
|
||||
|
|
@ -27,7 +27,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof (Settings.ShouldUsePinyin):
|
||||
case nameof(Settings.ShouldUsePinyin):
|
||||
if (_settings.ShouldUsePinyin)
|
||||
{
|
||||
Reload();
|
||||
|
|
@ -52,7 +52,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
private void CreateDoublePinyinTableFromStream(Stream jsonStream)
|
||||
{
|
||||
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
|
||||
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
|
||||
throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null");
|
||||
|
||||
var schemaKey = _settings.DoublePinyinSchema.ToString();
|
||||
|
|
@ -128,12 +128,12 @@ namespace Flow.Launcher.Infrastructure
|
|||
if (IsChineseCharacter(content[i]))
|
||||
{
|
||||
var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i];
|
||||
|
||||
if (i > 0)
|
||||
|
||||
if (i > 0 && content[i - 1] != ' ')
|
||||
{
|
||||
resultBuilder.Append(' ');
|
||||
}
|
||||
|
||||
|
||||
map.AddNewIndex(resultBuilder.Length, translated.Length);
|
||||
resultBuilder.Append(translated);
|
||||
previousIsChinese = true;
|
||||
|
|
@ -144,11 +144,14 @@ namespace Flow.Launcher.Infrastructure
|
|||
if (previousIsChinese)
|
||||
{
|
||||
previousIsChinese = false;
|
||||
resultBuilder.Append(' ');
|
||||
if (content[i] != ' ')
|
||||
{
|
||||
resultBuilder.Append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
map.AddNewIndex(resultBuilder.Length, resultList[i].Length);
|
||||
resultBuilder.Append(resultList[i]);
|
||||
|
||||
map.AddNewIndex(resultBuilder.Length, 1);
|
||||
resultBuilder.Append(content[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +159,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
var translation = resultBuilder.ToString();
|
||||
var result = (translation, map);
|
||||
|
||||
|
||||
return _pinyinCache[content] = result;
|
||||
}
|
||||
|
||||
|
|
@ -185,8 +188,8 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
private string ToDoublePinyin(string fullPinyin)
|
||||
{
|
||||
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
|
||||
? doublePinyinValue
|
||||
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
|
||||
? doublePinyinValue
|
||||
: fullPinyin;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,35 +2,118 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Storage
|
||||
{
|
||||
public class History
|
||||
{
|
||||
[JsonInclude]
|
||||
public List<HistoryItem> Items { get; private set; } = new List<HistoryItem>();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public List<HistoryItem> Items { get; private set; } = [];
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
private int _maxHistory = 300;
|
||||
[JsonInclude]
|
||||
public List<LastOpenedHistoryResult> LastOpenedHistoryItems { get; private set; } = [];
|
||||
|
||||
public void Add(string query)
|
||||
private readonly int _maxHistory = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Migrate legacy history data (stored in <see cref="Items"/>) into the new
|
||||
/// <see cref="LastOpenedHistoryResult"/> format and append them to
|
||||
/// <see cref="LastOpenedHistoryItems"/>.
|
||||
/// </summary>
|
||||
[Obsolete("For backwards compatibility. Remove after release v2.3.0")]
|
||||
public void PopulateHistoryFromLegacyHistory()
|
||||
{
|
||||
if (string.IsNullOrEmpty(query)) return;
|
||||
if (Items.Count > _maxHistory)
|
||||
if (Items.Count == 0) return;
|
||||
// Migrate old history items to new LastOpenedHistoryItems
|
||||
foreach (var item in Items)
|
||||
{
|
||||
Items.RemoveAt(0);
|
||||
LastOpenedHistoryItems.Add(new LastOpenedHistoryResult
|
||||
{
|
||||
Title = Localize.executeQuery(item.Query),
|
||||
OriginQuery = new Query { TrimmedQuery = item.Query },
|
||||
Query = item.Query,
|
||||
Action = _ =>
|
||||
{
|
||||
App.API.BackToQueryResults();
|
||||
App.API.ChangeQuery(item.Query);
|
||||
return false;
|
||||
},
|
||||
ExecutedDateTime = item.ExecutedDateTime
|
||||
});
|
||||
}
|
||||
Items.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a result into the last-opened history list (<see cref="LastOpenedHistoryItems"/>).
|
||||
/// This will also update the IcoPath if existing history item has one that is different.
|
||||
/// </summary>
|
||||
/// <param name="result">The result to add to history. Must have a non-empty <see cref="Result.OriginQuery"/>.<see cref="Query.TrimmedQuery"/>.</param>
|
||||
public void Add(Result result)
|
||||
{
|
||||
if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return;
|
||||
// History results triggered from homepage do not contain PluginID,
|
||||
// these are intentionally not saved otherwise cause duplicates due to subtitle
|
||||
// containing datetime string.
|
||||
if (string.IsNullOrEmpty(result.PluginID)) return;
|
||||
|
||||
// Maintain the max history limit
|
||||
if (LastOpenedHistoryItems.Count > _maxHistory)
|
||||
{
|
||||
LastOpenedHistoryItems.RemoveAt(0);
|
||||
}
|
||||
|
||||
if (Items.Count > 0 && Items.Last().Query == query)
|
||||
// If the last item is the same as the current result, just update the timestamp and the icon path
|
||||
if (LastOpenedHistoryItems.Count > 0 &&
|
||||
TryGetLastOpenedHistoryResult(result, out var existingHistoryItem))
|
||||
{
|
||||
Items.Last().ExecutedDateTime = DateTime.Now;
|
||||
existingHistoryItem.ExecutedDateTime = DateTime.Now;
|
||||
|
||||
if (existingHistoryItem.IcoPath != result.IcoPath)
|
||||
existingHistoryItem.IcoPath = result.IcoPath;
|
||||
|
||||
if (existingHistoryItem.Glyph?.Glyph != result.Glyph?.Glyph
|
||||
|| existingHistoryItem.Glyph?.FontFamily != result.Glyph?.FontFamily)
|
||||
existingHistoryItem.SetGlyph(result.Glyph);
|
||||
}
|
||||
else
|
||||
{
|
||||
Items.Add(new HistoryItem
|
||||
{
|
||||
Query = query,
|
||||
ExecutedDateTime = DateTime.Now
|
||||
});
|
||||
LastOpenedHistoryItems.Add(new LastOpenedHistoryResult(result));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find an existing <see cref="LastOpenedHistoryResult"/> in <see cref="LastOpenedHistoryItems"/>
|
||||
/// that is considered equal to the supplied <paramref name="result"/>.
|
||||
/// </summary>
|
||||
private bool TryGetLastOpenedHistoryResult(Result result, out LastOpenedHistoryResult historyItem)
|
||||
{
|
||||
historyItem = LastOpenedHistoryItems.FirstOrDefault(x => x.Equals(result));
|
||||
return historyItem is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flow uses IcoPathAbsolute property to display result the icons. This refreshes the IcoPathAbsolute
|
||||
/// property using current plugin metadata by updating the PluginDirectory property, which in turn also
|
||||
/// updates IcoPath. This keeps the saved icon paths of results updated correctly if flow is moved around.
|
||||
/// </summary>
|
||||
/// <remarks> Call this after plugins are loaded/initialized.</remarks>
|
||||
public void UpdateIcoPathAbsolute()
|
||||
{
|
||||
if (LastOpenedHistoryItems.Count == 0) return;
|
||||
|
||||
foreach (var item in LastOpenedHistoryItems)
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.PluginID)) continue;
|
||||
|
||||
var pluginPair = PluginManager.GetPluginForId(item.PluginID);
|
||||
if (pluginPair == null) continue;
|
||||
|
||||
item.PluginDirectory = pluginPair.Metadata.PluginDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace Flow.Launcher.Storage
|
||||
{
|
||||
[Obsolete("Use LastOpenedHistoryResult instead. This class will be removed in future versions.")]
|
||||
public class HistoryItem
|
||||
{
|
||||
public string Query { get; set; }
|
||||
|
|
@ -42,4 +43,4 @@ namespace Flow.Launcher.Storage
|
|||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
public int MapToOriginalIndex(int translatedIndex)
|
||||
{
|
||||
var searchResult = _originalToTranslated.BinarySearch(translatedIndex);
|
||||
return searchResult >= 0 ? searchResult : ~searchResult;
|
||||
return searchResult >= 0 ? searchResult + 1 : ~searchResult;
|
||||
}
|
||||
|
||||
public void EndConstruct()
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
public class CustomBrowserViewModel : BaseModel
|
||||
{
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public string Name { get; set; }
|
||||
[JsonIgnore]
|
||||
public string DisplayName => Name == "Default" ? API.GetTranslation("defaultBrowser_default") : Name;
|
||||
public string DisplayName => Name == "Default" ? Localize.defaultBrowser_default() : Name;
|
||||
public string Path { get; set; }
|
||||
public string PrivateArg { get; set; }
|
||||
public bool EnablePrivate { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
public class CustomExplorerViewModel : BaseModel
|
||||
{
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public string Name { get; set; }
|
||||
[JsonIgnore]
|
||||
public string DisplayName => Name == "Explorer" ? API.GetTranslation("fileManagerExplorer") : Name;
|
||||
public string DisplayName => Name == "Explorer" ? Localize.fileManagerExplorer() : Name;
|
||||
public string Path { get; set; }
|
||||
public string FileArgument { get; set; } = "\"%d\"";
|
||||
public string DirectoryArgument { get; set; } = "\"%d\"";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
|
|
@ -55,11 +53,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
public string Description { get; set; }
|
||||
|
||||
public string LocalizedDescription => API.GetTranslation(Description);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
public string LocalizedDescription => PublicApi.Instance.GetTranslation(Description);
|
||||
|
||||
public BaseBuiltinShortcutModel(string key, string description)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
|
|
@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
|
|||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Localization.Attributes;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
|
||||
|
|
@ -480,6 +481,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
public bool LeaveCmdOpen { get; set; }
|
||||
public bool HideWhenDeactivated { get; set; } = true;
|
||||
public bool ShowTaskbarWhenInvoked { get; set; } = false;
|
||||
|
||||
private bool _showAtTopmost = false;
|
||||
public bool ShowAtTopmost
|
||||
|
|
@ -513,6 +515,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected;
|
||||
|
||||
private HistoryStyle _historyStyle = HistoryStyle.Query;
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public HistoryStyle HistoryStyle
|
||||
{
|
||||
get => _historyStyle;
|
||||
set
|
||||
{
|
||||
if (_historyStyle != value)
|
||||
{
|
||||
_historyStyle = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
|
||||
public int CustomAnimationLength { get; set; } = 360;
|
||||
|
|
@ -695,4 +712,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
FullPathOpen,
|
||||
Directory
|
||||
}
|
||||
|
||||
[EnumLocalize]
|
||||
public enum HistoryStyle
|
||||
{
|
||||
[EnumLocalizeKey(nameof(Localize.queryHistory))]
|
||||
Query,
|
||||
|
||||
[EnumLocalizeKey(nameof(Localize.executedHistory))]
|
||||
LastOpened
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1016,5 +1016,32 @@ namespace Flow.Launcher.Infrastructure
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Taskbar
|
||||
|
||||
public static unsafe void ShowTaskbar()
|
||||
{
|
||||
// Find the taskbar window
|
||||
var taskbarHwnd = PInvoke.FindWindowEx(HWND.Null, HWND.Null, "Shell_TrayWnd", null);
|
||||
if (taskbarHwnd == HWND.Null) return;
|
||||
|
||||
// Magic from https://github.com/Oliviaophia/SmartTaskbar
|
||||
const uint TrayBarFlag = 0x05D1;
|
||||
var mon = PInvoke.MonitorFromWindow(taskbarHwnd, Windows.Win32.Graphics.Gdi.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||
PInvoke.PostMessage(taskbarHwnd, TrayBarFlag, new WPARAM(1), new LPARAM((nint)mon.Value));
|
||||
}
|
||||
|
||||
public static void HideTaskbar()
|
||||
{
|
||||
// Find the taskbar window
|
||||
var taskbarHwnd = PInvoke.FindWindowEx(HWND.Null, HWND.Null, "Shell_TrayWnd", null);
|
||||
if (taskbarHwnd == HWND.Null) return;
|
||||
|
||||
// Magic from https://github.com/Oliviaophia/SmartTaskbar
|
||||
const uint TrayBarFlag = 0x05D1;
|
||||
PInvoke.PostMessage(taskbarHwnd, TrayBarFlag, new WPARAM(0), IntPtr.Zero);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,12 +23,24 @@
|
|||
"resolved": "8.4.0",
|
||||
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
|
||||
},
|
||||
"Flow.Launcher.Localization": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.0.6, )",
|
||||
"resolved": "0.0.6",
|
||||
"contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw=="
|
||||
},
|
||||
"Fody": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.9.3, )",
|
||||
"resolved": "6.9.3",
|
||||
"contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA=="
|
||||
},
|
||||
"ini-parser": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.5.2, )",
|
||||
"resolved": "2.5.2",
|
||||
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
|
||||
},
|
||||
"InputSimulator": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.4, )",
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.102" PrivateAssets="All" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.205">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
|
|
@ -173,9 +173,21 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Get all loaded plugins
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will also return any plugins not fully initialized yet
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
List<PluginPair> GetAllPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// Get all initialized plugins
|
||||
/// </summary>
|
||||
/// <param name="includeFailed">
|
||||
/// Whether to include plugins that failed to initialize
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
List<PluginPair> GetAllInitializedPlugins(bool includeFailed);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback function for global keyboard events.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
|
|
@ -8,11 +9,29 @@ namespace Flow.Launcher.Plugin
|
|||
public class Query
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw query, this includes action keyword if it has.
|
||||
/// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace.
|
||||
/// We didn't recommend use this property directly. You should always use Search property.
|
||||
/// Original query, exactly how the user has typed into the search box.
|
||||
/// We don't recommend using this property directly. You should always use Search property.
|
||||
/// </summary>
|
||||
public string RawQuery { get; internal init; }
|
||||
public string OriginalQuery { get; internal init; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw query, this includes action keyword if it has.
|
||||
/// It has handled built-in custom query hotkeys and built-in shortcuts, and it trims the whitespace.
|
||||
/// We don't recommend using this property directly. You should always use Search property.
|
||||
/// </summary>
|
||||
[Obsolete("RawQuery is renamed to TrimmedQuery. This property will be removed. Update the code to use TrimmedQuery instead.")]
|
||||
public string RawQuery {
|
||||
get => TrimmedQuery;
|
||||
internal init { TrimmedQuery = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Original query but with trimmed whitespace. Includes action keyword.
|
||||
/// It has handled built-in custom query hotkeys and build-in shortcuts.
|
||||
/// If you need the exact original query from the search box, use OriginalQuery property instead.
|
||||
/// We don't recommend using this property directly. You should always use Search property.
|
||||
/// </summary>
|
||||
public string TrimmedQuery { get; internal init; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the query was forced to execute again.
|
||||
|
|
@ -28,7 +47,7 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
/// <summary>
|
||||
/// Search part of a query.
|
||||
/// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as RawQuery.
|
||||
/// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as TrimmedQuery.
|
||||
/// Since we allow user to switch a exclusive plugin to generic plugin,
|
||||
/// so this property will always give you the "real" query part of the query
|
||||
/// </summary>
|
||||
|
|
@ -103,6 +122,6 @@ namespace Flow.Launcher.Plugin
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => RawQuery;
|
||||
public override string ToString() => TrimmedQuery;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ using System.IO;
|
|||
using System.Threading.Tasks;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a result of a <see cref="Query"/> executed by a plugin
|
||||
/// Describes a result of a <see cref="Query"/> executed by a plugin.
|
||||
/// This or its child classes is serializable.
|
||||
/// </summary>
|
||||
public class Result
|
||||
{
|
||||
|
|
@ -21,6 +23,8 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
private string _icoPath;
|
||||
|
||||
private string _icoPathAbsolute;
|
||||
|
||||
private string _copyText = string.Empty;
|
||||
|
||||
private string _badgeIcoPath;
|
||||
|
|
@ -64,15 +68,27 @@ namespace Flow.Launcher.Plugin
|
|||
public string AutoCompleteText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The image to be displayed for the result.
|
||||
/// Path or URI to the icon image for this result.
|
||||
/// Updates <see cref="IcoPathAbsolute"/> appropriately when set.
|
||||
/// </summary>
|
||||
/// <value>Can be a local file path or a URL.</value>
|
||||
/// <remarks>GlyphInfo is prioritized if not null</remarks>
|
||||
/// <remarks>
|
||||
/// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png").
|
||||
/// Because <see cref="IcoPath"/> is serialized, using relative paths keeps the icon reference portable
|
||||
/// when Flow is moved.
|
||||
///
|
||||
/// Accepted formats:
|
||||
/// - Relative file paths (resolved against <see cref="PluginDirectory"/> into <see cref="IcoPathAbsolute"/>)
|
||||
/// - Absolute file paths (left as-is)
|
||||
/// - HTTP/HTTPS URLs (left as-is)
|
||||
/// - Data URIs (left as-is)
|
||||
/// </remarks>
|
||||
public string IcoPath
|
||||
{
|
||||
get => _icoPath;
|
||||
set
|
||||
{
|
||||
_icoPath = value;
|
||||
|
||||
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
|
||||
if (!string.IsNullOrEmpty(value)
|
||||
&& !string.IsNullOrEmpty(PluginDirectory)
|
||||
|
|
@ -81,15 +97,23 @@ namespace Flow.Launcher.Plugin
|
|||
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_icoPath = Path.Combine(PluginDirectory, value);
|
||||
_icoPathAbsolute = Path.Combine(PluginDirectory, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_icoPath = value;
|
||||
_icoPathAbsolute = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path or URI which is used to load and display the result icon for Flow.
|
||||
/// This is populated by the <see cref="IcoPath"/> setter.
|
||||
/// If a relative path was provided to <see cref="IcoPath"/>, this property will contain the resolved
|
||||
/// absolute local path after combining with <see cref="PluginDirectory"/>.
|
||||
/// </summary>
|
||||
public string IcoPathAbsolute => _icoPathAbsolute;
|
||||
|
||||
/// <summary>
|
||||
/// The image to be displayed for the badge of the result.
|
||||
/// </summary>
|
||||
|
|
@ -131,17 +155,34 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Delegate to load an icon for this result.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IconDelegate Icon = null;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate to load an icon for the badge of this result.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IconDelegate BadgeIcon = null;
|
||||
|
||||
private GlyphInfo _glyph;
|
||||
|
||||
/// <summary>
|
||||
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
|
||||
/// </summary>
|
||||
public GlyphInfo Glyph { get; init; }
|
||||
public GlyphInfo Glyph
|
||||
{
|
||||
get => _glyph;
|
||||
init => _glyph = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Glyph Icon after initialization
|
||||
/// </summary>
|
||||
/// <param name="glyph"></param>
|
||||
public void SetGlyph(GlyphInfo glyph)
|
||||
{
|
||||
_glyph = glyph;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An action to take in the form of a function call when the result has been selected.
|
||||
|
|
@ -151,6 +192,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// Its result determines what happens to Flow Launcher's query form:
|
||||
/// when true, the form will be hidden; when false, it will stay in focus.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<ActionContext, bool> Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -161,6 +203,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// Its result determines what happens to Flow Launcher's query form:
|
||||
/// when true, the form will be hidden; when false, it will stay in focus.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -203,11 +246,13 @@ namespace Flow.Launcher.Plugin
|
|||
/// <example>
|
||||
/// As external information for ContextMenu
|
||||
/// </example>
|
||||
[JsonIgnore]
|
||||
public object ContextData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin ID that generated this result
|
||||
/// </summary>
|
||||
[JsonInclude]
|
||||
public string PluginID { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -223,6 +268,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Customized Preview Panel
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Lazy<UserControl> PreviewPanel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -352,6 +398,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Delegate to get the preview panel's image
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IconDelegate PreviewDelegate { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@
|
|||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"requested": "[10.0.102, )",
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||
"Microsoft.SourceLink.Common": "10.0.102"
|
||||
}
|
||||
},
|
||||
"Microsoft.Windows.CsWin32": {
|
||||
|
|
@ -72,13 +72,13 @@
|
|||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
"resolved": "10.0.102",
|
||||
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||
},
|
||||
"Microsoft.Windows.SDK.Win32Docs": {
|
||||
"type": "Transitive",
|
||||
|
|
|
|||
|
|
@ -16,14 +16,15 @@ namespace Flow.Launcher.Test.Plugins
|
|||
{
|
||||
DecimalSeparator = DecimalSeparator.UseSystemLocale,
|
||||
MaxDecimalPlaces = 10,
|
||||
ShowErrorMessage = false // Make sure we return the empty results when error occurs
|
||||
ShowErrorMessage = false, // Make sure we return the empty results when error occurs
|
||||
UseThousandsSeparator = true // Default value
|
||||
};
|
||||
private readonly Engine _engine = new(new Configuration
|
||||
{
|
||||
Scope = new Dictionary<string, object>
|
||||
{
|
||||
{ "e", Math.E }, // e is not contained in the default mages engine
|
||||
}
|
||||
{
|
||||
{ "e", Math.E }, // e is not contained in the default mages engine
|
||||
}
|
||||
});
|
||||
|
||||
public CalculatorPluginTest()
|
||||
|
|
@ -41,6 +42,44 @@ namespace Flow.Launcher.Test.Plugins
|
|||
engineField.SetValue(null, _engine);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThousandsSeparatorTest_Enabled()
|
||||
{
|
||||
_settings.UseThousandsSeparator = true;
|
||||
|
||||
_settings.DecimalSeparator = DecimalSeparator.Dot;
|
||||
var result = GetCalculationResult("1000+234");
|
||||
// When thousands separator is enabled, the result should contain a separator
|
||||
// Since decimal separator is dot, thousands separator should be comma
|
||||
ClassicAssert.AreEqual("1,234", result);
|
||||
|
||||
_settings.DecimalSeparator = DecimalSeparator.Comma;
|
||||
var result2 = GetCalculationResult("1000+234");
|
||||
// When thousands separator is enabled, the result should contain a separator
|
||||
// Since decimal separator is comma, thousands separator should be dot
|
||||
ClassicAssert.AreEqual("1.234", result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThousandsSeparatorTest_Disabled()
|
||||
{
|
||||
_settings.UseThousandsSeparator = false;
|
||||
_settings.DecimalSeparator = DecimalSeparator.UseSystemLocale;
|
||||
|
||||
var result = GetCalculationResult("1000+234");
|
||||
ClassicAssert.AreEqual("1234", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThousandsSeparatorTest_LargeNumber()
|
||||
{
|
||||
_settings.UseThousandsSeparator = false;
|
||||
_settings.DecimalSeparator = DecimalSeparator.UseSystemLocale;
|
||||
|
||||
var result = GetCalculationResult("1000000+234567");
|
||||
ClassicAssert.AreEqual("1234567", result);
|
||||
}
|
||||
|
||||
// Basic operations
|
||||
[TestCase(@"1+1", "2")]
|
||||
[TestCase(@"2-1", "1")]
|
||||
|
|
@ -77,6 +116,9 @@ namespace Flow.Launcher.Test.Plugins
|
|||
[TestCase(@"invalid_expression", "")]
|
||||
public void CalculatorTest(string expression, string result)
|
||||
{
|
||||
_settings.UseThousandsSeparator = false;
|
||||
_settings.DecimalSeparator = DecimalSeparator.Dot;
|
||||
|
||||
ClassicAssert.AreEqual(GetCalculationResult(expression), result);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ namespace Flow.Launcher.Test
|
|||
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}}}}
|
||||
};
|
||||
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
|
||||
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);
|
||||
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.TrimmedQuery);
|
||||
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
|
||||
ClassicAssert.AreEqual(">", q.ActionKeyword);
|
||||
|
||||
|
|
@ -39,10 +39,10 @@ namespace Flow.Launcher.Test
|
|||
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}, Disabled = true}}}
|
||||
};
|
||||
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
|
||||
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search);
|
||||
ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
|
||||
ClassicAssert.AreEqual(q.Search, q.TrimmedQuery, "TrimmedQuery should be equal to Search.");
|
||||
ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
|
||||
ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
|
||||
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
|
||||
|
|
@ -51,7 +51,7 @@ namespace Flow.Launcher.Test
|
|||
[Test]
|
||||
public void GenericPluginQueryTest()
|
||||
{
|
||||
Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary<string, PluginPair>());
|
||||
Query q = QueryBuilder.Build("file.txt file2 file3", "file.txt file2 file3", new Dictionary<string, PluginPair>());
|
||||
|
||||
ClassicAssert.AreEqual("file.txt file2 file3", q.Search);
|
||||
ClassicAssert.AreEqual("", q.ActionKeyword);
|
||||
|
|
|
|||
|
|
@ -22,19 +22,33 @@ namespace Flow.Launcher.Test
|
|||
ClassicAssert.AreEqual(10, GetOriginalToTranslatedAt(mapping, 1));
|
||||
}
|
||||
|
||||
[TestCase(0, 0)]
|
||||
[TestCase(2, 1)]
|
||||
[TestCase(3, 1)]
|
||||
[TestCase(5, 2)]
|
||||
[TestCase(6, 2)]
|
||||
|
||||
[TestCase(0, 0)] // "F" -> "F"
|
||||
[TestCase(1, 1)] // "l" -> "l"
|
||||
[TestCase(2, 2)] // "o" -> "o"
|
||||
[TestCase(3, 3)] // "w" -> "w"
|
||||
[TestCase(4, 4)] // " " -> " "
|
||||
[TestCase(5, 5)] // "Y" (translated from "用") -> original index 5
|
||||
[TestCase(6, 5)] // "o" (translated from "用") -> original index 5
|
||||
[TestCase(7, 5)] // "n" (translated from "用") -> original index 5
|
||||
[TestCase(8, 5)] // "g" (translated from "用") -> original index 5
|
||||
[TestCase(10, 6)] // "H" (translated from "户") -> original index 6
|
||||
[TestCase(11, 6)] // "u" (translated from "户") -> original index 6
|
||||
public void MapToOriginalIndex_ShouldReturnExpectedIndex(int translatedIndex, int expectedOriginalIndex)
|
||||
{
|
||||
var mapping = new TranslationMapping();
|
||||
// a测试
|
||||
// a Ce Shi
|
||||
mapping.AddNewIndex(0, 1);
|
||||
mapping.AddNewIndex(2, 2);
|
||||
mapping.AddNewIndex(5, 3);
|
||||
// Test case :
|
||||
// 0123456
|
||||
// Flow 用户
|
||||
// 012345678901
|
||||
// Flow Yong Hu
|
||||
mapping.AddNewIndex(0, 1); // F
|
||||
mapping.AddNewIndex(1, 1); // l
|
||||
mapping.AddNewIndex(2, 1); // o
|
||||
mapping.AddNewIndex(3, 1); // w
|
||||
mapping.AddNewIndex(4, 1); // ' '
|
||||
mapping.AddNewIndex(5, 4); // 用 -> Yong
|
||||
mapping.AddNewIndex(10, 2); // 户 -> Hu
|
||||
|
||||
var result = mapping.MapToOriginalIndex(translatedIndex);
|
||||
ClassicAssert.AreEqual(expectedOriginalIndex, result);
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ namespace Flow.Launcher
|
|||
|
||||
if (addedActionKeywords.Any(App.API.ActionKeywordAssigned))
|
||||
{
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
|
||||
App.API.ShowMsgBox(Localize.newActionKeywordsHasBeenAssigned());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ namespace Flow.Launcher
|
|||
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
|
||||
{
|
||||
// User just changes the sequence of action keywords
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld"));
|
||||
App.API.ShowMsgBox(Localize.newActionKeywordsSameAsOld());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
x:Class="Flow.Launcher.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
ShutdownMode="OnMainWindowClose"
|
||||
Startup="OnStartup">
|
||||
<Application.Resources>
|
||||
|
|
@ -10,17 +11,17 @@
|
|||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemeResources>
|
||||
<ui:ThemeResources.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<ResourceDictionary x:Key="Light" ui:ThemeDictionary.Key="Light">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Light.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<ResourceDictionary x:Key="Dark" ui:ThemeDictionary.Key="Dark">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<ResourceDictionary x:Key="HighContrast" ui:ThemeDictionary.Key="HighContrast">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
|
@ -33,6 +34,15 @@
|
|||
<ResourceDictionary Source="pack://application:,,,/Themes/Win11Light.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Override styles in UI.Modern.WPF -->
|
||||
<Thickness x:Key="ListViewItemCompactSelectedBorderThemeThickness">2</Thickness>
|
||||
<sys:Double x:Key="CheckBoxMinWidth">0</sys:Double>
|
||||
<sys:Double x:Key="GridViewItemMinWidth">0</sys:Double>
|
||||
<sys:Double x:Key="GridViewItemMinHeight">40</sys:Double>
|
||||
<sys:Double x:Key="ListViewItemMinWidth">0</sys:Double>
|
||||
<sys:Double x:Key="ListViewItemMinHeight">36</sys:Double>
|
||||
<SolidColorBrush x:Key="NavigationViewSelectionIndicatorForeground" Color="#FF0063B1" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
|
@ -22,6 +22,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
|
|||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.SettingPages.ViewModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using iNKORE.UI.WPF.Modern.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
|
@ -56,6 +57,9 @@ namespace Flow.Launcher
|
|||
|
||||
public App()
|
||||
{
|
||||
// Do not use bitmap cache since it can cause WPF second window freezing issue
|
||||
ShadowAssist.UseBitmapCache = false;
|
||||
|
||||
// Initialize settings
|
||||
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
|
||||
|
||||
|
|
@ -183,12 +187,14 @@ namespace Flow.Launcher
|
|||
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
|
||||
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
|
||||
// Setup log level before any logging is done
|
||||
Log.SetLogLevel(_settings.LogLevel);
|
||||
|
||||
// Update dynamic resources base on settings
|
||||
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
|
||||
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
|
||||
|
||||
// Initialize notification system before any notification api is called
|
||||
Notification.Install();
|
||||
|
||||
// Enable Win32 dark mode if the system is in dark mode before creating all windows
|
||||
|
|
@ -197,6 +203,7 @@ namespace Flow.Launcher
|
|||
// Initialize language before portable clean up since it needs translations
|
||||
await _internationalization.InitializeLanguageAsync();
|
||||
|
||||
// Clean up after portability update
|
||||
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
|
||||
|
||||
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
|
||||
|
|
@ -206,32 +213,25 @@ namespace Flow.Launcher
|
|||
RegisterDispatcherUnhandledException();
|
||||
RegisterTaskSchedulerUnhandledException();
|
||||
|
||||
var imageLoadertask = ImageLoader.InitializeAsync();
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
|
||||
// Register ResultsUpdated event after all plugins are loaded
|
||||
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
|
||||
var imageLoaderTask = ImageLoader.InitializeAsync();
|
||||
|
||||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
|
||||
await API.UpdatePluginManifestAsync();
|
||||
|
||||
await PluginManager.InitializePluginsAsync();
|
||||
|
||||
// Update plugin titles after plugins are initialized with their api instances
|
||||
Internationalization.UpdatePluginMetadataTranslations();
|
||||
|
||||
await imageLoadertask;
|
||||
await imageLoaderTask;
|
||||
|
||||
_mainWindow = new MainWindow();
|
||||
|
||||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
// Initialize Dialog Jump before hotkey mapper since hotkey mapper will register its hotkey
|
||||
// Initialize Dialog Jump after main window is created so that it can access main window handle
|
||||
DialogJump.InitializeDialogJump();
|
||||
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
|
||||
|
||||
// Initialize hotkey mapper instantly after main window is created because
|
||||
// it will steal focus from main window which causes window hide
|
||||
HotKeyMapper.Initialize();
|
||||
|
|
@ -239,19 +239,43 @@ namespace Flow.Launcher
|
|||
// Initialize theme for main window
|
||||
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
|
||||
|
||||
DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs());
|
||||
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
RegisterExitEvents();
|
||||
|
||||
AutoStartup();
|
||||
AutoUpdates();
|
||||
AutoPluginUpdates();
|
||||
|
||||
API.SaveAppAllSettings();
|
||||
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
|
||||
API.LogInfo(ClassName, "End Flow Launcher startup ------------------------------------------------------");
|
||||
|
||||
_ = API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
|
||||
{
|
||||
API.LogInfo(ClassName, "Begin plugin initialization ----------------------------------------------------");
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
|
||||
await PluginManager.InitializePluginsAsync(_mainVM);
|
||||
|
||||
// Refresh the history results after plugins are initialized so that we can parse the absolute icon paths
|
||||
_mainVM.RefreshLastOpenedHistoryResults();
|
||||
|
||||
// Refresh home page after plugins are initialized because users may open main window during plugin initialization
|
||||
// And home page is created without full plugin list
|
||||
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
|
||||
{
|
||||
_mainVM.QueryResults();
|
||||
}
|
||||
|
||||
AutoPluginUpdates();
|
||||
|
||||
// Save all settings since we possibly update the plugin environment paths
|
||||
API.SaveAppAllSettings();
|
||||
|
||||
API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +300,7 @@ namespace Flow.Launcher
|
|||
// but if it fails (permissions, etc) then don't keep retrying
|
||||
// this also gives the user a visual indication in the Settings widget
|
||||
_settings.StartFlowLauncherOnSystemStartup = false;
|
||||
API.ShowMsgError(API.GetTranslation("setAutoStartFailed"), e.Message);
|
||||
API.ShowMsgError(Localize.setAutoStartFailed(), e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using System.Windows.Input;
|
|||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
internal class BoolToIMEConversionModeConverter : IValueConverter
|
||||
public class BoolToIMEConversionModeConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
|
|
@ -22,7 +22,7 @@ internal class BoolToIMEConversionModeConverter : IValueConverter
|
|||
}
|
||||
}
|
||||
|
||||
internal class BoolToIMEStateConverter : IValueConverter
|
||||
public class BoolToIMEStateConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
|
|
|
|||
91
Flow.Launcher/Converters/CornerRadiusFilterConverter.cs
Normal file
91
Flow.Launcher/Converters/CornerRadiusFilterConverter.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class CornerRadiusFilterConverter : DependencyObject, IValueConverter
|
||||
{
|
||||
public CornerRadiusFilterKind Filter { get; set; }
|
||||
|
||||
public double Scale { get; set; } = 1.0;
|
||||
|
||||
public static CornerRadius Convert(CornerRadius radius, CornerRadiusFilterKind filterKind)
|
||||
{
|
||||
CornerRadius result = radius;
|
||||
|
||||
switch (filterKind)
|
||||
{
|
||||
case CornerRadiusFilterKind.Top:
|
||||
result.BottomLeft = 0;
|
||||
result.BottomRight = 0;
|
||||
break;
|
||||
case CornerRadiusFilterKind.Right:
|
||||
result.TopLeft = 0;
|
||||
result.BottomLeft = 0;
|
||||
break;
|
||||
case CornerRadiusFilterKind.Bottom:
|
||||
result.TopLeft = 0;
|
||||
result.TopRight = 0;
|
||||
break;
|
||||
case CornerRadiusFilterKind.Left:
|
||||
result.TopRight = 0;
|
||||
result.BottomRight = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var cornerRadius = (CornerRadius)value;
|
||||
|
||||
var scale = Scale;
|
||||
if (!double.IsNaN(scale))
|
||||
{
|
||||
cornerRadius.TopLeft *= scale;
|
||||
cornerRadius.TopRight *= scale;
|
||||
cornerRadius.BottomRight *= scale;
|
||||
cornerRadius.BottomLeft *= scale;
|
||||
}
|
||||
|
||||
var filterType = Filter;
|
||||
if (filterType == CornerRadiusFilterKind.TopLeftValue ||
|
||||
filterType == CornerRadiusFilterKind.BottomRightValue)
|
||||
{
|
||||
return GetDoubleValue(cornerRadius, filterType);
|
||||
}
|
||||
|
||||
return Convert(cornerRadius, filterType);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static double GetDoubleValue(CornerRadius radius, CornerRadiusFilterKind filterKind)
|
||||
{
|
||||
switch (filterKind)
|
||||
{
|
||||
case CornerRadiusFilterKind.TopLeftValue:
|
||||
return radius.TopLeft;
|
||||
case CornerRadiusFilterKind.BottomRightValue:
|
||||
return radius.BottomRight;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public enum CornerRadiusFilterKind
|
||||
{
|
||||
None,
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
Left,
|
||||
TopLeftValue,
|
||||
BottomRightValue
|
||||
}
|
||||
32
Flow.Launcher/Converters/PlacementRectangleConverter.cs
Normal file
32
Flow.Launcher/Converters/PlacementRectangleConverter.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class PlacementRectangleConverter : IMultiValueConverter
|
||||
{
|
||||
public Thickness Margin { get; set; }
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (values.Length == 2 &&
|
||||
values[0] is double width &&
|
||||
values[1] is double height)
|
||||
{
|
||||
var margin = Margin;
|
||||
var topLeft = new Point(margin.Left, margin.Top);
|
||||
var bottomRight = new Point(width - margin.Right, height - margin.Bottom);
|
||||
var rect = new Rect(topLeft, bottomRight);
|
||||
return rect;
|
||||
}
|
||||
|
||||
return Rect.Empty;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
19
Flow.Launcher/Converters/SharedSizeGroupConverter.cs
Normal file
19
Flow.Launcher/Converters/SharedSizeGroupConverter.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class SharedSizeGroupConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return (Visibility)value != Visibility.Collapsed ? (string)parameter : null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ using System.Windows.Input;
|
|||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
class StringToKeyBindingConverter : IValueConverter
|
||||
public class StringToKeyBindingConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ namespace Flow.Launcher
|
|||
|
||||
if (string.IsNullOrEmpty(Hotkey) && string.IsNullOrEmpty(ActionKeyword))
|
||||
{
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("emptyPluginHotkey"));
|
||||
App.API.ShowMsgBox(Localize.emptyPluginHotkey());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,14 +40,14 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (string.IsNullOrEmpty(Key) || string.IsNullOrEmpty(Value))
|
||||
{
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut"));
|
||||
App.API.ShowMsgBox(Localize.emptyShortcut());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if key is modified or adding a new one
|
||||
if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key))
|
||||
{
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut"));
|
||||
App.API.ShowMsgBox(Localize.duplicateShortcut());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,14 +37,53 @@
|
|||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="RemoveUnnecessaryRuntimesAfterBuild" AfterTargets="Build">
|
||||
<RemoveDir Directories="$(OutputPath)runtimes\browser-wasm;
 $(OutputPath)runtimes\linux-arm;
 $(OutputPath)runtimes\linux-arm64;
 $(OutputPath)runtimes\linux-armel;
 $(OutputPath)runtimes\linux-mips64;
 $(OutputPath)runtimes\linux-musl-arm;
 $(OutputPath)runtimes\linux-musl-arm64;
 $(OutputPath)runtimes\linux-musl-x64;
 $(OutputPath)runtimes\linux-musl-s390x;
 $(OutputPath)runtimes\linux-ppc64le;
 $(OutputPath)runtimes\linux-s390x;
 $(OutputPath)runtimes\linux-x64;
 $(OutputPath)runtimes\linux-x86;
 $(OutputPath)runtimes\maccatalyst-arm64;
 $(OutputPath)runtimes\maccatalyst-x64;
 $(OutputPath)runtimes\osx;
 $(OutputPath)runtimes\osx-arm64;
 $(OutputPath)runtimes\osx-x64;
 $(OutputPath)runtimes\win-arm;
 $(OutputPath)runtimes\win-arm64;" />
|
||||
<RemoveDir Directories="$(OutputPath)runtimes\browser-wasm;
|
||||
$(OutputPath)runtimes\linux-arm;
|
||||
$(OutputPath)runtimes\linux-arm64;
|
||||
$(OutputPath)runtimes\linux-armel;
|
||||
$(OutputPath)runtimes\linux-mips64;
|
||||
$(OutputPath)runtimes\linux-musl-arm;
|
||||
$(OutputPath)runtimes\linux-musl-arm64;
|
||||
$(OutputPath)runtimes\linux-musl-x64;
|
||||
$(OutputPath)runtimes\linux-musl-s390x;
|
||||
$(OutputPath)runtimes\linux-ppc64le;
|
||||
$(OutputPath)runtimes\linux-s390x;
|
||||
$(OutputPath)runtimes\linux-x64;
|
||||
$(OutputPath)runtimes\linux-x86;
|
||||
$(OutputPath)runtimes\maccatalyst-arm64;
|
||||
$(OutputPath)runtimes\maccatalyst-x64;
|
||||
$(OutputPath)runtimes\osx;
|
||||
$(OutputPath)runtimes\osx-arm64;
|
||||
$(OutputPath)runtimes\osx-x64;
|
||||
$(OutputPath)runtimes\win-arm;
|
||||
$(OutputPath)runtimes\win-arm64;"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="RemoveUnnecessaryRuntimesAfterPublish" AfterTargets="Publish">
|
||||
<RemoveDir Directories="$(PublishDir)runtimes\browser-wasm;
 $(PublishDir)runtimes\linux-arm;
 $(PublishDir)runtimes\linux-arm64;
 $(PublishDir)runtimes\linux-armel;
 $(PublishDir)runtimes\linux-mips64;
 $(PublishDir)runtimes\linux-musl-arm;
 $(PublishDir)runtimes\linux-musl-arm64;
 $(PublishDir)runtimes\linux-musl-x64;
 $(PublishDir)runtimes\linux-musl-s390x;
 $(PublishDir)runtimes\linux-ppc64le;
 $(PublishDir)runtimes\linux-s390x;
 $(PublishDir)runtimes\linux-x64;
 $(PublishDir)runtimes\linux-x86;
 $(PublishDir)runtimes\maccatalyst-arm64;
 $(PublishDir)runtimes\maccatalyst-x64;
 $(PublishDir)runtimes\osx;
 $(PublishDir)runtimes\osx-arm64;
 $(PublishDir)runtimes\osx-x64;
 $(PublishDir)runtimes\win-arm;
 $(PublishDir)runtimes\win-arm64;" />
|
||||
<RemoveDir Directories="$(PublishDir)runtimes\browser-wasm;
|
||||
$(PublishDir)runtimes\linux-arm;
|
||||
$(PublishDir)runtimes\linux-arm64;
|
||||
$(PublishDir)runtimes\linux-armel;
|
||||
$(PublishDir)runtimes\linux-mips64;
|
||||
$(PublishDir)runtimes\linux-musl-arm;
|
||||
$(PublishDir)runtimes\linux-musl-arm64;
|
||||
$(PublishDir)runtimes\linux-musl-x64;
|
||||
$(PublishDir)runtimes\linux-musl-s390x;
|
||||
$(PublishDir)runtimes\linux-ppc64le;
|
||||
$(PublishDir)runtimes\linux-s390x;
|
||||
$(PublishDir)runtimes\linux-x64;
|
||||
$(PublishDir)runtimes\linux-x86;
|
||||
$(PublishDir)runtimes\maccatalyst-arm64;
|
||||
$(PublishDir)runtimes\maccatalyst-x64;
|
||||
$(PublishDir)runtimes\osx;
|
||||
$(PublishDir)runtimes\osx-arm64;
|
||||
$(PublishDir)runtimes\osx-x64;
|
||||
$(PublishDir)runtimes\win-arm;
|
||||
$(PublishDir)runtimes\win-arm64;"/>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -94,10 +133,12 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="ChefKeys" Version="0.1.2" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
||||
<PackageReference Include="Fody" Version="6.9.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.1" />
|
||||
<PackageReference Include="MdXaml" Version="1.27.0" />
|
||||
<PackageReference Include="MdXaml.AnimatedGif" Version="1.27.0" />
|
||||
<PackageReference Include="MdXaml.Html" Version="1.27.0" />
|
||||
|
|
@ -106,9 +147,6 @@
|
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<!-- ModernWpfUI v0.9.5 introduced WinRT changes that causes Notification platform unavailable error on some machines -->
|
||||
<!-- https://github.com/Flow-Launcher/Flow.Launcher/issues/1772#issuecomment-1502440801 -->
|
||||
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
|
@ -123,6 +161,10 @@
|
|||
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Resources\open.wav">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
|
|
@ -143,7 +185,7 @@
|
|||
</Target>
|
||||
|
||||
<Target Name="RemoveDuplicateAnalyzers" BeforeTargets="CoreCompile">
|
||||
<!-- Work around https://github.com/dotnet/wpf/issues/6792 -->
|
||||
<!-- Workaround https://github.com/dotnet/wpf/issues/6792 -->
|
||||
<ItemGroup>
|
||||
<FilteredAnalyzer Include="@(Analyzer->Distinct())" />
|
||||
<Analyzer Remove="@(Analyzer)" />
|
||||
|
|
|
|||
33
Flow.Launcher/Helper/BorderHelper.cs
Normal file
33
Flow.Launcher/Helper/BorderHelper.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
public static class BorderHelper
|
||||
{
|
||||
#region Child
|
||||
|
||||
public static readonly DependencyProperty ChildProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"Child",
|
||||
typeof(UIElement),
|
||||
typeof(BorderHelper),
|
||||
new PropertyMetadata(default(UIElement), OnChildChanged));
|
||||
|
||||
public static UIElement GetChild(Border border)
|
||||
{
|
||||
return (UIElement)border.GetValue(ChildProperty);
|
||||
}
|
||||
|
||||
public static void SetChild(Border border, UIElement value)
|
||||
{
|
||||
border.SetValue(ChildProperty, value);
|
||||
}
|
||||
|
||||
private static void OnChildChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((Border)d).Child = (UIElement)e.NewValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -61,8 +61,8 @@ internal static class HotKeyMapper
|
|||
string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}",
|
||||
e.Message,
|
||||
e.StackTrace));
|
||||
string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
||||
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
|
||||
string errorMsg = Localize.registerHotkeyFailed(hotkeyStr);
|
||||
string errorMsgTitle = Localize.MessageBoxTitle();
|
||||
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
|
||||
}
|
||||
}
|
||||
|
|
@ -87,8 +87,8 @@ internal static class HotKeyMapper
|
|||
e.Message,
|
||||
e.StackTrace,
|
||||
hotkeyStr));
|
||||
string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr);
|
||||
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
|
||||
string errorMsg = Localize.registerHotkeyFailed(hotkeyStr);
|
||||
string errorMsgTitle = Localize.MessageBoxTitle();
|
||||
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
|
||||
}
|
||||
}
|
||||
|
|
@ -112,8 +112,8 @@ internal static class HotKeyMapper
|
|||
string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}",
|
||||
e.Message,
|
||||
e.StackTrace));
|
||||
string errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr);
|
||||
string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle");
|
||||
string errorMsg = Localize.unregisterHotkeyFailed(hotkeyStr);
|
||||
string errorMsgTitle = Localize.MessageBoxTitle();
|
||||
App.API.ShowMsgBox(errorMsg, errorMsgTitle);
|
||||
}
|
||||
}
|
||||
|
|
@ -143,6 +143,8 @@ internal static class HotKeyMapper
|
|||
return;
|
||||
|
||||
App.API.ShowMainWindow();
|
||||
// Make sure to go back to the query results page first since it can cause issues if current page is context menu
|
||||
App.API.BackToQueryResults();
|
||||
App.API.ChangeQuery(hotkey.ActionKeyword, true);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
45
Flow.Launcher/Helper/ResultHelper.cs
Normal file
45
Flow.Launcher/Helper/ResultHelper.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Storage;
|
||||
|
||||
namespace Flow.Launcher.Helper;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public static class ResultHelper
|
||||
{
|
||||
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryResult item)
|
||||
{
|
||||
return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
|
||||
}
|
||||
|
||||
public static async Task<Result?> PopulateResultsAsync(string pluginId, string trimmedQuery, string title, string subTitle, string recordKey)
|
||||
{
|
||||
var plugin = PluginManager.GetPluginForId(pluginId);
|
||||
if (plugin == null) return null;
|
||||
var query = QueryBuilder.Build(trimmedQuery, trimmedQuery, PluginManager.GetNonGlobalPlugins());
|
||||
if (query == null) return null;
|
||||
try
|
||||
{
|
||||
var freshResults = await PluginManager.QueryForPluginAsync(plugin, query, CancellationToken.None);
|
||||
// Try to match by record key first if it is valid, otherwise fall back to title + subtitle match
|
||||
if (string.IsNullOrEmpty(recordKey))
|
||||
{
|
||||
return freshResults?.FirstOrDefault(r => r.Title == title && r.SubTitle == subTitle);
|
||||
}
|
||||
else
|
||||
{
|
||||
return freshResults?.FirstOrDefault(r => r.RecordKey == recordKey) ??
|
||||
freshResults?.FirstOrDefault(r => r.Title == title && r.SubTitle == subTitle);
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
App.API.LogException(nameof(ResultHelper), $"Failed to query results for {plugin.Metadata.Name}", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
|
@ -234,7 +234,7 @@ namespace Flow.Launcher
|
|||
private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) =>
|
||||
hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey);
|
||||
|
||||
public string EmptyHotkey => App.API.GetTranslation("none");
|
||||
public string EmptyHotkey => Localize.none();
|
||||
|
||||
public ObservableCollection<string> KeysToDisplay { get; set; } = new();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
x:Class="Flow.Launcher.HotkeyControlDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
Background="{DynamicResource PopuBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0 1 0 0"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ using Flow.Launcher.Helper;
|
|||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using ModernWpf.Controls;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
|
||||
namespace Flow.Launcher;
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ public partial class HotkeyControlDialog : ContentDialog
|
|||
|
||||
public EResultType ResultType { get; private set; } = EResultType.Cancel;
|
||||
public string ResultValue { get; private set; } = string.Empty;
|
||||
public static string EmptyHotkey => App.API.GetTranslation("none");
|
||||
public static string EmptyHotkey => Localize.none();
|
||||
|
||||
private static bool isOpenFlowHotkey;
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ public partial class HotkeyControlDialog : ContentDialog
|
|||
{
|
||||
WindowTitle = windowTitle switch
|
||||
{
|
||||
"" or null => App.API.GetTranslation("hotkeyRegTitle"),
|
||||
"" or null => Localize.hotkeyRegTitle(),
|
||||
_ => windowTitle
|
||||
};
|
||||
DefaultHotkey = defaultHotkey;
|
||||
|
|
@ -146,10 +146,7 @@ public partial class HotkeyControlDialog : ContentDialog
|
|||
Alert.Visibility = Visibility.Visible;
|
||||
if (registeredHotkeyData.RemoveHotkey is not null)
|
||||
{
|
||||
tbMsg.Text = string.Format(
|
||||
App.API.GetTranslation("hotkeyUnavailableEditable"),
|
||||
description
|
||||
);
|
||||
tbMsg.Text = Localize.hotkeyUnavailableEditable(description);
|
||||
SaveBtn.IsEnabled = false;
|
||||
SaveBtn.Visibility = Visibility.Collapsed;
|
||||
OverwriteBtn.IsEnabled = true;
|
||||
|
|
@ -158,10 +155,7 @@ public partial class HotkeyControlDialog : ContentDialog
|
|||
}
|
||||
else
|
||||
{
|
||||
tbMsg.Text = string.Format(
|
||||
App.API.GetTranslation("hotkeyUnavailableUneditable"),
|
||||
description
|
||||
);
|
||||
tbMsg.Text = Localize.hotkeyUnavailableUneditable(description);
|
||||
SaveBtn.IsEnabled = false;
|
||||
SaveBtn.Visibility = Visibility.Visible;
|
||||
OverwriteBtn.IsEnabled = false;
|
||||
|
|
@ -175,7 +169,7 @@ public partial class HotkeyControlDialog : ContentDialog
|
|||
|
||||
if (!CheckHotkeyAvailability(hotkey.Value, true))
|
||||
{
|
||||
tbMsg.Text = App.API.GetTranslation("hotkeyUnavailable");
|
||||
tbMsg.Text = Localize.hotkeyUnavailable();
|
||||
Alert.Visibility = Visibility.Visible;
|
||||
SaveBtn.IsEnabled = false;
|
||||
SaveBtn.Visibility = Visibility.Visible;
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@
|
|||
<system:String x:Key="PositionReset">Position Reset</system:String>
|
||||
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
|
||||
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
|
||||
<system:String x:Key="pluginStillInitializing">{0}: This plugin is still initializing...</system:String>
|
||||
<system:String x:Key="pluginStillInitializingSubtitle">Select this result to requery</system:String>
|
||||
<system:String x:Key="pluginFailedToRespond">{0}: Failed to respond!</system:String>
|
||||
<system:String x:Key="pluginFailedToRespondSubtitle">Select this result for more info</system:String>
|
||||
|
||||
<!-- Setting General -->
|
||||
<system:String x:Key="flowlauncher_settings">Settings</system:String>
|
||||
|
|
@ -77,6 +81,8 @@
|
|||
<system:String x:Key="useLogonTaskForStartupTooltip">After uninstallation, you need to manually remove this task (Flow.Launcher Startup) via Task Scheduler</system:String>
|
||||
<system:String x:Key="setAutoStartFailed">Error setting launch on startup</system:String>
|
||||
<system:String x:Key="hideFlowLauncherWhenLoseFocus">Hide Flow Launcher when focus is lost</system:String>
|
||||
<system:String x:Key="showTaskbarWhenOpened">Show taskbar when Flow Launcher is opened</system:String>
|
||||
<system:String x:Key="showTaskbarWhenOpenedToolTip">Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars.</system:String>
|
||||
<system:String x:Key="dontPromptUpdateMsg">Do not show new version notifications</system:String>
|
||||
<system:String x:Key="SearchWindowPosition">Search Window Location</system:String>
|
||||
<system:String x:Key="SearchWindowScreenRememberLastLaunchLocation">Remember Last Position</system:String>
|
||||
|
|
@ -166,6 +172,10 @@
|
|||
<system:String x:Key="homePageToolTip">Show home page results when query text is empty.</system:String>
|
||||
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
|
||||
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
|
||||
<system:String x:Key="historyStyle">History Style</system:String>
|
||||
<system:String x:Key="historyStyleTooltip">Choose the type of history to show in the History and Home Page</system:String>
|
||||
<system:String x:Key="queryHistory">Query history</system:String>
|
||||
<system:String x:Key="executedHistory">Last opened history</system:String>
|
||||
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
|
||||
<system:String x:Key="showAtTopmost">Show Search Window at Foremost</system:String>
|
||||
<system:String x:Key="showAtTopmostToolTip">Overrides other programs' 'Always on Top' setting and displays Flow in the foremost position.</system:String>
|
||||
|
|
@ -209,6 +219,8 @@
|
|||
<system:String x:Key="plugin_query_version">Version</system:String>
|
||||
<system:String x:Key="plugin_query_web">Website</system:String>
|
||||
<system:String x:Key="plugin_uninstall">Uninstall</system:String>
|
||||
<system:String x:Key="plugin_default_search_delay_time">Search delay time: default</system:String>
|
||||
<system:String x:Key="plugin_search_delay_time">Search delay time: {0}ms</system:String>
|
||||
<system:String x:Key="failedToRemovePluginSettingsTitle">Fail to remove plugin settings</system:String>
|
||||
<system:String x:Key="failedToRemovePluginSettingsMessage">Plugins: {0} - Fail to remove plugin settings files, please remove them manually</system:String>
|
||||
<system:String x:Key="failedToRemovePluginCacheTitle">Fail to remove plugin cache</system:String>
|
||||
|
|
@ -219,6 +231,7 @@
|
|||
<system:String x:Key="failedToUninstallPluginTitle">Fail to uninstall {0}</system:String>
|
||||
<system:String x:Key="fileNotFoundMessage">Unable to find plugin.json from the extracted zip file, or this path {0} does not exist</system:String>
|
||||
<system:String x:Key="pluginExistAlreadyMessage">A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin</system:String>
|
||||
<system:String x:Key="errorCreatingSettingPanel">Error creating setting panel for plugin {0}:{1}{2}</system:String>
|
||||
|
||||
<!-- Setting Plugin Store -->
|
||||
<system:String x:Key="pluginStore">Plugin Store</system:String>
|
||||
|
|
@ -467,8 +480,10 @@
|
|||
<system:String x:Key="userdatapathButton">Open Folder</system:String>
|
||||
<system:String x:Key="advanced">Advanced</system:String>
|
||||
<system:String x:Key="logLevel">Log Level</system:String>
|
||||
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
|
||||
<system:String x:Key="LogLevelNONE">Silent</system:String>
|
||||
<system:String x:Key="LogLevelERROR">Error</system:String>
|
||||
<system:String x:Key="LogLevelINFO">Info</system:String>
|
||||
<system:String x:Key="LogLevelDEBUG">Debug</system:String>
|
||||
<system:String x:Key="settingWindowFontTitle">Setting Window Font</system:String>
|
||||
|
||||
<!-- Release Notes Window -->
|
||||
|
|
@ -590,7 +605,7 @@
|
|||
The specified file manager could not be found. Please check the Custom File Manager setting under Settings > General.
|
||||
</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="folderOpenError">An error occurred while opening the folder.</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>
|
||||
<system:String x:Key="fileNotFoundError">File or directory not found: {0}</system:String>
|
||||
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@
|
|||
<system:String x:Key="PositionReset">位置のリセット</system:String>
|
||||
<system:String x:Key="PositionResetToolTip">検索ウィンドウの位置をリセット</system:String>
|
||||
<system:String x:Key="queryTextBoxPlaceholder">ここに入力して検索</system:String>
|
||||
<system:String x:Key="pluginStillInitializing">{0}: This plugin is still initializing...</system:String>
|
||||
<system:String x:Key="pluginStillInitializingSubtitle">Select this result to requery</system:String>
|
||||
<system:String x:Key="pluginFailedToRespond">{0}: Failed to respond!</system:String>
|
||||
<system:String x:Key="pluginFailedToRespondSubtitle">Select this result for more info</system:String>
|
||||
<system:String x:Key="pluginStillInitializing">{0}: このプラグインはまだ初期化中です…</system:String>
|
||||
<system:String x:Key="pluginStillInitializingSubtitle">この結果を選択して再検索する</system:String>
|
||||
<system:String x:Key="pluginFailedToRespond">{0}: 応答に失敗しました!</system:String>
|
||||
<system:String x:Key="pluginFailedToRespondSubtitle">詳細については、この結果を選択してください</system:String>
|
||||
|
||||
<!-- Setting General -->
|
||||
<system:String x:Key="flowlauncher_settings">設定</system:String>
|
||||
|
|
@ -454,7 +454,7 @@
|
|||
<system:String x:Key="icons">アイコン</system:String>
|
||||
<system:String x:Key="about_activate_times">あなたはFlow Launcherを {0} 回利用しました</system:String>
|
||||
<system:String x:Key="checkUpdates">アップデートを確認する</system:String>
|
||||
<system:String x:Key="BecomeASponsor">Become a Sponsor</system:String>
|
||||
<system:String x:Key="BecomeASponsor">スポンサーになる</system:String>
|
||||
<system:String x:Key="newVersionTips">新しいバージョン {0} が利用可能です。Flow Launcherを再起動してください。</system:String>
|
||||
<system:String x:Key="checkUpdatesFailed">アップデートの確認に失敗しました、api.github.com への接続とプロキシ設定を確認してください。</system:String>
|
||||
<system:String x:Key="downloadUpdatesFailed">
|
||||
|
|
|
|||
|
|
@ -420,13 +420,13 @@
|
|||
<system:String x:Key="DialogJumpWindowPositionFollowDefault">Default search window position. Displayed when triggered by search window hotkey</system:String>
|
||||
<system:String x:Key="dialogJumpResultBehaviour">Dialog Jump Result Navigation Behaviour</system:String>
|
||||
<system:String x:Key="dialogJumpResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window to the selected result path</system:String>
|
||||
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Left click or Enter key</system:String>
|
||||
<system:String x:Key="DialogJumpResultBehaviourRightClick">Right click</system:String>
|
||||
<system:String x:Key="DialogJumpResultBehaviourLeftClick">Clique esquerdo ou tecla Enter</system:String>
|
||||
<system:String x:Key="DialogJumpResultBehaviourRightClick">Clique direito</system:String>
|
||||
<system:String x:Key="dialogJumpFileResultBehaviour">Dialog Jump File Navigation Behaviour</system:String>
|
||||
<system:String x:Key="dialogJumpFileResultBehaviourToolTip">Behaviour to navigate Open/Save As dialog window when the result is a file path</system:String>
|
||||
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Fill full path in file name box</system:String>
|
||||
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Fill full path in file name box and open</system:String>
|
||||
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Fill directory in path box</system:String>
|
||||
<system:String x:Key="DialogJumpFileResultBehaviourFullPath">Preencher caminho total na caixa Nome do ficheiro</system:String>
|
||||
<system:String x:Key="DialogJumpFileResultBehaviourFullPathOpen">Preencher caminho total na caixa Nome do ficheiro e abrir</system:String>
|
||||
<system:String x:Key="DialogJumpFileResultBehaviourDirectory">Preencher diretório na caixa Caminho</system:String>
|
||||
|
||||
<!-- Setting Proxy -->
|
||||
<system:String x:Key="proxy">Proxy HTTP</system:String>
|
||||
|
|
|
|||
|
|
@ -636,7 +636,7 @@ Ak pri zadávaní skratky pred ňu pridáte "@", bude sa zhodovať s
|
|||
|
||||
<!-- Plugin Update Window -->
|
||||
<system:String x:Key="restartAfterUpdating">Po aktualizácii pluginov reštartovať Flow Launcher</system:String>
|
||||
<system:String x:Key="updatePluginCheckboxContent">{0}: Aktualizované z v{1} na v{2}</system:String>
|
||||
<system:String x:Key="updatePluginCheckboxContent">{0}: Aktualizácia z v{1} na v{2}</system:String>
|
||||
<system:String x:Key="updatePluginNoSelected">Nie je vybraný žiaden plugin</system:String>
|
||||
|
||||
<!-- Welcome Window -->
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
|
||||
Name="FlowMainWindow"
|
||||
Title="Flow Launcher"
|
||||
|
|
@ -526,7 +526,7 @@
|
|||
<TextBlock
|
||||
x:Name="PreviewSubTitle"
|
||||
Style="{DynamicResource PreviewItemSubTitleStyle}"
|
||||
Text="{Binding Result.SubTitle}" />
|
||||
Text="{Binding PreviewDescription}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Media;
|
||||
|
|
@ -25,7 +25,8 @@ using Flow.Launcher.Plugin;
|
|||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using ModernWpf.Controls;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using DataObject = System.Windows.DataObject;
|
||||
using Key = System.Windows.Input.Key;
|
||||
using MouseButtons = System.Windows.Forms.MouseButtons;
|
||||
|
|
@ -148,8 +149,8 @@ namespace Flow.Launcher
|
|||
_settings.ReleaseNotesVersion = Constant.Version;
|
||||
// Show release note popup with button
|
||||
App.API.ShowMsgWithButton(
|
||||
string.Format(App.API.GetTranslation("appUpdateTitle"), Constant.Version),
|
||||
App.API.GetTranslation("appUpdateButtonContent"),
|
||||
Localize.appUpdateTitle(Constant.Version),
|
||||
Localize.appUpdateButtonContent(),
|
||||
() =>
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
|
|
@ -191,11 +192,11 @@ namespace Flow.Launcher
|
|||
// Initialize color scheme
|
||||
if (_settings.ColorScheme == Constant.Light)
|
||||
{
|
||||
ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light;
|
||||
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light;
|
||||
}
|
||||
else if (_settings.ColorScheme == Constant.Dark)
|
||||
{
|
||||
ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark;
|
||||
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark;
|
||||
}
|
||||
|
||||
// Initialize position
|
||||
|
|
@ -321,6 +322,7 @@ namespace Flow.Launcher
|
|||
break;
|
||||
case nameof(Settings.ShowHomePage):
|
||||
case nameof(Settings.ShowHistoryResultsForHomePage):
|
||||
case nameof(Settings.HistoryStyle):
|
||||
if (_viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
|
||||
{
|
||||
_viewModel.QueryResults();
|
||||
|
|
@ -475,7 +477,7 @@ namespace Flow.Launcher
|
|||
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
|
||||
{
|
||||
var queryWithoutActionKeyword =
|
||||
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search;
|
||||
QueryBuilder.Build(QueryTextBox.Text, QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
|
||||
|
||||
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
|
||||
{
|
||||
|
|
@ -793,12 +795,12 @@ namespace Flow.Launcher
|
|||
private void UpdateNotifyIconText()
|
||||
{
|
||||
var menu = _contextMenu;
|
||||
((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") +
|
||||
((MenuItem)menu.Items[0]).Header = Localize.iconTrayOpen() +
|
||||
" (" + _settings.Hotkey + ")";
|
||||
((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode");
|
||||
((MenuItem)menu.Items[2]).Header = App.API.GetTranslation("PositionReset");
|
||||
((MenuItem)menu.Items[3]).Header = App.API.GetTranslation("iconTraySettings");
|
||||
((MenuItem)menu.Items[4]).Header = App.API.GetTranslation("iconTrayExit");
|
||||
((MenuItem)menu.Items[1]).Header = Localize.GameMode();
|
||||
((MenuItem)menu.Items[2]).Header = Localize.PositionReset();
|
||||
((MenuItem)menu.Items[3]).Header = Localize.iconTraySettings();
|
||||
((MenuItem)menu.Items[4]).Header = Localize.iconTrayExit();
|
||||
}
|
||||
|
||||
private void InitializeContextMenu()
|
||||
|
|
@ -808,31 +810,31 @@ namespace Flow.Launcher
|
|||
var openIcon = new FontIcon { Glyph = "\ue71e" };
|
||||
var open = new MenuItem
|
||||
{
|
||||
Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")",
|
||||
Header = Localize.iconTrayOpen() + " (" + _settings.Hotkey + ")",
|
||||
Icon = openIcon
|
||||
};
|
||||
var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };
|
||||
var gamemode = new MenuItem
|
||||
{
|
||||
Header = App.API.GetTranslation("GameMode"),
|
||||
Header = Localize.GameMode(),
|
||||
Icon = gamemodeIcon
|
||||
};
|
||||
var positionresetIcon = new FontIcon { Glyph = "\ue73f" };
|
||||
var positionreset = new MenuItem
|
||||
{
|
||||
Header = App.API.GetTranslation("PositionReset"),
|
||||
Header = Localize.PositionReset(),
|
||||
Icon = positionresetIcon
|
||||
};
|
||||
var settingsIcon = new FontIcon { Glyph = "\ue713" };
|
||||
var settings = new MenuItem
|
||||
{
|
||||
Header = App.API.GetTranslation("iconTraySettings"),
|
||||
Header = Localize.iconTraySettings(),
|
||||
Icon = settingsIcon
|
||||
};
|
||||
var exitIcon = new FontIcon { Glyph = "\ue7e8" };
|
||||
var exit = new MenuItem
|
||||
{
|
||||
Header = App.API.GetTranslation("iconTrayExit"),
|
||||
Header = Localize.iconTrayExit(),
|
||||
Icon = exitIcon
|
||||
};
|
||||
|
||||
|
|
@ -842,8 +844,8 @@ namespace Flow.Launcher
|
|||
settings.Click += (o, e) => App.API.OpenSettingDialog();
|
||||
exit.Click += (o, e) => Close();
|
||||
|
||||
gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip");
|
||||
positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip");
|
||||
gamemode.ToolTip = Localize.GameModeToolTip();
|
||||
positionreset.ToolTip = Localize.PositionResetToolTip();
|
||||
|
||||
_contextMenu.Items.Add(open);
|
||||
_contextMenu.Items.Add(gamemode);
|
||||
|
|
@ -858,7 +860,7 @@ namespace Flow.Launcher
|
|||
|
||||
public void UpdatePosition()
|
||||
{
|
||||
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
|
||||
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
|
||||
if (_viewModel.IsDialogJumpWindowUnderDialog())
|
||||
{
|
||||
InitializeDialogJumpPosition();
|
||||
|
|
@ -882,7 +884,7 @@ namespace Flow.Launcher
|
|||
|
||||
private void InitializePosition()
|
||||
{
|
||||
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
|
||||
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
|
||||
InitializePositionInner();
|
||||
InitializePositionInner();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
Title="{DynamicResource updateAllPluginsButtonContent}"
|
||||
Width="530"
|
||||
Background="{DynamicResource PopuBGColor}"
|
||||
|
|
@ -66,13 +67,13 @@
|
|||
Text="{DynamicResource updateAllPluginsButtonContent}"
|
||||
TextAlignment="Left" />
|
||||
|
||||
<ScrollViewer
|
||||
<ui:ScrollViewerEx
|
||||
MaxHeight="300"
|
||||
Margin="0 5 0 5"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="UpdatePluginStackPanel" />
|
||||
</ScrollViewer>
|
||||
</ui:ScrollViewerEx>
|
||||
|
||||
<Rectangle
|
||||
Height="1"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace Flow.Launcher
|
|||
{
|
||||
var checkBox = new CheckBox
|
||||
{
|
||||
Content = string.Format(App.API.GetTranslation("updatePluginCheckboxContent"), plugin.Name, plugin.CurrentVersion, plugin.NewVersion),
|
||||
Content = Localize.updatePluginCheckboxContent(plugin.Name, plugin.CurrentVersion, plugin.NewVersion),
|
||||
IsChecked = true,
|
||||
Margin = new Thickness(0, 5, 0, 5),
|
||||
Tag = plugin,
|
||||
|
|
@ -50,10 +50,7 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (sender is not CheckBox cb) return;
|
||||
if (cb.Tag is not PluginUpdateInfo plugin) return;
|
||||
if (Plugins.Contains(plugin))
|
||||
{
|
||||
Plugins.Remove(plugin);
|
||||
}
|
||||
Plugins.Remove(plugin);
|
||||
}
|
||||
|
||||
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
|
||||
|
|
@ -66,7 +63,7 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (Plugins.Count == 0)
|
||||
{
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("updatePluginNoSelected"));
|
||||
App.API.ShowMsgBox(Localize.updatePluginNoSelected());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -31,8 +30,8 @@ using Flow.Launcher.Plugin;
|
|||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
using JetBrains.Annotations;
|
||||
using ModernWpf;
|
||||
using Squirrel;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
|
||||
|
|
@ -184,14 +183,14 @@ namespace Flow.Launcher
|
|||
if (showDefaultNotification)
|
||||
{
|
||||
ShowMsg(
|
||||
$"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}",
|
||||
GetTranslation("completedSuccessfully"));
|
||||
$"{Localize.copy()} {(isFile ? Localize.fileTitle(): Localize.folderTitle())}",
|
||||
Localize.completedSuccessfully());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogException(nameof(PublicAPIInstance), "Failed to copy file/folder to clipboard", exception);
|
||||
ShowMsgError(GetTranslation("failedToCopy"));
|
||||
ShowMsgError(Localize.failedToCopy());
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -209,14 +208,14 @@ namespace Flow.Launcher
|
|||
if (showDefaultNotification)
|
||||
{
|
||||
ShowMsg(
|
||||
$"{GetTranslation("copy")} {GetTranslation("textTitle")}",
|
||||
GetTranslation("completedSuccessfully"));
|
||||
$"{Localize.copy()} {Localize.textTitle()}",
|
||||
Localize.completedSuccessfully());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception);
|
||||
ShowMsgError(GetTranslation("failedToCopy"));
|
||||
ShowMsgError(Localize.failedToCopy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -248,7 +247,10 @@ namespace Flow.Launcher
|
|||
|
||||
public string GetTranslation(string key) => Internationalization.GetTranslation(key);
|
||||
|
||||
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();
|
||||
public List<PluginPair> GetAllPlugins() => PluginManager.GetAllLoadedPlugins();
|
||||
|
||||
public List<PluginPair> GetAllInitializedPlugins(bool includeFailed) =>
|
||||
PluginManager.GetAllInitializedPlugins(includeFailed);
|
||||
|
||||
public MatchResult FuzzySearch(string query, string stringToCompare) =>
|
||||
StringMatcher.FuzzySearch(query, stringToCompare);
|
||||
|
|
@ -393,18 +395,18 @@ namespace Flow.Launcher
|
|||
}
|
||||
catch (Win32Exception ex) when (ex.NativeErrorCode == 2)
|
||||
{
|
||||
LogError(ClassName, "File Manager not found");
|
||||
LogException(ClassName, "File Manager not found", ex);
|
||||
ShowMsgError(
|
||||
GetTranslation("fileManagerNotFoundTitle"),
|
||||
string.Format(GetTranslation("fileManagerNotFound"), ex.Message)
|
||||
Localize.fileManagerNotFoundTitle(),
|
||||
Localize.fileManagerNotFound()
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogException(ClassName, "Failed to open folder", ex);
|
||||
ShowMsgError(
|
||||
GetTranslation("errorTitle"),
|
||||
string.Format(GetTranslation("folderOpenError"), ex.Message)
|
||||
Localize.errorTitle(),
|
||||
Localize.folderOpenError()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -413,7 +415,7 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (uri.IsFile && !FilesFolders.FileOrLocationExists(uri.LocalPath))
|
||||
{
|
||||
ShowMsgError(GetTranslation("errorTitle"), string.Format(GetTranslation("fileNotFoundError"), uri.LocalPath));
|
||||
ShowMsgError(Localize.errorTitle(), Localize.fileNotFoundError(uri.LocalPath));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -439,8 +441,8 @@ namespace Flow.Launcher
|
|||
var tabOrWindow = browserInfo.OpenInTab ? "tab" : "window";
|
||||
LogException(ClassName, $"Failed to open URL in browser {tabOrWindow}: {path}, {inPrivate ?? browserInfo.EnablePrivate}, {browserInfo.PrivateArg}", e);
|
||||
ShowMsgError(
|
||||
GetTranslation("errorTitle"),
|
||||
GetTranslation("browserOpenError")
|
||||
Localize.errorTitle(),
|
||||
Localize.browserOpenError()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -457,7 +459,7 @@ namespace Flow.Launcher
|
|||
catch (Exception e)
|
||||
{
|
||||
LogException(ClassName, $"Failed to open: {uri.AbsoluteUri}", e);
|
||||
ShowMsgError(GetTranslation("errorTitle"), e.Message);
|
||||
ShowMsgError(Localize.errorTitle(), e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
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:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
|
||||
Title="{DynamicResource releaseNotes}"
|
||||
Width="940"
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
MinHeight="600"
|
||||
Background="{DynamicResource PopuBGColor}"
|
||||
Closed="Window_Closed"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
Foreground="{DynamicResource PopupTextColor}"
|
||||
Loaded="Window_Loaded"
|
||||
ResizeMode="CanResize"
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="32" />
|
||||
<RowDefinition Height="24" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- TitleBar and Control -->
|
||||
|
|
@ -161,18 +162,23 @@
|
|||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
Margin="18 0 18 0">
|
||||
<cc:HyperLink x:Name="SeeMore" Text="{DynamicResource seeMoreReleaseNotes}" />
|
||||
Margin="6 0 18 0">
|
||||
<ui:HyperlinkButton
|
||||
x:Name="SeeMore"
|
||||
Content="{DynamicResource seeMoreReleaseNotes}"
|
||||
NavigateUri="{Binding ReleaseNotes}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Do not use scroll function of MarkdownViewer because it does not support smooth scroll -->
|
||||
<ScrollViewer
|
||||
<ui:ScrollViewerEx
|
||||
x:Name="MarkdownScrollViewer"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
Width="500"
|
||||
Height="500">
|
||||
Height="500"
|
||||
Margin="15 0 0 0"
|
||||
Padding="0 0 15 0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
|
|
@ -193,11 +199,11 @@
|
|||
VerticalScrollBarVisibility="Disabled"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</ui:ScrollViewerEx>
|
||||
|
||||
<!-- 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. -->
|
||||
<!-- Because VerticalAlignment="Stretch" can cause height issue with MarkdownScrollViewer. -->
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
|
|
|
|||
|
|
@ -10,27 +10,27 @@ using System.Windows;
|
|||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Flow.Launcher.Infrastructure.Http;
|
||||
using iNKORE.UI.WPF.Modern;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class ReleaseNotesWindow : Window
|
||||
{
|
||||
private static readonly string ReleaseNotes = Properties.Settings.Default.GithubRepo + "/releases";
|
||||
public string ReleaseNotes => Properties.Settings.Default.GithubRepo + "/releases";
|
||||
|
||||
public ReleaseNotesWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
SeeMore.Uri = ReleaseNotes;
|
||||
ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged += ThemeManager_ActualApplicationThemeChanged;
|
||||
ThemeManager.Current.ActualApplicationThemeChanged += ThemeManager_ActualApplicationThemeChanged;
|
||||
}
|
||||
|
||||
#region Window Events
|
||||
|
||||
private void ThemeManager_ActualApplicationThemeChanged(ModernWpf.ThemeManager sender, object args)
|
||||
private void ThemeManager_ActualApplicationThemeChanged(ThemeManager sender, object args)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (ModernWpf.ThemeManager.Current.ActualApplicationTheme == ModernWpf.ApplicationTheme.Light)
|
||||
if (ThemeManager.Current.ActualApplicationTheme == ApplicationTheme.Light)
|
||||
{
|
||||
MarkdownViewer.MarkdownStyle = (Style)Application.Current.Resources["DocumentStyleGithubLikeLight"];
|
||||
MarkdownViewer.Foreground = Brushes.Black;
|
||||
|
|
@ -58,7 +58,7 @@ namespace Flow.Launcher
|
|||
|
||||
private void Window_Closed(object sender, EventArgs e)
|
||||
{
|
||||
ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged;
|
||||
ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -132,8 +132,8 @@ namespace Flow.Launcher
|
|||
RefreshButton.Visibility = Visibility.Visible;
|
||||
MarkdownViewer.Visibility = Visibility.Collapsed;
|
||||
App.API.ShowMsgError(
|
||||
App.API.GetTranslation("checkNetworkConnectionTitle"),
|
||||
App.API.GetTranslation("checkNetworkConnectionSubTitle"));
|
||||
Localize.checkNetworkConnectionTitle(),
|
||||
Localize.checkNetworkConnectionSubTitle());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -147,7 +147,6 @@ namespace Flow.Launcher
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ namespace Flow.Launcher
|
|||
_ => Constant.IssuesUrl
|
||||
};
|
||||
|
||||
var paragraph = Hyperlink(App.API.GetTranslation("reportWindow_please_open_issue"), websiteUrl);
|
||||
paragraph.Inlines.Add(string.Format(App.API.GetTranslation("reportWindow_upload_log"), log.FullName));
|
||||
var paragraph = Hyperlink(Localize.reportWindow_please_open_issue(), websiteUrl);
|
||||
paragraph.Inlines.Add(Localize.reportWindow_upload_log(log.FullName));
|
||||
paragraph.Inlines.Add("\n");
|
||||
paragraph.Inlines.Add(App.API.GetTranslation("reportWindow_copy_below"));
|
||||
paragraph.Inlines.Add(Localize.reportWindow_copy_below());
|
||||
ErrorTextbox.Document.Blocks.Add(paragraph);
|
||||
|
||||
StringBuilder content = new StringBuilder();
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
<UserControl
|
||||
x:Class="Flow.Launcher.Resources.Controls.Card"
|
||||
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.Resources.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Template>
|
||||
<ControlTemplate TargetType="UserControl">
|
||||
<Border x:Name="BD" HorizontalAlignment="Stretch">
|
||||
<Border.Style>
|
||||
<Style TargetType="{x:Type Border}">
|
||||
<Setter Property="Background" Value="{DynamicResource Color00B}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Color03B}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="5" />
|
||||
<Setter Property="MinHeight" Value="68" />
|
||||
<Setter Property="Padding" Value="0 15 0 15" />
|
||||
<Setter Property="Margin" Value="0 4 0 0" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="Inside">
|
||||
<Setter Property="BorderThickness" Value="0 1 0 0" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Setter Property="Margin" Value="0 0 0 0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="InsideFit">
|
||||
<Setter Property="BorderThickness" Value="0 1 0 0" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Setter Property="Margin" Value="0 0 0 0" />
|
||||
<Setter Property="Padding" Value="35 0 26 0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="First">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="Middle">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0 1 0 0" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger Binding="{Binding Type, RelativeSource={RelativeSource AncestorType=local:Card}}" Value="Last">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0 1 0 0" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition
|
||||
Width="auto"
|
||||
MinWidth="20"
|
||||
MaxWidth="60" />
|
||||
<ColumnDefinition Width="8*" />
|
||||
<ColumnDefinition Width="Auto" MinWidth="30" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentControl
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Margin="0 0 16 0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}" />
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="{x:Type StackPanel}">
|
||||
<Setter Property="Grid.Column" Value="1" />
|
||||
<Setter Property="Width" Value="Auto" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
<TextBlock x:Name="ItemTitle" Text="{Binding Title, RelativeSource={RelativeSource AncestorType=local:Card}}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
|
||||
<Setter Property="Margin" Value="0 0 0 0" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="SubTitle" Text="{Binding Sub, RelativeSource={RelativeSource AncestorType=local:Card}}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="{x:Type TextBlock}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ElementName=SubTitle, Path=Text}" Value="{x:Static sys:String.Empty}">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Color04B}" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Margin" Value="0 0 0 0" />
|
||||
<Setter Property="Padding" Value="0 0 24 0" />
|
||||
<Setter Property="TextWrapping" Value="WrapWithOverflow" />
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="ItemIcon" Text="{Binding Icon, RelativeSource={RelativeSource AncestorType=local:Card}}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="{x:Type TextBlock}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ElementName=ItemIcon, Path=Text}" Value="{x:Static sys:String.Empty}">
|
||||
<Setter Property="Margin" Value="24 0 0 0" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="Grid.Column" Value="0" />
|
||||
<Setter Property="Margin" Value="24 0 16 0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
<Setter Property="FontFamily" Value="/Resources/#Segoe Fluent Icons" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</UserControl.Template>
|
||||
</UserControl>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
using System.Windows;
|
||||
using UserControl = System.Windows.Controls.UserControl;
|
||||
|
||||
namespace Flow.Launcher.Resources.Controls
|
||||
{
|
||||
public partial class Card : UserControl
|
||||
{
|
||||
public enum CardType
|
||||
{
|
||||
Default,
|
||||
Inside,
|
||||
InsideFit,
|
||||
First,
|
||||
Middle,
|
||||
Last
|
||||
}
|
||||
|
||||
public Card()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public string Title
|
||||
{
|
||||
get { return (string)GetValue(TitleProperty); }
|
||||
set { SetValue(TitleProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(string), typeof(Card), new PropertyMetadata(string.Empty));
|
||||
|
||||
public string Sub
|
||||
{
|
||||
get { return (string)GetValue(SubProperty); }
|
||||
set { SetValue(SubProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty SubProperty =
|
||||
DependencyProperty.Register(nameof(Sub), typeof(string), typeof(Card), new PropertyMetadata(string.Empty));
|
||||
|
||||
public string Icon
|
||||
{
|
||||
get { return (string)GetValue(IconProperty); }
|
||||
set { SetValue(IconProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register(nameof(Icon), typeof(string), typeof(Card), new PropertyMetadata(string.Empty));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional content for the UserControl
|
||||
/// </summary>
|
||||
public object AdditionalContent
|
||||
{
|
||||
get { return (object)GetValue(AdditionalContentProperty); }
|
||||
set { SetValue(AdditionalContentProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty AdditionalContentProperty =
|
||||
DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(Card),
|
||||
new PropertyMetadata(null));
|
||||
public CardType Type
|
||||
{
|
||||
get { return (CardType)GetValue(TypeProperty); }
|
||||
set { SetValue(TypeProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty TypeProperty =
|
||||
DependencyProperty.Register(nameof(Type), typeof(CardType), typeof(Card),
|
||||
new PropertyMetadata(CardType.Default));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<UserControl x:Class="Flow.Launcher.Resources.Controls.CardGroup"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance cc:CardGroup}"
|
||||
d:DesignHeight="300" d:DesignWidth="300">
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="cc:Card" x:Key="FirstStyle">
|
||||
<Setter Property="cc:CardGroup.Position" Value="First" />
|
||||
</Style>
|
||||
<Style TargetType="cc:Card" x:Key="MiddleStyle">
|
||||
<Setter Property="cc:CardGroup.Position" Value="Middle" />
|
||||
</Style>
|
||||
<Style TargetType="cc:Card" x:Key="LastStyle">
|
||||
<Setter Property="cc:CardGroup.Position" Value="Last" />
|
||||
</Style>
|
||||
|
||||
<cc:CardGroupCardStyleSelector
|
||||
x:Key="CardStyleSelector"
|
||||
FirstStyle="{StaticResource FirstStyle}"
|
||||
MiddleStyle="{StaticResource MiddleStyle}"
|
||||
LastStyle="{StaticResource LastStyle}" />
|
||||
</UserControl.Resources>
|
||||
<Border Background="{DynamicResource Color00B}" BorderBrush="{DynamicResource Color03B}" BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<ItemsControl ItemsSource="{Binding Content, RelativeSource={RelativeSource AncestorType=cc:CardGroup}}"
|
||||
ItemContainerStyleSelector="{StaticResource CardStyleSelector}" />
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Flow.Launcher.Resources.Controls;
|
||||
|
||||
public partial class CardGroup : UserControl
|
||||
{
|
||||
public enum CardGroupPosition
|
||||
{
|
||||
NotInGroup,
|
||||
First,
|
||||
Middle,
|
||||
Last
|
||||
}
|
||||
|
||||
public new ObservableCollection<Card> Content
|
||||
{
|
||||
get { return (ObservableCollection<Card>)GetValue(ContentProperty); }
|
||||
set { SetValue(ContentProperty, value); }
|
||||
}
|
||||
|
||||
public static new readonly DependencyProperty ContentProperty =
|
||||
DependencyProperty.Register(nameof(Content), typeof(ObservableCollection<Card>), typeof(CardGroup));
|
||||
|
||||
public static readonly DependencyProperty PositionProperty = DependencyProperty.RegisterAttached(
|
||||
"Position", typeof(CardGroupPosition), typeof(CardGroup),
|
||||
new FrameworkPropertyMetadata(CardGroupPosition.NotInGroup, FrameworkPropertyMetadataOptions.AffectsRender)
|
||||
);
|
||||
|
||||
public static void SetPosition(UIElement element, CardGroupPosition value)
|
||||
{
|
||||
element.SetValue(PositionProperty, value);
|
||||
}
|
||||
|
||||
public static CardGroupPosition GetPosition(UIElement element)
|
||||
{
|
||||
return (CardGroupPosition)element.GetValue(PositionProperty);
|
||||
}
|
||||
|
||||
public CardGroup()
|
||||
{
|
||||
InitializeComponent();
|
||||
Content = new ObservableCollection<Card>();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Flow.Launcher.Resources.Controls;
|
||||
|
||||
public class CardGroupCardStyleSelector : StyleSelector
|
||||
{
|
||||
public Style FirstStyle { get; set; }
|
||||
public Style MiddleStyle { get; set; }
|
||||
public Style LastStyle { get; set; }
|
||||
|
||||
public override Style SelectStyle(object item, DependencyObject container)
|
||||
{
|
||||
var itemsControl = ItemsControl.ItemsControlFromItemContainer(container);
|
||||
var index = itemsControl.ItemContainerGenerator.IndexFromContainer(container);
|
||||
|
||||
if (index == 0) return FirstStyle;
|
||||
if (index == itemsControl.Items.Count - 1) return LastStyle;
|
||||
return MiddleStyle;
|
||||
}
|
||||
}
|
||||
253
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
Normal file
253
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
using iNKORE.UI.WPF.Modern.Controls.Helpers;
|
||||
using iNKORE.UI.WPF.Modern.Controls.Primitives;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Flow.Launcher.Resources.Controls
|
||||
{
|
||||
// TODO: Use IsScrollAnimationEnabled property in future: https://github.com/iNKORE-NET/UI.WPF.Modern/pull/347
|
||||
public class CustomScrollViewerEx : ScrollViewer
|
||||
{
|
||||
private double LastVerticalLocation = 0;
|
||||
private double LastHorizontalLocation = 0;
|
||||
|
||||
public CustomScrollViewerEx()
|
||||
{
|
||||
Loaded += OnLoaded;
|
||||
var valueSource = DependencyPropertyHelper.GetValueSource(this, AutoPanningMode.IsEnabledProperty).BaseValueSource;
|
||||
if (valueSource == BaseValueSource.Default)
|
||||
{
|
||||
AutoPanningMode.SetIsEnabled(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
#region Orientation
|
||||
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Orientation),
|
||||
typeof(Orientation),
|
||||
typeof(CustomScrollViewerEx),
|
||||
new PropertyMetadata(Orientation.Vertical));
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AutoHideScrollBars
|
||||
|
||||
public static readonly DependencyProperty AutoHideScrollBarsProperty =
|
||||
ScrollViewerHelper.AutoHideScrollBarsProperty
|
||||
.AddOwner(
|
||||
typeof(CustomScrollViewerEx),
|
||||
new PropertyMetadata(true, OnAutoHideScrollBarsChanged));
|
||||
|
||||
public bool AutoHideScrollBars
|
||||
{
|
||||
get => (bool)GetValue(AutoHideScrollBarsProperty);
|
||||
set => SetValue(AutoHideScrollBarsProperty, value);
|
||||
}
|
||||
|
||||
private static void OnAutoHideScrollBarsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is CustomScrollViewerEx sv)
|
||||
{
|
||||
sv.UpdateVisualState();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
LastVerticalLocation = VerticalOffset;
|
||||
LastHorizontalLocation = HorizontalOffset;
|
||||
UpdateVisualState(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnInitialized(EventArgs e)
|
||||
{
|
||||
base.OnInitialized(e);
|
||||
|
||||
if (Style == null && ReadLocalValue(StyleProperty) == DependencyProperty.UnsetValue)
|
||||
{
|
||||
SetResourceReference(StyleProperty, typeof(ScrollViewer));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnMouseWheel(MouseWheelEventArgs e)
|
||||
{
|
||||
var Direction = GetDirection();
|
||||
ScrollViewerBehavior.SetIsAnimating(this, true);
|
||||
|
||||
if (Direction == Orientation.Vertical)
|
||||
{
|
||||
if (ScrollableHeight > 0)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
var WheelChange = e.Delta * (ViewportHeight / 1.5) / ActualHeight;
|
||||
var newOffset = LastVerticalLocation - WheelChange;
|
||||
|
||||
if (newOffset < 0)
|
||||
{
|
||||
newOffset = 0;
|
||||
}
|
||||
|
||||
if (newOffset > ScrollableHeight)
|
||||
{
|
||||
newOffset = ScrollableHeight;
|
||||
}
|
||||
|
||||
if (newOffset == LastVerticalLocation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ScrollToVerticalOffset(LastVerticalLocation);
|
||||
|
||||
ScrollToValue(newOffset, Direction);
|
||||
LastVerticalLocation = newOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ScrollableWidth > 0)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
var WheelChange = e.Delta * (ViewportWidth / 1.5) / ActualWidth;
|
||||
var newOffset = LastHorizontalLocation - WheelChange;
|
||||
|
||||
if (newOffset < 0)
|
||||
{
|
||||
newOffset = 0;
|
||||
}
|
||||
|
||||
if (newOffset > ScrollableWidth)
|
||||
{
|
||||
newOffset = ScrollableWidth;
|
||||
}
|
||||
|
||||
if (newOffset == LastHorizontalLocation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ScrollToHorizontalOffset(LastHorizontalLocation);
|
||||
|
||||
ScrollToValue(newOffset, Direction);
|
||||
LastHorizontalLocation = newOffset;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnScrollChanged(ScrollChangedEventArgs e)
|
||||
{
|
||||
base.OnScrollChanged(e);
|
||||
if (!ScrollViewerBehavior.GetIsAnimating(this))
|
||||
{
|
||||
LastVerticalLocation = VerticalOffset;
|
||||
LastHorizontalLocation = HorizontalOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private Orientation GetDirection()
|
||||
{
|
||||
var isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
|
||||
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
return isShiftDown ? Orientation.Vertical : Orientation.Horizontal;
|
||||
}
|
||||
else
|
||||
{
|
||||
return isShiftDown ? Orientation.Horizontal : Orientation.Vertical;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Causes the <see cref="ScrollViewerEx"/> to load a new view into the viewport using the specified offsets and zoom factor.
|
||||
/// </summary>
|
||||
/// <param name="horizontalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableWidth"/> that specifies the distance the content should be scrolled horizontally.</param>
|
||||
/// <param name="verticalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableHeight"/> that specifies the distance the content should be scrolled vertically.</param>
|
||||
/// <param name="zoomFactor">A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor.</param>
|
||||
/// <returns><see langword="true"/> if the view is changed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor)
|
||||
{
|
||||
return ChangeView(horizontalOffset, verticalOffset, zoomFactor, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Causes the <see cref="ScrollViewerEx"/> to load a new view into the viewport using the specified offsets and zoom factor, and optionally disables scrolling animation.
|
||||
/// </summary>
|
||||
/// <param name="horizontalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableWidth"/> that specifies the distance the content should be scrolled horizontally.</param>
|
||||
/// <param name="verticalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableHeight"/> that specifies the distance the content should be scrolled vertically.</param>
|
||||
/// <param name="zoomFactor">A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor.</param>
|
||||
/// <param name="disableAnimation"><see langword="true"/> to disable zoom/pan animations while changing the view; otherwise, <see langword="false"/>. The default is false.</param>
|
||||
/// <returns><see langword="true"/> if the view is changed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor, bool disableAnimation)
|
||||
{
|
||||
if (disableAnimation)
|
||||
{
|
||||
if (horizontalOffset.HasValue)
|
||||
{
|
||||
ScrollToHorizontalOffset(horizontalOffset.Value);
|
||||
}
|
||||
|
||||
if (verticalOffset.HasValue)
|
||||
{
|
||||
ScrollToVerticalOffset(verticalOffset.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (horizontalOffset.HasValue)
|
||||
{
|
||||
ScrollToHorizontalOffset(LastHorizontalLocation);
|
||||
ScrollToValue(Math.Min(ScrollableWidth, horizontalOffset.Value), Orientation.Horizontal);
|
||||
LastHorizontalLocation = horizontalOffset.Value;
|
||||
}
|
||||
|
||||
if (verticalOffset.HasValue)
|
||||
{
|
||||
ScrollToVerticalOffset(LastVerticalLocation);
|
||||
ScrollToValue(Math.Min(ScrollableHeight, verticalOffset.Value), Orientation.Vertical);
|
||||
LastVerticalLocation = verticalOffset.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ScrollToValue(double value, Orientation Direction)
|
||||
{
|
||||
if (Direction == Orientation.Vertical)
|
||||
{
|
||||
ScrollToVerticalOffset(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ScrollToHorizontalOffset(value);
|
||||
}
|
||||
|
||||
ScrollViewerBehavior.SetIsAnimating(this, false);
|
||||
}
|
||||
|
||||
private void UpdateVisualState(bool useTransitions = true)
|
||||
{
|
||||
var stateName = AutoHideScrollBars ? "NoIndicator" : "MouseIndicator";
|
||||
VisualStateManager.GoToState(this, stateName, useTransitions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
<UserControl
|
||||
x:Class="Flow.Launcher.Resources.Controls.ExCard"
|
||||
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.Resources.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Template>
|
||||
<ControlTemplate TargetType="UserControl">
|
||||
<Expander
|
||||
x:Name="expanderHeader"
|
||||
Padding="0"
|
||||
BorderThickness="1"
|
||||
SnapsToDevicePixels="False">
|
||||
<Expander.Style>
|
||||
<Style TargetType="{x:Type Expander}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
|
||||
<Setter Property="Background" Value="{DynamicResource Color00B}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Color03B}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Expander}">
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="5"
|
||||
SnapsToDevicePixels="true">
|
||||
<DockPanel>
|
||||
<ToggleButton
|
||||
x:Name="HeaderSite"
|
||||
MinWidth="0"
|
||||
MinHeight="68"
|
||||
Margin="0,0,0,0"
|
||||
Padding="0,0,0,0"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Header}"
|
||||
ContentTemplate="{TemplateBinding HeaderTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
|
||||
DockPanel.Dock="Top"
|
||||
FocusVisualStyle="{DynamicResource ExpanderHeaderFocusVisual}"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontStretch="{TemplateBinding FontStretch}"
|
||||
FontStyle="{TemplateBinding FontStyle}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=Expander}}">
|
||||
<ToggleButton.Style>
|
||||
<Style TargetType="{x:Type ToggleButton}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type ToggleButton}">
|
||||
<Border
|
||||
x:Name="ToggleBtn"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{DynamicResource Color00B}"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5">
|
||||
<Grid SnapsToDevicePixels="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="30" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter
|
||||
Grid.Column="0"
|
||||
Margin="0,0,0,0"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="True" />
|
||||
<Grid
|
||||
x:Name="ChevronGrid"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,18,0"
|
||||
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" />
|
||||
<Setter TargetName="ToggleBtn" Property="CornerRadius" Value="5 5 0 0" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="true">
|
||||
<Setter TargetName="circle" Property="Stroke" Value="Transparent" />
|
||||
<Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color05B}" />
|
||||
<Setter TargetName="ToggleBtn" Property="Background" Value="{DynamicResource CustomExpanderHover}" />
|
||||
</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>
|
||||
</ToggleButton.Style>
|
||||
</ToggleButton>
|
||||
<Border x:Name="ContentPresenterBorder" BorderThickness="0">
|
||||
<ContentPresenter
|
||||
x:Name="ExpandSite"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
DockPanel.Dock="Bottom"
|
||||
Focusable="false" />
|
||||
<Border.LayoutTransform>
|
||||
<ScaleTransform ScaleY="0" />
|
||||
</Border.LayoutTransform>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsExpanded" Value="true">
|
||||
<Setter TargetName="ExpandSite" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="ContentPresenterBorder" Property="BorderThickness" Value="0,0,0,0" />
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="ContentPresenterBorder"
|
||||
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
|
||||
From="0.0"
|
||||
To="1.0"
|
||||
Duration="00:00:00.00" />
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="ContentPresenterBorder"
|
||||
Storyboard.TargetProperty="(Border.Opacity)"
|
||||
From="0.0"
|
||||
To="1.0"
|
||||
Duration="00:00:00.00" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="ContentPresenterBorder"
|
||||
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
|
||||
From="1.0"
|
||||
To="0"
|
||||
Duration="00:00:00.00" />
|
||||
<!-- Animation 00:00:00.167 -->
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="ContentPresenterBorder"
|
||||
Storyboard.TargetProperty="(Border.Opacity)"
|
||||
From="1.0"
|
||||
To="0.0"
|
||||
Duration="00:00:00.00" />
|
||||
<!-- Animation 00:00:00.167 -->
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
<Trigger Property="ExpandDirection" Value="Right">
|
||||
<Setter TargetName="ExpandSite" Property="DockPanel.Dock" Value="Right" />
|
||||
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Left" />
|
||||
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource ExpanderRightHeaderStyle}" />
|
||||
</Trigger>
|
||||
<Trigger Property="ExpandDirection" Value="Up">
|
||||
<Setter TargetName="ExpandSite" Property="DockPanel.Dock" Value="Top" />
|
||||
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Bottom" />
|
||||
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource ExpanderUpHeaderStyle}" />
|
||||
</Trigger>
|
||||
<Trigger Property="ExpandDirection" Value="Left">
|
||||
<Setter TargetName="ExpandSite" Property="DockPanel.Dock" Value="Left" />
|
||||
<Setter TargetName="HeaderSite" Property="DockPanel.Dock" Value="Right" />
|
||||
<Setter TargetName="HeaderSite" Property="Style" Value="{StaticResource ExpanderLeftHeaderStyle}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Expander.Style>
|
||||
<Expander.Header>
|
||||
<Border Margin="0" Padding="0,12,0,12">
|
||||
<Grid Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Expander}}, Path=ActualWidth}" HorizontalAlignment="Left">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition
|
||||
Width="auto"
|
||||
MinWidth="20"
|
||||
MaxWidth="60" />
|
||||
<ColumnDefinition Width="7*" />
|
||||
<ColumnDefinition Width="Auto" MinWidth="30" />
|
||||
<ColumnDefinition Width="Auto" MinWidth="30" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentControl
|
||||
x:Name="firstContentPresenter"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,14,0"
|
||||
HorizontalAlignment="Right"
|
||||
Content="{Binding SideContent, RelativeSource={RelativeSource AncestorType=local:ExCard}}" />
|
||||
<TextBlock
|
||||
x:Name="ItemIcon"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Icon, RelativeSource={RelativeSource AncestorType=local:ExCard}}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="{x:Type TextBlock}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ElementName=ItemIcon, Path=Text}" Value="{x:Static sys:String.Empty}">
|
||||
<Setter Property="Margin" Value="24,0,0,0" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="Grid.Column" Value="0" />
|
||||
<Setter Property="Margin" Value="24,0,16,0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
<Setter Property="FontFamily" Value="/Resources/#Segoe Fluent Icons" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<StackPanel Grid.Column="1" Margin="0,0,14,0">
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="{x:Type StackPanel}">
|
||||
<Setter Property="Grid.Column" Value="1" />
|
||||
<Setter Property="Width" Value="Auto" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
<TextBlock x:Name="ItemTitle" Text="{Binding Title, RelativeSource={RelativeSource AncestorType=local:ExCard}}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
|
||||
<Setter Property="Margin" Value="0,0,0,0" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="SubTitle" Text="{Binding Sub, RelativeSource={RelativeSource AncestorType=local:ExCard}}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="{x:Type TextBlock}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ElementName=SubTitle, Path=Text}" Value="{x:Static sys:String.Empty}">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Color04B}" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Margin" Value="0,0,0,0" />
|
||||
<Setter Property="Padding" Value="0,0,24,0" />
|
||||
<Setter Property="TextWrapping" Value="WrapWithOverflow" />
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Expander.Header>
|
||||
<Grid
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
FlowDirection="LeftToRight">
|
||||
<StackPanel Margin="0,0,0,0" Orientation="Vertical">
|
||||
<ContentControl
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="4"
|
||||
Margin="0,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Content}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Expander>
|
||||
</ControlTemplate>
|
||||
</UserControl.Template>
|
||||
</UserControl>
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Flow.Launcher.Resources.Controls
|
||||
{
|
||||
public partial class ExCard : UserControl
|
||||
{
|
||||
public ExCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
public string Title
|
||||
{
|
||||
get { return (string)GetValue(TitleProperty); }
|
||||
set { SetValue(TitleProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty));
|
||||
|
||||
public string Sub
|
||||
{
|
||||
get { return (string)GetValue(SubProperty); }
|
||||
set { SetValue(SubProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty SubProperty =
|
||||
DependencyProperty.Register(nameof(Sub), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty));
|
||||
|
||||
public string Icon
|
||||
{
|
||||
get { return (string)GetValue(IconProperty); }
|
||||
set { SetValue(IconProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register(nameof(Icon), typeof(string), typeof(ExCard), new PropertyMetadata(string.Empty));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional content for the UserControl
|
||||
/// </summary>
|
||||
public object AdditionalContent
|
||||
{
|
||||
get { return (object)GetValue(AdditionalContentProperty); }
|
||||
set { SetValue(AdditionalContentProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty AdditionalContentProperty =
|
||||
DependencyProperty.Register(nameof(AdditionalContent), typeof(object), typeof(ExCard),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public object SideContent
|
||||
{
|
||||
get { return (object)GetValue(SideContentProperty); }
|
||||
set { SetValue(SideContentProperty, value); }
|
||||
}
|
||||
public static readonly DependencyProperty SideContentProperty =
|
||||
DependencyProperty.Register(nameof(SideContent), typeof(object), typeof(ExCard),
|
||||
new PropertyMetadata(null));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<UserControl x:Class="Flow.Launcher.Resources.Controls.HyperLink"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300">
|
||||
<TextBlock>
|
||||
<Hyperlink NavigateUri="{Binding Uri, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
RequestNavigate="Hyperlink_OnRequestNavigate">
|
||||
<Run Text="{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}}" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</UserControl>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Navigation;
|
||||
|
||||
namespace Flow.Launcher.Resources.Controls;
|
||||
|
||||
public partial class HyperLink : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty UriProperty = DependencyProperty.Register(
|
||||
nameof(Uri), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string))
|
||||
);
|
||||
|
||||
public string Uri
|
||||
{
|
||||
get => (string)GetValue(UriProperty);
|
||||
set => SetValue(UriProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
|
||||
nameof(Text), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string))
|
||||
);
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public HyperLink()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e)
|
||||
{
|
||||
App.API.OpenUrl(e.Uri);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<UserControl
|
||||
x:Class="Flow.Launcher.Resources.Controls.InfoBar"
|
||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
d:DesignHeight="45"
|
||||
d:DesignWidth="400"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources />
|
||||
<Grid>
|
||||
<Border
|
||||
x:Name="PART_Border"
|
||||
MinHeight="48"
|
||||
Padding="18 18 18 18"
|
||||
Background="{DynamicResource InfoBarInfoBG}"
|
||||
BorderBrush="{DynamicResource Color03B}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" MinWidth="24" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<Border
|
||||
x:Name="PART_IconBorder"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="0 0 12 0"
|
||||
VerticalAlignment="Top"
|
||||
CornerRadius="10">
|
||||
<ui:FontIcon
|
||||
x:Name="PART_Icon"
|
||||
Margin="1 0 0 1"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource Color01B}"
|
||||
Visibility="Visible" />
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="PART_StackPanel"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
x:Name="PART_Title"
|
||||
Margin="0 0 12 0"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Color05B}"
|
||||
Text="{Binding RelativeSource={RelativeSource AncestorType=cc:InfoBar}, Path=Title}" />
|
||||
<TextBlock
|
||||
x:Name="PART_Message"
|
||||
Foreground="{DynamicResource Color05B}"
|
||||
Text="{Binding RelativeSource={RelativeSource AncestorType=cc:InfoBar}, Path=Message}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<Button
|
||||
x:Name="PART_CloseButton"
|
||||
Grid.Column="2"
|
||||
Width="32"
|
||||
Height="32"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Close InfoBar"
|
||||
Click="PART_CloseButton_Click"
|
||||
Content=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12"
|
||||
ToolTip="Close"
|
||||
Visibility="Visible" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Flow.Launcher.Resources.Controls
|
||||
{
|
||||
public partial class InfoBar : UserControl
|
||||
{
|
||||
public InfoBar()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += InfoBar_Loaded;
|
||||
}
|
||||
|
||||
private void InfoBar_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateStyle();
|
||||
UpdateTitleVisibility();
|
||||
UpdateMessageVisibility();
|
||||
UpdateOrientation();
|
||||
UpdateIconAlignmentAndMargin();
|
||||
UpdateIconVisibility();
|
||||
UpdateCloseButtonVisibility();
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TypeProperty =
|
||||
DependencyProperty.Register(nameof(Type), typeof(InfoBarType), typeof(InfoBar), new PropertyMetadata(InfoBarType.Info, OnTypeChanged));
|
||||
|
||||
public InfoBarType Type
|
||||
{
|
||||
get => (InfoBarType)GetValue(TypeProperty);
|
||||
set => SetValue(TypeProperty, value);
|
||||
}
|
||||
|
||||
private static void OnTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is InfoBar infoBar)
|
||||
{
|
||||
infoBar.UpdateStyle();
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty MessageProperty =
|
||||
DependencyProperty.Register(nameof(Message), typeof(string), typeof(InfoBar), new PropertyMetadata(string.Empty, OnMessageChanged));
|
||||
|
||||
public string Message
|
||||
{
|
||||
get => (string)GetValue(MessageProperty);
|
||||
set
|
||||
{
|
||||
SetValue(MessageProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is InfoBar infoBar)
|
||||
{
|
||||
infoBar.UpdateMessageVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMessageVisibility()
|
||||
{
|
||||
PART_Message.Visibility = string.IsNullOrEmpty(Message) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(string), typeof(InfoBar), new PropertyMetadata(string.Empty, OnTitleChanged));
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => (string)GetValue(TitleProperty);
|
||||
set
|
||||
{
|
||||
SetValue(TitleProperty, value);
|
||||
UpdateTitleVisibility(); // Visibility update when change Title
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is InfoBar infoBar)
|
||||
{
|
||||
infoBar.UpdateTitleVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTitleVisibility()
|
||||
{
|
||||
PART_Title.Visibility = string.IsNullOrEmpty(Title) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsIconVisibleProperty =
|
||||
DependencyProperty.Register(nameof(IsIconVisible), typeof(bool), typeof(InfoBar), new PropertyMetadata(true, OnIsIconVisibleChanged));
|
||||
|
||||
public bool IsIconVisible
|
||||
{
|
||||
get => (bool)GetValue(IsIconVisibleProperty);
|
||||
set => SetValue(IsIconVisibleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty LengthProperty =
|
||||
DependencyProperty.Register(nameof(Length), typeof(InfoBarLength), typeof(InfoBar), new PropertyMetadata(InfoBarLength.Short, OnLengthChanged));
|
||||
|
||||
public InfoBarLength Length
|
||||
{
|
||||
get { return (InfoBarLength)GetValue(LengthProperty); }
|
||||
set { SetValue(LengthProperty, value); }
|
||||
}
|
||||
|
||||
private static void OnLengthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is InfoBar infoBar)
|
||||
{
|
||||
infoBar.UpdateOrientation();
|
||||
infoBar.UpdateIconAlignmentAndMargin();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOrientation()
|
||||
{
|
||||
PART_StackPanel.Orientation = Length == InfoBarLength.Long ? Orientation.Vertical : Orientation.Horizontal;
|
||||
}
|
||||
|
||||
private void UpdateIconAlignmentAndMargin()
|
||||
{
|
||||
if (Length == InfoBarLength.Short)
|
||||
{
|
||||
PART_IconBorder.VerticalAlignment = VerticalAlignment.Center;
|
||||
PART_IconBorder.Margin = new Thickness(0, 0, 12, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
PART_IconBorder.VerticalAlignment = VerticalAlignment.Top;
|
||||
PART_IconBorder.Margin = new Thickness(0, 2, 12, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ClosableProperty =
|
||||
DependencyProperty.Register(nameof(Closable), typeof(bool), typeof(InfoBar), new PropertyMetadata(true, OnClosableChanged));
|
||||
|
||||
public bool Closable
|
||||
{
|
||||
get => (bool)GetValue(ClosableProperty);
|
||||
set => SetValue(ClosableProperty, value);
|
||||
}
|
||||
|
||||
private void PART_CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void UpdateStyle()
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case InfoBarType.Info:
|
||||
PART_Border.Background = (Brush)FindResource("InfoBarInfoBG");
|
||||
PART_IconBorder.Background = (Brush)FindResource("InfoBarInfoIcon");
|
||||
PART_Icon.Glyph = "\xF13F";
|
||||
break;
|
||||
case InfoBarType.Success:
|
||||
PART_Border.Background = (Brush)FindResource("InfoBarSuccessBG");
|
||||
PART_IconBorder.Background = (Brush)FindResource("InfoBarSuccessIcon");
|
||||
PART_Icon.Glyph = "\xF13E";
|
||||
break;
|
||||
case InfoBarType.Warning:
|
||||
PART_Border.Background = (Brush)FindResource("InfoBarWarningBG");
|
||||
PART_IconBorder.Background = (Brush)FindResource("InfoBarWarningIcon");
|
||||
PART_Icon.Glyph = "\xF13C";
|
||||
break;
|
||||
case InfoBarType.Error:
|
||||
PART_Border.Background = (Brush)FindResource("InfoBarErrorBG");
|
||||
PART_IconBorder.Background = (Brush)FindResource("InfoBarErrorIcon");
|
||||
PART_Icon.Glyph = "\xF13D";
|
||||
break;
|
||||
default:
|
||||
PART_Border.Background = (Brush)FindResource("InfoBarInfoBG");
|
||||
PART_IconBorder.Background = (Brush)FindResource("InfoBarInfoIcon");
|
||||
PART_Icon.Glyph = "\xF13F";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnIsIconVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var infoBar = (InfoBar)d;
|
||||
infoBar.UpdateIconVisibility();
|
||||
}
|
||||
|
||||
private static void OnClosableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var infoBar = (InfoBar)d;
|
||||
infoBar.UpdateCloseButtonVisibility();
|
||||
}
|
||||
|
||||
private void UpdateIconVisibility()
|
||||
{
|
||||
PART_IconBorder.Visibility = IsIconVisible ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void UpdateCloseButtonVisibility()
|
||||
{
|
||||
PART_CloseButton.Visibility = Closable ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public enum InfoBarType
|
||||
{
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public enum InfoBarLength
|
||||
{
|
||||
Short,
|
||||
Long
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
xmlns:converters="clr-namespace:Flow.Launcher.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:viewModel="clr-namespace:Flow.Launcher.ViewModel"
|
||||
d:DataContext="{d:DesignInstance viewModel:PluginViewModel}"
|
||||
d:DesignHeight="300"
|
||||
|
|
@ -66,6 +66,7 @@
|
|||
Text="{DynamicResource priority}"
|
||||
ToolTip="{DynamicResource priorityToolTip}" />
|
||||
<ui:NumberBox
|
||||
MinWidth="120"
|
||||
Margin="0 0 8 0"
|
||||
Maximum="999"
|
||||
Minimum="-999"
|
||||
|
|
@ -89,6 +90,7 @@
|
|||
ToolTip="{DynamicResource searchDelayToolTip}" />
|
||||
<ui:NumberBox
|
||||
Width="120"
|
||||
MinWidth="120"
|
||||
Margin="0 0 8 0"
|
||||
IsEnabled="{Binding SearchDelayEnabled}"
|
||||
Maximum="1000"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ModernWpf.Controls;
|
||||
using iNKORE.UI.WPF.Modern.Controls;
|
||||
|
||||
namespace Flow.Launcher.Resources.Controls;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,7 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Flow.Launcher.Resources.Pages"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
Title="WelcomePage1"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
mc:Ignorable="d">
|
||||
|
|
@ -99,11 +99,11 @@
|
|||
</Style.Triggers>
|
||||
</Style>
|
||||
</Page.Resources>
|
||||
<ScrollViewer>
|
||||
<ui:ScrollViewerEx>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="250" />
|
||||
<RowDefinition Height="340"/>
|
||||
<RowDefinition Height="340" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" HorizontalAlignment="Stretch">
|
||||
|
|
@ -156,7 +156,8 @@
|
|||
<TextBlock
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource Welcome_Page1_Title}" TextWrapping="WrapWithOverflow"/>
|
||||
Text="{DynamicResource Welcome_Page1_Title}"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<TextBlock
|
||||
Margin="0 10 24 0"
|
||||
FontSize="14"
|
||||
|
|
@ -185,5 +186,5 @@
|
|||
</StackPanel>
|
||||
</Canvas>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</ui:ScrollViewerEx>
|
||||
</ui:Page>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
|
||||
xmlns:local="clr-namespace:Flow.Launcher.Resources.Pages"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
Title="WelcomePage2"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
mc:Ignorable="d">
|
||||
|
|
@ -34,11 +34,11 @@
|
|||
</Style.Triggers>
|
||||
</Style>
|
||||
</Page.Resources>
|
||||
<ScrollViewer>
|
||||
<ui:ScrollViewerEx>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="250" />
|
||||
<RowDefinition Height="340"/>
|
||||
<RowDefinition Height="340" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" HorizontalAlignment="Stretch">
|
||||
|
|
@ -89,12 +89,13 @@
|
|||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Visible">
|
||||
<StackPanel Margin="24 20 24 20">
|
||||
<Grid Grid.Row="1">
|
||||
<StackPanel Margin="24 20 24 20">
|
||||
<TextBlock
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource Welcome_Page2_Title}" TextWrapping="WrapWithOverflow"/>
|
||||
Text="{DynamicResource Welcome_Page2_Title}"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
<TextBlock
|
||||
Margin="0 10 0 0"
|
||||
FontSize="14"
|
||||
|
|
@ -119,7 +120,7 @@
|
|||
WindowTitle="{DynamicResource flowlauncherHotkey}" />
|
||||
</StackPanel>
|
||||
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</ui:ScrollViewerEx>
|
||||
</ui:Page>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
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:ikw="http://schemas.inkore.net/lib/ui/wpf"
|
||||
xmlns:local="clr-namespace:Flow.Launcher.Resources.Pages"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
Title="WelcomePage3"
|
||||
VerticalAlignment="Stretch"
|
||||
mc:Ignorable="d">
|
||||
|
|
@ -40,75 +41,97 @@
|
|||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource Welcome_Page3_Title}" />
|
||||
<ScrollViewer
|
||||
<ui:ScrollViewerEx
|
||||
Grid.Row="1"
|
||||
Height="478"
|
||||
Margin="0 0 0 0"
|
||||
Height="483"
|
||||
HorizontalAlignment="Stretch"
|
||||
FontSize="13">
|
||||
<StackPanel Margin="24 0 24 0">
|
||||
<Border
|
||||
BorderBrush="{DynamicResource Color03B}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="5">
|
||||
<StackPanel>
|
||||
<cc:Card
|
||||
Title="{DynamicResource HotkeyUpDownDesc}"
|
||||
BorderThickness="0 0 0 0"
|
||||
Type="Inside">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="←+→" Type="Small" />
|
||||
</StackPanel>
|
||||
</cc:Card>
|
||||
<cc:Card
|
||||
Title="{DynamicResource HotkeyLeftRightDesc}"
|
||||
BorderThickness="0 0 0 0"
|
||||
Type="Inside">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="↑+↓" Type="Small" />
|
||||
</StackPanel>
|
||||
</cc:Card>
|
||||
<cc:Card
|
||||
Title="{DynamicResource HotkeyESCDesc}"
|
||||
BorderThickness="0 0 0 0"
|
||||
Type="Inside">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="ESC" Type="Small" />
|
||||
</StackPanel>
|
||||
</cc:Card>
|
||||
<cc:Card
|
||||
Title="{DynamicResource HotkeyRunDesc}"
|
||||
BorderThickness="0 0 0 0"
|
||||
Type="Inside">
|
||||
<cc:HotkeyDisplay Keys="ENTER" Type="Small" />
|
||||
</cc:Card>
|
||||
<cc:Card
|
||||
Title="{DynamicResource HotkeyShiftEnterDesc}"
|
||||
BorderThickness="0 0 0 0"
|
||||
Type="Inside">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="SHIFT+ENTER" Type="Small" />
|
||||
</StackPanel>
|
||||
</cc:Card>
|
||||
<cc:Card
|
||||
Title="{DynamicResource HotkeyCtrlEnterDesc}"
|
||||
BorderThickness="0 0 0 0"
|
||||
Type="Inside">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="CTRL+ENTER" Type="Small" />
|
||||
</StackPanel>
|
||||
</cc:Card>
|
||||
<cc:Card
|
||||
Title="{DynamicResource HotkeyCtrlShiftEnterDesc}"
|
||||
BorderThickness="0 0 0 0"
|
||||
Type="Inside">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="CTRL+SHIFT+ENTER" Type="Small" />
|
||||
</StackPanel>
|
||||
</cc:Card>
|
||||
<ui:SettingsCard
|
||||
Background="Transparent"
|
||||
BorderThickness="0 0 0 0"
|
||||
Header="{DynamicResource HotkeyUpDownDesc}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="←+→" Type="Small" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ui:SettingsCard>
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{DynamicResource Color03B}"
|
||||
BorderThickness="0" />
|
||||
|
||||
<ui:SettingsCard
|
||||
Background="Transparent"
|
||||
BorderThickness="0 0 0 0"
|
||||
Header="{DynamicResource HotkeyLeftRightDesc}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="↑+↓" Type="Small" />
|
||||
</StackPanel>
|
||||
</ui:SettingsCard>
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{DynamicResource Color03B}"
|
||||
BorderThickness="0" />
|
||||
|
||||
<ui:SettingsCard
|
||||
Background="Transparent"
|
||||
BorderThickness="0 0 0 0"
|
||||
Header="{DynamicResource HotkeyESCDesc}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="ESC" Type="Small" />
|
||||
</StackPanel>
|
||||
</ui:SettingsCard>
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{DynamicResource Color03B}"
|
||||
BorderThickness="0" />
|
||||
|
||||
<ui:SettingsCard
|
||||
Background="Transparent"
|
||||
BorderThickness="0 0 0 0"
|
||||
Header="{DynamicResource HotkeyRunDesc}">
|
||||
<cc:HotkeyDisplay Keys="ENTER" Type="Small" />
|
||||
</ui:SettingsCard>
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{DynamicResource Color03B}"
|
||||
BorderThickness="0" />
|
||||
|
||||
<ui:SettingsCard
|
||||
Background="Transparent"
|
||||
BorderThickness="0 0 0 0"
|
||||
Header="{DynamicResource HotkeyShiftEnterDesc}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="SHIFT+ENTER" Type="Small" />
|
||||
</StackPanel>
|
||||
</ui:SettingsCard>
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{DynamicResource Color03B}"
|
||||
BorderThickness="0" />
|
||||
|
||||
<ui:SettingsCard
|
||||
Background="Transparent"
|
||||
BorderThickness="0 0 0 0"
|
||||
Header="{DynamicResource HotkeyCtrlEnterDesc}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="CTRL+ENTER" Type="Small" />
|
||||
</StackPanel>
|
||||
</ui:SettingsCard>
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{DynamicResource Color03B}"
|
||||
BorderThickness="0" />
|
||||
|
||||
<ui:SettingsCard
|
||||
Background="Transparent"
|
||||
BorderThickness="0 0 0 0"
|
||||
Header="{DynamicResource HotkeyCtrlShiftEnterDesc}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<cc:HotkeyDisplay Keys="CTRL+SHIFT+ENTER" Type="Small" />
|
||||
</StackPanel>
|
||||
</ui:SettingsCard>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ui:ScrollViewerEx>
|
||||
</Grid>
|
||||
</ui:Page>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue