Merge branch 'dev' into new_results_model

This commit is contained in:
DB P 2023-02-03 19:43:29 +09:00 committed by GitHub
commit f893c8af29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 655 additions and 443 deletions

View file

@ -62,6 +62,8 @@ TobiasSekan
Img Img
img img
resx resx
bak
tmp
directx directx
mvvm mvvm
dlg dlg
@ -83,4 +85,6 @@ btn
otf otf
searchplugin searchplugin
Noresult Noresult
wpftk wpftk
mkv
flac

View file

@ -259,9 +259,16 @@ namespace Flow.Launcher.Core.Plugin
await using var registeredEvent = token.Register(() => await using var registeredEvent = token.Register(() =>
{ {
if (!process.HasExited) try
process.Kill(); {
sourceBuffer.Dispose(); if (!process.HasExited)
process.Kill();
sourceBuffer.Dispose();
}
catch (Exception e)
{
Log.Exception("|JsonRPCPlugin.ExecuteAsync|Exception when kill process", e);
}
}); });
try try
@ -288,7 +295,7 @@ namespace Flow.Launcher.Core.Plugin
} }
sourceBuffer.Seek(0, SeekOrigin.Begin); sourceBuffer.Seek(0, SeekOrigin.Begin);
return sourceBuffer; return sourceBuffer;
} }
@ -376,7 +383,8 @@ namespace Flow.Launcher.Core.Plugin
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
var panel = new StackPanel var panel = new StackPanel
{ {
Orientation = Orientation.Vertical, VerticalAlignment = VerticalAlignment.Center, Orientation = Orientation.Vertical,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingLabelPanelMargin Margin = settingLabelPanelMargin
}; };
RowDefinition gridRow = new RowDefinition(); RowDefinition gridRow = new RowDefinition();
@ -390,8 +398,10 @@ namespace Flow.Launcher.Core.Plugin
}; };
var desc = new TextBlock() var desc = new TextBlock()
{ {
Text = attribute.Description, FontSize = 12, Text = attribute.Description,
VerticalAlignment = VerticalAlignment.Center,Margin = settingDescMargin, FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingDescMargin,
TextWrapping = TextWrapping.WrapWithOverflow TextWrapping = TextWrapping.WrapWithOverflow
}; };
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
@ -405,7 +415,7 @@ namespace Flow.Launcher.Core.Plugin
panel.Children.Add(name); panel.Children.Add(name);
panel.Children.Add(desc); panel.Children.Add(desc);
} }
Grid.SetColumn(panel, 0); Grid.SetColumn(panel, 0);
Grid.SetRow(panel, rowCount); Grid.SetRow(panel, rowCount);
@ -420,20 +430,20 @@ namespace Flow.Launcher.Core.Plugin
{ {
Text = attribute.Description.Replace("\\r\\n", "\r\n"), Text = attribute.Description.Replace("\\r\\n", "\r\n"),
Margin = settingTextBlockMargin, Margin = settingTextBlockMargin,
Padding = new Thickness(0,0,0,0), Padding = new Thickness(0, 0, 0, 0),
HorizontalAlignment = System.Windows.HorizontalAlignment.Left, HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
TextAlignment = TextAlignment.Left, TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap TextWrapping = TextWrapping.Wrap
}; };
Grid.SetColumn(contentControl, 0); Grid.SetColumn(contentControl, 0);
Grid.SetColumnSpan(contentControl, 2); Grid.SetColumnSpan(contentControl, 2);
Grid.SetRow(contentControl, rowCount); Grid.SetRow(contentControl, rowCount);
if (rowCount != 0) if (rowCount != 0)
mainPanel.Children.Add(sep); mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount); Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0); Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2); Grid.SetColumnSpan(sep, 2);
break; break;
} }
case "input": case "input":
{ {
@ -449,50 +459,49 @@ namespace Flow.Launcher.Core.Plugin
Settings[attribute.Name] = textBox.Text; Settings[attribute.Name] = textBox.Text;
}; };
contentControl = textBox; contentControl = textBox;
Grid.SetColumn(contentControl, 1); Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount); Grid.SetRow(contentControl, rowCount);
if (rowCount != 0) if (rowCount != 0)
mainPanel.Children.Add(sep); mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount); Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0); Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2); Grid.SetColumnSpan(sep, 2);
break; break;
} }
case "inputWithFileBtn": case "inputWithFileBtn":
{
var textBox = new TextBox()
{ {
var textBox = new TextBox() Margin = new Thickness(10, 0, 0, 0),
{ Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = new Thickness(10, 0, 0, 0), HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
Text = Settings[attribute.Name] as string ?? string.Empty, ToolTip = attribute.Description
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, };
ToolTip = attribute.Description textBox.TextChanged += (_, _) =>
}; {
textBox.TextChanged += (_, _) => Settings[attribute.Name] = textBox.Text;
{ };
Settings[attribute.Name] = textBox.Text; var Btn = new System.Windows.Controls.Button()
}; {
var Btn = new System.Windows.Controls.Button() Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
{ };
Margin = new Thickness(10,0,0,0), var dockPanel = new DockPanel()
Content = "Browse" {
}; Margin = settingControlMargin
var dockPanel = new DockPanel() };
{ DockPanel.SetDock(Btn, Dock.Right);
Margin = settingControlMargin dockPanel.Children.Add(Btn);
}; dockPanel.Children.Add(textBox);
DockPanel.SetDock(Btn, Dock.Right); contentControl = dockPanel;
dockPanel.Children.Add(Btn); Grid.SetColumn(contentControl, 1);
dockPanel.Children.Add(textBox); Grid.SetRow(contentControl, rowCount);
contentControl = dockPanel; if (rowCount != 0)
Grid.SetColumn(contentControl, 1); mainPanel.Children.Add(sep);
Grid.SetRow(contentControl, rowCount); Grid.SetRow(sep, rowCount);
if (rowCount != 0) Grid.SetColumn(sep, 0);
mainPanel.Children.Add(sep); Grid.SetColumnSpan(sep, 2);
Grid.SetRow(sep, rowCount); break;
Grid.SetColumn(sep, 0); }
Grid.SetColumnSpan(sep, 2);
break;
}
case "textarea": case "textarea":
{ {
var textBox = new TextBox() var textBox = new TextBox()
@ -511,14 +520,14 @@ namespace Flow.Launcher.Core.Plugin
Settings[attribute.Name] = ((TextBox)sender).Text; Settings[attribute.Name] = ((TextBox)sender).Text;
}; };
contentControl = textBox; contentControl = textBox;
Grid.SetColumn(contentControl, 1); Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount); Grid.SetRow(contentControl, rowCount);
if (rowCount != 0) if (rowCount != 0)
mainPanel.Children.Add(sep); mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount); Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0); Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2); Grid.SetColumnSpan(sep, 2);
break; break;
} }
case "passwordBox": case "passwordBox":
{ {
@ -535,14 +544,14 @@ namespace Flow.Launcher.Core.Plugin
Settings[attribute.Name] = ((PasswordBox)sender).Password; Settings[attribute.Name] = ((PasswordBox)sender).Password;
}; };
contentControl = passwordBox; contentControl = passwordBox;
Grid.SetColumn(contentControl, 1); Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount); Grid.SetRow(contentControl, rowCount);
if (rowCount != 0) if (rowCount != 0)
mainPanel.Children.Add(sep); mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount); Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0); Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2); Grid.SetColumnSpan(sep, 2);
break; break;
} }
case "dropdown": case "dropdown":
{ {
@ -559,14 +568,14 @@ namespace Flow.Launcher.Core.Plugin
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
}; };
contentControl = comboBox; contentControl = comboBox;
Grid.SetColumn(contentControl, 1); Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount); Grid.SetRow(contentControl, rowCount);
if (rowCount != 0) if (rowCount != 0)
mainPanel.Children.Add(sep); mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount); Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0); Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2); Grid.SetColumnSpan(sep, 2);
break; break;
} }
case "checkbox": case "checkbox":
var checkBox = new CheckBox var checkBox = new CheckBox
@ -592,13 +601,11 @@ namespace Flow.Launcher.Core.Plugin
case "hyperlink": case "hyperlink":
var hyperlink = new Hyperlink var hyperlink = new Hyperlink
{ {
ToolTip = attribute.Description, ToolTip = attribute.Description, NavigateUri = attribute.url
NavigateUri = attribute.url
}; };
var linkbtn = new System.Windows.Controls.Button var linkbtn = new System.Windows.Controls.Button
{ {
HorizontalAlignment = System.Windows.HorizontalAlignment.Right, HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin
Margin = settingControlMargin
}; };
linkbtn.Content = attribute.urlLabel; linkbtn.Content = attribute.urlLabel;
@ -619,7 +626,7 @@ namespace Flow.Launcher.Core.Plugin
mainPanel.Children.Add(panel); mainPanel.Children.Add(panel);
mainPanel.Children.Add(contentControl); mainPanel.Children.Add(contentControl);
rowCount++; rowCount++;
} }
return settingWindow; return settingWindow;
} }

View file

@ -33,7 +33,7 @@ namespace Flow.Launcher.Core
public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
{ {
await UpdateLock.WaitAsync(); await UpdateLock.WaitAsync().ConfigureAwait(false);
try try
{ {
if (!silentUpdate) if (!silentUpdate)
@ -88,9 +88,13 @@ namespace Flow.Launcher.Core
UpdateManager.RestartApp(Constant.ApplicationFileName); UpdateManager.RestartApp(Constant.ApplicationFileName);
} }
} }
catch (Exception e) when (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException) catch (Exception e)
{ {
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); if ((e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException))
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
else
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
if (!silentUpdate) if (!silentUpdate)
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
api.GetTranslation("update_flowlauncher_check_connection")); api.GetTranslation("update_flowlauncher_check_connection"));

View file

@ -1,4 +1,6 @@
using System; #nullable enable
using System;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
@ -16,7 +18,7 @@ namespace Flow.Launcher.Infrastructure
/// <summary> /// <summary>
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy /// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
/// </summary> /// </summary>
public static T NonNull<T>(this T obj) public static T NonNull<T>(this T? obj)
{ {
if (obj == null) if (obj == null)
{ {

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Navigation;
namespace Flow.Launcher.Infrastructure.Hotkey namespace Flow.Launcher.Infrastructure.Hotkey
{ {
@ -11,10 +12,10 @@ namespace Flow.Launcher.Infrastructure.Hotkey
public bool Shift { get; set; } public bool Shift { get; set; }
public bool Win { get; set; } public bool Win { get; set; }
public bool Ctrl { get; set; } public bool Ctrl { get; set; }
public Key CharKey { get; set; }
public Key CharKey { get; set; } = Key.None;
Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string> private static readonly Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string>
{ {
{Key.Space, "Space"}, {Key.Space, "Space"},
{Key.Oem3, "~"} {Key.Oem3, "~"}
@ -27,19 +28,19 @@ namespace Flow.Launcher.Infrastructure.Hotkey
ModifierKeys modifierKeys = ModifierKeys.None; ModifierKeys modifierKeys = ModifierKeys.None;
if (Alt) if (Alt)
{ {
modifierKeys = ModifierKeys.Alt; modifierKeys |= ModifierKeys.Alt;
} }
if (Shift) if (Shift)
{ {
modifierKeys = modifierKeys | ModifierKeys.Shift; modifierKeys |= ModifierKeys.Shift;
} }
if (Win) if (Win)
{ {
modifierKeys = modifierKeys | ModifierKeys.Windows; modifierKeys |= ModifierKeys.Windows;
} }
if (Ctrl) if (Ctrl)
{ {
modifierKeys = modifierKeys | ModifierKeys.Control; modifierKeys |= ModifierKeys.Control;
} }
return modifierKeys; return modifierKeys;
} }
@ -86,7 +87,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey
Ctrl = true; Ctrl = true;
keys.Remove("Ctrl"); keys.Remove("Ctrl");
} }
if (keys.Count > 0) if (keys.Count == 1)
{ {
string charKey = keys[0]; string charKey = keys[0];
KeyValuePair<Key, string>? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey); KeyValuePair<Key, string>? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey);
@ -110,36 +111,75 @@ namespace Flow.Launcher.Infrastructure.Hotkey
public override string ToString() public override string ToString()
{ {
string text = string.Empty; List<string> keys = new List<string>();
if (Ctrl) if (Ctrl)
{ {
text += "Ctrl + "; keys.Add("Ctrl");
} }
if (Alt) if (Alt)
{ {
text += "Alt + "; keys.Add("Alt");
} }
if (Shift) if (Shift)
{ {
text += "Shift + "; keys.Add("Shift");
} }
if (Win) if (Win)
{ {
text += "Win + "; keys.Add("Win");
} }
if (CharKey != Key.None) if (CharKey != Key.None)
{ {
text += specialSymbolDictionary.ContainsKey(CharKey) keys.Add(specialSymbolDictionary.ContainsKey(CharKey)
? specialSymbolDictionary[CharKey] ? specialSymbolDictionary[CharKey]
: CharKey.ToString(); : CharKey.ToString());
}
else if (!string.IsNullOrEmpty(text))
{
text = text.Remove(text.Length - 3);
} }
return string.Join(" + ", keys);
}
return text; public bool Validate()
{
switch (CharKey)
{
case Key.LeftAlt:
case Key.RightAlt:
case Key.LeftCtrl:
case Key.RightCtrl:
case Key.LeftShift:
case Key.RightShift:
case Key.LWin:
case Key.RWin:
return false;
default:
if (ModifierKeys == ModifierKeys.None)
{
return !((CharKey >= Key.A && CharKey <= Key.Z) ||
(CharKey >= Key.D0 && CharKey <= Key.D9) ||
(CharKey >= Key.NumPad0 && CharKey <= Key.NumPad9));
}
else
{
return CharKey != Key.None;
}
}
}
public override bool Equals(object obj)
{
if (obj is HotkeyModel other)
{
return ModifierKeys == other.ModifierKeys && CharKey == other.CharKey;
}
else
{
return false;
}
}
public override int GetHashCode()
{
return HashCode.Combine(ModifierKeys, CharKey);
} }
} }
} }

View file

@ -1,4 +1,5 @@
using System; #nullable enable
using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
@ -11,62 +12,82 @@ namespace Flow.Launcher.Infrastructure.Storage
/// </summary> /// </summary>
public class JsonStorage<T> where T : new() public class JsonStorage<T> where T : new()
{ {
protected T _data; protected T? Data;
// need a new directory name // need a new directory name
public const string DirectoryName = "Settings"; public const string DirectoryName = "Settings";
public const string FileSuffix = ".json"; public const string FileSuffix = ".json";
public string FilePath { get; set; }
public string DirectoryPath { get; set; } protected string FilePath { get; init; } = null!;
private string TempFilePath => $"{FilePath}.tmp";
private string BackupFilePath => $"{FilePath}.bak";
protected string DirectoryPath { get; init; } = null!;
public T Load() public T Load()
{ {
string? serialized = null;
if (File.Exists(FilePath)) if (File.Exists(FilePath))
{ {
var serialized = File.ReadAllText(FilePath); serialized = File.ReadAllText(FilePath);
if (!string.IsNullOrWhiteSpace(serialized)) }
if (!string.IsNullOrEmpty(serialized))
{
try
{ {
Deserialize(serialized); Data = JsonSerializer.Deserialize<T>(serialized) ?? TryLoadBackup() ?? LoadDefault();
} }
else catch (JsonException)
{ {
LoadDefault(); Data = TryLoadBackup() ?? LoadDefault();
} }
} }
else else
{ {
LoadDefault(); Data = TryLoadBackup() ?? LoadDefault();
} }
return _data.NonNull();
return Data.NonNull();
} }
private void Deserialize(string serialized) private T LoadDefault()
{
try
{
_data = JsonSerializer.Deserialize<T>(serialized);
}
catch (JsonException e)
{
LoadDefault();
Log.Exception($"|JsonStorage.Deserialize|Deserialize error for json <{FilePath}>", e);
}
if (_data == null)
{
LoadDefault();
}
}
private void LoadDefault()
{ {
if (File.Exists(FilePath)) if (File.Exists(FilePath))
{ {
BackupOriginFile(); BackupOriginFile();
} }
_data = new T(); return new T();
Save(); }
private T? TryLoadBackup()
{
if (!File.Exists(BackupFilePath))
return default;
try
{
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
if (data != null)
{
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
File.Replace(BackupFilePath, FilePath, null);
return data;
}
return default;
}
catch (JsonException)
{
return default;
}
} }
private void BackupOriginFile() private void BackupOriginFile()
@ -82,13 +103,22 @@ namespace Flow.Launcher.Infrastructure.Storage
public void Save() public void Save()
{ {
string serialized = JsonSerializer.Serialize(_data, new JsonSerializerOptions() { WriteIndented = true }); string serialized = JsonSerializer.Serialize(Data,
new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(FilePath, serialized); File.WriteAllText(TempFilePath, serialized);
if (!File.Exists(FilePath))
{
File.Move(TempFilePath, FilePath);
}
else
{
File.Replace(TempFilePath, FilePath, BackupFilePath);
}
} }
} }
[Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
"This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
public class JsonStrorage<T> : JsonStorage<T> where T : new() { }
} }

View file

@ -18,7 +18,7 @@ namespace Flow.Launcher.Infrastructure.Storage
public PluginJsonStorage(T data) : this() public PluginJsonStorage(T data) : this()
{ {
_data = data; Data = data;
} }
} }
} }

View file

@ -14,10 +14,10 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Version>3.0.1</Version> <Version>3.1.0</Version>
<PackageVersion>3.0.1</PackageVersion> <PackageVersion>3.1.0</PackageVersion>
<AssemblyVersion>3.0.1</AssemblyVersion> <AssemblyVersion>3.1.0</AssemblyVersion>
<FileVersion>3.0.1</FileVersion> <FileVersion>3.1.0</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId> <PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors> <Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>

View file

@ -1,7 +1,9 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
#pragma warning disable IDE0005
using System.Windows; using System.Windows;
#pragma warning restore IDE0005
namespace Flow.Launcher.Plugin.SharedCommands namespace Flow.Launcher.Plugin.SharedCommands
{ {
@ -206,22 +208,16 @@ namespace Flow.Launcher.Plugin.SharedCommands
///</summary> ///</summary>
public static string GetPreviousExistingDirectory(Func<string, bool> locationExists, string path) public static string GetPreviousExistingDirectory(Func<string, bool> locationExists, string path)
{ {
var previousDirectoryPath = "";
var index = path.LastIndexOf('\\'); var index = path.LastIndexOf('\\');
if (index > 0 && index < (path.Length - 1)) if (index > 0 && index < (path.Length - 1))
{ {
previousDirectoryPath = path.Substring(0, index + 1); string previousDirectoryPath = path.Substring(0, index + 1);
if (!locationExists(previousDirectoryPath)) return locationExists(previousDirectoryPath) ? previousDirectoryPath : "";
{
return "";
}
} }
else else
{ {
return ""; return "";
} }
return previousDirectoryPath;
} }
///<summary> ///<summary>
@ -241,5 +237,33 @@ namespace Flow.Launcher.Plugin.SharedCommands
return path; return path;
} }
/// <summary>
/// Returns if <paramref name="parentPath"/> contains <paramref name="subPath"/>.
/// From https://stackoverflow.com/a/66877016
/// </summary>
/// <param name="parentPath">Parent path</param>
/// <param name="subPath">Sub path</param>
/// <param name="allowEqual">If <see langword="true"/>, when <paramref name="parentPath"/> and <paramref name="subPath"/> are equal, returns <see langword="true"/></param>
/// <returns></returns>
public static bool PathContains(string parentPath, string subPath, bool allowEqual = false)
{
var rel = Path.GetRelativePath(parentPath.EnsureTrailingSlash(), subPath);
return (rel != "." || allowEqual)
&& rel != ".."
&& !rel.StartsWith("../")
&& !rel.StartsWith(@"..\")
&& !Path.IsPathRooted(rel);
}
/// <summary>
/// Returns path ended with "\"
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static string EnsureTrailingSlash(this string path)
{
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
}
} }
} }

View file

@ -0,0 +1,53 @@
using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework;
namespace Flow.Launcher.Test
{
[TestFixture]
public class FilesFoldersTest
{
// Testcases from https://stackoverflow.com/a/31941905/20703207
// Disk
[TestCase(@"c:", @"c:\foo", true)]
[TestCase(@"c:\", @"c:\foo", true)]
// Slash
[TestCase(@"c:\foo\bar\", @"c:\foo\", false)]
[TestCase(@"c:\foo\bar", @"c:\foo\", false)]
[TestCase(@"c:\foo", @"c:\foo\bar", true)]
[TestCase(@"c:\foo\", @"c:\foo\bar", true)]
// File
[TestCase(@"c:\foo", @"c:\foo\a.txt", true)]
[TestCase(@"c:\foo", @"c:/foo/a.txt", true)]
[TestCase(@"c:\FOO\a.txt", @"c:\foo", false)]
[TestCase(@"c:\foo\a.txt", @"c:\foo\", false)]
[TestCase(@"c:\foobar\a.txt", @"c:\foo", false)]
[TestCase(@"c:\foobar\a.txt", @"c:\foo\", false)]
[TestCase(@"c:\foo\", @"c:\foo.txt", false)]
// Prefix
[TestCase(@"c:\foo", @"c:\foobar", false)]
[TestCase(@"C:\Program", @"C:\Program Files\", false)]
[TestCase(@"c:\foobar", @"c:\foo\a.txt", false)]
[TestCase(@"c:\foobar\", @"c:\foo\a.txt", false)]
// Edge case
[TestCase(@"c:\foo", @"c:\foo\..\bar\baz", false)]
[TestCase(@"c:\bar", @"c:\foo\..\bar\baz", true)]
[TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)]
// Equality
[TestCase(@"c:\foo", @"c:\foo", false)]
[TestCase(@"c:\foo\", @"c:\foo", false)]
[TestCase(@"c:\foo", @"c:\foo\", false)]
public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult)
{
Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path));
}
[TestCase(@"c:\foo", @"c:\foo", true)]
[TestCase(@"c:\foo\", @"c:\foo", true)]
[TestCase(@"c:\foo", @"c:\foo\", true)]
public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeTrue(string parentPath, string path, bool expectedResult)
{
Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, true));
}
}
}

View file

@ -7,9 +7,11 @@ using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static Flow.Launcher.Plugin.Explorer.Search.SearchManager;
namespace Flow.Launcher.Test.Plugins namespace Flow.Launcher.Test.Plugins
{ {
@ -176,7 +178,7 @@ namespace Flow.Launcher.Test.Plugins
var searchManager = new SearchManager(new Settings(), new PluginInitContext()); var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When // When
var result = SearchManager.IsFileContentSearch(query.ActionKeyword); var result = searchManager.IsFileContentSearch(query.ActionKeyword);
// Then // Then
Assert.IsTrue(result, Assert.IsTrue(result,
@ -193,6 +195,7 @@ namespace Flow.Launcher.Test.Plugins
[TestCase(@"c:\>*", true)] [TestCase(@"c:\>*", true)]
[TestCase(@"c:\>", true)] [TestCase(@"c:\>", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation", true)]
public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
{ {
// When, Given // When, Given
@ -393,5 +396,68 @@ namespace Flow.Launcher.Test.Plugins
// Then // Then
Assert.AreEqual(result, expectedResult); Assert.AreEqual(result, expectedResult);
} }
[TestCase(@"c:\foo", @"c:\foo", true)]
[TestCase(@"C:\Foo\", @"c:\foo\", true)]
[TestCase(@"c:\foo", @"c:\foo\", false)]
public void GivenTwoPaths_WhenCompared_ThenShouldBeExpectedSameOrDifferent(string path1, string path2, bool expectedResult)
{
// Given
var comparator = PathEqualityComparator.Instance;
var result1 = new Result
{
Title = Path.GetFileName(path1),
SubTitle = path1
};
var result2 = new Result
{
Title = Path.GetFileName(path2),
SubTitle = path2
};
// When, Then
Assert.AreEqual(expectedResult, comparator.Equals(result1, result2));
}
[TestCase(@"c:\foo\", @"c:\foo\")]
[TestCase(@"C:\Foo\", @"c:\foo\")]
public void GivenTwoPaths_WhenComparedHasCode_ThenShouldBeSame(string path1, string path2)
{
// Given
var comparator = PathEqualityComparator.Instance;
var result1 = new Result
{
Title = Path.GetFileName(path1),
SubTitle = path1
};
var result2 = new Result
{
Title = Path.GetFileName(path2),
SubTitle = path2
};
var hash1 = comparator.GetHashCode(result1);
var hash2 = comparator.GetHashCode(result2);
// When, Then
Assert.IsTrue(hash1 == hash2);
}
[TestCase(@"%appdata%", true)]
[TestCase(@"%appdata%\123", true)]
[TestCase(@"c:\foo %appdata%\", false)]
[TestCase(@"c:\users\%USERNAME%\downloads", true)]
[TestCase(@"c:\downloads", false)]
[TestCase(@"%", false)]
[TestCase(@"%%", false)]
[TestCase(@"%bla%blabla%", false)]
public void GivenPath_WhenHavingEnvironmentVariableOrNot_ThenShouldBeExpected(string path, bool expectedResult)
{
// When
var result = EnvironmentVariables.HasEnvironmentVar(path);
// Then
Assert.AreEqual(result, expectedResult);
}
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
using System.Windows; using System.Windows;
@ -84,14 +85,14 @@ namespace Flow.Launcher
Current.MainWindow = window; Current.MainWindow = window;
Current.MainWindow.Title = Constant.FlowLauncher; Current.MainWindow.Title = Constant.FlowLauncher;
HotKeyMapper.Initialize(_mainVM); HotKeyMapper.Initialize(_mainVM);
// happlebao todo temp fix for instance code logic // todo temp fix for instance code logic
// load plugin before change language, because plugin language also needs be changed // load plugin before change language, because plugin language also needs be changed
InternationalizationManager.Instance.Settings = _settings; InternationalizationManager.Instance.Settings = _settings;
InternationalizationManager.Instance.ChangeLanguage(_settings.Language); InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
// main windows needs initialized before theme change because of blur settigns // main windows needs initialized before theme change because of blur settings
ThemeManager.Instance.Settings = _settings; ThemeManager.Instance.Settings = _settings;
ThemeManager.Instance.ChangeTheme(_settings.Theme); ThemeManager.Instance.ChangeTheme(_settings.Theme);
@ -130,20 +131,17 @@ namespace Flow.Launcher
//[Conditional("RELEASE")] //[Conditional("RELEASE")]
private void AutoUpdates() private void AutoUpdates()
{ {
Task.Run(async () => _ = Task.Run(async () =>
{ {
if (_settings.AutoUpdates) if (_settings.AutoUpdates)
{ {
// check udpate every 5 hours // check update every 5 hours
var timer = new Timer(1000 * 60 * 60 * 5); var timer = new PeriodicTimer(TimeSpan.FromHours(5));
timer.Elapsed += async (s, e) =>
{
await _updater.UpdateAppAsync(API);
};
timer.Start();
// check updates on startup
await _updater.UpdateAppAsync(API); await _updater.UpdateAppAsync(API);
while (await timer.WaitForNextTickAsync())
// check updates on startup
await _updater.UpdateAppAsync(API);
} }
}); });
} }

View file

@ -48,6 +48,7 @@
Margin="0,0,18,0" Margin="0,0,18,0"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
input:InputMethod.IsInputMethodEnabled="False" input:InputMethod.IsInputMethodEnabled="False"
GotFocus="tbHotkey_GotFocus"
LostFocus="tbHotkey_LostFocus" LostFocus="tbHotkey_LostFocus"
PreviewKeyDown="TbHotkey_OnPreviewKeyDown" PreviewKeyDown="TbHotkey_OnPreviewKeyDown"
TabIndex="100" /> TabIndex="100" />

View file

@ -9,16 +9,11 @@ using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using System.Threading; using System.Threading;
using System.Windows.Interop;
namespace Flow.Launcher namespace Flow.Launcher
{ {
public partial class HotkeyControl : UserControl public partial class HotkeyControl : UserControl
{ {
private Brush tbMsgForegroundColorOriginal;
private string tbMsgTextOriginal;
public HotkeyModel CurrentHotkey { get; private set; } public HotkeyModel CurrentHotkey { get; private set; }
public bool CurrentHotkeyAvailable { get; private set; } public bool CurrentHotkeyAvailable { get; private set; }
@ -29,8 +24,6 @@ namespace Flow.Launcher
public HotkeyControl() public HotkeyControl()
{ {
InitializeComponent(); InitializeComponent();
tbMsgTextOriginal = tbMsg.Text;
tbMsgForegroundColorOriginal = tbMsg.Foreground;
} }
private CancellationTokenSource hotkeyUpdateSource; private CancellationTokenSource hotkeyUpdateSource;
@ -55,9 +48,7 @@ namespace Flow.Launcher
specialKeyState.CtrlPressed, specialKeyState.CtrlPressed,
key); key);
var hotkeyString = hotkeyModel.ToString(); if (hotkeyModel.Equals(CurrentHotkey))
if (hotkeyString == tbHotkey.Text)
{ {
return; return;
} }
@ -72,33 +63,32 @@ namespace Flow.Launcher
public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true) public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true)
{ {
CurrentHotkey = keyModel; tbHotkey.Text = keyModel.ToString();
tbHotkey.Text = CurrentHotkey.ToString();
tbHotkey.Select(tbHotkey.Text.Length, 0); tbHotkey.Select(tbHotkey.Text.Length, 0);
if (triggerValidate) if (triggerValidate)
{ {
CurrentHotkeyAvailable = CheckHotkeyAvailability(); bool hotkeyAvailable = CheckHotkeyAvailability(keyModel);
if (!CurrentHotkeyAvailable) CurrentHotkeyAvailable = hotkeyAvailable;
{ SetMessage(hotkeyAvailable);
tbMsg.Foreground = new SolidColorBrush(Colors.Red);
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable");
}
else
{
tbMsg.Foreground = new SolidColorBrush(Colors.Green);
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success");
}
tbMsg.Visibility = Visibility.Visible;
OnHotkeyChanged(); OnHotkeyChanged();
var token = hotkeyUpdateSource.Token; var token = hotkeyUpdateSource.Token;
await Task.Delay(500, token); await Task.Delay(500, token);
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
return; return;
FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null);
Keyboard.ClearFocus(); if (CurrentHotkeyAvailable)
{
CurrentHotkey = keyModel;
// To trigger LostFocus
FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null);
Keyboard.ClearFocus();
}
}
else
{
CurrentHotkey = keyModel;
} }
} }
@ -107,14 +97,40 @@ namespace Flow.Launcher
return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate); return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate);
} }
private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey); private static bool CheckHotkeyAvailability(HotkeyModel hotkey) => hotkey.Validate() && HotKeyMapper.CheckAvailability(hotkey);
public new bool IsFocused => tbHotkey.IsFocused; public new bool IsFocused => tbHotkey.IsFocused;
private void tbHotkey_LostFocus(object sender, RoutedEventArgs e) private void tbHotkey_LostFocus(object sender, RoutedEventArgs e)
{ {
tbMsg.Text = tbMsgTextOriginal; tbHotkey.Text = CurrentHotkey?.ToString() ?? "";
tbHotkey.Select(tbHotkey.Text.Length, 0);
}
private void tbHotkey_GotFocus(object sender, RoutedEventArgs e)
{
ResetMessage();
}
private void ResetMessage()
{
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("flowlauncherPressHotkey");
tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B"); tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B");
} }
private void SetMessage(bool hotkeyAvailable)
{
if (!hotkeyAvailable)
{
tbMsg.Foreground = new SolidColorBrush(Colors.Red);
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable");
}
else
{
tbMsg.Foreground = new SolidColorBrush(Colors.Green);
tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success");
}
tbMsg.Visibility = Visibility.Visible;
}
} }
} }

View file

@ -85,6 +85,8 @@
<ui:NumberBox <ui:NumberBox
x:Name="tbAction" x:Name="tbAction"
Width="200" Width="200"
Maximum="100"
Minimum="-100"
Margin="10,0,15,0" Margin="10,0,15,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"

View file

@ -27,7 +27,7 @@ namespace Flow.Launcher.Resources.Pages
tbMsgTextOriginal = HotkeyControl.tbMsg.Text; tbMsgTextOriginal = HotkeyControl.tbMsg.Text;
tbMsgForegroundColorOriginal = HotkeyControl.tbMsg.Foreground; tbMsgForegroundColorOriginal = HotkeyControl.tbMsg.Foreground;
HotkeyControl.SetHotkeyAsync(new Infrastructure.Hotkey.HotkeyModel(Settings.Hotkey), false); HotkeyControl.SetHotkeyAsync(Settings.Hotkey, false);
} }
private void HotkeyControl_OnGotFocus(object sender, RoutedEventArgs args) private void HotkeyControl_OnGotFocus(object sender, RoutedEventArgs args)
{ {
@ -49,4 +49,4 @@ namespace Flow.Launcher.Resources.Pages
HotkeyControl.tbMsg.Foreground = tbMsgForegroundColorOriginal; HotkeyControl.tbMsg.Foreground = tbMsgForegroundColorOriginal;
} }
} }
} }

View file

@ -396,6 +396,12 @@
<Setter Property="Foreground" Value="#8f8f8f" /> <Setter Property="Foreground" Value="#8f8f8f" />
</Style> </Style>
<!-- DO NOT USE THIS KEY. this key for themes with wrong typo. This key should be removed. Right key is BaseItemHotkeySelectedStyle. -->
<Style x:Key="BaseItemHotkeySelecetedStyle" TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="15" />
<Setter Property="Foreground" Value="#8f8f8f" />
</Style>
<Geometry x:Key="SearchIconImg">F1 M12000,12000z M0,0z M10354,10962C10326,10951 10279,10927 10249,10907 10216,10886 9476,10153 8370,9046 7366,8042 6541,7220 6536,7220 6532,7220 6498,7242 6461,7268 6213,7447 5883,7619 5592,7721 5194,7860 4802,7919 4360,7906 3612,7886 2953,7647 2340,7174 2131,7013 1832,6699 1664,6465 1394,6088 1188,5618 1097,5170 1044,4909 1030,4764 1030,4470 1030,4130 1056,3914 1135,3609 1263,3110 1511,2633 1850,2235 1936,2134 2162,1911 2260,1829 2781,1395 3422,1120 4090,1045 4271,1025 4667,1025 4848,1045 5505,1120 6100,1368 6630,1789 6774,1903 7081,2215 7186,2355 7362,2588 7467,2759 7579,2990 7802,3455 7911,3937 7911,4460 7911,4854 7861,5165 7737,5542 7684,5702 7675,5724 7602,5885 7517,6071 7390,6292 7270,6460 7242,6499 7220,6533 7220,6538 7220,6542 8046,7371 9055,8380 10441,9766 10898,10229 10924,10274 10945,10308 10966,10364 10976,10408 10990,10472 10991,10493 10980,10554 10952,10717 10840,10865 10690,10937 10621,10971 10607,10974 10510,10977 10425,10980 10395,10977 10354,10962z M4685,7050C5214,7001 5694,6809 6100,6484 6209,6396 6396,6209 6484,6100 7151,5267 7246,4110 6721,3190 6369,2571 5798,2137 5100,1956 4706,1855 4222,1855 3830,1957 3448,2056 3140,2210 2838,2453 2337,2855 2010,3427 1908,4080 1877,4274 1877,4656 1908,4850 1948,5105 2028,5370 2133,5590 2459,6272 3077,6782 3810,6973 3967,7014 4085,7034 4290,7053 4371,7061 4583,7059 4685,7050z</Geometry> <Geometry x:Key="SearchIconImg">F1 M12000,12000z M0,0z M10354,10962C10326,10951 10279,10927 10249,10907 10216,10886 9476,10153 8370,9046 7366,8042 6541,7220 6536,7220 6532,7220 6498,7242 6461,7268 6213,7447 5883,7619 5592,7721 5194,7860 4802,7919 4360,7906 3612,7886 2953,7647 2340,7174 2131,7013 1832,6699 1664,6465 1394,6088 1188,5618 1097,5170 1044,4909 1030,4764 1030,4470 1030,4130 1056,3914 1135,3609 1263,3110 1511,2633 1850,2235 1936,2134 2162,1911 2260,1829 2781,1395 3422,1120 4090,1045 4271,1025 4667,1025 4848,1045 5505,1120 6100,1368 6630,1789 6774,1903 7081,2215 7186,2355 7362,2588 7467,2759 7579,2990 7802,3455 7911,3937 7911,4460 7911,4854 7861,5165 7737,5542 7684,5702 7675,5724 7602,5885 7517,6071 7390,6292 7270,6460 7242,6499 7220,6533 7220,6538 7220,6542 8046,7371 9055,8380 10441,9766 10898,10229 10924,10274 10945,10308 10966,10364 10976,10408 10990,10472 10991,10493 10980,10554 10952,10717 10840,10865 10690,10937 10621,10971 10607,10974 10510,10977 10425,10980 10395,10977 10354,10962z M4685,7050C5214,7001 5694,6809 6100,6484 6209,6396 6396,6209 6484,6100 7151,5267 7246,4110 6721,3190 6369,2571 5798,2137 5100,1956 4706,1855 4222,1855 3830,1957 3448,2056 3140,2210 2838,2453 2337,2855 2010,3427 1908,4080 1877,4274 1877,4656 1908,4850 1948,5105 2028,5370 2133,5590 2459,6272 3077,6782 3810,6973 3967,7014 4085,7034 4290,7053 4371,7061 4583,7059 4685,7050z</Geometry>

View file

@ -841,15 +841,20 @@ namespace Flow.Launcher.ViewModel
queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand()); queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand());
} }
string customExpanded = queryBuilder.ToString();
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
foreach (var shortcut in builtInShortcuts) foreach (var shortcut in builtInShortcuts)
{ {
try try
{ {
var expansion = shortcut.Expand(); if (customExpanded.Contains(shortcut.Key))
queryBuilder.Replace(shortcut.Key, expansion); {
queryBuilderTmp.Replace(shortcut.Key, expansion); var expansion = shortcut.Expand();
queryBuilder.Replace(shortcut.Key, expansion);
queryBuilderTmp.Replace(shortcut.Key, expansion);
}
} }
catch (Exception e) catch (Exception e)
{ {

View file

@ -3,8 +3,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using Flow.Launcher.Plugin.Explorer.Search.IProvider;
using JetBrains.Annotations;
namespace Flow.Launcher.Plugin.Explorer.Exceptions; namespace Flow.Launcher.Plugin.Explorer.Exceptions;
@ -20,7 +18,7 @@ public class EngineNotAvailableException : Exception
string engineName, string engineName,
string resolution, string resolution,
string message, string message,
Func<ActionContext, ValueTask<bool>> action = null) : base(message) Func<ActionContext, ValueTask<bool>>? action = null) : base(message)
{ {
EngineName = engineName; EngineName = engineName;
Resolution = resolution; Resolution = resolution;
@ -40,6 +38,23 @@ public class EngineNotAvailableException : Exception
EngineName = engineName; EngineName = engineName;
Resolution = resolution; Resolution = resolution;
} }
public EngineNotAvailableException(
string engineName,
string resolution,
string message,
string errorIconPath,
Func<ActionContext, ValueTask<bool>>? action = null) : base(message)
{
EngineName = engineName;
Resolution = resolution;
ErrorIcon = errorIconPath;
Action = action ?? (_ =>
{
Clipboard.SetDataObject(this.ToString());
return ValueTask.FromResult(true);
});
}
public override string ToString() public override string ToString()
{ {

View file

@ -45,7 +45,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Data.OleDb" Version="5.0.0" /> <PackageReference Include="System.Data.OleDb" Version="7.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="tlbimp-Microsoft.Search.Interop" Version="1.0.0" /> <PackageReference Include="tlbimp-Microsoft.Search.Interop" Version="1.0.0" />
</ItemGroup> </ItemGroup>

View file

@ -9,6 +9,7 @@
<system:String x:Key="plugin_explorer_delete_folder_link">Are you sure you want to delete {0}?</system:String> <system:String x:Key="plugin_explorer_delete_folder_link">Are you sure you want to delete {0}?</system:String>
<system:String x:Key="plugin_explorer_deletefolderconfirm">Are you sure you want to permanently delete this folder?</system:String> <system:String x:Key="plugin_explorer_deletefolderconfirm">Are you sure you want to permanently delete this folder?</system:String>
<system:String x:Key="plugin_explorer_deletefileconfirm">Are you sure you want to permanently delete this file?</system:String> <system:String x:Key="plugin_explorer_deletefileconfirm">Are you sure you want to permanently delete this file?</system:String>
<system:String x:Key="plugin_explorer_deletefilefolderconfirm">Are you sure you want to permanently delete this file/folder?</system:String>
<system:String x:Key="plugin_explorer_deletefilefoldersuccess">Deletion successful</system:String> <system:String x:Key="plugin_explorer_deletefilefoldersuccess">Deletion successful</system:String>
<system:String x:Key="plugin_explorer_deletefilefoldersuccess_detail">Successfully deleted {0}</system:String> <system:String x:Key="plugin_explorer_deletefilefoldersuccess_detail">Successfully deleted {0}</system:String>
<system:String x:Key="plugin_explorer_globalActionKeywordInvalid">Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword</system:String> <system:String x:Key="plugin_explorer_globalActionKeywordInvalid">Assigning the global action keyword could bring up too many results during search. Please choose a specific action keyword</system:String>

View file

@ -32,7 +32,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal const string DefaultContentSearchActionKeyword = "doc:"; internal const string DefaultContentSearchActionKeyword = "doc:";
internal const char DirectorySeperator = '\\'; internal const char DirectorySeparator = '\\';
internal const string WindowsIndexingOptions = "srchadmin.dll"; internal const string WindowsIndexingOptions = "srchadmin.dll";

View file

@ -15,7 +15,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
var criteria = ConstructSearchCriteria(search); var criteria = ConstructSearchCriteria(search);
if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) >
search.LastIndexOf(Constants.DirectorySeperator)) search.LastIndexOf(Constants.DirectorySeparator))
return DirectorySearch(new EnumerationOptions return DirectorySearch(new EnumerationOptions
{ {
RecurseSubdirectories = true RecurseSubdirectories = true
@ -29,9 +29,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{ {
string incompleteName = ""; string incompleteName = "";
if (!search.EndsWith(Constants.DirectorySeperator)) if (!search.EndsWith(Constants.DirectorySeparator))
{ {
var indexOfSeparator = search.LastIndexOf(Constants.DirectorySeperator); var indexOfSeparator = search.LastIndexOf(Constants.DirectorySeparator);
incompleteName = search[(indexOfSeparator + 1)..].ToLower(); incompleteName = search[(indexOfSeparator + 1)..].ToLower();

View file

@ -1,70 +1,75 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Linq;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.Explorer.Search namespace Flow.Launcher.Plugin.Explorer.Search
{ {
public static class EnvironmentVariables public static class EnvironmentVariables
{ {
internal static bool IsEnvironmentVariableSearch(string search) private static Dictionary<string, string> _envStringPaths = null;
private static Dictionary<string, string> EnvStringPaths
{ {
return search.StartsWith("%") get
&& search != "%%" {
&& !search.Contains("\\") && if (_envStringPaths == null)
LoadEnvironmentStringPaths().Count > 0; {
LoadEnvironmentStringPaths();
}
return _envStringPaths;
}
} }
internal static Dictionary<string, string> LoadEnvironmentStringPaths() internal static bool IsEnvironmentVariableSearch(string search)
{ {
var envStringPaths = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); return search.StartsWith("%")
&& search != "%%"
&& !search.Contains('\\')
&& EnvStringPaths.Count > 0;
}
public static bool HasEnvironmentVar(string search)
{
// "c:\foo %appdata%\" returns false
var splited = search.Split(Path.DirectorySeparatorChar);
return splited.Any(dir => dir.StartsWith('%') &&
dir.EndsWith('%') &&
dir.Length > 2 &&
dir.Split('%').Length == 3);
}
private static void LoadEnvironmentStringPaths()
{
_envStringPaths = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
var homedrive = Environment.GetEnvironmentVariable("HOMEDRIVE")?.EnsureTrailingSlash() ?? "C:\\";
foreach (DictionaryEntry special in Environment.GetEnvironmentVariables()) foreach (DictionaryEntry special in Environment.GetEnvironmentVariables())
{ {
var path = special.Value.ToString(); var path = special.Value.ToString();
// we add a trailing slash to the path to make sure drive paths become valid absolute paths.
// for example, if %systemdrive% is C: we turn it to C:\
path = path.EnsureTrailingSlash();
// if we don't have an absolute path, we use Path.GetFullPath to get one.
// for example, if %homepath% is \Users\John we turn it to C:\Users\John
// Add basepath for GetFullPath() to parse %HOMEPATH% correctly
path = Path.IsPathFullyQualified(path) ? path : Path.GetFullPath(path, homedrive);
if (Directory.Exists(path)) if (Directory.Exists(path))
{ {
// we add a trailing slash to the path to make sure drive paths become valid absolute paths.
// for example, if %systemdrive% is C: we turn it to C:\
path = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
// if we don't have an absolute path, we use Path.GetFullPath to get one.
// for example, if %homepath% is \Users\John we turn it to C:\Users\John
path = Path.IsPathFullyQualified(path) ? path : Path.GetFullPath(path);
// Variables are returned with a mixture of all upper/lower case. // Variables are returned with a mixture of all upper/lower case.
// Call ToLower() to make the results look consistent // Call ToUpper() to make the results look consistent
envStringPaths.Add(special.Key.ToString().ToLower(), path); _envStringPaths.Add(special.Key.ToString().ToUpper(), path);
} }
} }
return envStringPaths;
}
internal static string TranslateEnvironmentVariablePath(string environmentVariablePath)
{
var envStringPaths = LoadEnvironmentStringPaths();
var splitSearch = environmentVariablePath.Substring(1).Split("%");
var exactEnvStringPath = splitSearch[0];
// if there are more than 2 % characters in the query, don't bother
if (splitSearch.Length == 2 && envStringPaths.ContainsKey(exactEnvStringPath))
{
var queryPartToReplace = $"%{exactEnvStringPath}%";
var expandedPath = envStringPaths[exactEnvStringPath];
// replace the %envstring% part of the query with its expanded equivalent
return environmentVariablePath.Replace(queryPartToReplace, expandedPath);
}
return environmentVariablePath;
} }
internal static List<Result> GetEnvironmentStringPathSuggestions(string querySearch, Query query, PluginInitContext context) internal static List<Result> GetEnvironmentStringPathSuggestions(string querySearch, Query query, PluginInitContext context)
{ {
var results = new List<Result>(); var results = new List<Result>();
var environmentVariables = LoadEnvironmentStringPaths();
var search = querySearch; var search = querySearch;
if (querySearch.EndsWith("%") && search.Length > 1) if (querySearch.EndsWith("%") && search.Length > 1)
@ -72,12 +77,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
// query starts and ends with a %, find an exact match from env-string paths // query starts and ends with a %, find an exact match from env-string paths
search = querySearch.Substring(1, search.Length - 2); search = querySearch.Substring(1, search.Length - 2);
if (environmentVariables.ContainsKey(search)) if (EnvStringPaths.ContainsKey(search))
{ {
var expandedPath = environmentVariables[search]; var expandedPath = EnvStringPaths[search];
results.Add(ResultManager.CreateFolderResult($"%{search}%", expandedPath, expandedPath, query)); results.Add(ResultManager.CreateFolderResult($"%{search}%", expandedPath, expandedPath, query));
return results; return results;
} }
} }
@ -90,8 +95,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{ {
search = search.Substring(1); search = search.Substring(1);
} }
foreach (var p in environmentVariables) foreach (var p in EnvStringPaths)
{ {
if (p.Key.StartsWith(search, StringComparison.InvariantCultureIgnoreCase)) if (p.Key.StartsWith(search, StringComparison.InvariantCultureIgnoreCase))
{ {

View file

@ -13,13 +13,12 @@ using Flow.Launcher.Plugin.Explorer.Exceptions;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything namespace Flow.Launcher.Plugin.Explorer.Search.Everything
{ {
public static class EverythingApi public static class EverythingApi
{ {
private const int BufferSize = 4096; private const int BufferSize = 4096;
private static SemaphoreSlim _semaphore = new(1, 1); private static SemaphoreSlim _semaphore = new(1, 1);
// cached buffer to remove redundant allocations. // cached buffer to remove redundant allocations.
private static readonly StringBuilder buffer = new(BufferSize); private static readonly StringBuilder buffer = new(BufferSize);
@ -35,46 +34,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
InvalidCallError InvalidCallError
} }
/// <summary>
/// Gets or sets a value indicating whether [match path].
/// </summary>
/// <value><c>true</c> if [match path]; otherwise, <c>false</c>.</value>
public static bool MatchPath
{
get => EverythingApiDllImport.Everything_GetMatchPath();
set => EverythingApiDllImport.Everything_SetMatchPath(value);
}
/// <summary>
/// Gets or sets a value indicating whether [match case].
/// </summary>
/// <value><c>true</c> if [match case]; otherwise, <c>false</c>.</value>
public static bool MatchCase
{
get => EverythingApiDllImport.Everything_GetMatchCase();
set => EverythingApiDllImport.Everything_SetMatchCase(value);
}
/// <summary>
/// Gets or sets a value indicating whether [match whole word].
/// </summary>
/// <value><c>true</c> if [match whole word]; otherwise, <c>false</c>.</value>
public static bool MatchWholeWord
{
get => EverythingApiDllImport.Everything_GetMatchWholeWord();
set => EverythingApiDllImport.Everything_SetMatchWholeWord(value);
}
/// <summary>
/// Gets or sets a value indicating whether [enable regex].
/// </summary>
/// <value><c>true</c> if [enable regex]; otherwise, <c>false</c>.</value>
public static bool EnableRegex
{
get => EverythingApiDllImport.Everything_GetRegex();
set => EverythingApiDllImport.Everything_SetRegex(value);
}
/// <summary> /// <summary>
/// Checks whether the sort option is Fast Sort. /// Checks whether the sort option is Fast Sort.
/// </summary> /// </summary>
@ -95,7 +54,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
try try
{ {
EverythingApiDllImport.Everything_GetMajorVersion(); EverythingApiDllImport.Everything_GetMajorVersion();
var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError; var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError;
return result; return result;
} }
@ -122,7 +81,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
await _semaphore.WaitAsync(token); await _semaphore.WaitAsync(token);
try try
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
@ -152,6 +111,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
EverythingApiDllImport.Everything_SetMax(option.MaxCount); EverythingApiDllImport.Everything_SetMax(option.MaxCount);
EverythingApiDllImport.Everything_SetSort(option.SortOption); EverythingApiDllImport.Everything_SetSort(option.SortOption);
EverythingApiDllImport.Everything_SetMatchPath(option.IsFullPathSearch);
if (token.IsCancellationRequested) yield break; if (token.IsCancellationRequested) yield break;

View file

@ -27,20 +27,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
Enum.GetName(Settings.IndexSearchEngineOption.Everything)!, Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_click_to_launch_or_install"), Main.Context.API.GetTranslation("flowlauncher_plugin_everything_click_to_launch_or_install"),
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_is_not_running"), Main.Context.API.GetTranslation("flowlauncher_plugin_everything_is_not_running"),
ClickToInstallEverythingAsync) Constants.EverythingErrorImagePath,
{ ClickToInstallEverythingAsync);
ErrorIcon = Constants.EverythingErrorImagePath
};
} }
catch (DllNotFoundException) catch (DllNotFoundException)
{ {
throw new EngineNotAvailableException( throw new EngineNotAvailableException(
Enum.GetName(Settings.IndexSearchEngineOption.Everything)!, Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
"Please check whether your system is x86 or x64", "Please check whether your system is x86 or x64",
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_sdk_issue")) Constants.GeneralSearchErrorImagePath,
{ Main.Context.API.GetTranslation("flowlauncher_plugin_everything_sdk_issue"));
ErrorIcon = Constants.GeneralSearchErrorImagePath
};
} }
} }
private async ValueTask<bool> ClickToInstallEverythingAsync(ActionContext _) private async ValueTask<bool> ClickToInstallEverythingAsync(ActionContext _)
@ -72,16 +68,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
if (!Settings.EnableEverythingContentSearch) if (!Settings.EnableEverythingContentSearch)
{ {
throw new EngineNotAvailableException(Enum.GetName(Settings.IndexSearchEngineOption.Everything)!, throw new EngineNotAvailableException(Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
"Click to Enable Everything Content Search (only applicable to Everything 1.5+ with indexed content)", Main.Context.API.GetTranslation("flowlauncher_plugin_everything_enable_content_search"),
"Everything Content Search is not enabled.", Main.Context.API.GetTranslation("flowlauncher_plugin_everything_enable_content_search_tips"),
Constants.EverythingErrorImagePath,
_ => _ =>
{ {
Settings.EnableEverythingContentSearch = true; Settings.EnableEverythingContentSearch = true;
return ValueTask.FromResult(true); return ValueTask.FromResult(true);
}) });
{
ErrorIcon = Constants.EverythingErrorImagePath
};
} }
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
yield break; yield break;

View file

@ -3,12 +3,15 @@ using Flow.Launcher.Plugin.Everything.Everything;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything namespace Flow.Launcher.Plugin.Explorer.Search.Everything
{ {
public record struct EverythingSearchOption(string Keyword, public record struct EverythingSearchOption(
string Keyword,
SortOption SortOption, SortOption SortOption,
bool IsContentSearch = false, bool IsContentSearch = false,
string ContentSearchKeyword = default, string ContentSearchKeyword = default,
string ParentPath = default, string ParentPath = default,
bool IsRecursive = true, bool IsRecursive = true,
int Offset = 0, int Offset = 0,
int MaxCount = 100); int MaxCount = 100,
bool IsFullPathSearch = true
);
} }

View file

@ -15,7 +15,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
{ {
get get
{ {
var path = Path.EndsWith(Constants.DirectorySeperator) ? Path[0..^1] : Path; var path = Path.EndsWith(Constants.DirectorySeparator) ? Path[0..^1] : Path;
if (path.EndsWith(':')) if (path.EndsWith(':'))
return path[0..^1] + " Drive"; return path[0..^1] + " Drive";

View file

@ -27,8 +27,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
var usePathSearchActionKeyword = Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled; var usePathSearchActionKeyword = Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled;
var pathSearchActionKeyword = Settings.PathSearchActionKeyword == Query.GlobalPluginWildcardSign var pathSearchActionKeyword = Settings.PathSearchActionKeyword == Query.GlobalPluginWildcardSign
? string.Empty ? string.Empty
: $"{Settings.PathSearchActionKeyword} "; : $"{Settings.PathSearchActionKeyword} ";
var searchActionKeyword = Settings.SearchActionKeyword == Query.GlobalPluginWildcardSign var searchActionKeyword = Settings.SearchActionKeyword == Query.GlobalPluginWildcardSign
@ -36,12 +36,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
: $"{Settings.SearchActionKeyword} "; : $"{Settings.SearchActionKeyword} ";
var keyword = usePathSearchActionKeyword ? pathSearchActionKeyword : searchActionKeyword; var keyword = usePathSearchActionKeyword ? pathSearchActionKeyword : searchActionKeyword;
var formatted_path = path; var formatted_path = path;
if (type == ResultType.Folder) if (type == ResultType.Folder)
// the seperator is needed so when navigating the folder structure contents of the folder are listed // the separator is needed so when navigating the folder structure contents of the folder are listed
formatted_path = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator; formatted_path = path.EndsWith(Constants.DirectorySeparator) ? path : path + Constants.DirectorySeparator;
return $"{keyword}{formatted_path}"; return $"{keyword}{formatted_path}";
} }
@ -49,8 +49,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
public static string GetAutoCompleteText(string title, Query query, string path, ResultType resultType) public static string GetAutoCompleteText(string title, Query query, string path, ResultType resultType)
{ {
return !Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled return !Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled
? $"{query.ActionKeyword} {title}" // Only Quick Access action keyword is used in this scenario ? $"{query.ActionKeyword} {title}" // Only Quick Access action keyword is used in this scenario
: GetPathWithActionKeyword(path, resultType, query.ActionKeyword); : GetPathWithActionKeyword(path, resultType, query.ActionKeyword);
} }
public static Result CreateResult(Query query, SearchResult result) public static Result CreateResult(Query query, SearchResult result)
@ -71,7 +71,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{ {
Title = title, Title = title,
IcoPath = path, IcoPath = path,
SubTitle = Path.GetDirectoryName(path), SubTitle = subtitle,
AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder), AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
CopyText = path, CopyText = path,
@ -187,9 +187,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal static Result CreateOpenCurrentFolderResult(string path, string actionKeyword, bool windowsIndexed = false) internal static Result CreateOpenCurrentFolderResult(string path, string actionKeyword, bool windowsIndexed = false)
{ {
// Path passed from PathSearchAsync ends with Constants.DirectorySeperator ('\'), need to remove the seperator // Path passed from PathSearchAsync ends with Constants.DirectorySeparator ('\'), need to remove the separator
// so it's consistent with folder results returned by index search which does not end with one // so it's consistent with folder results returned by index search which does not end with one
var folderPath = path.TrimEnd(Constants.DirectorySeperator); var folderPath = path.TrimEnd(Constants.DirectorySeparator);
return new Result return new Result
{ {
@ -215,9 +215,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false)
{ {
Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) ? new Result.PreviewInfo { Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) ? new Result.PreviewInfo
IsMedia = true, {
PreviewImagePath = filePath, IsMedia = true, PreviewImagePath = filePath,
} : Result.PreviewInfo.Default; } : Result.PreviewInfo.Default;
var title = Path.GetFileName(filePath); var title = Path.GetFileName(filePath);
@ -246,6 +246,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{ {
FileName = filePath, FileName = filePath,
UseShellExecute = true, UseShellExecute = true,
WorkingDirectory = Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty,
Verb = "runas", Verb = "runas",
}); });
} }
@ -286,8 +287,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
public static bool IsMedia(string extension) public static bool IsMedia(string extension)
{ {
if (string.IsNullOrEmpty(extension)) if (string.IsNullOrEmpty(extension))
{ {
return false; return false;
} }
else else
{ {
@ -295,7 +296,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
} }
} }
public static readonly string[] MediaExtensions = { ".jpg", ".png", ".avi", ".mkv", ".bmp", ".gif", ".wmv", ".mp3", ".flac", ".mp4" }; public static readonly string[] MediaExtensions =
{
".jpg", ".png", ".avi", ".mkv", ".bmp", ".gif", ".wmv", ".mp3", ".flac", ".mp4"
};
} }
public enum ResultType public enum ResultType

View file

@ -13,9 +13,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{ {
public class SearchManager public class SearchManager
{ {
internal static PluginInitContext Context; internal PluginInitContext Context;
internal static Settings Settings; internal Settings Settings;
public SearchManager(Settings settings, PluginInitContext context) public SearchManager(Settings settings, PluginInitContext context)
{ {
@ -23,19 +23,23 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Settings = settings; Settings = settings;
} }
private class PathEqualityComparator : IEqualityComparer<Result> /// <summary>
/// Note: A path that ends with "\" and one that doesn't will not be regarded as equal.
/// </summary>
public class PathEqualityComparator : IEqualityComparer<Result>
{ {
private static PathEqualityComparator instance; private static PathEqualityComparator instance;
public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator(); public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator();
public bool Equals(Result x, Result y) public bool Equals(Result x, Result y)
{ {
return x.Title == y.Title && x.SubTitle == y.SubTitle; return x.Title.Equals(y.Title, StringComparison.OrdinalIgnoreCase)
&& string.Equals(x.SubTitle, y.SubTitle, StringComparison.OrdinalIgnoreCase);
} }
public int GetHashCode(Result obj) public int GetHashCode(Result obj)
{ {
return HashCode.Combine(obj.Title.GetHashCode(), obj.SubTitle?.GetHashCode() ?? 0); return HashCode.Combine(obj.Title.ToLowerInvariant(), obj.SubTitle?.ToLowerInvariant() ?? "");
} }
} }
@ -105,19 +109,21 @@ namespace Flow.Launcher.Plugin.Explorer.Search
await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false)) await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false))
results.Add(ResultManager.CreateResult(query, search)); results.Add(ResultManager.CreateResult(query, search));
} }
catch (OperationCanceledException)
{
return new List<Result>();
}
catch (EngineNotAvailableException)
{
throw;
}
catch (Exception e) catch (Exception e)
{ {
if (e is OperationCanceledException)
return results.ToList();
if (e is EngineNotAvailableException)
throw;
throw new SearchException(engineName, e.Message, e); throw new SearchException(engineName, e.Message, e);
} }
results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any( results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any(
excludedPath => r.SubTitle.StartsWith(excludedPath.Path, StringComparison.OrdinalIgnoreCase))); excludedPath => FilesFolders.PathContains(excludedPath.Path, r.SubTitle)));
return results.ToList(); return results.ToList();
} }
@ -142,7 +148,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}; };
} }
private static List<Result> EverythingContentSearchResult(Query query) private List<Result> EverythingContentSearchResult(Query query)
{ {
return new List<Result>() return new List<Result>()
{ {
@ -167,18 +173,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
var results = new HashSet<Result>(PathEqualityComparator.Instance); var results = new HashSet<Result>(PathEqualityComparator.Instance);
var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch); if (EnvironmentVariables.IsEnvironmentVariableSearch(querySearch))
if (isEnvironmentVariable)
return EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, Context); return EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, Context);
// Query is a location path with a full environment variable, eg. %appdata%\somefolder\ // Query is a location path with a full environment variable, eg. %appdata%\somefolder\, c:\users\%USERNAME%\downloads
var isEnvironmentVariablePath = querySearch[1..].Contains("%\\"); var needToExpand = EnvironmentVariables.HasEnvironmentVar(querySearch);
var locationPath = needToExpand ? Environment.ExpandEnvironmentVariables(querySearch) : querySearch;
var locationPath = querySearch;
if (isEnvironmentVariablePath)
locationPath = EnvironmentVariables.TranslateEnvironmentVariablePath(locationPath);
// Check that actual location exists, otherwise directory search will throw directory not found exception // Check that actual location exists, otherwise directory search will throw directory not found exception
if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath).LocationExists()) if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath).LocationExists())
@ -234,7 +234,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return results.ToList(); return results.ToList();
} }
public static bool IsFileContentSearch(string actionKeyword) => actionKeyword == Settings.FileContentSearchActionKeyword; public bool IsFileContentSearch(string actionKeyword) => actionKeyword == Settings.FileContentSearchActionKeyword;
private bool UseWindowsIndexForDirectorySearch(string locationPath) private bool UseWindowsIndexForDirectorySearch(string locationPath)
@ -245,10 +245,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory).StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)) x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory).StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))
&& WindowsIndex.WindowsIndex.PathIsIndexed(pathToDirectory); && WindowsIndex.WindowsIndex.PathIsIndexed(pathToDirectory);
} }
internal static bool IsEnvironmentVariableSearch(string search) internal static bool IsEnvironmentVariableSearch(string search)
{ {
return search.StartsWith("%") return search.StartsWith("%")
&& search != "%%" && search != "%%"
&& !search.Contains('\\'); && !search.Contains('\\');
} }

View file

@ -97,27 +97,25 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
private IAsyncEnumerable<SearchResult> HandledEngineNotAvailableExceptionAsync() private IAsyncEnumerable<SearchResult> HandledEngineNotAvailableExceptionAsync()
{ {
if (!SearchManager.Settings.WarnWindowsSearchServiceOff) if (!Settings.WarnWindowsSearchServiceOff)
return AsyncEnumerable.Empty<SearchResult>(); return AsyncEnumerable.Empty<SearchResult>();
var api = SearchManager.Context.API; var api = Main.Context.API;
throw new EngineNotAvailableException( throw new EngineNotAvailableException(
"Windows Index", "Windows Index",
api.GetTranslation("plugin_explorer_windowsSearchServiceFix"), api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"), api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
Constants.WindowsIndexErrorImagePath,
c => c =>
{ {
SearchManager.Settings.WarnWindowsSearchServiceOff = false; Settings.WarnWindowsSearchServiceOff = false;
// Clears the warning message so user is not mistaken that it has not worked // Clears the warning message so user is not mistaken that it has not worked
api.ChangeQuery(string.Empty); api.ChangeQuery(string.Empty);
return ValueTask.FromResult(false); return ValueTask.FromResult(false);
}) });
{
ErrorIcon = Constants.WindowsIndexErrorImagePath
};
} }
} }
} }

View file

@ -10,7 +10,7 @@
"Name": "Explorer", "Name": "Explorer",
"Description": "Find and manage files and folders via Windows Search or Everything", "Description": "Find and manage files and folders via Windows Search or Everything",
"Author": "Jeremy Wu", "Author": "Jeremy Wu",
"Version": "2.1.0", "Version": "2.2.0",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",

View file

@ -1,10 +1,10 @@
{ {
"ID": "6A122269676E40EB86EB543B945932B9", "ID": "6A122269676E40EB86EB543B945932B9",
"ActionKeyword": "*", "ActionKeyword": "?",
"Name": "Plugin Indicator", "Name": "Plugin Indicator",
"Description": "Provides plugin action keyword suggestions", "Description": "Provides plugin action keyword suggestions",
"Author": "qianlifeng", "Author": "qianlifeng",
"Version": "2.0.1", "Version": "2.0.2",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.PluginIndicator.dll", "ExecuteFileName": "Flow.Launcher.Plugin.PluginIndicator.dll",

View file

@ -38,6 +38,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.4.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -6,7 +6,7 @@
"Name": "Plugins Manager", "Name": "Plugins Manager",
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
"Author": "Jeremy Wu", "Author": "Jeremy Wu",
"Version": "2.0.0", "Version": "2.0.1",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",

View file

@ -378,15 +378,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
MatchResult matchResult; MatchResult matchResult;
// We suppose Name won't be null // We suppose Name won't be null
if (!Main._settings.EnableDescription || Description == null || Name.StartsWith(Description)) if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description))
{ {
title = Name; title = Name;
matchResult = StringMatcher.FuzzySearch(query, title); matchResult = StringMatcher.FuzzySearch(query, Name);
}
else if (Description.StartsWith(Name))
{
title = Description;
matchResult = StringMatcher.FuzzySearch(query, Description);
} }
else else
{ {
@ -401,15 +396,19 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
matchResult = descriptionMatch; matchResult = descriptionMatch;
} }
else matchResult = nameMatch; else
{
matchResult = nameMatch;
}
} }
if (!matchResult.Success) if (!matchResult.IsSearchPrecisionScoreMet())
return null; return null;
var result = new Result var result = new Result
{ {
Title = title, Title = title,
AutoCompleteText = Name,
SubTitle = Main._settings.HideAppsPath ? string.Empty : Location, SubTitle = Main._settings.HideAppsPath ? string.Empty : Location,
IcoPath = LogoPath, IcoPath = LogoPath,
Preview = new Result.PreviewInfo Preview = new Result.PreviewInfo

View file

@ -90,44 +90,28 @@ namespace Flow.Launcher.Plugin.Program.Programs
bool useLocalizedName = !string.IsNullOrEmpty(LocalizedName) && !Name.Equals(LocalizedName); bool useLocalizedName = !string.IsNullOrEmpty(LocalizedName) && !Name.Equals(LocalizedName);
string resultName = useLocalizedName ? LocalizedName : Name; string resultName = useLocalizedName ? LocalizedName : Name;
if (!Main._settings.EnableDescription) if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || resultName.Equals(Description))
{ {
title = resultName; title = resultName;
matchResult = StringMatcher.FuzzySearch(query, resultName); matchResult = StringMatcher.FuzzySearch(query, resultName);
} }
else else
{ {
if (string.IsNullOrEmpty(Description) || resultName.StartsWith(Description)) // Search in both
title = $"{resultName}: {Description}";
var nameMatch = StringMatcher.FuzzySearch(query, resultName);
var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
if (descriptionMatch.Score > nameMatch.Score)
{ {
// Description is invalid or included in resultName for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
// Description is always localized, so Name.StartsWith(Description) is generally useless {
title = resultName; descriptionMatch.MatchData[i] += resultName.Length + 2; // 2 is ": "
matchResult = StringMatcher.FuzzySearch(query, resultName); }
} matchResult = descriptionMatch;
else if (Description.StartsWith(resultName))
{
// resultName included in Description
title = Description;
matchResult = StringMatcher.FuzzySearch(query, Description);
} }
else else
{ {
// Search in both matchResult = nameMatch;
title = $"{resultName}: {Description}";
var nameMatch = StringMatcher.FuzzySearch(query, resultName);
var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
if (descriptionMatch.Score > nameMatch.Score)
{
for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
{
descriptionMatch.MatchData[i] += resultName.Length + 2; // 2 is ": "
}
matchResult = descriptionMatch;
}
else
{
matchResult = nameMatch;
}
} }
} }
@ -171,6 +155,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var result = new Result var result = new Result
{ {
Title = title, Title = title,
AutoCompleteText = resultName,
SubTitle = subtitle, SubTitle = subtitle,
IcoPath = IcoPath, IcoPath = IcoPath,
Score = matchResult.Score, Score = matchResult.Score,
@ -486,8 +471,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
var paths = pathEnv.Split(";", StringSplitOptions.RemoveEmptyEntries).DistinctBy(p => p.ToLowerInvariant()); var paths = pathEnv.Split(";", StringSplitOptions.RemoveEmptyEntries).DistinctBy(p => p.ToLowerInvariant());
var toFilter = paths.Where(x => commonParents.All(parent => !IsSubPathOf(x, parent))) var toFilter = paths.Where(x => commonParents.All(parent => !FilesFolders.PathContains(parent, x)))
.AsParallel() .AsParallel()
.SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false)); .SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false));
@ -779,17 +764,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
} }
// https://stackoverflow.com/a/66877016
private static bool IsSubPathOf(string subPath, string basePath)
{
var rel = Path.GetRelativePath(basePath, subPath);
return rel != "."
&& rel != ".."
&& !rel.StartsWith("../")
&& !rel.StartsWith(@"..\")
&& !Path.IsPathRooted(rel);
}
private static List<string> GetCommonParents(IEnumerable<ProgramSource> programSources) private static List<string> GetCommonParents(IEnumerable<ProgramSource> programSources)
{ {
// To avoid unnecessary io // To avoid unnecessary io
@ -801,8 +775,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
HashSet<ProgramSource> parents = group.ToHashSet(); HashSet<ProgramSource> parents = group.ToHashSet();
foreach (var source in group) foreach (var source in group)
{ {
if (parents.Any(p => IsSubPathOf(source.Location, p.Location) && if (parents.Any(p => FilesFolders.PathContains(p.Location, source.Location)))
source != p))
{ {
parents.Remove(source); parents.Remove(source);
} }

View file

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

View file

@ -4,7 +4,7 @@
"Name": "Shell", "Name": "Shell",
"Description": "Provide executing commands from Flow Launcher", "Description": "Provide executing commands from Flow Launcher",
"Author": "qianlifeng", "Author": "qianlifeng",
"Version": "2.0.0", "Version": "2.0.1",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll", "ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll",

View file

@ -4,7 +4,7 @@
"Name": "System Commands", "Name": "System Commands",
"Description": "Provide System related commands. e.g. shutdown,lock, setting etc.", "Description": "Provide System related commands. e.g. shutdown,lock, setting etc.",
"Author": "qianlifeng", "Author": "qianlifeng",
"Version": "2.0.0", "Version": "2.0.1",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Sys.dll", "ExecuteFileName": "Flow.Launcher.Plugin.Sys.dll",

View file

@ -4,7 +4,7 @@
"Name": "URL", "Name": "URL",
"Description": "Open the typed URL from Flow Launcher", "Description": "Open the typed URL from Flow Launcher",
"Author": "qianlifeng", "Author": "qianlifeng",
"Version": "2.0.0", "Version": "2.0.1",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Url.dll", "ExecuteFileName": "Flow.Launcher.Plugin.Url.dll",

View file

@ -26,7 +26,7 @@
"Name": "Web Searches", "Name": "Web Searches",
"Description": "Provide the web search ability", "Description": "Provide the web search ability",
"Author": "qianlifeng", "Author": "qianlifeng",
"Version": "2.0.1", "Version": "2.0.2",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll", "ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll",

View file

@ -4,7 +4,7 @@
"Description": "Search settings inside Control Panel and Settings App", "Description": "Search settings inside Control Panel and Settings App",
"Name": "Windows Settings", "Name": "Windows Settings",
"Author": "TobiasSekan", "Author": "TobiasSekan",
"Version": "3.0.1", "Version": "3.0.2",
"Language": "csharp", "Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher", "Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.WindowsSettings.dll", "ExecuteFileName": "Flow.Launcher.Plugin.WindowsSettings.dll",

View file

@ -330,6 +330,8 @@ And you can download <a href="https://github.com/Flow-Launcher/Flow.Launcher/dis
<a href="https://github.com/itsonlyfrans"><img src="https://avatars.githubusercontent.com/u/46535667?v=4" width="10%" /></a> <a href="https://github.com/itsonlyfrans"><img src="https://avatars.githubusercontent.com/u/46535667?v=4" width="10%" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://github.com/andreqramos"><img src="https://avatars.githubusercontent.com/u/49326063?v=4" width="10%" /></a> <a href="https://github.com/andreqramos"><img src="https://avatars.githubusercontent.com/u/49326063?v=4" width="10%" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://github.com/patrickdobler"><img src="https://avatars.githubusercontent.com/u/16536946?v=4" width="10%" /></a>
</p> </p>
### Mentions ### Mentions

View file

@ -1,4 +1,4 @@
version: '1.11.0.{build}' version: '1.12.1.{build}'
init: init:
- ps: | - ps: |