Merge branch 'dev' into dev

This commit is contained in:
Jack Ye 2025-02-17 19:00:18 +08:00 committed by GitHub
commit 2978226bcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 546 additions and 81 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,16 +161,19 @@ 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);
}
public void BackToQueryResults()
{
_api.BackToQueryResults();
}
}
}

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

@ -100,7 +100,7 @@
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" />
</ItemGroup>
<ItemGroup>

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

@ -95,7 +95,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.1" />
</ItemGroup>
</Project>

View file

@ -22,7 +22,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal const string WindowsIndexErrorImagePath = "Images\\index_error2.png";
internal const string GeneralSearchErrorImagePath = "Images\\robot_error.png";
internal const string ToolTipOpenDirectory = "Ctrl + Enter to open the directory";
internal const string ToolTipOpenContainingFolder = "Ctrl + Enter to open the containing folder";
@ -31,6 +30,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal const string DefaultContentSearchActionKeyword = "doc:";
internal const char UnixDirectorySeparator = '/';
internal const char DirectorySeparator = '\\';
internal const string WindowsIndexingOptions = "srchadmin.dll";

View file

@ -187,6 +187,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search
var needToExpand = EnvironmentVariables.HasEnvironmentVar(querySearch);
var path = needToExpand ? Environment.ExpandEnvironmentVariables(querySearch) : querySearch;
// if user uses the unix directory separator, we need to convert it to windows directory separator
path = path.Replace(Constants.UnixDirectorySeparator, Constants.DirectorySeparator);
// Check that actual location exists, otherwise directory search will throw directory not found exception
if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path).LocationExists())
return results.ToList();

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);