Merge branch 'dev' into enable_windows_key

This commit is contained in:
Jeremy Wu 2025-02-18 07:52:29 +11:00 committed by GitHub
commit fccab99644
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 674 additions and 112 deletions

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Logger;
using System;
using System.Collections.Generic;
using System.Threading;
@ -21,7 +21,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
public static List<UserPlugin> UserPlugins { get; private set; }
public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
public static async Task<bool> UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
{
try
{
@ -31,8 +31,14 @@ namespace Flow.Launcher.Core.ExternalPlugins
{
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
UserPlugins = results;
lastFetchedAt = DateTime.Now;
// If the results are empty, we shouldn't update the manifest because the results are invalid.
if (results.Count != 0)
{
UserPlugins = results;
lastFetchedAt = DateTime.Now;
return true;
}
}
}
catch (Exception e)
@ -43,6 +49,8 @@ namespace Flow.Launcher.Core.ExternalPlugins
{
manifestUpdateLock.Release();
}
return false;
}
}
}

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
@ -121,10 +120,10 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
return _api.HttpGetStreamAsync(url, token);
}
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null,
CancellationToken token = default)
{
return _api.HttpDownloadAsync(url, filePath, token);
return _api.HttpDownloadAsync(url, filePath, reportProgress, token);
}
public void AddActionKeyword(string pluginId, string newActionKeyword)
@ -162,13 +161,11 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
}
public void OpenUrl(string url, bool? inPrivate = null)
{
_api.OpenUrl(url, inPrivate);
}
public void OpenAppUri(string appUri)
{
_api.OpenAppUri(appUri);

View file

@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Threading;
@ -25,14 +26,13 @@ namespace Flow.Launcher.Core.Plugin
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
// Prevent Python from writing .py[co] files.
// Because .pyc contains location infos which will prevent python portable.
_startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
_startInfo.ArgumentList.Add("-B");
}
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
@ -50,10 +50,53 @@ namespace Flow.Launcher.Core.Plugin
// TODO: Async Action
return Execute(_startInfo);
}
public override async Task InitAsync(PluginInitContext context)
{
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
_startInfo.ArgumentList.Add("");
// Run .py files via `-c <code>`
if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
{
var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
var libDirectory = Path.Combine(rootDirectory, "lib");
var libPyWin32Directory = Path.Combine(libDirectory, "win32");
var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
var pluginDirectory = Path.Combine(rootDirectory, "plugin");
// This makes it easier for plugin authors to import their own modules.
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
_startInfo.ArgumentList.Add("-c");
_startInfo.ArgumentList.Add(
$"""
import sys
sys.path.append(r'{rootDirectory}')
sys.path.append(r'{libDirectory}')
sys.path.append(r'{libPyWin32LibDirectory}')
sys.path.append(r'{libPyWin32Directory}')
sys.path.append(r'{pluginDirectory}')
import runpy
runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__')
"""
);
// Plugins always expect the JSON data to be in the third argument
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
_startInfo.ArgumentList.Add("");
}
// Run .pyz files as is
else
{
// No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now,
// but the plugins still expect data to be sent as the third argument, so we're keeping
// the flag here, even though it's not necessary anymore.
_startInfo.ArgumentList.Add("-B");
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
// Plugins always expect the JSON data to be in the third argument
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
_startInfo.ArgumentList.Add("");
}
await base.InitAsync(context);
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
}

View file

@ -26,14 +26,45 @@ namespace Flow.Launcher.Core.Plugin
var path = Path.Combine(Constant.ProgramDirectory, JsonRpc);
StartInfo.EnvironmentVariables["PYTHONPATH"] = path;
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
StartInfo.ArgumentList.Add("-B");
StartInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
}
public override async Task InitAsync(PluginInitContext context)
{
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
// Run .py files via `-c <code>`
if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
{
var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
var libDirectory = Path.Combine(rootDirectory, "lib");
var libPyWin32Directory = Path.Combine(libDirectory, "win32");
var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
var pluginDirectory = Path.Combine(rootDirectory, "plugin");
var filePath = context.CurrentPluginMetadata.ExecuteFilePath;
// This makes it easier for plugin authors to import their own modules.
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
StartInfo.ArgumentList.Add("-c");
StartInfo.ArgumentList.Add(
$"""
import sys
sys.path.append(r'{rootDirectory}')
sys.path.append(r'{libDirectory}')
sys.path.append(r'{libPyWin32LibDirectory}')
sys.path.append(r'{libPyWin32Directory}')
sys.path.append(r'{pluginDirectory}')
import runpy
runpy.run_path(r'{filePath}', None, '__main__')
"""
);
}
// Run .pyz files as is
else
{
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
}
await base.InitAsync(context);
}

View file

@ -15,7 +15,20 @@ namespace Flow.Launcher.Infrastructure
{
var explorerWindow = GetActiveExplorer();
string locationUrl = explorerWindow?.LocationURL;
return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null;
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
}
/// <summary>
/// Get directory path from a file path
/// </summary>
private static string GetDirectoryPath(string path)
{
if (!path.EndsWith("\\"))
{
return path + "\\";
}
return path;
}
/// <summary>

View file

@ -83,15 +83,50 @@ namespace Flow.Launcher.Infrastructure.Http
}
}
public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default)
public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default)
{
try
{
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
if (response.StatusCode == HttpStatusCode.OK)
{
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
await response.Content.CopyToAsync(fileStream, token);
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
var canReportProgress = totalBytes != -1;
if (canReportProgress && reportProgress != null)
{
await using var contentStream = await response.Content.ReadAsStreamAsync(token);
await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true);
var buffer = new byte[8192];
long totalRead = 0;
int read;
double progressValue = 0;
reportProgress(0);
while ((read = await contentStream.ReadAsync(buffer, token)) > 0)
{
await fileStream.WriteAsync(buffer.AsMemory(0, read), token);
totalRead += read;
progressValue = totalRead * 100.0 / totalBytes;
if (token.IsCancellationRequested)
return;
else
reportProgress(progressValue);
}
if (progressValue < 100)
reportProgress(100);
}
else
{
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
await response.Content.CopyToAsync(fileStream, token);
}
}
else
{

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
@ -181,9 +181,13 @@ namespace Flow.Launcher.Plugin
/// </summary>
/// <param name="url">URL to download file</param>
/// <param name="filePath">path to save downloaded file</param>
/// <param name="reportProgress">
/// Action to report progress. The input of the action is the progress value which is a double value between 0 and 100.
/// It will be called if url support range request and the reportProgress is not null.
/// </param>
/// <param name="token">place to store file</param>
/// <returns>Task showing the progress</returns>
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default);
/// <summary>
/// Add ActionKeyword for specific plugin
@ -316,5 +320,19 @@ namespace Flow.Launcher.Plugin
/// <param name="defaultResult">Specifies the default result of the message box.</param>
/// <returns>Specifies which message box button is clicked by the user.</returns>
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK);
/// <summary>
/// Displays a standardised Flow message box.
/// If there is issue when showing the message box, it will return null.
/// </summary>
/// <param name="caption">The caption of the message box.</param>
/// <param name="reportProgressAsync">
/// Time-consuming task function, whose input is the action to report progress.
/// The input of the action is the progress value which is a double value between 0 and 100.
/// If there are any exceptions, this action will be null.
/// </param>
/// <param name="forceClosed">When user closes the progress box manually by button or esc key, this action will be called.</param>
/// <returns>A progress box interface.</returns>
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action forceClosed = null);
}
}

View file

@ -185,6 +185,16 @@ namespace Flow.Launcher.Plugin
TitleHighlightData = TitleHighlightData,
OriginQuery = OriginQuery,
PluginDirectory = PluginDirectory,
ContextData = ContextData,
PluginID = PluginID,
TitleToolTip = TitleToolTip,
SubTitleToolTip = SubTitleToolTip,
PreviewPanel = PreviewPanel,
ProgressBar = ProgressBar,
ProgressBarColor = ProgressBarColor,
Preview = Preview,
AddSelectedCount = AddSelectedCount,
RecordKey = RecordKey
};
}
@ -252,6 +262,13 @@ namespace Flow.Launcher.Plugin
/// </summary>
public const int MaxScore = int.MaxValue;
/// <summary>
/// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records.
/// This can be useful when your plugin will change the Title or SubTitle of the result dynamically.
/// If the plugin does not specific this, FL just uses Title and SubTitle to identify this result.
/// </summary>
public string RecordKey { get; set; } = string.Empty;
/// <summary>
/// Info of the preview section of a <see cref="Result"/>
/// </summary>

View file

@ -148,4 +148,64 @@ public class WindowsInteropHelper
return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
}
#region Alt Tab
private static int SetWindowLong(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong)
{
PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error
var result = PInvoke.SetWindowLong(hWnd, nIndex, dwNewLong);
if (result == 0 && Marshal.GetLastPInvokeError() != 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
return result;
}
/// <summary>
/// Hide windows in the Alt+Tab window list
/// </summary>
/// <param name="window">To hide a window</param>
public static void HideFromAltTab(Window window)
{
var exStyle = GetCurrentWindowStyle(window);
// Add TOOLWINDOW style, remove APPWINDOW style
var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
}
/// <summary>
/// Restore window display in the Alt+Tab window list.
/// </summary>
/// <param name="window">To restore the displayed window</param>
public static void ShowInAltTab(Window window)
{
var exStyle = GetCurrentWindowStyle(window);
// Remove the TOOLWINDOW style and add the APPWINDOW style.
var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW;
SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle);
}
/// <summary>
/// To obtain the current overridden style of a window.
/// </summary>
/// <param name="window">To obtain the style dialog window</param>
/// <returns>current extension style value</returns>
private static int GetCurrentWindowStyle(Window window)
{
var style = PInvoke.GetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
if (style == 0 && Marshal.GetLastPInvokeError() != 0)
{
throw new Win32Exception(Marshal.GetLastPInvokeError());
}
return style;
}
#endregion
}

View file

@ -20,6 +20,7 @@
Closing="OnClosing"
Deactivated="OnDeactivated"
Icon="Images/app.png"
SourceInitialized="OnSourceInitialized"
Initialized="OnInitialized"
Left="{Binding Settings.WindowLeft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Loaded="OnLoaded"

View file

@ -171,6 +171,11 @@ namespace Flow.Launcher
Environment.Exit(0);
}
private void OnSourceInitialized(object sender, EventArgs e)
{
WindowsInteropHelper.HideFromAltTab(this);
}
private void OnInitialized(object sender, EventArgs e)
{
}

View file

@ -14,4 +14,7 @@ FindWindowEx
WINDOW_STYLE
WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE
WM_EXITSIZEMOVE
SetLastError
WINDOW_EX_STYLE

View file

@ -0,0 +1,106 @@
<Window
x:Class="Flow.Launcher.ProgressBoxEx"
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"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="MessageBoxWindow"
Width="420"
Height="Auto"
Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="Close" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Close" Executed="KeyEsc_OnPress" />
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition MinHeight="68" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="1"
Click="Button_Cancel"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
</Grid>
</StackPanel>
</StackPanel>
<Grid Grid.Row="1" Margin="30 0 30 24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
x:Name="TitleTextBlock"
Grid.Row="0"
MaxWidth="400"
Margin="0 0 26 12"
VerticalAlignment="Center"
FontFamily="Segoe UI"
FontSize="20"
FontWeight="SemiBold"
TextAlignment="Left"
TextWrapping="Wrap" />
<ProgressBar
x:Name="ProgressBar"
Grid.Row="1"
Margin="0 0 26 0"
Maximum="100"
Minimum="0"
Value="0" />
</Grid>
<Border
Grid.Row="2"
Margin="0 0 0 0"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0 1 0 0">
<WrapPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button
x:Name="btnCancel"
MinWidth="120"
Margin="5 0 5 0"
Click="Button_Click"
Content="{DynamicResource commonCancel}" />
</WrapPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,114 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher
{
public partial class ProgressBoxEx : Window
{
private readonly Action _forceClosed;
private ProgressBoxEx(Action forceClosed)
{
_forceClosed = forceClosed;
InitializeComponent();
}
public static async Task ShowAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action forceClosed = null)
{
ProgressBoxEx prgBox = null;
try
{
if (!Application.Current.Dispatcher.CheckAccess())
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
prgBox = new ProgressBoxEx(forceClosed)
{
Title = caption
};
prgBox.TitleTextBlock.Text = caption;
prgBox.Show();
});
}
else
{
prgBox = new ProgressBoxEx(forceClosed)
{
Title = caption
};
prgBox.TitleTextBlock.Text = caption;
prgBox.Show();
}
await reportProgressAsync(prgBox.ReportProgress).ConfigureAwait(false);
}
catch (Exception e)
{
Log.Error($"|ProgressBoxEx.Show|An error occurred: {e.Message}");
await reportProgressAsync(null).ConfigureAwait(false);
}
finally
{
if (!Application.Current.Dispatcher.CheckAccess())
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
prgBox?.Close();
});
}
else
{
prgBox?.Close();
}
}
}
private void ReportProgress(double progress)
{
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(() => ReportProgress(progress));
return;
}
if (progress < 0)
{
ProgressBar.Value = 0;
}
else if (progress >= 100)
{
ProgressBar.Value = 100;
Close();
}
else
{
ProgressBar.Value = progress;
}
}
private void KeyEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
{
ForceClose();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ForceClose();
}
private void Button_Cancel(object sender, RoutedEventArgs e)
{
ForceClose();
}
private void ForceClose()
{
Close();
_forceClosed?.Invoke();
}
}
}

View file

@ -164,8 +164,8 @@ namespace Flow.Launcher
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default) =>
Http.GetStreamAsync(url);
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
CancellationToken token = default) => Http.DownloadAsync(url, filePath, token);
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null,
CancellationToken token = default) => Http.DownloadAsync(url, filePath, reportProgress, token);
public void AddActionKeyword(string pluginId, string newActionKeyword) =>
PluginManager.AddActionKeyword(pluginId, newActionKeyword);
@ -324,6 +324,8 @@ namespace Flow.Launcher
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK) =>
MessageBoxEx.Show(messageBoxText, caption, button, icon, defaultResult);
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action forceClosed = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, forceClosed);
#endregion
#region Private Methods

View file

@ -13,8 +13,8 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
{
public string FilterText { get; set; } = string.Empty;
public IList<PluginStoreItemViewModel> ExternalPlugins => PluginsManifest.UserPlugins
.Select(p => new PluginStoreItemViewModel(p))
public IList<PluginStoreItemViewModel> ExternalPlugins =>
PluginsManifest.UserPlugins?.Select(p => new PluginStoreItemViewModel(p))
.OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease)
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated)
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.None)
@ -24,8 +24,10 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
[RelayCommand]
private async Task RefreshExternalPluginsAsync()
{
await PluginsManifest.UpdateManifestAsync();
OnPropertyChanged(nameof(ExternalPlugins));
if (await PluginsManifest.UpdateManifestAsync())
{
OnPropertyChanged(nameof(ExternalPlugins));
}
}
public bool SatisfiesFilter(PluginStoreItemViewModel plugin)

View file

@ -33,7 +33,8 @@ namespace Flow.Launcher.Storage
{
PluginID = result.PluginID,
Title = result.Title,
SubTitle = result.SubTitle
SubTitle = result.SubTitle,
RecordKey = result.RecordKey
};
records.AddOrUpdate(result.OriginQuery.RawQuery, record, (key, oldValue) => record);
}
@ -49,12 +50,21 @@ namespace Flow.Launcher.Storage
public string Title { get; set; }
public string SubTitle { get; set; }
public string PluginID { get; set; }
public string RecordKey { get; set; }
public bool Equals(Result r)
{
return Title == r.Title
&& SubTitle == r.SubTitle
&& PluginID == r.PluginID;
if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey))
{
return Title == r.Title
&& SubTitle == r.SubTitle
&& PluginID == r.PluginID;
}
else
{
return RecordKey == r.RecordKey
&& PluginID == r.PluginID;
}
}
}
}

View file

@ -15,7 +15,6 @@ namespace Flow.Launcher.Storage
[JsonInclude, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, int> records { get; private set; }
public UserSelectedRecord()
{
recordsWithQuery = new Dictionary<int, int>();
@ -45,8 +44,15 @@ namespace Flow.Launcher.Storage
private static int GenerateResultHashCode(Result result)
{
int hashcode = GenerateStaticHashCode(result.Title);
return GenerateStaticHashCode(result.SubTitle, hashcode);
if (string.IsNullOrEmpty(result.RecordKey))
{
int hashcode = GenerateStaticHashCode(result.Title);
return GenerateStaticHashCode(result.SubTitle, hashcode);
}
else
{
return GenerateStaticHashCode(result.RecordKey);
}
}
private static int GenerateQueryAndResultHashCode(Query query, Result result)
@ -58,8 +64,16 @@ namespace Flow.Launcher.Storage
int hashcode = GenerateStaticHashCode(query.ActionKeyword);
hashcode = GenerateStaticHashCode(query.Search, hashcode);
hashcode = GenerateStaticHashCode(result.Title, hashcode);
hashcode = GenerateStaticHashCode(result.SubTitle, hashcode);
if (string.IsNullOrEmpty(result.RecordKey))
{
hashcode = GenerateStaticHashCode(result.Title, hashcode);
hashcode = GenerateStaticHashCode(result.SubTitle, hashcode);
}
else
{
hashcode = GenerateStaticHashCode(result.RecordKey, hashcode);
}
return hashcode;
}

View file

@ -51,7 +51,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return query.FirstSearch.ToLower() switch
{
//search could be url, no need ToLower() when passed in
Settings.InstallCommand => await pluginManager.RequestInstallOrUpdate(query.SecondToEndSearch, token, query.IsReQuery),
Settings.InstallCommand => await pluginManager.RequestInstallOrUpdateAsync(query.SecondToEndSearch, token, query.IsReQuery),
Settings.UninstallCommand => pluginManager.RequestUninstall(query.SecondToEndSearch),
Settings.UpdateCommand => await pluginManager.RequestUpdateAsync(query.SecondToEndSearch, token, query.IsReQuery),
_ => pluginManager.GetDefaultHotKeys().Where(hotkey =>

View file

@ -17,7 +17,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
internal class PluginsManager
{
const string zip = "zip";
private const string zip = "zip";
private PluginInitContext Context { get; set; }
@ -144,34 +144,43 @@ namespace Flow.Launcher.Plugin.PluginsManager
try
{
using var cts = new CancellationTokenSource();
if (!plugin.IsFromLocalInstallPath)
{
if (File.Exists(filePath))
File.Delete(filePath);
await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
await DownloadFileAsync(
$"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {plugin.Name}",
plugin.UrlDownload, filePath, cts);
}
else
{
filePath = plugin.LocalInstallPath;
}
Install(plugin, filePath);
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
return;
else
Install(plugin, filePath);
}
catch (HttpRequestException e)
{
// show error message
Context.API.ShowMsgError(
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), plugin.Name),
Context.API.GetTranslation("plugin_pluginsmanager_download_error"));
Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);
return;
}
catch (Exception e)
{
// show error message
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
plugin.Name));
Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);
return;
}
@ -190,6 +199,41 @@ namespace Flow.Launcher.Plugin.PluginsManager
}
}
private async Task DownloadFileAsync(string prgBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
{
if (deleteFile && File.Exists(filePath))
File.Delete(filePath);
if (showProgress)
{
var exceptionHappened = false;
await Context.API.ShowProgressBoxAsync(prgBoxTitle,
async (reportProgress) =>
{
if (reportProgress == null)
{
// when reportProgress is null, it means there is expcetion with the progress box
// so we record it with exceptionHappened and return so that progress box will close instantly
exceptionHappened = true;
return;
}
else
{
await Context.API.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 Context.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
else
{
await Context.API.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
}
}
internal async ValueTask<List<Result>> RequestUpdateAsync(string search, CancellationToken token,
bool usePrimaryUrlOnly = false)
{
@ -277,43 +321,48 @@ namespace Flow.Launcher.Plugin.PluginsManager
_ = Task.Run(async delegate
{
using var cts = new CancellationTokenSource();
if (!x.PluginNewUserPlugin.IsFromLocalInstallPath)
{
if (File.Exists(downloadToFilePath))
{
File.Delete(downloadToFilePath);
}
await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
.ConfigureAwait(false);
await DownloadFileAsync(
$"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {x.PluginNewUserPlugin.Name}",
x.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
}
else
{
downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath;
}
PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin,
downloadToFilePath);
if (Settings.AutoRestartAfterChanging)
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
{
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation(
"plugin_pluginsmanager_update_success_restart"),
x.Name));
Context.API.RestartApp();
return;
}
else
{
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation(
"plugin_pluginsmanager_update_success_no_restart"),
x.Name));
PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin,
downloadToFilePath);
if (Settings.AutoRestartAfterChanging)
{
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation(
"plugin_pluginsmanager_update_success_restart"),
x.Name));
Context.API.RestartApp();
}
else
{
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation(
"plugin_pluginsmanager_update_success_no_restart"),
x.Name));
}
}
}).ContinueWith(t =>
{
@ -324,7 +373,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
x.Name));
}, TaskContinuationOptions.OnlyOnFaulted);
}, token, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
return true;
},
@ -337,7 +386,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
});
// Update all result
if (resultsForUpdate.Count() > 1)
if (resultsForUpdate.Count > 1)
{
var updateAllResult = new Result
{
@ -351,13 +400,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
message = string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_all_prompt"),
resultsForUpdate.Count(), Environment.NewLine);
resultsForUpdate.Count, Environment.NewLine);
}
else
{
message = string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_all_prompt_no_restart"),
resultsForUpdate.Count());
resultsForUpdate.Count);
}
if (Context.API.ShowMsgBox(message,
@ -374,16 +423,18 @@ namespace Flow.Launcher.Plugin.PluginsManager
try
{
if (File.Exists(downloadToFilePath))
{
File.Delete(downloadToFilePath);
}
using var cts = new CancellationTokenSource();
await Http.DownloadAsync(plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
.ConfigureAwait(false);
await DownloadFileAsync(
$"{Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin")} {plugin.PluginNewUserPlugin.Name}",
plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
PluginManager.UpdatePlugin(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
downloadToFilePath);
// check if user cancelled download before installing plugin
if (cts.IsCancellationRequested)
return;
else
PluginManager.UpdatePlugin(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
downloadToFilePath);
}
catch (Exception ex)
{
@ -401,7 +452,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_all_success_restart"),
resultsForUpdate.Count()));
resultsForUpdate.Count));
Context.API.RestartApp();
}
else
@ -409,7 +460,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_all_success_no_restart"),
resultsForUpdate.Count()));
resultsForUpdate.Count));
}
return true;
@ -545,7 +596,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context.API.GetAllPlugins().Any(x => x.Metadata.Website.StartsWith(constructedUrlPart));
}
internal async ValueTask<List<Result>> RequestInstallOrUpdate(string search, CancellationToken token,
internal async ValueTask<List<Result>> RequestInstallOrUpdateAsync(string search, CancellationToken token,
bool usePrimaryUrlOnly = false)
{
await PluginsManifest.UpdateManifestAsync(token, usePrimaryUrlOnly);

View file

@ -7,6 +7,7 @@
<system:String x:Key="flowlauncher_plugin_cmd_press_any_key_to_close">Press any key to close this window...</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_leave_cmd_open">Do not close Command Prompt after command execution</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_always_run_as_administrator">Always run as administrator</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_use_windows_terminal">Use Windows Terminal</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_run_as_different_user">Run as different user</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_plugin_name">Shell</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_plugin_description">Allows to execute system commands from Flow Launcher</system:String>
@ -15,4 +16,4 @@
<system:String x:Key="flowlauncher_plugin_cmd_run_as_administrator">Run As Administrator</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_copy">Copy the command</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_history">Only show number of most used commands:</system:String>
</ResourceDictionary>
</ResourceDictionary>

View file

@ -202,28 +202,31 @@ namespace Flow.Launcher.Plugin.Shell
{
case Shell.Cmd:
{
info.FileName = "cmd.exe";
info.Arguments = $"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}";
//// Use info.Arguments instead of info.ArgumentList to enable users better control over the arguments they are writing.
//// Previous code using ArgumentList, commands needed to be separated correctly:
//// Incorrect:
// info.ArgumentList.Add(_settings.LeaveShellOpen ? "/k" : "/c");
// info.ArgumentList.Add(command); //<== info.ArgumentList.Add("mkdir \"c:\\test new\"");
//// Correct version should be:
//info.ArgumentList.Add(_settings.LeaveShellOpen ? "/k" : "/c");
//info.ArgumentList.Add("mkdir");
//info.ArgumentList.Add(@"c:\test new");
//https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.argumentlist?view=net-6.0#remarks
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("cmd");
}
else
{
info.FileName = "cmd.exe";
}
info.ArgumentList.Add($"{(_settings.LeaveShellOpen ? "/k" : "/c")} {command} {(_settings.CloseShellAfterPress ? $"&& echo {context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")} && pause > nul /c" : "")}");
break;
}
case Shell.Powershell:
{
info.FileName = "powershell.exe";
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("powershell");
}
else
{
info.FileName = "powershell.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
@ -232,21 +235,28 @@ namespace Flow.Launcher.Plugin.Shell
else
{
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'; [System.Console]::ReadKey(); exit" : "")}");
info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
}
break;
}
case Shell.Pwsh:
{
info.FileName = "pwsh.exe";
if (_settings.UseWindowsTerminal)
{
info.FileName = "wt.exe";
info.ArgumentList.Add("pwsh");
}
else
{
info.FileName = "pwsh.exe";
}
if (_settings.LeaveShellOpen)
{
info.ArgumentList.Add("-NoExit");
}
info.ArgumentList.Add("-Command");
info.ArgumentList.Add($"{command}; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'; [System.Console]::ReadKey(); exit" : "")}");
info.ArgumentList.Add($"{command}\\; {(_settings.CloseShellAfterPress ? $"Write-Host '{context.API.GetTranslation("flowlauncher_plugin_cmd_press_any_key_to_close")}'\\; [System.Console]::ReadKey()\\; exit" : "")}");
break;
}

View file

@ -14,6 +14,8 @@ namespace Flow.Launcher.Plugin.Shell
public bool RunAsAdministrator { get; set; } = true;
public bool UseWindowsTerminal { get; set; } = false;
public bool ShowOnlyMostUsedCMDs { get; set; }
public int ShowOnlyMostUsedCMDsNumber { get; set; }

View file

@ -16,6 +16,7 @@
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<CheckBox
x:Name="ReplaceWinR"
@ -41,9 +42,15 @@
Margin="10,5,5,5"
HorizontalAlignment="Left"
Content="{DynamicResource flowlauncher_plugin_cmd_always_run_as_administrator}" />
<CheckBox
x:Name="UseWindowsTerminal"
Grid.Row="4"
Margin="10,5,5,5"
HorizontalAlignment="Left"
Content="{DynamicResource flowlauncher_plugin_cmd_use_windows_terminal}" />
<ComboBox
x:Name="ShellComboBox"
Grid.Row="4"
Grid.Row="5"
Margin="10,5,5,5"
HorizontalAlignment="Left">
<ComboBoxItem>CMD</ComboBoxItem>
@ -51,7 +58,7 @@
<ComboBoxItem>Pwsh</ComboBoxItem>
<ComboBoxItem>RunCommand</ComboBoxItem>
</ComboBox>
<StackPanel Grid.Row="5" Orientation="Horizontal">
<StackPanel Grid.Row="6" Orientation="Horizontal">
<CheckBox
x:Name="ShowOnlyMostUsedCMDs"
Margin="10,5,5,5"

View file

@ -23,6 +23,8 @@ namespace Flow.Launcher.Plugin.Shell
LeaveShellOpen.IsChecked = _settings.LeaveShellOpen;
AlwaysRunAsAdministrator.IsChecked = _settings.RunAsAdministrator;
UseWindowsTerminal.IsChecked = _settings.UseWindowsTerminal;
LeaveShellOpen.IsEnabled = _settings.Shell != Shell.RunCommand;
@ -76,6 +78,16 @@ namespace Flow.Launcher.Plugin.Shell
_settings.RunAsAdministrator = false;
};
UseWindowsTerminal.Checked += (o, e) =>
{
_settings.UseWindowsTerminal = true;
};
UseWindowsTerminal.Unchecked += (o, e) =>
{
_settings.UseWindowsTerminal = false;
};
ReplaceWinR.Checked += (o, e) =>
{
_settings.ReplaceWinR = true;