diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
index f98815c1a..bb1279b2c 100644
--- a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
@@ -1,4 +1,6 @@
-namespace Flow.Launcher.Core.ExternalPlugins
+using System;
+
+namespace Flow.Launcher.Core.ExternalPlugins
{
public record UserPlugin
{
@@ -12,5 +14,8 @@
public string UrlDownload { get; set; }
public string UrlSourceCode { get; set; }
public string IcoPath { get; set; }
+ public DateTime LatestReleaseDate { get; set; }
+ public DateTime DateAdded { get; set; }
+
}
}
diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs
index f06bf58e6..0400a6879 100644
--- a/Flow.Launcher.Infrastructure/Constant.cs
+++ b/Flow.Launcher.Infrastructure/Constant.cs
@@ -21,6 +21,7 @@ namespace Flow.Launcher.Infrastructure
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
+ public static readonly string Dev = "Dev";
public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips";
public static readonly int ThumbnailSize = 64;
diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
index 4a7bc20e3..566e96502 100644
--- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
+++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
@@ -58,7 +58,7 @@
-
+
diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs
index 75f208c9e..b8f1408e7 100644
--- a/Flow.Launcher.Infrastructure/Logger/Log.cs
+++ b/Flow.Launcher.Infrastructure/Logger/Log.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics;
+using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using NLog;
diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs
index 3ffa9f7b1..46165a849 100644
--- a/Flow.Launcher.Infrastructure/StringMatcher.cs
+++ b/Flow.Launcher.Infrastructure/StringMatcher.cs
@@ -202,7 +202,11 @@ namespace Flow.Launcher.Infrastructure
if (allQuerySubstringsMatched)
{
var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex);
- var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1,
+
+ // firstMatchIndex - nearestSpaceIndex - 1 is to set the firstIndex as the index of the first matched char
+ // preceded by a space e.g. 'world' matching 'hello world' firstIndex would be 0 not 6
+ // giving more weight than 'we or donald' by allowing the distance calculation to treat the starting position at after the space.
+ var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, spaceIndices,
lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList();
@@ -296,7 +300,7 @@ namespace Flow.Launcher.Infrastructure
return currentQuerySubstringIndex >= querySubstringsLength;
}
- private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen,
+ private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, List spaceIndices, int matchLen,
bool allSubstringsContainedInCompareString)
{
// A match found near the beginning of a string is scored more than a match found near the end
@@ -304,6 +308,14 @@ namespace Flow.Launcher.Infrastructure
// while the score is lower if they are more spread out
var score = 100 * (query.Length + 1) / ((1 + firstIndex) + (matchLen + 1));
+ // Give more weight to a match that is closer to the start of the string.
+ // if the first matched char is immediately before space and all strings are contained in the compare string e.g. 'world' matching 'hello world'
+ // and 'world hello', because both have 'world' immediately preceded by space, their firstIndex will be 0 when distance is calculated,
+ // to prevent them scoring the same, we adjust the score by deducting the number of spaces it has from the start of the string, so 'world hello'
+ // will score slightly higher than 'hello world' because 'hello world' has one additional space.
+ if (firstIndex == 0 && allSubstringsContainedInCompareString)
+ score -= spaceIndices.Count;
+
// A match with less characters assigning more weights
if (stringToCompare.Length - query.Length < 5)
{
diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs
new file mode 100644
index 000000000..71020369a
--- /dev/null
+++ b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Flow.Launcher.Infrastructure.UserSettings
+{
+ public abstract class ShortcutBaseModel
+ {
+ public string Key { get; set; }
+
+ [JsonIgnore]
+ public Func Expand { get; set; } = () => { return ""; };
+
+ public override bool Equals(object obj)
+ {
+ return obj is ShortcutBaseModel other &&
+ Key == other.Key;
+ }
+
+ public override int GetHashCode()
+ {
+ return Key.GetHashCode();
+ }
+ }
+
+ public class CustomShortcutModel : ShortcutBaseModel
+ {
+ public string Value { get; set; }
+
+ [JsonConstructorAttribute]
+ public CustomShortcutModel(string key, string value)
+ {
+ Key = key;
+ Value = value;
+ Expand = () => { return Value; };
+ }
+
+ public void Deconstruct(out string key, out string value)
+ {
+ key = Key;
+ value = Value;
+ }
+
+ public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut)
+ {
+ return (shortcut.Key, shortcut.Value);
+ }
+
+ public static implicit operator CustomShortcutModel((string Key, string Value) shortcut)
+ {
+ return new CustomShortcutModel(shortcut.Key, shortcut.Value);
+ }
+ }
+
+ public class BuiltinShortcutModel : ShortcutBaseModel
+ {
+ public string Description { get; set; }
+
+ public BuiltinShortcutModel(string key, string description, Func expand)
+ {
+ Key = key;
+ Description = description;
+ Expand = expand ?? (() => { return ""; });
+ }
+ }
+}
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index d42fd425f..33072b53d 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Text.Json.Serialization;
+using System.Windows;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher;
@@ -41,12 +42,17 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool UseGlyphIcons { get; set; } = true;
public bool UseAnimation { get; set; } = true;
public bool UseSound { get; set; } = true;
+ public bool UseClock { get; set; } = true;
+ public bool UseDate { get; set; } = false;
+ public string TimeFormat { get; set; } = "hh:mm tt";
+ public string DateFormat { get; set; } = "MM'/'dd ddd";
public bool FirstLaunch { get; set; } = true;
public double SettingWindowWidth { get; set; } = 1000;
public double SettingWindowHeight { get; set; } = 700;
public double SettingWindowTop { get; set; }
public double SettingWindowLeft { get; set; }
+ public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
public int CustomExplorerIndex { get; set; } = 0;
@@ -125,8 +131,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
PrivateArg = "-private",
EnablePrivate = false,
Editable = false
- }
- ,
+ },
new()
{
Name = "MS Edge",
@@ -182,6 +187,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection();
+ public ObservableCollection CustomShortcuts { get; set; } = new ObservableCollection();
+
+ [JsonIgnore]
+ public ObservableCollection BuiltinShortcuts { get; set; } = new ObservableCollection() {
+ new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText)
+ };
+
public bool DontPromptUpdateMsg { get; set; }
public bool EnableUpdateLog { get; set; }
diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs
index 7633e34a7..a1d3b83ab 100644
--- a/Flow.Launcher.Plugin/Result.cs
+++ b/Flow.Launcher.Plugin/Result.cs
@@ -37,7 +37,11 @@ namespace Flow.Launcher.Plugin
/// user's clipboard when Ctrl + C is pressed on a result. If the text is a file/directory path
/// flow will copy the actual file/folder instead of just the path text.
///
- public string CopyText { get; set; } = string.Empty;
+ public string CopyText
+ {
+ get => string.IsNullOrEmpty(_copyText) ? SubTitle : _copyText;
+ set => _copyText = value;
+ }
///
/// This holds the text which can be provided by plugin to help Flow autocomplete text
@@ -81,6 +85,7 @@ namespace Flow.Launcher.Plugin
/// Delegate to Get Image Source
///
public IconDelegate Icon;
+ private string _copyText = string.Empty;
///
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs
index bbddcbd2a..46c848c7a 100644
--- a/Flow.Launcher.Test/FuzzyMatcherTest.cs
+++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs
@@ -129,14 +129,20 @@ namespace Flow.Launcher.Test
}
}
+
+ ///
+ /// These are standard match scenarios
+ /// The intention of this test is provide a bench mark for how much the score has increased from a change.
+ /// Usually the increase in scoring should not be drastic, increase of less than 10 is acceptable.
+ ///
[TestCase(Chrome, Chrome, 157)]
- [TestCase(Chrome, LastIsChrome, 147)]
+ [TestCase(Chrome, LastIsChrome, 145)]
[TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)]
[TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)]
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)]
[TestCase(Chrome, CandyCrushSagaFromKing, 0)]
- [TestCase("sql", MicrosoftSqlServerManagementStudio, 110)]
- [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended
+ [TestCase("sql", MicrosoftSqlServerManagementStudio, 109)]
+ [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 120)] //double spacing intended
public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring(
string queryString, string compareString, int expectedScore)
{
@@ -275,7 +281,40 @@ namespace Flow.Launcher.Test
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
$"Should be greater than{Environment.NewLine}" +
- $"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}");
+ $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
+ }
+
+ [TestCase("red", "red colour", "metro red")]
+ [TestCase("red", "this red colour", "this colour red")]
+ [TestCase("red", "this red colour", "this colour is very red")]
+ [TestCase("red", "this red colour", "this colour is surprisingly super awesome red and cool")]
+ [TestCase("red", "this colour is surprisingly super red very and cool", "this colour is surprisingly super very red and cool")]
+ public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToIndexZero(
+ string queryString, string compareString1, string compareString2)
+ {
+ // When
+ var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
+
+ // Given
+ var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
+ var compareString2Result = matcher.FuzzyMatch(queryString, compareString2);
+
+ Debug.WriteLine("");
+ Debug.WriteLine("###############################################");
+ Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}");
+ Debug.WriteLine(
+ $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}");
+ Debug.WriteLine(
+ $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
+ Debug.WriteLine("###############################################");
+ Debug.WriteLine("");
+
+ // Should
+ Assert.True(compareString1Result.Score > compareString2Result.Score,
+ $"Query: \"{queryString}\"{Environment.NewLine} " +
+ $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
+ $"Should be greater than{Environment.NewLine}" +
+ $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
}
[TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")]
diff --git a/Flow.Launcher.Test/Plugins/ProgramTest.cs b/Flow.Launcher.Test/Plugins/ProgramTest.cs
index a0d2243ce..e3a05f484 100644
--- a/Flow.Launcher.Test/Plugins/ProgramTest.cs
+++ b/Flow.Launcher.Test/Plugins/ProgramTest.cs
@@ -19,7 +19,7 @@ namespace Flow.Launcher.Test.Plugins
var app = new UWP.Application();
// Act
- var result = app.FormattedPriReferenceValue(packageName, rawPriReferenceValue);
+ var result = UWP.Application.FormattedPriReferenceValue(packageName, rawPriReferenceValue);
// Assert
Assert.IsTrue(result == expectedFormat,
diff --git a/Flow.Launcher/Converters/BoolToVisibilityConverter.cs b/Flow.Launcher/Converters/BoolToVisibilityConverter.cs
new file mode 100644
index 000000000..ad474d693
--- /dev/null
+++ b/Flow.Launcher/Converters/BoolToVisibilityConverter.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+
+namespace Flow.Launcher.Converters
+{
+ public class BoolToVisibilityConverter : IValueConverter
+ {
+ public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
+ {
+ if (parameter != null)
+ {
+ if (value is true)
+ {
+ return Visibility.Collapsed;
+ }
+
+ else
+ {
+ return Visibility.Visible;
+ }
+ }
+ else {
+ if (value is true)
+ {
+ return Visibility.Visible;
+ }
+
+ else {
+ return Visibility.Collapsed;
+ }
+ }
+ }
+
+ public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
+ }
+}
diff --git a/Flow.Launcher/Converters/TextConverter.cs b/Flow.Launcher/Converters/TextConverter.cs
new file mode 100644
index 000000000..90d445776
--- /dev/null
+++ b/Flow.Launcher/Converters/TextConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.ViewModel;
+
+namespace Flow.Launcher.Converters
+{
+ public class TextConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var ID = value.ToString();
+ switch(ID)
+ {
+ case PluginStoreItemViewModel.NewRelease:
+ return InternationalizationManager.Instance.GetTranslation("pluginStore_NewRelease");
+ case PluginStoreItemViewModel.RecentlyUpdated:
+ return InternationalizationManager.Instance.GetTranslation("pluginStore_RecentlyUpdated");
+ case PluginStoreItemViewModel.None:
+ return InternationalizationManager.Instance.GetTranslation("pluginStore_None");
+ case PluginStoreItemViewModel.Installed:
+ return InternationalizationManager.Instance.GetTranslation("pluginStore_Installed");
+ default:
+ return ID;
+ }
+
+ }
+
+ public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
+ }
+}
diff --git a/Flow.Launcher/Converters/TranslationConverter.cs b/Flow.Launcher/Converters/TranslationConverter.cs
new file mode 100644
index 000000000..e1e8a58e3
--- /dev/null
+++ b/Flow.Launcher/Converters/TranslationConverter.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using Flow.Launcher.Core.Resource;
+
+namespace Flow.Launcher.Converters
+{
+ public class TranlationConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var key = value.ToString();
+ if (String.IsNullOrEmpty(key))
+ return key;
+ return InternationalizationManager.Instance.GetTranslation(key);
+ }
+
+ public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
+ }
+}
diff --git a/Flow.Launcher/CustomShortcutSetting.xaml b/Flow.Launcher/CustomShortcutSetting.xaml
new file mode 100644
index 000000000..78b392f3e
--- /dev/null
+++ b/Flow.Launcher/CustomShortcutSetting.xaml
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Flow.Launcher/CustomShortcutSetting.xaml.cs b/Flow.Launcher/CustomShortcutSetting.xaml.cs
new file mode 100644
index 000000000..097d6a53b
--- /dev/null
+++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs
@@ -0,0 +1,73 @@
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.ViewModel;
+using System;
+using System.Windows;
+using System.Windows.Input;
+
+namespace Flow.Launcher
+{
+ public partial class CustomShortcutSetting : Window
+ {
+ private SettingWindowViewModel viewModel;
+ public string Key { get; set; } = String.Empty;
+ public string Value { get; set; } = String.Empty;
+ private string originalKey { get; init; } = null;
+ private string originalValue { get; init; } = null;
+ private bool update { get; init; } = false;
+
+ public CustomShortcutSetting(SettingWindowViewModel vm)
+ {
+ viewModel = vm;
+ InitializeComponent();
+ }
+
+ public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
+ {
+ viewModel = vm;
+ Key = key;
+ Value = value;
+ originalKey = key;
+ originalValue = value;
+ update = true;
+ InitializeComponent();
+ }
+
+ private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
+ {
+ MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
+ return;
+ }
+ // Check if key is modified or adding a new one
+ if (((update && originalKey != Key) || !update)
+ && viewModel.ShortcutExists(Key))
+ {
+ MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
+ return;
+ }
+ DialogResult = !update || originalKey != Key || originalValue != Value;
+ Close();
+ }
+
+ private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e)
+ {
+ App.API.ChangeQuery(tbExpand.Text);
+ Application.Current.MainWindow.Show();
+ Application.Current.MainWindow.Opacity = 1;
+ Application.Current.MainWindow.Focus();
+ }
+ }
+}
diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj
index 813a44527..c1fe421fb 100644
--- a/Flow.Launcher/Flow.Launcher.csproj
+++ b/Flow.Launcher/Flow.Launcher.csproj
@@ -89,7 +89,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
@@ -98,6 +98,7 @@
+
@@ -115,12 +116,12 @@
-
+
-
+
diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs
index a3ad20f77..b9ac6afb3 100644
--- a/Flow.Launcher/Helper/HotKeyMapper.cs
+++ b/Flow.Launcher/Helper/HotKeyMapper.cs
@@ -17,7 +17,7 @@ namespace Flow.Launcher.Helper
internal static void Initialize(MainViewModel mainVM)
{
mainViewModel = mainVM;
- settings = mainViewModel._settings;
+ settings = mainViewModel.Settings;
SetHotkey(settings.Hotkey, OnToggleHotkey);
LoadCustomPluginHotkey();
diff --git a/Flow.Launcher/HotkeyControl.xaml b/Flow.Launcher/HotkeyControl.xaml
index 9b5f671d8..5a593d20a 100644
--- a/Flow.Launcher/HotkeyControl.xaml
+++ b/Flow.Launcher/HotkeyControl.xaml
@@ -38,9 +38,8 @@
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource Color05B}"
- Visibility="Visible">
- Press key
-
+ Text="{DynamicResource flowlauncherPressHotkey}"
+ Visibility="Visible" />
@@ -49,8 +48,8 @@
Margin="0,0,18,0"
VerticalContentAlignment="Center"
input:InputMethod.IsInputMethodEnabled="False"
+ LostFocus="tbHotkey_LostFocus"
PreviewKeyDown="TbHotkey_OnPreviewKeyDown"
- TabIndex="100"
- LostFocus="tbHotkey_LostFocus"/>
+ TabIndex="100" />
\ No newline at end of file
diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs
index 7e07695d8..d746c8fd2 100644
--- a/Flow.Launcher/HotkeyControl.xaml.cs
+++ b/Flow.Launcher/HotkeyControl.xaml.cs
@@ -9,6 +9,7 @@ using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin;
using System.Threading;
+using System.Windows.Interop;
namespace Flow.Launcher
{
@@ -113,7 +114,7 @@ namespace Flow.Launcher
private void tbHotkey_LostFocus(object sender, RoutedEventArgs e)
{
tbMsg.Text = tbMsgTextOriginal;
- tbMsg.Foreground = tbMsgForegroundColorOriginal;
+ tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B");
}
}
}
diff --git a/Flow.Launcher/Languages/da.xaml b/Flow.Launcher/Languages/da.xaml
index f430bd8a3..72211f877 100644
--- a/Flow.Launcher/Languages/da.xaml
+++ b/Flow.Launcher/Languages/da.xaml
@@ -76,13 +76,13 @@
Søgetid:
| Version
Website
- Uninstall
+ Uninstall
Plugin Store
Refresh
- Install
+ Install
Tema
diff --git a/Flow.Launcher/Languages/de.xaml b/Flow.Launcher/Languages/de.xaml
index 09881009b..eb71a0a82 100644
--- a/Flow.Launcher/Languages/de.xaml
+++ b/Flow.Launcher/Languages/de.xaml
@@ -76,13 +76,13 @@
Abfragezeit:
Version
Webseite
- Deinstallieren
+ Deinstallieren
Erweiterungen laden
Aktualisieren
- Installieren
+ Installieren
Design
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 92f2dbded..75278cb91 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -27,7 +27,7 @@
Reset search window position
- Flow Launcher Settings
+ Settings
General
Portable Mode
Store all settings and user data in one folder (Useful when used with removable drives or cloud services).
@@ -100,8 +100,20 @@
Plugin Store
+ New Release
+ Recently Updated
+ Plugins
+ Installed
Refresh
- Install
+ Install
+ Uninstall
+ Update
+ Plugin already installed
+ New Version
+ This plugin has been updated within the last 7 days
+ New Update is Available
+
+
Theme
@@ -124,6 +136,8 @@
Play a small sound when the search window opens
Animation
Use Animation in UI
+ Clock
+ Date
Hotkey
@@ -134,18 +148,26 @@
Show Hotkey
Show result selection hotkey with results.
Custom Query Hotkey
+ Custom Query Shortcut
+ Built-in Shortcuts
Query
+ Shortcut
+ Expanded
+ Description
Delete
Edit
Add
Please select an item
Are you sure you want to delete {0} plugin hotkey?
+ Are you sure you want to delete shortcut: {0} with expansion {1}?
+ Get text from clipboard.
Query window shadow effect
Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited.
Window Width Size
You can also quickly adjust this by using Ctrl+[ and Ctrl+].
Use Segoe Fluent Icons
Use Segoe Fluent Icons for query results where supported
+ Press Key
HTTP Proxy
@@ -169,6 +191,7 @@
Github
Docs
Version
+ Icons
You have activated Flow Launcher {0} times
Check for Updates
New version {0} is available, would you like to restart Flow Launcher to use the update?
@@ -231,6 +254,12 @@
Invalid plugin hotkey
Update
+
+ Custom Query Shortcut
+ Enter a shortcut that automatically expands to the specified query.
+ Shortcut already exists, please enter a new Shortcut or edit the existing one.
+ Shortcut and/or its expansion is empty.
+
Hotkey Unavailable
diff --git a/Flow.Launcher/Languages/es-419.xaml b/Flow.Launcher/Languages/es-419.xaml
index 7afcb711e..ec81c8169 100644
--- a/Flow.Launcher/Languages/es-419.xaml
+++ b/Flow.Launcher/Languages/es-419.xaml
@@ -76,13 +76,13 @@
Tiempo de consulta:
| Versión
Sitio web
- Uninstall
+ Uninstall
Tienda de Plugins
Recargar
- Instalar
+ Instalar
Tema
diff --git a/Flow.Launcher/Languages/fr.xaml b/Flow.Launcher/Languages/fr.xaml
index 189417170..a11050fa8 100644
--- a/Flow.Launcher/Languages/fr.xaml
+++ b/Flow.Launcher/Languages/fr.xaml
@@ -76,13 +76,13 @@
Utilisation :
| Version
Website
- Désinstaller
+ Désinstaller
Plugin Store
Refresh
- Install
+ Install
Thèmes
diff --git a/Flow.Launcher/Languages/it.xaml b/Flow.Launcher/Languages/it.xaml
index 59298c048..3e0c24ddb 100644
--- a/Flow.Launcher/Languages/it.xaml
+++ b/Flow.Launcher/Languages/it.xaml
@@ -76,13 +76,13 @@
Tempo ricerca:
| Versione
Sito Web
- Disinstalla
+ Disinstalla
Negozio dei Plugin
Aggiorna
- Installa
+ Installa
Tema
diff --git a/Flow.Launcher/Languages/ja.xaml b/Flow.Launcher/Languages/ja.xaml
index 27f2210c6..f71b5834c 100644
--- a/Flow.Launcher/Languages/ja.xaml
+++ b/Flow.Launcher/Languages/ja.xaml
@@ -76,13 +76,13 @@
クエリ時間:
| バージョン
ウェブサイト
- アンインストール
+ アンインストール
プラグインストア
Refresh
- Install
+ Install
テーマ
diff --git a/Flow.Launcher/Languages/ko.xaml b/Flow.Launcher/Languages/ko.xaml
index 4f6cf98c0..c9d4b8b69 100644
--- a/Flow.Launcher/Languages/ko.xaml
+++ b/Flow.Launcher/Languages/ko.xaml
@@ -76,13 +76,13 @@
쿼리 시간:
| 버전
웹사이트
- 제거
+ 제거
플러그인 스토어
새로고침
- 설치
+ 설치
테마
diff --git a/Flow.Launcher/Languages/nb.xaml b/Flow.Launcher/Languages/nb.xaml
index e94ef8e20..daca59eb2 100644
--- a/Flow.Launcher/Languages/nb.xaml
+++ b/Flow.Launcher/Languages/nb.xaml
@@ -76,13 +76,13 @@
Query time:
| Version
Website
- Uninstall
+ Uninstall
Plugin Store
Refresh
- Install
+ Install
Theme
diff --git a/Flow.Launcher/Languages/nl.xaml b/Flow.Launcher/Languages/nl.xaml
index 6b0737a61..db6600582 100644
--- a/Flow.Launcher/Languages/nl.xaml
+++ b/Flow.Launcher/Languages/nl.xaml
@@ -76,13 +76,13 @@
Query tijd:
| Versie
Website
- Uninstall
+ Uninstall
Plugin Winkel
Vernieuwen
- Installeren
+ Installeren
Thema
diff --git a/Flow.Launcher/Languages/pl.xaml b/Flow.Launcher/Languages/pl.xaml
index d461f0552..19a830813 100644
--- a/Flow.Launcher/Languages/pl.xaml
+++ b/Flow.Launcher/Languages/pl.xaml
@@ -76,13 +76,13 @@
Czas zapytania:
| Version
Website
- Odinstalowywanie
+ Odinstalowywanie
Plugin Store
Refresh
- Install
+ Install
Skórka
diff --git a/Flow.Launcher/Languages/pt-br.xaml b/Flow.Launcher/Languages/pt-br.xaml
index 1037f79d6..80b2aa825 100644
--- a/Flow.Launcher/Languages/pt-br.xaml
+++ b/Flow.Launcher/Languages/pt-br.xaml
@@ -76,13 +76,13 @@
Tempo de consulta:
| Version
Website
- Desinstalar
+ Desinstalar
Plugin Store
Refresh
- Install
+ Install
Tema
diff --git a/Flow.Launcher/Languages/pt-pt.xaml b/Flow.Launcher/Languages/pt-pt.xaml
index 2ae8f3baa..9d4d12f2c 100644
--- a/Flow.Launcher/Languages/pt-pt.xaml
+++ b/Flow.Launcher/Languages/pt-pt.xaml
@@ -76,13 +76,13 @@
Tempo de consulta:
| Versão
Site
- Desinstalar
+ Desinstalar
Loja de plugins
Recarregar
- Instalar
+ Instalar
Tema
diff --git a/Flow.Launcher/Languages/ru.xaml b/Flow.Launcher/Languages/ru.xaml
index a62b0544b..f4a2cf73f 100644
--- a/Flow.Launcher/Languages/ru.xaml
+++ b/Flow.Launcher/Languages/ru.xaml
@@ -76,13 +76,13 @@
Запрос:
| Version
Website
- Удалить
+ Удалить
Plugin Store
Refresh
- Install
+ Install
Тема
diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml
index fc2ae710e..28759650b 100644
--- a/Flow.Launcher/Languages/sk.xaml
+++ b/Flow.Launcher/Languages/sk.xaml
@@ -76,13 +76,13 @@
Trvanie dopytu:
| Verzia
Webstránka
- Odinštalovať
+ Odinštalovať
Repozitár pluginov
Obnoviť
- Inštalovať
+ Inštalovať
Motív
diff --git a/Flow.Launcher/Languages/sr.xaml b/Flow.Launcher/Languages/sr.xaml
index 82c40b8fa..aed21e7b6 100644
--- a/Flow.Launcher/Languages/sr.xaml
+++ b/Flow.Launcher/Languages/sr.xaml
@@ -76,13 +76,13 @@
Vreme upita:
| Version
Website
- Uninstall
+ Uninstall
Plugin Store
Refresh
- Install
+ Install
Tema
diff --git a/Flow.Launcher/Languages/tr.xaml b/Flow.Launcher/Languages/tr.xaml
index e888612ce..c89d38915 100644
--- a/Flow.Launcher/Languages/tr.xaml
+++ b/Flow.Launcher/Languages/tr.xaml
@@ -76,13 +76,13 @@
Sorgu Süresi:
Sürüm
İnternet Sitesi
- Kaldır
+ Kaldır
Eklenti Mağazası
Yenile
- İndir
+ İndir
Temalar
diff --git a/Flow.Launcher/Languages/uk-UA.xaml b/Flow.Launcher/Languages/uk-UA.xaml
index 95a9311b4..43874b5ed 100644
--- a/Flow.Launcher/Languages/uk-UA.xaml
+++ b/Flow.Launcher/Languages/uk-UA.xaml
@@ -76,13 +76,13 @@
Запит:
| Версія
Сайт
- Uninstall
+ Uninstall
Магазин плагінів
Оновити
- Встановити
+ Встановити
Тема
diff --git a/Flow.Launcher/Languages/zh-cn.xaml b/Flow.Launcher/Languages/zh-cn.xaml
index a495e294e..d8b2e8d69 100644
--- a/Flow.Launcher/Languages/zh-cn.xaml
+++ b/Flow.Launcher/Languages/zh-cn.xaml
@@ -76,13 +76,13 @@
查询耗时:
| 版本
官方网站
- 卸载
+ 卸载
插件商店
刷新
- 安装
+ 安装
主题
diff --git a/Flow.Launcher/Languages/zh-tw.xaml b/Flow.Launcher/Languages/zh-tw.xaml
index 17990b80d..52440e396 100644
--- a/Flow.Launcher/Languages/zh-tw.xaml
+++ b/Flow.Launcher/Languages/zh-tw.xaml
@@ -76,13 +76,13 @@
查詢耗時:
| 版本
官方網站
- 解除安裝
+ 解除安裝
外掛商店
重新整理
- 安裝
+ 安裝
主題
diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml
index 9f7d16ab1..17444aae8 100644
--- a/Flow.Launcher/MainWindow.xaml
+++ b/Flow.Launcher/MainWindow.xaml
@@ -1,5 +1,4 @@
-
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
+
+
+
+
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
-
-
+
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
+ LeftClickResultCommand="{Binding LeftClickResultCommand}"
+ RightClickResultCommand="{Binding RightClickResultCommand}" />
-
-
-
+
+
+
-
+ LeftClickResultCommand="{Binding LeftClickResultCommand}"
+ RightClickResultCommand="{Binding RightClickResultCommand}" />
-
+
\ No newline at end of file
diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs
index fe6c119e1..10b22e33a 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -20,8 +20,16 @@ using Flow.Launcher.Infrastructure;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin.SharedCommands;
-using System.Windows.Data;
+using System.Text;
+using DataObject = System.Windows.DataObject;
using System.Diagnostics;
+using Microsoft.AspNetCore.Http;
+using System.IO;
+using System.Windows.Threading;
+using System.Windows.Data;
+using ModernWpf.Controls;
+using System.Drawing;
+using System.Windows.Forms.Design.Behavior;
namespace Flow.Launcher
{
@@ -45,6 +53,7 @@ namespace Flow.Launcher
DataContext = mainVM;
_viewModel = mainVM;
_settings = settings;
+
InitializeComponent();
InitializePosition();
animationSound.Open(new Uri(AppDomain.CurrentDomain.BaseDirectory + "Resources\\open.wav"));
@@ -54,6 +63,7 @@ namespace Flow.Launcher
{
InitializeComponent();
}
+
private void OnCopy(object sender, ExecutedRoutedEventArgs e)
{
if (QueryTextBox.SelectionLength == 0)
@@ -217,11 +227,12 @@ namespace Flow.Launcher
private void UpdateNotifyIconText()
{
var menu = contextMenu;
- ((MenuItem)menu.Items[1]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")";
- ((MenuItem)menu.Items[2]).Header = InternationalizationManager.Instance.GetTranslation("GameMode");
- ((MenuItem)menu.Items[3]).Header = InternationalizationManager.Instance.GetTranslation("PositionReset");
- ((MenuItem)menu.Items[4]).Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings");
- ((MenuItem)menu.Items[5]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit");
+ ((MenuItem)menu.Items[0]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")";
+ ((MenuItem)menu.Items[1]).Header = InternationalizationManager.Instance.GetTranslation("GameMode");
+ ((MenuItem)menu.Items[2]).Header = InternationalizationManager.Instance.GetTranslation("PositionReset");
+ ((MenuItem)menu.Items[3]).Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings");
+ ((MenuItem)menu.Items[4]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit");
+
}
private void InitializeNotifyIcon()
@@ -233,31 +244,35 @@ namespace Flow.Launcher
Visible = !_settings.HideNotifyIcon
};
contextMenu = new ContextMenu();
-
- var header = new MenuItem
- {
- Header = "Flow Launcher",
- IsEnabled = false
- };
+ var openIcon = new FontIcon { Glyph = "\ue71e" };
var open = new MenuItem
{
- Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" +_settings.Hotkey + ")"
+ Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")",
+ Icon = openIcon
};
+ var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };
var gamemode = new MenuItem
{
- Header = InternationalizationManager.Instance.GetTranslation("GameMode")
+ Header = InternationalizationManager.Instance.GetTranslation("GameMode"),
+ Icon = gamemodeIcon
};
+ var positionresetIcon = new FontIcon { Glyph = "\ue73f" };
var positionreset = new MenuItem
{
- Header = InternationalizationManager.Instance.GetTranslation("PositionReset")
+ Header = InternationalizationManager.Instance.GetTranslation("PositionReset"),
+ Icon = positionresetIcon
};
+ var settingsIcon = new FontIcon { Glyph = "\ue713" };
var settings = new MenuItem
{
- Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings")
+ Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"),
+ Icon = settingsIcon
};
+ var exitIcon = new FontIcon { Glyph = "\ue7e8" };
var exit = new MenuItem
{
- Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit")
+ Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"),
+ Icon = exitIcon
};
open.Click += (o, e) => _viewModel.ToggleFlowLauncher();
@@ -265,7 +280,6 @@ namespace Flow.Launcher
positionreset.Click += (o, e) => PositionReset();
settings.Click += (o, e) => App.API.OpenSettingDialog();
exit.Click += (o, e) => Close();
- contextMenu.Items.Add(header);
contextMenu.Items.Add(open);
gamemode.ToolTip = InternationalizationManager.Instance.GetTranslation("GameModeToolTip");
positionreset.ToolTip = InternationalizationManager.Instance.GetTranslation("PositionResetToolTip");
@@ -391,28 +405,6 @@ namespace Flow.Launcher
if (e.ChangedButton == MouseButton.Left) DragMove();
}
- private void OnPreviewMouseButtonDown(object sender, MouseButtonEventArgs e)
- {
- if (sender != null && e.OriginalSource != null)
- {
- var r = (ResultListBox)sender;
- var d = (DependencyObject)e.OriginalSource;
- var item = ItemsControl.ContainerFromElement(r, d) as ListBoxItem;
- var result = (ResultViewModel)item?.DataContext;
- if (result != null)
- {
- if (e.ChangedButton == MouseButton.Left)
- {
- _viewModel.OpenResultCommand.Execute(null);
- }
- else if (e.ChangedButton == MouseButton.Right)
- {
- _viewModel.LoadContextMenuCommand.Execute(null);
- }
- }
- }
- }
-
private void OnPreviewDragOver(object sender, DragEventArgs e)
{
e.Handled = true;
diff --git a/Flow.Launcher/Resources/CustomControlTemplate.xaml b/Flow.Launcher/Resources/CustomControlTemplate.xaml
index 6ab7ab5ad..07897361c 100644
--- a/Flow.Launcher/Resources/CustomControlTemplate.xaml
+++ b/Flow.Launcher/Resources/CustomControlTemplate.xaml
@@ -1407,7 +1407,8 @@
Padding="{TemplateBinding Padding}"
BorderBrush="{DynamicResource ButtonInsideBorder}"
BorderThickness="{DynamicResource CustomButtonInsideBorderThickness}"
- CornerRadius="4">
+ CornerRadius="4"
+ SnapsToDevicePixels="True">
-
+
-
-
+
+
+
+
+
+
+
@@ -2077,12 +2090,12 @@
-
+
-
+
@@ -2110,7 +2123,7 @@
x:Name="HeaderSite"
MinWidth="0"
MinHeight="0"
- Margin="18,0,0,0"
+ Margin="18,0,18,0"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
@@ -2127,19 +2140,62 @@
Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource ExpanderDownHeaderStyle}" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2672,73 +2728,491 @@
-
+
+
-
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/Resources/Dark.xaml b/Flow.Launcher/Resources/Dark.xaml
index 445e07c63..a5eec29ab 100644
--- a/Flow.Launcher/Resources/Dark.xaml
+++ b/Flow.Launcher/Resources/Dark.xaml
@@ -65,10 +65,13 @@
-
+
+
+ #272727
+
#202020
#2b2b2b
#1d1d1d
diff --git a/Flow.Launcher/Resources/Light.xaml b/Flow.Launcher/Resources/Light.xaml
index cee8b63cd..a78b14d65 100644
--- a/Flow.Launcher/Resources/Light.xaml
+++ b/Flow.Launcher/Resources/Light.xaml
@@ -58,10 +58,13 @@
-
+
+
+ #f6f6f6
+
#f3f3f3
#ffffff
#e5e5e5
diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml
index 5c497b925..691158630 100644
--- a/Flow.Launcher/ResultListBox.xaml
+++ b/Flow.Launcher/ResultListBox.xaml
@@ -25,12 +25,17 @@
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard"
Visibility="{Binding Visbility}"
- mc:Ignorable="d">
+ mc:Ignorable="d"
+ PreviewMouseMove="ResultList_MouseMove"
+ PreviewMouseLeftButtonDown="ResultList_PreviewMouseLeftButtonDown"
+ PreviewMouseLeftButtonUp="ResultListBox_OnPreviewMouseUp"
+ PreviewMouseRightButtonDown="ResultListBox_OnPreviewMouseRightButtonDown">
-
-
-
-
- Always
-
-
- Always
-
-
@@ -64,8 +55,8 @@
-
-
+
+
@@ -73,4 +64,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/x64/SQLite.Interop.dll b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/x64/SQLite.Interop.dll
deleted file mode 100644
index 877b4d78e..000000000
Binary files a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/x64/SQLite.Interop.dll and /dev/null differ
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/x86/SQLite.Interop.dll b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/x86/SQLite.Interop.dll
deleted file mode 100644
index ccb5e03b6..000000000
Binary files a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/x86/SQLite.Interop.dll and /dev/null differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml
index 195fc7df9..8397145cf 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml
@@ -6,12 +6,13 @@
xmlns:local="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{DynamicResource plugin_explorer_manageactionkeywords_header}"
- Width="400"
- SizeToContent="Height"
+ Width="Auto"
+ Height="255"
Background="{DynamicResource PopuBGColor}"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize"
+ SizeToContent="Width"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
@@ -68,7 +69,7 @@
-
LoadContextMenus(Result selectedResult)
@@ -74,4 +74,4 @@ namespace Flow.Launcher.Plugin.PluginsManager
return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description");
}
}
-}
\ No newline at end of file
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml
index 9230eb062..b1a8744fd 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml
@@ -5,12 +5,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{DynamicResource flowlauncher_plugin_program_directory}"
- Width="400"
+ Width="Auto"
+ Height="276"
Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
- SizeToContent="Height"
- WindowStartupLocation="CenterScreen"
- mc:Ignorable="d">
+ ResizeMode="NoResize"
+ SizeToContent="Width"
+ WindowStartupLocation="CenterScreen">
@@ -19,7 +20,6 @@
-
@@ -53,33 +53,80 @@
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -87,19 +134,17 @@
-
-
-
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml.cs
index 045d363b3..e88c9b988 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml.cs
@@ -9,11 +9,12 @@ namespace Flow.Launcher.Plugin.Program
///
/// Interaction logic for AddProgramSource.xaml
///
- public partial class AddProgramSource
+ public partial class AddProgramSource : Window
{
private PluginInitContext _context;
- private Settings.ProgramSource _editing;
+ private ProgramSource _editing;
private Settings _settings;
+ private bool update;
public AddProgramSource(PluginInitContext context, Settings settings)
{
@@ -21,14 +22,19 @@ namespace Flow.Launcher.Plugin.Program
_context = context;
_settings = settings;
Directory.Focus();
+ Chkbox.IsChecked = true;
+ update = false;
+ btnAdd.Content = _context.API.GetTranslation("flowlauncher_plugin_program_add");
}
- public AddProgramSource(Settings.ProgramSource edit, Settings settings)
+ public AddProgramSource(PluginInitContext context, Settings settings, ProgramSource source)
{
- _editing = edit;
- _settings = settings;
-
InitializeComponent();
+ _context = context;
+ _editing = source;
+ _settings = settings;
+ update = true;
+ Chkbox.IsChecked = _editing.Enabled;
Directory.Text = _editing.Location;
}
@@ -47,34 +53,54 @@ namespace Flow.Launcher.Plugin.Program
Close();
}
- private void ButtonAdd_OnClick(object sender, RoutedEventArgs e)
+ private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
{
- string s = Directory.Text;
- if (!System.IO.Directory.Exists(s))
+ string path = Directory.Text;
+ bool modified = false;
+ if (!System.IO.Directory.Exists(path))
{
System.Windows.MessageBox.Show(_context.API.GetTranslation("flowlauncher_plugin_program_invalid_path"));
return;
}
- if (_editing == null)
+ if (!update)
{
- if (!ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == Directory.Text))
+ if (!ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier.Equals(path, System.StringComparison.OrdinalIgnoreCase)))
{
- var source = new ProgramSource
- {
- Location = Directory.Text,
- UniqueIdentifier = Directory.Text
- };
-
+ var source = new ProgramSource(path);
+ modified = true;
_settings.ProgramSources.Insert(0, source);
ProgramSetting.ProgramSettingDisplayList.Add(source);
}
+ else
+ {
+ System.Windows.MessageBox.Show(_context.API.GetTranslation("flowlauncher_plugin_program_duplicate_program_source"));
+ return;
+ }
}
else
{
- _editing.Location = Directory.Text;
+ // Separate checks to avoid changing UniqueIdentifier of UWP
+ if (!_editing.Location.Equals(path, System.StringComparison.OrdinalIgnoreCase))
+ {
+ if (ProgramSetting.ProgramSettingDisplayList
+ .Any(x => x.UniqueIdentifier.Equals(path, System.StringComparison.OrdinalIgnoreCase)))
+ {
+ // Check if the new location is used
+ // No need to check win32 or uwp, just override them
+ System.Windows.MessageBox.Show(_context.API.GetTranslation("flowlauncher_plugin_program_duplicate_program_source"));
+ return;
+ }
+ modified = true;
+ _editing.Location = path; // Changes UniqueIdentifier internally
+ }
+ if (_editing.Enabled != Chkbox.IsChecked)
+ {
+ modified = true;
+ _editing.Enabled = Chkbox.IsChecked ?? true;
+ }
}
- DialogResult = true;
+ DialogResult = modified;
Close();
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/FileChangeWatcher.cs b/Plugins/Flow.Launcher.Plugin.Program/FileChangeWatcher.cs
deleted file mode 100644
index 4ede37dec..000000000
--- a/Plugins/Flow.Launcher.Plugin.Program/FileChangeWatcher.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-using Flow.Launcher.Infrastructure.Logger;
-using Flow.Launcher.Plugin.Program.Programs;
-
-namespace Flow.Launcher.Plugin.Program
-{
- //internal static class FileChangeWatcher
- //{
- // private static readonly List WatchedPath = new List();
- // // todo remove previous watcher events
- // public static void AddAll(List sources, string[] suffixes)
- // {
- // foreach (var s in sources)
- // {
- // if (Directory.Exists(s.Location))
- // {
- // AddWatch(s.Location, suffixes);
- // }
- // }
- // }
-
- // public static void AddWatch(string path, string[] programSuffixes, bool includingSubDirectory = true)
- // {
- // if (WatchedPath.Contains(path)) return;
- // if (!Directory.Exists(path))
- // {
- // Log.Warn($"|FileChangeWatcher|{path} doesn't exist");
- // return;
- // }
-
- // WatchedPath.Add(path);
- // foreach (string fileType in programSuffixes)
- // {
- // FileSystemWatcher watcher = new FileSystemWatcher
- // {
- // Path = path,
- // IncludeSubdirectories = includingSubDirectory,
- // Filter = $"*.{fileType}",
- // EnableRaisingEvents = true
- // };
- // watcher.Changed += FileChanged;
- // watcher.Created += FileChanged;
- // watcher.Deleted += FileChanged;
- // watcher.Renamed += FileChanged;
- // }
- // }
-
- // private static void FileChanged(object source, FileSystemEventArgs e)
- // {
- // Task.Run(() =>
- // {
- // Main.IndexPrograms();
- // });
- // }
- //}
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
index 2809e0b5c..83f9464c4 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj
@@ -58,6 +58,7 @@
+
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml
index 8d8cae02c..6d9f916e1 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml
@@ -4,21 +4,27 @@
xmlns:system="clr-namespace:System;assembly=mscorlib">
+ Reset Default
Delete
Edit
Add
Name
Enable
+ Enabled
Disable
Location
All Programs
- File Suffixes
+ File Type
Reindex
Indexing
- Index Start Menu
+ Index Sources
+ Options
+ Start Menu
When enabled, Flow will load programs from the start menu
- Index Registry
+ Registry
When enabled, Flow will load programs from the registry
+ PATH
+ When enabled, Flow will load programs from the PATH environment variable
Hide app path
For executable files such as UWP or lnk, hide the file path from being visible
Search in Program Description
@@ -33,11 +39,30 @@
Please select a program source
Are you sure you want to delete the selected program sources?
+ Another program source with the same location alreaday exists.
- OK
- Flow Launcher will only index files that end with the following suffixes. (Each suffix should split by ';' )
+ Program Source
+ Edit directory and status of this program source.
+
+ Update
+ Program Plugin will only index files with selected suffixes and .url files with selected protocols.
Successfully updated file suffixes
File suffixes can't be empty
+ Protocols can't be empty
+
+ File Suffixes
+ URL Protocols
+ Steam Games
+ Epic Games
+ Http/Https
+ Custom URL Protocols
+ Custom File Suffixes
+
+ Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py)
+
+
+ Insert protocols of .url files you want to index. Protocols should be separated by ';', and should end with "://". (ex>ftp://;mailto://)
+
Run As Different User
Run As Administrator
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Logger/ProgramLogger.cs b/Plugins/Flow.Launcher.Plugin.Program/Logger/ProgramLogger.cs
index cbf4960a3..1ae6d8a29 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Logger/ProgramLogger.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Logger/ProgramLogger.cs
@@ -1,13 +1,8 @@
using NLog;
-using NLog.Config;
-using NLog.Targets;
using System;
-using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security;
-using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher.Plugin.Program.Logger
{
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
index f0a53ed77..feeb8e78a 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading;
@@ -11,9 +9,8 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.Program.Programs;
using Flow.Launcher.Plugin.Program.Views;
+using Flow.Launcher.Plugin.Program.Views.Models;
using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Primitives;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Plugin.Program
@@ -82,13 +79,9 @@ namespace Flow.Launcher.Plugin.Program
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () =>
{
_win32Storage = new BinaryStorage("Win32");
- _win32s = _win32Storage.TryLoad(new Win32[]
- {
- });
+ _win32s = _win32Storage.TryLoad(Array.Empty());
_uwpStorage = new BinaryStorage("UWP");
- _uwps = _uwpStorage.TryLoad(new UWP.Application[]
- {
- });
+ _uwps = _uwpStorage.TryLoad(Array.Empty());
});
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>");
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>");
@@ -102,14 +95,14 @@ namespace Flow.Launcher.Plugin.Program
var b = Task.Run(() =>
{
- Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUwpPrograms);
+ Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|UWPPRogram index cost", IndexUwpPrograms);
});
if (cacheEmpty)
await Task.WhenAll(a, b);
Win32.WatchProgramUpdate(_settings);
- UWP.WatchPackageChange();
+ _ = UWP.WatchPackageChange();
}
public static void IndexWin32Programs()
@@ -123,18 +116,23 @@ namespace Flow.Launcher.Plugin.Program
{
var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major;
- var applications = support ? UWP.All() : new UWP.Application[]
- {
- };
+ var applications = support ? UWP.All() : Array.Empty();
_uwps = applications;
ResetCache();
}
public static async Task IndexProgramsAsync()
{
- var t1 = Task.Run(IndexWin32Programs);
- var t2 = Task.Run(IndexUwpPrograms);
- await Task.WhenAll(t1, t2).ConfigureAwait(false);
+ var a = Task.Run(() =>
+ {
+ Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs);
+ });
+
+ var b = Task.Run(() =>
+ {
+ Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|UWPProgram index cost", IndexUwpPrograms);
+ });
+ await Task.WhenAll(a, b).ConfigureAwait(false);
_settings.LastIndexTime = DateTime.Today;
}
@@ -190,29 +188,33 @@ namespace Flow.Launcher.Plugin.Program
return menuOptions;
}
- private void DisableProgram(IProgram programToDelete)
+ private static void DisableProgram(IProgram programToDelete)
{
if (_settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
return;
if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
- _uwps.FirstOrDefault(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)
- .Enabled = false;
-
- if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
- _win32s.FirstOrDefault(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)
- .Enabled = false;
-
- _settings.DisabledProgramSources
- .Add(
- new Settings.DisabledProgramSource
- {
- Name = programToDelete.Name,
- Location = programToDelete.Location,
- UniqueIdentifier = programToDelete.UniqueIdentifier,
- Enabled = false
- }
- );
+ {
+ var program = _uwps.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
+ program.Enabled = false;
+ _settings.DisabledProgramSources.Add(new ProgramSource(program));
+ _ = Task.Run(() =>
+ {
+ IndexUwpPrograms();
+ _settings.LastIndexTime = DateTime.Today;
+ });
+ }
+ else if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
+ {
+ var program = _win32s.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
+ program.Enabled = false;
+ _settings.DisabledProgramSources.Add(new ProgramSource(program));
+ _ = Task.Run(() =>
+ {
+ IndexWin32Programs();
+ _settings.LastIndexTime = DateTime.Today;
+ });
+ }
}
public static void StartProcess(Func runProcess, ProcessStartInfo info)
@@ -233,6 +235,7 @@ namespace Flow.Launcher.Plugin.Program
{
await IndexProgramsAsync();
}
+
public void Dispose()
{
Win32.Dispose();
diff --git a/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml b/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml
index e5f404141..e4467a8b6 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml
@@ -4,9 +4,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:ui="http://schemas.modernwpf.com/2019"
Title="{DynamicResource flowlauncher_plugin_program_suffixes}"
- Width="400"
+ Width="600"
Background="{DynamicResource PopuBGColor}"
+ DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize"
SizeToContent="Height"
@@ -15,9 +17,73 @@
-
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -55,7 +121,9 @@
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ appref-ms
+
+
+ exe
+
+
+ lnk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -90,7 +259,7 @@
MinWidth="140"
Margin="5,0,0,0"
HorizontalAlignment="Right"
- Click="ButtonBase_OnClick"
+ Click="BtnAdd_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_update}"
Style="{DynamicResource AccentButtonStyle}" />
diff --git a/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml.cs
index 2a10928e6..31565c8b0 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/ProgramSuffixes.xaml.cs
@@ -1,44 +1,76 @@
using System;
+using System.Collections.Generic;
using System.Windows;
namespace Flow.Launcher.Plugin.Program
{
- ///
- /// ProgramSuffixes.xaml 的交互逻辑
- ///
public partial class ProgramSuffixes
{
private PluginInitContext context;
private Settings _settings;
+ public Dictionary SuffixesStatus { get; set; }
+ public Dictionary ProtocolsStatus { get; set; }
+ public bool UseCustomSuffixes { get; set; }
+ public bool UseCustomProtocols { get; set; }
public ProgramSuffixes(PluginInitContext context, Settings settings)
{
this.context = context;
- InitializeComponent();
_settings = settings;
- tbSuffixes.Text = string.Join(Settings.SuffixSeperator.ToString(), _settings.ProgramSuffixes);
+ SuffixesStatus = new Dictionary(_settings.BuiltinSuffixesStatus);
+ ProtocolsStatus = new Dictionary(_settings.BuiltinProtocolsStatus);
+ UseCustomSuffixes = _settings.UseCustomSuffixes;
+ UseCustomProtocols = _settings.UseCustomProtocols;
+ InitializeComponent();
+ tbSuffixes.Text = string.Join(Settings.SuffixSeparator, _settings.CustomSuffixes);
+ tbProtocols.Text = string.Join(Settings.SuffixSeparator, _settings.CustomProtocols);
}
+
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
- private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
- {
- var suffixes = tbSuffixes.Text.Split(Settings.SuffixSeperator, StringSplitOptions.RemoveEmptyEntries);
- if (suffixes.Length == 0)
+ private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
+ {
+ var suffixes = tbSuffixes.Text.Split(Settings.SuffixSeparator, StringSplitOptions.RemoveEmptyEntries);
+ var protocols = tbProtocols.Text.Split(Settings.SuffixSeparator, StringSplitOptions.RemoveEmptyEntries);
+
+ if (suffixes.Length == 0 && UseCustomSuffixes)
{
string warning = context.API.GetTranslation("flowlauncher_plugin_program_suffixes_cannot_empty");
MessageBox.Show(warning);
return;
}
- _settings.ProgramSuffixes = suffixes;
+ if (protocols.Length == 0 && UseCustomProtocols)
+ {
+ string warning = context.API.GetTranslation("flowlauncher_plugin_protocols_cannot_empty");
+ MessageBox.Show(warning);
+ return;
+ }
- string msg = context.API.GetTranslation("flowlauncher_plugin_program_update_file_suffixes");
- MessageBox.Show(msg);
+ _settings.CustomSuffixes = suffixes;
+ _settings.CustomProtocols = protocols;
+ _settings.BuiltinSuffixesStatus = new Dictionary(SuffixesStatus);
+ _settings.BuiltinProtocolsStatus = new Dictionary(ProtocolsStatus);
+ _settings.UseCustomSuffixes = UseCustomSuffixes;
+ _settings.UseCustomProtocols = UseCustomProtocols;
DialogResult = true;
}
+
+ private void BtnReset_OnClick(object sender, RoutedEventArgs e)
+ {
+ apprefMS.IsChecked = true;
+ exe.IsChecked = true;
+ lnk.IsChecked = true;
+ CustomFiles.IsChecked = false;
+
+ steam.IsChecked = true;
+ epic.IsChecked = true;
+ http.IsChecked = false;
+ CustomProtocol.IsChecked = false;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs
index 4b8423ed6..790a70392 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ApplicationActivationHelper.cs
@@ -1,8 +1,6 @@
using System;
-using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
namespace Flow.Launcher.Plugin.Program.Programs
{
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs
index e48f30757..62416609e 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/AppxPackageHelper.cs
@@ -1,20 +1,18 @@
using System;
using System.Collections.Generic;
-using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
-using Windows.Storage;
namespace Flow.Launcher.Plugin.Program.Programs
{
public class AppxPackageHelper
{
// This function returns a list of attributes of applications
- public List getAppsFromManifest(IStream stream)
+ public static List GetAppsFromManifest(IStream stream)
{
+ IAppxFactory appxFactory = (IAppxFactory)new AppxFactory();
List apps = new List();
- var appxFactory = new AppxFactory();
- var reader = ((IAppxFactory)appxFactory).CreateManifestReader(stream);
+ var reader = appxFactory.CreateManifestReader(stream);
var manifestApps = reader.GetApplications();
while (manifestApps.GetHasCurrent())
{
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
index d17048dcb..72b7b0a91 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/ShellLinkHelper.cs
@@ -1,11 +1,8 @@
using System;
-using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
-using System.IO;
using Accessibility;
using System.Runtime.InteropServices.ComTypes;
-using System.Security.Policy;
namespace Flow.Launcher.Plugin.Program.Programs
{
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
index ad7387f10..8a731f5b9 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
@@ -18,7 +18,6 @@ using Flow.Launcher.Plugin.Program.Logger;
using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger;
-using System.Runtime.Versioning;
using System.Threading.Channels;
namespace Flow.Launcher.Plugin.Program.Programs
@@ -42,39 +41,27 @@ namespace Flow.Launcher.Plugin.Program.Programs
FullName = package.Id.FullName;
FamilyName = package.Id.FamilyName;
InitializeAppInfo();
- Apps = Apps.Where(a =>
- {
- var valid =
- !string.IsNullOrEmpty(a.UserModelId) &&
- !string.IsNullOrEmpty(a.DisplayName);
- return valid;
- }).ToArray();
}
private void InitializeAppInfo()
{
- AppxPackageHelper _helper = new AppxPackageHelper();
var path = Path.Combine(Location, "AppxManifest.xml");
var namespaces = XmlNamespaces(path);
InitPackageVersion(namespaces);
const uint noAttribute = 0x80;
- const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive;
- var hResult = SHCreateStreamOnFileEx(path, exclusiveRead, noAttribute, false, null, out IStream stream);
+ const Stgm nonExclusiveRead = Stgm.Read | Stgm.ShareDenyNone;
+ var hResult = SHCreateStreamOnFileEx(path, nonExclusiveRead, noAttribute, false, null, out IStream stream);
if (hResult == Hresult.Ok)
{
- var apps = new List();
+ List _apps = AppxPackageHelper.GetAppsFromManifest(stream);
- List _apps = _helper.getAppsFromManifest(stream);
- foreach (var _app in _apps)
- {
- var app = new Application(_app, this);
- apps.Add(app);
- }
-
- Apps = apps.Where(a => a.AppListEntry != "none").ToArray();
+ Apps = _apps.Select(x => new Application(x, this))
+ .Where(a => !string.IsNullOrEmpty(a.UserModelId)
+ && !string.IsNullOrEmpty(a.DisplayName))
+ .ToArray();
}
else
{
@@ -82,17 +69,17 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|UWP|InitializeAppInfo|{path}" +
"|Error caused while trying to get the details of the UWP program", e);
- Apps = new List().ToArray();
+ Apps = Array.Empty();
}
- if (Marshal.ReleaseComObject(stream) > 0)
+ if (stream != null && Marshal.ReleaseComObject(stream) > 0)
{
Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked");
}
}
/// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx
- private string[] XmlNamespaces(string path)
+ private static string[] XmlNamespaces(string path)
{
XDocument z = XDocument.Load(path);
if (z.Root != null)
@@ -110,9 +97,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|UWP|XmlNamespaces|{path}" +
$"|Error occured while trying to get the XML from {path}", new ArgumentNullException());
- return new string[]
- {
- };
+ return Array.Empty();
}
}
@@ -178,16 +163,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
var updatedListWithoutDisabledApps = applications
.Where(t1 => !Main._settings.DisabledProgramSources
- .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
- .Select(x => x);
+ .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier));
return updatedListWithoutDisabledApps.ToArray();
}
else
{
- return new Application[]
- {
- };
+ return Array.Empty();
}
}
@@ -233,9 +215,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
else
{
- return new Package[]
- {
- };
+ return Array.Empty();
}
}
@@ -297,8 +277,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
[Serializable]
public class Application : IProgram
{
- public string AppListEntry { get; set; }
- public string UniqueIdentifier { get; set; }
+ private string _uid = string.Empty;
+ public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); }
public string DisplayName { get; set; }
public string Description { get; set; }
public string UserModelId { get; set; }
@@ -317,7 +297,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
public Application() { }
-
public Result Result(string query, IPublicAPI api)
{
string title;
@@ -544,7 +523,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- public string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue)
+ public static string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue)
{
const string prefix = "ms-resource:";
@@ -601,92 +580,97 @@ namespace Flow.Launcher.Plugin.Program.Programs
// windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size
// windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx
- string path;
- if (uri.Contains("\\"))
- {
- path = Path.Combine(Package.Location, uri);
- }
- else
+ string path = Path.Combine(Package.Location, uri);
+
+ var logoPath = TryToFindLogo(uri, path);
+ if (String.IsNullOrEmpty(logoPath))
{
+ // TODO: Don't know why, just keep it at the moment
+ // Maybe on older version of Windows 10?
// for C:\Windows\MiracastView etc
- path = Path.Combine(Package.Location, "Assets", uri);
+ return TryToFindLogo(uri, Path.Combine(Package.Location, "Assets", uri));
}
+ return logoPath;
- var extension = Path.GetExtension(path);
- if (extension != null)
+ string TryToFindLogo(string uri, string path)
{
- var end = path.Length - extension.Length;
- var prefix = path.Substring(0, end);
- var paths = new List
+ var extension = Path.GetExtension(path);
+ if (extension != null)
{
- path
- };
+ //if (File.Exists(path))
+ //{
+ // return path; // shortcut, avoid enumerating files
+ //}
- var scaleFactors = new Dictionary>
- {
- // scale factors on win10: https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables,
+ var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44
+ var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets
+ if (String.IsNullOrEmpty(logoNamePrefix) || String.IsNullOrEmpty(logoDir) || !Directory.Exists(logoDir))
{
- PackageVersion.Windows10, new List
- {
- 100,
- 125,
- 150,
- 200,
- 400
- }
- },
+ // Known issue: Edge always triggers it since logo is not at uri
+ ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
+ $"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Package.Location}", new FileNotFoundException());
+ return string.Empty;
+ }
+
+ var files = Directory.EnumerateFiles(logoDir);
+
+ // Currently we don't care which one to choose
+ // Just ignore all qualifiers
+ // select like logo.[xxx_yyy].png
+ // https://learn.microsoft.com/en-us/windows/uwp/app-resources/tailor-resources-lang-scale-contrast
+ var logos = files.Where(file =>
+ Path.GetFileName(file)?.StartsWith(logoNamePrefix, StringComparison.OrdinalIgnoreCase) ?? false
+ && extension.Equals(Path.GetExtension(file), StringComparison.OrdinalIgnoreCase)
+ );
+
+ var selected = logos.FirstOrDefault();
+ var closest = selected;
+ int min = int.MaxValue;
+ foreach(var logo in logos)
{
- PackageVersion.Windows81, new List
+
+ var imageStream = File.OpenRead(logo);
+ var decoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.None);
+ var height = decoder.Frames[0].PixelHeight;
+ var width = decoder.Frames[0].PixelWidth;
+ int pixelCountDiff = Math.Abs(height * width - 1936); // 44*44=1936
+ if(pixelCountDiff < min)
{
- 100,
- 120,
- 140,
- 160,
- 180
- }
- },
- {
- PackageVersion.Windows8, new List
- {
- 100
+ // try to find the closest to 44x44 logo
+ closest = logo;
+ if (pixelCountDiff == 0)
+ break; // found 44x44
+ min = pixelCountDiff;
}
}
- };
- if (scaleFactors.ContainsKey(Package.Version))
- {
- foreach (var factor in scaleFactors[Package.Version])
+ selected = closest;
+ if (!string.IsNullOrEmpty(selected))
{
- paths.Add($"{prefix}.scale-{factor}{extension}");
+ return selected;
+ }
+ else
+ {
+ ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
+ $"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Package.Location}", new FileNotFoundException());
+ return string.Empty;
}
- }
-
- var selected = paths.FirstOrDefault(File.Exists);
- if (!string.IsNullOrEmpty(selected))
- {
- return selected;
}
else
{
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
- $"|{UserModelId} can't find logo uri for {uri} in package location: {Package.Location}", new FileNotFoundException());
+ $"|Unable to find extension from {uri} for {UserModelId} " +
+ $"in package location {Package.Location}", new FileNotFoundException());
return string.Empty;
}
}
- else
- {
- ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
- $"|Unable to find extension from {uri} for {UserModelId} " +
- $"in package location {Package.Location}", new FileNotFoundException());
- return string.Empty;
- }
}
public ImageSource Logo()
{
var logo = ImageFromPath(LogoPath);
- var plated = PlatedImage(logo);
+ var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package?
// todo magic! temp fix for cross thread object
plated.Freeze();
@@ -696,9 +680,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
private BitmapImage ImageFromPath(string path)
{
+ // TODO: Consider using infrastructure.image.imageloader?
if (File.Exists(path))
{
- var image = new BitmapImage(new Uri(path));
+ var image = new BitmapImage();
+ image.BeginInit();
+ image.UriSource = new Uri(path);
+ image.CacheOption = BitmapCacheOption.OnLoad;
+ image.EndInit();
+ image.Freeze();
return image;
}
else
@@ -770,6 +760,23 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
return $"{DisplayName}: {Description}";
}
+
+ public override bool Equals(object obj)
+ {
+ if (obj is Application other)
+ {
+ return UniqueIdentifier == other.UniqueIdentifier;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public override int GetHashCode()
+ {
+ return UniqueIdentifier.GetHashCode();
+ }
}
public enum PackageVersion
@@ -785,6 +792,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
Read = 0x0,
ShareExclusive = 0x10,
+ ShareDenyNone = 0x40
}
private enum Hresult : uint
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
index 6a8b232e9..8ba40b881 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
@@ -11,12 +11,11 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels;
-using Flow.Launcher.Infrastructure.Logger;
-using System.Collections;
using System.Diagnostics;
-using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Channels;
+using Flow.Launcher.Plugin.Program.Views.Models;
+using IniParser;
namespace Flow.Launcher.Plugin.Program.Programs
{
@@ -24,10 +23,21 @@ namespace Flow.Launcher.Plugin.Program.Programs
public class Win32 : IProgram, IEquatable
{
public string Name { get; set; }
- public string UniqueIdentifier { get; set; }
+ public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison
public string IcoPath { get; set; }
+ ///
+ /// Path of the file. It's the path of .lnk or .url for .lnk and .url.
+ ///
public string FullPath { get; set; }
+ ///
+ /// Path of the excutable for .lnk, or the URL for .url.
+ ///
public string LnkResolvedPath { get; set; }
+ ///
+ /// Path of the actual executable file.
+ ///
+ public string ExecutablePath => LnkResolvedPath ?? FullPath;
+ public string WorkingDir => Directory.GetParent(ExecutablePath)?.FullName ?? string.Empty;
public string ParentDirectory { get; set; }
public string ExecutableName { get; set; }
public string Description { get; set; }
@@ -36,7 +46,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string Location => ParentDirectory;
private const string ShortcutExtension = "lnk";
+ private const string UrlExtension = "url";
private const string ExeExtension = "exe";
+ private string _uid = string.Empty;
private static readonly Win32 Default = new Win32()
{
@@ -96,10 +108,23 @@ namespace Flow.Launcher.Plugin.Program.Programs
matchResult.MatchData = new List();
}
+ string subtitle = string.Empty;
+ if (!Main._settings.HideAppsPath)
+ {
+ if (Extension(FullPath) == UrlExtension)
+ {
+ subtitle = LnkResolvedPath;
+ }
+ else
+ {
+ subtitle = FullPath;
+ }
+ }
+
var result = new Result
{
Title = title,
- SubTitle = Main._settings.HideAppsPath ? string.Empty : LnkResolvedPath ?? FullPath,
+ SubTitle = subtitle,
IcoPath = IcoPath,
Score = matchResult.Score,
TitleHighlightData = matchResult.MatchData,
@@ -115,8 +140,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
var info = new ProcessStartInfo
{
- FileName = LnkResolvedPath ?? FullPath,
- WorkingDirectory = ParentDirectory,
+ FileName = ExecutablePath,
+ WorkingDirectory = WorkingDir,
UseShellExecute = true,
Verb = runAsAdmin ? "runas" : null
};
@@ -142,8 +167,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var info = new ProcessStartInfo
{
- FileName = FullPath,
- WorkingDirectory = ParentDirectory,
+ FileName = ExecutablePath,
+ WorkingDirectory = WorkingDir,
UseShellExecute = true
};
@@ -161,8 +186,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var info = new ProcessStartInfo
{
- FileName = FullPath,
- WorkingDirectory = ParentDirectory,
+ FileName = ExecutablePath,
+ WorkingDirectory = WorkingDir,
Verb = "runas",
UseShellExecute = true
};
@@ -221,11 +246,17 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|Win32|Win32Program|{path}" +
$"|Permission denied when trying to load the program from {path}", e);
- return new Win32()
- {
- Valid = false, Enabled = false
- };
+ return Default;
}
+#if !DEBUG
+ catch (Exception e)
+ {
+ ProgramLogger.LogException($"|Win32|Win32Program|{path}" +
+ "|An unexpected error occurred in the calling method Win32Program", e);
+
+ return Default;
+ }
+#endif
}
private static Win32 LnkProgram(string path)
@@ -243,8 +274,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var extension = Extension(target);
if (extension == ExeExtension && File.Exists(target))
{
- program.LnkResolvedPath = program.FullPath;
- program.FullPath = Path.GetFullPath(target).ToLower();
+ program.LnkResolvedPath = Path.GetFullPath(target);
program.ExecutableName = Path.GetFileName(target);
var description = _helper.description;
@@ -272,8 +302,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
"|Error caused likely due to trying to get the description of the program",
e);
- program.Valid = false;
- return program;
+ return Default;
+ }
+ catch (FileNotFoundException e)
+ {
+ ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
+ "|An unexpected error occurred in the calling method LnkProgram", e);
+
+ return Default;
}
#if !DEBUG //Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
catch (Exception e)
@@ -281,12 +317,50 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
"|An unexpected error occurred in the calling method LnkProgram", e);
- program.Valid = false;
- return program;
+ return Default;
}
#endif
}
+ private static Win32 UrlProgram(string path, string[] protocols)
+ {
+ var program = Win32Program(path);
+ program.Valid = false;
+
+ try
+ {
+ var parser = new FileIniDataParser();
+ var data = parser.ReadFile(path);
+ var urlSection = data["InternetShortcut"];
+ var url = urlSection?["URL"];
+ if (String.IsNullOrEmpty(url))
+ {
+ return program;
+ }
+ foreach (var protocol in protocols)
+ {
+ if (url.StartsWith(protocol))
+ {
+ program.LnkResolvedPath = url;
+ program.Valid = true;
+ break;
+ }
+ }
+
+ var iconPath = urlSection?["IconFile"];
+ if (!String.IsNullOrEmpty(iconPath))
+ {
+ program.IcoPath = iconPath;
+ }
+ }
+ catch (Exception e)
+ {
+ // Many files do not have the required fields, so no logging is done.
+ }
+
+ return program;
+ }
+
private static Win32 ExeProgram(string path)
{
try
@@ -297,35 +371,40 @@ namespace Flow.Launcher.Plugin.Program.Programs
program.Description = info.FileDescription;
return program;
}
+ catch (FileNotFoundException e)
+ {
+ ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
+ $"|File not found when trying to load the program from {path}", e);
+
+ return Default;
+ }
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
$"|Permission denied when trying to load the program from {path}", e);
- return new Win32()
- {
- Valid = false, Enabled = false
- };
+ return Default;
}
}
- private static IEnumerable ProgramPaths(string directory, string[] suffixes)
+ private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true)
{
if (!Directory.Exists(directory))
return Enumerable.Empty();
return Directory.EnumerateFiles(directory, "*", new EnumerationOptions
{
- IgnoreInaccessible = true, RecurseSubdirectories = true
+ IgnoreInaccessible = true,
+ RecurseSubdirectories = recursive
}).Where(x => suffixes.Contains(Extension(x)));
}
private static string Extension(string path)
{
- var extension = Path.GetExtension(path)?.ToLower();
+ var extension = Path.GetExtension(path)?.ToLowerInvariant();
if (!string.IsNullOrEmpty(extension))
{
- return extension.Substring(1);
+ return extension.Substring(1); // remove dot
}
else
{
@@ -333,44 +412,51 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- private static IEnumerable UnregisteredPrograms(List sources, string[] suffixes)
+ private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes, string[] protocols)
{
- var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
- .SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x)
- .Distinct();
-
- var programs = paths.Select(x => Extension(x) switch
- {
- ExeExtension => ExeProgram(x),
- ShortcutExtension => LnkProgram(x),
- _ => Win32Program(x)
- });
-
+ // Disabled custom sources are not in DisabledProgramSources
+ var paths = directories.AsParallel()
+ .SelectMany(s => EnumerateProgramsInDir(s, suffixes));
+ // Remove disabled programs in DisabledProgramSources
+ var programs = ExceptDisabledSource(paths).Select(x => GetProgramFromPath(x, protocols));
return programs;
}
- private static IEnumerable StartMenuPrograms(string[] suffixes)
+ private static IEnumerable StartMenuPrograms(string[] suffixes, string[] protocols)
{
- var disabledProgramsList = Main._settings.DisabledProgramSources;
-
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms);
- var paths1 = ProgramPaths(directory1, suffixes);
- var paths2 = ProgramPaths(directory2, suffixes);
+ var paths1 = EnumerateProgramsInDir(directory1, suffixes);
+ var paths2 = EnumerateProgramsInDir(directory2, suffixes);
var toFilter = paths1.Concat(paths2);
var programs = ExceptDisabledSource(toFilter.Distinct())
- .Select(x => Extension(x) switch
- {
- ShortcutExtension => LnkProgram(x),
- _ => Win32Program(x)
- }).Where(x => x.Valid);
+ .Select(x => GetProgramFromPath(x, protocols));
return programs;
}
- private static IEnumerable AppPathsPrograms(string[] suffixes)
+ private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols, List commonParents)
+ {
+ var pathEnv = Environment.GetEnvironmentVariable("Path");
+ if (String.IsNullOrEmpty(pathEnv))
+ {
+ return Array.Empty();
+ }
+
+ var paths = pathEnv.Split(";", StringSplitOptions.RemoveEmptyEntries).DistinctBy(p => p.ToLowerInvariant());
+
+ var toFilter = paths.Where(x => commonParents.All(parent => !IsSubPathOf(x, parent)))
+ .AsParallel()
+ .SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false));
+
+ var programs = ExceptDisabledSource(toFilter.Distinct())
+ .Select(x => GetProgramFromPath(x, protocols));
+ return programs;
+ }
+
+ private static IEnumerable AppPathsPrograms(string[] suffixes, string[] protocols)
{
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
@@ -390,12 +476,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
toFilter = toFilter.Concat(GetPathFromRegistry(rootUser));
}
-
toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p)));
- var filtered = ExceptDisabledSource(toFilter);
-
- return filtered.Select(GetProgramFromPath).ToList(); // ToList due to disposing issue
+ var programs = ExceptDisabledSource(toFilter)
+ .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid).ToList(); // ToList due to disposing issue
+ return programs;
}
private static IEnumerable GetPathFromRegistry(RegistryKey root)
@@ -435,24 +520,25 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- private static Win32 GetProgramFromPath(string path)
+ private static Win32 GetProgramFromPath(string path, string[] protocols)
{
if (string.IsNullOrEmpty(path))
return Default;
path = Environment.ExpandEnvironmentVariables(path);
- if (!File.Exists(path))
- return Default;
-
- var entry = Win32Program(path);
-
- return entry;
+ return Extension(path) switch
+ {
+ ShortcutExtension => LnkProgram(path),
+ ExeExtension => ExeProgram(path),
+ UrlExtension => UrlProgram(path, protocols),
+ _ => Win32Program(path)
+ }; ;
}
- public static IEnumerable ExceptDisabledSource(IEnumerable sources)
+ public static IEnumerable ExceptDisabledSource(IEnumerable paths)
{
- return ExceptDisabledSource(sources, x => x);
+ return ExceptDisabledSource(paths, x => x.ToLowerInvariant());
}
public static IEnumerable ExceptDisabledSource(IEnumerable sources,
@@ -487,14 +573,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static IEnumerable ProgramsHasher(IEnumerable programs)
{
- return programs.GroupBy(p => p.FullPath.ToLower())
+ return programs.GroupBy(p => p.ExecutablePath.ToLowerInvariant())
+ .AsParallel()
.SelectMany(g =>
{
var temp = g.Where(g => !string.IsNullOrEmpty(g.Description)).ToList();
if (temp.Any())
return DistinctBy(temp, x => x.Description);
return g.Take(1);
- }).ToArray();
+ });
}
@@ -503,28 +590,40 @@ namespace Flow.Launcher.Plugin.Program.Programs
try
{
var programs = Enumerable.Empty();
+ var suffixes = settings.GetSuffixes();
+ var protocols = settings.GetProtocols();
- var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes);
+ // Disabled custom sources are not in DisabledProgramSources
+ var sources = settings.ProgramSources.Where(s => Directory.Exists(s.Location) && s.Enabled).Distinct();
+ var commonParents = GetCommonParents(sources);
+
+ var unregistered = UnregisteredPrograms(commonParents, suffixes, protocols);
programs = programs.Concat(unregistered);
- var autoIndexPrograms = Enumerable.Empty();
+ var autoIndexPrograms = Enumerable.Empty(); // for single programs, not folders
if (settings.EnableRegistrySource)
{
- var appPaths = AppPathsPrograms(settings.ProgramSuffixes);
+ var appPaths = AppPathsPrograms(suffixes, protocols);
autoIndexPrograms = autoIndexPrograms.Concat(appPaths);
}
if (settings.EnableStartMenuSource)
{
- var startMenu = StartMenuPrograms(settings.ProgramSuffixes);
+ var startMenu = StartMenuPrograms(suffixes, protocols);
autoIndexPrograms = autoIndexPrograms.Concat(startMenu);
}
- autoIndexPrograms = ProgramsHasher(autoIndexPrograms);
+ if (settings.EnablePATHSource)
+ {
+ var path = PATHPrograms(settings.GetSuffixes(), protocols, commonParents);
+ programs = programs.Concat(path);
+ }
- return programs.Concat(autoIndexPrograms).Distinct().ToArray();
+ autoIndexPrograms = ProgramsHasher(autoIndexPrograms).ToArray();
+
+ return programs.Concat(autoIndexPrograms).Where(x => x.Valid).Distinct().ToArray();
}
#if DEBUG //This is to make developer aware of any unhandled exception and add in handling.
catch (Exception)
@@ -556,6 +655,18 @@ namespace Flow.Launcher.Plugin.Program.Programs
return UniqueIdentifier == other.UniqueIdentifier;
}
+ public override bool Equals(object obj)
+ {
+ if (obj is Win32 other)
+ {
+ return UniqueIdentifier == other.UniqueIdentifier;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
private static IEnumerable GetStartMenuPaths()
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
@@ -572,11 +683,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (settings.EnableStartMenuSource)
paths.AddRange(GetStartMenuPaths());
- paths.AddRange(from source in settings.ProgramSources where source.Enabled select source.Location);
+ var customSources = GetCommonParents(settings.ProgramSources);
+ paths.AddRange(customSources);
+ var fileExtensionToWatch = settings.GetSuffixes();
foreach (var directory in from path in paths where Directory.Exists(path) select path)
{
- WatchDirectory(directory);
+ WatchDirectory(directory, fileExtensionToWatch);
}
_ = Task.Run(MonitorDirectoryChangeAsync);
@@ -596,8 +709,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
await Task.Run(Main.IndexWin32Programs);
}
}
-
- public static void WatchDirectory(string directory)
+
+ public static void WatchDirectory(string directory, string[] extensions)
{
if (!Directory.Exists(directory))
{
@@ -609,7 +722,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
watcher.Deleted += static (_, _) => indexQueue.Writer.TryWrite(default);
watcher.EnableRaisingEvents = true;
watcher.IncludeSubdirectories = true;
-
+ foreach (var extension in extensions)
+ {
+ watcher.Filters.Add($"*.{extension}");
+ }
+
Watchers.Add(watcher);
}
@@ -620,5 +737,38 @@ namespace Flow.Launcher.Plugin.Program.Programs
fileSystemWatcher.Dispose();
}
}
+
+ // 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 GetCommonParents(IEnumerable programSources)
+ {
+ // To avoid unnecessary io
+ // like c:\windows and c:\windows\system32
+ var grouped = programSources.GroupBy(p => p.Location.ToLowerInvariant()[0]); // group by disk
+ List result = new();
+ foreach (var group in grouped)
+ {
+ HashSet parents = group.ToHashSet();
+ foreach (var source in group)
+ {
+ if (parents.Any(p => IsSubPathOf(source.Location, p.Location) &&
+ source != p))
+ {
+ parents.Remove(source);
+ }
+ }
+ result.AddRange(parents.Select(x => x.Location));
+ }
+ return result.DistinctBy(x => x.ToLowerInvariant()).ToList();
+ }
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs
index d97ddd993..e3e8b99b9 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs
@@ -1,48 +1,132 @@
using System;
using System.Collections.Generic;
-using System.IO;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Flow.Launcher.Plugin.Program.Views.Models;
namespace Flow.Launcher.Plugin.Program
{
public class Settings
{
public DateTime LastIndexTime { get; set; }
+
+ ///
+ /// User-added program sources' directories
+ ///
public List ProgramSources { get; set; } = new List();
- public List DisabledProgramSources { get; set; } = new List();
- public string[] ProgramSuffixes { get; set; } = {"appref-ms", "exe", "lnk"};
+
+ ///
+ /// Disabled single programs, not including User-added directories
+ ///
+ public List DisabledProgramSources { get; set; } = new List();
+
+ [Obsolete("Should use GetSuffixes() instead."), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string[] ProgramSuffixes { get; set; } = null;
+ public string[] CustomSuffixes { get; set; } = Array.Empty(); // Custom suffixes only
+ public string[] CustomProtocols { get; set; } = Array.Empty();
+
+ public Dictionary BuiltinSuffixesStatus { get; set; } = new Dictionary{
+ { "exe", true }, { "appref-ms", true }, { "lnk", true }
+ };
+
+ public Dictionary BuiltinProtocolsStatus { get; set; } = new Dictionary{
+ { "steam", true }, { "epic", true }, { "http", false }
+ };
+
+ [JsonIgnore]
+ public Dictionary BuiltinProtocols { get; set; } = new Dictionary{
+ { "steam", $"steam://run/{SuffixSeparator}steam://rungameid/" }, { "epic", "com.epicgames.launcher://apps/" }, { "http", $"http://{SuffixSeparator}https://"}
+ };
+
+ public bool UseCustomSuffixes { get; set; } = false;
+ public bool UseCustomProtocols { get; set; } = false;
+
+ public string[] GetSuffixes()
+ {
+ RemoveRedundantSuffixes();
+ List extensions = new List();
+ foreach (var item in BuiltinSuffixesStatus)
+ {
+ if (item.Value)
+ {
+ extensions.Add(item.Key);
+ }
+ }
+
+ if (BuiltinProtocolsStatus.Values.Any(x => x == true) || UseCustomProtocols)
+ {
+ extensions.Add("url");
+ }
+
+ if (UseCustomSuffixes)
+ {
+ return extensions.Concat(CustomSuffixes).DistinctBy(x => x.ToLower()).ToArray();
+ }
+ else
+ {
+ return extensions.DistinctBy(x => x.ToLower()).ToArray();
+ }
+ }
+
+ public string[] GetProtocols()
+ {
+ List protocols = new List();
+ foreach (var item in BuiltinProtocolsStatus)
+ {
+ if (item.Value)
+ {
+ if (BuiltinProtocols.TryGetValue(item.Key, out string ps))
+ {
+ var tmp = ps.Split(SuffixSeparator, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var protocol in tmp)
+ {
+ protocols.Add(protocol);
+ }
+ }
+ }
+ }
+
+ if (UseCustomProtocols)
+ {
+ return protocols.Concat(CustomProtocols).DistinctBy(x => x.ToLower()).ToArray();
+ }
+ else
+ {
+ return protocols.DistinctBy(x => x.ToLower()).ToArray();
+ }
+ }
+
+ private void RemoveRedundantSuffixes()
+ {
+ // Migrate to new settings
+ // CustomSuffixes no longer contains custom suffixes
+ // users has tweaked the settings
+ // or this function has been executed once
+ if (UseCustomSuffixes == true || ProgramSuffixes == null)
+ return;
+ var suffixes = ProgramSuffixes.ToList();
+ foreach(var item in BuiltinSuffixesStatus)
+ {
+ suffixes.Remove(item.Key);
+ }
+ CustomSuffixes = suffixes.ToArray(); // Custom suffixes
+ UseCustomSuffixes = CustomSuffixes.Length != 0; // Search custom suffixes or not
+ ProgramSuffixes = null;
+ }
public bool EnableStartMenuSource { get; set; } = true;
-
public bool EnableDescription { get; set; } = false;
public bool HideAppsPath { get; set; } = true;
public bool EnableRegistrySource { get; set; } = true;
+ public bool EnablePATHSource { get; set; } = true;
+
public string CustomizedExplorer { get; set; } = Explorer;
public string CustomizedArgs { get; set; } = ExplorerArgs;
- internal const char SuffixSeperator = ';';
+ internal const char SuffixSeparator = ';';
internal const string Explorer = "explorer";
internal const string ExplorerArgs = "%s";
-
- ///
- /// Contains user added folder location contents as well as all user disabled applications
- ///
- ///
- /// Win32 class applications set UniqueIdentifier using their full file path
- /// UWP class applications set UniqueIdentifier using their Application User Model ID
- /// Custom user added program sources set UniqueIdentifier using their location
- ///
- public class ProgramSource
- {
- private string name;
-
- public string Location { get; set; }
- public string Name { get => name ?? new DirectoryInfo(Location).Name; set => name = value; }
- public bool Enabled { get; set; } = true;
- public string UniqueIdentifier { get; set; }
- }
-
- public class DisabledProgramSource : ProgramSource { }
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/SuffixesConverter.cs b/Plugins/Flow.Launcher.Plugin.Program/SuffixesConverter.cs
index a5e9f75dc..ef93913a5 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/SuffixesConverter.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/SuffixesConverter.cs
@@ -12,7 +12,7 @@ namespace Flow.Launcher.Plugin.Program
var text = value as string[];
if (text != null)
{
- return string.Join(";", text);
+ return string.Join(Settings.SuffixSeparator, text);
}
else
{
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs
index 3129b2b99..e4d7c323a 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs
@@ -1,146 +1,88 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using Flow.Launcher.Plugin.Program.Views.Models;
namespace Flow.Launcher.Plugin.Program.Views.Commands
{
internal static class ProgramSettingDisplay
{
- internal static List LoadProgramSources(this List programSources)
+ internal static List LoadProgramSources()
{
- var list = new List();
-
- programSources.ForEach(x => list
- .Add(
- new ProgramSource
- {
- Enabled = x.Enabled,
- Location = x.Location,
- Name = x.Name,
- UniqueIdentifier = x.UniqueIdentifier
- }
- ));
-
// Even though these are disabled, we still want to display them so users can enable later on
- Main._settings
- .DisabledProgramSources
- .Where(t1 => !Main._settings
- .ProgramSources // program sourcces added above already, so exlcude
- .Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
- .Select(x => x)
- .ToList()
- .ForEach(x => list
- .Add(
- new ProgramSource
- {
- Enabled = x.Enabled,
- Location = x.Location,
- Name = x.Name,
- UniqueIdentifier = x.UniqueIdentifier
- }
- ));
-
- return list;
+ return Main._settings
+ .DisabledProgramSources
+ .Union(Main._settings.ProgramSources)
+ .ToList();
}
- internal static void LoadAllApplications(this List list)
+ internal static void DisplayAllPrograms()
{
- Main._win32s
- .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
- .ToList()
- .ForEach(t1 => ProgramSetting.ProgramSettingDisplayList
- .Add(
- new ProgramSource
- {
- Name = t1.Name,
- Location = t1.ParentDirectory,
- UniqueIdentifier = t1.UniqueIdentifier,
- Enabled = t1.Enabled
- }
- ));
+ var win32 = Main._win32s
+ .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
+ .Select(x => new ProgramSource(x));
- Main._uwps
- .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
- .ToList()
- .ForEach(t1 => ProgramSetting.ProgramSettingDisplayList
- .Add(
- new ProgramSource
- {
- Name = t1.DisplayName,
- Location = t1.Package.Location,
- UniqueIdentifier = t1.UniqueIdentifier,
- Enabled = t1.Enabled
- }
- ));
+ var uwp = Main._uwps
+ .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
+ .Select(x => new ProgramSource(x));
+
+ ProgramSetting.ProgramSettingDisplayList.AddRange(win32);
+ ProgramSetting.ProgramSettingDisplayList.AddRange(uwp);
}
- internal static void SetProgramSourcesStatus(this List list, List selectedProgramSourcesToDisable, bool status)
+ internal static void SetProgramSourcesStatus(List selectedProgramSourcesToDisable, bool status)
{
- ProgramSetting.ProgramSettingDisplayList
- .Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status))
- .ToList()
- .ForEach(t1 => t1.Enabled = status);
+ foreach(var program in ProgramSetting.ProgramSettingDisplayList)
+ {
+ if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status))
+ {
+ program.Enabled = status;
+ }
+ }
- Main._win32s
- .Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status))
- .ToList()
- .ForEach(t1 => t1.Enabled = status);
+ foreach(var program in Main._win32s)
+ {
+ if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status))
+ {
+ program.Enabled = status;
+ }
+ }
- Main._uwps
- .Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status))
- .ToList()
- .ForEach(t1 => t1.Enabled = status);
+ foreach (var program in Main._uwps)
+ {
+ if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status))
+ {
+ program.Enabled = status;
+ }
+ }
}
- internal static void StoreDisabledInSettings(this List list)
+ internal static void StoreDisabledInSettings()
{
- Main._settings.ProgramSources
- .Where(t1 => ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && !x.Enabled))
- .ToList()
- .ForEach(t1 => t1.Enabled = false);
-
- ProgramSetting.ProgramSettingDisplayList
+ // Disabled, not in DisabledProgramSources or ProgramSources
+ var tmp = ProgramSetting.ProgramSettingDisplayList
.Where(t1 => !t1.Enabled
- && !Main._settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
- .ToList()
- .ForEach(x => Main._settings.DisabledProgramSources
- .Add(
- new Settings.DisabledProgramSource
- {
- Name = x.Name,
- Location = x.Location,
- UniqueIdentifier = x.UniqueIdentifier,
- Enabled = false
- }
- ));
+ && !Main._settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)
+ && !Main._settings.ProgramSources.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier));
+
+ Main._settings.DisabledProgramSources.AddRange(tmp);
}
- internal static void RemoveDisabledFromSettings(this List list)
+ internal static void RemoveDisabledFromSettings()
{
- Main._settings.ProgramSources
- .Where(t1 => ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && x.Enabled))
- .ToList()
- .ForEach(t1 => t1.Enabled = true);
-
- Main._settings.DisabledProgramSources
- .Where(t1 => ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && x.Enabled))
- .ToList()
- .ForEach(x => Main._settings.DisabledProgramSources.Remove(x));
+ Main._settings.DisabledProgramSources.RemoveAll(t1 => t1.Enabled);
}
internal static bool IsReindexRequired(this List selectedItems)
{
- if (selectedItems.Where(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0
- && selectedItems.Where(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0)
+ // Not in cache
+ if (selectedItems.Any(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
+ && selectedItems.Any(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)))
return true;
// ProgramSources holds list of user added directories,
// so when we enable/disable we need to reindex to show/not show the programs
// that are found in those directories.
- if (selectedItems.Where(t1 => Main._settings.ProgramSources.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0)
+ if (selectedItems.Any(t1 => Main._settings.ProgramSources.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)))
return true;
return false;
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs
index 8e3184ff7..571dc0017 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs
@@ -1,5 +1,89 @@
-
+using System;
+using System.IO;
+using System.Text.Json.Serialization;
+using Flow.Launcher.Plugin.Program.Programs;
+
namespace Flow.Launcher.Plugin.Program.Views.Models
{
- public class ProgramSource : Settings.ProgramSource { }
+ ///
+ /// Contains user added folder location contents as well as all user disabled applications
+ ///
+ ///
+ /// Win32 class applications set UniqueIdentifier using their full file path
+ /// UWP class applications set UniqueIdentifier using their Application User Model ID
+ /// Custom user added program sources set UniqueIdentifier using their location
+ ///
+ public class ProgramSource
+ {
+ private string name = string.Empty;
+ private string loc = string.Empty;
+ private string uniqueIdentifier = string.Empty;
+
+ public string Location
+ {
+ get => loc;
+ set
+ {
+ loc = value ?? string.Empty;
+ UniqueIdentifier = value;
+ }
+ }
+
+ public string Name { get => name; set => name = value ?? string.Empty; }
+ public bool Enabled { get; set; } = true;
+
+ public string UniqueIdentifier
+ {
+ get => uniqueIdentifier;
+ private set
+ {
+ uniqueIdentifier = value == null ? string.Empty : value.ToLowerInvariant();
+ }
+ }
+
+ [JsonConstructor]
+ public ProgramSource(string name, string location, bool enabled, string uniqueIdentifier)
+ {
+ loc = location ?? string.Empty;
+ Name = name;
+ Enabled = enabled;
+ UniqueIdentifier = uniqueIdentifier;
+ }
+
+ ///
+ /// Add source by location
+ ///
+ /// location of program source
+ /// enabled
+ public ProgramSource(string location, bool enabled = true)
+ {
+ loc = location ?? string.Empty;
+ Enabled = enabled;
+ UniqueIdentifier = location; // For path comparison
+ Name = new DirectoryInfo(Location).Name;
+ }
+
+ public ProgramSource(IProgram source)
+ {
+ loc = source.Location ?? string.Empty;
+ Name = source.Name;
+ Enabled = source.Enabled;
+ UniqueIdentifier = source.UniqueIdentifier;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is ProgramSource other && other.UniqueIdentifier == this.UniqueIdentifier;
+ }
+
+ public bool Equals(IProgram program)
+ {
+ return program != null && program.UniqueIdentifier == this.UniqueIdentifier;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(UniqueIdentifier);
+ }
+ }
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml
index f07879465..3f9e7196c 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml
@@ -10,58 +10,89 @@
mc:Ignorable="d">
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
+
+
+
@@ -126,7 +159,7 @@
-
+
@@ -173,66 +206,3 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
index 1a31e8c28..b8c2bca22 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
@@ -1,7 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
@@ -36,6 +35,7 @@ namespace Flow.Launcher.Plugin.Program.Views
_settings.EnableDescription = value;
}
}
+
public bool HideAppsPath
{
get => _settings.HideAppsPath;
@@ -66,6 +66,16 @@ namespace Flow.Launcher.Plugin.Program.Views
}
}
+ public bool EnablePATHSource
+ {
+ get => _settings.EnablePATHSource;
+ set
+ {
+ _settings.EnablePATHSource = value;
+ ReIndexing();
+ }
+ }
+
public string CustomizedExplorerPath
{
get => _settings.CustomizedExplorer;
@@ -88,7 +98,7 @@ namespace Flow.Launcher.Plugin.Program.Views
private void Setting_Loaded(object sender, RoutedEventArgs e)
{
- ProgramSettingDisplayList = _settings.ProgramSources.LoadProgramSources();
+ ProgramSettingDisplayList = ProgramSettingDisplay.LoadProgramSources();
programSourceView.ItemsSource = ProgramSettingDisplayList;
ViewRefresh();
@@ -147,20 +157,35 @@ namespace Flow.Launcher.Plugin.Program.Views
private void btnEditProgramSource_OnClick(object sender, RoutedEventArgs e)
{
- var selectedProgramSource = programSourceView.SelectedItem as Settings.ProgramSource;
- if (selectedProgramSource != null)
- {
- var add = new AddProgramSource(selectedProgramSource, _settings);
- if (add.ShowDialog() ?? false)
- {
- ReIndexing();
- }
- }
- else
+ var selectedProgramSource = programSourceView.SelectedItem as ProgramSource;
+ EditProgramSource(selectedProgramSource);
+ }
+
+ private void EditProgramSource(ProgramSource selectedProgramSource)
+ {
+ if (selectedProgramSource == null)
{
string msg = context.API.GetTranslation("flowlauncher_plugin_program_pls_select_program_source");
MessageBox.Show(msg);
}
+ else
+ {
+ var add = new AddProgramSource(context, _settings, selectedProgramSource);
+ if (add.ShowDialog() ?? false)
+ {
+ if (selectedProgramSource.Enabled)
+ {
+ ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, true); // sync status in win32, uwp and disabled
+ ProgramSettingDisplay.RemoveDisabledFromSettings();
+ }
+ else
+ {
+ ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, false);
+ ProgramSettingDisplay.StoreDisabledInSettings();
+ }
+ ReIndexing();
+ }
+ }
}
private void btnReindex_Click(object sender, RoutedEventArgs e)
@@ -199,19 +224,16 @@ namespace Flow.Launcher.Plugin.Program.Views
{
foreach (string directory in directories)
{
- if (Directory.Exists(directory) && !ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == directory))
+ if (Directory.Exists(directory)
+ && !ProgramSettingDisplayList.Any(x => x.UniqueIdentifier.Equals(directory, System.StringComparison.OrdinalIgnoreCase)))
{
- var source = new ProgramSource
- {
- Location = directory,
- UniqueIdentifier = directory
- };
+ var source = new ProgramSource(directory);
directoriesToAdd.Add(source);
}
}
- if (directoriesToAdd.Count() > 0)
+ if (directoriesToAdd.Count > 0)
{
directoriesToAdd.ForEach(x => _settings.ProgramSources.Add(x));
directoriesToAdd.ForEach(x => ProgramSettingDisplayList.Add(x));
@@ -224,7 +246,7 @@ namespace Flow.Launcher.Plugin.Program.Views
private void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e)
{
- ProgramSettingDisplayList.LoadAllApplications();
+ ProgramSettingDisplay.DisplayAllPrograms();
ViewRefresh();
}
@@ -235,18 +257,14 @@ namespace Flow.Launcher.Plugin.Program.Views
.SelectedItems.Cast()
.ToList();
- if (selectedItems.Count() == 0)
+ if (selectedItems.Count == 0)
{
string msg = context.API.GetTranslation("flowlauncher_plugin_program_pls_select_program_source");
MessageBox.Show(msg);
return;
}
- if (selectedItems
- .Where(t1 => !_settings
- .ProgramSources
- .Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
- .Count() == 0)
+ if (IsAllItemsUserAdded(selectedItems))
{
var msg = string.Format(context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source"));
@@ -257,17 +275,17 @@ namespace Flow.Launcher.Plugin.Program.Views
DeleteProgramSources(selectedItems);
}
- else if (IsSelectedRowStatusEnabledMoreOrEqualThanDisabled(selectedItems))
+ else if (HasMoreOrEqualEnabledItems(selectedItems))
{
- ProgramSettingDisplayList.SetProgramSourcesStatus(selectedItems, false);
+ ProgramSettingDisplay.SetProgramSourcesStatus(selectedItems, false);
- ProgramSettingDisplayList.StoreDisabledInSettings();
+ ProgramSettingDisplay.StoreDisabledInSettings();
}
else
{
- ProgramSettingDisplayList.SetProgramSourcesStatus(selectedItems, true);
+ ProgramSettingDisplay.SetProgramSourcesStatus(selectedItems, true);
- ProgramSettingDisplayList.RemoveDisabledFromSettings();
+ ProgramSettingDisplay.RemoveDisabledFromSettings();
}
if (selectedItems.IsReindexRequired())
@@ -329,35 +347,41 @@ namespace Flow.Launcher.Plugin.Program.Views
dataView.Refresh();
}
- private bool IsSelectedRowStatusEnabledMoreOrEqualThanDisabled(List selectedItems)
+ private static bool HasMoreOrEqualEnabledItems(List items)
{
- return selectedItems.Where(x => x.Enabled).Count() >= selectedItems.Where(x => !x.Enabled).Count();
+ var enableCount = items.Where(x => x.Enabled).Count();
+ return enableCount >= items.Count - enableCount;
}
- private void Row_OnClick(object sender, RoutedEventArgs e)
+ private void programSourceView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedItems = programSourceView
- .SelectedItems.Cast()
- .ToList();
+ .SelectedItems.Cast()
+ .ToList();
- if (selectedItems
- .Where(t1 => !_settings
- .ProgramSources
- .Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
- .Count() == 0)
+ if (IsAllItemsUserAdded(selectedItems))
{
btnProgramSourceStatus.Content = context.API.GetTranslation("flowlauncher_plugin_program_delete");
- return;
}
-
- if (IsSelectedRowStatusEnabledMoreOrEqualThanDisabled(selectedItems))
+ else if (HasMoreOrEqualEnabledItems(selectedItems))
{
- btnProgramSourceStatus.Content = "Disable";
+ btnProgramSourceStatus.Content = context.API.GetTranslation("flowlauncher_plugin_program_disable");
}
else
{
- btnProgramSourceStatus.Content = "Enable";
+ btnProgramSourceStatus.Content = context.API.GetTranslation("flowlauncher_plugin_program_enable");
}
}
+
+ private void programSourceView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
+ {
+ var selectedProgramSource = programSourceView.SelectedItem as ProgramSource;
+ EditProgramSource(selectedProgramSource);
+ }
+
+ private bool IsAllItemsUserAdded(List items)
+ {
+ return items.All(x => _settings.ProgramSources.Any(y => y.UniqueIdentifier == x.UniqueIdentifier));
+ }
}
-}
\ No newline at end of file
+}
diff --git a/appveyor.yml b/appveyor.yml
index a71779fdc..aa490fd25 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -4,9 +4,17 @@ init:
- ps: |
$version = new-object System.Version $env:APPVEYOR_BUILD_VERSION
$env:flowVersion = "{0}.{1}.{2}" -f $version.Major, $version.Minor, $version.Build
+ if ($env:APPVEYOR_REPO_BRANCH -eq "dev")
+ {
+ $env:prereleaseTag = "{0}.{1}.{2}.{3}" -f $version.Major, $version.Minor, $version.Build, $version.Revision
+ }
- sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest
- net start WSearch
+cache:
+ - '%USERPROFILE%\.nuget\packages -> **.sln, **.csproj' # preserve nuget folder (packages) unless the solution or projects change
+
+
assembly_info:
patch: true
file: SolutionAssemblyInfo.cs
@@ -14,12 +22,6 @@ assembly_info:
assembly_file_version: $(flowVersion)
assembly_informational_version: $(flowVersion)
-skip_branch_with_pr: true
-
-skip_commits:
- files:
- - '*.md'
-
image: Visual Studio 2022
platform: Any CPU
configuration: Release
@@ -28,7 +30,9 @@ before_build:
build:
project: Flow.Launcher.sln
verbosity: minimal
-after_build:
+test_script:
+ - dotnet test --no-build -c Release
+after_test:
- ps: .\Scripts\post_build.ps1
artifacts:
@@ -51,6 +55,17 @@ deploy:
on:
APPVEYOR_REPO_TAG: true
+ - provider: GitHub
+ repository: Flow-Launcher/Prereleases
+ release: v$(prereleaseTag)
+ description: 'This is the early access build of our upcoming release. All changes contained here are reviewed, tested and stable to use.\n\nSee our [release](https://github.com/Flow-Launcher/Flow.Launcher/pulls?q=is%3Aopen+is%3Apr+label%3Arelease) Pull Request for details.\n\nFor latest production release visit [here](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest)\n\nPlease report any bugs or issues over at the [main repository](https://github.com/Flow-Launcher/Flow.Launcher/issues)'
+ auth_token:
+ secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
+ artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
+ force_update: true
+ on:
+ branch: dev
+
- provider: GitHub
release: v$(flowVersion)
auth_token: