diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserDiscovery.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserDiscovery.cs index 3773cc8..9888207 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserDiscovery.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserDiscovery.cs @@ -1,44 +1,31 @@ using System; using System.Linq; using System.Collections.Generic; +using System.Threading; using Microsoft.Win32; namespace WebSearchShortcut.Browsers; public static class BrowserDiscovery { - private static readonly object _loadLock = new(); - private static bool _isLoaded; - private static List _cachedBrowsers = []; + private static Lazy _installedBrowsersCache = CreateInstalledBrowsersCache(); + private static Lazy CreateInstalledBrowsersCache() => + new(LoadInstalledBrowsers, LazyThreadSafetyMode.ExecutionAndPublication); + public static IReadOnlyCollection GetAllInstalledBrowsers() => _installedBrowsersCache.Value; - public static List GetAllInstalledBrowsers() + public static void Reload(bool warm = false) { - if (_isLoaded) return _cachedBrowsers; + var newCache = CreateInstalledBrowsersCache(); - lock (_loadLock) - { - if (!_isLoaded) - { - _cachedBrowsers = LoadInstalledBrowsers(); - _isLoaded = true; - } - } + Interlocked.Exchange(ref _installedBrowsersCache, newCache); - return _cachedBrowsers; - } - - public static void Reload() - { - lock (_loadLock) - { - _cachedBrowsers = LoadInstalledBrowsers(); - _isLoaded = true; - } + if (warm) + _ = newCache.Value; } - private static List LoadInstalledBrowsers() + private static BrowserInfo[] LoadInstalledBrowsers() { - List progIds = GetAssociatedProgIds(); + string[] progIds = GetAssociatedProgIds(); List result = []; foreach (var progId in progIds) @@ -56,9 +43,9 @@ private static List LoadInstalledBrowsers() return [.. result.OrderBy(b => b.Name, StringComparer.OrdinalIgnoreCase)]; } - private static List GetAssociatedProgIds() + private static string[] GetAssociatedProgIds() { - HashSet progIdSet = new HashSet(); + HashSet progIdSet = []; progIdSet.UnionWith(ScanProgIdsFromRegistry( Registry.LocalMachine, diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserExecutionInfo.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserExecutionInfo.cs index f3d5cfa..e377774 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserExecutionInfo.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserExecutionInfo.cs @@ -3,26 +3,26 @@ namespace WebSearchShortcut.Browsers; -public class BrowserExecutionInfo +internal sealed class BrowserExecutionInfo { public string? Path { get; } public string? ArgumentsPattern { get; } - public BrowserExecutionInfo(WebSearchShortcutItem item) + public BrowserExecutionInfo(WebSearchShortcutDataEntry shortcut) { DefaultBrowserProvider.UpdateIfTimePassed(); - Path = !string.IsNullOrWhiteSpace(item.BrowserPath) - ? item.BrowserPath + Path = !string.IsNullOrWhiteSpace(shortcut.BrowserPath) + ? shortcut.BrowserPath : DefaultBrowserProvider.Path; string? trimmedArgs; - if (!string.IsNullOrWhiteSpace(item.BrowserArgs)) + if (!string.IsNullOrWhiteSpace(shortcut.BrowserArgs)) { - trimmedArgs = item.BrowserArgs.Trim(); + trimmedArgs = shortcut.BrowserArgs.Trim(); } - else if (string.IsNullOrWhiteSpace(item.BrowserPath)) + else if (string.IsNullOrWhiteSpace(shortcut.BrowserPath)) { trimmedArgs = DefaultBrowserProvider.ArgumentsPattern; } @@ -30,7 +30,7 @@ public BrowserExecutionInfo(WebSearchShortcutItem item) { trimmedArgs = BrowserDiscovery .GetAllInstalledBrowsers() - .FirstOrDefault(b => string.Equals(b.Path, item.BrowserPath, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(b => string.Equals(b.Path, shortcut.BrowserPath, StringComparison.OrdinalIgnoreCase)) ?.ArgumentsPattern.Trim(); } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserInfo.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserInfo.cs index d1e74d0..f2c8e46 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserInfo.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/BrowserInfo.cs @@ -1,3 +1,3 @@ namespace WebSearchShortcut.Browsers; -public record BrowserInfo(string Id, string Name, string Path, string ArgumentsPattern); +public sealed record BrowserInfo(string Id, string Name, string Path, string ArgumentsPattern); diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/DefaultBrowserProvider.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/DefaultBrowserProvider.cs index 80625fb..c1b54f6 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/DefaultBrowserProvider.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Browsers/DefaultBrowserProvider.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Text; using System.Threading; using Microsoft.Win32; diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Commands/OpenHomePageCommand.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Commands/OpenHomePageCommand.cs index 309bc92..57b0b41 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Commands/OpenHomePageCommand.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Commands/OpenHomePageCommand.cs @@ -1,37 +1,31 @@ using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Properties; +using WebSearchShortcut.Browsers; using WebSearchShortcut.Helpers; +using WebSearchShortcut.Properties; namespace WebSearchShortcut.Commands; internal sealed partial class OpenHomePageCommand : InvokableCommand { - // private readonly SettingsManager _settingsManager; - public WebSearchShortcutItem Item; + private readonly WebSearchShortcutDataEntry _shortcut; + private readonly BrowserExecutionInfo _browserInfo; - internal OpenHomePageCommand(WebSearchShortcutItem item) + internal OpenHomePageCommand(WebSearchShortcutDataEntry shortcut) { - Icon = new IconInfo("\uE721"); - Name = StringFormatter.Format(Resources.OpenHomePageCommand_Name, new() { ["engine"] = item.Name }); - Item = item; - // Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"); - // Name = Properties.Resources.open_in_default_browser; - // _settingsManager = settingsManager; + Name = StringFormatter.Format(Resources.OpenHomePage_NameTemplate, new() { ["engine"] = shortcut.Name }); + Icon = Icons.Search; + _shortcut = shortcut; + _browserInfo = new BrowserExecutionInfo(shortcut); } public override CommandResult Invoke() { - if (!HomePageLauncher.OpenHomePageWithBrowser(Item)) + if (!ShellHelpers.OpenCommandInShell(_browserInfo.Path, _browserInfo.ArgumentsPattern, WebSearchShortcutDataEntry.GetHomePageUrl(_shortcut))) { // TODO GH# 138 --> actually display feedback from the extension somewhere. return CommandResult.KeepOpen(); } - // if (_settingsManager.ShowHistory != Resources.history_none) - // { - // _settingsManager.SaveHistory(new HistoryItem(Arguments, DateTime.Now)); - // } - return CommandResult.Dismiss(); } } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Commands/SearchCommand.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Commands/SearchWebCommand.cs similarity index 50% rename from CmdPalWebSearchShortcut/WebSearchShortcut/Commands/SearchCommand.cs rename to CmdPalWebSearchShortcut/WebSearchShortcut/Commands/SearchWebCommand.cs index fe783fb..8539b71 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Commands/SearchCommand.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Commands/SearchWebCommand.cs @@ -7,26 +7,24 @@ namespace WebSearchShortcut.Commands; internal sealed partial class SearchWebCommand : InvokableCommand { + private readonly string _query; + private readonly WebSearchShortcutDataEntry _shortcut; + private readonly BrowserExecutionInfo _browserInfo; // private readonly SettingsManager _settingsManager; - public string Arguments { get; internal set; } = string.Empty; - public WebSearchShortcutItem Item; - private readonly BrowserExecutionInfo BrowserInfo; - internal SearchWebCommand(string arguments, WebSearchShortcutItem item) + public SearchWebCommand(WebSearchShortcutDataEntry shortcut, string query) { - Arguments = arguments; - BrowserInfo = new BrowserExecutionInfo(item); - Icon = new IconInfo("\uE721"); - Name = StringFormatter.Format(Resources.SearchWebCommand_Name, new() { ["engine"] = item.Name, ["query"] = arguments }); - Item = item; - // Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"); - // Name = Properties.Resources.open_in_default_browser; + Name = StringFormatter.Format(Resources.SearchQuery_NameTemplate, new() { ["engine"] = shortcut.Name, ["query"] = query }); + Icon = Icons.Search; + _query = query; + _shortcut = shortcut; + _browserInfo = new BrowserExecutionInfo(shortcut); // _settingsManager = settingsManager; } public override CommandResult Invoke() { - if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{WebSearchShortcutItem.GetSearchUrl(Item, Arguments)}")) + if (!ShellHelpers.OpenCommandInShell(_browserInfo.Path, _browserInfo.ArgumentsPattern, WebSearchShortcutDataEntry.GetSearchUrl(_shortcut, _query))) { // TODO GH# 138 --> actually display feedback from the extension somewhere. return CommandResult.KeepOpen(); diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Forms/AddShortcutForm.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Forms/AddShortcutForm.cs index eb4b9c8..71b2935 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Forms/AddShortcutForm.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Forms/AddShortcutForm.cs @@ -10,20 +10,18 @@ namespace WebSearchShortcut; internal sealed partial class AddShortcutForm : FormContent { - internal event TypedEventHandler? AddedCommand; + private readonly WebSearchShortcutDataEntry? _shortcut; - private readonly WebSearchShortcutItem? _item; - - public AddShortcutForm(WebSearchShortcutItem? item) + public AddShortcutForm(WebSearchShortcutDataEntry? shortcut) { - _item = item; - var name = _item?.Name ?? string.Empty; - var url = _item?.Url ?? string.Empty; - var suggestionProvider = _item?.SuggestionProvider ?? string.Empty; - var replaceWhitespace = _item?.ReplaceWhitespace ?? string.Empty; - var homePage = _item?.HomePage ?? string.Empty; - var browserPath = _item?.BrowserPath ?? string.Empty; - var browserArgs = _item?.BrowserArgs ?? string.Empty; + _shortcut = shortcut; + var name = shortcut?.Name ?? string.Empty; + var url = shortcut?.Url ?? string.Empty; + var suggestionProvider = shortcut?.SuggestionProvider ?? string.Empty; + var replaceWhitespace = shortcut?.ReplaceWhitespace ?? string.Empty; + var homePage = shortcut?.HomePage ?? string.Empty; + var browserPath = shortcut?.BrowserPath ?? string.Empty; + var browserArgs = shortcut?.BrowserArgs ?? string.Empty; TemplateJson = $$""" { @@ -59,10 +57,10 @@ public AddShortcutForm(WebSearchShortcutItem? item) "title": {{JsonSerializer.Serialize(Resources.AddShortcutForm_SuggestionProviderNone, AppJsonSerializerContext.Default.String)}}, "value": "" }, - {{Suggestions.SuggestionProviders.Keys.Select(k => $$""" + {{SuggestionsRegistry.ProviderNames.Select(key => $$""" { - "title": {{JsonSerializer.Serialize(k, AppJsonSerializerContext.Default.String)}}, - "value": {{JsonSerializer.Serialize(k, AppJsonSerializerContext.Default.String)}} + "title": {{JsonSerializer.Serialize(key, AppJsonSerializerContext.Default.String)}}, + "value": {{JsonSerializer.Serialize(key, AppJsonSerializerContext.Default.String)}} } """).Aggregate((a, b) => a + "," + b)}} ], @@ -139,33 +137,23 @@ public AddShortcutForm(WebSearchShortcutItem? item) """; } - public override CommandResult SubmitForm(string payload) - { - var formInput = JsonNode.Parse(payload); - if (formInput == null) - { - return CommandResult.GoHome(); - } + internal event TypedEventHandler? AddedCommand; - // get the name and url out of the values - var formName = formInput["name"] ?? string.Empty; - var formUrl = formInput["url"] ?? string.Empty; - var formSuggestionProvider = formInput["suggestionProvider"] ?? string.Empty; - var formReplaceWhitespace = formInput["replaceWhitespace"] ?? string.Empty; - var formHomePage = formInput["homePage"] ?? string.Empty; - var formBrowserPath = formInput["browserPath"] ?? string.Empty; - var formBrowserArgs = formInput["browserArgs"] ?? string.Empty; + public override CommandResult SubmitForm(string inputs) + { + var root = JsonNode.Parse(inputs); + if (root is null) return CommandResult.GoHome(); - var updated = _item ?? new WebSearchShortcutItem(); - updated.Name = formName.ToString(); - updated.Url = formUrl.ToString(); - updated.SuggestionProvider = formSuggestionProvider.ToString(); - updated.ReplaceWhitespace = formReplaceWhitespace.ToString(); - updated.HomePage = formHomePage.ToString(); - updated.BrowserPath = formBrowserPath.ToString(); - updated.BrowserArgs = formBrowserArgs.ToString(); + var shortcut = _shortcut ?? new WebSearchShortcutDataEntry(); + shortcut.Name = root["name"]?.GetValue() ?? string.Empty; + shortcut.Url = root["url"]?.GetValue() ?? string.Empty; + shortcut.SuggestionProvider = root["suggestionProvider"]?.GetValue() ?? string.Empty; + shortcut.ReplaceWhitespace = root["replaceWhitespace"]?.GetValue() ?? string.Empty; + shortcut.HomePage = root["homePage"]?.GetValue() ?? string.Empty; + shortcut.BrowserPath = root["browserPath"]?.GetValue() ?? string.Empty; + shortcut.BrowserArgs = root["browserArgs"]?.GetValue() ?? string.Empty; - AddedCommand?.Invoke(this, updated); + AddedCommand?.Invoke(this, shortcut); return CommandResult.GoHome(); } } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Helpers/HomePageLauncher.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Helpers/HomePageLauncher.cs deleted file mode 100644 index acc95ea..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Helpers/HomePageLauncher.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Browsers; - -namespace WebSearchShortcut.Helpers; - - -internal static class HomePageLauncher -{ - private static string GetHomePageUrl(WebSearchShortcutItem item) - { - return !string.IsNullOrWhiteSpace(item.HomePage) - ? item.HomePage - : item.Domain; - } - - public static bool OpenHomePageWithBrowser(WebSearchShortcutItem item) - { - var homePageUrl = GetHomePageUrl(item); - var browserInfo = new BrowserExecutionInfo(item); - - return ShellHelpers.OpenCommandInShell( - browserInfo.Path, - browserInfo.ArgumentsPattern, - homePageUrl - ); - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Helpers/JsonPrettyFormatter.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Helpers/JsonPrettyFormatter.cs index 10b85ea..b9014bb 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Helpers/JsonPrettyFormatter.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Helpers/JsonPrettyFormatter.cs @@ -6,7 +6,7 @@ namespace WebSearchShortcut.Helpers; -internal static class JsonPrettyFormatter +public static class JsonPrettyFormatter { private static readonly JsonWriterOptions PrettyWriterOptions = new() { @@ -18,11 +18,15 @@ internal static class JsonPrettyFormatter public static string ToPrettyJson(T obj, JsonTypeInfo typeInfo) { byte[] utf8Json = JsonSerializer.SerializeToUtf8Bytes(obj, typeInfo); + using JsonDocument doc = JsonDocument.Parse(utf8Json); + using var output = new MemoryStream(); using var writer = new Utf8JsonWriter(output, PrettyWriterOptions); + doc.RootElement.WriteTo(writer); writer.Flush(); + return Encoding.UTF8.GetString(output.ToArray()); } } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/JSONContext.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/JSONContext.cs index 937e9cf..63c62f0 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/JSONContext.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/JSONContext.cs @@ -4,7 +4,7 @@ namespace WebSearchShortcut; [JsonSourceGenerationOptions(IncludeFields = true)] [JsonSerializable(typeof(Storage))] -[JsonSerializable(typeof(WebSearchShortcutItem))] +[JsonSerializable(typeof(WebSearchShortcutDataEntry))] internal partial class AppJsonSerializerContext : JsonSerializerContext { } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/AddShortcutPage.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/AddShortcutPage.cs index 8260f2e..12ed498 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/AddShortcutPage.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/AddShortcutPage.cs @@ -1,6 +1,3 @@ - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Nodes; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.Foundation; @@ -10,25 +7,25 @@ namespace WebSearchShortcut; internal sealed partial class AddShortcutPage : ContentPage { - private readonly AddShortcutForm _addShortcut; - - internal event TypedEventHandler? AddedCommand - { - add => _addShortcut.AddedCommand += value; - remove => _addShortcut.AddedCommand -= value; - } - - public override IContent[] GetContent() => [_addShortcut]; + private readonly AddShortcutForm _addShortcutForm; - public AddShortcutPage(WebSearchShortcutItem? item) + public AddShortcutPage(WebSearchShortcutDataEntry? shortcut) { - var name = item?.Name ?? string.Empty; - var url = item?.Url ?? string.Empty; + var name = shortcut?.Name ?? string.Empty; + var url = shortcut?.Url ?? string.Empty; var isAdd = string.IsNullOrEmpty(name) && string.IsNullOrEmpty(url); - _addShortcut = new AddShortcutForm(item); + _addShortcutForm = new AddShortcutForm(shortcut); Icon = IconHelpers.FromRelativePath("Assets\\SearchAdd.png"); - Title = isAdd ? Resources.AddShortcutPage_AddTitle : Resources.AddShortcutPage_EditTitle; - Name = isAdd ? Resources.AddShortcutPage_AddName : Resources.AddShortcutPage_EditName; + Title = isAdd ? Resources.AddShortcut_AddTitle : Resources.SearchShortcut_EditTitle; + Name = isAdd ? Resources.AddShortcut_AddName : Resources.SearchShortcut_EditName; } + + internal event TypedEventHandler? AddedCommand + { + add => _addShortcutForm.AddedCommand += value; + remove => _addShortcutForm.AddedCommand -= value; + } + + public override IContent[] GetContent() => [_addShortcutForm]; } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/SearchPage.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/SearchPage.cs deleted file mode 100644 index 7672f67..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/SearchPage.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.CommandPalette.Extensions; -using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Commands; -using WebSearchShortcut.Helpers; -using WebSearchShortcut.Properties; -using WebSearchShortcut.Services; - -namespace WebSearchShortcut; - -public partial class SearchPage : DynamicListPage -{ - public string Url { get; } - public WebSearchShortcutItem Item { get; } - - private List allItems; - private List allSuggestItems; - private readonly ListItem _emptyListItem; - - public SearchPage(WebSearchShortcutItem data) - { - Item = data; - Name = data.Name; - Url = data.Url; - Icon = IconService.GetIconInfo(data); - _emptyListItem = new ListItem(new OpenHomePageCommand(data)); - allItems = [_emptyListItem]; - - _lastSuggestionId = 0; - allSuggestItems = []; - } - - public override IListItem[] GetItems() - { - return [ - ..allItems, - ]; - } - - public List Query(string query) - { - var results = new List(); - // empty query - if (string.IsNullOrEmpty(query)) - { - allSuggestItems = []; - results.Add(_emptyListItem); - } - else - { - var searchTerm = query; - var result = new ListItem(new SearchWebCommand(searchTerm, Item)) - { - Title = searchTerm, - Subtitle = StringFormatter.Format(Resources.SearchPage_Subtitle, new() { ["engine"] = Name, ["query"] = searchTerm }), - MoreCommands = [new CommandContextItem( - title: StringFormatter.Format(Resources.SearchPage_MoreCommandsTitle, new () { ["engine"] = Name }), - name: StringFormatter.Format(Resources.SearchPage_MoreCommandsName, new () { ["engine"] = Name }), - action: () => HomePageLauncher.OpenHomePageWithBrowser(Item) - )] - }; - results.Add(result); - } - - return results; - } - - private int _lastSuggestionId; - public override async void UpdateSearchText(string oldSearch, string newSearch) - { - var ignoreId = ++_lastSuggestionId; - var queryItems = Query(newSearch); - allItems = [.. queryItems, .. allSuggestItems]; - RaiseItemsChanged(allItems.Count); - if (string.IsNullOrWhiteSpace(Item.SuggestionProvider) || string.IsNullOrEmpty(newSearch)) - { - return; - } - var suggestions = await Suggestions.QuerySuggestionsAsync(Item.SuggestionProvider, newSearch); - if (ignoreId != _lastSuggestionId) - { - return; - } - List suggestItems = [.. suggestions - .Select(s => new ListItem(new SearchWebCommand(s.Title, Item)) - { - Title = s.Title, - Subtitle = s.Description ?? StringFormatter.Format(Resources.SearchPage_Subtitle, new() { ["engine"] = Name, ["query"] = s.Title }), - TextToSuggest = s.Title, - MoreCommands = [new CommandContextItem( - title: StringFormatter.Format(Resources.SearchPage_MoreCommandsTitle, new() { ["engine"] = Name }), - name: StringFormatter.Format(Resources.SearchPage_MoreCommandsName, new() { ["engine"] = Name }), - action: () => HomePageLauncher.OpenHomePageWithBrowser(Item) - )] - })]; - List items = [.. queryItems, .. suggestItems]; - allSuggestItems = suggestItems; - - allItems = items; - RaiseItemsChanged(allItems.Count); - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/SearchWebPage.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/SearchWebPage.cs new file mode 100644 index 0000000..9945812 --- /dev/null +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/SearchWebPage.cs @@ -0,0 +1,107 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using WebSearchShortcut.Commands; +using WebSearchShortcut.Helpers; +using WebSearchShortcut.Properties; +using WebSearchShortcut.Services; + +namespace WebSearchShortcut; + +internal sealed partial class SearchWebPage : DynamicListPage +{ + private readonly WebSearchShortcutDataEntry _shortcut; + private readonly IListItem _openHomePageItem; + private IListItem[] _items = []; + private IListItem[] _suggestionItems = []; + private int _updateEpoch; + + public SearchWebPage(WebSearchShortcutDataEntry shortcut) + { + _shortcut = shortcut; + + Name = shortcut.Name; + Icon = IconService.GetIconInfo(shortcut); + _openHomePageItem = new ListItem(new OpenHomePageCommand(shortcut)) + { + Title = StringFormatter.Format(Resources.OpenHomePage_TitleTemplate, new() { ["engine"] = Name }) + }; + + _updateEpoch = 0; + + _items = [_openHomePageItem]; + } + + public override IListItem[] GetItems() => _items; + + public override async void UpdateSearchText(string oldSearch, string newSearch) + { + var capturedEpoch = Interlocked.Increment(ref _updateEpoch); + + if (string.IsNullOrEmpty(newSearch)) + { + _suggestionItems = []; + + RenderItems([_openHomePageItem]); + + return; + } + + var primaryItems = BuildPrimaryItems(newSearch); + + RenderItems([.. primaryItems, .. _suggestionItems]); + + if (string.IsNullOrEmpty(_shortcut.SuggestionProvider)) + return; + + var suggestionItems = await GetSuggestionItemsAsync(newSearch); + + if (capturedEpoch != _updateEpoch) + return; + + _suggestionItems = suggestionItems; + + RenderItems([.. primaryItems, .. _suggestionItems]); + } + + private void RenderItems(IListItem[] items) + { + _items = items; + + RaiseItemsChanged(_items.Length); + } + + private ListItem[] BuildPrimaryItems(string searchText) + { + return + [ + new ListItem(new SearchWebCommand(_shortcut, searchText)) + { + Title = searchText, + Subtitle = StringFormatter.Format(Resources.SearchQuery_SubtitleTemplate, new() { ["engine"] = Name, ["query"] = searchText }), + MoreCommands = [new CommandContextItem(new OpenHomePageCommand(_shortcut))] + } + ]; + } + + private async Task GetSuggestionItemsAsync(string searchText) + { + var suggestions = await SuggestionsRegistry + .Get(_shortcut.SuggestionProvider!) + .GetSuggestionsAsync(searchText) + .ConfigureAwait(false); + + return + [ + .. suggestions.Select(suggestion => new ListItem(new SearchWebCommand(_shortcut, suggestion.Title)) + { + Title = suggestion.Title, + Subtitle = suggestion.Description ?? StringFormatter.Format(Resources.SearchQuery_SubtitleTemplate, new() { ["engine"] = Name, ["query"] = suggestion.Title }), + TextToSuggest = suggestion.Title, + MoreCommands = [new CommandContextItem(new OpenHomePageCommand(_shortcut))] + }) + ]; + } +} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/WebSearchShortcutPage.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/WebSearchShortcutPage.cs deleted file mode 100644 index 7f22ef0..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Pages/WebSearchShortcutPage.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.CommandPalette.Extensions; -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace WebSearchShortcut; - -internal sealed partial class WebSearchShortcutPage : ListPage -{ - public WebSearchShortcutPage() - { - Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png"); - Title = "WebSearchShortcut"; - Name = "Open"; - } - - public override IListItem[] GetItems() - { - return [ - new ListItem(new NoOpCommand()) { Title = "TODO: Implement your extension here" } - ]; - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Constants/Icons.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Icons.cs similarity index 91% rename from CmdPalWebSearchShortcut/WebSearchShortcut/Constants/Icons.cs rename to CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Icons.cs index 5bf6e68..9dbb021 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Constants/Icons.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Icons.cs @@ -2,33 +2,32 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -namespace WebSearchShortcut.Constants; +namespace WebSearchShortcut.Properties; /// /// Provides commonly used icons for the WebSearchShortcut application /// public static class Icons { - /// - /// Delete icon (trash can) - /// - public static IconInfo Delete { get; } = new("\uE74D"); - /// /// Edit icon (pencil) /// public static IconInfo Edit { get; } = new("\uE70F"); /// - /// Search icon + /// Delete icon (trash can) /// - public static IconInfo Search { get; } = new("\uE721"); + public static IconInfo Delete { get; } = new("\uE74D"); /// /// Default fallback icon for links /// public static IconInfo Link { get; } = new("🔗"); + + /// + /// Search icon + /// + public static IconInfo Search { get; } = new("\uE721"); } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.Designer.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.Designer.cs index 04ecc30..33a9abf 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.Designer.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.Designer.cs @@ -60,6 +60,24 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to Add Search Shortcut. + /// + internal static string AddShortcut_AddName { + get { + return ResourceManager.GetString("AddShortcut_AddName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add Search Shortcut. + /// + internal static string AddShortcut_AddTitle { + get { + return ResourceManager.GetString("AddShortcut_AddTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Browser Arguments (Optional). /// @@ -214,119 +232,92 @@ internal static string AddShortcutForm_UrlPlaceholder { } /// - /// Looks up a localized string similar to Add Search Shortcut. - /// - internal static string AddShortcutPage_AddName { - get { - return ResourceManager.GetString("AddShortcutPage_AddName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add Search Shortcut. - /// - internal static string AddShortcutPage_AddTitle { - get { - return ResourceManager.GetString("AddShortcutPage_AddTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edit Search Shortcut. - /// - internal static string AddShortcutPage_EditName { - get { - return ResourceManager.GetString("AddShortcutPage_EditName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edit Search Shortcut. + /// Looks up a localized string similar to Open {engine}. /// - internal static string AddShortcutPage_EditTitle { + internal static string OpenHomePage_NameTemplate { get { - return ResourceManager.GetString("AddShortcutPage_EditTitle", resourceCulture); + return ResourceManager.GetString("OpenHomePage_NameTemplate", resourceCulture); } } /// /// Looks up a localized string similar to Open {engine}. /// - internal static string OpenHomePageCommand_Name { + internal static string OpenHomePage_TitleTemplate { get { - return ResourceManager.GetString("OpenHomePageCommand_Name", resourceCulture); + return ResourceManager.GetString("OpenHomePage_TitleTemplate", resourceCulture); } } /// - /// Looks up a localized string similar to Open {engine}. + /// Looks up a localized string similar to Search for "{query}". /// - internal static string SearchPage_MoreCommandsName { + internal static string SearchQuery_NameTemplate { get { - return ResourceManager.GetString("SearchPage_MoreCommandsName", resourceCulture); + return ResourceManager.GetString("SearchQuery_NameTemplate", resourceCulture); } } /// - /// Looks up a localized string similar to Open {engine}. + /// Looks up a localized string similar to Search {engine} for "{query}". /// - internal static string SearchPage_MoreCommandsTitle { + internal static string SearchQuery_SubtitleTemplate { get { - return ResourceManager.GetString("SearchPage_MoreCommandsTitle", resourceCulture); + return ResourceManager.GetString("SearchQuery_SubtitleTemplate", resourceCulture); } } /// - /// Looks up a localized string similar to Search {engine} for "{query}". + /// Looks up a localized string similar to Delete. /// - internal static string SearchPage_Subtitle { + internal static string SearchShortcut_DeleteName { get { - return ResourceManager.GetString("SearchPage_Subtitle", resourceCulture); + return ResourceManager.GetString("SearchShortcut_DeleteName", resourceCulture); } } /// - /// Looks up a localized string similar to Search for "{query}". + /// Looks up a localized string similar to Delete. /// - internal static string SearchWebCommand_Name { + internal static string SearchShortcut_DeleteTitle { get { - return ResourceManager.GetString("SearchWebCommand_Name", resourceCulture); + return ResourceManager.GetString("SearchShortcut_DeleteTitle", resourceCulture); } } /// - /// Looks up a localized string similar to Delete. + /// Looks up a localized string similar to Edit Search Shortcut. /// - internal static string WebSearchShortcutCommandsProvider_CommandItemDeleteName { + internal static string SearchShortcut_EditName { get { - return ResourceManager.GetString("WebSearchShortcutCommandsProvider_CommandItemDeleteName", resourceCulture); + return ResourceManager.GetString("SearchShortcut_EditName", resourceCulture); } } /// - /// Looks up a localized string similar to Delete. + /// Looks up a localized string similar to Edit Search Shortcut. /// - internal static string WebSearchShortcutCommandsProvider_CommandItemDeleteTitle { + internal static string SearchShortcut_EditTitle { get { - return ResourceManager.GetString("WebSearchShortcutCommandsProvider_CommandItemDeleteTitle", resourceCulture); + return ResourceManager.GetString("SearchShortcut_EditTitle", resourceCulture); } } /// /// Looks up a localized string similar to Search {engine}. /// - internal static string WebSearchShortcutCommandsProvider_CommandItemSubtitle { + internal static string SearchShortcut_SubtitleTemplate { get { - return ResourceManager.GetString("WebSearchShortcutCommandsProvider_CommandItemSubtitle", resourceCulture); + return ResourceManager.GetString("SearchShortcut_SubtitleTemplate", resourceCulture); } } /// /// Looks up a localized string similar to WebSearchShortcut. /// - internal static string WebSearchShortcutCommandsProvider_DisplayName { + internal static string WebSearchShortcut_DisplayName { get { - return ResourceManager.GetString("WebSearchShortcutCommandsProvider_DisplayName", resourceCulture); + return ResourceManager.GetString("WebSearchShortcut_DisplayName", resourceCulture); } } } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.fr.resx b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.fr.resx index 8eb0651..02d563f 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.fr.resx +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.fr.resx @@ -117,23 +117,17 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Nom - - - Page d'accueil personnalisée (Optionnel) - - - Arguments du navigateur (Optionnel) + + WebSearchShortcut - - Arguments de lancement du navigateur, utilisez %1 pour l'URL + + Ajouter un raccourci de recherche - - Navigateur + + Ajouter un raccourci de recherche - - Par défaut + + Nom Le nom est obligatoire @@ -141,70 +135,73 @@ URL - - L'URL est obligatoire - Utilisez %s dans l'URL pour le terme de recherche - - Ajouter un raccourci de recherche - - - Modifier le raccourci de recherche - - - Ajouter un raccourci de recherche - - - Modifier le raccourci de recherche + + L'URL est obligatoire Fournisseur de suggestions + + Fournisseur de suggestions invalide + + + Aucun + Remplacer les espaces (Optionnel) Spécifiez quel(s) caractère(s) remplace(nt) un espace - - Fournisseur de suggestions invalide - - - Aucun + + Page d'accueil personnalisée (Optionnel) Utiliser le domaine par défaut + + Arguments du navigateur (Optionnel) + + + Arguments de lancement du navigateur, utilisez %1 pour l'URL + + + Navigateur + + + Par défaut + Enregistrer - - Supprimer + + Rechercher sur {engine} + + + Modifier le raccourci de recherche + + + Modifier le raccourci de recherche - + Supprimer - - WebSearchShortcut + + Supprimer - + Ouvrir {engine} - + Ouvrir {engine} - - Ouvrir {engine} + + Rechercher "{query}" - + Rechercher "{query}" sur {engine} - - Rechercher sur {engine} - - - Rechercher "{query}" - \ No newline at end of file diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.resx b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.resx index a9fd53d..dddca41 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.resx +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.resx @@ -117,23 +117,17 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Name - - - Custom Home Page (Optional) - - - Browser Arguments (Optional) + + WebSearchShortcut - - Browser launch arguments, use %1 for URL + + Add Search Shortcut - - Browser + + Add Search Shortcut - - Default + + Name Name is required @@ -141,70 +135,73 @@ URL - - URL is required - Use %s in the URL for the search term - - Add Search Shortcut - - - Edit Search Shortcut - - - Add Search Shortcut - - - Edit Search Shortcut + + URL is required Suggestion Provider + + Invalid Suggestion Provider + + + None + Replace Whitespace (Optional) Specify which character(s) to replace a space - - Invalid Suggestion Provider - - - None + + Custom Home Page (Optional) Use domain by default + + Browser Arguments (Optional) + + + Browser launch arguments, use %1 for URL + + + Browser + + + Default + Save - - Delete + + Search {engine} + + + Edit Search Shortcut + + + Edit Search Shortcut - + Delete - - WebSearchShortcut + + Delete - + Open {engine} - + Open {engine} - - Open {engine} + + Search for "{query}" - + Search {engine} for "{query}" - - Search {engine} - - - Search for "{query}" - \ No newline at end of file diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.zh-CN.resx b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.zh-CN.resx index 30259b2..cb2c3dd 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.zh-CN.resx +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.zh-CN.resx @@ -1,17 +1,17 @@  - @@ -117,23 +117,17 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - 名称 - - - 自定义主页(可选) - - - 浏览器参数(可选) + + WebSearchShortcut - - 浏览器启动参数,使用 %1 作为 URL 占位符 + + 添加搜索捷径(Shortcut) - - 浏览器 + + 添加搜索捷径 - - 默认 + + 名称 名称为必填项 @@ -141,70 +135,73 @@ URL - - URL 为必填项 - 在 URL 中使用 %s 作为搜索词占位符 - - 添加搜索捷径 - - - 编辑搜索捷径 - - - 添加搜索捷径(Shortcut) - - - 编辑搜索捷径 + + URL 为必填项 搜索建议来源 + + 无效的搜索建议来源 + + + + 替换空格(可选) 指定用于替换空格的字符 - - 无效的搜索建议来源 - - - + + 自定义主页(可选) 默认使用主域名 + + 浏览器参数(可选) + + + 浏览器启动参数,使用 %1 作为 URL 占位符 + + + 浏览器 + + + 默认 + 保存 - - 删除 + + 搜索 {engine} + + + 编辑搜索捷径 - + + 编辑搜索捷径 + + 删除 - - WebSearchShortcut + + 删除 - + 打开 {engine} - + 打开 {engine} - - 打开 {engine} + + 搜索“{query}” - + 在 {engine} 中搜索“{query}” - - 搜索 {engine} - - - 搜索“{query}” - \ No newline at end of file diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.zh-TW.resx b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.zh-TW.resx index 1ffb4ec..3dc1931 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.zh-TW.resx +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Resources.zh-TW.resx @@ -117,23 +117,17 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - 名稱 - - - 自訂首頁(可選) - - - 瀏覽器參數(可選) + + 網頁搜尋捷徑 - - 瀏覽器啟動參數,請用 %1 取代網址的位置 + + +新增搜尋捷徑 - - 瀏覽器 + + 新增搜尋捷徑 - - 使用者預設瀏覽器 + + 名稱 必須填寫名稱 @@ -141,70 +135,73 @@ 網址 - - 必須填寫網址 - 搜尋引擎的網址,請用 %s 取代關鍵字的位置 - - 新增搜尋捷徑 - - - 編輯搜尋捷徑 - - - +新增搜尋捷徑 - - - 編輯搜尋捷徑 + + 必須填寫網址 關鍵字建議來源 + + 無效的關鍵字建議來源 + + + + 替換空白字元(可選) 搜尋關鍵字中的空白會被替換為這裡指定的字元 - - 無效的關鍵字建議來源 - - - + + 自訂首頁(可選) 預設為網域 + + 瀏覽器參數(可選) + + + 瀏覽器啟動參數,請用 %1 取代網址的位置 + + + 瀏覽器 + + + 使用者預設瀏覽器 + 儲存 - - 刪除搜尋捷徑 + + 在 {engine} 上搜尋 + + + 編輯搜尋捷徑 + + + 編輯搜尋捷徑 - + 刪除搜尋捷徑 - - 網頁搜尋捷徑 + + 刪除搜尋捷徑 - + 開啟 {engine} 首頁 - + 開啟 {engine} 首頁 - - 開啟 {engine} 首頁 + + 搜尋「{query}」 - + 在 {engine} 上搜尋「{query}」 - - 在 {engine} 上搜尋 - - - 搜尋「{query}」 - \ No newline at end of file diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Services/IconService.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Services/IconService.cs index a2872b1..59a7cb2 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Services/IconService.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Services/IconService.cs @@ -6,16 +6,15 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Constants; +using WebSearchShortcut.Properties; namespace WebSearchShortcut.Services; /// /// Provides icon-related services for web search shortcuts /// -public static class IconService +internal static class IconService { private static readonly string DefaultIconFallback = "🔗"; private static readonly string UserAgent = "Mozilla/5.0 (compatible; AcmeInc/1.0)"; @@ -31,15 +30,15 @@ public static class IconService }; /// - /// Gets an IconInfo for the specified item, using cached icon URL if available or generating one from the URL + /// Gets an IconInfo for the specified shortcut, using cached icon URL if available or generating one from the URL /// - /// The web search shortcut item + /// The web search shortcut data entry /// IconInfo instance - public static IconInfo GetIconInfo(WebSearchShortcutItem item) + public static IconInfo GetIconInfo(WebSearchShortcutDataEntry shortcut) { - return !string.IsNullOrWhiteSpace(item.IconUrl) - ? new IconInfo(item.IconUrl) - : new IconInfo(GetFaviconUrlFromUrl(item.Url)); + return !string.IsNullOrWhiteSpace(shortcut.IconUrl) + ? new IconInfo(shortcut.IconUrl) + : new IconInfo(GetFaviconUrlFromUrl(shortcut.Url)); } /// @@ -131,24 +130,24 @@ private static async Task IsValidFaviconAsync(string faviconUrl) } /// - /// Updates the icon URL for a web search shortcut item asynchronously + /// Updates the icon URL for a web search shortcut data entry asynchronously /// - /// The item to update + /// The shortcut to update /// The updated icon URL - public static async Task UpdateIconUrlAsync(WebSearchShortcutItem item) + public static async Task UpdateIconUrlAsync(WebSearchShortcutDataEntry shortcut) { - if (item == null || !string.IsNullOrWhiteSpace(item.IconUrl)) + if (shortcut == null || !string.IsNullOrWhiteSpace(shortcut.IconUrl)) { - return item?.IconUrl ?? DefaultIconFallback; + return shortcut?.IconUrl ?? DefaultIconFallback; } try { - var uri = UriHelper.GetUri(item.Domain ?? item.Url); + var uri = UriHelper.GetUri(shortcut.Domain ?? shortcut.Url); if (uri != null) { var iconUrl = await GetValidFaviconUrlAsync(uri); - item.IconUrl = iconUrl; + shortcut.IconUrl = iconUrl; return iconUrl; } } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Storage.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Storage.cs index 3b62621..2c8afe8 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Storage.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Storage.cs @@ -5,14 +5,9 @@ namespace WebSearchShortcut; -public sealed class Storage +internal sealed class Storage { - public List Data { get; set; } = []; - - // private static readonly JsonSerializerOptions _jsonOptions = new() - // { - // IncludeFields = true, - // }; + public List Data { get; set; } = []; public static Storage ReadFromFile(string path) { @@ -22,19 +17,19 @@ public static Storage ReadFromFile(string path) { var defaultStorage = new Storage(); defaultStorage.Data.AddRange([ - new WebSearchShortcutItem + new WebSearchShortcutDataEntry { Name = "Google", Url = "https://www.google.com/search?q=%s", SuggestionProvider = "Google", }, - new WebSearchShortcutItem + new WebSearchShortcutDataEntry { Name = "Bing", Url = "https://www.bing.com/search?q=%s", SuggestionProvider = "Bing", }, - new WebSearchShortcutItem + new WebSearchShortcutDataEntry { Name = "Youtube", Url = "https://www.youtube.com/results?search_query=%s", @@ -43,7 +38,8 @@ public static Storage ReadFromFile(string path) ]); WriteToFile(path, defaultStorage); } - // if the file exists, load it and append the new item + + // if the file exists, load the saved shortcuts if (File.Exists(path)) { var jsonStringReading = File.ReadAllText(path); @@ -61,6 +57,6 @@ public static void WriteToFile(string path, Storage data) { var jsonString = JsonPrettyFormatter.ToPrettyJson(data, AppJsonSerializerContext.Default.Storage); - File.WriteAllText(WebSearchShortcutCommandsProvider.StateJsonPath(), jsonString); + File.WriteAllText(path, jsonString); } } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/Suggestion.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/Suggestion.cs index 166ec53..0c9eda1 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/Suggestion.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/Suggestion.cs @@ -1,45 +1,40 @@ +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; -using WebSearchShortcut.SuggestionsProvider; +using WebSearchShortcut.SuggestionsProviders; namespace WebSearchShortcut; -public interface IWebSearchShortcutSuggestionsProvider +internal interface ISuggestionsProvider { - Task> QuerySuggestionsAsync(string query); + string Name { get; } + Task> GetSuggestionsAsync(string query); } -public class SuggestionsItem(string title, string? description = null) -{ - public string Title { get; } = title; - public string? Description { get; } = description; -} +internal sealed record Suggestion(string Title, string? Description = null); -public class Suggestions +internal static class SuggestionsRegistry { - static public Task> QuerySuggestionsAsync(string name, string query) - { - var provider = SuggestionProviders[name]; - if (provider is not null) - { - return provider.QuerySuggestionsAsync(query); - } - return Task.FromResult(new List()); - } + public static IReadOnlyCollection ProviderNames => _suggestionsProviders.Keys; + + public static ISuggestionsProvider Get(string name) => + _suggestionsProviders.TryGetValue(name, out var provider) + ? provider + : throw new KeyNotFoundException($"Unknown suggestion provider: '{name}'."); + + public static bool TryGet(string name, out ISuggestionsProvider provider) => + _suggestionsProviders.TryGetValue(name, out provider!); - static public Dictionary< - string, - IWebSearchShortcutSuggestionsProvider - > SuggestionProviders - { get; set; } = - new() + private static readonly Dictionary _suggestionsProviders = + new ISuggestionsProvider[] { - { Google.Name, new Google() }, - { Bing.Name, new Bing() }, - { DuckDuckGo.Name, new DuckDuckGo() }, - { YouTube.Name, new YouTube() }, - { Wikipedia.Name, new Wikipedia() }, - { Npm.Name, new Npm() }, - { CanIUse.Name, new CanIUse() }, - }; + new Google(), + new Bing(), + new DuckDuckGo(), + new YouTube(), + new Wikipedia(), + new Npm(), + new CanIUse(), + }.ToDictionary(provider => provider.Name, StringComparer.OrdinalIgnoreCase); } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Bing.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Bing.cs deleted file mode 100644 index 9836232..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Bing.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Properties; - -namespace WebSearchShortcut.SuggestionsProvider; - -public class Bing : IWebSearchShortcutSuggestionsProvider -{ - public static string Name => "Bing"; - - private HttpClient Http { get; } = new HttpClient(); - - public async Task> QuerySuggestionsAsync(string query) - { - try - { - const string api = "https://api.bing.com/qsonhs.aspx?q="; - - await using var resultStream = await Http.GetStreamAsync( - api + Uri.EscapeDataString(query) - ) - .ConfigureAwait(false); - - using var json = await JsonDocument.ParseAsync(resultStream); - var root = json.RootElement.GetProperty("AS"); - - if (root.GetProperty("FullResults").GetInt32() == 0) - return []; - - List titles = root.GetProperty("Results") - .EnumerateArray() - .SelectMany(r => - r.GetProperty("Suggests") - .EnumerateArray() - .Select(s => s.GetProperty("Txt").GetString()) - ) - .Where(s => s is not null) - .Select(s => s!) - .ToList(); - - return titles - .Select(t => new SuggestionsItem(t)) - .ToList(); - } - catch (Exception e) - when (e is HttpRequestException or { InnerException: TimeoutException }) - { - ExtensionHost.LogMessage($"{e.Message}"); - return []; - } - } - - public override string ToString() - { - return "Bing"; - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/CanIUse.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/CanIUse.cs deleted file mode 100644 index 302c8dd..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/CanIUse.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Properties; - -namespace WebSearchShortcut.SuggestionsProvider; -public class CanIUse : IWebSearchShortcutSuggestionsProvider -{ - public static string Name => "CanIUse"; - - private HttpClient Http { get; } = new HttpClient(); - - public async Task> QuerySuggestionsAsync(string query) - { - try - { - const string api = "https://caniuse.com/process/query.php?search="; - - await using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query)) - .ConfigureAwait(false); - - using var json = await JsonDocument.ParseAsync(resultStream); - var featureIds = json - .RootElement.GetProperty("featureIds") - .EnumerateArray() - .Take(10); - - List items = featureIds - .Select(o => - { - var title = o.GetString(); - return title is null - ? null - : new SuggestionsItem(title); - }) - .Where(s => s != null) - .Select(s => s!) - .ToList(); - - return items; - } - catch (Exception e) - { - ExtensionHost.LogMessage($"{e.Message}"); - return []; - } - } - - public override string ToString() - { - return "CanIUse"; - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/DuckDuckGo.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/DuckDuckGo.cs deleted file mode 100644 index 247aa97..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/DuckDuckGo.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Properties; - -namespace WebSearchShortcut.SuggestionsProvider; - -class DuckDuckGo : IWebSearchShortcutSuggestionsProvider -{ - public static string Name => "DuckDuckGo"; - - private HttpClient Http { get; } = new HttpClient(); - - public async Task> QuerySuggestionsAsync(string query) - { - try - { - const string api = "https://duckduckgo.com/ac/?q="; - - await using var resultStream = await Http.GetStreamAsync( - api + Uri.EscapeDataString(query) - ) - .ConfigureAwait(false); - - using var json = await JsonDocument.ParseAsync(resultStream); - - var results = json.RootElement; - - List titles = results - .EnumerateArray() - .Select(o => o.GetProperty("phrase").GetString()) - .Where(s => !string.IsNullOrEmpty(s) && !s.Equals(query, StringComparison.OrdinalIgnoreCase)) - .Select(s => s!) - .ToList(); - - return titles - .Select(t => new SuggestionsItem(t)) - .ToList(); - } - catch (Exception e) - { - ExtensionHost.LogMessage($"{e.Message}"); - return []; - } - } - - public override string ToString() - { - return "DuckDuckGo"; - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Google.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Google.cs deleted file mode 100644 index 8c8e440..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Google.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Properties; - -namespace WebSearchShortcut.SuggestionsProvider; - -class Google : IWebSearchShortcutSuggestionsProvider -{ - public static string Name => "Google"; - - private HttpClient Http { get; } = new HttpClient(); - - public async Task> QuerySuggestionsAsync(string query) - { - try - { - const string api = "https://www.google.com/complete/search?output=chrome&q="; - - await using var resultStream = await Http.GetStreamAsync( - api + Uri.EscapeDataString(query) - ) - .ConfigureAwait(false); - - using var json = await JsonDocument.ParseAsync(resultStream); - - var results = json.RootElement.EnumerateArray().ElementAt(1); - - List titles = results - .EnumerateArray() - .Select(o => o.GetString()) - .Where(s => s is not null) - .Select(s => s!) - .ToList(); - - return titles - .Select(t => new SuggestionsItem(t)) - .ToList(); - } - catch (Exception e) - { - ExtensionHost.LogMessage($"{e.Message}"); - return []; - } - } - - public override string ToString() - { - return "Google"; - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Npm.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Npm.cs deleted file mode 100644 index eb15cb9..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Npm.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace WebSearchShortcut.SuggestionsProvider; - -public class Npm : IWebSearchShortcutSuggestionsProvider -{ - public static string Name => "Npm"; - - private HttpClient Http { get; } = new HttpClient(); - - public async Task> QuerySuggestionsAsync(string query) - { - try - { - const string api = "https://www.npmjs.com/search/suggestions?q="; - - await using var resultStream = await Http.GetStreamAsync( - api + Uri.EscapeDataString(query) - ) - .ConfigureAwait(false); - - using var json = await JsonDocument.ParseAsync(resultStream); - - var results = json.RootElement.EnumerateArray(); - - List items = results - .Select(o => - { - var title = o.GetProperty("name").GetString(); - var description = o.GetProperty("description").GetString(); - return title is null ? null : new SuggestionsItem(title, description ?? ""); - }) - .Where(s => s is not null) - .Select(s => s!) - .ToList(); - - return items; - } - catch (Exception e) - { - ExtensionHost.LogMessage($"{e.Message}"); - return []; - } - } - - public override string ToString() - { - return "Npm"; - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Wikipedia.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Wikipedia.cs deleted file mode 100644 index f6b9466..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/Wikipedia.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Properties; - -namespace WebSearchShortcut.SuggestionsProvider; - -class Wikipedia : IWebSearchShortcutSuggestionsProvider -{ - public static string Name => "Wikipedia"; - - private HttpClient Http { get; } = new HttpClient(); - - public async Task> QuerySuggestionsAsync(string query) - { - try - { - const string api = "https://api.wikimedia.org/core/v1/wikipedia/en/search/title?q="; - - await using var resultStream = await Http.GetStreamAsync( - api + Uri.EscapeDataString(query) - ) - .ConfigureAwait(false); - - using var json = await JsonDocument.ParseAsync(resultStream); - - var results = json.RootElement.GetProperty("pages"); - - List items = [ - .. results - .EnumerateArray() - .Select(o => ( - Title: o.TryGetProperty("title", out var t) ? t.GetString() : null, - Description: o.TryGetProperty("description", out var d) ? d.GetString() : null - )) - .Where(p => !string.IsNullOrWhiteSpace(p.Title)) - .Select(p => new SuggestionsItem(p.Title!, p.Description ?? "")) - ]; - - return items; - } - catch (Exception e) - { - ExtensionHost.LogMessage($"{e.Message}"); - return []; - } - } - - public override string ToString() - { - return "Wikipedia"; - } -} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Bing.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Bing.cs new file mode 100644 index 0000000..ebd3957 --- /dev/null +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Bing.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using WebSearchShortcut.Properties; + +namespace WebSearchShortcut.SuggestionsProviders; + +internal sealed class Bing : ISuggestionsProvider +{ + public string Name => "Bing"; + + private HttpClient Http { get; } = new HttpClient(); + + public async Task> GetSuggestionsAsync(string query) + { + try + { + const string api = "https://api.bing.com/qsonhs.aspx?q="; + + await using var resultStream = await Http + .GetStreamAsync(api + Uri.EscapeDataString(query)) + .ConfigureAwait(false); + + using var json = await JsonDocument + .ParseAsync(resultStream) + .ConfigureAwait(false); + + var root = json.RootElement.GetProperty("AS"); + + if (root.GetProperty("FullResults").GetInt32() == 0) + return []; + + string[] titles = [ + .. root.GetProperty("Results") + .EnumerateArray() + .SelectMany(r => r.GetProperty("Suggests").EnumerateArray()) + .Select(s => s.GetProperty("Txt").GetString()) + .Where(s => s is not null) + .Select(s => s!) + ]; + + return [.. titles.Select(t => new Suggestion(t))]; + } + catch (Exception e) + when (e is HttpRequestException or { InnerException: TimeoutException }) + { + ExtensionHost.LogMessage($"{e.Message}"); + + return []; + } + } + + public override string ToString() + { + return "Bing"; + } +} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/CanIUse.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/CanIUse.cs new file mode 100644 index 0000000..77aced8 --- /dev/null +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/CanIUse.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using WebSearchShortcut.Properties; + +namespace WebSearchShortcut.SuggestionsProviders; + +internal sealed class CanIUse : ISuggestionsProvider +{ + public string Name => "CanIUse"; + + private HttpClient Http { get; } = new HttpClient(); + + public async Task> GetSuggestionsAsync(string query) + { + try + { + const string api = "https://caniuse.com/process/query.php?search="; + + await using var resultStream = await Http + .GetStreamAsync(api + Uri.EscapeDataString(query)) + .ConfigureAwait(false); + + using var json = await JsonDocument + .ParseAsync(resultStream) + .ConfigureAwait(false); + + var featureIds = json + .RootElement.GetProperty("featureIds") + .EnumerateArray() + .Take(10); + + Suggestion[] items = [ + .. featureIds + .Select(o => + { + var title = o.GetString(); + return title is null + ? null + : new Suggestion(title); + }) + .Where(s => s != null) + .Select(s => s!) + ]; + + return items; + } + catch (Exception e) + { + ExtensionHost.LogMessage($"{e.Message}"); + + return []; + } + } + + public override string ToString() + { + return "CanIUse"; + } +} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/DuckDuckGo.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/DuckDuckGo.cs new file mode 100644 index 0000000..2faf018 --- /dev/null +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/DuckDuckGo.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using WebSearchShortcut.Properties; + +namespace WebSearchShortcut.SuggestionsProviders; + +internal sealed class DuckDuckGo : ISuggestionsProvider +{ + public string Name => "DuckDuckGo"; + + private HttpClient Http { get; } = new HttpClient(); + + public async Task> GetSuggestionsAsync(string query) + { + try + { + const string api = "https://duckduckgo.com/ac/?q="; + + await using var resultStream = await Http + .GetStreamAsync(api + Uri.EscapeDataString(query)) + .ConfigureAwait(false); + + using var json = await JsonDocument + .ParseAsync(resultStream) + .ConfigureAwait(false); + + var results = json.RootElement; + + string[] titles = [ + .. results + .EnumerateArray() + .Select(o => o.GetProperty("phrase").GetString()) + .Where(s => + !string.IsNullOrEmpty(s) && + !s.Equals(query, StringComparison.OrdinalIgnoreCase) + ) + .Select(s => s!) + ]; + + return [.. titles.Select(t => new Suggestion(t))]; + } + catch (Exception e) + { + ExtensionHost.LogMessage($"{e.Message}"); + + return []; + } + } + + public override string ToString() + { + return "DuckDuckGo"; + } +} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Google.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Google.cs new file mode 100644 index 0000000..9158b8a --- /dev/null +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Google.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using WebSearchShortcut.Properties; + +namespace WebSearchShortcut.SuggestionsProviders; + +internal sealed class Google : ISuggestionsProvider +{ + public string Name => "Google"; + + private HttpClient Http { get; } = new HttpClient(); + + public async Task> GetSuggestionsAsync(string query) + { + try + { + const string api = "https://www.google.com/complete/search?output=chrome&q="; + + await using var resultStream = await Http + .GetStreamAsync(api + Uri.EscapeDataString(query)) + .ConfigureAwait(false); + + using var json = await JsonDocument + .ParseAsync(resultStream) + .ConfigureAwait(false); + + var results = json.RootElement.EnumerateArray().ElementAt(1); + + string[] titles = [ + .. results + .EnumerateArray() + .Select(o => o.GetString()) + .Where(s => s is not null) + .Select(s => s!) + ]; + + return [.. titles.Select(t => new Suggestion(t))]; + } + catch (Exception e) + { + ExtensionHost.LogMessage($"{e.Message}"); + + return []; + } + } + + public override string ToString() + { + return "Google"; + } +} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Npm.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Npm.cs new file mode 100644 index 0000000..e078fcd --- /dev/null +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Npm.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using WebSearchShortcut.Properties; + +namespace WebSearchShortcut.SuggestionsProviders; + +internal sealed class Npm : ISuggestionsProvider +{ + public string Name => "Npm"; + + private HttpClient Http { get; } = new HttpClient(); + + public async Task> GetSuggestionsAsync(string query) + { + try + { + const string api = "https://www.npmjs.com/search/suggestions?q="; + + await using var resultStream = await Http + .GetStreamAsync(api + Uri.EscapeDataString(query)) + .ConfigureAwait(false); + + using var json = await JsonDocument + .ParseAsync(resultStream) + .ConfigureAwait(false); + + var results = json.RootElement.EnumerateArray(); + + Suggestion[] items = [ + .. results + .Select(o => + { + var title = o.GetProperty("name").GetString(); + var description = o.GetProperty("description").GetString(); + return title is null ? null : new Suggestion(title, description ?? ""); + }) + .Where(s => s is not null) + .Select(s => s!) + ]; + + return items; + } + catch (Exception e) + { + ExtensionHost.LogMessage($"{e.Message}"); + + return []; + } + } + + public override string ToString() + { + return "Npm"; + } +} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Wikipedia.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Wikipedia.cs new file mode 100644 index 0000000..1c58828 --- /dev/null +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/Wikipedia.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using WebSearchShortcut.Properties; + +namespace WebSearchShortcut.SuggestionsProviders; + +internal sealed class Wikipedia : ISuggestionsProvider +{ + public string Name => "Wikipedia"; + + private HttpClient Http { get; } = new HttpClient(); + + public async Task> GetSuggestionsAsync(string query) + { + try + { + const string api = "https://api.wikimedia.org/core/v1/wikipedia/en/search/title?q="; + + await using var resultStream = await Http + .GetStreamAsync(api + Uri.EscapeDataString(query)) + .ConfigureAwait(false); + + using var json = await JsonDocument + .ParseAsync(resultStream) + .ConfigureAwait(false); + + var results = json.RootElement.GetProperty("pages"); + + Suggestion[] items = [ + .. results + .EnumerateArray() + .Select(o => ( + Title: o.TryGetProperty("title", out var t) ? t.GetString() : null, + Description: o.TryGetProperty("description", out var d) ? d.GetString() : null + )) + .Where(p => !string.IsNullOrWhiteSpace(p.Title)) + .Select(p => new Suggestion(p.Title!, p.Description ?? "")) + ]; + + return items; + } + catch (Exception e) + { + ExtensionHost.LogMessage($"{e.Message}"); + + return []; + } + } + + public override string ToString() + { + return "Wikipedia"; + } +} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/YouTube.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/YouTube.cs similarity index 59% rename from CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/YouTube.cs rename to CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/YouTube.cs index df1f551..142e62f 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProvider/YouTube.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/SuggestionsProviders/YouTube.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using System.Linq; @@ -9,48 +8,53 @@ using Microsoft.CommandPalette.Extensions.Toolkit; using WebSearchShortcut.Properties; -namespace WebSearchShortcut.SuggestionsProvider; +namespace WebSearchShortcut.SuggestionsProviders; -public class YouTube : IWebSearchShortcutSuggestionsProvider +internal sealed class YouTube : ISuggestionsProvider { - public static string Name => "YouTube"; + public string Name => "YouTube"; private HttpClient Http { get; } = new HttpClient(); - public async Task> QuerySuggestionsAsync(string query) + public async Task> GetSuggestionsAsync(string query) { try { const string api = "https://suggestqueries-clients6.youtube.com/complete/search?ds=yt&client=youtube&gs_ri=youtube&q="; - var result = await Http.GetStringAsync(api + Uri.EscapeDataString(query)); + var result = await Http + .GetStringAsync(api + Uri.EscapeDataString(query)) + .ConfigureAwait(false); var match = Regex.Match(result, @"window\.google\.ac\.h\((.*)\)$"); if (!match.Success) { ExtensionHost.LogMessage("No match found in the response."); - return new List(); + + return []; } var jsonContent = match.Groups[1].Value; using var json = JsonDocument.Parse(jsonContent); var results = json.RootElement[1].EnumerateArray(); - List items = results - .Select(o => - { - var title = o[0].GetString(); - return title is null ? null : new SuggestionsItem(title); - }) - .Where(s => s is not null) - .Select(s => s!) - .ToList(); + Suggestion[] items = [ + .. results + .Select(o => + { + var title = o[0].GetString(); + return title is null ? null : new Suggestion(title); + }) + .Where(s => s is not null) + .Select(s => s!) + ]; return items; } catch (Exception e) { ExtensionHost.LogMessage($"{e.Message}"); + return []; } } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcut.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcut.cs index 5068509..0a8fae2 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcut.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcut.cs @@ -4,7 +4,6 @@ using System; using System.Runtime.InteropServices; -using System.Collections.Generic; using System.Threading; using Microsoft.CommandPalette.Extensions; diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutCommandsProvider.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutCommandsProvider.cs index 986714f..c1bcf71 100644 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutCommandsProvider.cs +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutCommandsProvider.cs @@ -9,7 +9,6 @@ using System.Linq; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -using WebSearchShortcut.Constants; using WebSearchShortcut.Helpers; using WebSearchShortcut.Properties; using WebSearchShortcut.Services; @@ -18,79 +17,100 @@ namespace WebSearchShortcut; public partial class WebSearchShortcutCommandsProvider : CommandProvider { - private readonly List _commands; - private readonly AddShortcutPage _addNewCommand = new(null); - + private readonly AddShortcutPage _addShortcutPage = new(null); + private ICommandItem[] _topLevelCommands = []; private Storage? _storage; - public WebSearchShortcutCommandsProvider() { - DisplayName = Resources.WebSearchShortcutCommandsProvider_DisplayName; + DisplayName = Resources.WebSearchShortcut_DisplayName; Icon = IconHelpers.FromRelativePath("Assets\\Search.png"); - _commands = [ - // new CommandItem(new WebSearchShortcutPage()) { Title = DisplayName }, - ]; - _addNewCommand.AddedCommand += AddNewCommand_AddedCommand; + + _addShortcutPage.AddedCommand += AddNewCommand_AddedCommand; } - private void AddNewCommand_AddedCommand(object sender, WebSearchShortcutItem args) + public override ICommandItem[] TopLevelCommands() { - ExtensionHost.LogMessage($"Adding bookmark ({args.Name},{args.Url})"); - if (_storage != null) + if (_topLevelCommands.Length == 0) { - _storage.Data.Add(args); - UpdateIconUrl(args); + ReloadCommands(); } - SaveAndUpdateCommands(); + return _topLevelCommands; } - private async void UpdateIconUrl(WebSearchShortcutItem item) + internal static string GetShortcutsJsonPath() { - if (_storage == null || !string.IsNullOrWhiteSpace(item.IconUrl)) + string directory = Utilities.BaseSettingsPath("WebSearchShortcut"); + Directory.CreateDirectory(directory); + + return Path.Combine(directory, "WebSearchShortcut.json"); + } + + private void AddNewCommand_AddedCommand(object sender, WebSearchShortcutDataEntry shortcut) + { + ExtensionHost.LogMessage($"Adding bookmark ({shortcut.Name},{shortcut.Url})"); + if (_storage != null) { - return; + _storage.Data.Add(shortcut); + UpdateIconUrlAsync(shortcut); } - var url = await IconService.UpdateIconUrlAsync(item); - SaveAndUpdateCommands(); - ExtensionHost.LogMessage($"Updating icon URL for bookmark ({item.Name},{item.Url}) to {url}"); + SaveAndRefresh(); + } + + private void Edit_AddedCommand(object sender, WebSearchShortcutDataEntry shortcut) + { + ExtensionHost.LogMessage($"Edited bookmark ({shortcut.Name},{shortcut.Url})"); + UpdateIconUrlAsync(shortcut); + + SaveAndRefresh(); } - private void Edit_AddedCommand(object sender, WebSearchShortcutItem args) + private async void UpdateIconUrlAsync(WebSearchShortcutDataEntry shortcut) { - ExtensionHost.LogMessage($"Edited bookmark ({args.Name},{args.Url})"); - UpdateIconUrl(args); + if (_storage is null) return; + if (!string.IsNullOrWhiteSpace(shortcut.IconUrl)) return; - SaveAndUpdateCommands(); + var url = await IconService.UpdateIconUrlAsync(shortcut); + SaveAndRefresh(); + ExtensionHost.LogMessage($"Updating icon URL for bookmark ({shortcut.Name},{shortcut.Url}) to {url}"); } - private void LoadCommands() + private void SaveAndRefresh() { - List collected = []; - collected.Add(new CommandItem(_addNewCommand)); + if (_storage is not null) + { + var jsonPath = GetShortcutsJsonPath(); + Storage.WriteToFile(jsonPath, _storage); + } - if (_storage == null) + ReloadCommands(); + RaiseItemsChanged(0); + } + + private void ReloadCommands() + { + List items = [new CommandItem(_addShortcutPage)]; + + if (_storage is null) { LoadShortcutFromFile(); } - if (_storage != null) + if (_storage is not null) { - collected.AddRange(_storage.Data.Select(ShortcutToCommandItem)); + items.AddRange(_storage.Data.Select(CreateCommandItem)); } - _commands.Clear(); - _commands.AddRange(collected); + _topLevelCommands = [.. items]; } private void LoadShortcutFromFile() { try { - - var jsonFile = StateJsonPath(); + var jsonFile = GetShortcutsJsonPath(); _storage = Storage.ReadFromFile(jsonFile); } catch (Exception ex) @@ -100,75 +120,38 @@ private void LoadShortcutFromFile() } } - private CommandItem ShortcutToCommandItem(WebSearchShortcutItem item) + private CommandItem CreateCommandItem(WebSearchShortcutDataEntry shortcut) { - ICommand command = new SearchPage(item); - var listItem = new CommandItem(command) { Icon = command.Icon }; - List contextMenu = []; + var editShortcutPage = new AddShortcutPage(shortcut); + editShortcutPage.AddedCommand += Edit_AddedCommand; + var editCommand = new CommandContextItem(editShortcutPage) { Icon = Icons.Edit }; - if (command is SearchPage searchPage) - { - listItem.Subtitle = StringFormatter.Format(Resources.WebSearchShortcutCommandsProvider_CommandItemSubtitle, new() { ["engine"] = item.Name }); - } - - var edit = new AddShortcutPage(item) { Icon = Icons.Edit }; - edit.AddedCommand += Edit_AddedCommand; - contextMenu.Add(new CommandContextItem(edit)); - - var delete = new CommandContextItem( - title: Resources.WebSearchShortcutCommandsProvider_CommandItemDeleteTitle, - name: Resources.WebSearchShortcutCommandsProvider_CommandItemDeleteName, + var deleteCommand = new CommandContextItem( + title: Resources.SearchShortcut_DeleteTitle, + name: Resources.SearchShortcut_DeleteName, action: () => { if (_storage != null) { - ExtensionHost.LogMessage($"Deleting bookmark ({item.Name},{item.Url})"); + ExtensionHost.LogMessage($"Deleting bookmark ({shortcut.Name},{shortcut.Url})"); - _storage.Data.Remove(item); + _storage.Data.Remove(shortcut); - SaveAndUpdateCommands(); + SaveAndRefresh(); } }, result: CommandResult.KeepOpen()) { - IsCritical = true, Icon = Icons.Delete, + IsCritical = true }; - contextMenu.Add(delete); - - listItem.MoreCommands = [.. contextMenu]; - - return listItem; - } - - private void SaveAndUpdateCommands() - { - if (_storage != null) - { - var jsonPath = StateJsonPath(); - Storage.WriteToFile(jsonPath, _storage); - } - LoadCommands(); - RaiseItemsChanged(0); - } - - public override ICommandItem[] TopLevelCommands() - { - if (_commands.Count == 0) + var commandItem = new CommandItem(new SearchWebPage(shortcut)) { - LoadCommands(); - } - - return [.. _commands]; - } - - internal static string StateJsonPath() - { - var directory = Utilities.BaseSettingsPath("WebSearchShortcut"); - Directory.CreateDirectory(directory); + Subtitle = StringFormatter.Format(Resources.SearchShortcut_SubtitleTemplate, new() { ["engine"] = shortcut.Name }), + MoreCommands = [editCommand, deleteCommand] + }; - // now, the state is just next to the exe - return Path.Combine(directory, "WebSearchShortcut.json"); + return commandItem; } } diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutDataEntry.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutDataEntry.cs new file mode 100644 index 0000000..0be1fa7 --- /dev/null +++ b/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutDataEntry.cs @@ -0,0 +1,55 @@ +using System; +using System.Net; +using System.Text.Json.Serialization; + +namespace WebSearchShortcut; + +internal sealed class WebSearchShortcutDataEntry +{ + public string Name { get; set; } = string.Empty; + public string? Keyword { get; set; } + public string Url { get; set; } = string.Empty; + // public string[]? Urls { get; set; } + public string? SuggestionProvider { get; set; } + public string? ReplaceWhitespace { get; set; } + public string? IconUrl { get; set; } + public string? HomePage { get; set; } + public string? BrowserPath { get; set; } + public string? BrowserArgs { get; set; } + + [JsonIgnore] + public string Domain + { + get + { + return new Uri(Url.Split(' ')[0].Split('?')[0]).GetLeftPart(UriPartial.Authority); + } + } + + static string UrlEncode(WebSearchShortcutDataEntry shortcut, string query) + { + if (string.IsNullOrWhiteSpace(shortcut.ReplaceWhitespace) || shortcut.ReplaceWhitespace == " ") + { + return WebUtility.UrlEncode(query); + } + if (shortcut.ReplaceWhitespace == "%20") + { + return WebUtility.UrlEncode(query).Replace("+", "%20"); + } + return WebUtility.UrlEncode(query.Replace(" ", shortcut.ReplaceWhitespace)); + } + + static public string GetSearchUrl(WebSearchShortcutDataEntry shortcut, string query) + { + string arguments = shortcut.Url.Replace("%s", UrlEncode(shortcut, query)); + + return arguments; + } + + static public string GetHomePageUrl(WebSearchShortcutDataEntry shortcut) + { + return !string.IsNullOrWhiteSpace(shortcut.HomePage) + ? shortcut.HomePage + : shortcut.Domain; + } +} diff --git a/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutItem.cs b/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutItem.cs deleted file mode 100644 index 4872263..0000000 --- a/CmdPalWebSearchShortcut/WebSearchShortcut/WebSearchShortcutItem.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Net; -using System.Text.Json.Serialization; - -namespace WebSearchShortcut -{ - public class WebSearchShortcutItem - { - public string Name { get; set; } = string.Empty; - public string? Keyword { get; set; } - public string Url { get; set; } = string.Empty; - // public string[]? Urls { get; set; } - public string? SuggestionProvider { get; set; } - // public bool? IsDefault { get; set; } - public string? IconUrl { get; set; } - public string? BrowserPath { get; set; } - public string? BrowserArgs { get; set; } - public string? ReplaceWhitespace { get; set; } - public string? HomePage { get; set; } - [JsonIgnore] - public string Domain - { - get - { - return new Uri(Url.Split(' ')[0].Split('?')[0]).GetLeftPart(UriPartial.Authority); - } - } - - static string UrlEncode(WebSearchShortcutItem item, string search) - { - if (string.IsNullOrWhiteSpace(item.ReplaceWhitespace) || item.ReplaceWhitespace == " ") - { - return WebUtility.UrlEncode(search); - } - if (item.ReplaceWhitespace == "%20") - { - return WebUtility.UrlEncode(search).Replace("+", "%20"); - } - return WebUtility.UrlEncode(search.Replace(" ", item.ReplaceWhitespace)); - } - - static public string GetSearchUrl(WebSearchShortcutItem item, string search) - { - string arguments = item.Url.Replace("%s", UrlEncode(item, search)); - return arguments; - } - } -};