mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into new_results_model
This commit is contained in:
commit
f893c8af29
45 changed files with 655 additions and 443 deletions
6
.github/actions/spelling/expect.txt
vendored
6
.github/actions/spelling/expect.txt
vendored
|
|
@ -62,6 +62,8 @@ TobiasSekan
|
|||
Img
|
||||
img
|
||||
resx
|
||||
bak
|
||||
tmp
|
||||
directx
|
||||
mvvm
|
||||
dlg
|
||||
|
|
@ -83,4 +85,6 @@ btn
|
|||
otf
|
||||
searchplugin
|
||||
Noresult
|
||||
wpftk
|
||||
wpftk
|
||||
mkv
|
||||
flac
|
||||
|
|
|
|||
|
|
@ -259,9 +259,16 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
await using var registeredEvent = token.Register(() =>
|
||||
{
|
||||
if (!process.HasExited)
|
||||
process.Kill();
|
||||
sourceBuffer.Dispose();
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
process.Kill();
|
||||
sourceBuffer.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception("|JsonRPCPlugin.ExecuteAsync|Exception when kill process", e);
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
|
|
@ -288,7 +295,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
sourceBuffer.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
|
||||
return sourceBuffer;
|
||||
}
|
||||
|
||||
|
|
@ -376,7 +383,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Vertical, VerticalAlignment = VerticalAlignment.Center,
|
||||
Orientation = Orientation.Vertical,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingLabelPanelMargin
|
||||
};
|
||||
RowDefinition gridRow = new RowDefinition();
|
||||
|
|
@ -390,8 +398,10 @@ namespace Flow.Launcher.Core.Plugin
|
|||
};
|
||||
var desc = new TextBlock()
|
||||
{
|
||||
Text = attribute.Description, FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,Margin = settingDescMargin,
|
||||
Text = attribute.Description,
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = settingDescMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
};
|
||||
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
|
||||
|
|
@ -405,7 +415,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
panel.Children.Add(name);
|
||||
panel.Children.Add(desc);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Grid.SetColumn(panel, 0);
|
||||
Grid.SetRow(panel, rowCount);
|
||||
|
|
@ -420,20 +430,20 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
|
||||
Margin = settingTextBlockMargin,
|
||||
Padding = new Thickness(0,0,0,0),
|
||||
Padding = new Thickness(0, 0, 0, 0),
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
|
||||
TextAlignment = TextAlignment.Left,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
Grid.SetColumn(contentControl, 0);
|
||||
Grid.SetColumnSpan(contentControl, 2);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "input":
|
||||
{
|
||||
|
|
@ -449,50 +459,49 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
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,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
var Btn = new System.Windows.Controls.Button()
|
||||
{
|
||||
Margin = new Thickness(10,0,0,0),
|
||||
Content = "Browse"
|
||||
};
|
||||
var dockPanel = new DockPanel()
|
||||
{
|
||||
Margin = settingControlMargin
|
||||
};
|
||||
DockPanel.SetDock(Btn, Dock.Right);
|
||||
dockPanel.Children.Add(Btn);
|
||||
dockPanel.Children.Add(textBox);
|
||||
contentControl = dockPanel;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
Margin = new Thickness(10, 0, 0, 0),
|
||||
Text = Settings[attribute.Name] as string ?? string.Empty,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
|
||||
ToolTip = attribute.Description
|
||||
};
|
||||
textBox.TextChanged += (_, _) =>
|
||||
{
|
||||
Settings[attribute.Name] = textBox.Text;
|
||||
};
|
||||
var Btn = new System.Windows.Controls.Button()
|
||||
{
|
||||
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
|
||||
};
|
||||
var dockPanel = new DockPanel()
|
||||
{
|
||||
Margin = settingControlMargin
|
||||
};
|
||||
DockPanel.SetDock(Btn, Dock.Right);
|
||||
dockPanel.Children.Add(Btn);
|
||||
dockPanel.Children.Add(textBox);
|
||||
contentControl = dockPanel;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "textarea":
|
||||
{
|
||||
var textBox = new TextBox()
|
||||
|
|
@ -511,14 +520,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Settings[attribute.Name] = ((TextBox)sender).Text;
|
||||
};
|
||||
contentControl = textBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "passwordBox":
|
||||
{
|
||||
|
|
@ -535,14 +544,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Settings[attribute.Name] = ((PasswordBox)sender).Password;
|
||||
};
|
||||
contentControl = passwordBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "dropdown":
|
||||
{
|
||||
|
|
@ -559,14 +568,14 @@ namespace Flow.Launcher.Core.Plugin
|
|||
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
|
||||
};
|
||||
contentControl = comboBox;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
Grid.SetColumn(contentControl, 1);
|
||||
Grid.SetRow(contentControl, rowCount);
|
||||
if (rowCount != 0)
|
||||
mainPanel.Children.Add(sep);
|
||||
Grid.SetRow(sep, rowCount);
|
||||
Grid.SetColumn(sep, 0);
|
||||
Grid.SetColumnSpan(sep, 2);
|
||||
break;
|
||||
}
|
||||
case "checkbox":
|
||||
var checkBox = new CheckBox
|
||||
|
|
@ -592,13 +601,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
case "hyperlink":
|
||||
var hyperlink = new Hyperlink
|
||||
{
|
||||
ToolTip = attribute.Description,
|
||||
NavigateUri = attribute.url
|
||||
ToolTip = attribute.Description, NavigateUri = attribute.url
|
||||
};
|
||||
var linkbtn = new System.Windows.Controls.Button
|
||||
{
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
|
||||
Margin = settingControlMargin
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin
|
||||
};
|
||||
linkbtn.Content = attribute.urlLabel;
|
||||
|
||||
|
|
@ -619,7 +626,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
mainPanel.Children.Add(panel);
|
||||
mainPanel.Children.Add(contentControl);
|
||||
rowCount++;
|
||||
|
||||
|
||||
}
|
||||
return settingWindow;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ namespace Flow.Launcher.Core
|
|||
|
||||
public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
|
||||
{
|
||||
await UpdateLock.WaitAsync();
|
||||
await UpdateLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!silentUpdate)
|
||||
|
|
@ -88,9 +88,13 @@ namespace Flow.Launcher.Core
|
|||
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)
|
||||
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
|
||||
api.GetTranslation("update_flowlauncher_check_connection"));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
|
@ -16,7 +18,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
/// <summary>
|
||||
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
|
||||
/// </summary>
|
||||
public static T NonNull<T>(this T obj)
|
||||
public static T NonNull<T>(this T? obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Navigation;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Hotkey
|
||||
{
|
||||
|
|
@ -11,10 +12,10 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
public bool Shift { get; set; }
|
||||
public bool Win { 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.Oem3, "~"}
|
||||
|
|
@ -27,19 +28,19 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
ModifierKeys modifierKeys = ModifierKeys.None;
|
||||
if (Alt)
|
||||
{
|
||||
modifierKeys = ModifierKeys.Alt;
|
||||
modifierKeys |= ModifierKeys.Alt;
|
||||
}
|
||||
if (Shift)
|
||||
{
|
||||
modifierKeys = modifierKeys | ModifierKeys.Shift;
|
||||
modifierKeys |= ModifierKeys.Shift;
|
||||
}
|
||||
if (Win)
|
||||
{
|
||||
modifierKeys = modifierKeys | ModifierKeys.Windows;
|
||||
modifierKeys |= ModifierKeys.Windows;
|
||||
}
|
||||
if (Ctrl)
|
||||
{
|
||||
modifierKeys = modifierKeys | ModifierKeys.Control;
|
||||
modifierKeys |= ModifierKeys.Control;
|
||||
}
|
||||
return modifierKeys;
|
||||
}
|
||||
|
|
@ -86,7 +87,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
Ctrl = true;
|
||||
keys.Remove("Ctrl");
|
||||
}
|
||||
if (keys.Count > 0)
|
||||
if (keys.Count == 1)
|
||||
{
|
||||
string charKey = keys[0];
|
||||
KeyValuePair<Key, string>? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey);
|
||||
|
|
@ -110,36 +111,75 @@ namespace Flow.Launcher.Infrastructure.Hotkey
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
string text = string.Empty;
|
||||
List<string> keys = new List<string>();
|
||||
if (Ctrl)
|
||||
{
|
||||
text += "Ctrl + ";
|
||||
keys.Add("Ctrl");
|
||||
}
|
||||
if (Alt)
|
||||
{
|
||||
text += "Alt + ";
|
||||
keys.Add("Alt");
|
||||
}
|
||||
if (Shift)
|
||||
{
|
||||
text += "Shift + ";
|
||||
keys.Add("Shift");
|
||||
}
|
||||
if (Win)
|
||||
{
|
||||
text += "Win + ";
|
||||
keys.Add("Win");
|
||||
}
|
||||
|
||||
if (CharKey != Key.None)
|
||||
{
|
||||
text += specialSymbolDictionary.ContainsKey(CharKey)
|
||||
keys.Add(specialSymbolDictionary.ContainsKey(CharKey)
|
||||
? specialSymbolDictionary[CharKey]
|
||||
: CharKey.ToString();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
text = text.Remove(text.Length - 3);
|
||||
: CharKey.ToString());
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
|
@ -11,62 +12,82 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
/// </summary>
|
||||
public class JsonStorage<T> where T : new()
|
||||
{
|
||||
protected T _data;
|
||||
protected T? Data;
|
||||
|
||||
// need a new directory name
|
||||
public const string DirectoryName = "Settings";
|
||||
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()
|
||||
{
|
||||
string? serialized = null;
|
||||
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
var serialized = File.ReadAllText(FilePath);
|
||||
if (!string.IsNullOrWhiteSpace(serialized))
|
||||
serialized = File.ReadAllText(FilePath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(serialized))
|
||||
{
|
||||
try
|
||||
{
|
||||
Deserialize(serialized);
|
||||
Data = JsonSerializer.Deserialize<T>(serialized) ?? TryLoadBackup() ?? LoadDefault();
|
||||
}
|
||||
else
|
||||
catch (JsonException)
|
||||
{
|
||||
LoadDefault();
|
||||
Data = TryLoadBackup() ?? LoadDefault();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadDefault();
|
||||
Data = TryLoadBackup() ?? LoadDefault();
|
||||
}
|
||||
return _data.NonNull();
|
||||
|
||||
return Data.NonNull();
|
||||
}
|
||||
|
||||
private void Deserialize(string serialized)
|
||||
{
|
||||
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()
|
||||
private T LoadDefault()
|
||||
{
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
BackupOriginFile();
|
||||
}
|
||||
|
||||
_data = new T();
|
||||
Save();
|
||||
return new T();
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
@ -82,13 +103,22 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
|
||||
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() { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
|
||||
public PluginJsonStorage(T data) : this()
|
||||
{
|
||||
_data = data;
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>3.0.1</Version>
|
||||
<PackageVersion>3.0.1</PackageVersion>
|
||||
<AssemblyVersion>3.0.1</AssemblyVersion>
|
||||
<FileVersion>3.0.1</FileVersion>
|
||||
<Version>3.1.0</Version>
|
||||
<PackageVersion>3.1.0</PackageVersion>
|
||||
<AssemblyVersion>3.1.0</AssemblyVersion>
|
||||
<FileVersion>3.1.0</FileVersion>
|
||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||
<Authors>Flow-Launcher</Authors>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
#pragma warning disable IDE0005
|
||||
using System.Windows;
|
||||
#pragma warning restore IDE0005
|
||||
|
||||
namespace Flow.Launcher.Plugin.SharedCommands
|
||||
{
|
||||
|
|
@ -206,22 +208,16 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
///</summary>
|
||||
public static string GetPreviousExistingDirectory(Func<string, bool> locationExists, string path)
|
||||
{
|
||||
var previousDirectoryPath = "";
|
||||
var index = path.LastIndexOf('\\');
|
||||
if (index > 0 && index < (path.Length - 1))
|
||||
{
|
||||
previousDirectoryPath = path.Substring(0, index + 1);
|
||||
if (!locationExists(previousDirectoryPath))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
string previousDirectoryPath = path.Substring(0, index + 1);
|
||||
return locationExists(previousDirectoryPath) ? previousDirectoryPath : "";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return previousDirectoryPath;
|
||||
}
|
||||
|
||||
///<summary>
|
||||
|
|
@ -241,5 +237,33 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
Flow.Launcher.Test/FilesFoldersTest.cs
Normal file
53
Flow.Launcher.Test/FilesFoldersTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,11 @@ using Flow.Launcher.Plugin.SharedCommands;
|
|||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static Flow.Launcher.Plugin.Explorer.Search.SearchManager;
|
||||
|
||||
namespace Flow.Launcher.Test.Plugins
|
||||
{
|
||||
|
|
@ -176,7 +178,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
|
||||
|
||||
// When
|
||||
var result = SearchManager.IsFileContentSearch(query.ActionKeyword);
|
||||
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
|
||||
|
||||
// Then
|
||||
Assert.IsTrue(result,
|
||||
|
|
@ -193,6 +195,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
[TestCase(@"c:\>*", true)]
|
||||
[TestCase(@"c:\>", true)]
|
||||
[TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)]
|
||||
[TestCase(@"c:\SomeLocation\SomeOtherLocation", true)]
|
||||
public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
|
||||
{
|
||||
// When, Given
|
||||
|
|
@ -393,5 +396,68 @@ namespace Flow.Launcher.Test.Plugins
|
|||
// Then
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows;
|
||||
|
|
@ -84,14 +85,14 @@ namespace Flow.Launcher
|
|||
|
||||
Current.MainWindow = window;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
|
||||
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
|
||||
InternationalizationManager.Instance.Settings = _settings;
|
||||
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.ChangeTheme(_settings.Theme);
|
||||
|
||||
|
|
@ -130,20 +131,17 @@ namespace Flow.Launcher
|
|||
//[Conditional("RELEASE")]
|
||||
private void AutoUpdates()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (_settings.AutoUpdates)
|
||||
{
|
||||
// check udpate every 5 hours
|
||||
var timer = new Timer(1000 * 60 * 60 * 5);
|
||||
timer.Elapsed += async (s, e) =>
|
||||
{
|
||||
await _updater.UpdateAppAsync(API);
|
||||
};
|
||||
timer.Start();
|
||||
|
||||
// check updates on startup
|
||||
// check update every 5 hours
|
||||
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
|
||||
await _updater.UpdateAppAsync(API);
|
||||
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
// check updates on startup
|
||||
await _updater.UpdateAppAsync(API);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
Margin="0,0,18,0"
|
||||
VerticalContentAlignment="Center"
|
||||
input:InputMethod.IsInputMethodEnabled="False"
|
||||
GotFocus="tbHotkey_GotFocus"
|
||||
LostFocus="tbHotkey_LostFocus"
|
||||
PreviewKeyDown="TbHotkey_OnPreviewKeyDown"
|
||||
TabIndex="100" />
|
||||
|
|
|
|||
|
|
@ -9,16 +9,11 @@ using Flow.Launcher.Helper;
|
|||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Threading;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class HotkeyControl : UserControl
|
||||
{
|
||||
private Brush tbMsgForegroundColorOriginal;
|
||||
|
||||
private string tbMsgTextOriginal;
|
||||
|
||||
public HotkeyModel CurrentHotkey { get; private set; }
|
||||
public bool CurrentHotkeyAvailable { get; private set; }
|
||||
|
||||
|
|
@ -29,8 +24,6 @@ namespace Flow.Launcher
|
|||
public HotkeyControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
tbMsgTextOriginal = tbMsg.Text;
|
||||
tbMsgForegroundColorOriginal = tbMsg.Foreground;
|
||||
}
|
||||
|
||||
private CancellationTokenSource hotkeyUpdateSource;
|
||||
|
|
@ -55,9 +48,7 @@ namespace Flow.Launcher
|
|||
specialKeyState.CtrlPressed,
|
||||
key);
|
||||
|
||||
var hotkeyString = hotkeyModel.ToString();
|
||||
|
||||
if (hotkeyString == tbHotkey.Text)
|
||||
if (hotkeyModel.Equals(CurrentHotkey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -72,33 +63,32 @@ namespace Flow.Launcher
|
|||
|
||||
public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true)
|
||||
{
|
||||
CurrentHotkey = keyModel;
|
||||
|
||||
tbHotkey.Text = CurrentHotkey.ToString();
|
||||
tbHotkey.Text = keyModel.ToString();
|
||||
tbHotkey.Select(tbHotkey.Text.Length, 0);
|
||||
|
||||
if (triggerValidate)
|
||||
{
|
||||
CurrentHotkeyAvailable = CheckHotkeyAvailability();
|
||||
if (!CurrentHotkeyAvailable)
|
||||
{
|
||||
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;
|
||||
bool hotkeyAvailable = CheckHotkeyAvailability(keyModel);
|
||||
CurrentHotkeyAvailable = hotkeyAvailable;
|
||||
SetMessage(hotkeyAvailable);
|
||||
OnHotkeyChanged();
|
||||
|
||||
var token = hotkeyUpdateSource.Token;
|
||||
await Task.Delay(500, token);
|
||||
if (token.IsCancellationRequested)
|
||||
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);
|
||||
}
|
||||
|
||||
private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey);
|
||||
private static bool CheckHotkeyAvailability(HotkeyModel hotkey) => hotkey.Validate() && HotKeyMapper.CheckAvailability(hotkey);
|
||||
|
||||
public new bool IsFocused => tbHotkey.IsFocused;
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@
|
|||
<ui:NumberBox
|
||||
x:Name="tbAction"
|
||||
Width="200"
|
||||
Maximum="100"
|
||||
Minimum="-100"
|
||||
Margin="10,0,15,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ namespace Flow.Launcher.Resources.Pages
|
|||
tbMsgTextOriginal = HotkeyControl.tbMsg.Text;
|
||||
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)
|
||||
{
|
||||
|
|
@ -49,4 +49,4 @@ namespace Flow.Launcher.Resources.Pages
|
|||
HotkeyControl.tbMsg.Foreground = tbMsgForegroundColorOriginal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -396,6 +396,12 @@
|
|||
<Setter Property="Foreground" Value="#8f8f8f" />
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -841,15 +841,20 @@ namespace Flow.Launcher.ViewModel
|
|||
queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand());
|
||||
}
|
||||
|
||||
string customExpanded = queryBuilder.ToString();
|
||||
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
foreach (var shortcut in builtInShortcuts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var expansion = shortcut.Expand();
|
||||
queryBuilder.Replace(shortcut.Key, expansion);
|
||||
queryBuilderTmp.Replace(shortcut.Key, expansion);
|
||||
if (customExpanded.Contains(shortcut.Key))
|
||||
{
|
||||
var expansion = shortcut.Expand();
|
||||
queryBuilder.Replace(shortcut.Key, expansion);
|
||||
queryBuilderTmp.Replace(shortcut.Key, expansion);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Flow.Launcher.Plugin.Explorer.Search.IProvider;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Explorer.Exceptions;
|
||||
|
||||
|
|
@ -20,7 +18,7 @@ public class EngineNotAvailableException : Exception
|
|||
string engineName,
|
||||
string resolution,
|
||||
string message,
|
||||
Func<ActionContext, ValueTask<bool>> action = null) : base(message)
|
||||
Func<ActionContext, ValueTask<bool>>? action = null) : base(message)
|
||||
{
|
||||
EngineName = engineName;
|
||||
Resolution = resolution;
|
||||
|
|
@ -40,6 +38,23 @@ public class EngineNotAvailableException : Exception
|
|||
EngineName = engineName;
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
</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="tlbimp-Microsoft.Search.Interop" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
|
||||
internal const string DefaultContentSearchActionKeyword = "doc:";
|
||||
|
||||
internal const char DirectorySeperator = '\\';
|
||||
internal const char DirectorySeparator = '\\';
|
||||
|
||||
internal const string WindowsIndexingOptions = "srchadmin.dll";
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
|
|||
var criteria = ConstructSearchCriteria(search);
|
||||
|
||||
if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) >
|
||||
search.LastIndexOf(Constants.DirectorySeperator))
|
||||
search.LastIndexOf(Constants.DirectorySeparator))
|
||||
return DirectorySearch(new EnumerationOptions
|
||||
{
|
||||
RecurseSubdirectories = true
|
||||
|
|
@ -29,9 +29,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
|
|||
{
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,70 +1,75 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Explorer.Search
|
||||
{
|
||||
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("%")
|
||||
&& search != "%%"
|
||||
&& !search.Contains("\\") &&
|
||||
LoadEnvironmentStringPaths().Count > 0;
|
||||
get
|
||||
{
|
||||
if (_envStringPaths == null)
|
||||
{
|
||||
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())
|
||||
{
|
||||
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))
|
||||
{
|
||||
// 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.
|
||||
// Call ToLower() to make the results look consistent
|
||||
envStringPaths.Add(special.Key.ToString().ToLower(), path);
|
||||
// Call ToUpper() to make the results look consistent
|
||||
_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)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
|
||||
var environmentVariables = LoadEnvironmentStringPaths();
|
||||
var search = querySearch;
|
||||
|
||||
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
|
||||
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));
|
||||
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
|
@ -90,8 +95,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
{
|
||||
search = search.Substring(1);
|
||||
}
|
||||
|
||||
foreach (var p in environmentVariables)
|
||||
|
||||
foreach (var p in EnvStringPaths)
|
||||
{
|
||||
if (p.Key.StartsWith(search, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,13 +13,12 @@ using Flow.Launcher.Plugin.Explorer.Exceptions;
|
|||
|
||||
namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
||||
{
|
||||
|
||||
public static class EverythingApi
|
||||
{
|
||||
|
||||
private const int BufferSize = 4096;
|
||||
|
||||
private static SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
// cached buffer to remove redundant allocations.
|
||||
private static readonly StringBuilder buffer = new(BufferSize);
|
||||
|
||||
|
|
@ -35,46 +34,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
|||
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>
|
||||
/// Checks whether the sort option is Fast Sort.
|
||||
/// </summary>
|
||||
|
|
@ -95,7 +54,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
|||
|
||||
try
|
||||
{
|
||||
EverythingApiDllImport.Everything_GetMajorVersion();
|
||||
EverythingApiDllImport.Everything_GetMajorVersion();
|
||||
var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError;
|
||||
return result;
|
||||
}
|
||||
|
|
@ -122,7 +81,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
|||
|
||||
await _semaphore.WaitAsync(token);
|
||||
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
|
|
@ -152,6 +111,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
|||
EverythingApiDllImport.Everything_SetMax(option.MaxCount);
|
||||
|
||||
EverythingApiDllImport.Everything_SetSort(option.SortOption);
|
||||
EverythingApiDllImport.Everything_SetMatchPath(option.IsFullPathSearch);
|
||||
|
||||
if (token.IsCancellationRequested) yield break;
|
||||
|
||||
|
|
|
|||
|
|
@ -27,20 +27,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search.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_is_not_running"),
|
||||
ClickToInstallEverythingAsync)
|
||||
{
|
||||
ErrorIcon = Constants.EverythingErrorImagePath
|
||||
};
|
||||
Constants.EverythingErrorImagePath,
|
||||
ClickToInstallEverythingAsync);
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
throw new EngineNotAvailableException(
|
||||
Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
|
||||
"Please check whether your system is x86 or x64",
|
||||
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_sdk_issue"))
|
||||
{
|
||||
ErrorIcon = Constants.GeneralSearchErrorImagePath
|
||||
};
|
||||
Constants.GeneralSearchErrorImagePath,
|
||||
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_sdk_issue"));
|
||||
}
|
||||
}
|
||||
private async ValueTask<bool> ClickToInstallEverythingAsync(ActionContext _)
|
||||
|
|
@ -72,16 +68,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
|||
if (!Settings.EnableEverythingContentSearch)
|
||||
{
|
||||
throw new EngineNotAvailableException(Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
|
||||
"Click to Enable Everything Content Search (only applicable to Everything 1.5+ with indexed content)",
|
||||
"Everything Content Search is not enabled.",
|
||||
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_enable_content_search"),
|
||||
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_enable_content_search_tips"),
|
||||
Constants.EverythingErrorImagePath,
|
||||
_ =>
|
||||
{
|
||||
Settings.EnableEverythingContentSearch = true;
|
||||
return ValueTask.FromResult(true);
|
||||
})
|
||||
{
|
||||
ErrorIcon = Constants.EverythingErrorImagePath
|
||||
};
|
||||
});
|
||||
}
|
||||
if (token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ using Flow.Launcher.Plugin.Everything.Everything;
|
|||
|
||||
namespace Flow.Launcher.Plugin.Explorer.Search.Everything
|
||||
{
|
||||
public record struct EverythingSearchOption(string Keyword,
|
||||
public record struct EverythingSearchOption(
|
||||
string Keyword,
|
||||
SortOption SortOption,
|
||||
bool IsContentSearch = false,
|
||||
bool IsContentSearch = false,
|
||||
string ContentSearchKeyword = default,
|
||||
string ParentPath = default,
|
||||
bool IsRecursive = true,
|
||||
int Offset = 0,
|
||||
int MaxCount = 100);
|
||||
int Offset = 0,
|
||||
int MaxCount = 100,
|
||||
bool IsFullPathSearch = true
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
|
|||
{
|
||||
get
|
||||
{
|
||||
var path = Path.EndsWith(Constants.DirectorySeperator) ? Path[0..^1] : Path;
|
||||
var path = Path.EndsWith(Constants.DirectorySeparator) ? Path[0..^1] : Path;
|
||||
|
||||
if (path.EndsWith(':'))
|
||||
return path[0..^1] + " Drive";
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
|
||||
var usePathSearchActionKeyword = Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled;
|
||||
|
||||
var pathSearchActionKeyword = Settings.PathSearchActionKeyword == Query.GlobalPluginWildcardSign
|
||||
? string.Empty
|
||||
var pathSearchActionKeyword = Settings.PathSearchActionKeyword == Query.GlobalPluginWildcardSign
|
||||
? string.Empty
|
||||
: $"{Settings.PathSearchActionKeyword} ";
|
||||
|
||||
var searchActionKeyword = Settings.SearchActionKeyword == Query.GlobalPluginWildcardSign
|
||||
|
|
@ -36,12 +36,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
: $"{Settings.SearchActionKeyword} ";
|
||||
|
||||
var keyword = usePathSearchActionKeyword ? pathSearchActionKeyword : searchActionKeyword;
|
||||
|
||||
|
||||
var formatted_path = path;
|
||||
|
||||
if (type == ResultType.Folder)
|
||||
// the seperator is needed so when navigating the folder structure contents of the folder are listed
|
||||
formatted_path = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator;
|
||||
// the separator is needed so when navigating the folder structure contents of the folder are listed
|
||||
formatted_path = path.EndsWith(Constants.DirectorySeparator) ? path : path + Constants.DirectorySeparator;
|
||||
|
||||
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)
|
||||
{
|
||||
return !Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled
|
||||
? $"{query.ActionKeyword} {title}" // Only Quick Access action keyword is used in this scenario
|
||||
: GetPathWithActionKeyword(path, resultType, query.ActionKeyword);
|
||||
? $"{query.ActionKeyword} {title}" // Only Quick Access action keyword is used in this scenario
|
||||
: GetPathWithActionKeyword(path, resultType, query.ActionKeyword);
|
||||
}
|
||||
|
||||
public static Result CreateResult(Query query, SearchResult result)
|
||||
|
|
@ -71,7 +71,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
{
|
||||
Title = title,
|
||||
IcoPath = path,
|
||||
SubTitle = Path.GetDirectoryName(path),
|
||||
SubTitle = subtitle,
|
||||
AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder),
|
||||
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
|
||||
CopyText = path,
|
||||
|
|
@ -187,9 +187,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
|
||||
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
|
||||
var folderPath = path.TrimEnd(Constants.DirectorySeperator);
|
||||
var folderPath = path.TrimEnd(Constants.DirectorySeparator);
|
||||
|
||||
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)
|
||||
{
|
||||
Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) ? new Result.PreviewInfo {
|
||||
IsMedia = true,
|
||||
PreviewImagePath = filePath,
|
||||
Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) ? new Result.PreviewInfo
|
||||
{
|
||||
IsMedia = true, PreviewImagePath = filePath,
|
||||
} : Result.PreviewInfo.Default;
|
||||
|
||||
var title = Path.GetFileName(filePath);
|
||||
|
|
@ -246,6 +246,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
{
|
||||
FileName = filePath,
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty,
|
||||
Verb = "runas",
|
||||
});
|
||||
}
|
||||
|
|
@ -286,8 +287,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
public static bool IsMedia(string extension)
|
||||
{
|
||||
if (string.IsNullOrEmpty(extension))
|
||||
{
|
||||
return false;
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
{
|
||||
public class SearchManager
|
||||
{
|
||||
internal static PluginInitContext Context;
|
||||
internal PluginInitContext Context;
|
||||
|
||||
internal static Settings Settings;
|
||||
internal Settings Settings;
|
||||
|
||||
public SearchManager(Settings settings, PluginInitContext context)
|
||||
{
|
||||
|
|
@ -23,19 +23,23 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
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;
|
||||
public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator();
|
||||
|
||||
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)
|
||||
{
|
||||
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))
|
||||
results.Add(ResultManager.CreateResult(query, search));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new List<Result>();
|
||||
}
|
||||
catch (EngineNotAvailableException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is OperationCanceledException)
|
||||
return results.ToList();
|
||||
|
||||
if (e is EngineNotAvailableException)
|
||||
throw;
|
||||
|
||||
throw new SearchException(engineName, e.Message, e);
|
||||
}
|
||||
|
||||
|
||||
results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any(
|
||||
excludedPath => r.SubTitle.StartsWith(excludedPath.Path, StringComparison.OrdinalIgnoreCase)));
|
||||
excludedPath => FilesFolders.PathContains(excludedPath.Path, r.SubTitle)));
|
||||
|
||||
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>()
|
||||
{
|
||||
|
|
@ -167,18 +173,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
|
||||
var results = new HashSet<Result>(PathEqualityComparator.Instance);
|
||||
|
||||
var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch);
|
||||
|
||||
if (isEnvironmentVariable)
|
||||
if (EnvironmentVariables.IsEnvironmentVariableSearch(querySearch))
|
||||
return EnvironmentVariables.GetEnvironmentStringPathSuggestions(querySearch, query, Context);
|
||||
|
||||
// Query is a location path with a full environment variable, eg. %appdata%\somefolder\
|
||||
var isEnvironmentVariablePath = querySearch[1..].Contains("%\\");
|
||||
|
||||
var locationPath = querySearch;
|
||||
|
||||
if (isEnvironmentVariablePath)
|
||||
locationPath = EnvironmentVariables.TranslateEnvironmentVariablePath(locationPath);
|
||||
// Query is a location path with a full environment variable, eg. %appdata%\somefolder\, c:\users\%USERNAME%\downloads
|
||||
var needToExpand = EnvironmentVariables.HasEnvironmentVar(querySearch);
|
||||
var locationPath = needToExpand ? Environment.ExpandEnvironmentVariables(querySearch) : querySearch;
|
||||
|
||||
// Check that actual location exists, otherwise directory search will throw directory not found exception
|
||||
if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath).LocationExists())
|
||||
|
|
@ -234,7 +234,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
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)
|
||||
|
|
@ -245,10 +245,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
|
|||
x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory).StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))
|
||||
&& WindowsIndex.WindowsIndex.PathIsIndexed(pathToDirectory);
|
||||
}
|
||||
|
||||
|
||||
internal static bool IsEnvironmentVariableSearch(string search)
|
||||
{
|
||||
return search.StartsWith("%")
|
||||
return search.StartsWith("%")
|
||||
&& search != "%%"
|
||||
&& !search.Contains('\\');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,27 +97,25 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
|
||||
private IAsyncEnumerable<SearchResult> HandledEngineNotAvailableExceptionAsync()
|
||||
{
|
||||
if (!SearchManager.Settings.WarnWindowsSearchServiceOff)
|
||||
if (!Settings.WarnWindowsSearchServiceOff)
|
||||
return AsyncEnumerable.Empty<SearchResult>();
|
||||
|
||||
var api = SearchManager.Context.API;
|
||||
var api = Main.Context.API;
|
||||
|
||||
throw new EngineNotAvailableException(
|
||||
"Windows Index",
|
||||
api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
|
||||
api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
|
||||
Constants.WindowsIndexErrorImagePath,
|
||||
c =>
|
||||
{
|
||||
SearchManager.Settings.WarnWindowsSearchServiceOff = false;
|
||||
Settings.WarnWindowsSearchServiceOff = false;
|
||||
|
||||
// Clears the warning message so user is not mistaken that it has not worked
|
||||
api.ChangeQuery(string.Empty);
|
||||
|
||||
return ValueTask.FromResult(false);
|
||||
})
|
||||
{
|
||||
ErrorIcon = Constants.WindowsIndexErrorImagePath
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"Name": "Explorer",
|
||||
"Description": "Find and manage files and folders via Windows Search or Everything",
|
||||
"Author": "Jeremy Wu",
|
||||
"Version": "2.1.0",
|
||||
"Version": "2.2.0",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"ID": "6A122269676E40EB86EB543B945932B9",
|
||||
"ActionKeyword": "*",
|
||||
"ActionKeyword": "?",
|
||||
"Name": "Plugin Indicator",
|
||||
"Description": "Provides plugin action keyword suggestions",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "2.0.1",
|
||||
"Version": "2.0.2",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.PluginIndicator.dll",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,6 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
"Name": "Plugins Manager",
|
||||
"Description": "Management of installing, uninstalling or updating Flow Launcher plugins",
|
||||
"Author": "Jeremy Wu",
|
||||
"Version": "2.0.0",
|
||||
"Version": "2.0.1",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll",
|
||||
|
|
|
|||
|
|
@ -378,15 +378,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
MatchResult matchResult;
|
||||
|
||||
// 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;
|
||||
matchResult = StringMatcher.FuzzySearch(query, title);
|
||||
}
|
||||
else if (Description.StartsWith(Name))
|
||||
{
|
||||
title = Description;
|
||||
matchResult = StringMatcher.FuzzySearch(query, Description);
|
||||
matchResult = StringMatcher.FuzzySearch(query, Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -401,15 +396,19 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
}
|
||||
matchResult = descriptionMatch;
|
||||
}
|
||||
else matchResult = nameMatch;
|
||||
else
|
||||
{
|
||||
matchResult = nameMatch;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchResult.Success)
|
||||
if (!matchResult.IsSearchPrecisionScoreMet())
|
||||
return null;
|
||||
|
||||
var result = new Result
|
||||
{
|
||||
Title = title,
|
||||
AutoCompleteText = Name,
|
||||
SubTitle = Main._settings.HideAppsPath ? string.Empty : Location,
|
||||
IcoPath = LogoPath,
|
||||
Preview = new Result.PreviewInfo
|
||||
|
|
|
|||
|
|
@ -90,44 +90,28 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
bool useLocalizedName = !string.IsNullOrEmpty(LocalizedName) && !Name.Equals(LocalizedName);
|
||||
string resultName = useLocalizedName ? LocalizedName : Name;
|
||||
|
||||
if (!Main._settings.EnableDescription)
|
||||
if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || resultName.Equals(Description))
|
||||
{
|
||||
title = resultName;
|
||||
matchResult = StringMatcher.FuzzySearch(query, resultName);
|
||||
}
|
||||
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
|
||||
// Description is always localized, so Name.StartsWith(Description) is generally useless
|
||||
title = resultName;
|
||||
matchResult = StringMatcher.FuzzySearch(query, resultName);
|
||||
}
|
||||
else if (Description.StartsWith(resultName))
|
||||
{
|
||||
// resultName included in Description
|
||||
title = Description;
|
||||
matchResult = StringMatcher.FuzzySearch(query, Description);
|
||||
for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
|
||||
{
|
||||
descriptionMatch.MatchData[i] += resultName.Length + 2; // 2 is ": "
|
||||
}
|
||||
matchResult = descriptionMatch;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Search in both
|
||||
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;
|
||||
}
|
||||
matchResult = nameMatch;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,6 +155,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
var result = new Result
|
||||
{
|
||||
Title = title,
|
||||
AutoCompleteText = resultName,
|
||||
SubTitle = subtitle,
|
||||
IcoPath = IcoPath,
|
||||
Score = matchResult.Score,
|
||||
|
|
@ -486,8 +471,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
}
|
||||
|
||||
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()
|
||||
.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)
|
||||
{
|
||||
// To avoid unnecessary io
|
||||
|
|
@ -801,8 +775,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
|
|||
HashSet<ProgramSource> parents = group.ToHashSet();
|
||||
foreach (var source in group)
|
||||
{
|
||||
if (parents.Any(p => IsSubPathOf(source.Location, p.Location) &&
|
||||
source != p))
|
||||
if (parents.Any(p => FilesFolders.PathContains(p.Location, source.Location)))
|
||||
{
|
||||
parents.Remove(source);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Name": "Program",
|
||||
"Description": "Search programs in Flow.Launcher",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "2.1.0",
|
||||
"Version": "2.2.0",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Name": "Shell",
|
||||
"Description": "Provide executing commands from Flow Launcher",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "2.0.0",
|
||||
"Version": "2.0.1",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Name": "System Commands",
|
||||
"Description": "Provide System related commands. e.g. shutdown,lock, setting etc.",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "2.0.0",
|
||||
"Version": "2.0.1",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Sys.dll",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Name": "URL",
|
||||
"Description": "Open the typed URL from Flow Launcher",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "2.0.0",
|
||||
"Version": "2.0.1",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.Url.dll",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
"Name": "Web Searches",
|
||||
"Description": "Provide the web search ability",
|
||||
"Author": "qianlifeng",
|
||||
"Version": "2.0.1",
|
||||
"Version": "2.0.2",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Description": "Search settings inside Control Panel and Settings App",
|
||||
"Name": "Windows Settings",
|
||||
"Author": "TobiasSekan",
|
||||
"Version": "3.0.1",
|
||||
"Version": "3.0.2",
|
||||
"Language": "csharp",
|
||||
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
|
||||
"ExecuteFileName": "Flow.Launcher.Plugin.WindowsSettings.dll",
|
||||
|
|
|
|||
|
|
@ -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/andreqramos"><img src="https://avatars.githubusercontent.com/u/49326063?v=4" width="10%" /></a>
|
||||
|
||||
<a href="https://github.com/patrickdobler"><img src="https://avatars.githubusercontent.com/u/16536946?v=4" width="10%" /></a>
|
||||
</p>
|
||||
|
||||
### Mentions
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version: '1.11.0.{build}'
|
||||
version: '1.12.1.{build}'
|
||||
|
||||
init:
|
||||
- ps: |
|
||||
|
|
|
|||
Loading…
Reference in a new issue