diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs new file mode 100644 index 000000000..c0cd022ea --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -0,0 +1,57 @@ +using Flow.Launcher.Infrastructure.Http; +using Flow.Launcher.Infrastructure.Logger; +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Core.ExternalPlugins +{ + public static class PluginsManifest + { + static PluginsManifest() + { + UpdateTask = UpdateManifestAsync(); + } + + public static List UserPlugins { get; private set; } = new List(); + + public static Task UpdateTask { get; private set; } + + private static readonly SemaphoreSlim manifestUpdateLock = new(1); + + public static Task UpdateManifestAsync() + { + if (manifestUpdateLock.CurrentCount == 0) + { + return UpdateTask; + } + + return UpdateTask = DownloadManifestAsync(); + } + + private async static Task DownloadManifestAsync() + { + try + { + await manifestUpdateLock.WaitAsync().ConfigureAwait(false); + + await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json") + .ConfigureAwait(false); + + UserPlugins = await JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); + } + catch (Exception e) + { + Log.Exception("|PluginManagement.GetManifest|Encountered error trying to download plugins manifest", e); + + UserPlugins = new List(); + } + finally + { + manifestUpdateLock.Release(); + } + } + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs similarity index 77% rename from Plugins/Flow.Launcher.Plugin.PluginsManager/Models/UserPlugin.cs rename to Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs index c1af3014b..f98815c1a 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/UserPlugin.cs +++ b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs @@ -1,7 +1,6 @@ - -namespace Flow.Launcher.Plugin.PluginsManager.Models +namespace Flow.Launcher.Core.ExternalPlugins { - public class UserPlugin + public record UserPlugin { public string ID { get; set; } public string Name { get; set; } @@ -12,5 +11,6 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models public string Website { get; set; } public string UrlDownload { get; set; } public string UrlSourceCode { get; set; } + public string IcoPath { get; set; } } } diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 9e932a508..60c4ec3de 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -53,8 +53,8 @@ - - + + diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 1b78c68ae..b3d56221a 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -120,8 +120,8 @@ namespace Flow.Launcher.Core.Plugin var pythonPath = string.Empty; - if (MessageBox.Show("Flow detected you have installed Python plugins, " + - "would you like to install Python to run them? " + + if (MessageBox.Show("Flow detected you have installed Python plugins, which " + + "will need Python to run. Would you like to download Python? " + Environment.NewLine + Environment.NewLine + "Click no if it's already installed, " + "and you will be prompted to select the folder that contains the Python executable", diff --git a/Flow.Launcher.Core/Plugin/QueryBuilder.cs b/Flow.Launcher.Core/Plugin/QueryBuilder.cs index df336e14b..ef387b693 100644 --- a/Flow.Launcher.Core/Plugin/QueryBuilder.cs +++ b/Flow.Launcher.Core/Plugin/QueryBuilder.cs @@ -10,35 +10,31 @@ namespace Flow.Launcher.Core.Plugin public static Query Build(string text, Dictionary nonGlobalPlugins) { // replace multiple white spaces with one white space - var terms = text.Split(new[] { Query.TermSeperater }, StringSplitOptions.RemoveEmptyEntries); + var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries); if (terms.Length == 0) { // nothing was typed return null; } - var rawQuery = string.Join(Query.TermSeperater, terms); + var rawQuery = string.Join(Query.TermSeparator, terms); string actionKeyword, search; string possibleActionKeyword = terms[0]; - List actionParameters; + string[] searchTerms; + if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled) { // use non global plugin for query actionKeyword = possibleActionKeyword; - actionParameters = terms.Skip(1).ToList(); - search = actionParameters.Count > 0 ? rawQuery.Substring(actionKeyword.Length + 1) : string.Empty; + search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..] : string.Empty; + searchTerms = terms[1..]; } else { // non action keyword actionKeyword = string.Empty; search = rawQuery; + searchTerms = terms; } - var query = new Query - { - Terms = terms, - RawQuery = rawQuery, - ActionKeyword = actionKeyword, - Search = search - }; + var query = new Query(rawQuery, search,terms, searchTerms, actionKeyword); return query; } diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index e70de673e..4648aef63 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -145,11 +145,9 @@ namespace Flow.Launcher.Core.Resource public ResourceDictionary GetResourceDictionary() { var dict = CurrentThemeResourceDictionary(); - - Style queryBoxStyle = dict["QueryBoxStyle"] as Style; - Style querySuggestionBoxStyle = dict["QuerySuggestionBoxStyle"] as Style; - - if (queryBoxStyle != null && querySuggestionBoxStyle != null) + + if (dict["QueryBoxStyle"] is Style queryBoxStyle && + dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) { var fontFamily = new FontFamily(Settings.QueryBoxFont); var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle); @@ -174,11 +172,12 @@ namespace Flow.Launcher.Core.Resource querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); } - Style resultItemStyle = dict["ItemTitleStyle"] as Style; - Style resultSubItemStyle = dict["ItemSubTitleStyle"] as Style; - Style resultItemSelectedStyle = dict["ItemTitleSelectedStyle"] as Style; - Style resultSubItemSelectedStyle = dict["ItemSubTitleSelectedStyle"] as Style; - if (resultItemStyle != null && resultSubItemStyle != null && resultSubItemSelectedStyle != null && resultItemSelectedStyle != null) + if (dict["ItemTitleStyle"] is Style resultItemStyle && + dict["ItemSubTitleStyle"] is Style resultSubItemStyle && + dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle && + dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && + dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && + dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) { Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultFont)); Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultFontStyle)); @@ -186,7 +185,9 @@ namespace Flow.Launcher.Core.Resource Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultFontStretch)); Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; - Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); + Array.ForEach( + new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o + => Array.ForEach(setters, p => o.Setters.Add(p))); } var windowStyle = dict["WindowStyle"] as Style; @@ -236,17 +237,19 @@ namespace Flow.Launcher.Core.Resource public void AddDropShadowEffectToCurrentTheme() { - var dict = CurrentThemeResourceDictionary(); + var dict = GetResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; - var effectSetter = new Setter(); - effectSetter.Property = Border.EffectProperty; - effectSetter.Value = new DropShadowEffect + var effectSetter = new Setter { - Opacity = 0.9, - ShadowDepth = 2, - BlurRadius = 15 + Property = Border.EffectProperty, + Value = new DropShadowEffect + { + Opacity = 0.4, + ShadowDepth = 2, + BlurRadius = 15 + } }; var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; @@ -261,7 +264,7 @@ namespace Flow.Launcher.Core.Resource } else { - var baseMargin = (Thickness) marginSetter.Value; + var baseMargin = (Thickness)marginSetter.Value; var newMargin = new Thickness( baseMargin.Left + ShadowExtraMargin, baseMargin.Top + ShadowExtraMargin, @@ -282,8 +285,8 @@ namespace Flow.Launcher.Core.Resource var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter; var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; - - if(effectSetter != null) + + if (effectSetter != null) { windowBorderStyle.Setters.Remove(effectSetter); } @@ -371,11 +374,11 @@ namespace Flow.Launcher.Core.Resource private void SetWindowAccent(Window w, AccentState state) { var windowHelper = new WindowInteropHelper(w); - + // this determines the width of the main query window w.Width = mainWindowWidth; windowHelper.EnsureHandle(); - + var accent = new AccentPolicy { AccentState = state }; var accentStructSize = Marshal.SizeOf(accent); diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 76713ce2a..e09c6380c 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -16,6 +16,7 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using System.Text.Json.Serialization; +using System.Threading; namespace Flow.Launcher.Core { @@ -28,21 +29,21 @@ namespace Flow.Launcher.Core GitHubRepository = gitHubRepository; } - public async Task UpdateApp(IPublicAPI api, bool silentUpdate = true) + private SemaphoreSlim UpdateLock { get; } = new SemaphoreSlim(1); + + public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) { + await UpdateLock.WaitAsync(); try { - UpdateInfo newUpdateInfo; - if (!silentUpdate) api.ShowMsg(api.GetTranslation("pleaseWait"), - api.GetTranslation("update_flowlauncher_update_check")); + api.GetTranslation("update_flowlauncher_update_check")); using var updateManager = await GitHubUpdateManager(GitHubRepository).ConfigureAwait(false); - // UpdateApp CheckForUpdate will return value only if the app is squirrel installed - newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false); + var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false); var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); var currentVersion = Version.Parse(Constant.Version); @@ -58,7 +59,7 @@ namespace Flow.Launcher.Core if (!silentUpdate) api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), - api.GetTranslation("update_flowlauncher_updating")); + api.GetTranslation("update_flowlauncher_updating")); await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); @@ -70,8 +71,8 @@ namespace Flow.Launcher.Core FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), - DataLocation.PortableDataPath, - targetDestination)); + DataLocation.PortableDataPath, + targetDestination)); } else { @@ -87,12 +88,15 @@ namespace Flow.Launcher.Core UpdateManager.RestartApp(Constant.ApplicationFileName); } } - catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException || e.InnerException is TimeoutException) + catch (Exception e) when (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException) { Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), - api.GetTranslation("update_flowlauncher_check_connection")); - return; + api.GetTranslation("update_flowlauncher_check_connection")); + } + finally + { + UpdateLock.Release(); } } @@ -115,13 +119,16 @@ namespace Flow.Launcher.Core var uri = new Uri(repository); var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; - var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false); + await using var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false); var releases = await System.Text.Json.JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First(); var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/"); - - var client = new WebClient { Proxy = Http.WebProxy }; + + var client = new WebClient + { + Proxy = Http.WebProxy + }; var downloader = new FileDownloader(client); var manager = new UpdateManager(latestUrl, urlDownloader: downloader); diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index aea43506e..ef76fd617 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -53,8 +53,10 @@ - - + + + + diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index fd2464f2e..c06e1587d 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings @@ -15,13 +15,24 @@ namespace Flow.Launcher.Infrastructure.UserSettings if (Plugins.ContainsKey(metadata.ID)) { var settings = Plugins[metadata.ID]; - - // TODO: Remove. This is backwards compatibility for 1.8.0 release. - // Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder. + if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count > settings.ActionKeywords.Count) { - settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search - settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search + // TODO: Remove. This is backwards compatibility for Explorer 1.8.0 release. + // Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder. + if (settings.Version.CompareTo("1.8.0") < 0) + { + settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search + settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search + settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for quick access action keyword + } + + // TODO: Remove. This is backwards compatibility for Explorer 1.9.0 release. + // Introduced a new action keywords in Explorer since 1.8.0, so need to update plugin setting in the UserData folder. + if (settings.Version.CompareTo("1.8.0") > 0) + { + settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for quick access action keyword + } } if (string.IsNullOrEmpty(settings.Version)) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index ebef2c631..102446158 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -32,6 +32,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public string ResultFontStyle { get; set; } public string ResultFontWeight { get; set; } public string ResultFontStretch { get; set; } + public bool UseGlyphIcons { get; set; } = true; /// @@ -98,8 +99,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool RememberLastLaunchLocation { get; set; } public bool IgnoreHotkeysOnFullscreen { get; set; } - public bool AutoHideScrollBar { get; set; } - public HttpProxy Proxy { get; set; } = new HttpProxy(); [JsonConverter(typeof(JsonStringEnumConverter))] diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 664373434..c808052b0 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -60,8 +60,13 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + diff --git a/Flow.Launcher.Plugin/GlyphInfo.cs b/Flow.Launcher.Plugin/GlyphInfo.cs new file mode 100644 index 000000000..d24624d8f --- /dev/null +++ b/Flow.Launcher.Plugin/GlyphInfo.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; + +namespace Flow.Launcher.Plugin +{ + public record GlyphInfo(string FontFamily, string Glyph); +} diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index d9cdf5581..974eafa96 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -59,6 +59,11 @@ namespace Flow.Launcher.Plugin /// Optional message subtitle void ShowMsgError(string title, string subTitle = ""); + /// + /// Show the MainWindow when hiding + /// + void ShowMainWindow(); + /// /// Show message box /// diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 1eb5c9c14..3fcf3c1d7 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -1,4 +1,5 @@ -using System; +using JetBrains.Annotations; +using System; using System.Collections.Generic; using System.Linq; @@ -11,11 +12,12 @@ namespace Flow.Launcher.Plugin /// /// to allow unit tests for plug ins /// - public Query(string rawQuery, string search, string[] terms, string actionKeyword = "") + public Query(string rawQuery, string search, string[] terms, string[] searchTerms, string actionKeyword = "") { Search = search; RawQuery = rawQuery; Terms = terms; + SearchTerms = searchTerms; ActionKeyword = actionKeyword; } @@ -23,7 +25,7 @@ namespace Flow.Launcher.Plugin /// Raw query, this includes action keyword if it has /// We didn't recommend use this property directly. You should always use Search property. /// - public string RawQuery { get; internal set; } + public string RawQuery { get; internal init; } /// /// Search part of a query. @@ -31,45 +33,53 @@ namespace Flow.Launcher.Plugin /// Since we allow user to switch a exclusive plugin to generic plugin, /// so this property will always give you the "real" query part of the query /// - public string Search { get; internal set; } + public string Search { get; internal init; } /// - /// The raw query splited into a string array. + /// The search string split into a string array. /// - public string[] Terms { get; set; } + public string[] SearchTerms { get; init; } + + /// + /// The raw query split into a string array + /// + [Obsolete("It may or may not include action keyword, which can be confusing. Use SearchTerms instead")] + public string[] Terms { get; init; } /// /// Query can be splited into multiple terms by whitespace /// - public const string TermSeperater = " "; + public const string TermSeparator = " "; + + [Obsolete("Typo")] + public const string TermSeperater = TermSeparator; /// /// User can set multiple action keywords seperated by ';' /// - public const string ActionKeywordSeperater = ";"; + public const string ActionKeywordSeparator = ";"; + + [Obsolete("Typo")] + public const string ActionKeywordSeperater = ActionKeywordSeparator; + /// /// '*' is used for System Plugin /// public const string GlobalPluginWildcardSign = "*"; - public string ActionKeyword { get; set; } + public string ActionKeyword { get; init; } /// /// Return first search split by space if it has /// public string FirstSearch => SplitSearch(0); + private string _secondToEndSearch; + /// /// strings from second search (including) to last search /// - public string SecondToEndSearch - { - get - { - var index = string.IsNullOrEmpty(ActionKeyword) ? 1 : 2; - return string.Join(TermSeperater, Terms.Skip(index).ToArray()); - } - } + public string SecondToEndSearch => SearchTerms.Length > 1 ? (_secondToEndSearch ??= string.Join(' ', SearchTerms[1..])) : ""; /// /// Return second search split by space if it has @@ -83,16 +93,9 @@ namespace Flow.Launcher.Plugin private string SplitSearch(int index) { - try - { - return string.IsNullOrEmpty(ActionKeyword) ? Terms[index] : Terms[index + 1]; - } - catch (IndexOutOfRangeException) - { - return string.Empty; - } + return index < SearchTerms.Length ? SearchTerms[index] : string.Empty; } public override string ToString() => RawQuery; } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index ac9c10df0..171b30c26 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -47,7 +47,15 @@ namespace Flow.Launcher.Plugin public delegate ImageSource IconDelegate(); - public IconDelegate Icon; + /// + /// Delegate to Get Image Source + /// + public IconDelegate Icon { get; set; } + + /// + /// Information for Glyph Icon + /// + public GlyphInfo Glyph { get; init; } /// diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index 84c9137b7..8de0681c8 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -48,13 +48,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/Flow.Launcher/ActionKeywords.xaml.cs b/Flow.Launcher/ActionKeywords.xaml.cs index 4a2368347..e116e6f4d 100644 --- a/Flow.Launcher/ActionKeywords.xaml.cs +++ b/Flow.Launcher/ActionKeywords.xaml.cs @@ -30,7 +30,7 @@ namespace Flow.Launcher private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e) { - tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeperater, plugin.Metadata.ActionKeywords.ToArray()); + tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, plugin.Metadata.ActionKeywords.ToArray()); tbAction.Focus(); } @@ -44,7 +44,7 @@ namespace Flow.Launcher var oldActionKeyword = plugin.Metadata.ActionKeywords[0]; var newActionKeyword = tbAction.Text.Trim(); newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*"; - if (!pluginViewModel.IsActionKeywordRegistered(newActionKeyword)) + if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword)) { pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword); Close(); diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index 18addac73..13d88e1c6 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -6,6 +6,227 @@ Startup="OnStartupAsync"> + + + + + + + + diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index c2a32100d..8c869e941 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -129,12 +129,12 @@ namespace Flow.Launcher var timer = new Timer(1000 * 60 * 60 * 5); timer.Elapsed += async (s, e) => { - await _updater.UpdateApp(API); + await _updater.UpdateAppAsync(API); }; timer.Start(); // check updates on startup - await _updater.UpdateApp(API); + await _updater.UpdateAppAsync(API); } }); } diff --git a/Flow.Launcher/Converters/BorderClipConverter.cs b/Flow.Launcher/Converters/BorderClipConverter.cs new file mode 100644 index 000000000..83e83f1de --- /dev/null +++ b/Flow.Launcher/Converters/BorderClipConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; +using System.Windows.Documents; +using System.Windows.Shapes; + +// For Clipping inside listbox item + +namespace Flow.Launcher.Converters +{ + public class BorderClipConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Length == 3 && values[0] is double && values[1] is double && values[2] is CornerRadius) + { + var width = (double)values[0]; + var height = (double)values[1]; + Path myPath = new Path(); + if (width < Double.Epsilon || height < Double.Epsilon) + { + return Geometry.Empty; + } + var radius = (CornerRadius)values[2]; + var radiusHeight = radius.TopLeft; + + // Drawing Round box for bottom round, and rect for top area of listbox. + var corner = new RectangleGeometry(new Rect(0, 0, width, height), radius.TopLeft, radius.TopLeft); + var box = new RectangleGeometry(new Rect(0, 0, width, radiusHeight), 0, 0); + + GeometryGroup myGeometryGroup = new GeometryGroup(); + myGeometryGroup.Children.Add(corner); + myGeometryGroup.Children.Add(box); + + CombinedGeometry c1 = new CombinedGeometry(GeometryCombineMode.Union, corner, box); + myPath.Data = c1; + + myPath.Data.Freeze(); + return myPath.Data; + } + + return DependencyProperty.UnsetValue; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } + +} diff --git a/Flow.Launcher/Converters/HighlightTextConverter.cs b/Flow.Launcher/Converters/HighlightTextConverter.cs index 006e52320..972dd1bc8 100644 --- a/Flow.Launcher/Converters/HighlightTextConverter.cs +++ b/Flow.Launcher/Converters/HighlightTextConverter.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Data; +using System.Windows.Media; using System.Windows.Documents; namespace Flow.Launcher.Converters @@ -30,7 +31,9 @@ namespace Flow.Launcher.Converters var currentCharacter = text.Substring(i, 1); if (this.ShouldHighlight(highlightData, i)) { - textBlock.Inlines.Add(new Bold(new Run(currentCharacter))); + + textBlock.Inlines.Add(new Run(currentCharacter) { Style = (Style)Application.Current.FindResource("HighlightStyle") }); + } else { diff --git a/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs b/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs index 7de5af79a..e82fa959c 100644 --- a/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs +++ b/Flow.Launcher/Converters/OpenResultHotkeyVisibilityConverter.cs @@ -16,12 +16,10 @@ namespace Flow.Launcher.Converters public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var hotkeyNumber = int.MaxValue; - - if (value is ListBoxItem listBoxItem) - { - ListBox listBox = ItemsControl.ItemsControlFromItemContainer(listBoxItem) as ListBox; + + if (value is ListBoxItem listBoxItem + && ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox) hotkeyNumber = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; - } return hotkeyNumber <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed; } diff --git a/Flow.Launcher/Converters/OrdinalConverter.cs b/Flow.Launcher/Converters/OrdinalConverter.cs index 970ed183c..f9fa220e3 100644 --- a/Flow.Launcher/Converters/OrdinalConverter.cs +++ b/Flow.Launcher/Converters/OrdinalConverter.cs @@ -8,11 +8,9 @@ namespace Flow.Launcher.Converters { public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture) { - if (value is ListBoxItem listBoxItem) - { - ListBox listBox = ItemsControl.ItemsControlFromItemContainer(listBoxItem) as ListBox; + if (value is ListBoxItem listBoxItem + && ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox) return listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1; - } return 0; } diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index f3885ccfd..fd2f5f1d9 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -77,18 +77,25 @@ Designer PreserveNewest + + PreserveNewest + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index d5fcabace..fe25ecf18 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -6,6 +6,8 @@ using NHotkey.Wpf; using Flow.Launcher.Core.Resource; using System.Windows; using Flow.Launcher.ViewModel; +using System.Threading.Tasks; +using System.Threading; namespace Flow.Launcher.Helper { @@ -19,10 +21,15 @@ namespace Flow.Launcher.Helper mainViewModel = mainVM; settings = mainViewModel._settings; - SetHotkey(settings.Hotkey, OnHotkey); + SetHotkey(settings.Hotkey, OnToggleHotkey); LoadCustomPluginHotkey(); } + internal static void OnToggleHotkey(object sender, HotkeyEventArgs args) + { + mainViewModel.ToggleFlowLauncher(); + } + private static void SetHotkey(string hotkeyStr, EventHandler action) { var hotkey = new HotkeyModel(hotkeyStr); @@ -53,44 +60,6 @@ namespace Flow.Launcher.Helper } } - internal static void OnHotkey(object sender, HotkeyEventArgs e) - { - if (!ShouldIgnoreHotkeys()) - { - UpdateLastQUeryMode(); - - mainViewModel.ToggleFlowLauncher(); - e.Handled = true; - } - } - - /// - /// Checks if Flow Launcher should ignore any hotkeys - /// - private static bool ShouldIgnoreHotkeys() - { - return settings.IgnoreHotkeysOnFullscreen && WindowsInteropHelper.IsWindowFullscreen(); - } - - private static void UpdateLastQUeryMode() - { - switch(settings.LastQueryMode) - { - case LastQueryMode.Empty: - mainViewModel.ChangeQueryText(string.Empty); - break; - case LastQueryMode.Preserved: - mainViewModel.LastQuerySelected = true; - break; - case LastQueryMode.Selected: - mainViewModel.LastQuerySelected = false; - break; - default: - throw new ArgumentException($"wrong LastQueryMode: <{settings.LastQueryMode}>"); - - } - } - internal static void LoadCustomPluginHotkey() { if (settings.CustomPluginHotkeys == null) @@ -106,11 +75,11 @@ namespace Flow.Launcher.Helper { SetHotkey(hotkey.Hotkey, (s, e) => { - if (ShouldIgnoreHotkeys()) + if (mainViewModel.ShouldIgnoreHotkeys()) return; mainViewModel.MainWindowVisibility = Visibility.Visible; - mainViewModel.ChangeQueryText(hotkey.ActionKeyword); + mainViewModel.ChangeQueryText(hotkey.ActionKeyword, true); }); } diff --git a/Flow.Launcher/HotkeyControl.xaml b/Flow.Launcher/HotkeyControl.xaml index e732cbe97..ab786fd56 100644 --- a/Flow.Launcher/HotkeyControl.xaml +++ b/Flow.Launcher/HotkeyControl.xaml @@ -9,11 +9,11 @@ d:DesignHeight="300" d:DesignWidth="300"> - + - - + + \ No newline at end of file diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 90a920e3e..051891a2b 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -13,50 +13,59 @@ Settings About Exit + Close Flow Launcher Settings General Portable Mode + Store all settings and user data in one folder (Useful when used with removable drives or cloud services). Start Flow Launcher on system startup Hide Flow Launcher when focus is lost Do not show new version notifications Remember last launch location Language Last Query Style + Show/Hide previous results when Flow Launcher is reactivated. Preserve Last Query Select last Query Empty last Query Maximum results shown Ignore hotkeys in fullscreen mode + Disable Flow Launcher activation when a full screen application is active (Recommended for games). Python Directory Auto Update - Auto Hide Scroll Bar - Automatically hides the Settings window scroll bar and show when hover the mouse over it Select Hide Flow Launcher on startup Hide tray icon Query Search Precision + Changes minimum match score required for results. Should Use Pinyin - Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for transliterating Chinese + Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese Shadow effect is not allowed while current theme has blur effect enabled - Plugin + Plugins Find more plugins - Enable - Disable - Action keyword: + On + Off + Action keyword Current action keyword: New action keyword: Current Priority: New Priority: - Priority: + Priority Plugin Directory - Author + Author: Init time: Query time: + + + Plugin Store + Refresh + Install + Theme Browse for more themes @@ -67,12 +76,17 @@ Opacity Theme {0} not exists, fallback to default theme Fail to load theme {0}, fallback to default theme + Theme Folder + Open Theme Folder Hotkey Flow Launcher Hotkey - Open Result Modifiers + Enter shortcut to show/hide Flow Launcher. + Open Result Modifier Key + Select a modifier key to open selected result via keyboard. Show Hotkey + Show result selection hotkey with results. Custom Query Hotkey Query Delete @@ -81,8 +95,9 @@ Please select an item Are you sure you want to delete {0} plugin hotkey? Query window shadow effect - Shadow effect has a substantial usage of GPU. - Not recommended if your computer performance is limited. + Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited. + Use Segoe Fluent Icons + Use Segoe Fluent Icons for query results where supported HTTP Proxy @@ -116,6 +131,7 @@ Usage Tips: + Change Priority Greater the number, the higher the result will be ranked. Try setting it as 5. If you want the results to be lower than any other plugin's, provide a negative number Please provide an valid integer for Priority! @@ -179,4 +195,4 @@ Update files Update description - \ No newline at end of file + diff --git a/Flow.Launcher/Languages/ko.xaml b/Flow.Launcher/Languages/ko.xaml index 6c755dbae..649895cea 100644 --- a/Flow.Launcher/Languages/ko.xaml +++ b/Flow.Launcher/Languages/ko.xaml @@ -13,55 +13,90 @@ 설정 정보 종료 + 닫기 Flow Launcher 설정 일반 + 포터블 모드 + 모든 설정이 폴더안에 들어갑니다. USB 드라이브나 클라우드로 사용 가능합니다. 시스템 시작 시 Flow Launcher 실행 포커스 잃으면 Flow Launcher 숨김 새 버전 알림 끄기 마지막 실행 위치 기억 언어 마지막 쿼리 스타일 + 쿼리박스를 열었을 때 쿼리 처리 방식 직전 쿼리에 계속 입력 직전 쿼리 내용 선택 직전 쿼리 지우기 표시할 결과 수 전체화면 모드에서는 핫키 무시 + 게이머라면 켜는 것을 추천합니다. Python 디렉토리 자동 업데이트 선택 시작 시 Flow Launcher 숨김 + 트레이 아이콘 숨기기 + 쿼리 검색 정확도 + 검색 결과가 좀 더 정확해집니다. + 항상 Pinyin 사용 + Pinyin을 사용하여 검색할 수 있습니다. Pinyin(병음)은 로마자 중국어 입력 방식입니다. + 반투명 흐림 효과를 사용하는 경우, 그림자 효과를 쓸 수 없습니다. 플러그인 플러그인 더 찾아보기 - 비활성화 + On + Off 액션 키워드 + 현재 중요도: + 새 중요도: + 중요도 플러그인 디렉토리 저자 초기화 시간: 쿼리 시간: + + + + 플러그인 스토어 + 새로고침 + 설치 + 테마 테마 더 찾아보기 + Hi There 쿼리 상자 글꼴 결과 항목 글꼴 윈도우 모드 투명도 + {0} 테마가 존재하지 않습니다. 기본 테마로 변경합니다. + {0} 테마 로드에 실패했습니다. 기본 테마로 변경합니다. + 테마 폴더 + 테마 폴더 열기 핫키 Flow Launcher 핫키 - 결과 수정 자 열기 - 사용자지정 쿼리 핫키 + Flow Launcher를 열 때 사용할 단축키를 입력합니다. + 결과 선택 단축키 + 결과 목록을 선택하는 단축키입니다. 단축키 표시 + 결과창에서 결과 선택 단축키를 표시합니다. + 사용자지정 쿼리 핫키 + Query 삭제 편집 추가 항목을 선택하세요. {0} 플러그인 핫키를 삭제하시겠습니까? + 그림자 효과 + 그림자 효과는 GPU를 사용합니다. 컴퓨터 퍼포먼스가 제한적인 경우 사용을 추천하지 않습니다. + 플루언트 아이콘 사용 + 결과 및 일부 메뉴에서 플루언트 아이콘을 사용합니다. HTTP 프록시 @@ -86,8 +121,18 @@ Flow Launcher를 {0}번 실행했습니다. 업데이트 확인 새 버전({0})이 있습니다. Flow Launcher를 재시작하세요. + 업데이트 확인을 실패했습니다. api.github.com로의 연결 또는 프록시 설정을 확인해주세요. + + 업데이트 다운로드에 실패했습니다. github-cloud.s3.amazonaws.com의 연결 또는 프록시 설정을 확인해주세요. + 수동 다운로드를 하려면 https://github.com/Flow-Launcher/Flow.Launcher/releases 으로 방문하세요. + 릴리즈 노트: + 사용 팁: + + 중요도 변경 + 높은 수를 넣을수록 상위 결과에 표시됩니다. 5를 시도해보세요. 다른 플러그인 보다 결과를 낮추고 싶다면, 그보다 낮은 수를 입력하세요. + 중요도에 올바른 정수를 입력하세요. 예전 액션 키워드 새 액션 키워드 @@ -97,9 +142,11 @@ 새 액션 키워드를 입력하세요. 새 액션 키워드가 할당된 플러그인이 이미 있습니다. 다른 액션 키워드를 입력하세요. 성공 + 성공적으로 완료했습니다. 액션 키워드를 지정하지 않으려면 *를 사용하세요. + 커스텀 플러그인 핫키 미리보기 핫키를 사용할 수 없습니다. 다른 핫키를 입력하세요. 플러그인 핫키가 유효하지 않습니다. @@ -123,12 +170,23 @@ 보고서를 정상적으로 보냈습니다. 보고서를 보내지 못했습니다. Flow Launcher에 문제가 발생했습니다. - + + + 잠시 기다려주세요... + 새 업데이트 확인 중 새 Flow Launcher 버전({0})을 사용할 수 있습니다. + 이미 가장 최신 버전의 Flow Launcher를 사용중입니다. + 업데이트 발견 + 업데이트 중... + Flow Launcher가 유저 정보 데이터를 새버전으로 옮길 수 없습니다. + 프로필 데이터 폴더를 수동으로 {0} 에서 {1}로 옮겨주세요. + 새 업데이트 소프트웨어 업데이트를 설치하는 중에 오류가 발생했습니다. 업데이트 취소 + 업데이트 실패 + Check your connection and try updating proxy settings to github-cloud.s3.amazonaws.com. 업데이트를 위해 Flow Launcher를 재시작합니다. 아래 파일들이 업데이트됩니다. 업데이트 파일 diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml index 346c70837..70a5d3b7c 100644 --- a/Flow.Launcher/Languages/sk.xaml +++ b/Flow.Launcher/Languages/sk.xaml @@ -31,8 +31,6 @@ Ignorovať klávesové skratky v režime na celú obrazovku Priečinok s Pythonom Automatická aktualizácia - Automaticky skryť posuvník - Automaticky skrývať posuvník v okne nastavení a zobraziť ho, keď naň prejdete myšou Vybrať Schovať Flow Launcher po spustení Schovať ikonu z oblasti oznámení @@ -81,8 +79,9 @@ Vyberte položku, prosím Ste si istý, že chcete odstrániť klávesovú skratku {0} pre plugin? Tieňový efekt v poli vyhľadávania - Tieňový efekt významne využíva GPU. - Neodporúča sa, ak je výkon počítača obmedzený. + Tieňový efekt významne využíva GPU. Neodporúča sa, ak je výkon počítača obmedzený. + Použiť ikony Segoe Fluent + Použiť ikony Segoe Fluent, ak sú podporované HTTP Proxy @@ -179,4 +178,4 @@ Aktualizovať súbory Aktualizovať popis - \ No newline at end of file + diff --git a/Flow.Launcher/Languages/zh-cn.xaml b/Flow.Launcher/Languages/zh-cn.xaml index efc7f19d9..0c2307dc5 100644 --- a/Flow.Launcher/Languages/zh-cn.xaml +++ b/Flow.Launcher/Languages/zh-cn.xaml @@ -76,8 +76,7 @@ 请选择一项 你确定要删除插件 {0} 的热键吗? 查询窗口阴影效果 - 阴影效果将占用大量的GPU资源。 - 如果您的计算机性能有限,则不建议使用。 + 阴影效果将占用大量的GPU资源。 如果您的计算机性能有限,则不建议使用。 HTTP 代理 diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index fcc3af5c5..b517faf19 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -30,6 +30,7 @@ d:DataContext="{d:DesignInstance vm:MainViewModel}"> + @@ -45,7 +46,10 @@ + + + @@ -61,13 +65,12 @@ - + + IsEnabled="False"> @@ -81,34 +84,67 @@ PreviewDragOver="OnPreviewDragOver" AllowDrop="True" Visibility="Visible" - Background="Transparent" - Margin="18,0,56,0"> + Background="Transparent"> - + + - + + + - + + - - - - - - - - - - + Y1="0" Y2="0" X1="-150" X2="-50" Height="2" Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}},Path=ActualWidth}" StrokeThickness="1"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 7521c65d2..057c3f07d 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -207,9 +207,9 @@ namespace Flow.Launcher private void InitProgressbarAnimation() { - var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, + var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 150, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); - var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); + var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth + 50, new Duration(new TimeSpan(0, 0, 0, 0, 1600))); Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)")); Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)")); _progressBarStoryboard.Children.Add(da); @@ -262,7 +262,7 @@ namespace Flow.Launcher { if (_settings.HideWhenDeactive) { - Hide(); + _viewModel.Hide(); } } @@ -313,25 +313,43 @@ namespace Flow.Launcher /// private void OnKeyDown(object sender, KeyEventArgs e) { - if (e.Key == Key.Down) + switch (e.Key) { - _viewModel.SelectNextItemCommand.Execute(null); - e.Handled = true; - } - else if (e.Key == Key.Up) - { - _viewModel.SelectPrevItemCommand.Execute(null); - e.Handled = true; - } - else if (e.Key == Key.PageDown) - { - _viewModel.SelectNextPageCommand.Execute(null); - e.Handled = true; - } - else if (e.Key == Key.PageUp) - { - _viewModel.SelectPrevPageCommand.Execute(null); - e.Handled = true; + case Key.Down: + _viewModel.SelectNextItemCommand.Execute(null); + e.Handled = true; + break; + case Key.Up: + _viewModel.SelectPrevItemCommand.Execute(null); + e.Handled = true; + break; + case Key.PageDown: + _viewModel.SelectNextPageCommand.Execute(null); + e.Handled = true; + break; + case Key.PageUp: + _viewModel.SelectPrevPageCommand.Execute(null); + e.Handled = true; + break; + case Key.Right: + if (_viewModel.SelectedIsFromQueryResults() + && QueryTextBox.CaretIndex == QueryTextBox.Text.Length + && !string.IsNullOrEmpty(QueryTextBox.Text)) + { + _viewModel.LoadContextMenuCommand.Execute(null); + e.Handled = true; + } + break; + case Key.Left: + if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0) + { + _viewModel.EscCommand.Execute(null); + e.Handled = true; + } + break; + default: + break; + } } diff --git a/Flow.Launcher/Notification.cs b/Flow.Launcher/Notification.cs new file mode 100644 index 000000000..2c82e1451 --- /dev/null +++ b/Flow.Launcher/Notification.cs @@ -0,0 +1,42 @@ +using Flow.Launcher.Infrastructure; +using System; +using System.IO; +using Windows.Data.Xml.Dom; +using Windows.UI.Notifications; + +namespace Flow.Launcher +{ + internal static class Notification + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "")] + public static void Show(string title, string subTitle, string iconPath) + { + var legacy = Environment.OSVersion.Version.Major < 10; + // Handle notification for win7/8 + if (legacy) + { + LegacyShow(title, subTitle, iconPath); + return; + } + + // Using Windows Notification System + var Icon = !File.Exists(iconPath) + ? Path.Combine(Constant.ProgramDirectory, "Images\\app.png") + : iconPath; + + var xml = $"\"meziantou\"/{title}" + + $"{subTitle}"; + var toastXml = new XmlDocument(); + toastXml.LoadXml(xml); + var toast = new ToastNotification(toastXml); + ToastNotificationManager.CreateToastNotifier("Flow Launcher").Show(toast); + + } + + private static void LegacyShow(string title, string subTitle, string iconPath) + { + var msg = new Msg(); + msg.Show(title, subTitle, iconPath); + } + } +} \ No newline at end of file diff --git a/Flow.Launcher/PriorityChangeWindow.xaml b/Flow.Launcher/PriorityChangeWindow.xaml index 68b5a49b7..60a289e61 100644 --- a/Flow.Launcher/PriorityChangeWindow.xaml +++ b/Flow.Launcher/PriorityChangeWindow.xaml @@ -7,37 +7,51 @@ Loaded="PriorityChangeWindow_Loaded" mc:Ignorable="d" WindowStartupLocation="CenterScreen" - Title="PriorityChangeWindow" Height="250" Width="300"> + Title="{DynamicResource changePriorityWindow}" Height="400" Width="350" ResizeMode="NoResize" Background="#f3f3f3"> - - + + - - - - - - - + + +  + - + + + + + + + - - diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index f3403696e..71f6f1be4 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -48,12 +48,7 @@ namespace Flow.Launcher public void ChangeQuery(string query, bool requery = false) { - _mainVM.ChangeQueryText(query); - } - - public void ChangeQueryText(string query, bool selectAll = false) - { - _mainVM.ChangeQueryText(query); + _mainVM.ChangeQueryText(query, requery); } public void RestartApp() @@ -73,6 +68,8 @@ namespace Flow.Launcher public void RestarApp() => RestartApp(); + public void ShowMainWindow() => _mainVM.MainWindowVisibility = Visibility.Visible; + public void CheckForNewUpdate() => _settingsVM.UpdateApp(); public void SaveAppAllSettings() @@ -95,8 +92,7 @@ namespace Flow.Launcher { Application.Current.Dispatcher.Invoke(() => { - var msg = useMainWindowAsOwner ? new Msg {Owner = Application.Current.MainWindow} : new Msg(); - msg.Show(title, subTitle, iconPath); + Notification.Show(title, subTitle, iconPath); }); } diff --git a/Flow.Launcher/Resources/Segoe Fluent Icons.ttf b/Flow.Launcher/Resources/Segoe Fluent Icons.ttf new file mode 100644 index 000000000..8f05a4bbc Binary files /dev/null and b/Flow.Launcher/Resources/Segoe Fluent Icons.ttf differ diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index 0f70dce41..d52474d3f 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -20,6 +20,7 @@ IsSynchronizedWithCurrentItem="True" PreviewMouseDown="ListBox_PreviewMouseDown"> + - + + @@ -101,7 +110,7 @@ + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + + + + + + + + + +  + + + + + - - + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + +  + - - - + + + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + +  + + + + - + + + + + + + + + + + + + + +  + + - + + + + + + + + + + + + + + + + + + + + + + + + +  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /// - public void ChangeQueryText(string queryText) + public void ChangeQueryText(string queryText, bool reQuery = false) { - QueryText = queryText; + if (QueryText!=queryText) + { + // re-query is done in QueryText's setter method + QueryText = queryText; + } + else if (reQuery) + { + Query(); + } QueryTextCursorMovedToEnd = true; } public bool LastQuerySelected { get; set; } + + // This is not a reliable indicator of the cursor's position, it is manually set for a specific purpose. public bool QueryTextCursorMovedToEnd { get; set; } private ResultsViewModel _selectedResults; @@ -333,7 +371,6 @@ namespace Flow.Launcher.ViewModel } public Visibility ProgressBarVisibility { get; set; } - public Visibility MainWindowVisibility { get; set; } public ICommand EscCommand { get; set; } @@ -347,6 +384,7 @@ namespace Flow.Launcher.ViewModel public ICommand LoadHistoryCommand { get; set; } public ICommand OpenResultCommand { get; set; } public ICommand ReloadPluginDataCommand { get; set; } + public ICommand ClearQueryCommand { get; private set; } public string OpenResultCommandModifiers { get; private set; } @@ -595,6 +633,7 @@ namespace Flow.Launcher.ViewModel { Title = InternationalizationManager.Instance.GetTranslation("setAsTopMostInThisQuery"), IcoPath = "Images\\up.png", + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xeac2"), PluginDirectory = Constant.ProgramDirectory, Action = _ => { @@ -632,7 +671,7 @@ namespace Flow.Launcher.ViewModel return menu; } - private bool SelectedIsFromQueryResults() + internal bool SelectedIsFromQueryResults() { var selected = SelectedResults == Results; return selected; @@ -657,7 +696,7 @@ namespace Flow.Launcher.ViewModel OpenResultCommandModifiers = _settings.OpenResultModifiers ?? DefaultOpenResultModifiers; } - internal void ToggleFlowLauncher() + public async void ToggleFlowLauncher() { if (MainWindowVisibility != Visibility.Visible) { @@ -665,12 +704,48 @@ namespace Flow.Launcher.ViewModel } else { + switch (_settings.LastQueryMode) + { + case LastQueryMode.Empty: + ChangeQueryText(string.Empty); + Application.Current.MainWindow.Opacity = 0; // Trick for no delay + await Task.Delay(100); + Application.Current.MainWindow.Opacity = 1; + break; + case LastQueryMode.Preserved: + LastQuerySelected = true; + break; + case LastQueryMode.Selected: + LastQuerySelected = false; + break; + default: + throw new ArgumentException($"wrong LastQueryMode: <{_settings.LastQueryMode}>"); + } MainWindowVisibility = Visibility.Collapsed; } } + public void Hide() + { + if (MainWindowVisibility != Visibility.Collapsed) + { + ToggleFlowLauncher(); + } + } + #endregion + + /// + /// Checks if Flow Launcher should ignore any hotkeys + /// + public bool ShouldIgnoreHotkeys() + { + return _settings.IgnoreHotkeysOnFullscreen && WindowsInteropHelper.IsWindowFullscreen(); + } + + + #region Public Methods public void Save() diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 1ac320cf0..209198822 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -3,32 +3,46 @@ using System.Windows.Media; using Flow.Launcher.Plugin; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Core.Plugin; +using System.Windows.Controls; namespace Flow.Launcher.ViewModel { public class PluginViewModel : BaseModel { - public PluginPair PluginPair { get; set; } + private readonly PluginPair _pluginPair; + public PluginPair PluginPair + { + get => _pluginPair; + init + { + _pluginPair = value; + value.Metadata.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(PluginPair.Metadata.AvgQueryTime)) + OnPropertyChanged(nameof(QueryTime)); + }; + } + } public ImageSource Image => ImageLoader.Load(PluginPair.Metadata.IcoPath); public bool PluginState { - get { return !PluginPair.Metadata.Disabled; } - set - { - PluginPair.Metadata.Disabled = !value; - } + get => !PluginPair.Metadata.Disabled; + set => PluginPair.Metadata.Disabled = !value; } + + private Control _settingControl; + public Control SettingControl => _settingControl ??= PluginPair.Plugin is not ISettingProvider settingProvider ? new Control() : settingProvider.CreateSettingPanel(); + public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed; - public string InitilizaTime => PluginPair.Metadata.InitTime.ToString() + "ms"; + public string InitilizaTime => PluginPair.Metadata.InitTime + "ms"; public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms"; - public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords); + public string ActionKeywordsText => string.Join(Query.ActionKeywordSeparator, PluginPair.Metadata.ActionKeywords); public int Priority => PluginPair.Metadata.Priority; public void ChangeActionKeyword(string newActionKeyword, string oldActionKeyword) { PluginManager.ReplaceActionKeyword(PluginPair.Metadata.ID, oldActionKeyword, newActionKeyword); - OnPropertyChanged(nameof(ActionKeywordsText)); } @@ -38,6 +52,7 @@ namespace Flow.Launcher.ViewModel OnPropertyChanged(nameof(Priority)); } - public bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword); + public static bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword); } -} + +} \ No newline at end of file diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 190f592d7..ed3d842e3 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -6,6 +6,7 @@ using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using System.IO; namespace Flow.Launcher.ViewModel { @@ -16,24 +17,78 @@ namespace Flow.Launcher.ViewModel if (result != null) { Result = result; + + if (Result.Glyph is { FontFamily: not null } glyph) + { + // Checks if it's a system installed font, which does not require path to be provided. + if (glyph.FontFamily.EndsWith(".ttf") || glyph.FontFamily.EndsWith(".otf")) + { + var fontPath = Result.Glyph.FontFamily; + Glyph = Path.IsPathRooted(fontPath) + ? Result.Glyph + : Result.Glyph with + { + FontFamily = Path.Combine(Result.PluginDirectory, fontPath) + }; + } + else + { + Glyph = glyph; + } + } } Settings = settings; } - public Settings Settings { get; private set; } + private Settings Settings { get; } - public Visibility ShowOpenResultHotkey => Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Hidden; + public Visibility ShowOpenResultHotkey => + Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Collapsed; + + public Visibility ShowIcon + { + get + { + // If both glyph and image icons are not available, it will then be the default icon + if (!ImgIconAvailable && !GlyphAvailable) + return Visibility.Visible; + + // Although user can choose to use glyph icons, plugins may choose to supply only image icons. + // In this case we ignore the setting because otherwise icons will not display as intended + if (Settings.UseGlyphIcons && !GlyphAvailable && ImgIconAvailable) + return Visibility.Visible; + + return !Settings.UseGlyphIcons && ImgIconAvailable ? Visibility.Visible : Visibility.Hidden; + } + } + + public Visibility ShowGlyph + { + get + { + // Although user can choose to not use glyph icons, plugins may choose to supply only glyph icons. + // In this case we ignore the setting because otherwise icons will not display as intended + if (!Settings.UseGlyphIcons && !ImgIconAvailable && GlyphAvailable) + return Visibility.Visible; + + return Settings.UseGlyphIcons && GlyphAvailable ? Visibility.Visible : Visibility.Hidden; + } + } + + private bool GlyphAvailable => Glyph is not null; + + private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPath) || Result.Icon is not null; public string OpenResultModifiers => Settings.OpenResultModifiers; public string ShowTitleToolTip => string.IsNullOrEmpty(Result.TitleToolTip) - ? Result.Title - : Result.TitleToolTip; + ? Result.Title + : Result.TitleToolTip; public string ShowSubTitleToolTip => string.IsNullOrEmpty(Result.SubTitleToolTip) - ? Result.SubTitle - : Result.SubTitleToolTip; + ? Result.SubTitle + : Result.SubTitleToolTip; private volatile bool ImageLoaded; @@ -48,10 +103,14 @@ namespace Flow.Launcher.ViewModel ImageLoaded = true; _ = LoadImageAsync(); } + return image; } private set => image = value; } + + public GlyphInfo Glyph { get; set; } + private async ValueTask LoadImageAsync() { var imagePath = Result.IcoPath; @@ -64,7 +123,9 @@ namespace Flow.Launcher.ViewModel } catch (Exception e) { - Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e); + Log.Exception( + $"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", + e); } } @@ -74,7 +135,7 @@ namespace Flow.Launcher.ViewModel image = ImageLoader.Load(imagePath); return; } - + // We need to modify the property not field here to trigger the OnPropertyChanged event Image = await Task.Run(() => ImageLoader.Load(imagePath)).ConfigureAwait(false); } diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 0766c7bbc..01a19d871 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -42,7 +42,7 @@ namespace Flow.Launcher.ViewModel #region Properties - public int MaxHeight => MaxResults * 50; + public int MaxHeight => MaxResults * 52; public int SelectedIndex { get; set; } @@ -166,7 +166,7 @@ namespace Flow.Launcher.ViewModel switch (Visbility) { case Visibility.Collapsed when Results.Count > 0: - Margin = new Thickness { Top = 8 }; + Margin = new Thickness { Top = 0 }; SelectedIndex = 0; Visbility = Visibility.Visible; break; diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index 53789d2d9..dde540b0c 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -3,12 +3,14 @@ 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.Configuration; +using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; @@ -46,7 +48,7 @@ namespace Flow.Launcher.ViewModel public async void UpdateApp() { - await _updater.UpdateApp(App.API, false); + await _updater.UpdateAppAsync(App.API, false); } public bool AutoUpdates @@ -61,12 +63,6 @@ namespace Flow.Launcher.ViewModel } } - public bool AutoHideScrollBar - { - get => Settings.AutoHideScrollBar; - set => Settings.AutoHideScrollBar = value; - } - // 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 @@ -237,6 +233,14 @@ namespace Flow.Launcher.ViewModel } } + public IList ExternalPlugins + { + get + { + return PluginsManifest.UserPlugins; + } + } + public Control SettingProvider { get @@ -256,6 +260,12 @@ namespace Flow.Launcher.ViewModel } } + public async Task RefreshExternalPluginsAsync() + { + await PluginsManifest.UpdateManifestAsync(); + OnPropertyChanged(nameof(ExternalPlugins)); + } + #endregion @@ -304,6 +314,12 @@ namespace Flow.Launcher.ViewModel } } + public bool UseGlyphIcons + { + get { return Settings.UseGlyphIcons; } + set { Settings.UseGlyphIcons = value; } + } + public Brush PreviewBackground { get diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Bookmark.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Bookmark.cs deleted file mode 100644 index 790c03686..000000000 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Bookmark.cs +++ /dev/null @@ -1,56 +0,0 @@ -using BinaryAnalysis.UnidecodeSharp; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - - -namespace Flow.Launcher.Plugin.BrowserBookmark -{ - public class Bookmark : IEquatable, IEqualityComparer - { - private string m_Name; - public string Name - { - get - { - return m_Name; - } - set - { - m_Name = value; - } - } - public string Url { get; set; } - public string Source { get; set; } - public int Score { get; set; } - - /* TODO: since Source maybe unimportant, we just need to compare Name and Url */ - public bool Equals(Bookmark other) - { - return Equals(this, other); - } - - public bool Equals(Bookmark x, Bookmark y) - { - if (Object.ReferenceEquals(x, y)) return true; - if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null)) - return false; - - return x.Name == y.Name && x.Url == y.Url; - } - - public int GetHashCode(Bookmark bookmark) - { - if (Object.ReferenceEquals(bookmark, null)) return 0; - int hashName = bookmark.Name == null ? 0 : bookmark.Name.GetHashCode(); - int hashUrl = bookmark.Url == null ? 0 : bookmark.Url.GetHashCode(); - return hashName ^ hashUrl; - } - - public override int GetHashCode() - { - return GetHashCode(this); - } - } -} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs new file mode 100644 index 000000000..ec6f8423d --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs @@ -0,0 +1,27 @@ +using Flow.Launcher.Plugin.BrowserBookmark.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Flow.Launcher.Plugin.BrowserBookmark +{ + public class ChromeBookmarkLoader : ChromiumBookmarkLoader + { + public override List GetBookmarks() + { + return LoadChromeBookmarks(); + } + + private List LoadChromeBookmarks() + { + var bookmarks = new List(); + var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome\User Data"), "Google Chrome")); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome SxS\User Data"), "Google Chrome Canary")); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Chromium\User Data"), "Chromium")); + return bookmarks; + } + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarks.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarks.cs deleted file mode 100644 index def7f3abe..000000000 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarks.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Flow.Launcher.Plugin.BrowserBookmark -{ - public class ChromeBookmarks - { - private List bookmarks = new List(); - - public List GetBookmarks() - { - bookmarks.Clear(); - LoadChromeBookmarks(); - - return bookmarks; - } - - private void ParseChromeBookmarks(String path, string source) - { - if (!File.Exists(path)) return; - - string all = File.ReadAllText(path); - Regex nameRegex = new Regex("\"name\": \"(?.*?)\""); - MatchCollection nameCollection = nameRegex.Matches(all); - Regex typeRegex = new Regex("\"type\": \"(?.*?)\""); - MatchCollection typeCollection = typeRegex.Matches(all); - Regex urlRegex = new Regex("\"url\": \"(?.*?)\""); - MatchCollection urlCollection = urlRegex.Matches(all); - - List names = (from Match match in nameCollection select match.Groups["name"].Value).ToList(); - List types = (from Match match in typeCollection select match.Groups["type"].Value).ToList(); - List urls = (from Match match in urlCollection select match.Groups["url"].Value).ToList(); - - int urlIndex = 0; - for (int i = 0; i < names.Count; i++) - { - string name = DecodeUnicode(names[i]); - string type = types[i]; - if (type == "url") - { - string url = urls[urlIndex]; - urlIndex++; - - if (url == null) continue; - if (url.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase)) continue; - if (url.StartsWith("vbscript:", StringComparison.OrdinalIgnoreCase)) continue; - - bookmarks.Add(new Bookmark() - { - Name = name, - Url = url, - Source = source - }); - } - } - } - - private void LoadChromeBookmarks(string path, string name) - { - if (!Directory.Exists(path)) return; - var paths = Directory.GetDirectories(path); - - foreach (var profile in paths) - { - if (File.Exists(Path.Combine(profile, "Bookmarks"))) - ParseChromeBookmarks(Path.Combine(profile, "Bookmarks"), name + (Path.GetFileName(profile) == "Default" ? "" : (" (" + Path.GetFileName(profile) + ")"))); - } - } - - private void LoadChromeBookmarks() - { - String platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - LoadChromeBookmarks(Path.Combine(platformPath, @"Google\Chrome\User Data"), "Google Chrome"); - LoadChromeBookmarks(Path.Combine(platformPath, @"Google\Chrome SxS\User Data"), "Google Chrome Canary"); - LoadChromeBookmarks(Path.Combine(platformPath, @"Chromium\User Data"), "Chromium"); - } - - private String DecodeUnicode(String dataStr) - { - Regex reg = new Regex(@"(?i)\\[uU]([0-9a-f]{4})"); - return reg.Replace(dataStr, m => ((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString()); - } - } -} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs new file mode 100644 index 000000000..d7b412392 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs @@ -0,0 +1,67 @@ +using Flow.Launcher.Plugin.BrowserBookmark.Models; +using Microsoft.AspNetCore.Authentication; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace Flow.Launcher.Plugin.BrowserBookmark +{ + public abstract class ChromiumBookmarkLoader : IBookmarkLoader + { + public abstract List GetBookmarks(); + protected List LoadBookmarks(string browserDataPath, string name) + { + var bookmarks = new List(); + if (!Directory.Exists(browserDataPath)) return bookmarks; + var paths = Directory.GetDirectories(browserDataPath); + + foreach (var profile in paths) + { + var bookmarkPath = Path.Combine(profile, "Bookmarks"); + if (!File.Exists(bookmarkPath)) + continue; + + var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})"); + bookmarks.AddRange(LoadBookmarksFromFile(bookmarkPath, source)); + } + return bookmarks; + } + + protected List LoadBookmarksFromFile(string path, string source) + { + if (!File.Exists(path)) + return new(); + var bookmarks = new List(); + using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path)); + if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement)) + return new(); + foreach (var folder in rootElement.EnumerateObject()) + { + EnumerateFolderBookmark(folder.Value, bookmarks, source); + } + return bookmarks; + } + + private void EnumerateFolderBookmark(JsonElement folderElement, List bookmarks, string source) + { + if (!folderElement.TryGetProperty("children", out var childrenElement)) + return; + foreach (var subElement in childrenElement.EnumerateArray()) + { + switch (subElement.GetProperty("type").GetString()) + { + case "folder": + EnumerateFolderBookmark(subElement, bookmarks, source); + break; + default: + bookmarks.Add(new Bookmark( + subElement.GetProperty("name").GetString(), + subElement.GetProperty("url").GetString(), + source)); + break; + } + } + + } + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs new file mode 100644 index 000000000..40d10b26f --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin.BrowserBookmark.Models; +using Flow.Launcher.Plugin.SharedModels; + +namespace Flow.Launcher.Plugin.BrowserBookmark.Commands +{ + internal static class BookmarkLoader + { + internal static MatchResult MatchProgram(Bookmark bookmark, string queryString) + { + var match = StringMatcher.FuzzySearch(queryString, bookmark.Name); + if (match.IsSearchPrecisionScoreMet()) + return match; + + return StringMatcher.FuzzySearch(queryString, bookmark.Url); + } + + internal static List LoadAllBookmarks(Settings setting) + { + + var chromeBookmarks = new ChromeBookmarkLoader(); + var mozBookmarks = new FirefoxBookmarkLoader(); + var edgeBookmarks = new EdgeBookmarkLoader(); + + var allBookmarks = new List(); + + // Add Firefox bookmarks + allBookmarks.AddRange(mozBookmarks.GetBookmarks()); + + // Add Chrome bookmarks + allBookmarks.AddRange(chromeBookmarks.GetBookmarks()); + + // Add Edge (Chromium) bookmarks + allBookmarks.AddRange(edgeBookmarks.GetBookmarks()); + + foreach (var browser in setting.CustomChromiumBrowsers) + { + var loader = new CustomChromiumBookmarkLoader(browser); + allBookmarks.AddRange(loader.GetBookmarks()); + } + + return allBookmarks.Distinct().ToList(); + } + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs deleted file mode 100644 index 60c4a0ee6..000000000 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/Bookmarks.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Plugin.SharedModels; - -namespace Flow.Launcher.Plugin.BrowserBookmark.Commands -{ - internal static class Bookmarks - { - internal static MatchResult MatchProgram(Bookmark bookmark, string queryString) - { - var match = StringMatcher.FuzzySearch(queryString, bookmark.Name); - if (match.IsSearchPrecisionScoreMet()) - return match; - - return StringMatcher.FuzzySearch(queryString, bookmark.Url); - } - - internal static List LoadAllBookmarks() - { - var allbookmarks = new List(); - - var chromeBookmarks = new ChromeBookmarks(); - var mozBookmarks = new FirefoxBookmarks(); - var edgeBookmarks = new EdgeBookmarks(); - - //TODO: Let the user select which browser's bookmarks are displayed - // Add Firefox bookmarks - mozBookmarks.GetBookmarks().ForEach(x => allbookmarks.Add(x)); - - // Add Chrome bookmarks - chromeBookmarks.GetBookmarks().ForEach(x => allbookmarks.Add(x)); - - // Add Edge (Chromium) bookmarks - edgeBookmarks.GetBookmarks().ForEach(x => allbookmarks.Add(x)); - - return allbookmarks.Distinct().ToList(); - } - } -} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs new file mode 100644 index 000000000..fa98f4d7c --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs @@ -0,0 +1,19 @@ +using Flow.Launcher.Plugin.BrowserBookmark.Models; +using System.Collections.Generic; + +namespace Flow.Launcher.Plugin.BrowserBookmark +{ + public class CustomChromiumBookmarkLoader : ChromiumBookmarkLoader + { + public CustomChromiumBookmarkLoader(CustomBrowser browser) + { + BrowserName = browser.Name; + BrowserDataPath = browser.DataDirectoryPath; + } + public string BrowserDataPath { get; init; } + public string BookmarkFilePath { get; init; } + public string BrowserName { get; init; } + + public override List GetBookmarks() => BrowserDataPath != null ? LoadBookmarks(BrowserDataPath, BrowserName) : LoadBookmarksFromFile(BookmarkFilePath, BrowserName); + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs new file mode 100644 index 000000000..276f6368e --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs @@ -0,0 +1,26 @@ +using Flow.Launcher.Plugin.BrowserBookmark.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Flow.Launcher.Plugin.BrowserBookmark +{ + public class EdgeBookmarkLoader : ChromiumBookmarkLoader + { + private List LoadEdgeBookmarks() + { + var bookmarks = new List(); + var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge\User Data"), "Microsoft Edge")); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge Dev\User Data"), "Microsoft Edge Dev")); + bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge SxS\User Data"), "Microsoft Edge Canary")); + + return bookmarks; + } + + public override List GetBookmarks() => LoadEdgeBookmarks(); + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarks.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarks.cs deleted file mode 100644 index 376808549..000000000 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarks.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Flow.Launcher.Plugin.BrowserBookmark -{ - public class EdgeBookmarks - { - private List bookmarks = new List(); - - public List GetBookmarks() - { - bookmarks.Clear(); - LoadEdgeBookmarks(); - - return bookmarks; - } - - private void ParseEdgeBookmarks(string path, string source) - { - if (!File.Exists(path)) return; - - string all = File.ReadAllText(path); - Regex nameRegex = new Regex("\"name\": \"(?.*?)\""); - MatchCollection nameCollection = nameRegex.Matches(all); - Regex typeRegex = new Regex("\"type\": \"(?.*?)\""); - MatchCollection typeCollection = typeRegex.Matches(all); - Regex urlRegex = new Regex("\"url\": \"(?.*?)\""); - MatchCollection urlCollection = urlRegex.Matches(all); - - List names = (from Match match in nameCollection select match.Groups["name"].Value).ToList(); - List types = (from Match match in typeCollection select match.Groups["type"].Value).ToList(); - List urls = (from Match match in urlCollection select match.Groups["url"].Value).ToList(); - - int urlIndex = 0; - for (int i = 0; i < names.Count; i++) - { - string name = DecodeUnicode(names[i]); - string type = types[i]; - if (type == "url") - { - string url = urls[urlIndex]; - urlIndex++; - - if (url == null) continue; - if (url.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase)) continue; - if (url.StartsWith("vbscript:", StringComparison.OrdinalIgnoreCase)) continue; - - bookmarks.Add(new Bookmark() - { - Name = name, - Url = url, - Source = source - }); - } - } - } - - private void LoadEdgeBookmarks(string path, string name) - { - if (!Directory.Exists(path)) return; - var paths = Directory.GetDirectories(path); - - foreach (var profile in paths) - { - if (File.Exists(Path.Combine(profile, "Bookmarks"))) - ParseEdgeBookmarks(Path.Combine(profile, "Bookmarks"), name + (Path.GetFileName(profile) == "Default" ? "" : (" (" + Path.GetFileName(profile) + ")"))); - } - } - - private void LoadEdgeBookmarks() - { - string platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - LoadEdgeBookmarks(Path.Combine(platformPath, @"Microsoft\Edge\User Data"), "Microsoft Edge"); - LoadEdgeBookmarks(Path.Combine(platformPath, @"Microsoft\Edge Dev\User Data"), "Microsoft Edge Dev"); - LoadEdgeBookmarks(Path.Combine(platformPath, @"Microsoft\Edge SxS\User Data"), "Microsoft Edge Canary"); - } - - private string DecodeUnicode(string dataStr) - { - Regex reg = new Regex(@"(?i)\\[uU]([0-9a-f]{4})"); - return reg.Replace(dataStr, m => ((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString()); - } - } -} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarks.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs similarity index 84% rename from Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarks.cs rename to Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs index b718da3cf..3df034781 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarks.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs @@ -1,3 +1,4 @@ +using Flow.Launcher.Plugin.BrowserBookmark.Models; using System; using System.Collections.Generic; using System.Data.SQLite; @@ -6,7 +7,7 @@ using System.Linq; namespace Flow.Launcher.Plugin.BrowserBookmark { - public class FirefoxBookmarks + public class FirefoxBookmarkLoader : IBookmarkLoader { private const string queryAllBookmarks = @"SELECT moz_places.url, moz_bookmarks.title FROM moz_places @@ -27,10 +28,10 @@ namespace Flow.Launcher.Plugin.BrowserBookmark if (string.IsNullOrEmpty(PlacesPath) || !File.Exists(PlacesPath)) return new List(); - var bookmarList = new List(); + var bookmarkList = new List(); // create the connection string and init the connection - string dbPath = string.Format(dbPathFormat, PlacesPath); + string dbPath = string.Format(dbPathFormat, PlacesPath); using (var dbConnection = new SQLiteConnection(dbPath)) { // Open connection to the database file and execute the query @@ -38,14 +39,13 @@ namespace Flow.Launcher.Plugin.BrowserBookmark var reader = new SQLiteCommand(queryAllBookmarks, dbConnection).ExecuteReader(); // return results in List format - bookmarList = reader.Select(x => new Bookmark() - { - Name = (x["title"] is DBNull) ? string.Empty : x["title"].ToString(), - Url = x["url"].ToString() - }).ToList(); + bookmarkList = reader.Select( + x => new Bookmark(x["title"] is DBNull ? string.Empty : x["title"].ToString(), + x["url"].ToString()) + ).ToList(); } - return bookmarList; + return bookmarkList; } /// @@ -63,7 +63,8 @@ namespace Flow.Launcher.Plugin.BrowserBookmark // get firefox default profile directory from profiles.ini string ini; - using (var sReader = new StreamReader(profileIni)) { + using (var sReader = new StreamReader(profileIni)) + { ini = sReader.ReadToEnd(); } @@ -95,7 +96,10 @@ namespace Flow.Launcher.Plugin.BrowserBookmark Version=2 */ - var lines = ini.Split(new string[] { "\r\n" }, StringSplitOptions.None).ToList(); + var lines = ini.Split(new string[] + { + "\r\n" + }, StringSplitOptions.None).ToList(); var defaultProfileFolderNameRaw = lines.Where(x => x.Contains("Default=") && x != "Default=1").FirstOrDefault() ?? string.Empty; @@ -104,14 +108,14 @@ namespace Flow.Launcher.Plugin.BrowserBookmark var defaultProfileFolderName = defaultProfileFolderNameRaw.Split('=').Last(); - var indexOfDefaultProfileAtttributePath = lines.IndexOf("Path="+ defaultProfileFolderName); + var indexOfDefaultProfileAtttributePath = lines.IndexOf("Path=" + defaultProfileFolderName); // Seen in the example above, the IsRelative attribute is always above the Path attribute var relativeAttribute = lines[indexOfDefaultProfileAtttributePath - 1]; return relativeAttribute == "0" // See above, the profile is located in a custom location, path is not relative, so IsRelative=0 - ? defaultProfileFolderName + @"\places.sqlite" - : Path.Combine(profileFolderPath, defaultProfileFolderName) + @"\places.sqlite"; + ? defaultProfileFolderName + @"\places.sqlite" + : Path.Combine(profileFolderPath, defaultProfileFolderName) + @"\places.sqlite"; } } } @@ -126,4 +130,4 @@ namespace Flow.Launcher.Plugin.BrowserBookmark } } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index acfc79d7a..2a586818c 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -64,9 +64,13 @@ - - + + + + + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs new file mode 100644 index 000000000..2c48cfd55 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs @@ -0,0 +1,10 @@ +using Flow.Launcher.Plugin.BrowserBookmark.Models; +using System.Collections.Generic; + +namespace Flow.Launcher.Plugin.BrowserBookmark +{ + public interface IBookmarkLoader + { + public List GetBookmarks(); + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml index f456e7495..ab854b8ed 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml @@ -14,4 +14,9 @@ Choose Copy url Copy the bookmark's url to clipboard + Load Browser From: + Browser Name + DataDirectoryPath + Add + Delete \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sk.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sk.xaml index cec49d79b..62f95534a 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sk.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/sk.xaml @@ -14,4 +14,9 @@ Prehliadať Kopírovať URL Kopírovať URL záložky do schránky - \ No newline at end of file + Načítať prehliadač z: + Názov prehliadača + Umiestnenie priečinka Data + Pridať + Odstrániť + diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index a0b443e75..f8610a9ec 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -26,7 +26,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark _settings = context.API.LoadSettingJsonStorage(); - cachedBookmarks = Bookmarks.LoadAllBookmarks(); + cachedBookmarks = BookmarkLoader.LoadAllBookmarks(_settings); } public List Query(Query query) @@ -45,7 +45,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark Title = c.Name, SubTitle = c.Url, IcoPath = @"Images\bookmark.png", - Score = Bookmarks.MatchProgram(c, param).Score, + Score = BookmarkLoader.MatchProgram(c, param).Score, Action = _ => { if (_settings.OpenInNewBrowserWindow) @@ -93,7 +93,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark { cachedBookmarks.Clear(); - cachedBookmarks = Bookmarks.LoadAllBookmarks(); + cachedBookmarks = BookmarkLoader.LoadAllBookmarks(_settings); } public string GetTranslatedPluginTitle() diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs new file mode 100644 index 000000000..c2fa9d977 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace Flow.Launcher.Plugin.BrowserBookmark.Models +{ + // Source may be important in the future + public record Bookmark(string Name, string Url, string Source = "") + { + public override int GetHashCode() + { + var hashName = Name?.GetHashCode() ?? 0; + var hashUrl = Url?.GetHashCode() ?? 0; + return hashName ^ hashUrl; + } + + public virtual bool Equals(Bookmark other) + { + return other != null && Name == other.Name && Url == other.Url; + } + + public List CustomBrowsers { get; set; }= new(); + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs new file mode 100644 index 000000000..c19de08a4 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs @@ -0,0 +1,26 @@ +namespace Flow.Launcher.Plugin.BrowserBookmark.Models +{ + public class CustomBrowser : BaseModel + { + private string _name; + private string _dataDirectoryPath; + public string Name + { + get => _name; + set + { + _name = value; + OnPropertyChanged(nameof(Name)); + } + } + public string DataDirectoryPath + { + get => _dataDirectoryPath; + set + { + _dataDirectoryPath = value; + OnPropertyChanged(nameof(DataDirectoryPath)); + } + } + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs index dc444fd73..5080ad301 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs @@ -1,9 +1,15 @@ -namespace Flow.Launcher.Plugin.BrowserBookmark.Models +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text.Json.Serialization; + +namespace Flow.Launcher.Plugin.BrowserBookmark.Models { public class Settings : BaseModel { public bool OpenInNewBrowserWindow { get; set; } = true; public string BrowserPath { get; set; } + + public ObservableCollection CustomChromiumBrowsers { get; set; } = new(); } } \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml new file mode 100644 index 000000000..ff3fbb38e --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + +