diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
index 64c4cd627..79d6d7605 100644
--- a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace Flow.Launcher.Core.ExternalPlugins
{
@@ -13,9 +13,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
public string Website { get; set; }
public string UrlDownload { get; set; }
public string UrlSourceCode { get; set; }
+ public string LocalInstallPath { get; set; }
public string IcoPath { get; set; }
public DateTime? LatestReleaseDate { get; set; }
public DateTime? DateAdded { get; set; }
+ public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
}
}
diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index 7f4d4ea35..e7dfb31c0 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -380,7 +380,8 @@ namespace Flow.Launcher.Core.Plugin
///
- /// Update a plugin to new version, from a zip file. Will Delete zip after updating.
+ /// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
+ /// unless it's a local path installation
///
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
{
@@ -390,11 +391,11 @@ namespace Flow.Launcher.Core.Plugin
}
///
- /// Install a plugin. Will Delete zip after updating.
+ /// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
///
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
{
- InstallPlugin(plugin, zipFilePath, true);
+ InstallPlugin(plugin, zipFilePath, checkModified: true);
}
///
@@ -420,7 +421,9 @@ namespace Flow.Launcher.Core.Plugin
// Unzip plugin files to temp folder
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
- File.Delete(zipFilePath);
+
+ if(!plugin.IsFromLocalInstallPath)
+ File.Delete(zipFilePath);
var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 18abd4adf..d83c767dd 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -31,6 +31,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public string SelectPrevPageHotkey { get; set; } = $"PageDown";
public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O";
public string SettingWindowHotkey { get; set; } = $"Ctrl+I";
+ public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up";
+ public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down";
public string Language
{
@@ -283,42 +285,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
get
{
- var list = new List
- {
- new("Up", "HotkeyLeftRightDesc"),
- new("Down", "HotkeyLeftRightDesc"),
- new("Left", "HotkeyUpDownDesc"),
- new("Right", "HotkeyUpDownDesc"),
- new("Escape", "HotkeyESCDesc"),
- new("F5", "ReloadPluginHotkey"),
- new("Alt+Home", "HotkeySelectFirstResult"),
- new("Alt+End", "HotkeySelectLastResult"),
- new("Ctrl+R", "HotkeyRequery"),
- new("Ctrl+H", "ToggleHistoryHotkey"),
- new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"),
- new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"),
- new("Ctrl+OemPlus", "QuickHeightHotkey"),
- new("Ctrl+OemMinus", "QuickHeightHotkey"),
- new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"),
- new("Shift+Enter", "OpenContextMenuHotkey"),
- new("Enter", "HotkeyRunDesc"),
- new("Ctrl+Enter", "OpenContainFolderHotkey"),
- new("Alt+Enter", "HotkeyOpenResult"),
- new("Ctrl+F12", "ToggleGameModeHotkey"),
- new("Ctrl+Shift+C", "CopyFilePathHotkey"),
-
- new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1),
- new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2),
- new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3),
- new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4),
- new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5),
- new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6),
- new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7),
- new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8),
- new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9),
- new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10)
- };
+ var list = FixedHotkeys();
+ // Customizeable hotkeys
if(!string.IsNullOrEmpty(Hotkey))
list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = ""));
if(!string.IsNullOrEmpty(PreviewHotkey))
@@ -343,7 +312,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = ""));
if(!string.IsNullOrEmpty(SelectPrevPageHotkey))
list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = ""));
+ if (!string.IsNullOrEmpty(CycleHistoryUpHotkey))
+ list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = ""));
+ if (!string.IsNullOrEmpty(CycleHistoryDownHotkey))
+ list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = ""));
+ // Custom Query Hotkeys
foreach (var customPluginHotkey in CustomPluginHotkeys)
{
if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey))
@@ -353,6 +327,45 @@ namespace Flow.Launcher.Infrastructure.UserSettings
return list;
}
}
+
+ private List FixedHotkeys()
+ {
+ return new List
+ {
+ new("Up", "HotkeyLeftRightDesc"),
+ new("Down", "HotkeyLeftRightDesc"),
+ new("Left", "HotkeyUpDownDesc"),
+ new("Right", "HotkeyUpDownDesc"),
+ new("Escape", "HotkeyESCDesc"),
+ new("F5", "ReloadPluginHotkey"),
+ new("Alt+Home", "HotkeySelectFirstResult"),
+ new("Alt+End", "HotkeySelectLastResult"),
+ new("Ctrl+R", "HotkeyRequery"),
+ new("Ctrl+H", "ToggleHistoryHotkey"),
+ new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"),
+ new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"),
+ new("Ctrl+OemPlus", "QuickHeightHotkey"),
+ new("Ctrl+OemMinus", "QuickHeightHotkey"),
+ new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"),
+ new("Shift+Enter", "OpenContextMenuHotkey"),
+ new("Enter", "HotkeyRunDesc"),
+ new("Ctrl+Enter", "OpenContainFolderHotkey"),
+ new("Alt+Enter", "HotkeyOpenResult"),
+ new("Ctrl+F12", "ToggleGameModeHotkey"),
+ new("Ctrl+Shift+C", "CopyFilePathHotkey"),
+
+ new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1),
+ new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2),
+ new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3),
+ new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4),
+ new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5),
+ new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6),
+ new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7),
+ new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8),
+ new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9),
+ new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10)
+ };
+ }
}
public enum LastQueryMode
diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
index 4137ca8d0..dd8c4b112 100644
--- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
+++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Linq;
#pragma warning disable IDE0005
using System.Windows;
#pragma warning restore IDE0005
@@ -200,6 +201,24 @@ namespace Flow.Launcher.Plugin.SharedCommands
}
}
+ ///
+ /// This checks whether a given string is a zip file path.
+ /// By default does not check if the zip file actually exist on disk, can do so by
+ /// setting checkFileExists = true.
+ ///
+ public static bool IsZipFilePath(string querySearchString, bool checkFileExists = false)
+ {
+ if (IsLocationPathString(querySearchString) && querySearchString.Split('.').Last() == "zip")
+ {
+ if (checkFileExists)
+ return FileExists(querySearchString);
+
+ return true;
+ }
+
+ return false;
+ }
+
///
/// This checks whether a given string is a directory path or network location string.
/// It does not check if location actually exists.
diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
index e9d37433f..80cb74729 100644
--- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs
+++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
@@ -191,11 +191,15 @@ namespace Flow.Launcher.Test.Plugins
[TestCase(@"\c:\", false)]
[TestCase(@"cc:\", false)]
[TestCase(@"\\\SomeNetworkLocation\", false)]
+ [TestCase(@"\\SomeNetworkLocation\", true)]
[TestCase("RandomFile", false)]
[TestCase(@"c:\>*", true)]
[TestCase(@"c:\>", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation", true)]
+ [TestCase(@"c:\SomeLocation\SomeOtherLocation\SomeFile.exe", true)]
+ [TestCase(@"\\SomeNetworkLocation\SomeFile.exe", true)]
+
public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
{
// When, Given
diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml
index b8e2a1cfe..76a613c3f 100644
--- a/Flow.Launcher/App.xaml
+++ b/Flow.Launcher/App.xaml
@@ -24,6 +24,7 @@
+
diff --git a/Flow.Launcher/CustomShortcutSetting.xaml.cs b/Flow.Launcher/CustomShortcutSetting.xaml.cs
index 097d6a53b..82efb4869 100644
--- a/Flow.Launcher/CustomShortcutSetting.xaml.cs
+++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs
@@ -8,7 +8,6 @@ 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;
@@ -17,13 +16,11 @@ namespace Flow.Launcher
public CustomShortcutSetting(SettingWindowViewModel vm)
{
- viewModel = vm;
InitializeComponent();
}
- public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
+ public CustomShortcutSetting(string key, string value)
{
- viewModel = vm;
Key = key;
Value = value;
originalKey = key;
@@ -46,8 +43,7 @@ namespace Flow.Launcher
return;
}
// Check if key is modified or adding a new one
- if (((update && originalKey != Key) || !update)
- && viewModel.ShortcutExists(Key))
+ if ((update && originalKey != Key) || !update)
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
return;
diff --git a/Flow.Launcher/Images/info.png b/Flow.Launcher/Images/info.png
new file mode 100644
index 000000000..75ffe11e0
Binary files /dev/null and b/Flow.Launcher/Images/info.png differ
diff --git a/Flow.Launcher/Images/keyboard.png b/Flow.Launcher/Images/keyboard.png
new file mode 100644
index 000000000..5ac670419
Binary files /dev/null and b/Flow.Launcher/Images/keyboard.png differ
diff --git a/Flow.Launcher/Images/plugins.png b/Flow.Launcher/Images/plugins.png
new file mode 100644
index 000000000..cb1e5906c
Binary files /dev/null and b/Flow.Launcher/Images/plugins.png differ
diff --git a/Flow.Launcher/Images/proxy.png b/Flow.Launcher/Images/proxy.png
new file mode 100644
index 000000000..ded7126aa
Binary files /dev/null and b/Flow.Launcher/Images/proxy.png differ
diff --git a/Flow.Launcher/Images/store.png b/Flow.Launcher/Images/store.png
new file mode 100644
index 000000000..a4b19c8f9
Binary files /dev/null and b/Flow.Launcher/Images/store.png differ
diff --git a/Flow.Launcher/Images/theme.png b/Flow.Launcher/Images/theme.png
new file mode 100644
index 000000000..9518343b7
Binary files /dev/null and b/Flow.Launcher/Images/theme.png differ
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 7fc7a6325..633b84b19 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -189,6 +189,8 @@
Select Previous Item
Next Page
Previous Page
+ Cycle Previous Query
+ Cycle Next Query
Open Context Menu
Open Setting Window
Copy File Path
diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml
index 34b6dfcdc..2315d99ec 100644
--- a/Flow.Launcher/MainWindow.xaml
+++ b/Flow.Launcher/MainWindow.xaml
@@ -198,6 +198,14 @@
Key="{Binding SelectPrevPageHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}"
Command="{Binding SelectPrevPageCommand}"
Modifiers="{Binding SelectPrevPageHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" />
+
+
diff --git a/Flow.Launcher/Resources/Controls/Card.xaml b/Flow.Launcher/Resources/Controls/Card.xaml
index 24a895485..c29a5f602 100644
--- a/Flow.Launcher/Resources/Controls/Card.xaml
+++ b/Flow.Launcher/Resources/Controls/Card.xaml
@@ -20,6 +20,7 @@
+
@@ -36,6 +37,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/Resources/Controls/Card.xaml.cs b/Flow.Launcher/Resources/Controls/Card.xaml.cs
index 06266c775..c8f788aca 100644
--- a/Flow.Launcher/Resources/Controls/Card.xaml.cs
+++ b/Flow.Launcher/Resources/Controls/Card.xaml.cs
@@ -16,6 +16,7 @@ namespace Flow.Launcher.Resources.Controls
{
InitializeComponent();
}
+
public string Title
{
get { return (string)GetValue(TitleProperty); }
diff --git a/Flow.Launcher/Resources/Controls/CardGroup.xaml b/Flow.Launcher/Resources/Controls/CardGroup.xaml
new file mode 100644
index 000000000..f48bf4b6c
--- /dev/null
+++ b/Flow.Launcher/Resources/Controls/CardGroup.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs b/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs
new file mode 100644
index 000000000..b9588275c
--- /dev/null
+++ b/Flow.Launcher/Resources/Controls/CardGroup.xaml.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace Flow.Launcher.Resources.Controls;
+
+public partial class CardGroup : UserControl
+{
+ public enum CardGroupPosition
+ {
+ NotInGroup,
+ First,
+ Middle,
+ Last
+ }
+
+ public new ObservableCollection Content
+ {
+ get { return (ObservableCollection)GetValue(ContentProperty); }
+ set { SetValue(ContentProperty, value); }
+ }
+
+ public static new readonly DependencyProperty ContentProperty =
+ DependencyProperty.Register(nameof(Content), typeof(ObservableCollection), typeof(CardGroup));
+
+ public static readonly DependencyProperty PositionProperty = DependencyProperty.RegisterAttached(
+ "Position", typeof(CardGroupPosition), typeof(CardGroup),
+ new FrameworkPropertyMetadata(CardGroupPosition.NotInGroup, FrameworkPropertyMetadataOptions.AffectsRender)
+ );
+
+ public static void SetPosition(UIElement element, CardGroupPosition value)
+ {
+ element.SetValue(PositionProperty, value);
+ }
+
+ public static CardGroupPosition GetPosition(UIElement element)
+ {
+ return (CardGroupPosition)element.GetValue(PositionProperty);
+ }
+
+ public CardGroup()
+ {
+ InitializeComponent();
+ Content = new ObservableCollection();
+ }
+}
diff --git a/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs b/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs
new file mode 100644
index 000000000..605934e80
--- /dev/null
+++ b/Flow.Launcher/Resources/Controls/CardGroupCardStyleSelector.cs
@@ -0,0 +1,21 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace Flow.Launcher.Resources.Controls;
+
+public class CardGroupCardStyleSelector : StyleSelector
+{
+ public Style FirstStyle { get; set; }
+ public Style MiddleStyle { get; set; }
+ public Style LastStyle { get; set; }
+
+ public override Style SelectStyle(object item, DependencyObject container)
+ {
+ var itemsControl = ItemsControl.ItemsControlFromItemContainer(container);
+ var index = itemsControl.ItemContainerGenerator.IndexFromContainer(container);
+
+ if (index == 0) return FirstStyle;
+ if (index == itemsControl.Items.Count - 1) return LastStyle;
+ return MiddleStyle;
+ }
+}
diff --git a/Flow.Launcher/Resources/Controls/HyperLink.xaml b/Flow.Launcher/Resources/Controls/HyperLink.xaml
new file mode 100644
index 000000000..9ea550afd
--- /dev/null
+++ b/Flow.Launcher/Resources/Controls/HyperLink.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs b/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs
new file mode 100644
index 000000000..855cccdbd
--- /dev/null
+++ b/Flow.Launcher/Resources/Controls/HyperLink.xaml.cs
@@ -0,0 +1,39 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Navigation;
+
+namespace Flow.Launcher.Resources.Controls;
+
+public partial class HyperLink : UserControl
+{
+ public static readonly DependencyProperty UriProperty = DependencyProperty.Register(
+ nameof(Uri), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string))
+ );
+
+ public string Uri
+ {
+ get => (string)GetValue(UriProperty);
+ set => SetValue(UriProperty, value);
+ }
+
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
+ nameof(Text), typeof(string), typeof(HyperLink), new PropertyMetadata(default(string))
+ );
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public HyperLink()
+ {
+ InitializeComponent();
+ }
+
+ private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e)
+ {
+ App.API.OpenUrl(e.Uri);
+ e.Handled = true;
+ }
+}
diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml
new file mode 100644
index 000000000..c2ed4975b
--- /dev/null
+++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs
new file mode 100644
index 000000000..dfa03a204
--- /dev/null
+++ b/Flow.Launcher/Resources/Controls/InstalledPluginDisplay.xaml.cs
@@ -0,0 +1,9 @@
+namespace Flow.Launcher.Resources.Controls;
+
+public partial class InstalledPluginDisplay
+{
+ public InstalledPluginDisplay()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/Flow.Launcher/Resources/CustomControlTemplate.xaml b/Flow.Launcher/Resources/CustomControlTemplate.xaml
index 2ce53bfdb..cbb828582 100644
--- a/Flow.Launcher/Resources/CustomControlTemplate.xaml
+++ b/Flow.Launcher/Resources/CustomControlTemplate.xaml
@@ -1487,10 +1487,11 @@
0.1,0.9 0.2,1.0
+
+
+
+ 48
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/Resources/MarkupExtensions/CollapsedWhenExtension.cs b/Flow.Launcher/Resources/MarkupExtensions/CollapsedWhenExtension.cs
new file mode 100644
index 000000000..cdcd49339
--- /dev/null
+++ b/Flow.Launcher/Resources/MarkupExtensions/CollapsedWhenExtension.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Markup;
+
+namespace Flow.Launcher.Resources.MarkupExtensions;
+
+#nullable enable
+
+public class CollapsedWhenExtension : MarkupExtension {
+ private Binding? When { get; set; }
+ public object? IsEqualTo { get; set; }
+
+ public bool? IsEqualToBool
+ {
+ get => IsEqualTo switch
+ {
+ bool b => b,
+ _ => null
+ };
+ set => IsEqualTo = value;
+ }
+
+ public int? IsEqualToInt
+ {
+ get => IsEqualTo switch
+ {
+ int i => i,
+ _ => null
+ };
+ set => IsEqualTo = value;
+ }
+
+ protected virtual Visibility DefaultVisibility => Visibility.Visible;
+ protected virtual Visibility InvertedVisibility => Visibility.Collapsed;
+
+ public CollapsedWhenExtension(Binding when)
+ {
+ When = when;
+ }
+
+ public override object ProvideValue(IServiceProvider serviceProvider) {
+ if (serviceProvider.GetService(typeof(IProvideValueTarget)) is not IProvideValueTarget provideValueTarget)
+ return DependencyProperty.UnsetValue;
+
+ if (provideValueTarget is not { TargetObject: not null, TargetProperty: not null })
+ return DependencyProperty.UnsetValue;
+
+
+ if (When is null)
+ return DependencyProperty.UnsetValue;
+
+ if (IsEqualTo is Binding isEqualToBinding)
+ {
+ var multiBinding = new MultiBinding
+ {
+ Converter = new HideableVisibilityConverter
+ {
+ DefaultVisibility = DefaultVisibility,
+ InvertedVisibility = InvertedVisibility
+ },
+ Bindings = { When, isEqualToBinding }
+ };
+
+ return multiBinding.ProvideValue(serviceProvider);
+ }
+
+ When.Converter = new HideableVisibilityConverter
+ {
+ DefaultVisibility = DefaultVisibility,
+ InvertedVisibility = InvertedVisibility,
+ IsEqualTo = IsEqualTo
+ };
+ return When.ProvideValue(serviceProvider);
+ }
+}
diff --git a/Flow.Launcher/Resources/MarkupExtensions/HideableVisibilityConverter.cs b/Flow.Launcher/Resources/MarkupExtensions/HideableVisibilityConverter.cs
new file mode 100644
index 000000000..e7d1fc6ea
--- /dev/null
+++ b/Flow.Launcher/Resources/MarkupExtensions/HideableVisibilityConverter.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace Flow.Launcher.Resources.MarkupExtensions;
+
+#nullable enable
+
+public class HideableVisibilityConverter : IMultiValueConverter, IValueConverter {
+ public Visibility DefaultVisibility { get; init; } = Visibility.Visible;
+ public Visibility InvertedVisibility { get; init; } = Visibility.Collapsed;
+
+ public object? IsEqualTo { get; set; }
+
+ public object Convert(object?[] values, Type targetType, object? parameter, CultureInfo culture) {
+ if (values is not { Length: 2 })
+ return DependencyProperty.UnsetValue;
+
+ var value1 = values[0];
+ var value2 = values[1];
+ if (value1 is Enum enum1 && value2 is Enum enum2)
+ {
+ value1 = System.Convert.ToInt32(enum1);
+ value2 = System.Convert.ToInt32(enum2);
+ }
+
+ return Equals(value1, value2) ? InvertedVisibility : DefaultVisibility;
+ }
+
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return Equals(value, IsEqualTo) ? InvertedVisibility : DefaultVisibility;
+ }
+
+ public object[] ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture) {
+ throw new NotSupportedException();
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/Flow.Launcher/Resources/MarkupExtensions/VisibleWhenExtension.cs b/Flow.Launcher/Resources/MarkupExtensions/VisibleWhenExtension.cs
new file mode 100644
index 000000000..b5cc26ae5
--- /dev/null
+++ b/Flow.Launcher/Resources/MarkupExtensions/VisibleWhenExtension.cs
@@ -0,0 +1,14 @@
+using System.Windows;
+using System.Windows.Data;
+
+namespace Flow.Launcher.Resources.MarkupExtensions;
+
+#nullable enable
+
+public class VisibleWhenExtension : CollapsedWhenExtension
+{
+ protected override Visibility DefaultVisibility => Visibility.Collapsed;
+ protected override Visibility InvertedVisibility => Visibility.Visible;
+
+ public VisibleWhenExtension(Binding when) : base(when) { }
+}
diff --git a/Flow.Launcher/Resources/SettingWindowStyle.xaml b/Flow.Launcher/Resources/SettingWindowStyle.xaml
new file mode 100644
index 000000000..2c73094c7
--- /dev/null
+++ b/Flow.Launcher/Resources/SettingWindowStyle.xaml
@@ -0,0 +1,413 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs b/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs
new file mode 100644
index 000000000..025c73f85
--- /dev/null
+++ b/Flow.Launcher/SettingPages/ViewModels/DropdownDataGeneric.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Plugin;
+
+namespace Flow.Launcher.SettingPages.ViewModels;
+
+public class DropdownDataGeneric : BaseModel where TValue : Enum
+{
+ public string Display { get; set; }
+ public TValue Value { get; set; }
+
+ public static List GetValues
(string keyPrefix) where TR : DropdownDataGeneric, new()
+ {
+ var data = new List();
+ var enumValues = (TValue[])Enum.GetValues(typeof(TValue));
+
+ foreach (var value in enumValues)
+ {
+ var key = keyPrefix + value;
+ var display = InternationalizationManager.Instance.GetTranslation(key);
+ data.Add(new TR { Display = display, Value = value });
+ }
+
+ return data;
+ }
+}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs
new file mode 100644
index 000000000..b6563d3e5
--- /dev/null
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneAboutViewModel.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using CommunityToolkit.Mvvm.Input;
+using Flow.Launcher.Core;
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+
+namespace Flow.Launcher.SettingPages.ViewModels;
+
+public partial class SettingsPaneAboutViewModel : BaseModel
+{
+ private readonly Settings _settings;
+ private readonly Updater _updater;
+
+ public string LogFolderSize
+ {
+ get
+ {
+ var size = GetLogFiles().Sum(file => file.Length);
+ return $"{InternationalizationManager.Instance.GetTranslation("clearlogfolder")} ({BytesToReadableString(size)})";
+ }
+ }
+
+ public string Website => Constant.Website;
+ public string SponsorPage => Constant.SponsorPage;
+ public string ReleaseNotes => _updater.GitHubRepository + "/releases/latest";
+ public string Documentation => Constant.Documentation;
+ public string Docs => Constant.Docs;
+ public string Github => Constant.GitHub;
+
+ public string Version => Constant.Version switch
+ {
+ "1.0.0" => Constant.Dev,
+ _ => Constant.Version
+ };
+
+ public string ActivatedTimes => string.Format(
+ InternationalizationManager.Instance.GetTranslation("about_activate_times"),
+ _settings.ActivateTimes
+ );
+
+ public SettingsPaneAboutViewModel(Settings settings, Updater updater)
+ {
+ _settings = settings;
+ _updater = updater;
+ }
+
+ [RelayCommand]
+ private void OpenWelcomeWindow()
+ {
+ var window = new WelcomeWindow(_settings);
+ window.ShowDialog();
+ }
+
+ [RelayCommand]
+ private void AskClearLogFolderConfirmation()
+ {
+ var confirmResult = MessageBox.Show(
+ InternationalizationManager.Instance.GetTranslation("clearlogfolderMessage"),
+ InternationalizationManager.Instance.GetTranslation("clearlogfolder"),
+ MessageBoxButton.YesNo
+ );
+
+ if (confirmResult == MessageBoxResult.Yes)
+ {
+ ClearLogFolder();
+ }
+ }
+
+ [RelayCommand]
+ private void OpenSettingsFolder()
+ {
+ PluginManager.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Settings));
+ }
+
+ [RelayCommand]
+ private void OpenLogsFolder()
+ {
+ App.API.OpenDirectory(GetLogDir(Constant.Version).FullName);
+ }
+
+ [RelayCommand]
+ private Task UpdateApp() => _updater.UpdateAppAsync(App.API, false);
+
+ private void ClearLogFolder()
+ {
+ var logDirectory = GetLogDir();
+ var logFiles = GetLogFiles();
+
+ logFiles.ForEach(f => f.Delete());
+
+ logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
+ .Where(dir => !Constant.Version.Equals(dir.Name))
+ .ToList()
+ .ForEach(dir => dir.Delete());
+
+ OnPropertyChanged(nameof(LogFolderSize));
+ }
+
+ private static DirectoryInfo GetLogDir(string version = "")
+ {
+ return new DirectoryInfo(Path.Combine(DataLocation.DataDirectory(), Constant.Logs, version));
+ }
+
+ private static List GetLogFiles(string version = "")
+ {
+ return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList();
+ }
+
+ private static string BytesToReadableString(long bytes)
+ {
+ const int scale = 1024;
+ string[] orders = { "GB", "MB", "KB", "B" };
+ long max = (long)Math.Pow(scale, orders.Length - 1);
+
+ foreach (string order in orders)
+ {
+ if (bytes > max)
+ return $"{decimal.Divide(bytes, max):##.##} {order}";
+
+ max /= scale;
+ }
+
+ return "0 B";
+ }
+
+}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
new file mode 100644
index 000000000..a9718a0ac
--- /dev/null
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Forms;
+using CommunityToolkit.Mvvm.Input;
+using Flow.Launcher.Core;
+using Flow.Launcher.Core.Configuration;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Helper;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.Plugin.SharedModels;
+
+namespace Flow.Launcher.SettingPages.ViewModels;
+
+public partial class SettingsPaneGeneralViewModel : BaseModel
+{
+ public Settings Settings { get; }
+ private readonly Updater _updater;
+ private readonly IPortable _portable;
+
+ public SettingsPaneGeneralViewModel(Settings settings, Updater updater, IPortable portable)
+ {
+ Settings = settings;
+ _updater = updater;
+ _portable = portable;
+ UpdateLastQueryModeDisplay();
+ }
+
+ public class SearchWindowScreen : DropdownDataGeneric { }
+ public class SearchWindowAlign : DropdownDataGeneric { }
+ // todo a better name?
+ public class LastQueryMode : DropdownDataGeneric { }
+
+ public bool StartFlowLauncherOnSystemStartup
+ {
+ get => Settings.StartFlowLauncherOnSystemStartup;
+ set
+ {
+ Settings.StartFlowLauncherOnSystemStartup = value;
+
+ try
+ {
+ if (value)
+ AutoStartup.Enable();
+ else
+ AutoStartup.Disable();
+ }
+ catch (Exception e)
+ {
+ Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
+ e.Message);
+ }
+ }
+ }
+
+
+ public List SearchWindowScreens =>
+ DropdownDataGeneric.GetValues("SearchWindowScreen");
+
+ public List SearchWindowAligns =>
+ DropdownDataGeneric.GetValues("SearchWindowAlign");
+
+ public List ScreenNumbers
+ {
+ get
+ {
+ var screens = Screen.AllScreens;
+ var screenNumbers = new List();
+ for (int i = 1; i <= screens.Length; i++)
+ {
+ screenNumbers.Add(i);
+ }
+
+ return screenNumbers;
+ }
+ }
+
+ // This is only required to set at startup. When portable mode enabled/disabled a restart is always required
+ private bool _portableMode = DataLocation.PortableDataLocationInUse();
+
+ public bool PortableMode
+ {
+ get => _portableMode;
+ set
+ {
+ if (!_portable.CanUpdatePortability())
+ return;
+
+ if (DataLocation.PortableDataLocationInUse())
+ {
+ _portable.DisablePortableMode();
+ }
+ else
+ {
+ _portable.EnablePortableMode();
+ }
+ }
+ }
+
+ private List _lastQueryModes = new();
+
+ public List LastQueryModes
+ {
+ get
+ {
+ if (_lastQueryModes.Count == 0)
+ {
+ _lastQueryModes =
+ DropdownDataGeneric
+ .GetValues("LastQuery");
+ }
+
+ return _lastQueryModes;
+ }
+ }
+
+ private void UpdateLastQueryModeDisplay()
+ {
+ foreach (var item in LastQueryModes)
+ {
+ item.Display = InternationalizationManager.Instance.GetTranslation($"LastQuery{item.Value}");
+ }
+ }
+
+ public string Language
+ {
+ get => Settings.Language;
+ set
+ {
+ InternationalizationManager.Instance.ChangeLanguage(value);
+
+ if (InternationalizationManager.Instance.PromptShouldUsePinyin(value))
+ ShouldUsePinyin = true;
+
+ UpdateLastQueryModeDisplay();
+ }
+ }
+
+ public bool ShouldUsePinyin
+ {
+ get => Settings.ShouldUsePinyin;
+ set => Settings.ShouldUsePinyin = value;
+ }
+
+ public List QuerySearchPrecisionStrings => Enum
+ .GetValues(typeof(SearchPrecisionScore))
+ .Cast()
+ .Select(v => v.ToString())
+ .ToList();
+
+ public List Languages => InternationalizationManager.Instance.LoadAvailableLanguages();
+ public IEnumerable MaxResultsRange => Enumerable.Range(2, 16);
+
+ public string AlwaysPreviewToolTip => string.Format(
+ InternationalizationManager.Instance.GetTranslation("AlwaysPreviewToolTip"),
+ Settings.PreviewHotkey
+ );
+
+ private string GetFileFromDialog(string title, string filter = "")
+ {
+ var dlg = new OpenFileDialog
+ {
+ InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
+ Multiselect = false,
+ CheckFileExists = true,
+ CheckPathExists = true,
+ Title = title,
+ Filter = filter
+ };
+
+ return dlg.ShowDialog() switch
+ {
+ DialogResult.OK => dlg.FileName,
+ _ => string.Empty
+ };
+ }
+
+ private void UpdateApp()
+ {
+ _ = _updater.UpdateAppAsync(App.API, false);
+ }
+
+ public bool AutoUpdates
+ {
+ get => Settings.AutoUpdates;
+ set
+ {
+ Settings.AutoUpdates = value;
+
+ if (value)
+ {
+ UpdateApp();
+ }
+ }
+ }
+
+ [RelayCommand]
+ private void SelectPython()
+ {
+ var selectedFile = GetFileFromDialog(
+ InternationalizationManager.Instance.GetTranslation("selectPythonExecutable"),
+ "Python|pythonw.exe"
+ );
+
+ if (!string.IsNullOrEmpty(selectedFile))
+ Settings.PluginSettings.PythonExecutablePath = selectedFile;
+ }
+
+ [RelayCommand]
+ private void SelectNode()
+ {
+ var selectedFile = GetFileFromDialog(
+ InternationalizationManager.Instance.GetTranslation("selectNodeExecutable"),
+ "*.exe"
+ );
+
+ if (!string.IsNullOrEmpty(selectedFile))
+ Settings.PluginSettings.NodeExecutablePath = selectedFile;
+ }
+
+ [RelayCommand]
+ private void SelectFileManager()
+ {
+ var fileManagerChangeWindow = new SelectFileManagerWindow(Settings);
+ fileManagerChangeWindow.ShowDialog();
+ }
+
+ [RelayCommand]
+ private void SelectBrowser()
+ {
+ var browserWindow = new SelectBrowserWindow(Settings);
+ browserWindow.ShowDialog();
+ }
+}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs
new file mode 100644
index 000000000..7eb05d945
--- /dev/null
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs
@@ -0,0 +1,134 @@
+using System.Windows;
+using CommunityToolkit.Mvvm.Input;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Helper;
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Infrastructure.Hotkey;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+
+namespace Flow.Launcher.SettingPages.ViewModels;
+
+public partial class SettingsPaneHotkeyViewModel : BaseModel
+{
+ public Settings Settings { get; }
+
+ public CustomPluginHotkey SelectedCustomPluginHotkey { get; set; }
+ public CustomShortcutModel SelectedCustomShortcut { get; set; }
+
+ public string[] OpenResultModifiersList => new[]
+ {
+ KeyConstant.Alt,
+ KeyConstant.Ctrl,
+ $"{KeyConstant.Ctrl}+{KeyConstant.Alt}"
+ };
+
+ public SettingsPaneHotkeyViewModel(Settings settings)
+ {
+ Settings = settings;
+ }
+
+ [RelayCommand]
+ private void SetTogglingHotkey(HotkeyModel hotkey)
+ {
+ HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey);
+ }
+
+ [RelayCommand]
+ private void CustomHotkeyDelete()
+ {
+ var item = SelectedCustomPluginHotkey;
+ if (item is null)
+ {
+ MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
+ return;
+ }
+
+ var result = MessageBox.Show(
+ string.Format(
+ InternationalizationManager.Instance.GetTranslation("deleteCustomHotkeyWarning"), item.Hotkey
+ ),
+ InternationalizationManager.Instance.GetTranslation("delete"),
+ MessageBoxButton.YesNo
+ );
+
+ if (result is MessageBoxResult.Yes)
+ {
+ Settings.CustomPluginHotkeys.Remove(item);
+ HotKeyMapper.RemoveHotkey(item.Hotkey);
+ }
+ }
+
+ [RelayCommand]
+ private void CustomHotkeyEdit()
+ {
+ var item = SelectedCustomPluginHotkey;
+ if (item is null)
+ {
+ MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
+ return;
+ }
+
+ var window = new CustomQueryHotkeySetting(null, Settings);
+ window.UpdateItem(item);
+ window.ShowDialog();
+ }
+
+ [RelayCommand]
+ private void CustomHotkeyAdd()
+ {
+ new CustomQueryHotkeySetting(null, Settings).ShowDialog();
+ }
+
+ [RelayCommand]
+ private void CustomShortcutDelete()
+ {
+ var item = SelectedCustomShortcut;
+ if (item is null)
+ {
+ MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
+ return;
+ }
+
+ var result = MessageBox.Show(
+ string.Format(
+ InternationalizationManager.Instance.GetTranslation("deleteCustomShortcutWarning"), item.Key, item.Value
+ ),
+ InternationalizationManager.Instance.GetTranslation("delete"),
+ MessageBoxButton.YesNo
+ );
+
+ if (result is MessageBoxResult.Yes)
+ {
+ Settings.CustomShortcuts.Remove(item);
+ }
+ }
+
+ [RelayCommand]
+ private void CustomShortcutEdit()
+ {
+ var item = SelectedCustomShortcut;
+ if (item is null)
+ {
+ MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
+ return;
+ }
+
+ var window = new CustomShortcutSetting(item.Key, item.Value);
+ if (window.ShowDialog() is not true) return;
+
+ var index = Settings.CustomShortcuts.IndexOf(item);
+ Settings.CustomShortcuts[index] = new CustomShortcutModel(window.Key, window.Value);
+ }
+
+ [RelayCommand]
+ private void CustomShortcutAdd()
+ {
+ var window = new CustomShortcutSetting(null);
+ if (window.ShowDialog() is true)
+ {
+ var shortcut = new CustomShortcutModel(window.Key, window.Value);
+ Settings.CustomShortcuts.Add(shortcut);
+ }
+ }
+}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
new file mode 100644
index 000000000..e06931011
--- /dev/null
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginStoreViewModel.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.Input;
+using Flow.Launcher.Core.ExternalPlugins;
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.ViewModel;
+
+namespace Flow.Launcher.SettingPages.ViewModels;
+
+public partial class SettingsPanePluginStoreViewModel : BaseModel
+{
+ public string FilterText { get; set; } = string.Empty;
+
+ public IList ExternalPlugins => PluginsManifest.UserPlugins
+ .Select(p => new PluginStoreItemViewModel(p))
+ .OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease)
+ .ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated)
+ .ThenByDescending(p => p.Category == PluginStoreItemViewModel.None)
+ .ThenByDescending(p => p.Category == PluginStoreItemViewModel.Installed)
+ .ToList();
+
+ [RelayCommand]
+ private async Task RefreshExternalPluginsAsync()
+ {
+ await PluginsManifest.UpdateManifestAsync();
+ OnPropertyChanged(nameof(ExternalPlugins));
+ }
+
+ public bool SatisfiesFilter(PluginStoreItemViewModel plugin)
+ {
+ return string.IsNullOrEmpty(FilterText) ||
+ StringMatcher.FuzzySearch(FilterText, plugin.Name).IsSearchPrecisionScoreMet() ||
+ StringMatcher.FuzzySearch(FilterText, plugin.Description).IsSearchPrecisionScoreMet();
+ }
+}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
new file mode 100644
index 000000000..dd9e5786d
--- /dev/null
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPanePluginsViewModel.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Linq;
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.ViewModel;
+
+#nullable enable
+
+namespace Flow.Launcher.SettingPages.ViewModels;
+
+public class SettingsPanePluginsViewModel : BaseModel
+{
+ private readonly Settings _settings;
+
+ public SettingsPanePluginsViewModel(Settings settings)
+ {
+ _settings = settings;
+ }
+
+ public string FilterText { get; set; } = string.Empty;
+
+ public PluginViewModel? SelectedPlugin { get; set; }
+
+ private IEnumerable? _pluginViewModels;
+ private IEnumerable PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins
+ .OrderBy(plugin => plugin.Metadata.Disabled)
+ .ThenBy(plugin => plugin.Metadata.Name)
+ .Select(plugin => new PluginViewModel
+ {
+ PluginPair = plugin,
+ PluginSettingsObject = _settings.PluginSettings.Plugins[plugin.Metadata.ID]
+ })
+ .ToList();
+
+ public List FilteredPluginViewModels => PluginViewModels
+ .Where(v =>
+ string.IsNullOrEmpty(FilterText) ||
+ StringMatcher.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() ||
+ StringMatcher.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet()
+ )
+ .ToList();
+}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs
new file mode 100644
index 000000000..2dd57809d
--- /dev/null
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneProxyViewModel.cs
@@ -0,0 +1,62 @@
+using System.Net;
+using System.Windows;
+using CommunityToolkit.Mvvm.Input;
+using Flow.Launcher.Core;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+
+namespace Flow.Launcher.SettingPages.ViewModels;
+
+public partial class SettingsPaneProxyViewModel : BaseModel
+{
+ private readonly Updater _updater;
+ public Settings Settings { get; }
+
+ public SettingsPaneProxyViewModel(Settings settings, Updater updater)
+ {
+ _updater = updater;
+ Settings = settings;
+ }
+
+ [RelayCommand]
+ private void OnTestProxyClicked()
+ {
+ var message = TestProxy();
+ MessageBox.Show(InternationalizationManager.Instance.GetTranslation(message));
+ }
+
+ private string TestProxy()
+ {
+ if (string.IsNullOrEmpty(Settings.Proxy.Server)) return "serverCantBeEmpty";
+ if (Settings.Proxy.Port <= 0) return "portCantBeEmpty";
+
+ HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository);
+
+ if (string.IsNullOrEmpty(Settings.Proxy.UserName) || string.IsNullOrEmpty(Settings.Proxy.Password))
+ {
+ request.Proxy = new WebProxy(Settings.Proxy.Server, Settings.Proxy.Port);
+ }
+ else
+ {
+ request.Proxy = new WebProxy(Settings.Proxy.Server, Settings.Proxy.Port)
+ {
+ Credentials = new NetworkCredential(Settings.Proxy.UserName, Settings.Proxy.Password)
+ };
+ }
+
+ try
+ {
+ var response = (HttpWebResponse)request.GetResponse();
+ return response.StatusCode switch
+ {
+ HttpStatusCode.OK => "proxyIsCorrect",
+ _ => "proxyConnectFailed"
+ };
+ }
+ catch
+ {
+ return "proxyConnectFailed";
+ }
+ }
+}
diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs
new file mode 100644
index 000000000..89f4fe15c
--- /dev/null
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs
@@ -0,0 +1,392 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using CommunityToolkit.Mvvm.Input;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Helper;
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.ViewModel;
+using ModernWpf;
+using ThemeManager = Flow.Launcher.Core.Resource.ThemeManager;
+using ThemeManagerForColorSchemeSwitch = ModernWpf.ThemeManager;
+
+namespace Flow.Launcher.SettingPages.ViewModels;
+
+public partial class SettingsPaneThemeViewModel : BaseModel
+{
+ private CultureInfo Culture => CultureInfo.DefaultThreadCurrentCulture;
+
+ public Settings Settings { get; }
+
+ public static string LinkHowToCreateTheme => @"https://flowlauncher.com/docs/#/how-to-create-a-theme";
+ public static string LinkThemeGallery => "https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438";
+
+ public string SelectedTheme
+ {
+ get => Settings.Theme;
+ set
+ {
+ ThemeManager.Instance.ChangeTheme(value);
+
+ if (ThemeManager.Instance.BlurEnabled && Settings.UseDropShadowEffect)
+ DropShadowEffect = false;
+ }
+ }
+
+ public bool DropShadowEffect
+ {
+ get => Settings.UseDropShadowEffect;
+ set
+ {
+ if (ThemeManager.Instance.BlurEnabled && value)
+ {
+ MessageBox.Show(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed"));
+ return;
+ }
+
+ if (value)
+ {
+ ThemeManager.Instance.AddDropShadowEffectToCurrentTheme();
+ }
+ else
+ {
+ ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme();
+ }
+
+ Settings.UseDropShadowEffect = value;
+ }
+ }
+
+ public List Themes =>
+ ThemeManager.Instance.LoadAvailableThemes().Select(Path.GetFileNameWithoutExtension).ToList();
+
+
+ public class ColorScheme
+ {
+ public string Display { get; set; }
+ public ColorSchemes Value { get; set; }
+ }
+
+ public List ColorSchemes
+ {
+ get
+ {
+ List modes = new List();
+ var enums = (ColorSchemes[])Enum.GetValues(typeof(ColorSchemes));
+ foreach (var e in enums)
+ {
+ var key = $"ColorScheme{e}";
+ var display = InternationalizationManager.Instance.GetTranslation(key);
+ var m = new ColorScheme { Display = display, Value = e, };
+ modes.Add(m);
+ }
+
+ return modes;
+ }
+ }
+
+ public List TimeFormatList { get; } = new()
+ {
+ "h:mm",
+ "hh:mm",
+ "H:mm",
+ "HH:mm",
+ "tt h:mm",
+ "tt hh:mm",
+ "h:mm tt",
+ "hh:mm tt",
+ "hh:mm:ss tt",
+ "HH:mm:ss"
+ };
+
+ public List DateFormatList { get; } = new()
+ {
+ "MM'/'dd dddd",
+ "MM'/'dd ddd",
+ "MM'/'dd",
+ "MM'-'dd",
+ "MMMM', 'dd",
+ "dd'/'MM",
+ "dd'-'MM",
+ "ddd MM'/'dd",
+ "dddd MM'/'dd",
+ "dddd",
+ "ddd dd'/'MM",
+ "dddd dd'/'MM",
+ "dddd dd', 'MMMM",
+ "dd', 'MMMM"
+ };
+
+ public string TimeFormat
+ {
+ get => Settings.TimeFormat;
+ set => Settings.TimeFormat = value;
+ }
+
+ public string DateFormat
+ {
+ get => Settings.DateFormat;
+ set => Settings.DateFormat = value;
+ }
+
+ public string ClockText => DateTime.Now.ToString(TimeFormat, Culture);
+
+ public string DateText => DateTime.Now.ToString(DateFormat, Culture);
+
+ public double WindowWidthSize
+ {
+ get => Settings.WindowSize;
+ set => Settings.WindowSize = value;
+ }
+
+ public bool UseGlyphIcons
+ {
+ get => Settings.UseGlyphIcons;
+ set => Settings.UseGlyphIcons = value;
+ }
+
+ public bool UseAnimation
+ {
+ get => Settings.UseAnimation;
+ set => Settings.UseAnimation = value;
+ }
+
+ public class AnimationSpeed
+ {
+ public string Display { get; set; }
+ public AnimationSpeeds Value { get; set; }
+ }
+
+ public List AnimationSpeeds
+ {
+ get
+ {
+ List speeds = new List();
+ var enums = (AnimationSpeeds[])Enum.GetValues(typeof(AnimationSpeeds));
+ foreach (var e in enums)
+ {
+ var key = $"AnimationSpeed{e}";
+ var display = InternationalizationManager.Instance.GetTranslation(key);
+ var m = new AnimationSpeed { Display = display, Value = e, };
+ speeds.Add(m);
+ }
+
+ return speeds;
+ }
+ }
+
+ public bool UseSound
+ {
+ get => Settings.UseSound;
+ set => Settings.UseSound = value;
+ }
+
+ public double SoundEffectVolume
+ {
+ get => Settings.SoundVolume;
+ set => Settings.SoundVolume = value;
+ }
+
+ public bool UseClock
+ {
+ get => Settings.UseClock;
+ set => Settings.UseClock = value;
+ }
+
+ public bool UseDate
+ {
+ get => Settings.UseDate;
+ set => Settings.UseDate = value;
+ }
+
+ public Brush PreviewBackground
+ {
+ get
+ {
+ var wallpaper = WallpaperPathRetrieval.GetWallpaperPath();
+ if (wallpaper is not null && File.Exists(wallpaper))
+ {
+ var memStream = new MemoryStream(File.ReadAllBytes(wallpaper));
+ var bitmap = new BitmapImage();
+ bitmap.BeginInit();
+ bitmap.StreamSource = memStream;
+ bitmap.DecodePixelWidth = 800;
+ bitmap.DecodePixelHeight = 600;
+ bitmap.EndInit();
+ return new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill };
+ }
+
+ var wallpaperColor = WallpaperPathRetrieval.GetWallpaperColor();
+ return new SolidColorBrush(wallpaperColor);
+ }
+ }
+
+ public ResultsViewModel PreviewResults
+ {
+ get
+ {
+ var results = new List
+ {
+ new Result
+ {
+ Title = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"),
+ SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"),
+ IcoPath = Path.Combine(
+ Constant.ProgramDirectory,
+ @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png"
+ )
+ },
+ new Result
+ {
+ Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"),
+ SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"),
+ IcoPath = Path.Combine(
+ Constant.ProgramDirectory,
+ @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png"
+ )
+ },
+ new Result
+ {
+ Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"),
+ SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"),
+ IcoPath = Path.Combine(
+ Constant.ProgramDirectory,
+ @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png"
+ )
+ },
+ new Result
+ {
+ Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"),
+ SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"),
+ IcoPath = Path.Combine(
+ Constant.ProgramDirectory,
+ @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png"
+ )
+ }
+ };
+ var vm = new ResultsViewModel(Settings);
+ vm.AddResults(results, "PREVIEW");
+ return vm;
+ }
+ }
+
+ public FontFamily SelectedQueryBoxFont
+ {
+ get
+ {
+ var fontExists = Fonts.SystemFontFamilies.Any(
+ fontFamily =>
+ fontFamily.FamilyNames.Values != null &&
+ fontFamily.FamilyNames.Values.Contains(Settings.QueryBoxFont)
+ );
+
+ return fontExists switch
+ {
+ true => new FontFamily(Settings.QueryBoxFont),
+ _ => new FontFamily("Segoe UI")
+ };
+ }
+ set
+ {
+ Settings.QueryBoxFont = value.ToString();
+ ThemeManager.Instance.ChangeTheme(Settings.Theme);
+ }
+ }
+
+ public FamilyTypeface SelectedQueryBoxFontFaces
+ {
+ get
+ {
+ var typeface = SyntaxSugars.CallOrRescueDefault(
+ () => SelectedQueryBoxFont.ConvertFromInvariantStringsOrNormal(
+ Settings.QueryBoxFontStyle,
+ Settings.QueryBoxFontWeight,
+ Settings.QueryBoxFontStretch
+ )
+ );
+ return typeface;
+ }
+ set
+ {
+ Settings.QueryBoxFontStretch = value.Stretch.ToString();
+ Settings.QueryBoxFontWeight = value.Weight.ToString();
+ Settings.QueryBoxFontStyle = value.Style.ToString();
+ ThemeManager.Instance.ChangeTheme(Settings.Theme);
+ }
+ }
+
+ public FontFamily SelectedResultFont
+ {
+ get
+ {
+ var fontExists = Fonts.SystemFontFamilies.Any(
+ fontFamily =>
+ fontFamily.FamilyNames.Values != null &&
+ fontFamily.FamilyNames.Values.Contains(Settings.ResultFont)
+ );
+ return fontExists switch
+ {
+ true => new FontFamily(Settings.ResultFont),
+ _ => new FontFamily("Segoe UI")
+ };
+ }
+ set
+ {
+ Settings.ResultFont = value.ToString();
+ ThemeManager.Instance.ChangeTheme(Settings.Theme);
+ }
+ }
+
+ public FamilyTypeface SelectedResultFontFaces
+ {
+ get
+ {
+ var typeface = SyntaxSugars.CallOrRescueDefault(
+ () => SelectedResultFont.ConvertFromInvariantStringsOrNormal(
+ Settings.ResultFontStyle,
+ Settings.ResultFontWeight,
+ Settings.ResultFontStretch
+ )
+ );
+ return typeface;
+ }
+ set
+ {
+ Settings.ResultFontStretch = value.Stretch.ToString();
+ Settings.ResultFontWeight = value.Weight.ToString();
+ Settings.ResultFontStyle = value.Style.ToString();
+ ThemeManager.Instance.ChangeTheme(Settings.Theme);
+ }
+ }
+
+ public string ThemeImage => Constant.QueryTextBoxIconImagePath;
+
+ [RelayCommand]
+ private void OpenThemesFolder()
+ {
+ App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes));
+ }
+
+ public void UpdateColorScheme()
+ {
+ ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme = Settings.ColorScheme switch
+ {
+ Constant.Light => ApplicationTheme.Light,
+ Constant.Dark => ApplicationTheme.Dark,
+ Constant.System => null,
+ _ => ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme
+ };
+ }
+
+ public SettingsPaneThemeViewModel(Settings settings)
+ {
+ Settings = settings;
+ }
+}
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml
new file mode 100644
index 000000000..15afd7741
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs
new file mode 100644
index 000000000..de6a4df5e
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneAbout.xaml.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Windows.Navigation;
+using Flow.Launcher.SettingPages.ViewModels;
+
+namespace Flow.Launcher.SettingPages.Views;
+
+public partial class SettingsPaneAbout
+{
+ private SettingsPaneAboutViewModel _viewModel = null!;
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ if (!IsInitialized)
+ {
+ if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings, Updater: { } updater })
+ throw new ArgumentException("Settings are required for SettingsPaneAbout.");
+ _viewModel = new SettingsPaneAboutViewModel(settings, updater);
+ DataContext = _viewModel;
+ InitializeComponent();
+ }
+ base.OnNavigatedTo(e);
+ }
+
+ private void OnRequestNavigate(object sender, RequestNavigateEventArgs e)
+ {
+ App.API.OpenUrl(e.Uri.AbsoluteUri);
+ e.Handled = true;
+ }
+}
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
new file mode 100644
index 000000000..95505165d
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml.cs
new file mode 100644
index 000000000..f95015b2e
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Windows.Navigation;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.SettingPages.ViewModels;
+
+namespace Flow.Launcher.SettingPages.Views;
+
+public partial class SettingsPaneGeneral
+{
+ private SettingsPaneGeneralViewModel _viewModel = null!;
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ if (!IsInitialized)
+ {
+ if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings, Updater: {} updater, Portable: {} portable })
+ throw new ArgumentException("Settings, Updater and Portable are required for SettingsPaneGeneral.");
+ _viewModel = new SettingsPaneGeneralViewModel(settings, updater, portable);
+ DataContext = _viewModel;
+ InitializeComponent();
+ }
+ base.OnNavigatedTo(e);
+ }
+}
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml
new file mode 100644
index 000000000..2698e8609
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml
@@ -0,0 +1,452 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml.cs
new file mode 100644
index 000000000..061eabf51
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Windows.Navigation;
+using Flow.Launcher.SettingPages.ViewModels;
+
+namespace Flow.Launcher.SettingPages.Views;
+
+public partial class SettingsPaneHotkey
+{
+ private SettingsPaneHotkeyViewModel _viewModel = null!;
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ if (!IsInitialized)
+ {
+ if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings })
+ throw new ArgumentException("Settings are required for SettingsPaneHotkey.");
+ _viewModel = new SettingsPaneHotkeyViewModel(settings);
+ DataContext = _viewModel;
+ InitializeComponent();
+ }
+ base.OnNavigatedTo(e);
+ }
+}
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml
new file mode 100644
index 000000000..675cef7de
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml
@@ -0,0 +1,352 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs
new file mode 100644
index 000000000..dfb4a7eaf
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml.cs
@@ -0,0 +1,66 @@
+using System;
+using System.ComponentModel;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Navigation;
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.SettingPages.ViewModels;
+using Flow.Launcher.ViewModel;
+
+namespace Flow.Launcher.SettingPages.Views;
+
+public partial class SettingsPanePluginStore
+{
+ private SettingsPanePluginStoreViewModel _viewModel = null!;
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ if (!IsInitialized)
+ {
+ if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings })
+ throw new ArgumentException($"Settings are required for {nameof(SettingsPanePluginStore)}.");
+ _viewModel = new SettingsPanePluginStoreViewModel();
+ DataContext = _viewModel;
+ InitializeComponent();
+ }
+ _viewModel.PropertyChanged += ViewModel_PropertyChanged;
+ base.OnNavigatedTo(e);
+ }
+
+ private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(SettingsPanePluginStoreViewModel.FilterText))
+ {
+ ((CollectionViewSource)FindResource("PluginStoreCollectionView")).View.Refresh();
+ }
+ }
+
+ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
+ {
+ _viewModel.PropertyChanged -= ViewModel_PropertyChanged;
+ base.OnNavigatingFrom(e);
+ }
+
+ private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return;
+ PluginStoreFilterTextbox.Focus();
+ }
+
+ private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e)
+ {
+ PluginManager.API.OpenUrl(e.Uri.AbsoluteUri);
+ e.Handled = true;
+ }
+
+ private void PluginStoreCollectionView_OnFilter(object sender, FilterEventArgs e)
+ {
+ if (e.Item is not PluginStoreItemViewModel plugin)
+ {
+ e.Accepted = false;
+ return;
+ }
+
+ e.Accepted = _viewModel.SatisfiesFilter(plugin);
+ }
+}
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml
new file mode 100644
index 000000000..37079a46f
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs
new file mode 100644
index 000000000..d48505c3d
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPanePlugins.xaml.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Windows.Input;
+using System.Windows.Navigation;
+using Flow.Launcher.SettingPages.ViewModels;
+
+namespace Flow.Launcher.SettingPages.Views;
+
+public partial class SettingsPanePlugins
+{
+ private SettingsPanePluginsViewModel _viewModel = null!;
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ if (!IsInitialized)
+ {
+ if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings })
+ throw new ArgumentException("Settings are required for SettingsPaneHotkey.");
+ _viewModel = new SettingsPanePluginsViewModel(settings);
+ DataContext = _viewModel;
+ InitializeComponent();
+ }
+ base.OnNavigatedTo(e);
+ }
+
+ private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return;
+ PluginFilterTextbox.Focus();
+ }
+}
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml
new file mode 100644
index 000000000..63a43a8fb
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml.cs
new file mode 100644
index 000000000..95c88d627
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneProxy.xaml.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Windows.Navigation;
+using Flow.Launcher.SettingPages.ViewModels;
+
+namespace Flow.Launcher.SettingPages.Views;
+
+public partial class SettingsPaneProxy
+{
+ private SettingsPaneProxyViewModel _viewModel = null!;
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ if (!IsInitialized)
+ {
+ if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings, Updater: { } updater })
+ throw new ArgumentException($"Settings are required for {nameof(SettingsPaneProxy)}.");
+ _viewModel = new SettingsPaneProxyViewModel(settings, updater);
+ DataContext = _viewModel;
+ InitializeComponent();
+ }
+
+ base.OnNavigatedTo(e);
+ }
+}
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml
new file mode 100644
index 000000000..3b581767c
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml
@@ -0,0 +1,492 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs
new file mode 100644
index 000000000..93cf7ad18
--- /dev/null
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Windows.Controls;
+using System.Windows.Navigation;
+using Flow.Launcher.SettingPages.ViewModels;
+using Page = ModernWpf.Controls.Page;
+
+namespace Flow.Launcher.SettingPages.Views;
+
+public partial class SettingsPaneTheme : Page
+{
+ private SettingsPaneThemeViewModel _viewModel = null!;
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ if (!IsInitialized)
+ {
+ if (e.ExtraData is not SettingWindow.PaneData { Settings: { } settings })
+ throw new ArgumentException($"Settings are required for {nameof(SettingsPaneTheme)}.");
+ _viewModel = new SettingsPaneThemeViewModel(settings);
+ DataContext = _viewModel;
+ InitializeComponent();
+ }
+
+ base.OnNavigatedTo(e);
+ }
+
+ private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ _viewModel.UpdateColorScheme();
+ }
+}
diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml
index bc423bead..6194db201 100644
--- a/Flow.Launcher/SettingWindow.xaml
+++ b/Flow.Launcher/SettingWindow.xaml
@@ -2,18 +2,10 @@
x:Class="Flow.Launcher.SettingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
- xmlns:converters="clr-namespace:Flow.Launcher.Converters"
- xmlns:core="clr-namespace:Flow.Launcher.Core.Resource;assembly=Flow.Launcher.Core"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:flowlauncher="clr-namespace:Flow.Launcher"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
- xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.modernwpf.com/2019"
- xmlns:userSettings="clr-namespace:Flow.Launcher.Infrastructure.UserSettings;assembly=Flow.Launcher.Infrastructure"
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
- xmlns:wpftk="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
Title="{DynamicResource flowlauncher_settings}"
Width="{Binding SettingWindowWidth, Mode=TwoWay}"
Height="{Binding SettingWindowHeight, Mode=TwoWay}"
@@ -40,566 +32,162 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -609,2954 +197,72 @@
Grid.Row="0"
Width="50"
Height="50"
+ RenderOptions.BitmapScalingMode="HighQuality"
Source="images/app.png" />
+ TextAlignment="Center" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs
index 48491564e..957379ce4 100644
--- a/Flow.Launcher/SettingWindow.xaml.cs
+++ b/Flow.Launcher/SettingWindow.xaml.cs
@@ -1,515 +1,172 @@
-using Flow.Launcher.Core.Plugin;
-using Flow.Launcher.Core.Resource;
-using Flow.Launcher.Helper;
-using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Infrastructure.UserSettings;
-using Flow.Launcher.Plugin;
-using Flow.Launcher.ViewModel;
-using ModernWpf;
-using ModernWpf.Controls;
-using System;
-using System.ComponentModel;
-using System.IO;
+using System;
using System.Windows;
-using System.Windows.Data;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Interop;
-using System.Windows.Navigation;
-using NHotkey;
-using Button = System.Windows.Controls.Button;
-using Control = System.Windows.Controls.Control;
-using KeyEventArgs = System.Windows.Input.KeyEventArgs;
-using MessageBox = System.Windows.MessageBox;
+using Flow.Launcher.Core;
+using Flow.Launcher.Core.Configuration;
+using Flow.Launcher.Helper;
+using Flow.Launcher.Infrastructure.UserSettings;
+using Flow.Launcher.Plugin;
+using Flow.Launcher.SettingPages.Views;
+using Flow.Launcher.ViewModel;
+using ModernWpf.Controls;
using TextBox = System.Windows.Controls.TextBox;
-using ThemeManager = ModernWpf.ThemeManager;
-namespace Flow.Launcher
+namespace Flow.Launcher;
+
+public partial class SettingWindow
{
- public partial class SettingWindow
+ private readonly IPublicAPI _api;
+ private readonly Settings _settings;
+ private readonly SettingWindowViewModel _viewModel;
+
+ public SettingWindow(IPublicAPI api, SettingWindowViewModel viewModel)
{
- public readonly IPublicAPI API;
- private Settings settings;
- private SettingWindowViewModel viewModel;
+ _settings = viewModel.Settings;
+ DataContext = viewModel;
+ _viewModel = viewModel;
+ _api = api;
+ InitializePosition();
+ InitializeComponent();
+ NavView.SelectedItem = NavView.MenuItems[0]; /* Set First Page */
+ }
- public SettingWindow(IPublicAPI api, SettingWindowViewModel viewModel)
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ RefreshMaximizeRestoreButton();
+ // Fix (workaround) for the window freezes after lock screen (Win+L)
+ // https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf
+ HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource;
+ HwndTarget hwndTarget = hwndSource.CompositionTarget;
+ hwndTarget.RenderMode = RenderMode.Default;
+
+ InitializePosition();
+ }
+
+ private void OnClosed(object sender, EventArgs e)
+ {
+ _settings.SettingWindowState = WindowState;
+ _settings.SettingWindowTop = Top;
+ _settings.SettingWindowLeft = Left;
+ _viewModel.Save();
+ _api.SavePluginSettings();
+ }
+
+ private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */
+ {
+ if (Keyboard.FocusedElement is not TextBox textBox)
{
- settings = viewModel.Settings;
- DataContext = viewModel;
- this.viewModel = viewModel;
- API = api;
- InitializePosition();
- InitializeComponent();
+ return;
}
+ var tRequest = new TraversalRequest(FocusNavigationDirection.Next);
+ textBox.MoveFocus(tRequest);
+ }
- #region General
+ /* Custom TitleBar */
- private void OnLoaded(object sender, RoutedEventArgs e)
+ private void OnMinimizeButtonClick(object sender, RoutedEventArgs e)
+ {
+ WindowState = WindowState.Minimized;
+ }
+
+ private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)
+ {
+ WindowState = WindowState switch
{
- RefreshMaximizeRestoreButton();
- // Fix (workaround) for the window freezes after lock screen (Win+L)
- // https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf
- HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource;
- HwndTarget hwndTarget = hwndSource.CompositionTarget;
- hwndTarget.RenderMode = RenderMode.SoftwareOnly;
+ WindowState.Maximized => WindowState.Normal,
+ _ => WindowState.Maximized
+ };
+ }
- pluginListView = (CollectionView)CollectionViewSource.GetDefaultView(Plugins.ItemsSource);
- pluginListView.Filter = PluginListFilter;
+ private void OnCloseButtonClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
- pluginStoreView = (CollectionView)CollectionViewSource.GetDefaultView(StoreListBox.ItemsSource);
- pluginStoreView.Filter = PluginStoreFilter;
-
- viewModel.PropertyChanged += new PropertyChangedEventHandler(SettingsWindowViewModelChanged);
-
- CheckMediaPlayer();
- InitializePosition();
- }
-
- private void CheckMediaPlayer()
+ private void RefreshMaximizeRestoreButton()
+ {
+ if (WindowState == WindowState.Maximized)
{
- if (settings.WMPInstalled)
- {
- WMPWarning.Visibility = Visibility.Visible;
- VolumeAdjustCard.Visibility = Visibility.Collapsed;
- }
+ MaximizeButton.Visibility = Visibility.Collapsed;
+ RestoreButton.Visibility = Visibility.Visible;
}
-
- private void SettingsWindowViewModelChanged(object sender, PropertyChangedEventArgs e)
+ else
{
- if (e.PropertyName == nameof(viewModel.ExternalPlugins))
- {
- pluginStoreView = (CollectionView)CollectionViewSource.GetDefaultView(StoreListBox.ItemsSource);
- pluginStoreView.Filter = PluginStoreFilter;
- pluginStoreView.Refresh();
- }
- }
-
- private void OnSelectPythonPathClick(object sender, RoutedEventArgs e)
- {
- var selectedFile = viewModel.GetFileFromDialog(
- InternationalizationManager.Instance.GetTranslation("selectPythonExecutable"),
- "Python|pythonw.exe");
-
- if (!string.IsNullOrEmpty(selectedFile))
- settings.PluginSettings.PythonExecutablePath = selectedFile;
- }
-
- private void OnSelectNodePathClick(object sender, RoutedEventArgs e)
- {
- var selectedFile = viewModel.GetFileFromDialog(
- InternationalizationManager.Instance.GetTranslation("selectNodeExecutable"));
-
- if (!string.IsNullOrEmpty(selectedFile))
- settings.PluginSettings.NodeExecutablePath = selectedFile;
- }
-
- private void OnSelectFileManagerClick(object sender, RoutedEventArgs e)
- {
- SelectFileManagerWindow fileManagerChangeWindow = new SelectFileManagerWindow(settings);
- fileManagerChangeWindow.ShowDialog();
- }
-
- private void OnSelectDefaultBrowserClick(object sender, RoutedEventArgs e)
- {
- var browserWindow = new SelectBrowserWindow(settings);
- browserWindow.ShowDialog();
- }
-
- #endregion
-
- #region Hotkey
-
- private void OnToggleHotkey(object sender, HotkeyEventArgs e)
- {
- HotKeyMapper.OnToggleHotkey(sender, e);
- }
-
- private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e)
- {
- var item = viewModel.SelectedCustomPluginHotkey;
- if (item == null)
- {
- MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
- return;
- }
-
- string deleteWarning =
- string.Format(InternationalizationManager.Instance.GetTranslation("deleteCustomHotkeyWarning"),
- item.Hotkey);
- if (
- MessageBox.Show(deleteWarning, InternationalizationManager.Instance.GetTranslation("delete"),
- MessageBoxButton.YesNo) == MessageBoxResult.Yes)
- {
- settings.CustomPluginHotkeys.Remove(item);
- HotKeyMapper.RemoveHotkey(item.Hotkey);
- }
- }
-
- private void OnEditCustomHotkeyClick(object sender, RoutedEventArgs e)
- {
- var item = viewModel.SelectedCustomPluginHotkey;
- if (item != null)
- {
- CustomQueryHotkeySetting window = new CustomQueryHotkeySetting(this, settings);
- window.UpdateItem(item);
- window.ShowDialog();
- }
- else
- {
- MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
- }
- }
-
- private void OnAddCustomHotkeyClick(object sender, RoutedEventArgs e)
- {
- new CustomQueryHotkeySetting(this, settings).ShowDialog();
- }
-
- #endregion
-
- #region Plugin
-
- private void OnPluginToggled(object sender, RoutedEventArgs e)
- {
- var id = viewModel.SelectedPlugin.PluginPair.Metadata.ID;
- // used to sync the current status from the plugin manager into the setting to keep consistency after save
- settings.PluginSettings.Plugins[id].Disabled = viewModel.SelectedPlugin.PluginPair.Metadata.Disabled;
- }
-
- private void OnPluginPriorityClick(object sender, RoutedEventArgs e)
- {
- if (sender is Control { DataContext: PluginViewModel pluginViewModel })
- {
- PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(pluginViewModel.PluginPair.Metadata.ID, pluginViewModel);
- priorityChangeWindow.ShowDialog();
- }
- }
-
- #endregion
-
- #region Proxy
-
- private void OnTestProxyClick(object sender, RoutedEventArgs e)
- { // TODO: change to command
- var msg = viewModel.TestProxy();
- MessageBox.Show(msg); // TODO: add message box service
- }
-
- #endregion
-
- private void OnCheckUpdates(object sender, RoutedEventArgs e)
- {
- viewModel.UpdateApp(); // TODO: change to command
- }
-
- private void OnRequestNavigate(object sender, RequestNavigateEventArgs e)
- {
- API.OpenUrl(e.Uri.AbsoluteUri);
- e.Handled = true;
- }
-
- private void OnClosed(object sender, EventArgs e)
- {
- settings.SettingWindowState = WindowState;
- settings.SettingWindowTop = Top;
- settings.SettingWindowLeft = Left;
- viewModel.Save();
- API.SavePluginSettings();
- }
-
- private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e)
- {
- Close();
- }
-
- private void OpenThemeFolder(object sender, RoutedEventArgs e)
- {
- PluginManager.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes));
- }
-
- private void OpenSettingFolder(object sender, RoutedEventArgs e)
- {
- PluginManager.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Settings));
- }
-
- private void OpenWelcomeWindow(object sender, RoutedEventArgs e)
- {
- var WelcomeWindow = new WelcomeWindow(settings);
- WelcomeWindow.ShowDialog();
- }
- private void OpenLogFolder(object sender, RoutedEventArgs e)
- {
- viewModel.OpenLogFolder();
- }
- private void ClearLogFolder(object sender, RoutedEventArgs e)
- {
- var confirmResult = MessageBox.Show(
- InternationalizationManager.Instance.GetTranslation("clearlogfolderMessage"),
- InternationalizationManager.Instance.GetTranslation("clearlogfolder"),
- MessageBoxButton.YesNo);
-
- if (confirmResult == MessageBoxResult.Yes)
- {
- viewModel.ClearLogFolder();
- }
- }
-
- private void OnExternalPluginInstallClick(object sender, RoutedEventArgs e)
- {
- if (sender is not Button { DataContext: PluginStoreItemViewModel plugin } button)
- {
- return;
- }
-
- if (storeClickedButton != null)
- {
- FlyoutService.GetFlyout(storeClickedButton).Hide();
- }
-
- viewModel.DisplayPluginQuery($"install {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
- }
-
- private void OnExternalPluginUninstallClick(object sender, MouseButtonEventArgs e)
- {
- if (e.ChangedButton == MouseButton.Left)
- {
- var name = viewModel.SelectedPlugin.PluginPair.Metadata.Name;
- viewModel.DisplayPluginQuery($"uninstall {name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
- }
- }
-
- private void OnExternalPluginUninstallClick(object sender, RoutedEventArgs e)
- {
- if (storeClickedButton != null)
- {
- FlyoutService.GetFlyout(storeClickedButton).Hide();
- }
-
- if (sender is Button { DataContext: PluginStoreItemViewModel plugin })
- viewModel.DisplayPluginQuery($"uninstall {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
-
- }
-
- private void OnExternalPluginUpdateClick(object sender, RoutedEventArgs e)
- {
- if (storeClickedButton != null)
- {
- FlyoutService.GetFlyout(storeClickedButton).Hide();
- }
- if (sender is Button { DataContext: PluginStoreItemViewModel plugin })
- viewModel.DisplayPluginQuery($"update {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
-
- }
-
- private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */
- {
- if (Keyboard.FocusedElement is not TextBox textBox)
- {
- return;
- }
- var tRequest = new TraversalRequest(FocusNavigationDirection.Next);
- textBox.MoveFocus(tRequest);
- }
-
- private void ColorSchemeSelectedIndexChanged(object sender, EventArgs e)
- => ThemeManager.Current.ApplicationTheme = settings.ColorScheme switch
- {
- Constant.Light => ApplicationTheme.Light,
- Constant.Dark => ApplicationTheme.Dark,
- Constant.System => null,
- _ => ThemeManager.Current.ApplicationTheme
- };
-
- /* Custom TitleBar */
-
- private void OnMinimizeButtonClick(object sender, RoutedEventArgs e)
- {
- WindowState = WindowState.Minimized;
- }
-
- private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)
- {
- WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
- }
-
- private void OnCloseButtonClick(object sender, RoutedEventArgs e)
- {
-
- Close();
- }
-
- private void RefreshMaximizeRestoreButton()
- {
- if (WindowState == WindowState.Maximized)
- {
- maximizeButton.Visibility = Visibility.Collapsed;
- restoreButton.Visibility = Visibility.Visible;
- }
- else
- {
- maximizeButton.Visibility = Visibility.Visible;
- restoreButton.Visibility = Visibility.Collapsed;
- }
- }
-
- private void Window_StateChanged(object sender, EventArgs e)
- {
- RefreshMaximizeRestoreButton();
- }
-
- #region Shortcut
-
- private void OnDeleteCustomShortCutClick(object sender, RoutedEventArgs e)
- {
- viewModel.DeleteSelectedCustomShortcut();
- }
-
- private void OnEditCustomShortCutClick(object sender, RoutedEventArgs e)
- {
- if (viewModel.EditSelectedCustomShortcut())
- {
- //customShortcutView.Items.Refresh(); Should Fix
- }
- }
-
- private void OnAddCustomShortCutClick(object sender, RoutedEventArgs e)
- {
- viewModel.AddCustomShortcut();
- }
-
- #endregion
-
- private CollectionView pluginListView;
- private CollectionView pluginStoreView;
-
- private bool PluginListFilter(object item)
- {
- if (string.IsNullOrEmpty(pluginFilterTxb.Text))
- return true;
- if (item is PluginViewModel model)
- {
- return StringMatcher.FuzzySearch(pluginFilterTxb.Text, model.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet();
- }
- return false;
- }
-
- private bool PluginStoreFilter(object item)
- {
- if (string.IsNullOrEmpty(pluginStoreFilterTxb.Text))
- return true;
- if (item is PluginStoreItemViewModel model)
- {
- return StringMatcher.FuzzySearch(pluginStoreFilterTxb.Text, model.Name).IsSearchPrecisionScoreMet()
- || StringMatcher.FuzzySearch(pluginStoreFilterTxb.Text, model.Description).IsSearchPrecisionScoreMet();
- }
- return false;
- }
-
- private string lastPluginListSearch = "";
- private string lastPluginStoreSearch = "";
-
- private void RefreshPluginListEventHandler(object sender, RoutedEventArgs e)
- {
- if (pluginFilterTxb.Text != lastPluginListSearch)
- {
- lastPluginListSearch = pluginFilterTxb.Text;
- pluginListView.Refresh();
- }
- }
-
- private void RefreshPluginStoreEventHandler(object sender, RoutedEventArgs e)
- {
- if (pluginStoreFilterTxb.Text != lastPluginStoreSearch)
- {
- lastPluginStoreSearch = pluginStoreFilterTxb.Text;
- pluginStoreView.Refresh();
- }
- }
-
- private void PluginFilterTxb_OnKeyDown(object sender, KeyEventArgs e)
- {
- if (e.Key == Key.Enter)
- RefreshPluginListEventHandler(sender, e);
- }
-
- private void PluginStoreFilterTxb_OnKeyDown(object sender, KeyEventArgs e)
- {
- if (e.Key == Key.Enter)
- RefreshPluginStoreEventHandler(sender, e);
- }
-
- private void OnPluginSettingKeydown(object sender, KeyEventArgs e)
- {
- if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && e.Key == Key.F)
- pluginFilterTxb.Focus();
- }
-
- private void PluginStore_OnKeyDown(object sender, KeyEventArgs e)
- {
- if (e.Key == Key.F && (Keyboard.Modifiers & ModifierKeys.Control) != 0)
- {
- pluginStoreFilterTxb.Focus();
- }
- }
-
- public void InitializePosition()
- {
- if (settings.SettingWindowTop >= 0 && settings.SettingWindowLeft >= 0)
- {
- Top = settings.SettingWindowTop;
- Left = settings.SettingWindowLeft;
- }
- else
- {
- Top = WindowTop();
- Left = WindowLeft();
- }
- WindowState = settings.SettingWindowState;
- }
-
- public double WindowLeft()
- {
- var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
- var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
- var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
- var left = (dip2.X - this.ActualWidth) / 2 + dip1.X;
- return left;
- }
-
- public double WindowTop()
- {
- var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
- var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
- var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height);
- var top = (dip2.Y - this.ActualHeight) / 2 + dip1.Y - 20;
- return top;
- }
-
- private Button storeClickedButton;
-
- private void StoreListItem_Click(object sender, RoutedEventArgs e)
- {
- if (sender is not Button button)
- return;
-
- storeClickedButton = button;
-
- var flyout = FlyoutService.GetFlyout(button);
- flyout.Closed += (_, _) =>
- {
- storeClickedButton = null;
- };
-
- }
-
- private void PluginStore_GotFocus(object sender, RoutedEventArgs e)
- {
- Keyboard.Focus(pluginStoreFilterTxb);
- }
-
- private void Plugin_GotFocus(object sender, RoutedEventArgs e)
- {
- Keyboard.Focus(pluginFilterTxb);
+ MaximizeButton.Visibility = Visibility.Visible;
+ RestoreButton.Visibility = Visibility.Collapsed;
}
}
+
+ private void Window_StateChanged(object sender, EventArgs e)
+ {
+ RefreshMaximizeRestoreButton();
+ }
+
+ public void InitializePosition()
+ {
+ if (_settings.SettingWindowTop == null)
+ {
+ Top = WindowTop();
+ Left = WindowLeft();
+ }
+ else
+ {
+ Top = _settings.SettingWindowTop;
+ Left = _settings.SettingWindowLeft;
+ }
+ WindowState = _settings.SettingWindowState;
+ }
+
+ private double WindowLeft()
+ {
+ var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
+ var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
+ var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
+ var left = (dip2.X - this.ActualWidth) / 2 + dip1.X;
+ return left;
+ }
+
+ private double WindowTop()
+ {
+ var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position);
+ var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
+ var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height);
+ var top = (dip2.Y - this.ActualHeight) / 2 + dip1.Y - 20;
+ return top;
+ }
+
+ private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
+ {
+ var paneData = new PaneData(_settings, _viewModel.Updater, _viewModel.Portable);
+ if (args.IsSettingsSelected)
+ {
+ ContentFrame.Navigate(typeof(SettingsPaneGeneral), paneData);
+ }
+ else
+ {
+ var selectedItem = (NavigationViewItem)args.SelectedItem;
+ if (selectedItem == null) return;
+
+ var pageType = selectedItem.Name switch
+ {
+ nameof(General) => typeof(SettingsPaneGeneral),
+ nameof(Plugins) => typeof(SettingsPanePlugins),
+ nameof(PluginStore) => typeof(SettingsPanePluginStore),
+ nameof(Theme) => typeof(SettingsPaneTheme),
+ nameof(Hotkey) => typeof(SettingsPaneHotkey),
+ nameof(Proxy) => typeof(SettingsPaneProxy),
+ nameof(About) => typeof(SettingsPaneAbout),
+ _ => typeof(SettingsPaneGeneral)
+ };
+ ContentFrame.Navigate(pageType, paneData);
+ }
+ }
+
+ public record PaneData(Settings Settings, Updater Updater, IPortable Portable);
}
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index 67efc6a86..01dc67b98 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -42,6 +42,7 @@ namespace Flow.Launcher.ViewModel
private readonly FlowLauncherJsonStorage _userSelectedRecordStorage;
private readonly FlowLauncherJsonStorage _topMostRecordStorage;
private readonly History _history;
+ private int lastHistoryIndex = 1;
private readonly UserSelectedRecord _userSelectedRecord;
private readonly TopMostRecord _topMostRecord;
@@ -83,6 +84,12 @@ namespace Flow.Launcher.ViewModel
case nameof(Settings.AutoCompleteHotkey):
OnPropertyChanged(nameof(AutoCompleteHotkey));
break;
+ case nameof(Settings.CycleHistoryUpHotkey):
+ OnPropertyChanged(nameof(CycleHistoryUpHotkey));
+ break;
+ case nameof(Settings.CycleHistoryDownHotkey):
+ OnPropertyChanged(nameof(CycleHistoryDownHotkey));
+ break;
case nameof(Settings.AutoCompleteHotkey2):
OnPropertyChanged(nameof(AutoCompleteHotkey2));
break;
@@ -256,6 +263,32 @@ namespace Flow.Launcher.ViewModel
}
}
+ [RelayCommand]
+ public void ReverseHistory()
+ {
+ if (_history.Items.Count > 0)
+ {
+ ChangeQueryText(_history.Items[_history.Items.Count - lastHistoryIndex].Query.ToString());
+ if (lastHistoryIndex < _history.Items.Count)
+ {
+ lastHistoryIndex++;
+ }
+ }
+ }
+
+ [RelayCommand]
+ public void ForwardHistory()
+ {
+ if (_history.Items.Count > 0)
+ {
+ ChangeQueryText(_history.Items[_history.Items.Count - lastHistoryIndex].Query.ToString());
+ if (lastHistoryIndex > 1)
+ {
+ lastHistoryIndex--;
+ }
+ }
+ }
+
[RelayCommand]
private void LoadContextMenu()
{
@@ -346,6 +379,7 @@ namespace Flow.Launcher.ViewModel
{
_userSelectedRecord.Add(result);
_history.Add(result.OriginQuery.RawQuery);
+ lastHistoryIndex = 1;
}
if (hideWindow)
@@ -394,7 +428,18 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void SelectPrevItem()
{
- SelectedResults.SelectPrevResult();
+ if (_history.Items.Count > 0
+ && QueryText == string.Empty
+ && SelectedIsFromQueryResults())
+ {
+ lastHistoryIndex = 1;
+ ReverseHistory();
+ }
+ else
+ {
+ SelectedResults.SelectPrevResult();
+ }
+
}
[RelayCommand]
@@ -690,6 +735,8 @@ namespace Flow.Launcher.ViewModel
public string SelectPrevPageHotkey => VerifyOrSetDefaultHotkey(Settings.SelectPrevPageHotkey, "");
public string OpenContextMenuHotkey => VerifyOrSetDefaultHotkey(Settings.OpenContextMenuHotkey, "Ctrl+O");
public string SettingWindowHotkey => VerifyOrSetDefaultHotkey(Settings.SettingWindowHotkey, "Ctrl+I");
+ public string CycleHistoryUpHotkey => VerifyOrSetDefaultHotkey(Settings.CycleHistoryUpHotkey, "Alt+Up");
+ public string CycleHistoryDownHotkey => VerifyOrSetDefaultHotkey(Settings.CycleHistoryDownHotkey, "Alt+Down");
public string Image => Constant.QueryTextBoxIconImagePath;
@@ -1116,6 +1163,7 @@ namespace Flow.Launcher.ViewModel
public async void Hide()
{
+ lastHistoryIndex = 1;
// Trick for no delay
MainWindowOpacity = 0;
lastContextMenuResult = new Result();
diff --git a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs
index bc98efabc..513d0443f 100644
--- a/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs
+++ b/Flow.Launcher/ViewModel/PluginStoreItemViewModel.cs
@@ -1,12 +1,15 @@
using System;
+using System.Linq;
+using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.ViewModel
{
- public class PluginStoreItemViewModel : BaseModel
+ public partial class PluginStoreItemViewModel : BaseModel
{
+ private PluginPair PluginManagerData => PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7");
public PluginStoreItemViewModel(UserPlugin plugin)
{
_plugin = plugin;
@@ -26,19 +29,13 @@ namespace Flow.Launcher.ViewModel
public string IcoPath => _plugin.IcoPath;
public bool LabelInstalled => PluginManager.GetPluginForId(_plugin.ID) != null;
- public bool LabelUpdate => LabelInstalled && VersionConvertor(_plugin.Version) > VersionConvertor(PluginManager.GetPluginForId(_plugin.ID).Metadata.Version);
+ public bool LabelUpdate => LabelInstalled && new Version(_plugin.Version) > new Version(PluginManager.GetPluginForId(_plugin.ID).Metadata.Version);
internal const string None = "None";
internal const string RecentlyUpdated = "RecentlyUpdated";
internal const string NewRelease = "NewRelease";
internal const string Installed = "Installed";
- public Version VersionConvertor(string version)
- {
- Version ResultVersion = new Version(version);
- return ResultVersion;
- }
-
public string Category
{
get
@@ -60,5 +57,13 @@ namespace Flow.Launcher.ViewModel
return category;
}
}
+
+ [RelayCommand]
+ private void ShowCommandQuery(string action)
+ {
+ var actionKeyword = PluginManagerData.Metadata.ActionKeywords.Any() ? PluginManagerData.Metadata.ActionKeywords[0] + " " : String.Empty;
+ App.API.ChangeQuery($"{actionKeyword}{action} {_plugin.Name}");
+ App.API.ShowMainWindow();
+ }
}
}
diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs
index d2919507d..b4daa8c7a 100644
--- a/Flow.Launcher/ViewModel/PluginViewModel.cs
+++ b/Flow.Launcher/ViewModel/PluginViewModel.cs
@@ -1,4 +1,5 @@
-using System.Windows;
+using System.Linq;
+using System.Windows;
using System.Windows.Media;
using Flow.Launcher.Plugin;
using Flow.Launcher.Infrastructure.Image;
@@ -26,6 +27,21 @@ namespace Flow.Launcher.ViewModel
}
}
+ private string PluginManagerActionKeyword
+ {
+ get
+ {
+ var keyword = PluginManager
+ .GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")
+ .Metadata.ActionKeywords.FirstOrDefault();
+ return keyword switch
+ {
+ null or "*" => string.Empty,
+ _ => keyword
+ };
+ }
+ }
+
private async void LoadIconAsync()
{
@@ -46,7 +62,11 @@ namespace Flow.Launcher.ViewModel
public bool PluginState
{
get => !PluginPair.Metadata.Disabled;
- set => PluginPair.Metadata.Disabled = !value;
+ set
+ {
+ PluginPair.Metadata.Disabled = !value;
+ PluginSettingsObject.Disabled = !value;
+ }
}
public bool IsExpanded
{
@@ -62,11 +82,13 @@ namespace Flow.Launcher.ViewModel
private Control _settingControl;
private bool _isExpanded;
+
+ public bool HasSettingControl => PluginPair.Plugin is ISettingProvider;
public Control SettingControl
=> IsExpanded
? _settingControl
??= PluginPair.Plugin is not ISettingProvider settingProvider
- ? new Control()
+ ? null
: settingProvider.CreateSettingPanel()
: null;
private ImageSource _image = ImageLoader.MissingImage;
@@ -78,6 +100,7 @@ namespace Flow.Launcher.ViewModel
public string InitAndQueryTime => InternationalizationManager.Instance.GetTranslation("plugin_init_time") + " " + PluginPair.Metadata.InitTime + "ms, " + InternationalizationManager.Instance.GetTranslation("plugin_query_time") + " " + PluginPair.Metadata.AvgQueryTime + "ms";
public string ActionKeywordsText => string.Join(Query.ActionKeywordSeparator, PluginPair.Metadata.ActionKeywords);
public int Priority => PluginPair.Metadata.Priority;
+ public Infrastructure.UserSettings.Plugin PluginSettingsObject { get; set; }
public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword)
{
@@ -88,13 +111,14 @@ namespace Flow.Launcher.ViewModel
public void ChangePriority(int newPriority)
{
PluginPair.Metadata.Priority = newPriority;
+ PluginSettingsObject.Priority = newPriority;
OnPropertyChanged(nameof(Priority));
}
[RelayCommand]
private void EditPluginPriority()
{
- PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(PluginPair.Metadata.ID, this);
+ PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(PluginPair. Metadata.ID, this);
priorityChangeWindow.ShowDialog();
}
@@ -106,6 +130,19 @@ namespace Flow.Launcher.ViewModel
PluginManager.API.OpenDirectory(directory);
}
+ [RelayCommand]
+ private void OpenSourceCodeLink()
+ {
+ PluginManager.API.OpenUrl(PluginPair.Metadata.Website);
+ }
+
+ [RelayCommand]
+ private void OpenDeletePluginWindow()
+ {
+ PluginManager.API.ChangeQuery($"{PluginManagerActionKeyword} uninstall {PluginPair.Metadata.Name}".Trim(), true);
+ PluginManager.API.ShowMainWindow();
+ }
+
public static bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
[RelayCommand]
diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
index fe1ea4e7b..481802045 100644
--- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
+++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs
@@ -1,1006 +1,66 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using Flow.Launcher.Core;
+using Flow.Launcher.Core;
using Flow.Launcher.Core.Configuration;
-using Flow.Launcher.Core.ExternalPlugins;
-using Flow.Launcher.Core.Plugin;
-using Flow.Launcher.Core.Resource;
-using Flow.Launcher.Helper;
-using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
-using Flow.Launcher.Plugin.SharedModels;
-using System.Collections.ObjectModel;
-using CommunityToolkit.Mvvm.Input;
-using System.Globalization;
-using System.Runtime.CompilerServices;
-using Flow.Launcher.Infrastructure.Hotkey;
-namespace Flow.Launcher.ViewModel
+namespace Flow.Launcher.ViewModel;
+
+public class SettingWindowViewModel : BaseModel
{
- public partial class SettingWindowViewModel : BaseModel
+ private readonly FlowLauncherJsonStorage _storage;
+
+ public Updater Updater { get; }
+
+ public IPortable Portable { get; }
+
+ public Settings Settings { get; }
+
+ public SettingWindowViewModel(Updater updater, IPortable portable)
{
- private readonly Updater _updater;
- private readonly IPortable _portable;
- private readonly FlowLauncherJsonStorage _storage;
-
- public SettingWindowViewModel(Updater updater, IPortable portable)
- {
- _updater = updater;
- _portable = portable;
- _storage = new FlowLauncherJsonStorage();
- Settings = _storage.Load();
- Settings.PropertyChanged += (s, e) =>
- {
- switch (e.PropertyName)
- {
- case nameof(Settings.ActivateTimes):
- OnPropertyChanged(nameof(ActivatedTimes));
- break;
- case nameof(Settings.WindowSize):
- OnPropertyChanged(nameof(WindowWidthSize));
- break;
- case nameof(Settings.UseDate):
- case nameof(Settings.DateFormat):
- OnPropertyChanged(nameof(DateText));
- break;
- case nameof(Settings.UseClock):
- case nameof(Settings.TimeFormat):
- OnPropertyChanged(nameof(ClockText));
- break;
- case nameof(Settings.Language):
- OnPropertyChanged(nameof(ClockText));
- OnPropertyChanged(nameof(DateText));
- OnPropertyChanged(nameof(AlwaysPreviewToolTip));
- break;
- case nameof(Settings.PreviewHotkey):
- OnPropertyChanged(nameof(AlwaysPreviewToolTip));
- break;
- case nameof(Settings.SoundVolume):
- OnPropertyChanged(nameof(SoundEffectVolume));
- break;
- }
- };
- }
-
- [RelayCommand]
- public void SetTogglingHotkey(HotkeyModel hotkey)
- {
- HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey);
- }
-
- public Settings Settings { get; set; }
-
- public async void UpdateApp()
- {
- await _updater.UpdateAppAsync(App.API, false);
- }
-
- public bool AutoUpdates
- {
- get => Settings.AutoUpdates;
- set
- {
- Settings.AutoUpdates = value;
-
- if (value)
- {
- UpdateApp();
- }
- }
- }
-
- public CultureInfo Culture => CultureInfo.DefaultThreadCurrentCulture;
-
- public bool StartFlowLauncherOnSystemStartup
- {
- get => Settings.StartFlowLauncherOnSystemStartup;
- set
- {
- Settings.StartFlowLauncherOnSystemStartup = value;
-
- try
- {
- if (value)
- AutoStartup.Enable();
- else
- AutoStartup.Disable();
- }
- catch (Exception e)
- {
- Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
- e.Message);
- }
- }
- }
-
- // This is only required to set at startup. When portable mode enabled/disabled a restart is always required
- private bool _portableMode = DataLocation.PortableDataLocationInUse();
-
- public bool PortableMode
- {
- get => _portableMode;
- set
- {
- if (!_portable.CanUpdatePortability())
- return;
-
- if (DataLocation.PortableDataLocationInUse())
- {
- _portable.DisablePortableMode();
- }
- else
- {
- _portable.EnablePortableMode();
- }
- }
- }
-
- ///
- /// Save Flow settings. Plugins settings are not included.
- ///
- public void Save()
- {
- foreach (var vm in PluginViewModels)
- {
- var id = vm.PluginPair.Metadata.ID;
-
- Settings.PluginSettings.Plugins[id].Disabled = vm.PluginPair.Metadata.Disabled;
- Settings.PluginSettings.Plugins[id].Priority = vm.Priority;
- }
-
- _storage.Save();
- }
-
- public string GetFileFromDialog(string title, string filter = "")
- {
- var dlg = new System.Windows.Forms.OpenFileDialog
- {
- InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
- Multiselect = false,
- CheckFileExists = true,
- CheckPathExists = true,
- Title = title,
- Filter = filter
- };
-
- var result = dlg.ShowDialog();
- if (result == System.Windows.Forms.DialogResult.OK)
- {
- return dlg.FileName;
- }
- else
- {
- return string.Empty;
- }
- }
-
- #region general
-
- // todo a better name?
- public class LastQueryMode : BaseModel
- {
- public string Display { get; set; }
- public Infrastructure.UserSettings.LastQueryMode Value { get; set; }
- }
-
- private List _lastQueryModes = new List();
-
- public List LastQueryModes
- {
- get
- {
- if (_lastQueryModes.Count == 0)
- {
- _lastQueryModes = InitLastQueryModes();
- }
-
- return _lastQueryModes;
- }
- }
-
- private List InitLastQueryModes()
- {
- var modes = new List();
- var enums = (Infrastructure.UserSettings.LastQueryMode[])Enum.GetValues(
- typeof(Infrastructure.UserSettings.LastQueryMode));
- foreach (var e in enums)
- {
- var key = $"LastQuery{e}";
- var display = _translater.GetTranslation(key);
- var m = new LastQueryMode { Display = display, Value = e, };
- modes.Add(m);
- }
-
- return modes;
- }
-
- private void UpdateLastQueryModeDisplay()
- {
- foreach (var item in LastQueryModes)
- {
- item.Display = _translater.GetTranslation($"LastQuery{item.Value}");
- }
- }
-
- public string Language
- {
- get
- {
- return Settings.Language;
- }
- set
- {
- InternationalizationManager.Instance.ChangeLanguage(value);
-
- if (InternationalizationManager.Instance.PromptShouldUsePinyin(value))
- ShouldUsePinyin = true;
-
- UpdateLastQueryModeDisplay();
- }
- }
-
- public bool ShouldUsePinyin
- {
- get
- {
- return Settings.ShouldUsePinyin;
- }
- set
- {
- Settings.ShouldUsePinyin = value;
- }
- }
-
- public List QuerySearchPrecisionStrings
- {
- get
- {
- var precisionStrings = new List();
-
- var enumList = Enum.GetValues(typeof(SearchPrecisionScore)).Cast().ToList();
-
- enumList.ForEach(x => precisionStrings.Add(x.ToString()));
-
- return precisionStrings;
- }
- }
-
- public List OpenResultModifiersList => new List
- {
- KeyConstant.Alt, KeyConstant.Ctrl, $"{KeyConstant.Ctrl}+{KeyConstant.Alt}"
- };
-
- private Internationalization _translater => InternationalizationManager.Instance;
- public List Languages => _translater.LoadAvailableLanguages();
- public IEnumerable MaxResultsRange => Enumerable.Range(2, 16);
-
- public string AlwaysPreviewToolTip =>
- string.Format(_translater.GetTranslation("AlwaysPreviewToolTip"), Settings.PreviewHotkey);
-
- public string TestProxy()
- {
- var proxyServer = Settings.Proxy.Server;
- var proxyUserName = Settings.Proxy.UserName;
- if (string.IsNullOrEmpty(proxyServer))
- {
- return InternationalizationManager.Instance.GetTranslation("serverCantBeEmpty");
- }
-
- if (Settings.Proxy.Port <= 0)
- {
- return InternationalizationManager.Instance.GetTranslation("portCantBeEmpty");
- }
-
- HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository);
-
- if (string.IsNullOrEmpty(proxyUserName) || string.IsNullOrEmpty(Settings.Proxy.Password))
- {
- request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port);
- }
- else
- {
- request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port)
- {
- Credentials = new NetworkCredential(proxyUserName, Settings.Proxy.Password)
- };
- }
-
- try
- {
- var response = (HttpWebResponse)request.GetResponse();
- if (response.StatusCode == HttpStatusCode.OK)
- {
- return InternationalizationManager.Instance.GetTranslation("proxyIsCorrect");
- }
- else
- {
- return InternationalizationManager.Instance.GetTranslation("proxyConnectFailed");
- }
- }
- catch
- {
- return InternationalizationManager.Instance.GetTranslation("proxyConnectFailed");
- }
- }
-
- #endregion
-
- #region plugin
-
- public static string Plugin => @"https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest";
- public PluginViewModel SelectedPlugin { get; set; }
-
- public IList PluginViewModels
- {
- get => PluginManager.AllPlugins
- .OrderBy(x => x.Metadata.Disabled)
- .ThenBy(y => y.Metadata.Name)
- .Select(p => new PluginViewModel { PluginPair = p })
- .ToList();
- }
-
- public IList ExternalPlugins
- {
- get
- {
- return LabelMaker(PluginsManifest.UserPlugins);
- }
- }
-
- private IList LabelMaker(IList list)
- {
- return list.Select(p => new PluginStoreItemViewModel(p))
- .OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease)
- .ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated)
- .ThenByDescending(p => p.Category == PluginStoreItemViewModel.None)
- .ThenByDescending(p => p.Category == PluginStoreItemViewModel.Installed)
- .ToList();
- }
-
- public Control SettingProvider
- {
- get
- {
- var settingProvider = SelectedPlugin.PluginPair.Plugin as ISettingProvider;
- if (settingProvider != null)
- {
- var control = settingProvider.CreateSettingPanel();
- control.HorizontalAlignment = HorizontalAlignment.Stretch;
- control.VerticalAlignment = VerticalAlignment.Stretch;
- return control;
- }
- else
- {
- return new Control();
- }
- }
- }
-
- [RelayCommand]
- private async Task RefreshExternalPluginsAsync()
- {
- await PluginsManifest.UpdateManifestAsync();
- OnPropertyChanged(nameof(ExternalPlugins));
- }
-
-
- internal void DisplayPluginQuery(string queryToDisplay, PluginPair plugin, int actionKeywordPosition = 0)
- {
- var actionKeyword = plugin.Metadata.ActionKeywords.Count == 0
- ? string.Empty
- : plugin.Metadata.ActionKeywords[actionKeywordPosition];
-
- App.API.ChangeQuery($"{actionKeyword} {queryToDisplay}");
- App.API.ShowMainWindow();
- }
-
- #endregion
-
- #region theme
-
- public static string Theme => @"https://flowlauncher.com/docs/#/how-to-create-a-theme";
- public static string ThemeGallery => @"https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438";
-
- public string SelectedTheme
- {
- get { return Settings.Theme; }
- set
- {
- ThemeManager.Instance.ChangeTheme(value);
-
- if (ThemeManager.Instance.BlurEnabled && Settings.UseDropShadowEffect)
- DropShadowEffect = false;
- }
- }
-
- public List Themes
- => ThemeManager.Instance.LoadAvailableThemes().Select(Path.GetFileNameWithoutExtension).ToList();
-
- public bool DropShadowEffect
- {
- get { return Settings.UseDropShadowEffect; }
- set
- {
- if (ThemeManager.Instance.BlurEnabled && value)
- {
- MessageBox.Show(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed"));
- return;
- }
-
- if (value)
- {
- ThemeManager.Instance.AddDropShadowEffectToCurrentTheme();
- }
- else
- {
- ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme();
- }
-
- Settings.UseDropShadowEffect = value;
- }
- }
-
- public class ColorScheme
- {
- public string Display { get; set; }
- public ColorSchemes Value { get; set; }
- }
-
- public List ColorSchemes
- {
- get
- {
- List modes = new List();
- var enums = (ColorSchemes[])Enum.GetValues(typeof(ColorSchemes));
- foreach (var e in enums)
- {
- var key = $"ColorScheme{e}";
- var display = _translater.GetTranslation(key);
- var m = new ColorScheme { Display = display, Value = e, };
- modes.Add(m);
- }
-
- return modes;
- }
- }
-
- public class SearchWindowScreen
- {
- public string Display { get; set; }
- public SearchWindowScreens Value { get; set; }
- }
-
- public List SearchWindowScreens
- {
- get
- {
- List modes = new List();
- var enums = (SearchWindowScreens[])Enum.GetValues(typeof(SearchWindowScreens));
- foreach (var e in enums)
- {
- var key = $"SearchWindowScreen{e}";
- var display = _translater.GetTranslation(key);
- var m = new SearchWindowScreen { Display = display, Value = e, };
- modes.Add(m);
- }
-
- return modes;
- }
- }
-
- public class SearchWindowAlign
- {
- public string Display { get; set; }
- public SearchWindowAligns Value { get; set; }
- }
-
- public List SearchWindowAligns
- {
- get
- {
- List modes = new List();
- var enums = (SearchWindowAligns[])Enum.GetValues(typeof(SearchWindowAligns));
- foreach (var e in enums)
- {
- var key = $"SearchWindowAlign{e}";
- var display = _translater.GetTranslation(key);
- var m = new SearchWindowAlign { Display = display, Value = e, };
- modes.Add(m);
- }
-
- return modes;
- }
- }
-
- public List ScreenNumbers
- {
- get
- {
- var screens = System.Windows.Forms.Screen.AllScreens;
- var screenNumbers = new List();
- for (int i = 1; i <= screens.Length; i++)
- {
- screenNumbers.Add(i);
- }
-
- return screenNumbers;
- }
- }
-
- public List TimeFormatList { get; } = new()
- {
- "h:mm",
- "hh:mm",
- "H:mm",
- "HH:mm",
- "tt h:mm",
- "tt hh:mm",
- "h:mm tt",
- "hh:mm tt",
- "hh:mm:ss tt",
- "HH:mm:ss"
- };
-
- public List DateFormatList { get; } = new()
- {
- "MM'/'dd dddd",
- "MM'/'dd ddd",
- "MM'/'dd",
- "MM'-'dd",
- "MMMM', 'dd",
- "dd'/'MM",
- "dd'-'MM",
- "ddd MM'/'dd",
- "dddd MM'/'dd",
- "dddd",
- "ddd dd'/'MM",
- "dddd dd'/'MM",
- "dddd dd', 'MMMM",
- "dd', 'MMMM"
- };
-
- public string TimeFormat
- {
- get => Settings.TimeFormat;
- set => Settings.TimeFormat = value;
- }
-
- public string DateFormat
- {
- get => Settings.DateFormat;
- set => Settings.DateFormat = value;
- }
-
- public string ClockText => DateTime.Now.ToString(TimeFormat, Culture);
-
- public string DateText => DateTime.Now.ToString(DateFormat, Culture);
-
- public double WindowWidthSize
- {
- get => Settings.WindowSize;
- set => Settings.WindowSize = value;
- }
-
- public bool UseGlyphIcons
- {
- get => Settings.UseGlyphIcons;
- set => Settings.UseGlyphIcons = value;
- }
-
- public bool UseAnimation
- {
- get => Settings.UseAnimation;
- set => Settings.UseAnimation = value;
- }
-
- public class AnimationSpeed
- {
- public string Display { get; set; }
- public AnimationSpeeds Value { get; set; }
- }
-
- public List AnimationSpeeds
- {
- get
- {
- List speeds = new List();
- var enums = (AnimationSpeeds[])Enum.GetValues(typeof(AnimationSpeeds));
- foreach (var e in enums)
- {
- var key = $"AnimationSpeed{e}";
- var display = _translater.GetTranslation(key);
- var m = new AnimationSpeed { Display = display, Value = e, };
- speeds.Add(m);
- }
-
- return speeds;
- }
- }
-
- public bool UseSound
- {
- get => Settings.UseSound;
- set => Settings.UseSound = value;
- }
-
- public double SoundEffectVolume
- {
- get => Settings.SoundVolume;
- set => Settings.SoundVolume = value;
- }
-
- public bool UseClock
- {
- get => Settings.UseClock;
- set => Settings.UseClock = value;
- }
-
- public bool UseDate
- {
- get => Settings.UseDate;
- set => Settings.UseDate = value;
- }
-
- public double SettingWindowWidth
- {
- get => Settings.SettingWindowWidth;
- set => Settings.SettingWindowWidth = value;
- }
-
- public double SettingWindowHeight
- {
- get => Settings.SettingWindowHeight;
- set => Settings.SettingWindowHeight = value;
- }
-
- public double SettingWindowTop
- {
- get => Settings.SettingWindowTop;
- set => Settings.SettingWindowTop = value;
- }
-
- public double SettingWindowLeft
- {
- get => Settings.SettingWindowLeft;
- set => Settings.SettingWindowLeft = value;
- }
-
- public Brush PreviewBackground
- {
- get
- {
- var wallpaper = WallpaperPathRetrieval.GetWallpaperPath();
- if (wallpaper != null && File.Exists(wallpaper))
- {
- var memStream = new MemoryStream(File.ReadAllBytes(wallpaper));
- var bitmap = new BitmapImage();
- bitmap.BeginInit();
- bitmap.StreamSource = memStream;
- bitmap.DecodePixelWidth = 800;
- bitmap.DecodePixelHeight = 600;
- bitmap.EndInit();
- var brush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill };
- return brush;
- }
- else
- {
- var wallpaperColor = WallpaperPathRetrieval.GetWallpaperColor();
- var brush = new SolidColorBrush(wallpaperColor);
- return brush;
- }
- }
- }
-
- public ResultsViewModel PreviewResults
- {
- get
- {
- var results = new List
- {
- new Result
- {
- Title = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"),
- SubTitle =
- InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"),
- IcoPath =
- Path.Combine(Constant.ProgramDirectory,
- @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png")
- },
- new Result
- {
- Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"),
- SubTitle =
- InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"),
- IcoPath =
- Path.Combine(Constant.ProgramDirectory,
- @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png")
- },
- new Result
- {
- Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"),
- SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"),
- IcoPath =
- Path.Combine(Constant.ProgramDirectory,
- @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png")
- },
- new Result
- {
- Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"),
- SubTitle =
- InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"),
- IcoPath = Path.Combine(Constant.ProgramDirectory,
- @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png")
- }
- };
- var vm = new ResultsViewModel(Settings);
- vm.AddResults(results, "PREVIEW");
- return vm;
- }
- }
-
- public FontFamily SelectedQueryBoxFont
- {
- get
- {
- if (Fonts.SystemFontFamilies.Count(o =>
- o.FamilyNames.Values != null &&
- o.FamilyNames.Values.Contains(Settings.QueryBoxFont)) > 0)
- {
- var font = new FontFamily(Settings.QueryBoxFont);
- return font;
- }
- else
- {
- var font = new FontFamily("Segoe UI");
- return font;
- }
- }
- set
- {
- Settings.QueryBoxFont = value.ToString();
- ThemeManager.Instance.ChangeTheme(Settings.Theme);
- }
- }
-
- public FamilyTypeface SelectedQueryBoxFontFaces
- {
- get
- {
- var typeface = SyntaxSugars.CallOrRescueDefault(
- () => SelectedQueryBoxFont.ConvertFromInvariantStringsOrNormal(
- Settings.QueryBoxFontStyle,
- Settings.QueryBoxFontWeight,
- Settings.QueryBoxFontStretch
- ));
- return typeface;
- }
- set
- {
- Settings.QueryBoxFontStretch = value.Stretch.ToString();
- Settings.QueryBoxFontWeight = value.Weight.ToString();
- Settings.QueryBoxFontStyle = value.Style.ToString();
- ThemeManager.Instance.ChangeTheme(Settings.Theme);
- }
- }
-
- public FontFamily SelectedResultFont
- {
- get
- {
- if (Fonts.SystemFontFamilies.Count(o =>
- o.FamilyNames.Values != null &&
- o.FamilyNames.Values.Contains(Settings.ResultFont)) > 0)
- {
- var font = new FontFamily(Settings.ResultFont);
- return font;
- }
- else
- {
- var font = new FontFamily("Segoe UI");
- return font;
- }
- }
- set
- {
- Settings.ResultFont = value.ToString();
- ThemeManager.Instance.ChangeTheme(Settings.Theme);
- }
- }
-
- public FamilyTypeface SelectedResultFontFaces
- {
- get
- {
- var typeface = SyntaxSugars.CallOrRescueDefault(
- () => SelectedResultFont.ConvertFromInvariantStringsOrNormal(
- Settings.ResultFontStyle,
- Settings.ResultFontWeight,
- Settings.ResultFontStretch
- ));
- return typeface;
- }
- set
- {
- Settings.ResultFontStretch = value.Stretch.ToString();
- Settings.ResultFontWeight = value.Weight.ToString();
- Settings.ResultFontStyle = value.Style.ToString();
- ThemeManager.Instance.ChangeTheme(Settings.Theme);
- }
- }
-
- public string ThemeImage => Constant.QueryTextBoxIconImagePath;
-
- #endregion
-
- #region hotkey
-
- public CustomPluginHotkey SelectedCustomPluginHotkey { get; set; }
-
- #endregion
-
- #region shortcut
-
- public ObservableCollection CustomShortcuts => Settings.CustomShortcuts;
-
- public ObservableCollection BuiltinShortcuts => Settings.BuiltinShortcuts;
-
- public CustomShortcutModel? SelectedCustomShortcut { get; set; }
-
- public void DeleteSelectedCustomShortcut()
- {
- var item = SelectedCustomShortcut;
- if (item == null)
- {
- MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
- return;
- }
-
- string deleteWarning = string.Format(
- InternationalizationManager.Instance.GetTranslation("deleteCustomShortcutWarning"),
- item.Key, item.Value);
- if (MessageBox.Show(deleteWarning, InternationalizationManager.Instance.GetTranslation("delete"),
- MessageBoxButton.YesNo) == MessageBoxResult.Yes)
- {
- Settings.CustomShortcuts.Remove(item);
- }
- }
-
- public bool EditSelectedCustomShortcut()
- {
- var item = SelectedCustomShortcut;
- if (item == null)
- {
- MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
- return false;
- }
-
- var shortcutSettingWindow = new CustomShortcutSetting(item.Key, item.Value, this);
- if (shortcutSettingWindow.ShowDialog() == true)
- {
- // Fix un-selectable shortcut item after the first selection
- // https://stackoverflow.com/questions/16789360/wpf-listbox-items-with-changing-hashcode
- SelectedCustomShortcut = null;
- item.Key = shortcutSettingWindow.Key;
- item.Value = shortcutSettingWindow.Value;
- SelectedCustomShortcut = item;
- return true;
- }
-
- return false;
- }
-
- public void AddCustomShortcut()
- {
- var shortcutSettingWindow = new CustomShortcutSetting(this);
- if (shortcutSettingWindow.ShowDialog() == true)
- {
- var shortcut = new CustomShortcutModel(shortcutSettingWindow.Key, shortcutSettingWindow.Value);
- Settings.CustomShortcuts.Add(shortcut);
- }
- }
-
- public bool ShortcutExists(string key)
- {
- return Settings.CustomShortcuts.Any(x => x.Key == key) || Settings.BuiltinShortcuts.Any(x => x.Key == key);
- }
-
- #endregion
-
- #region about
-
- public string Website => Constant.Website;
- public string SponsorPage => Constant.SponsorPage;
- public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest";
- public string Documentation => Constant.Documentation;
- public string Docs => Constant.Docs;
- public string Github => Constant.GitHub;
-
- public string Version
- {
- get
- {
- if (Constant.Version == "1.0.0")
- {
- return Constant.Dev;
- }
- else
- {
- return Constant.Version;
- }
- }
- }
-
- public string ActivatedTimes =>
- string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes);
-
- public string CheckLogFolder
- {
- get
- {
- var logFiles = GetLogFiles();
- long size = logFiles.Sum(file => file.Length);
- return string.Format("{0} ({1})", _translater.GetTranslation("clearlogfolder"),
- BytesToReadableString(size));
- }
- }
-
- private static DirectoryInfo GetLogDir(string version = "")
- {
- return new DirectoryInfo(Path.Combine(DataLocation.DataDirectory(), Constant.Logs, version));
- }
-
- private static List GetLogFiles(string version = "")
- {
- return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList();
- }
-
- internal void ClearLogFolder()
- {
- var logDirectory = GetLogDir();
- var logFiles = GetLogFiles();
-
- logFiles.ForEach(f => f.Delete());
-
- logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
- .Where(dir => !Constant.Version.Equals(dir.Name))
- .ToList()
- .ForEach(dir => dir.Delete());
-
- OnPropertyChanged(nameof(CheckLogFolder));
- }
-
- internal void OpenLogFolder()
- {
- App.API.OpenDirectory(GetLogDir(Constant.Version).FullName);
- }
-
- internal static string BytesToReadableString(long bytes)
- {
- const int scale = 1024;
- string[] orders = new string[] { "GB", "MB", "KB", "B" };
- long max = (long)Math.Pow(scale, orders.Length - 1);
-
- foreach (string order in orders)
- {
- if (bytes > max)
- return string.Format("{0:##.##} {1}", decimal.Divide(bytes, max), order);
-
- max /= scale;
- }
-
- return "0 B";
- }
-
- #endregion
+ _storage = new FlowLauncherJsonStorage();
+
+ Updater = updater;
+ Portable = portable;
+ Settings = _storage.Load();
+ }
+
+ public async void UpdateApp()
+ {
+ await Updater.UpdateAppAsync(App.API, false);
+ }
+
+
+
+ ///
+ /// Save Flow settings. Plugins settings are not included.
+ ///
+ public void Save()
+ {
+ _storage.Save();
+ }
+
+ public double SettingWindowWidth
+ {
+ get => Settings.SettingWindowWidth;
+ set => Settings.SettingWindowWidth = value;
+ }
+
+ public double SettingWindowHeight
+ {
+ get => Settings.SettingWindowHeight;
+ set => Settings.SettingWindowHeight = value;
+ }
+
+ public double SettingWindowTop
+ {
+ get => Settings.SettingWindowTop;
+ set => Settings.SettingWindowTop = value;
+ }
+
+ public double SettingWindowLeft
+ {
+ get => Settings.SettingWindowLeft;
+ set => Settings.SettingWindowLeft = value;
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/zipfolder.png b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/zipfolder.png
new file mode 100644
index 000000000..5d3ed0ace
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/zipfolder.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
index a89d9df21..de6a3a2fb 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml
@@ -26,7 +26,6 @@
{0} by {1} {2}{3}Would you like to update this plugin? After the update Flow will automatically restart.
{0} by {1} {2}{2}Would you like to update this plugin?
Plugin Update
- This plugin has an update, would you like to see it?
This plugin is already installed
Plugin Manifest Download Failed
Please check if you can connect to github.com. This error means you may not be able to install or update plugins.
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
index 8cd58ac52..15cbda7f2 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
@@ -3,11 +3,9 @@ using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger;
-using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -99,13 +97,12 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (Context.API.GetAllPlugins()
.Any(x => x.Metadata.ID == plugin.ID && x.Metadata.Version.CompareTo(plugin.Version) < 0))
{
- if (MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_update_exists"),
- Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
- MessageBoxButton.YesNo) == MessageBoxResult.Yes)
- Context
- .API
- .ChangeQuery(
- $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.UpdateCommand} {plugin.Name}");
+ var updateDetail = !plugin.IsFromLocalInstallPath ? plugin.Name : plugin.LocalInstallPath;
+
+ Context
+ .API
+ .ChangeQuery(
+ $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.UpdateCommand} {updateDetail}");
var mainWindow = Application.Current.MainWindow;
mainWindow.Show();
@@ -147,12 +144,17 @@ namespace Flow.Launcher.Plugin.PluginsManager
try
{
- if (File.Exists(filePath))
+ if (!plugin.IsFromLocalInstallPath)
{
- File.Delete(filePath);
- }
+ if (File.Exists(filePath))
+ File.Delete(filePath);
- await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
+ await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
+ }
+ else
+ {
+ filePath = plugin.LocalInstallPath;
+ }
Install(plugin, filePath);
}
@@ -193,24 +195,38 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
await PluginsManifest.UpdateManifestAsync(token, usePrimaryUrlOnly);
+ var pluginFromLocalPath = null as UserPlugin;
+ var updateFromLocalPath = false;
+
+ if (FilesFolders.IsZipFilePath(search, checkFileExists: true))
+ {
+ pluginFromLocalPath = Utilities.GetPluginInfoFromZip(search);
+ pluginFromLocalPath.LocalInstallPath = search;
+ updateFromLocalPath = true;
+ }
+
+ var updateSource = !updateFromLocalPath
+ ? PluginsManifest.UserPlugins
+ : new List { pluginFromLocalPath };
+
var resultsForUpdate = (
from existingPlugin in Context.API.GetAllPlugins()
- join pluginFromManifest in PluginsManifest.UserPlugins
- on existingPlugin.Metadata.ID equals pluginFromManifest.ID
- where String.Compare(existingPlugin.Metadata.Version, pluginFromManifest.Version,
+ join pluginUpdateSource in updateSource
+ on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
+ where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
StringComparison.InvariantCulture) <
- 0 // if current version precedes manifest version
+ 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
&& !PluginManager.PluginModified(existingPlugin.Metadata.ID)
select
new
{
- pluginFromManifest.Name,
- pluginFromManifest.Author,
+ pluginUpdateSource.Name,
+ pluginUpdateSource.Author,
CurrentVersion = existingPlugin.Metadata.Version,
- NewVersion = pluginFromManifest.Version,
+ NewVersion = pluginUpdateSource.Version,
existingPlugin.Metadata.IcoPath,
PluginExistingMetadata = existingPlugin.Metadata,
- PluginNewUserPlugin = pluginFromManifest
+ PluginNewUserPlugin = pluginUpdateSource
}).ToList();
if (!resultsForUpdate.Any())
@@ -261,13 +277,21 @@ namespace Flow.Launcher.Plugin.PluginsManager
_ = Task.Run(async delegate
{
- if (File.Exists(downloadToFilePath))
+ if (!x.PluginNewUserPlugin.IsFromLocalInstallPath)
{
- File.Delete(downloadToFilePath);
- }
+ if (File.Exists(downloadToFilePath))
+ {
+ File.Delete(downloadToFilePath);
+ }
- await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
- .ConfigureAwait(false);
+ await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath;
+ }
+
PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin,
downloadToFilePath);
@@ -396,7 +420,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
results = results.Prepend(updateAllResult);
}
- return Search(results, search);
+ return !updateFromLocalPath ? Search(results, search) : results.ToList();
}
internal bool PluginExists(string id)
@@ -470,6 +494,42 @@ namespace Flow.Launcher.Plugin.PluginsManager
return new List { result };
}
+ internal List InstallFromLocalPath(string localPath)
+ {
+ var plugin = Utilities.GetPluginInfoFromZip(localPath);
+
+ plugin.LocalInstallPath = localPath;
+
+ return new List
+ {
+ new Result
+ {
+ Title = $"{plugin.Name} by {plugin.Author}",
+ SubTitle = plugin.Description,
+ IcoPath = plugin.IcoPath,
+ Action = e =>
+ {
+ if (Settings.WarnFromUnknownSource)
+ {
+ if (!InstallSourceKnown(plugin.Website)
+ && MessageBox.Show(string.Format(
+ Context.API.GetTranslation("plugin_pluginsmanager_install_unknown_source_warning"),
+ Environment.NewLine),
+ Context.API.GetTranslation(
+ "plugin_pluginsmanager_install_unknown_source_warning_title"),
+ MessageBoxButton.YesNo) == MessageBoxResult.No)
+ return false;
+ }
+
+ Application.Current.MainWindow.Hide();
+ _ = InstallOrUpdateAsync(plugin);
+
+ return ShouldHideWindow;
+ }
+ }
+ };
+ }
+
private bool InstallSourceKnown(string url)
{
var author = url.Split('/')[3];
@@ -489,6 +549,9 @@ namespace Flow.Launcher.Plugin.PluginsManager
&& search.Split('.').Last() == zip)
return InstallFromWeb(search);
+ if (FilesFolders.IsZipFilePath(search, checkFileExists: true))
+ return InstallFromLocalPath(search);
+
var results =
PluginsManifest
.UserPlugins
@@ -522,10 +585,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
if (!File.Exists(downloadedFilePath))
throw new FileNotFoundException($"Plugin {plugin.ID} zip file not found at {downloadedFilePath}",
downloadedFilePath);
+
try
{
PluginManager.InstallPlugin(plugin, downloadedFilePath);
- File.Delete(downloadedFilePath);
+
+ if (!plugin.IsFromLocalInstallPath)
+ File.Delete(downloadedFilePath);
}
catch (FileNotFoundException e)
{
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs
index 792891ad1..9800fa020 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs
@@ -1,5 +1,10 @@
-using ICSharpCode.SharpZipLib.Zip;
+using Flow.Launcher.Core.ExternalPlugins;
+using Flow.Launcher.Infrastructure.UserSettings;
+using ICSharpCode.SharpZipLib.Zip;
+using Newtonsoft.Json;
using System.IO;
+using System.IO.Compression;
+using System.Linq;
namespace Flow.Launcher.Plugin.PluginsManager
{
@@ -55,5 +60,28 @@ namespace Flow.Launcher.Plugin.PluginsManager
return string.Empty;
}
+
+ internal static UserPlugin GetPluginInfoFromZip(string filePath)
+ {
+ var plugin = null as UserPlugin;
+
+ using (ZipArchive archive = System.IO.Compression.ZipFile.OpenRead(filePath))
+ {
+ var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json").ToString();
+ ZipArchiveEntry pluginJsonEntry = archive.GetEntry(pluginJsonPath);
+
+ if (pluginJsonEntry != null)
+ {
+ using (StreamReader reader = new StreamReader(pluginJsonEntry.Open()))
+ {
+ string pluginJsonContent = reader.ReadToEnd();
+ plugin = JsonConvert.DeserializeObject(pluginJsonContent);
+ plugin.IcoPath = "Images\\zipfolder.png";
+ }
+ }
+ }
+
+ return plugin;
+ }
}
}