diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index fab1b3e8f00..e3f0e2a2f28 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -29,13 +29,13 @@ public static async Task UpdateManifestAsync(CancellationToken token = default) var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl); request.Headers.Add("If-None-Match", latestEtag); - var response = await Http.SendAsync(request, token).ConfigureAwait(false); + using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.OK) { Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo"); - var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false); + await using var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false); UserPlugins = await JsonSerializer.DeserializeAsync>(json, cancellationToken: token).ConfigureAwait(false); @@ -56,4 +56,4 @@ public static async Task UpdateManifestAsync(CancellationToken token = default) } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index bad0344eb30..44c47cf289d 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -79,7 +79,7 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false); } - var newVersionTips = NewVersinoTips(newReleaseVersion.ToString()); + var newVersionTips = NewVersionTips(newReleaseVersion.ToString()); Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); @@ -137,10 +137,10 @@ private async Task GitHubUpdateManagerAsync(string repository) return manager; } - public string NewVersinoTips(string version) + public string NewVersionTips(string version) { - var translater = InternationalizationManager.Instance; - var tips = string.Format(translater.GetTranslation("newVersionTips"), version); + var translator = InternationalizationManager.Instance; + var tips = string.Format(translator.GetTranslation("newVersionTips"), version); return tips; } diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 9f4146b7be3..e5be0701f2d 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -68,7 +68,7 @@ public static void UpdateProxy(ProxyProperty property) var userName when string.IsNullOrEmpty(userName) => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), null), _ => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), - new NetworkCredential(Proxy.UserName, Proxy.Password)) + new NetworkCredential(Proxy.UserName, Proxy.Password)) }, _ => (null, null) }, @@ -79,7 +79,7 @@ var userName when string.IsNullOrEmpty(userName) => _ => throw new ArgumentOutOfRangeException() }; } - catch(UriFormatException e) + catch (UriFormatException e) { API.ShowMsg("Please try again", "Unable to parse Http Proxy"); Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e); @@ -94,7 +94,7 @@ public static async Task DownloadAsync([NotNull] string url, [NotNull] string fi if (response.StatusCode == HttpStatusCode.OK) { await using var fileStream = new FileStream(filePath, FileMode.CreateNew); - await response.Content.CopyToAsync(fileStream); + await response.Content.CopyToAsync(fileStream, token); } else { @@ -117,7 +117,7 @@ public static async Task DownloadAsync([NotNull] string url, [NotNull] string fi public static Task GetAsync([NotNull] string url, CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - return GetAsync(new Uri(url.Replace("#", "%23")), token); + return GetAsync(new Uri(url), token); } /// @@ -130,36 +130,57 @@ public static async Task GetAsync([NotNull] Uri url, CancellationToken t { Log.Debug($"|Http.Get|Url <{url}>"); using var response = await client.GetAsync(url, token); - var content = await response.Content.ReadAsStringAsync(); - if (response.StatusCode == HttpStatusCode.OK) - { - return content; - } - else + var content = await response.Content.ReadAsStringAsync(token); + if (response.StatusCode != HttpStatusCode.OK) { throw new HttpRequestException( $"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>"); } + + return content; } /// - /// Asynchrously get the result as stream from url. + /// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation. + /// + /// The Uri the request is sent to. + /// An HTTP completion option value that indicates when the operation should be considered completed. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation + /// + public static Task GetStreamAsync([NotNull] string url, + CancellationToken token = default) => GetStreamAsync(new Uri(url), token); + + + /// + /// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation. /// /// + /// /// - public static async Task GetStreamAsync([NotNull] string url, CancellationToken token = default) + public static async Task GetStreamAsync([NotNull] Uri url, + CancellationToken token = default) + { + Log.Debug($"|Http.Get|Url <{url}>"); + return await client.GetStreamAsync(url, token); + } + + public static async Task GetResponseAsync(string url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, + CancellationToken token = default) + => await GetResponseAsync(new Uri(url), completionOption, token); + + public static async Task GetResponseAsync([NotNull] Uri url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, + CancellationToken token = default) { Log.Debug($"|Http.Get|Url <{url}>"); - var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - return await response.Content.ReadAsStreamAsync(); + return await client.GetAsync(url, completionOption, token); } /// /// Asynchrously send an HTTP request. /// - public static async Task SendAsync(HttpRequestMessage request, CancellationToken token = default) + public static async Task SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken token = default) { - return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + return await client.SendAsync(request, completionOption, token); } } } diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index 04e11bf1ace..2fd2291d44f 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -25,11 +25,11 @@ public ImageUsage(int usage, ImageSource image) public class ImageCache { private const int MaxCached = 50; - public ConcurrentDictionary Data { get; private set; } = new ConcurrentDictionary(); + public ConcurrentDictionary<(string, bool), ImageUsage> Data { get; } = new(); private const int permissibleFactor = 2; private SemaphoreSlim semaphore = new(1, 1); - public void Initialization(Dictionary usage) + public void Initialization(Dictionary<(string, bool), int> usage) { foreach (var key in usage.Keys) { @@ -37,29 +37,29 @@ public void Initialization(Dictionary usage) } } - public ImageSource this[string path] + public ImageSource this[string path, bool isFullImage = false] { get { - if (Data.TryGetValue(path, out var value)) + if (!Data.TryGetValue((path, isFullImage), out var value)) { - value.usage++; - return value.imageSource; + return null; } + value.usage++; + return value.imageSource; - return null; } set { Data.AddOrUpdate( - path, - new ImageUsage(0, value), - (k, v) => - { - v.imageSource = value; - v.usage++; - return v; - } + (path, isFullImage), + new ImageUsage(0, value), + (k, v) => + { + v.imageSource = value; + v.usage++; + return v; + } ); SliceExtra(); @@ -82,9 +82,9 @@ async void SliceExtra() } } - public bool ContainsKey(string key) + public bool ContainsKey(string key, bool isFullImage) { - return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null; + return key is not null && Data.ContainsKey((key, isFullImage)) && Data[(key, isFullImage)].imageSource != null; } public int CacheSize() @@ -100,4 +100,4 @@ public int UniqueImagesInCache() return Data.Values.Select(x => x.imageSource).Distinct().Count(); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 11f66c8affd..13022137960 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -3,57 +3,58 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; +using static Flow.Launcher.Infrastructure.Http.Http; namespace Flow.Launcher.Infrastructure.Image { public static class ImageLoader { private static readonly ImageCache ImageCache = new(); - private static BinaryStorage> _storage; + private static BinaryStorage> _storage; private static readonly ConcurrentDictionary GuidToKey = new(); private static IImageHashGenerator _hashGenerator; private static readonly bool EnableImageHash = true; public static ImageSource DefaultImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon)); + public const int SmallIconSize = 32; private static readonly string[] ImageExtensions = { - ".png", - ".jpg", - ".jpeg", - ".gif", - ".bmp", - ".tiff", - ".ico" + ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" }; public static void Initialize() { - _storage = new BinaryStorage>("Image"); + _storage = new BinaryStorage>("Image"); _hashGenerator = new ImageHashGenerator(); var usage = LoadStorageToConcurrentDictionary(); - foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon }) + foreach (var icon in new[] + { + Constant.DefaultIcon, Constant.MissingImgIcon + }) { ImageSource img = new BitmapImage(new Uri(icon)); img.Freeze(); - ImageCache[icon] = img; + ImageCache[icon, false] = img; } - _ = Task.Run(() => + _ = Task.Run(async () => { - Stopwatch.Normal("|ImageLoader.Initialize|Preload images cost", () => + await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () => { - ImageCache.Data.AsParallel().ForAll(x => + foreach (var ((path, isFullImage), _) in ImageCache.Data) { - Load(x.Key); - }); + await LoadAsync(path, isFullImage); + } }); Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); }); @@ -63,17 +64,20 @@ public static void Save() { lock (_storage) { - _storage.Save(ImageCache.Data.Select(x => (x.Key, x.Value.usage)).ToDictionary(x => x.Key, x => x.usage)); + _storage.Save(ImageCache.Data + .ToDictionary( + x => x.Key, + x => x.Value.usage)); } } - private static ConcurrentDictionary LoadStorageToConcurrentDictionary() + private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary() { lock (_storage) { - var loaded = _storage.TryLoad(new Dictionary()); + var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>()); - return new ConcurrentDictionary(loaded); + return new ConcurrentDictionary<(string, bool), int>(loaded); } } @@ -99,7 +103,7 @@ private enum ImageType Cache } - private static ImageResult LoadInternal(string path, bool loadFullImage = false) + private static async ValueTask LoadInternalAsync(string path, bool loadFullImage = false) { ImageResult imageResult; @@ -107,13 +111,21 @@ private static ImageResult LoadInternal(string path, bool loadFullImage = false) { if (string.IsNullOrEmpty(path)) { - return new ImageResult(ImageCache[Constant.MissingImgIcon], ImageType.Error); + return new ImageResult(DefaultImage, ImageType.Error); } - if (ImageCache.ContainsKey(path)) + + if (ImageCache.ContainsKey(path, loadFullImage)) { - return new ImageResult(ImageCache[path], ImageType.Cache); + return new ImageResult(ImageCache[path, loadFullImage], ImageType.Cache); } + if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) + { + var image = await LoadRemoteImageAsync(loadFullImage, uriResult); + ImageCache[path, loadFullImage] = image; + return new ImageResult(image, ImageType.ImageFile); + } if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) { var imageSource = new BitmapImage(new Uri(path)); @@ -121,12 +133,7 @@ private static ImageResult LoadInternal(string path, bool loadFullImage = false) return new ImageResult(imageSource, ImageType.Data); } - if (!Path.IsPathRooted(path)) - { - path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path)); - } - - imageResult = GetThumbnailResult(ref path, loadFullImage); + imageResult = await Task.Run(() => GetThumbnailResult(ref path, loadFullImage)); } catch (System.Exception e) { @@ -140,14 +147,35 @@ private static ImageResult LoadInternal(string path, bool loadFullImage = false) Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e); Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2); - ImageSource image = ImageCache[Constant.MissingImgIcon]; - ImageCache[path] = image; + ImageSource image = ImageCache[Constant.MissingImgIcon, false]; + ImageCache[path, false] = image; imageResult = new ImageResult(image, ImageType.Error); } } return imageResult; } + private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult) + { + // Download image from url + await using var resp = await GetStreamAsync(uriResult); + await using var buffer = new MemoryStream(); + await resp.CopyToAsync(buffer); + buffer.Seek(0, SeekOrigin.Begin); + var image = new BitmapImage(); + image.BeginInit(); + image.CacheOption = BitmapCacheOption.OnLoad; + if (!loadFullImage) + { + image.DecodePixelHeight = SmallIconSize; + image.DecodePixelWidth = SmallIconSize; + } + image.StreamSource = buffer; + image.EndInit(); + image.StreamSource = null; + image.Freeze(); + return image; + } private static ImageResult GetThumbnailResult(ref string path, bool loadFullImage = false) { @@ -192,7 +220,7 @@ private static ImageResult GetThumbnailResult(ref string path, bool loadFullImag } else { - image = ImageCache[Constant.MissingImgIcon]; + image = ImageCache[Constant.MissingImgIcon, false]; path = Constant.MissingImgIcon; } @@ -213,14 +241,14 @@ private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = option); } - public static bool CacheContainImage(string path) + public static bool CacheContainImage(string path, bool loadFullImage = false) { - return ImageCache.ContainsKey(path) && ImageCache[path] != null; + return ImageCache.ContainsKey(path, false) && ImageCache[path, loadFullImage] != null; } - public static ImageSource Load(string path, bool loadFullImage = false) + public static async ValueTask LoadAsync(string path, bool loadFullImage = false) { - var imageResult = LoadInternal(path, loadFullImage); + var imageResult = await LoadInternalAsync(path, loadFullImage); var img = imageResult.ImageSource; if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache) @@ -231,7 +259,7 @@ public static ImageSource Load(string path, bool loadFullImage = false) if (GuidToKey.TryGetValue(hash, out string key)) { // image already exists - img = ImageCache[key] ?? img; + img = ImageCache[key, false] ?? img; } else { // new guid @@ -240,7 +268,7 @@ public static ImageSource Load(string path, bool loadFullImage = false) } // update cache - ImageCache[path] = img; + ImageCache[path, false] = img; } return img; diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index a1d3b83abf2..f2d9323ef83 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -60,9 +60,14 @@ public string IcoPath get { return _icoPath; } set { - if (!string.IsNullOrEmpty(PluginDirectory) && !Path.IsPathRooted(value)) + // As a standard this property will handle prepping and converting to absolute local path for icon image processing + if (!string.IsNullOrEmpty(value) + && !string.IsNullOrEmpty(PluginDirectory) + && !Path.IsPathRooted(value) + && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { - _icoPath = Path.Combine(value, IcoPath); + _icoPath = Path.Combine(PluginDirectory, value); } else { @@ -140,10 +145,11 @@ public string PluginDirectory set { _pluginDirectory = value; - if (!string.IsNullOrEmpty(IcoPath) && !Path.IsPathRooted(IcoPath)) - { - IcoPath = Path.Combine(value, IcoPath); - } + + // When the Result object is returned from the query call, PluginDirectory is not provided until + // UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available + // we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded. + IcoPath = _icoPath; } } diff --git a/Flow.Launcher/Msg.xaml.cs b/Flow.Launcher/Msg.xaml.cs index 6bb2fc2dc6f..1be89d71604 100644 --- a/Flow.Launcher/Msg.xaml.cs +++ b/Flow.Launcher/Msg.xaml.cs @@ -38,10 +38,16 @@ public Msg() Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(TopProperty)); fadeOutStoryboard.Children.Add(fadeOutAnimation); - imgClose.Source = ImageLoader.Load(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\close.png")); + _ = LoadImageAsync(); + imgClose.MouseUp += imgClose_MouseUp; } + private async System.Threading.Tasks.Task LoadImageAsync() + { + imgClose.Source = await ImageLoader.LoadAsync(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\close.png")); + } + void imgClose_MouseUp(object sender, MouseButtonEventArgs e) { if (!closing) @@ -56,7 +62,7 @@ private void fadeOutStoryboard_Completed(object sender, EventArgs e) Close(); } - public void Show(string title, string subTitle, string iconPath) + public async void Show(string title, string subTitle, string iconPath) { tbTitle.Text = title; tbSubTitle.Text = subTitle; @@ -66,15 +72,15 @@ public void Show(string title, string subTitle, string iconPath) } if (!File.Exists(iconPath)) { - imgIco.Source = ImageLoader.Load(Path.Combine(Constant.ProgramDirectory, "Images\\app.png")); + imgIco.Source = await ImageLoader.LoadAsync(Path.Combine(Constant.ProgramDirectory, "Images\\app.png")); } else { - imgIco.Source = ImageLoader.Load(iconPath); + imgIco.Source = await ImageLoader.LoadAsync(iconPath); } Show(); - Dispatcher.InvokeAsync(async () => + await Dispatcher.InvokeAsync(async () => { if (!closing) { diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 2294681b4dc..725857e2f28 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -1,4 +1,5 @@ -using System.Windows; +using System.Threading.Tasks; +using System.Windows; using System.Windows.Media; using Flow.Launcher.Plugin; using Flow.Launcher.Infrastructure.Image; @@ -24,7 +25,23 @@ public PluginPair PluginPair } } - public ImageSource Image => ImageLoader.Load(PluginPair.Metadata.IcoPath); + + private async void LoadIconAsync() + { + Image = await ImageLoader.LoadAsync(PluginPair.Metadata.IcoPath); + } + + public ImageSource Image + { + get + { + if (_image == ImageLoader.DefaultImage) + LoadIconAsync(); + + return _image; + } + set => _image = value; + } public bool PluginState { get => !PluginPair.Metadata.Disabled; @@ -50,6 +67,7 @@ public Control SettingControl ? new Control() : settingProvider.CreateSettingPanel() : null; + private ImageSource _image = ImageLoader.DefaultImage; public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed; public string InitilizaTime => PluginPair.Metadata.InitTime + "ms"; diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 458aa498f4b..2d61f6cabd3 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -15,7 +15,7 @@ namespace Flow.Launcher.ViewModel public class ResultViewModel : BaseModel { private static PrivateFontCollection fontCollection = new(); - private static Dictionary fonts = new(); + private static Dictionary fonts = new(); public ResultViewModel(Result result, Settings settings) { @@ -145,7 +145,7 @@ public ImageSource Image public GlyphInfo Glyph { get; set; } - private async ValueTask LoadImageAsync() + private async Task LoadImageAsync() { var imagePath = Result.IcoPath; if (string.IsNullOrEmpty(imagePath) && Result.Icon != null) @@ -168,12 +168,12 @@ private async ValueTask LoadImageAsync() if (ImageLoader.CacheContainImage(imagePath)) { // will get here either when icoPath has value\icon delegate is null\when had exception in delegate - image = ImageLoader.Load(imagePath, loadFullImage); + image = await ImageLoader.LoadAsync(imagePath, loadFullImage); return; } // We need to modify the property not field here to trigger the OnPropertyChanged event - Image = await Task.Run(() => ImageLoader.Load(imagePath, loadFullImage)).ConfigureAwait(false); + Image = await ImageLoader.LoadAsync(imagePath, loadFullImage).ConfigureAwait(false); } public Result Result { get; } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SearchSourceSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SearchSourceSetting.xaml.cs index fd0701c012a..c19396da0dc 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SearchSourceSetting.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SearchSourceSetting.xaml.cs @@ -30,7 +30,7 @@ public SearchSourceSettingWindow(IList sources, PluginInitContext Initilize(sources, context, Action.Add); } - private void Initilize(IList sources, PluginInitContext context, Action action) + private async void Initilize(IList sources, PluginInitContext context, Action action) { InitializeComponent(); DataContext = _viewModel; @@ -42,7 +42,7 @@ private void Initilize(IList sources, PluginInitContext context, A _viewModel.SetupCustomImagesDirectory(); - imgPreviewIcon.Source = _viewModel.LoadPreviewIcon(_searchSource.IconPath); + imgPreviewIcon.Source = await _viewModel.LoadPreviewIconAsync(_searchSource.IconPath); } private void OnCancelButtonClick(object sender, RoutedEventArgs e) @@ -125,7 +125,7 @@ private void EditSearchSource() } } - private void OnSelectIconClick(object sender, RoutedEventArgs e) + private async void OnSelectIconClick(object sender, RoutedEventArgs e) { const string filter = "Image files (*.jpg, *.jpeg, *.gif, *.png, *.bmp) |*.jpg; *.jpeg; *.gif; *.png; *.bmp"; var dialog = new OpenFileDialog {InitialDirectory = Main.CustomImagesDirectory, Filter = filter}; @@ -140,7 +140,7 @@ private void OnSelectIconClick(object sender, RoutedEventArgs e) if (_viewModel.ShouldProvideHint(selectedNewIconImageFullPath)) MessageBox.Show(_api.GetTranslation("flowlauncher_plugin_websearch_iconpath_hint")); - imgPreviewIcon.Source = _viewModel.LoadPreviewIcon(selectedNewIconImageFullPath); + imgPreviewIcon.Source = await _viewModel.LoadPreviewIconAsync(selectedNewIconImageFullPath); } } } @@ -151,4 +151,4 @@ public enum Action Add, Edit } -} \ No newline at end of file +} diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SearchSourceViewModel.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SearchSourceViewModel.cs index 372e601e049..1b42bf788ee 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SearchSourceViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SearchSourceViewModel.cs @@ -2,6 +2,7 @@ using System; using System.Drawing; using System.IO; +using System.Threading.Tasks; using System.Windows; using System.Windows.Media; @@ -57,9 +58,9 @@ internal bool ShouldProvideHint(string fullPathToSelectedImage) return Directory.GetParent(fullPathToSelectedImage).ToString() == Main.DefaultImagesDirectory; } - internal ImageSource LoadPreviewIcon(string pathToPreviewIconImage) + internal async ValueTask LoadPreviewIconAsync(string pathToPreviewIconImage) { - return ImageLoader.Load(pathToPreviewIconImage); + return await ImageLoader.LoadAsync(pathToPreviewIconImage); } } } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs index e01b66df55f..b8ad9a586da 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Bing.cs @@ -21,8 +21,8 @@ public override async Task> SuggestionsAsync(string query, Cancella try { const string api = "https://api.bing.com/qsonhs.aspx?q="; - - using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query), token).ConfigureAwait(false); + + await using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query), token).ConfigureAwait(false); using var json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token)); var root = json.RootElement.GetProperty("AS"); @@ -31,16 +31,16 @@ public override async Task> SuggestionsAsync(string query, Cancella return new List(); return root.GetProperty("Results") - .EnumerateArray() - .SelectMany(r => r.GetProperty("Suggests") - .EnumerateArray() - .Select(s => s.GetProperty("Txt").GetString())) - .ToList(); + .EnumerateArray() + .SelectMany(r => r.GetProperty("Suggests") + .EnumerateArray() + .Select(s => s.GetProperty("Txt").GetString())) + .ToList(); } - catch (Exception e) when (e is HttpRequestException or {InnerException: TimeoutException}) + catch (Exception e) when (e is HttpRequestException or { InnerException: TimeoutException }) { Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e); return null; @@ -49,7 +49,7 @@ public override async Task> SuggestionsAsync(string query, Cancella { Log.Exception("|Bing.Suggestions|can't parse suggestions", e); return new List(); - } + } } public override string ToString() diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs index bde63ab29d5..567e896b588 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/Google.cs @@ -20,13 +20,10 @@ public override async Task> SuggestionsAsync(string query, Cancella { const string api = "https://www.google.com/complete/search?output=chrome&q="; - using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query)).ConfigureAwait(false); + await using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query), token: token).ConfigureAwait(false); using var json = await JsonDocument.ParseAsync(resultStream, cancellationToken: token); - if (json == null) - return new List(); - var results = json.RootElement.EnumerateArray().ElementAt(1); return results.EnumerateArray().Select(o => o.GetString()).ToList();