mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into dev
This commit is contained in:
commit
2978226bcf
22 changed files with 546 additions and 81 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,7 @@ FindWindowEx
|
|||
WINDOW_STYLE
|
||||
|
||||
WM_ENTERSIZEMOVE
|
||||
WM_EXITSIZEMOVE
|
||||
WM_EXITSIZEMOVE
|
||||
|
||||
SetLastError
|
||||
WINDOW_EX_STYLE
|
||||
106
Flow.Launcher/ProgressBoxEx.xaml
Normal file
106
Flow.Launcher/ProgressBoxEx.xaml
Normal 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>
|
||||
114
Flow.Launcher/ProgressBoxEx.xaml.cs
Normal file
114
Flow.Launcher/ProgressBoxEx.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue