Merge remote-tracking branch 'origin/dev' into explorerMerge

This commit is contained in:
Hongtao Zhang 2022-09-21 19:19:13 -05:00
commit 1622e57e78
No known key found for this signature in database
GPG key ID: 75F655B91C7AC9BB
16 changed files with 615 additions and 299 deletions

View file

@ -1,33 +1,27 @@
using Accessibility;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using ICSharpCode.SharpZipLib.Zip;
using JetBrains.Annotations;
using Microsoft.IO;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Controls;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using CheckBox = System.Windows.Controls.CheckBox;
using Control = System.Windows.Controls.Control;
using Label = System.Windows.Controls.Label;
using Orientation = System.Windows.Controls.Orientation;
using TextBox = System.Windows.Controls.TextBox;
using UserControl = System.Windows.Controls.UserControl;
using System.Windows.Data;
namespace Flow.Launcher.Core.Plugin
{
@ -69,7 +63,7 @@ namespace Flow.Launcher.Core.Plugin
private static readonly JsonSerializerOptions options = new()
{
PropertyNameCaseInsensitive = true,
#pragma warning disable SYSLIB0020
#pragma warning disable SYSLIB0020
// IgnoreNullValues is obsolete, but the replacement JsonIgnoreCondition.WhenWritingNull still
// deserializes null, instead of ignoring it and leaving the default (empty list). We can change the behaviour
// to accept null and fallback to a default etc, or just keep IgnoreNullValues for now
@ -92,12 +86,15 @@ namespace Flow.Launcher.Core.Plugin
private async Task<List<Result>> DeserializedResultAsync(Stream output)
{
if (output == Stream.Null) return null;
await using (output)
{
if (output == Stream.Null) return null;
var queryResponseModel =
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
var queryResponseModel =
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
return ParseResults(queryResponseModel);
return ParseResults(queryResponseModel);
}
}
private List<Result> DeserializedResult(string output)
@ -139,7 +136,7 @@ namespace Flow.Launcher.Core.Plugin
}
else
{
var actionResponse = await RequestAsync(result.JsonRPCAction);
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
if (actionResponse.Length == 0)
{
@ -247,74 +244,55 @@ namespace Flow.Launcher.Core.Plugin
protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default)
{
Process process = null;
using var exitTokenSource = new CancellationTokenSource();
using var process = Process.Start(startInfo);
if (process == null)
{
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null;
}
var sourceBuffer = BufferManager.GetStream();
using var errorBuffer = BufferManager.GetStream();
var sourceCopyTask = process.StandardOutput.BaseStream.CopyToAsync(sourceBuffer, token);
var errorCopyTask = process.StandardError.BaseStream.CopyToAsync(errorBuffer, token);
await using var registeredEvent = token.Register(() =>
{
if (!process.HasExited)
process.Kill();
sourceBuffer.Dispose();
});
try
{
process = Process.Start(startInfo);
if (process == null)
{
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null;
}
await using var source = process.StandardOutput.BaseStream;
var buffer = BufferManager.GetStream();
token.Register(() =>
{
// ReSharper disable once AccessToModifiedClosure
// Manually Check whether disposed
if (!exitTokenSource.IsCancellationRequested && !process.HasExited)
process.Kill();
});
try
{
// token expire won't instantly trigger the exception,
// manually kill process at before
await source.CopyToAsync(buffer, token);
}
catch (OperationCanceledException)
{
await buffer.DisposeAsync();
return Stream.Null;
}
buffer.Seek(0, SeekOrigin.Begin);
token.ThrowIfCancellationRequested();
if (buffer.Length == 0)
{
var errorMessage = process.StandardError.EndOfStream ?
"Empty JSONRPC Response" :
await process.StandardError.ReadToEndAsync();
throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}");
}
if (!process.StandardError.EndOfStream)
{
using var standardError = process.StandardError;
var error = await standardError.ReadToEndAsync();
if (!string.IsNullOrEmpty(error))
{
Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}");
}
}
return buffer;
// token expire won't instantly trigger the exception,
// manually kill process at before
await process.WaitForExitAsync(token);
await Task.WhenAll(sourceCopyTask, errorCopyTask);
}
finally
catch (OperationCanceledException)
{
exitTokenSource.Cancel();
process?.Dispose();
await sourceBuffer.DisposeAsync();
return Stream.Null;
}
switch (sourceBuffer.Length, errorBuffer.Length)
{
case (0, 0):
const string errorMessage = "Empty JSON-RPC Response.";
Log.Warn($"|{nameof(JsonRPCPlugin)}.{nameof(ExecuteAsync)}|{errorMessage}");
break;
case (_, not 0):
throw new InvalidDataException(Encoding.UTF8.GetString(errorBuffer.ToArray())); // The process has exited with an error message
}
sourceBuffer.Seek(0, SeekOrigin.Begin);
return sourceBuffer;
}
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
var request = new JsonRPCRequestModel
@ -366,6 +344,7 @@ namespace Flow.Launcher.Core.Plugin
private static readonly Thickness settingPanelMargin = new(15, 20, 15, 20);
private static readonly Thickness settingTextBlockMargin = new(10, 4, 10, 4);
private JsonRpcConfigurationModel _settingsTemplate;
public Control CreateSettingPanel()
{
if (Settings == null)
@ -397,84 +376,84 @@ namespace Flow.Launcher.Core.Plugin
switch (type)
{
case "textBlock":
{
contentControl = new TextBlock
{
contentControl = new TextBlock
{
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
Margin = settingTextBlockMargin,
MaxWidth = 500,
TextWrapping = TextWrapping.WrapWithOverflow
};
break;
}
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
Margin = settingTextBlockMargin,
MaxWidth = 500,
TextWrapping = TextWrapping.WrapWithOverflow
};
break;
}
case "input":
{
var textBox = new TextBox()
{
var textBox = new TextBox()
{
Width = 300,
Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = settingControlMargin,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
contentControl = textBox;
break;
}
Width = 300,
Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = settingControlMargin,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
contentControl = textBox;
break;
}
case "textarea":
{
var textBox = new TextBox()
{
var textBox = new TextBox()
{
Width = 300,
Height = 120,
Margin = settingControlMargin,
TextWrapping = TextWrapping.WrapWithOverflow,
AcceptsReturn = true,
Text = Settings[attribute.Name] as string ?? string.Empty,
ToolTip = attribute.Description
};
textBox.TextChanged += (sender, _) =>
{
Settings[attribute.Name] = ((TextBox)sender).Text;
};
contentControl = textBox;
break;
}
Width = 300,
Height = 120,
Margin = settingControlMargin,
TextWrapping = TextWrapping.WrapWithOverflow,
AcceptsReturn = true,
Text = Settings[attribute.Name] as string ?? string.Empty,
ToolTip = attribute.Description
};
textBox.TextChanged += (sender, _) =>
{
Settings[attribute.Name] = ((TextBox)sender).Text;
};
contentControl = textBox;
break;
}
case "passwordBox":
{
var passwordBox = new PasswordBox()
{
var passwordBox = new PasswordBox()
{
Width = 300,
Margin = settingControlMargin,
Password = Settings[attribute.Name] as string ?? string.Empty,
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
ToolTip = attribute.Description
};
passwordBox.PasswordChanged += (sender, _) =>
{
Settings[attribute.Name] = ((PasswordBox)sender).Password;
};
contentControl = passwordBox;
break;
}
Width = 300,
Margin = settingControlMargin,
Password = Settings[attribute.Name] as string ?? string.Empty,
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
ToolTip = attribute.Description
};
passwordBox.PasswordChanged += (sender, _) =>
{
Settings[attribute.Name] = ((PasswordBox)sender).Password;
};
contentControl = passwordBox;
break;
}
case "dropdown":
{
var comboBox = new ComboBox()
{
var comboBox = new ComboBox()
{
ItemsSource = attribute.Options,
SelectedItem = Settings[attribute.Name],
Margin = settingControlMargin,
ToolTip = attribute.Description
};
comboBox.SelectionChanged += (sender, _) =>
{
Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem;
};
contentControl = comboBox;
break;
}
ItemsSource = attribute.Options,
SelectedItem = Settings[attribute.Name],
Margin = settingControlMargin,
ToolTip = attribute.Description
};
comboBox.SelectionChanged += (sender, _) =>
{
Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem;
};
contentControl = comboBox;
break;
}
case "checkbox":
var checkBox = new CheckBox
{
@ -499,6 +478,7 @@ namespace Flow.Launcher.Core.Plugin
}
return settingWindow;
}
public void Save()
{
if (Settings != null)
@ -541,4 +521,5 @@ namespace Flow.Launcher.Core.Plugin
}
}
}
}

View file

@ -197,5 +197,16 @@ namespace Flow.Launcher.Plugin
{
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
}
/// <summary>
/// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result
/// </summary>
public int? ProgressBar { get; set; }
/// <summary>
/// Optionally set the color of the progress bar
/// </summary>
/// <default>#26a0da (blue)</default>
public string ProgressBarColor { get; set; } = "#26a0da";
}
}

View file

@ -77,7 +77,6 @@ namespace Flow.Launcher
};
link.Inlines.Add(url);
link.NavigateUri = new Uri(url);
link.RequestNavigate += (s, e) => SearchWeb.OpenInBrowserTab(e.Uri.ToString());
link.Click += (s, e) => SearchWeb.OpenInBrowserTab(url);
paragraph.Inlines.Add(textBeforeUrl);

View file

@ -30,7 +30,7 @@
<ListBox.ItemTemplate>
<DataTemplate>
<Button>
<Button HorizontalAlignment="Stretch">
<Button.Template>
<ControlTemplate>
<ContentPresenter Content="{TemplateBinding Button.Content}" />
@ -39,7 +39,7 @@
<Button.Content>
<Grid
Margin="0"
HorizontalAlignment="Left"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Cursor="Hand"
UseLayoutRounding="False">
@ -50,7 +50,7 @@
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="9*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
@ -111,7 +111,11 @@
<RowDefinition />
<RowDefinition x:Name="SubTitleRowDefinition" Height="Auto" />
</Grid.RowDefinitions>
<ProgressBar
x:Name="progressbarResult"
Foreground="{Binding Result.ProgressBarColor}"
Style="{DynamicResource ProgressBarResult}"
Value="{Binding Result.ProgressBar}" />
<TextBlock
x:Name="Title"
VerticalAlignment="Center"
@ -129,12 +133,9 @@
<TextBlock
x:Name="SubTitle"
Grid.Row="1"
MinWidth="750"
Style="{DynamicResource ItemSubTitleStyle}"
Text="{Binding Result.SubTitle}"
ToolTip="{Binding ShowSubTitleToolTip}">
</TextBlock>
ToolTip="{Binding ShowSubTitleToolTip}" />
</Grid>

View file

@ -14,7 +14,7 @@
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
Title="{DynamicResource flowlauncher_settings}"
Width="1000"
Height="650"
Height="700"
MinWidth="900"
MinHeight="600"
d:DataContext="{d:DesignInstance vm:SettingWindowViewModel}"
@ -597,9 +597,7 @@
<StackPanel Style="{StaticResource TextPanel}">
<TextBlock Style="{DynamicResource SettingTitleLabel}" Text="{DynamicResource startFlowLauncherOnSystemStartup}" />
</StackPanel>
<CheckBox
IsChecked="{Binding StartFlowLauncherOnSystemStartup}"
Style="{DynamicResource SideControlCheckBox}" />
<CheckBox IsChecked="{Binding StartFlowLauncherOnSystemStartup}" Style="{DynamicResource SideControlCheckBox}" />
<TextBlock Style="{StaticResource Glyph}">
&#xe8fc;
</TextBlock>
@ -1620,7 +1618,7 @@
Margin="0,0,18,0"
IsMoveToPointEnabled="True"
IsSnapToTickEnabled="True"
Maximum="900"
Maximum="1920"
Minimum="400"
TickFrequency="10"
Value="{Binding WindowWidthSize, Mode=TwoWay}" />
@ -1791,9 +1789,9 @@
Margin="20,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
IsSynchronizedWithCurrentItem="False"
ItemsSource="{Binding Source={StaticResource SortedFonts}}"
SelectedItem="{Binding SelectedQueryBoxFont}"
IsSynchronizedWithCurrentItem="False" />
SelectedItem="{Binding SelectedQueryBoxFont}" />
<ComboBox
Width="130"
Margin="10,0,0,0"
@ -1842,9 +1840,9 @@
Margin="20,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
IsSynchronizedWithCurrentItem="False"
ItemsSource="{Binding Source={StaticResource SortedFonts}}"
SelectedItem="{Binding SelectedResultFont}"
IsSynchronizedWithCurrentItem="False" />
SelectedItem="{Binding SelectedResultFont}" />
<ComboBox
Width="130"
Margin="10,-2,0,0"
@ -2486,6 +2484,26 @@
</ItemsControl>
</Border>
<Border Height="62" Style="{DynamicResource SettingGroupBox}">
<ItemsControl Style="{StaticResource SettingGrid}">
<StackPanel Style="{StaticResource TextPanel}">
<TextBlock Style="{DynamicResource SettingTitleLabel}" Text="Icons" />
</StackPanel>
<TextBlock
Margin="0,0,-12,0"
VerticalAlignment="Center"
Style="{StaticResource SideTextAbout}">
<Hyperlink NavigateUri="https://icons8.com" RequestNavigate="OnRequestNavigate">
<Run Text="icons8.com" />
</Hyperlink>
</TextBlock>
<TextBlock Style="{StaticResource Glyph}">
&#xE8FE;
</TextBlock>
</ItemsControl>
</Border>
<Border Style="{DynamicResource SettingGroupBox}">
<ItemsControl Style="{StaticResource SettingGrid}">
<StackPanel Style="{StaticResource TextPanel}">
@ -2512,19 +2530,7 @@
</ItemsControl>
</Border>
<TextBlock
Margin="14,14,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontSize="12"
Foreground="{DynamicResource Color15B}"
TextWrapping="WrapWithOverflow">
<Hyperlink NavigateUri="https://icons8.com" RequestNavigate="OnRequestNavigate">
<Run Text="Icons by icons8.com" />
</Hyperlink>
</TextBlock>
<TextBlock
Margin="14,4,0,0"
Margin="14,20,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
DockPanel.Dock="Bottom"

View file

@ -81,6 +81,21 @@
<!-- Item Style -->
<Style x:Key="ProgressBarResult" TargetType="{x:Type ProgressBar}">
<Setter Property="Height" Value="18" />
<Setter Property="Margin" Value="0,0,0,4" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Maximum" Value="100" />
<Setter Property="Minimum" Value="0" />
<Setter Property="Visibility" Value="Visible" />
<Setter Property="Foreground" Value="#26a0da " />
<Style.Triggers>
<DataTrigger Binding="{Binding Result.ProgressBar}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="BaseItemTitleStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#FFFFF8" />
<Setter Property="FontSize" Value="16" />

View file

@ -20,6 +20,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
var bookmarkPath = Path.Combine(profile, "Bookmarks");
if (!File.Exists(bookmarkPath))
continue;
Main.RegisterBookmarkFile(bookmarkPath);
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
bookmarks.AddRange(LoadBookmarksFromFile(bookmarkPath, source));
@ -31,6 +33,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
{
if (!File.Exists(path))
return new();
var bookmarks = new List<Bookmark>();
using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path));
if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))

View file

@ -12,7 +12,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
private const string queryAllBookmarks = @"SELECT moz_places.url, moz_bookmarks.title
FROM moz_places
INNER JOIN moz_bookmarks ON (
moz_bookmarks.fk NOT NULL AND moz_bookmarks.fk = moz_places.id
moz_bookmarks.fk NOT NULL AND moz_bookmarks.title NOT NULL AND moz_bookmarks.fk = moz_places.id
)
ORDER BY moz_places.visit_count DESC
";
@ -29,21 +29,21 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
return new List<Bookmark>();
var bookmarkList = new List<Bookmark>();
Main.RegisterBookmarkFile(PlacesPath);
// create the connection string and init the connection
string dbPath = string.Format(dbPathFormat, PlacesPath);
using (var dbConnection = new SQLiteConnection(dbPath))
{
// Open connection to the database file and execute the query
dbConnection.Open();
var reader = new SQLiteCommand(queryAllBookmarks, dbConnection).ExecuteReader();
using var dbConnection = new SQLiteConnection(dbPath);
// Open connection to the database file and execute the query
dbConnection.Open();
var reader = new SQLiteCommand(queryAllBookmarks, dbConnection).ExecuteReader();
// return results in List<Bookmark> format
bookmarkList = reader.Select(
x => new Bookmark(x["title"] is DBNull ? string.Empty : x["title"].ToString(),
x["url"].ToString())
).ToList();
}
// return results in List<Bookmark> format
bookmarkList = reader.Select(
x => new Bookmark(x["title"] is DBNull ? string.Empty : x["title"].ToString(),
x["url"].ToString())
).ToList();
return bookmarkList;
}

View file

@ -9,24 +9,29 @@ using Flow.Launcher.Plugin.BrowserBookmark.Commands;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Flow.Launcher.Plugin.BrowserBookmark.Views;
using Flow.Launcher.Plugin.SharedCommands;
using System.IO;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin.BrowserBookmark
{
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable
{
private PluginInitContext context;
private List<Bookmark> cachedBookmarks = new List<Bookmark>();
private Settings _settings { get; set;}
private Settings _settings { get; set; }
public void Init(PluginInitContext context)
{
this.context = context;
_settings = context.API.LoadSettingJsonStorage<Settings>();
cachedBookmarks = BookmarkLoader.LoadAllBookmarks(_settings);
_ = MonitorRefreshQueue();
}
public List<Result> Query(Query query)
@ -52,7 +57,10 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
return true;
},
ContextData = new BookmarkAttributes { Url = c.Url }
ContextData = new BookmarkAttributes
{
Url = c.Url
}
}).Where(r => r.Score > 0);
return returnList.ToList();
}
@ -69,11 +77,64 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
context.API.OpenUrl(c.Url);
return true;
},
ContextData = new BookmarkAttributes { Url = c.Url }
ContextData = new BookmarkAttributes
{
Url = c.Url
}
}).ToList();
}
}
private static Channel<byte> refreshQueue = Channel.CreateBounded<byte>(1);
private async Task MonitorRefreshQueue()
{
var reader = refreshQueue.Reader;
while (await reader.WaitToReadAsync())
{
await Task.Delay(2000);
if (reader.TryRead(out _))
{
ReloadData();
}
}
}
private static readonly List<FileSystemWatcher> Watchers = new();
internal static void RegisterBookmarkFile(string path)
{
var directory = Path.GetDirectoryName(path);
if (!Directory.Exists(directory))
return;
var watcher = new FileSystemWatcher(directory!);
if (File.Exists(path))
{
var fileName = Path.GetFileName(path);
watcher.Filter = fileName;
}
watcher.NotifyFilter = NotifyFilters.FileName |
NotifyFilters.LastAccess |
NotifyFilters.LastWrite |
NotifyFilters.Size;
watcher.Changed += static (_, _) =>
{
refreshQueue.Writer.TryWrite(default);
};
watcher.Renamed += static (_, _) =>
{
refreshQueue.Writer.TryWrite(default);
};
watcher.EnableRaisingEvents = true;
Watchers.Add(watcher);
}
public void ReloadData()
{
cachedBookmarks.Clear();
@ -98,7 +159,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
public List<Result> LoadContextMenus(Result selectedResult)
{
return new List<Result>() {
return new List<Result>()
{
new Result
{
Title = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"),
@ -114,7 +176,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
catch (Exception e)
{
var message = "Failed to set url in clipboard";
Log.Exception("Main",message, e, "LoadContextMenus");
Log.Exception("Main", message, e, "LoadContextMenus");
context.API.ShowMsg(message);
@ -122,12 +184,20 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
}
},
IcoPath = "Images\\copylink.png"
}};
}
};
}
internal class BookmarkAttributes
{
internal string Url { get; set; }
}
public void Dispose()
{
foreach (var watcher in Watchers)
{
watcher.Dispose();
}
}
}
}
}

View file

@ -4,7 +4,7 @@
"Name": "Browser Bookmarks",
"Description": "Search your browser bookmarks",
"Author": "qianlifeng, Ioannis G.",
"Version": "1.6.3",
"Version": "1.7.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.BrowserBookmark.dll",

View file

@ -89,6 +89,92 @@ namespace Flow.Launcher.Plugin.Explorer.Search
};
}
internal static Result CreateDriveSpaceDisplayResult(string path, bool windowsIndexed = false)
{
var progressBarColor = "#26a0da";
int? progressValue = null;
var title = string.Empty; // hide title when use progress bar,
var driveLetter = path.Substring(0, 1).ToUpper();
var driveName = driveLetter + ":\\";
DriveInfo drv = new DriveInfo(driveLetter);
var subtitle = toReadableSize(drv.AvailableFreeSpace, 2) + " free of " + toReadableSize(drv.TotalSize, 2);
double UsingSize = (Convert.ToDouble(drv.TotalSize) - Convert.ToDouble(drv.AvailableFreeSpace)) / Convert.ToDouble(drv.TotalSize) * 100;
progressValue = Convert.ToInt32(UsingSize);
if (progressValue >= 90)
progressBarColor = "#da2626";
return new Result
{
Title = title,
SubTitle = subtitle,
AutoCompleteText = GetPathWithActionKeyword(path, ResultType.Folder),
IcoPath = path,
Score = 500,
ProgressBar = progressValue,
ProgressBarColor = progressBarColor,
Action = c =>
{
Context.API.OpenDirectory(path);
return true;
},
TitleToolTip = path,
SubTitleToolTip = path,
ContextData = new SearchResult
{
Type = ResultType.Folder,
FullPath = path,
ShowIndexState = true,
WindowsIndexed = windowsIndexed
}
};
}
private static string toReadableSize(long pDrvSize, int pi)
{
int mok = 0;
double drvSize = pDrvSize;
string Space = "Byte";
while (drvSize > 1024.0)
{
drvSize /= 1024.0;
mok++;
}
if (mok == 1)
Space = "KB";
else if (mok == 2)
Space = " MB";
else if (mok == 3)
Space = " GB";
else if (mok == 4)
Space = " TB";
var returnStr = string.Format("{0}{1}", Convert.ToInt32(drvSize), Space);
if (mok != 0)
{
switch (pi)
{
case 1:
returnStr = string.Format("{0:F1}{1}", drvSize, Space);
break;
case 2:
returnStr = string.Format("{0:F2}{1}", drvSize, Space);
break;
case 3:
returnStr = string.Format("{0:F3}{1}", drvSize, Space);
break;
default:
returnStr = string.Format("{0}{1}", Convert.ToInt32(drvSize), Space);
break;
}
}
return returnStr;
}
internal static Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false)
{
var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);

View file

@ -167,7 +167,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search
var useIndexSearch = Settings.IndexSearchEngine is Settings.IndexSearchEngineOption.WindowsIndex
&& UseWindowsIndexForDirectorySearch(locationPath);
results.Add(ResultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch));
if (locationPath.EndsWith(":\\"))
{
results.Add(ResultManager.CreateDriveSpaceDisplayResult(locationPath, useIndexSearch));
}
else
{
results.Add(ResultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch));
}
if (token.IsCancellationRequested)
return new List<Result>();

View file

@ -18,13 +18,12 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Plugin.Program
{
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable
public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable, IDisposable
{
internal static Win32[] _win32s { get; set; }
internal static UWP.Application[] _uwps { get; set; }
internal static Settings _settings { get; set; }
private static bool IsStartupIndexProgramsRequired => _settings.LastIndexTime.AddDays(3) < DateTime.Today;
internal static PluginInitContext Context { get; private set; }
@ -51,29 +50,25 @@ namespace Flow.Launcher.Plugin.Program
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
if (IsStartupIndexProgramsRequired)
_ = IndexProgramsAsync();
var result = await cache.GetOrCreateAsync(query.Search, async entry =>
{
var resultList = await Task.Run(() =>
_win32s.Cast<IProgram>()
.Concat(_uwps)
.AsParallel()
.WithCancellation(token)
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, Context.API))
.Where(r => r?.Score > 0)
.ToList());
{
var resultList = await Task.Run(() =>
_win32s.Cast<IProgram>()
.Concat(_uwps)
.AsParallel()
.WithCancellation(token)
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, Context.API))
.Where(r => r?.Score > 0)
.ToList());
resultList = resultList.Any() ? resultList : emptyResults;
resultList = resultList.Any() ? resultList : emptyResults;
entry.SetSize(resultList.Count);
entry.SetSlidingExpiration(TimeSpan.FromHours(8));
entry.SetSize(resultList.Count);
entry.SetSlidingExpiration(TimeSpan.FromHours(8));
return resultList;
});
return resultList;
});
return result;
}
@ -84,62 +79,55 @@ namespace Flow.Launcher.Plugin.Program
_settings = context.API.LoadSettingJsonStorage<Settings>();
await Task.Yield();
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () =>
{
_win32Storage = new BinaryStorage<Win32[]>("Win32");
_win32s = _win32Storage.TryLoad(new Win32[] { });
_win32s = _win32Storage.TryLoad(new Win32[]
{
});
_uwpStorage = new BinaryStorage<UWP.Application[]>("UWP");
_uwps = _uwpStorage.TryLoad(new UWP.Application[] { });
_uwps = _uwpStorage.TryLoad(new UWP.Application[]
{
});
});
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>");
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>");
bool indexedWinApps = false;
bool indexedUWPApps = false;
bool cacheEmpty = !_win32s.Any() && !_uwps.Any();
var a = Task.Run(() =>
{
if (IsStartupIndexProgramsRequired || !_win32s.Any())
{
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs);
indexedWinApps = true;
}
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs);
});
var b = Task.Run(() =>
{
if (IsStartupIndexProgramsRequired || !_uwps.Any())
{
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUwpPrograms);
indexedUWPApps = true;
}
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUwpPrograms);
});
var indexTask = Task.WhenAll(a, b).ContinueWith(t =>
{
if (indexedWinApps && indexedUWPApps)
_settings.LastIndexTime = DateTime.Today;
}, TaskScheduler.Current);
if (cacheEmpty)
await Task.WhenAll(a, b);
if (!(_win32s.Any() && _uwps.Any()))
await indexTask;
Win32.WatchProgramUpdate(_settings);
UWP.WatchPackageChange();
}
public static void IndexWin32Programs()
{
var win32S = Win32.All(_settings);
_win32s = win32S;
ResetCache();
}
public static void IndexUwpPrograms()
{
var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major;
var applications = support ? UWP.All() : new UWP.Application[] { };
var applications = support ? UWP.All() : new UWP.Application[]
{
};
_uwps = applications;
ResetCache();
}
public static async Task IndexProgramsAsync()
@ -147,7 +135,6 @@ namespace Flow.Launcher.Plugin.Program
var t1 = Task.Run(IndexWin32Programs);
var t2 = Task.Run(IndexUwpPrograms);
await Task.WhenAll(t1, t2).ConfigureAwait(false);
ResetCache();
_settings.LastIndexTime = DateTime.Today;
}
@ -209,13 +196,11 @@ namespace Flow.Launcher.Plugin.Program
return;
if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
_uwps.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)
.FirstOrDefault()
_uwps.FirstOrDefault(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)
.Enabled = false;
if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
_win32s.Where(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)
.FirstOrDefault()
_win32s.FirstOrDefault(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)
.Enabled = false;
_settings.DisabledProgramSources
@ -248,5 +233,9 @@ namespace Flow.Launcher.Plugin.Program
{
await IndexProgramsAsync();
}
public void Dispose()
{
Win32.Dispose();
}
}
}

View file

@ -18,6 +18,8 @@ using Flow.Launcher.Plugin.Program.Logger;
using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger;
using System.Runtime.Versioning;
using System.Threading.Channels;
namespace Flow.Launcher.Plugin.Program.Programs
{
@ -78,7 +80,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var e = Marshal.GetExceptionForHR((int)hResult);
ProgramLogger.LogException($"|UWP|InitializeAppInfo|{path}" +
"|Error caused while trying to get the details of the UWP program", e);
"|Error caused while trying to get the details of the UWP program", e);
Apps = new List<Application>().ToArray();
}
@ -89,30 +91,28 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
/// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx
private string[] XmlNamespaces(string path)
{
XDocument z = XDocument.Load(path);
if (z.Root != null)
{
var namespaces = z.Root.Attributes().
Where(a => a.IsNamespaceDeclaration).
GroupBy(
a => a.Name.Namespace == XNamespace.None ? string.Empty : a.Name.LocalName,
a => XNamespace.Get(a.Value)
).Select(
g => g.First().ToString()
).ToArray();
var namespaces = z.Root.Attributes().Where(a => a.IsNamespaceDeclaration).GroupBy(
a => a.Name.Namespace == XNamespace.None ? string.Empty : a.Name.LocalName,
a => XNamespace.Get(a.Value)
).Select(
g => g.First().ToString()
).ToArray();
return namespaces;
}
else
{
ProgramLogger.LogException($"|UWP|XmlNamespaces|{path}" +
$"|Error occured while trying to get the XML from {path}", new ArgumentNullException());
$"|Error occured while trying to get the XML from {path}", new ArgumentNullException());
return new string[] { };
return new string[]
{
};
}
}
@ -120,9 +120,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var versionFromNamespace = new Dictionary<string, PackageVersion>
{
{"http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10},
{"http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81},
{"http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8},
{
"http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10
},
{
"http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81
},
{
"http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8
},
};
foreach (var n in versionFromNamespace.Keys)
@ -135,8 +141,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
ProgramLogger.LogException($"|UWP|XmlNamespaces|{Location}" +
"|Trying to get the package version of the UWP program, but a unknown UWP appmanifest version "
+ $"{FullName} from location {Location} is returned.", new FormatException());
"|Trying to get the package version of the UWP program, but a unknown UWP appmanifest version "
+ $"{FullName} from location {Location} is returned.", new FormatException());
Version = PackageVersion.Unknown;
}
@ -171,15 +177,17 @@ namespace Flow.Launcher.Plugin.Program.Programs
}).ToArray();
var updatedListWithoutDisabledApps = applications
.Where(t1 => !Main._settings.DisabledProgramSources
.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
.Select(x => x);
.Where(t1 => !Main._settings.DisabledProgramSources
.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
.Select(x => x);
return updatedListWithoutDisabledApps.ToArray();
}
else
{
return new Application[] { };
return new Application[]
{
};
}
}
@ -215,7 +223,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
catch (Exception e)
{
ProgramLogger.LogException("UWP", "CurrentUserPackages", $"id", "An unexpected error occured and "
+ $"unable to verify if package is valid", e);
+ $"unable to verify if package is valid", e);
return false;
}
@ -225,7 +233,42 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
else
{
return new Package[] { };
return new Package[]
{
};
}
}
private static Channel<byte> PackageChangeChannel = Channel.CreateBounded<byte>(1);
public static async Task WatchPackageChange()
{
if (Environment.OSVersion.Version.Major >= 10)
{
var catalog = PackageCatalog.OpenForCurrentUser();
catalog.PackageInstalling += (_, args) =>
{
if (args.IsComplete)
PackageChangeChannel.Writer.TryWrite(default);
};
catalog.PackageUninstalling += (_, args) =>
{
if (args.IsComplete)
PackageChangeChannel.Writer.TryWrite(default);
};
catalog.PackageUpdating += (_, args) =>
{
if (args.IsComplete)
PackageChangeChannel.Writer.TryWrite(default);
};
while (await PackageChangeChannel.Reader.WaitToReadAsync().ConfigureAwait(false))
{
await Task.Delay(3000).ConfigureAwait(false);
PackageChangeChannel.Reader.TryRead(out _);
await Task.Run(Main.IndexUwpPrograms);
}
}
}
@ -325,7 +368,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
e.SpecialKeyState.ShiftPressed &&
!e.SpecialKeyState.AltPressed &&
!e.SpecialKeyState.WinPressed
);
);
if (elevated && CanRunElevated)
{
@ -358,14 +401,12 @@ namespace Flow.Launcher.Plugin.Program.Programs
new Result
{
Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
Action = _ =>
{
Main.Context.API.OpenDirectory(Package.Location);
return true;
},
IcoPath = "Images/folder.png"
}
};
@ -414,8 +455,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var info = new ProcessStartInfo(command)
{
UseShellExecute = true,
Verb = "runas",
UseShellExecute = true, Verb = "runas",
};
Main.StartProcess(Process.Start, info);
@ -492,7 +532,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
else
{
ProgramLogger.LogException($"|UWP|ResourceFromPri|{Package.Location}|Can't load null or empty result "
+ $"pri {source} in uwp location {Package.Location}", new NullReferenceException());
+ $"pri {source} in uwp location {Package.Location}", new NullReferenceException());
return string.Empty;
}
}
@ -532,9 +572,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var logoKeyFromVersion = new Dictionary<PackageVersion, string>
{
{ PackageVersion.Windows10, "Square44x44Logo" },
{ PackageVersion.Windows81, "Square30x30Logo" },
{ PackageVersion.Windows8, "SmallLogo" },
{
PackageVersion.Windows10, "Square44x44Logo"
},
{
PackageVersion.Windows81, "Square30x30Logo"
},
{
PackageVersion.Windows8, "SmallLogo"
},
};
if (logoKeyFromVersion.ContainsKey(Package.Version))
{
@ -571,14 +617,40 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var end = path.Length - extension.Length;
var prefix = path.Substring(0, end);
var paths = new List<string> { path };
var paths = new List<string>
{
path
};
var scaleFactors = new Dictionary<PackageVersion, List<int>>
{
// scale factors on win10: https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables,
{ PackageVersion.Windows10, new List<int> { 100, 125, 150, 200, 400 } },
{ PackageVersion.Windows81, new List<int> { 100, 120, 140, 160, 180 } },
{ PackageVersion.Windows8, new List<int> { 100 } }
{
PackageVersion.Windows10, new List<int>
{
100,
125,
150,
200,
400
}
},
{
PackageVersion.Windows81, new List<int>
{
100,
120,
140,
160,
180
}
},
{
PackageVersion.Windows8, new List<int>
{
100
}
}
};
if (scaleFactors.ContainsKey(Package.Version))
@ -597,15 +669,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
else
{
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
$"|{UserModelId} can't find logo uri for {uri} in package location: {Package.Location}", new FileNotFoundException());
$"|{UserModelId} can't find logo uri for {uri} in package location: {Package.Location}", new FileNotFoundException());
return string.Empty;
}
}
else
{
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
$"|Unable to find extension from {uri} for {UserModelId} " +
$"in package location {Package.Location}", new FileNotFoundException());
$"|Unable to find extension from {uri} for {UserModelId} " +
$"in package location {Package.Location}", new FileNotFoundException());
return string.Empty;
}
}
@ -632,8 +704,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
else
{
ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Avaliable" : path)}" +
$"|Unable to get logo for {UserModelId} from {path} and" +
$" located in {Package.Location}", new FileNotFoundException());
$"|Unable to get logo for {UserModelId} from {path} and" +
$" located in {Package.Location}", new FileNotFoundException());
return new BitmapImage(new Uri(Constant.MissingImgIcon));
}
}
@ -681,8 +753,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
else
{
ProgramLogger.LogException($"|UWP|PlatedImage|{Package.Location}" +
$"|Unable to convert background string {BackgroundColor} " +
$"to color for {Package.Location}", new InvalidOperationException());
$"|Unable to convert background string {BackgroundColor} " +
$"to color for {Package.Location}", new InvalidOperationException());
return new BitmapImage(new Uri(Constant.MissingImgIcon));
}
@ -727,5 +799,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern Hresult SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf,
IntPtr ppvReserved);
}
}

View file

@ -12,9 +12,11 @@ using Flow.Launcher.Plugin.Program.Logger;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger;
using System.Collections;
using System.Diagnostics;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Channels;
namespace Flow.Launcher.Plugin.Program.Programs
{
@ -109,7 +111,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
c.SpecialKeyState.ShiftPressed &&
!c.SpecialKeyState.AltPressed &&
!c.SpecialKeyState.WinPressed
);
);
var info = new ProcessStartInfo
{
@ -194,6 +196,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
return Name;
}
public static List<FileSystemWatcher> Watchers = new List<FileSystemWatcher>();
private static Win32 Win32Program(string path)
{
try
@ -216,7 +221,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|Win32|Win32Program|{path}" +
$"|Permission denied when trying to load the program from {path}", e);
return new Win32() { Valid = false, Enabled = false };
return new Win32()
{
Valid = false, Enabled = false
};
}
}
@ -294,7 +302,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
$"|Permission denied when trying to load the program from {path}", e);
return new Win32() { Valid = false, Enabled = false };
return new Win32()
{
Valid = false, Enabled = false
};
}
}
@ -305,8 +316,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
return Directory.EnumerateFiles(directory, "*", new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = true
IgnoreInaccessible = true, RecurseSubdirectories = true
}).Where(x => suffixes.Contains(Extension(x)));
}
@ -545,5 +555,70 @@ namespace Flow.Launcher.Plugin.Program.Programs
return UniqueIdentifier == other.UniqueIdentifier;
}
private static IEnumerable<string> GetStartMenuPaths()
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms);
return new[]
{
directory1, directory2
};
}
public static void WatchProgramUpdate(Settings settings)
{
var paths = new List<string>();
if (settings.EnableStartMenuSource)
paths.AddRange(GetStartMenuPaths());
paths.AddRange(from source in settings.ProgramSources where source.Enabled select source.Location);
foreach (var directory in from path in paths where Directory.Exists(path) select path)
{
WatchDirectory(directory);
}
_ = Task.Run(MonitorDirectoryChangeAsync);
}
private static Channel<byte> indexQueue = Channel.CreateBounded<byte>(1);
public static async Task MonitorDirectoryChangeAsync()
{
var reader = indexQueue.Reader;
while (await reader.WaitToReadAsync())
{
await Task.Delay(500);
while (reader.TryRead(out _))
{
}
await Task.Run(Main.IndexWin32Programs);
}
}
public static void WatchDirectory(string directory)
{
if (!Directory.Exists(directory))
{
throw new ArgumentException("Path Not Exist");
}
var watcher = new FileSystemWatcher(directory);
watcher.Created += static (_, _) => indexQueue.Writer.TryWrite(default);
watcher.Deleted += static (_, _) => indexQueue.Writer.TryWrite(default);
watcher.EnableRaisingEvents = true;
watcher.IncludeSubdirectories = true;
Watchers.Add(watcher);
}
public static void Dispose()
{
foreach (var fileSystemWatcher in Watchers)
{
fileSystemWatcher.Dispose();
}
}
}
}

View file

@ -4,7 +4,7 @@
"Name": "Program",
"Description": "Search programs in Flow.Launcher",
"Author": "qianlifeng",
"Version": "1.8.2",
"Version": "1.9.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",