Merge branch 'dev' into ThemeRedesign

This commit is contained in:
DB P 2022-11-25 13:32:07 +09:00 committed by GitHub
commit 2df24c2790
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 225 additions and 127 deletions

View file

@ -29,13 +29,13 @@ namespace Flow.Launcher.Core.ExternalPlugins
var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl); var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl);
request.Headers.Add("If-None-Match", latestEtag); 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) if (response.StatusCode == HttpStatusCode.OK)
{ {
Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo"); 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<List<UserPlugin>>(json, cancellationToken: token).ConfigureAwait(false); UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(json, cancellationToken: token).ConfigureAwait(false);
@ -56,4 +56,4 @@ namespace Flow.Launcher.Core.ExternalPlugins
} }
} }
} }
} }

View file

@ -79,7 +79,7 @@ namespace Flow.Launcher.Core
await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false); await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false);
} }
var newVersionTips = NewVersinoTips(newReleaseVersion.ToString()); var newVersionTips = NewVersionTips(newReleaseVersion.ToString());
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
@ -137,10 +137,10 @@ namespace Flow.Launcher.Core
return manager; return manager;
} }
public string NewVersinoTips(string version) public string NewVersionTips(string version)
{ {
var translater = InternationalizationManager.Instance; var translator = InternationalizationManager.Instance;
var tips = string.Format(translater.GetTranslation("newVersionTips"), version); var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
return tips; return tips;
} }

View file

@ -45,6 +45,7 @@ namespace Flow.Launcher.Infrastructure
public const string Logs = "Logs"; public const string Logs = "Logs";
public const string Website = "https://flowlauncher.com"; public const string Website = "https://flowlauncher.com";
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher"; public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
public const string Docs = "https://flowlauncher.com/docs"; public const string Docs = "https://flowlauncher.com/docs";
} }

View file

@ -68,7 +68,7 @@ namespace Flow.Launcher.Infrastructure.Http
var userName when string.IsNullOrEmpty(userName) => var userName when string.IsNullOrEmpty(userName) =>
(new Uri($"http://{Proxy.Server}:{Proxy.Port}"), null), (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), null),
_ => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"), _ => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"),
new NetworkCredential(Proxy.UserName, Proxy.Password)) new NetworkCredential(Proxy.UserName, Proxy.Password))
}, },
_ => (null, null) _ => (null, null)
}, },
@ -79,7 +79,7 @@ namespace Flow.Launcher.Infrastructure.Http
_ => throw new ArgumentOutOfRangeException() _ => throw new ArgumentOutOfRangeException()
}; };
} }
catch(UriFormatException e) catch (UriFormatException e)
{ {
API.ShowMsg("Please try again", "Unable to parse Http Proxy"); API.ShowMsg("Please try again", "Unable to parse Http Proxy");
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e); Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
@ -94,7 +94,7 @@ namespace Flow.Launcher.Infrastructure.Http
if (response.StatusCode == HttpStatusCode.OK) if (response.StatusCode == HttpStatusCode.OK)
{ {
await using var fileStream = new FileStream(filePath, FileMode.CreateNew); await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
await response.Content.CopyToAsync(fileStream); await response.Content.CopyToAsync(fileStream, token);
} }
else else
{ {
@ -117,7 +117,7 @@ namespace Flow.Launcher.Infrastructure.Http
public static Task<string> GetAsync([NotNull] string url, CancellationToken token = default) public static Task<string> GetAsync([NotNull] string url, CancellationToken token = default)
{ {
Log.Debug($"|Http.Get|Url <{url}>"); Log.Debug($"|Http.Get|Url <{url}>");
return GetAsync(new Uri(url.Replace("#", "%23")), token); return GetAsync(new Uri(url), token);
} }
/// <summary> /// <summary>
@ -130,36 +130,57 @@ namespace Flow.Launcher.Infrastructure.Http
{ {
Log.Debug($"|Http.Get|Url <{url}>"); Log.Debug($"|Http.Get|Url <{url}>");
using var response = await client.GetAsync(url, token); using var response = await client.GetAsync(url, token);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync(token);
if (response.StatusCode == HttpStatusCode.OK) if (response.StatusCode != HttpStatusCode.OK)
{
return content;
}
else
{ {
throw new HttpRequestException( throw new HttpRequestException(
$"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>"); $"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>");
} }
return content;
} }
/// <summary> /// <summary>
/// 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.
/// </summary>
/// <param name="url">The Uri the request is sent to.</param>
/// <param name="completionOption">An HTTP completion option value that indicates when the operation should be considered completed.</param>
/// <param name="token">A cancellation token that can be used by other objects or threads to receive notice of cancellation</param>
/// <returns></returns>
public static Task<Stream> GetStreamAsync([NotNull] string url,
CancellationToken token = default) => GetStreamAsync(new Uri(url), token);
/// <summary>
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
/// </summary> /// </summary>
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default) public static async Task<Stream> GetStreamAsync([NotNull] Uri url,
CancellationToken token = default)
{ {
Log.Debug($"|Http.Get|Url <{url}>"); Log.Debug($"|Http.Get|Url <{url}>");
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); return await client.GetStreamAsync(url, token);
return await response.Content.ReadAsStreamAsync(); }
public static async Task<HttpResponseMessage> GetResponseAsync(string url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
CancellationToken token = default)
=> await GetResponseAsync(new Uri(url), completionOption, token);
public static async Task<HttpResponseMessage> GetResponseAsync([NotNull] Uri url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
return await client.GetAsync(url, completionOption, token);
} }
/// <summary> /// <summary>
/// Asynchrously send an HTTP request. /// Asynchrously send an HTTP request.
/// </summary> /// </summary>
public static async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token = default) public static async Task<HttpResponseMessage> 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);
} }
} }
} }

View file

@ -25,11 +25,11 @@ namespace Flow.Launcher.Infrastructure.Image
public class ImageCache public class ImageCache
{ {
private const int MaxCached = 50; private const int MaxCached = 50;
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>(); public ConcurrentDictionary<(string, bool), ImageUsage> Data { get; } = new();
private const int permissibleFactor = 2; private const int permissibleFactor = 2;
private SemaphoreSlim semaphore = new(1, 1); private SemaphoreSlim semaphore = new(1, 1);
public void Initialization(Dictionary<string, int> usage) public void Initialization(Dictionary<(string, bool), int> usage)
{ {
foreach (var key in usage.Keys) foreach (var key in usage.Keys)
{ {
@ -37,29 +37,29 @@ namespace Flow.Launcher.Infrastructure.Image
} }
} }
public ImageSource this[string path] public ImageSource this[string path, bool isFullImage = false]
{ {
get get
{ {
if (Data.TryGetValue(path, out var value)) if (!Data.TryGetValue((path, isFullImage), out var value))
{ {
value.usage++; return null;
return value.imageSource;
} }
value.usage++;
return value.imageSource;
return null;
} }
set set
{ {
Data.AddOrUpdate( Data.AddOrUpdate(
path, (path, isFullImage),
new ImageUsage(0, value), new ImageUsage(0, value),
(k, v) => (k, v) =>
{ {
v.imageSource = value; v.imageSource = value;
v.usage++; v.usage++;
return v; return v;
} }
); );
SliceExtra(); SliceExtra();
@ -82,9 +82,9 @@ namespace Flow.Launcher.Infrastructure.Image
} }
} }
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() public int CacheSize()
@ -100,4 +100,4 @@ namespace Flow.Launcher.Infrastructure.Image
return Data.Values.Select(x => x.imageSource).Distinct().Count(); return Data.Values.Select(x => x.imageSource).Distinct().Count();
} }
} }
} }

View file

@ -3,57 +3,58 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.Storage;
using static Flow.Launcher.Infrastructure.Http.Http;
namespace Flow.Launcher.Infrastructure.Image namespace Flow.Launcher.Infrastructure.Image
{ {
public static class ImageLoader public static class ImageLoader
{ {
private static readonly ImageCache ImageCache = new(); private static readonly ImageCache ImageCache = new();
private static BinaryStorage<Dictionary<string, int>> _storage; private static BinaryStorage<Dictionary<(string, bool), int>> _storage;
private static readonly ConcurrentDictionary<string, string> GuidToKey = new(); private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
private static IImageHashGenerator _hashGenerator; private static IImageHashGenerator _hashGenerator;
private static readonly bool EnableImageHash = true; private static readonly bool EnableImageHash = true;
public static ImageSource DefaultImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon)); public static ImageSource DefaultImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
public const int SmallIconSize = 32;
private static readonly string[] ImageExtensions = private static readonly string[] ImageExtensions =
{ {
".png", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"
".jpg",
".jpeg",
".gif",
".bmp",
".tiff",
".ico"
}; };
public static void Initialize() public static void Initialize()
{ {
_storage = new BinaryStorage<Dictionary<string, int>>("Image"); _storage = new BinaryStorage<Dictionary<(string, bool), int>>("Image");
_hashGenerator = new ImageHashGenerator(); _hashGenerator = new ImageHashGenerator();
var usage = LoadStorageToConcurrentDictionary(); 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)); ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze(); 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()}"); Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
}); });
@ -63,17 +64,20 @@ namespace Flow.Launcher.Infrastructure.Image
{ {
lock (_storage) 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<string, int> LoadStorageToConcurrentDictionary() private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary()
{ {
lock (_storage) lock (_storage)
{ {
var loaded = _storage.TryLoad(new Dictionary<string, int>()); var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>());
return new ConcurrentDictionary<string, int>(loaded); return new ConcurrentDictionary<(string, bool), int>(loaded);
} }
} }
@ -99,7 +103,7 @@ namespace Flow.Launcher.Infrastructure.Image
Cache Cache
} }
private static ImageResult LoadInternal(string path, bool loadFullImage = false) private static async ValueTask<ImageResult> LoadInternalAsync(string path, bool loadFullImage = false)
{ {
ImageResult imageResult; ImageResult imageResult;
@ -107,13 +111,21 @@ namespace Flow.Launcher.Infrastructure.Image
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
return new ImageResult(ImageCache[Constant.MissingImgIcon], ImageType.Error); return new ImageResult(DefaultImage, ImageType.Error);
}
if (ImageCache.ContainsKey(path))
{
return new ImageResult(ImageCache[path], ImageType.Cache);
} }
if (ImageCache.ContainsKey(path, loadFullImage))
{
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)) if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{ {
var imageSource = new BitmapImage(new Uri(path)); var imageSource = new BitmapImage(new Uri(path));
@ -121,12 +133,7 @@ namespace Flow.Launcher.Infrastructure.Image
return new ImageResult(imageSource, ImageType.Data); return new ImageResult(imageSource, ImageType.Data);
} }
if (!Path.IsPathRooted(path)) imageResult = await Task.Run(() => GetThumbnailResult(ref path, loadFullImage));
{
path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path));
}
imageResult = GetThumbnailResult(ref path, loadFullImage);
} }
catch (System.Exception e) catch (System.Exception e)
{ {
@ -140,14 +147,35 @@ namespace Flow.Launcher.Infrastructure.Image
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 first try", e);
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2); Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2);
ImageSource image = ImageCache[Constant.MissingImgIcon]; ImageSource image = ImageCache[Constant.MissingImgIcon, false];
ImageCache[path] = image; ImageCache[path, false] = image;
imageResult = new ImageResult(image, ImageType.Error); imageResult = new ImageResult(image, ImageType.Error);
} }
} }
return imageResult; return imageResult;
} }
private static async Task<BitmapImage> 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) private static ImageResult GetThumbnailResult(ref string path, bool loadFullImage = false)
{ {
@ -192,7 +220,7 @@ namespace Flow.Launcher.Infrastructure.Image
} }
else else
{ {
image = ImageCache[Constant.MissingImgIcon]; image = ImageCache[Constant.MissingImgIcon, false];
path = Constant.MissingImgIcon; path = Constant.MissingImgIcon;
} }
@ -213,14 +241,14 @@ namespace Flow.Launcher.Infrastructure.Image
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<ImageSource> LoadAsync(string path, bool loadFullImage = false)
{ {
var imageResult = LoadInternal(path, loadFullImage); var imageResult = await LoadInternalAsync(path, loadFullImage);
var img = imageResult.ImageSource; var img = imageResult.ImageSource;
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache) if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
@ -231,7 +259,7 @@ namespace Flow.Launcher.Infrastructure.Image
if (GuidToKey.TryGetValue(hash, out string key)) if (GuidToKey.TryGetValue(hash, out string key))
{ // image already exists { // image already exists
img = ImageCache[key] ?? img; img = ImageCache[key, false] ?? img;
} }
else else
{ // new guid { // new guid
@ -240,7 +268,7 @@ namespace Flow.Launcher.Infrastructure.Image
} }
// update cache // update cache
ImageCache[path] = img; ImageCache[path, false] = img;
} }
return img; return img;

View file

@ -60,9 +60,14 @@ namespace Flow.Launcher.Plugin
get { return _icoPath; } get { return _icoPath; }
set 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 else
{ {
@ -140,10 +145,11 @@ namespace Flow.Launcher.Plugin
set set
{ {
_pluginDirectory = value; _pluginDirectory = value;
if (!string.IsNullOrEmpty(IcoPath) && !Path.IsPathRooted(IcoPath))
{ // When the Result object is returned from the query call, PluginDirectory is not provided until
IcoPath = Path.Combine(value, IcoPath); // 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;
} }
} }

View file

@ -191,6 +191,7 @@
<system:String x:Key="icons">Icons</system:String> <system:String x:Key="icons">Icons</system:String>
<system:String x:Key="about_activate_times">You have activated Flow Launcher {0} times</system:String> <system:String x:Key="about_activate_times">You have activated Flow Launcher {0} times</system:String>
<system:String x:Key="checkUpdates">Check for Updates</system:String> <system:String x:Key="checkUpdates">Check for Updates</system:String>
<system:String x:Key="BecomeASponsor">Become A Sponsor</system:String>
<system:String x:Key="newVersionTips">New version {0} is available, would you like to restart Flow Launcher to use the update?</system:String> <system:String x:Key="newVersionTips">New version {0} is available, would you like to restart Flow Launcher to use the update?</system:String>
<system:String x:Key="checkUpdatesFailed">Check updates failed, please check your connection and proxy settings to api.github.com.</system:String> <system:String x:Key="checkUpdatesFailed">Check updates failed, please check your connection and proxy settings to api.github.com.</system:String>
<system:String x:Key="downloadUpdatesFailed"> <system:String x:Key="downloadUpdatesFailed">

View file

@ -38,10 +38,16 @@ namespace Flow.Launcher
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(TopProperty)); Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(TopProperty));
fadeOutStoryboard.Children.Add(fadeOutAnimation); fadeOutStoryboard.Children.Add(fadeOutAnimation);
imgClose.Source = ImageLoader.Load(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\close.png")); _ = LoadImageAsync();
imgClose.MouseUp += imgClose_MouseUp; 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) void imgClose_MouseUp(object sender, MouseButtonEventArgs e)
{ {
if (!closing) if (!closing)
@ -56,7 +62,7 @@ namespace Flow.Launcher
Close(); Close();
} }
public void Show(string title, string subTitle, string iconPath) public async void Show(string title, string subTitle, string iconPath)
{ {
tbTitle.Text = title; tbTitle.Text = title;
tbSubTitle.Text = subTitle; tbSubTitle.Text = subTitle;
@ -66,15 +72,15 @@ namespace Flow.Launcher
} }
if (!File.Exists(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 { else {
imgIco.Source = ImageLoader.Load(iconPath); imgIco.Source = await ImageLoader.LoadAsync(iconPath);
} }
Show(); Show();
Dispatcher.InvokeAsync(async () => await Dispatcher.InvokeAsync(async () =>
{ {
if (!closing) if (!closing)
{ {

View file

@ -1,4 +1,4 @@
<Window <Window
x:Class="Flow.Launcher.SettingWindow" x:Class="Flow.Launcher.SettingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -2858,13 +2858,29 @@
Text="{Binding Version}" /> Text="{Binding Version}" />
<TextBlock Style="{DynamicResource SettingSubTitleLabel}" Text="{DynamicResource version}" /> <TextBlock Style="{DynamicResource SettingSubTitleLabel}" Text="{DynamicResource version}" />
</StackPanel> </StackPanel>
<Button <StackPanel Grid.Column="2" Orientation="Horizontal">
Grid.Column="1" <Button
Margin="0,0,-14,0" Margin="0,0,10,0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Click="OnCheckUpdates" Click="OnCheckUpdates"
Content="{DynamicResource checkUpdates}" Content="{DynamicResource checkUpdates}" />
Style="{StaticResource AccentButtonStyle}" /> <Button
Margin="0,0,14,0"
Padding="0"
HorizontalAlignment="Right"
Style="{StaticResource AccentButtonStyle}">
<Hyperlink
NavigateUri="{Binding SponsorPage, Mode=OneWay}"
RequestNavigate="OnRequestNavigate"
TextDecorations="None">
<TextBlock
Padding="10,5,10,5"
Foreground="White"
Text="{DynamicResource BecomeASponsor}" />
</Hyperlink>
</Button>
</StackPanel>
<TextBlock Style="{StaticResource Glyph}"> <TextBlock Style="{StaticResource Glyph}">
&#xe946; &#xe946;
</TextBlock> </TextBlock>

View file

@ -10,7 +10,9 @@ using Flow.Launcher.ViewModel;
using ModernWpf; using ModernWpf;
using ModernWpf.Controls; using ModernWpf.Controls;
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Security.Policy;
using System.Windows; using System.Windows;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Forms; using System.Windows.Forms;

View file

@ -1,4 +1,5 @@
using System.Windows; using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Image;
@ -24,7 +25,23 @@ namespace Flow.Launcher.ViewModel
} }
} }
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 public bool PluginState
{ {
get => !PluginPair.Metadata.Disabled; get => !PluginPair.Metadata.Disabled;
@ -50,6 +67,7 @@ namespace Flow.Launcher.ViewModel
? new Control() ? new Control()
: settingProvider.CreateSettingPanel() : settingProvider.CreateSettingPanel()
: null; : null;
private ImageSource _image = ImageLoader.DefaultImage;
public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed; public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed;
public string InitilizaTime => PluginPair.Metadata.InitTime + "ms"; public string InitilizaTime => PluginPair.Metadata.InitTime + "ms";

View file

@ -15,7 +15,7 @@ namespace Flow.Launcher.ViewModel
public class ResultViewModel : BaseModel public class ResultViewModel : BaseModel
{ {
private static PrivateFontCollection fontCollection = new(); private static PrivateFontCollection fontCollection = new();
private static Dictionary<string, string> fonts = new(); private static Dictionary<string, string> fonts = new();
public ResultViewModel(Result result, Settings settings) public ResultViewModel(Result result, Settings settings)
{ {
@ -145,7 +145,7 @@ namespace Flow.Launcher.ViewModel
public GlyphInfo Glyph { get; set; } public GlyphInfo Glyph { get; set; }
private async ValueTask LoadImageAsync() private async Task LoadImageAsync()
{ {
var imagePath = Result.IcoPath; var imagePath = Result.IcoPath;
if (string.IsNullOrEmpty(imagePath) && Result.Icon != null) if (string.IsNullOrEmpty(imagePath) && Result.Icon != null)
@ -168,12 +168,12 @@ namespace Flow.Launcher.ViewModel
if (ImageLoader.CacheContainImage(imagePath)) if (ImageLoader.CacheContainImage(imagePath))
{ {
// will get here either when icoPath has value\icon delegate is null\when had exception in delegate // 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; return;
} }
// We need to modify the property not field here to trigger the OnPropertyChanged event // 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; } public Result Result { get; }

View file

@ -800,6 +800,7 @@ namespace Flow.Launcher.ViewModel
#region about #region about
public string Website => Constant.Website; public string Website => Constant.Website;
public string SponsorPage => Constant.SponsorPage;
public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest"; public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest";
public string Documentation => Constant.Documentation; public string Documentation => Constant.Documentation;
public string Docs => Constant.Docs; public string Docs => Constant.Docs;

View file

@ -30,7 +30,7 @@ namespace Flow.Launcher.Plugin.WebSearch
Initilize(sources, context, Action.Add); Initilize(sources, context, Action.Add);
} }
private void Initilize(IList<SearchSource> sources, PluginInitContext context, Action action) private async void Initilize(IList<SearchSource> sources, PluginInitContext context, Action action)
{ {
InitializeComponent(); InitializeComponent();
DataContext = _viewModel; DataContext = _viewModel;
@ -42,7 +42,7 @@ namespace Flow.Launcher.Plugin.WebSearch
_viewModel.SetupCustomImagesDirectory(); _viewModel.SetupCustomImagesDirectory();
imgPreviewIcon.Source = _viewModel.LoadPreviewIcon(_searchSource.IconPath); imgPreviewIcon.Source = await _viewModel.LoadPreviewIconAsync(_searchSource.IconPath);
} }
private void OnCancelButtonClick(object sender, RoutedEventArgs e) private void OnCancelButtonClick(object sender, RoutedEventArgs e)
@ -125,7 +125,7 @@ namespace Flow.Launcher.Plugin.WebSearch
} }
} }
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"; const string filter = "Image files (*.jpg, *.jpeg, *.gif, *.png, *.bmp) |*.jpg; *.jpeg; *.gif; *.png; *.bmp";
var dialog = new OpenFileDialog {InitialDirectory = Main.CustomImagesDirectory, Filter = filter}; var dialog = new OpenFileDialog {InitialDirectory = Main.CustomImagesDirectory, Filter = filter};
@ -140,7 +140,7 @@ namespace Flow.Launcher.Plugin.WebSearch
if (_viewModel.ShouldProvideHint(selectedNewIconImageFullPath)) if (_viewModel.ShouldProvideHint(selectedNewIconImageFullPath))
MessageBox.Show(_api.GetTranslation("flowlauncher_plugin_websearch_iconpath_hint")); MessageBox.Show(_api.GetTranslation("flowlauncher_plugin_websearch_iconpath_hint"));
imgPreviewIcon.Source = _viewModel.LoadPreviewIcon(selectedNewIconImageFullPath); imgPreviewIcon.Source = await _viewModel.LoadPreviewIconAsync(selectedNewIconImageFullPath);
} }
} }
} }
@ -151,4 +151,4 @@ namespace Flow.Launcher.Plugin.WebSearch
Add, Add,
Edit Edit
} }
} }

View file

@ -2,6 +2,7 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.IO; using System.IO;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
@ -57,9 +58,9 @@ namespace Flow.Launcher.Plugin.WebSearch
return Directory.GetParent(fullPathToSelectedImage).ToString() == Main.DefaultImagesDirectory; return Directory.GetParent(fullPathToSelectedImage).ToString() == Main.DefaultImagesDirectory;
} }
internal ImageSource LoadPreviewIcon(string pathToPreviewIconImage) internal async ValueTask<ImageSource> LoadPreviewIconAsync(string pathToPreviewIconImage)
{ {
return ImageLoader.Load(pathToPreviewIconImage); return await ImageLoader.LoadAsync(pathToPreviewIconImage);
} }
} }
} }

View file

@ -21,8 +21,8 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
try try
{ {
const string api = "https://api.bing.com/qsonhs.aspx?q="; 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)); using var json = (await JsonDocument.ParseAsync(resultStream, cancellationToken: token));
var root = json.RootElement.GetProperty("AS"); var root = json.RootElement.GetProperty("AS");
@ -31,16 +31,16 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
return new List<string>(); return new List<string>();
return root.GetProperty("Results") return root.GetProperty("Results")
.EnumerateArray() .EnumerateArray()
.SelectMany(r => r.GetProperty("Suggests") .SelectMany(r => r.GetProperty("Suggests")
.EnumerateArray() .EnumerateArray()
.Select(s => s.GetProperty("Txt").GetString())) .Select(s => s.GetProperty("Txt").GetString()))
.ToList(); .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); Log.Exception("|Baidu.Suggestions|Can't get suggestion from baidu", e);
return null; return null;
@ -49,7 +49,7 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{ {
Log.Exception("|Bing.Suggestions|can't parse suggestions", e); Log.Exception("|Bing.Suggestions|can't parse suggestions", e);
return new List<string>(); return new List<string>();
} }
} }
public override string ToString() public override string ToString()

View file

@ -20,13 +20,10 @@ namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources
{ {
const string api = "https://www.google.com/complete/search?output=chrome&q="; 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); using var json = await JsonDocument.ParseAsync(resultStream, cancellationToken: token);
if (json == null)
return new List<string>();
var results = json.RootElement.EnumerateArray().ElementAt(1); var results = json.RootElement.EnumerateArray().ElementAt(1);
return results.EnumerateArray().Select(o => o.GetString()).ToList(); return results.EnumerateArray().Select(o => o.GetString()).ToList();