Merge branch 'dev' into administrator_mode

This commit is contained in:
Jack Ye 2025-10-12 14:23:02 +08:00 committed by GitHub
commit cf1d162f2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 413 additions and 37 deletions

View file

@ -89,9 +89,12 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
if (window.ShowDialog() is not true) return;
var index = Settings.CustomPluginHotkeys.IndexOf(settingItem);
Settings.CustomPluginHotkeys[index] = new CustomPluginHotkey(window.Hotkey, window.ActionKeyword);
HotKeyMapper.RemoveHotkey(settingItem.Hotkey); // remove origin hotkey
HotKeyMapper.SetCustomQueryHotkey(Settings.CustomPluginHotkeys[index]); // set new hotkey
if (index >= 0 && index < Settings.CustomPluginHotkeys.Count)
{
Settings.CustomPluginHotkeys[index] = new CustomPluginHotkey(window.Hotkey, window.ActionKeyword);
HotKeyMapper.RemoveHotkey(settingItem.Hotkey); // remove origin hotkey
HotKeyMapper.SetCustomQueryHotkey(Settings.CustomPluginHotkeys[index]); // set new hotkey
}
}
[RelayCommand]
@ -150,7 +153,10 @@ public partial class SettingsPaneHotkeyViewModel : BaseModel
if (window.ShowDialog() is not true) return;
var index = Settings.CustomShortcuts.IndexOf(settingItem);
Settings.CustomShortcuts[index] = new CustomShortcutModel(window.Key, window.Value);
if (index >= 0 && index < Settings.CustomShortcuts.Count)
{
Settings.CustomShortcuts[index] = new CustomShortcutModel(window.Key, window.Value);
}
}
[RelayCommand]

View file

@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Flow.Launcher.Plugin.Url.Converters;
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not bool)
throw new ArgumentException("value should be boolean", nameof(value));
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}

View file

@ -0,0 +1,25 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Flow.Launcher.Plugin.Url.Converters;
[ValueConversion(typeof(bool), typeof(bool))]
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not bool)
throw new ArgumentException("value should be boolean", nameof(value));
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not bool)
throw new ArgumentException("value should be boolean", nameof(value));
return !(bool)value;
}
}

View file

@ -1,18 +1,24 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="flowlauncher_plugin_url_open_search_in">Open search in:</system:String>
<system:String x:Key="flowlauncher_plugin_new_window">New Window</system:String>
<system:String x:Key="flowlauncher_plugin_new_tab">New Tab</system:String>
<system:String x:Key="flowlauncher_plugin_url_open_url">Open url:{0}</system:String>
<system:String x:Key="flowlauncher_plugin_url_cannot_open_url">Can't open url:{0}</system:String>
<system:String x:Key="flowlauncher_plugin_url_plugin_name">URL</system:String>
<system:String x:Key="flowlauncher_plugin_url_plugin_description">Open the typed URL from Flow Launcher</system:String>
<system:String x:Key="flowlauncher_plugin_url_plugin_set_tip">Please set your browser path:</system:String>
<system:String x:Key="flowlauncher_plugin_url_plugin_choose">Choose</system:String>
<system:String x:Key="flowlauncher_plugin_url_plugin_filter">Application(*.exe)|*.exe|All files|*.*</system:String>
<system:String x:Key="flowlauncher_plugin_url_use_custom_browser">Use custom instead of Flow's default web browser</system:String>
<system:String x:Key="flowlauncher_plugin_url_browser_path">Browser path</system:String>
<system:String x:Key="flowlauncher_plugin_url_new_tab">New tab</system:String>
<system:String x:Key="flowlauncher_plugin_url_new_window">New window</system:String>
<system:String x:Key="flowlauncher_plugin_url_private_mode">Private mode</system:String>
<system:String x:Key="flowlauncher_plugin_url_usehttps">Prefer https over http</system:String>
</ResourceDictionary>

View file

@ -1,13 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Windows.Controls;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.Url
{
public class Main : IPlugin, IPluginI18n
public class Main : IPlugin, IPluginI18n, ISettingProvider
{
//based on https://gist.github.com/dperini/729294
private const string urlPattern = "^" +
private const string UrlPattern = "^" +
// protocol identifier
"(?:(?:https?|ftp)://|)" +
// user:pass authentication
@ -39,18 +41,18 @@ namespace Flow.Launcher.Plugin.Url
// resource path
"(?:/\\S*)?" +
"$";
Regex reg = new Regex(urlPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly Regex UrlRegex = new(UrlPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static PluginInitContext Context { get; private set; }
private Settings _settings;
internal static Settings Settings { get; private set; }
public List<Result> Query(Query query)
{
var raw = query.Search;
if (IsURL(raw))
{
return new List<Result>
{
new Result
return
[
new()
{
Title = raw,
SubTitle = Localize.flowlauncher_plugin_url_open_url(raw),
@ -58,14 +60,28 @@ namespace Flow.Launcher.Plugin.Url
Score = 8,
Action = _ =>
{
if (!raw.ToLower().StartsWith("http"))
if (!raw.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !raw.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
raw = "http://" + raw;
raw = GetHttpPreference() + "://" + raw;
}
try
{
Context.API.OpenUrl(raw);
if (Settings.UseCustomBrowser)
{
if (Settings.OpenInNewBrowserWindow)
{
SearchWeb.OpenInBrowserWindow(raw, Settings.BrowserPath, Settings.OpenInPrivateMode, Settings.PrivateModeArgument);
}
else
{
SearchWeb.OpenInBrowserTab(raw, Settings.BrowserPath, Settings.OpenInPrivateMode, Settings.PrivateModeArgument);
}
}
else
{
Context.API.OpenWebUrl(raw);
}
return true;
}
catch(Exception)
@ -75,16 +91,22 @@ namespace Flow.Launcher.Plugin.Url
}
}
}
};
];
}
return new List<Result>(0);
return [];
}
private static string GetHttpPreference()
{
return Settings.AlwaysOpenWithHttps ? "https" : "http";
}
public bool IsURL(string raw)
{
raw = raw.ToLower();
if (reg.Match(raw).Value == raw) return true;
if (UrlRegex.Match(raw).Value == raw) return true;
if (raw == "localhost" || raw.StartsWith("localhost:") ||
raw == "http://localhost" || raw.StartsWith("http://localhost:") ||
@ -100,8 +122,8 @@ namespace Flow.Launcher.Plugin.Url
public void Init(PluginInitContext context)
{
Context = context;
_settings = context.API.LoadSettingJsonStorage<Settings>();
Settings = context.API.LoadSettingJsonStorage<Settings>();
}
public string GetTranslatedPluginTitle()
@ -113,5 +135,10 @@ namespace Flow.Launcher.Plugin.Url
{
return Localize.flowlauncher_plugin_url_plugin_description();
}
public Control CreateSettingPanel()
{
return new SettingsControl();
}
}
}

View file

@ -1,9 +1,53 @@
namespace Flow.Launcher.Plugin.Url
{
public class Settings
public class Settings : BaseModel
{
public string BrowserPath { get; set; }
private bool _useCustomBrowser = false;
public bool UseCustomBrowser
{
get => _useCustomBrowser;
set
{
if (_useCustomBrowser != value)
{
_useCustomBrowser = value;
OnPropertyChanged();
}
}
}
public bool OpenInNewBrowserWindow { get; set; } = true;
private string _browserPath = string.Empty;
public string BrowserPath
{
get => _browserPath;
set
{
if (_browserPath != value)
{
_browserPath = value;
OnPropertyChanged();
}
}
}
private bool _openInNewBrowserWindow = true;
public bool OpenInNewBrowserWindow
{
get => _openInNewBrowserWindow;
set
{
if (_openInNewBrowserWindow != value)
{
_openInNewBrowserWindow = value;
OnPropertyChanged();
}
}
}
public bool OpenInPrivateMode { get; set; } = false;
public string PrivateModeArgument { get; set; } = string.Empty;
public bool AlwaysOpenWithHttps { get; set; } = false;
}
}

View file

@ -0,0 +1,126 @@
<UserControl
x:Class="Flow.Launcher.Plugin.Url.SettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Flow.Launcher.Plugin.Url.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Flow.Launcher.Plugin.Url"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d">
<UserControl.Resources>
<converters:InverseBoolConverter x:Key="InverseBoolConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</UserControl.Resources>
<Grid Margin="{StaticResource SettingPanelMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<CheckBox
Grid.Row="0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_url_use_custom_browser}"
IsChecked="{Binding Settings.UseCustomBrowser, Mode=TwoWay}" />
<Grid
Grid.Row="1"
HorizontalAlignment="Left"
Visibility="{Binding Settings.UseCustomBrowser, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_url_browser_path}" />
<Grid
Grid.Row="0"
Grid.Column="1"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsReadOnly="True"
Text="{Binding Settings.BrowserPath, Mode=OneWay}" />
<Button
Grid.Column="1"
Margin="{StaticResource SettingPanelItemLeftMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Click="SelectBrowserPath"
Content="{DynamicResource flowlauncher_plugin_url_plugin_choose}" />
</Grid>
<StackPanel
Grid.Row="1"
Grid.Column="1"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}"
Orientation="Horizontal">
<RadioButton Content="{DynamicResource flowlauncher_plugin_url_new_tab}" IsChecked="{Binding Settings.OpenInNewBrowserWindow, Converter={StaticResource InverseBoolConverter}, Mode=TwoWay}" />
<RadioButton Content="{DynamicResource flowlauncher_plugin_url_new_window}" IsChecked="{Binding Settings.OpenInNewBrowserWindow, Mode=TwoWay}" />
</StackPanel>
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_url_private_mode}" />
<Grid
Grid.Row="2"
Grid.Column="1"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Settings.PrivateModeArgument, Mode=TwoWay}" />
<CheckBox
Grid.Column="1"
Margin="{StaticResource SettingPanelItemLeftMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content=""
IsChecked="{Binding Settings.OpenInPrivateMode, Mode=TwoWay}" />
</Grid>
</Grid>
<CheckBox
Grid.Row="2"
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{DynamicResource flowlauncher_plugin_url_usehttps}"
IsChecked="{Binding Settings.AlwaysOpenWithHttps, Mode=TwoWay}" />
</Grid>
</UserControl>

View file

@ -0,0 +1,27 @@
using System.Windows;
using System.Windows.Controls;
namespace Flow.Launcher.Plugin.Url;
public partial class SettingsControl : UserControl
{
public Settings Settings { get; } = Main.Settings;
public SettingsControl()
{
InitializeComponent();
}
private void SelectBrowserPath(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = Localize.flowlauncher_plugin_url_plugin_filter()
};
if (dlg.ShowDialog() == true && !string.IsNullOrEmpty(dlg.FileName))
{
Settings.BrowserPath = dlg.FileName;
}
}
}

View file

@ -47,6 +47,7 @@
<system:String x:Key="flowlauncher_plugin_websearch_input_url">Please enter a URL</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_action_keyword_exist">Action keyword already exists, please enter a different one</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_succeed">Success</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_edit_failed">Failed to update search source. The item may have been removed.</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_iconpath_hint">Hint: You do not need to place custom images in this directory, if Flow's version is updated they will be lost. Flow will automatically copy any images outside of this directory across to WebSearch's custom image location.</system:String>
<system:String x:Key="flowlauncher_plugin_websearch_plugin_name">Web Searches</system:String>

View file

@ -108,9 +108,19 @@ namespace Flow.Launcher.Plugin.WebSearch
_context.API.AddActionKeyword(id, newKeyword);
var index = _searchSources.IndexOf(_oldSearchSource);
_searchSources[index] = _searchSource;
Close();
// Only update if we found the item in the collection
if (index >= 0 && index < _searchSources.Count)
{
_searchSources[index] = _searchSource;
Close();
}
else
{
var warning = _api.GetTranslation("flowlauncher_plugin_websearch_edit_failed");
_context.API.ShowMsgBox(warning);
Close();
}
}
else
{

View file

@ -52,6 +52,10 @@
MouseDoubleClick="MouseDoubleClickItem"
SelectedItem="{Binding Settings.SelectedSearchSource}"
SizeChanged="ListView_SizeChanged"
PreviewMouseLeftButtonDown="ListView_PreviewMouseLeftButtonDown"
PreviewMouseMove="ListView_PreviewMouseMove"
AllowDrop="True"
Drop="ListView_Drop"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
<ListView.View>
<GridView>

View file

@ -1,7 +1,10 @@
using System.Windows;
using System.Windows.Controls;
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
namespace Flow.Launcher.Plugin.WebSearch
{
@ -12,6 +15,7 @@ namespace Flow.Launcher.Plugin.WebSearch
{
private readonly Settings _settings;
private readonly PluginInitContext _context;
private Point _dragStartPoint;
public SettingsControl(PluginInitContext context, SettingsViewModel viewModel)
{
@ -163,5 +167,78 @@ namespace Flow.Launcher.Plugin.WebSearch
gView.Columns[3].Width = workingWidth * col4;
gView.Columns[4].Width = workingWidth * col5;
}
private void ListView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_dragStartPoint = e.GetPosition(null);
}
private void ListView_PreviewMouseMove(object sender, MouseEventArgs e)
{
Point mousePos = e.GetPosition(null);
Vector diff = _dragStartPoint - mousePos;
if (e.LeftButton == MouseButtonState.Pressed &&
(Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance))
{
var listView = (ListView)sender;
ListViewItem listViewItem = FindAncestor<ListViewItem>((DependencyObject)e.OriginalSource);
if (listViewItem == null) return;
SearchSource item = (SearchSource)listView.ItemContainerGenerator.ItemFromContainer(listViewItem);
if (item == null) return;
DragDrop.DoDragDrop(listViewItem, item, DragDropEffects.Move);
}
}
private void ListView_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(typeof(SearchSource)))
{
SearchSource droppedData = e.Data.GetData(typeof(SearchSource)) as SearchSource;
var listView = (ListView)sender;
var target = GetNearestContainer(e.OriginalSource);
if (target == null)
return;
SearchSource targetData = (SearchSource)listView.ItemContainerGenerator.ItemFromContainer(target);
if (targetData == null)
return;
var items = _settings.SearchSources;
int removedIdx = items.IndexOf(droppedData);
int targetIdx = items.IndexOf(targetData);
if (removedIdx == targetIdx)
return;
items.Move(removedIdx, targetIdx);
}
}
private ListViewItem GetNearestContainer(object source)
{
var element = source as UIElement;
while (element != null && !(element is ListViewItem))
element = VisualTreeHelper.GetParent(element) as UIElement;
return element as ListViewItem;
}
private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
{
while (current != null)
{
if (current is T)
return (T)current;
current = VisualTreeHelper.GetParent(current);
}
return null;
}
}
}