diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs
index 64c4cd627d2..79d6d7605e3 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 @@ public record UserPlugin
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 7f4d4ea35cf..e7dfb31c07a 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -380,7 +380,8 @@ public static bool PluginModified(string uuid)
///
- /// 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 @@ public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVe
}
///
- /// 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 @@ internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool c
// 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 7bb8fe2009c..61a9da40050 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -31,6 +31,8 @@ public class Settings : BaseModel, IHotkeySettings
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
{
@@ -280,42 +282,9 @@ public List RegisteredHotkeys
{
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))
@@ -340,7 +309,12 @@ public List RegisteredHotkeys
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))
@@ -350,6 +324,45 @@ public List RegisteredHotkeys
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 a835c5b964e..1f34cd23e5d 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;
@@ -199,6 +200,24 @@ public static void OpenFile(string filePath, string workingDir = "", bool asAdmi
}
}
+ ///
+ /// 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 ead4a25cf68..7fd6512eb65 100644
--- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs
+++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
@@ -192,11 +192,15 @@ public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileConte
[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 b8e2a1cfe48..76a613c3f96 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 097d6a53b8a..82efb48693c 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 @@ public partial class CustomShortcutSetting : Window
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 @@ private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
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 00000000000..75ffe11e0e8
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 00000000000..5ac670419ab
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 00000000000..cb1e5906c3e
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 00000000000..ded7126aa08
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 00000000000..a4b19c8f933
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 00000000000..9518343b7fe
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 fd24f825ecc..9ab05555c1e 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -188,6 +188,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 2e6e973a720..2315d99ec31 100644
--- a/Flow.Launcher/MainWindow.xaml
+++ b/Flow.Launcher/MainWindow.xaml
@@ -25,6 +25,7 @@
LocationChanged="OnLocationChanged"
Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
PreviewKeyDown="OnKeyDown"
+ PreviewKeyUp="OnKeyUp"
ResizeMode="NoResize"
ShowInTaskbar="False"
SizeToContent="Height"
@@ -197,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/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs
index 76e587cdca5..9c5ed46c0cc 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -47,6 +47,7 @@ public partial class MainWindow
private MainViewModel _viewModel;
private bool _animating;
MediaPlayer animationSound = new MediaPlayer();
+ private bool isArrowKeyPressed = false;
#endregion
@@ -109,9 +110,11 @@ private async void OnClosing(object sender, CancelEventArgs e)
private void OnInitialized(object sender, EventArgs e)
{
}
-
private void OnLoaded(object sender, RoutedEventArgs _)
{
+ // MouseEventHandler
+ PreviewMouseMove += MainPreviewMouseMove;
+
CheckFirstLaunch();
HideStartup();
// show notify icon when flowlauncher is hidden
@@ -406,6 +409,7 @@ public void WindowAnimator()
if (_animating)
return;
+ isArrowKeyPressed = true;
_animating = true;
UpdatePosition();
@@ -494,6 +498,7 @@ public void WindowAnimator()
windowsb.Completed += (_, _) => _animating = false;
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
+ isArrowKeyPressed = false;
if (QueryTextBox.Text.Length == 0)
{
@@ -644,10 +649,12 @@ private void OnKeyDown(object sender, KeyEventArgs e)
switch (e.Key)
{
case Key.Down:
+ isArrowKeyPressed = true;
_viewModel.SelectNextItemCommand.Execute(null);
e.Handled = true;
break;
case Key.Up:
+ isArrowKeyPressed = true;
_viewModel.SelectPrevItemCommand.Execute(null);
e.Handled = true;
break;
@@ -698,7 +705,21 @@ private void OnKeyDown(object sender, KeyEventArgs e)
}
}
+ private void OnKeyUp(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Up || e.Key == Key.Down)
+ {
+ isArrowKeyPressed = false;
+ }
+ }
+ private void MainPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
+ {
+ if (isArrowKeyPressed)
+ {
+ e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
+ }
+ }
public void PreviewReset()
{
_viewModel.ResetPreview();
diff --git a/Flow.Launcher/Resources/Controls/Card.xaml b/Flow.Launcher/Resources/Controls/Card.xaml
index 24a89548513..c29a5f6025b 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 06266c77507..c8f788acab0 100644
--- a/Flow.Launcher/Resources/Controls/Card.xaml.cs
+++ b/Flow.Launcher/Resources/Controls/Card.xaml.cs
@@ -16,6 +16,7 @@ public Card()
{
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 00000000000..f48bf4b6c9f
--- /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 00000000000..b9588275c7f
--- /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 00000000000..605934e80c1
--- /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 00000000000..9ea550afd51
--- /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 00000000000..855cccdbd60
--- /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 00000000000..c2ed4975bf8
--- /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 00000000000..dfa03a2043f
--- /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 2ce53bfdbc0..cbb8285829b 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 00000000000..cdcd49339d8
--- /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 00000000000..e7d1fc6ead1
--- /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 00000000000..b5cc26ae537
--- /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 00000000000..2c73094c744
--- /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 00000000000..025c73f85df
--- /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 00000000000..b6563d3e502
--- /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 00000000000..a9718a0ac19
--- /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 00000000000..7eb05d9450e
--- /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 00000000000..e069310119b
--- /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 00000000000..dd9e5786de1
--- /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 00000000000..2dd57809db2
--- /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 00000000000..89f4fe15c5d
--- /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 00000000000..15afd7741e5
--- /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 00000000000..de6a4df5ee8
--- /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 00000000000..95505165d33
--- /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 00000000000..f95015b2eef
--- /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 00000000000..2698e860950
--- /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 00000000000..061eabf515d
--- /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 00000000000..675cef7de1b
--- /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 00000000000..dfb4a7eaf6c
--- /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 00000000000..37079a46fef
--- /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 00000000000..d48505c3dbe
--- /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 00000000000..63a43a8fb3b
--- /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 00000000000..95c88d6277b
--- /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 00000000000..3b581767c10
--- /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 00000000000..93cf7ad18bf
--- /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 119268d6bd9..6194db201e3 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,2920 +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 8144c8ff8a3..957379ce42d 100644
--- a/Flow.Launcher/SettingWindow.xaml.cs
+++ b/Flow.Launcher/SettingWindow.xaml.cs
@@ -1,507 +1,172 @@
-using Flow.Launcher.Core.Plugin;
-using Flow.Launcher.Core.Resource;
+using System;
+using System.Windows;
+using System.Windows.Forms;
+using System.Windows.Input;
+using System.Windows.Interop;
+using Flow.Launcher.Core;
+using Flow.Launcher.Core.Configuration;
using Flow.Launcher.Helper;
-using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
+using Flow.Launcher.SettingPages.Views;
using Flow.Launcher.ViewModel;
-using ModernWpf;
using ModernWpf.Controls;
-using System;
-using System.ComponentModel;
-using System.IO;
-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 TextBox = System.Windows.Controls.TextBox;
-using ThemeManager = ModernWpf.ThemeManager;
-
-namespace Flow.Launcher
-{
- public partial class SettingWindow
- {
- public readonly IPublicAPI API;
- private Settings settings;
- private SettingWindowViewModel viewModel;
-
- public SettingWindow(IPublicAPI api, SettingWindowViewModel viewModel)
- {
- settings = viewModel.Settings;
- DataContext = viewModel;
- this.viewModel = viewModel;
- API = api;
- InitializePosition();
- InitializeComponent();
-
- }
-
- #region General
-
- 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.SoftwareOnly;
-
- pluginListView = (CollectionView)CollectionViewSource.GetDefaultView(Plugins.ItemsSource);
- pluginListView.Filter = PluginListFilter;
-
- pluginStoreView = (CollectionView)CollectionViewSource.GetDefaultView(StoreListBox.ItemsSource);
- pluginStoreView.Filter = PluginStoreFilter;
-
- viewModel.PropertyChanged += new PropertyChangedEventHandler(SettingsWindowViewModelChanged);
-
- InitializePosition();
- }
-
- private void SettingsWindowViewModelChanged(object sender, PropertyChangedEventArgs e)
- {
- 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;
- }
+namespace Flow.Launcher;
- 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;
- }
- }
+public partial class SettingWindow
+{
+ private readonly IPublicAPI _api;
+ private readonly Settings _settings;
+ private readonly SettingWindowViewModel _viewModel;
- private void Window_StateChanged(object sender, EventArgs e)
- {
- RefreshMaximizeRestoreButton();
- }
+ public SettingWindow(IPublicAPI api, SettingWindowViewModel viewModel)
+ {
+ _settings = viewModel.Settings;
+ DataContext = viewModel;
+ _viewModel = viewModel;
+ _api = api;
+ InitializePosition();
+ InitializeComponent();
+ NavView.SelectedItem = NavView.MenuItems[0]; /* Set First Page */
+ }
- #region Shortcut
+ 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 OnDeleteCustomShortCutClick(object sender, RoutedEventArgs e)
- {
- viewModel.DeleteSelectedCustomShortcut();
- }
+ private void OnClosed(object sender, EventArgs e)
+ {
+ _settings.SettingWindowState = WindowState;
+ _settings.SettingWindowTop = Top;
+ _settings.SettingWindowLeft = Left;
+ _viewModel.Save();
+ _api.SavePluginSettings();
+ }
- private void OnEditCustomShortCutClick(object sender, RoutedEventArgs e)
- {
- if (viewModel.EditSelectedCustomShortcut())
- {
- //customShortcutView.Items.Refresh(); Should Fix
- }
- }
+ private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e)
+ {
+ Close();
+ }
- private void OnAddCustomShortCutClick(object sender, RoutedEventArgs e)
+ private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */
+ {
+ if (Keyboard.FocusedElement is not TextBox textBox)
{
- viewModel.AddCustomShortcut();
+ return;
}
+ var tRequest = new TraversalRequest(FocusNavigationDirection.Next);
+ textBox.MoveFocus(tRequest);
+ }
- #endregion
-
- private CollectionView pluginListView;
- private CollectionView pluginStoreView;
+ /* Custom TitleBar */
- 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 void OnMinimizeButtonClick(object sender, RoutedEventArgs e)
+ {
+ WindowState = WindowState.Minimized;
+ }
- private bool PluginStoreFilter(object item)
+ private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)
+ {
+ WindowState = WindowState switch
{
- 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 = "";
+ WindowState.Maximized => WindowState.Normal,
+ _ => WindowState.Maximized
+ };
+ }
- private void RefreshPluginListEventHandler(object sender, RoutedEventArgs e)
- {
- if (pluginFilterTxb.Text != lastPluginListSearch)
- {
- lastPluginListSearch = pluginFilterTxb.Text;
- pluginListView.Refresh();
- }
- }
+ private void OnCloseButtonClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
- private void RefreshPluginStoreEventHandler(object sender, RoutedEventArgs e)
+ private void RefreshMaximizeRestoreButton()
+ {
+ if (WindowState == WindowState.Maximized)
{
- if (pluginStoreFilterTxb.Text != lastPluginStoreSearch)
- {
- lastPluginStoreSearch = pluginStoreFilterTxb.Text;
- pluginStoreView.Refresh();
- }
+ MaximizeButton.Visibility = Visibility.Collapsed;
+ RestoreButton.Visibility = Visibility.Visible;
}
-
- private void PluginFilterTxb_OnKeyDown(object sender, KeyEventArgs e)
+ else
{
- if (e.Key == Key.Enter)
- RefreshPluginListEventHandler(sender, e);
+ MaximizeButton.Visibility = Visibility.Visible;
+ RestoreButton.Visibility = Visibility.Collapsed;
}
+ }
- private void PluginStoreFilterTxb_OnKeyDown(object sender, KeyEventArgs e)
- {
- if (e.Key == Key.Enter)
- RefreshPluginStoreEventHandler(sender, e);
- }
+ private void Window_StateChanged(object sender, EventArgs e)
+ {
+ RefreshMaximizeRestoreButton();
+ }
- private void OnPluginSettingKeydown(object sender, KeyEventArgs e)
+ public void InitializePosition()
+ {
+ if (_settings.SettingWindowTop == null)
{
- if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && e.Key == Key.F)
- pluginFilterTxb.Focus();
+ Top = WindowTop();
+ Left = WindowLeft();
}
-
- private void PluginStore_OnKeyDown(object sender, KeyEventArgs e)
+ else
{
- if (e.Key == Key.F && (Keyboard.Modifiers & ModifierKeys.Control) != 0)
- {
- pluginStoreFilterTxb.Focus();
- }
+ Top = _settings.SettingWindowTop;
+ Left = _settings.SettingWindowLeft;
}
+ WindowState = _settings.SettingWindowState;
+ }
- public void InitializePosition()
- {
- if (settings.SettingWindowTop >= 0 && settings.SettingWindowLeft >= 0)
- {
- Top = settings.SettingWindowTop;
- Left = settings.SettingWindowLeft;
- }
- else
- {
- Top = WindowTop();
- Left = WindowLeft();
- }
- 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;
+ }
- 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;
- }
+ 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;
+ }
- public double WindowTop()
+ private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
+ {
+ var paneData = new PaneData(_settings, _viewModel.Updater, _viewModel.Portable);
+ if (args.IsSettingsSelected)
{
- 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;
+ ContentFrame.Navigate(typeof(SettingsPaneGeneral), paneData);
}
-
- private Button storeClickedButton;
-
- private void StoreListItem_Click(object sender, RoutedEventArgs e)
+ else
{
- if (sender is not Button button)
- return;
+ var selectedItem = (NavigationViewItem)args.SelectedItem;
+ if (selectedItem == null) return;
- storeClickedButton = button;
-
- var flyout = FlyoutService.GetFlyout(button);
- flyout.Closed += (_, _) =>
+ var pageType = selectedItem.Name switch
{
- storeClickedButton = null;
+ 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)
};
-
- }
-
- private void PluginStore_GotFocus(object sender, RoutedEventArgs e)
- {
- Keyboard.Focus(pluginStoreFilterTxb);
- }
-
- private void Plugin_GotFocus(object sender, RoutedEventArgs e)
- {
- Keyboard.Focus(pluginFilterTxb);
+ 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 67efc6a8657..01dc67b9844 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -42,6 +42,7 @@ public partial class MainViewModel : BaseModel, ISavable
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 @@ public MainViewModel(Settings settings)
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 @@ public void ReQuery(bool reselect)
}
}
+ [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 @@ private async Task OpenResultAsync(string index)
{
_userSelectedRecord.Add(result);
_history.Add(result.OriginQuery.RawQuery);
+ lastHistoryIndex = 1;
}
if (hideWindow)
@@ -394,7 +428,18 @@ private void SelectNextPage()
[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 @@ public string VerifyOrSetDefaultHotkey(string hotkey, string defaultHotkey)
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 @@ public void Show()
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 bc98efabc86..513d0443fe4 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 @@ public PluginStoreItemViewModel(UserPlugin plugin)
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 @@ public string Category
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 d2919507db0..b4daa8c7af4 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 @@ public PluginPair PluginPair
}
}
+ 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 @@ public ImageSource Image
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 @@ public bool IsExpanded
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 @@ public Control SettingControl
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 @@ public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword
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 @@ private void OpenPluginDirectory()
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 fe1ea4e7b0b..48180204550 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
-{
- public partial class SettingWindowViewModel : BaseModel
- {
- 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; }
- }
+namespace Flow.Launcher.ViewModel;
- 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 class SettingWindowViewModel : BaseModel
+{
+ private readonly FlowLauncherJsonStorage _storage;
- public string CheckLogFolder
- {
- get
- {
- var logFiles = GetLogFiles();
- long size = logFiles.Sum(file => file.Length);
- return string.Format("{0} ({1})", _translater.GetTranslation("clearlogfolder"),
- BytesToReadableString(size));
- }
- }
+ public Updater Updater { get; }
- private static DirectoryInfo GetLogDir(string version = "")
- {
- return new DirectoryInfo(Path.Combine(DataLocation.DataDirectory(), Constant.Logs, version));
- }
+ public IPortable Portable { get; }
- private static List GetLogFiles(string version = "")
- {
- return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList();
- }
+ public Settings Settings { get; }
- internal void ClearLogFolder()
- {
- var logDirectory = GetLogDir();
- var logFiles = GetLogFiles();
+ public SettingWindowViewModel(Updater updater, IPortable portable)
+ {
+ _storage = new FlowLauncherJsonStorage();
- logFiles.ForEach(f => f.Delete());
+ Updater = updater;
+ Portable = portable;
+ Settings = _storage.Load();
+ }
- logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
- .Where(dir => !Constant.Version.Equals(dir.Name))
- .ToList()
- .ForEach(dir => dir.Delete());
+ public async void UpdateApp()
+ {
+ await Updater.UpdateAppAsync(App.API, false);
+ }
- 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);
+ ///
+ /// Save Flow settings. Plugins settings are not included.
+ ///
+ public void Save()
+ {
+ _storage.Save();
+ }
- foreach (string order in orders)
- {
- if (bytes > max)
- return string.Format("{0:##.##} {1}", decimal.Divide(bytes, max), order);
+ public double SettingWindowWidth
+ {
+ get => Settings.SettingWindowWidth;
+ set => Settings.SettingWindowWidth = value;
+ }
- max /= scale;
- }
+ public double SettingWindowHeight
+ {
+ get => Settings.SettingWindowHeight;
+ set => Settings.SettingWindowHeight = value;
+ }
- return "0 B";
- }
+ public double SettingWindowTop
+ {
+ get => Settings.SettingWindowTop;
+ set => Settings.SettingWindowTop = value;
+ }
- #endregion
+ 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 00000000000..5d3ed0ace3f
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 a89d9df213d..de6a3a2fb4c 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 8cd58ac5212..15cbda7f211 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs
@@ -3,11 +3,9 @@
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 @@ internal async Task InstallOrUpdateAsync(UserPlugin plugin)
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 @@ internal async Task InstallOrUpdateAsync(UserPlugin plugin)
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 @@ internal async ValueTask> RequestUpdateAsync(string search, Cancell
{
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 @@ where String.Compare(existingPlugin.Metadata.Version, pluginFromManifest.Version
_ = 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 @@ await Http.DownloadAsync(plugin.PluginNewUserPlugin.UrlDownload, downloadToFileP
results = results.Prepend(updateAllResult);
}
- return Search(results, search);
+ return !updateFromLocalPath ? Search(results, search) : results.ToList();
}
internal bool PluginExists(string id)
@@ -470,6 +494,42 @@ internal List InstallFromWeb(string url)
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 @@ internal async ValueTask> RequestInstallOrUpdate(string search, Can
&& search.Split('.').Last() == zip)
return InstallFromWeb(search);
+ if (FilesFolders.IsZipFilePath(search, checkFileExists: true))
+ return InstallFromLocalPath(search);
+
var results =
PluginsManifest
.UserPlugins
@@ -522,10 +585,13 @@ private void Install(UserPlugin plugin, string downloadedFilePath)
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 7a7496ece1a..691f9c26cc8 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs
@@ -1,5 +1,10 @@
-using System.IO;
+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
{
@@ -33,5 +38,28 @@ internal static string GetContainingFolderPathAfterUnzip(string unzippedParentFo
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;
+ }
}
}