diff --git a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs index d3ee4695cc2..68be746f28e 100644 --- a/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs +++ b/Flow.Launcher.Core/ExternalPlugins/CommunityPluginSource.cs @@ -5,6 +5,8 @@ using System.Net; using System.Net.Http; using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -16,6 +18,11 @@ public record CommunityPluginSource(string ManifestFileUrl) private List plugins = new(); + private static JsonSerializerOptions PluginStoreItemSerializationOption = new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + }; + /// /// Fetch and deserialize the contents of a plugins.json file found at . /// We use conditional http requests to keep repeat requests fast. @@ -32,12 +39,15 @@ public async Task> FetchAsync(CancellationToken token) request.Headers.Add("If-None-Match", latestEtag); - using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false); + using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token) + .ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.OK) { - this.plugins = await response.Content.ReadFromJsonAsync>(cancellationToken: token).ConfigureAwait(false); - this.latestEtag = response.Headers.ETag.Tag; + this.plugins = await response.Content + .ReadFromJsonAsync>(PluginStoreItemSerializationOption, cancellationToken: token) + .ConfigureAwait(false); + this.latestEtag = response.Headers.ETag?.Tag; Log.Info(nameof(CommunityPluginSource), $"Loaded {this.plugins.Count} plugins from {ManifestFileUrl}"); return this.plugins; @@ -49,7 +59,8 @@ public async Task> FetchAsync(CancellationToken token) } else { - Log.Warn(nameof(CommunityPluginSource), $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}"); + Log.Warn(nameof(CommunityPluginSource), + $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}"); throw new Exception($"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}"); } } diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs index bb1279b2c61..64c4cd627d2 100644 --- a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs +++ b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs @@ -14,8 +14,8 @@ public record UserPlugin public string UrlDownload { get; set; } public string UrlSourceCode { get; set; } public string IcoPath { get; set; } - public DateTime LatestReleaseDate { get; set; } - public DateTime DateAdded { get; set; } + public DateTime? LatestReleaseDate { get; set; } + public DateTime? DateAdded { get; set; } } } diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 5cd09d407f1..68e7c115cc6 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -55,6 +55,7 @@ + diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index 3848af6a4a6..408d9785d60 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -37,7 +37,7 @@ public async Task InitializeAsync() _storage = new JsonStorage>(SettingPath); Settings = await _storage.LoadAsync(); - if (Settings != null || Configuration == null) + if (Configuration == null) { return; } diff --git a/Flow.Launcher.Core/Plugin/NodePlugin.cs b/Flow.Launcher.Core/Plugin/NodePlugin.cs index 40eb057cbc7..aeac18ca955 100644 --- a/Flow.Launcher.Core/Plugin/NodePlugin.cs +++ b/Flow.Launcher.Core/Plugin/NodePlugin.cs @@ -28,20 +28,21 @@ public NodePlugin(string filename) protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - _startInfo.ArgumentList[1] = JsonSerializer.Serialize(request); + _startInfo.ArgumentList[1] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[1] = JsonSerializer.Serialize(rpcRequest); + _startInfo.ArgumentList[1] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); return Execute(_startInfo); } public override async Task InitAsync(PluginInitContext context) { _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + _startInfo.ArgumentList.Add(string.Empty); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; await base.InitAsync(context); } diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs index a476f06e9bf..16f9dfbf98f 100644 --- a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -5,12 +5,24 @@ using System.Threading.Tasks; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; +using Meziantou.Framework.Win32; using Nerdbank.Streams; namespace Flow.Launcher.Core.Plugin { internal abstract class ProcessStreamPluginV2 : JsonRPCPluginV2 { + private static JobObject _jobObject = new JobObject(); + + static ProcessStreamPluginV2() + { + _jobObject.SetLimits(new JobObjectLimits() + { + Flags = JobObjectLimitFlags.KillOnJobClose | JobObjectLimitFlags.DieOnUnhandledException + }); + + _jobObject.AssignProcess(Process.GetCurrentProcess()); + } public override string SupportedLanguage { get; set; } protected sealed override IDuplexPipe ClientPipe { get; set; } @@ -30,22 +42,22 @@ public override async Task InitAsync(PluginInitContext context) ClientProcess = Process.Start(StartInfo); ArgumentNullException.ThrowIfNull(ClientProcess); - + SetupPipe(ClientProcess); ErrorStream = ClientProcess.StandardError; await base.InitAsync(context); } - + private void SetupPipe(Process process) { var (reader, writer) = (PipeReader.Create(process.StandardOutput.BaseStream), PipeWriter.Create(process.StandardInput.BaseStream)); ClientPipe = new DuplexPipe(reader, writer); } - - + + public override async Task ReloadDataAsync() { var oldProcess = ClientProcess; @@ -57,8 +69,8 @@ public override async Task ReloadDataAsync() await oldProcess.WaitForExitAsync(); oldProcess.Dispose(); } - - + + public override async ValueTask DisposeAsync() { ClientProcess.Kill(true); diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Sys/Languages/de.xaml index c929a1b16db..04238424211 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Languages/de.xaml +++ b/Plugins/Flow.Launcher.Plugin.Sys/Languages/de.xaml @@ -5,57 +5,57 @@ Befehl Beschreibung - Shutdown - Restart - Restart With Advanced Boot Options - Log Off/Sign Out - Lock - Sleep - Hibernate - Index Option - Empty Recycle Bin - Open Recycle Bin + Herunterfahren + Neu starten + Neustart mit erweiterten Startoptionen + Abmelden + Sperren + Energie sparen + Ruhezustand + Indizierungsoptionen + Papierkorb leeren + Papierkorb öffnen Schließen - Save Settings - Restart Flow Launcher" + Einstellungen speichern + Flow Launcher neu starten Einstellungen Plugin-Daten neu laden - Check For Update - Open Log Location - Flow Launcher Tips - Flow Launcher UserData Folder + Auf Updates prüfen + Log-Speicherort öffnen + Flow Launcher Tipps + Flow Launcher Benutzerdaten-Ordner Gott Modus Computer herunterfahren Computer neu starten - Restart the computer with Advanced Boot Options for Safe and Debugging modes, as well as other options + Startet den Computer mit erweiterten Startoptionen für den abgesicherten Modus neu Abmelden Computer sperren Flow Launcher schließen Flow Launcher neu starten - Tweak Flow Launcher's settings + Einstellungen des Flow Launchers anpassen Computer in Schlafmodus versetzen Papierkorb leeren - Open recycle bin - Indexierungsoptionen - Hibernate computer - Save all Flow Launcher settings - Refreshes plugin data with new content - Open Flow Launcher's log location - Check for new Flow Launcher update - Visit Flow Launcher's documentation for more help and how to use tips - Open the location where Flow Launcher's settings are stored + Papierkorb öffnen + Indizierungsoptionen + Computer in den Ruhezustand versetzen + Alle Flow Launcher Einstellungen speichern + Aktualisiert Plugin-Daten mit neuen Inhalten + Öffnet den Speicherort der Flow Launcher Logs + Prüft auf neue Flow Launcher Updates + Öffnet die Dokumentation von Flow Launcher für Hilfe und Tipps + Öffnet den Ort an dem Flow Laucher Einstellungen speichert Gott Modus Erfolgreich - All Flow Launcher settings saved - Reloaded all applicable plugin data - Are you sure you want to shut the computer down? - Are you sure you want to restart the computer? - Are you sure you want to restart the computer with Advanced Boot Options? - Are you sure you want to log off? + Alle Flow Launcher Einstellungen wurden gespeichert + Alle betroffenen Plugin-Daten wurden neu geladen + Soll der Computer wirklich heruntergefahren werden? + Soll der Computer wirklich neu gestartet werden? + Soll der Computer wirklich mit erweiterten Startoptionen neu gestartet werden? + Soll der aktuelle Benutzer wirklich abgemeldet werden? Systembefehle Stellt Systemrelevante Befehle bereit. z.B. herunterfahren, sperren, Einstellungen, usw. diff --git a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json index 0da3aaca744..5276690e30d 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json @@ -4,7 +4,7 @@ "Name": "System Commands", "Description": "Provide System related commands. e.g. shutdown,lock, setting etc.", "Author": "qianlifeng", - "Version": "3.1.0", + "Version": "3.1.1", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Sys.dll", diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs index 6cf1446afbd..f0e5ac32744 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs @@ -197,7 +197,8 @@ public Settings() public SuggestionSource[] Suggestions { get; set; } = { new Google(), new Baidu(), - new Bing() + new Bing(), + new DuckDuckGo() }; [JsonIgnore] diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/DuckDuckGo.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/DuckDuckGo.cs new file mode 100644 index 00000000000..8fafb44cce1 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SuggestionSources/DuckDuckGo.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Flow.Launcher.Infrastructure.Http; +using Flow.Launcher.Infrastructure.Logger; +using System.Net.Http; +using System.Threading; +using System.Text.Json; + +namespace Flow.Launcher.Plugin.WebSearch.SuggestionSources +{ + public class DuckDuckGo : SuggestionSource + { + public override async Task> SuggestionsAsync(string query, CancellationToken token) + { + // When the search query is empty, DuckDuckGo returns `[]`. When it's not empty, it returns data + // in the following format: `["query", ["suggestion1", "suggestion2", ...]]`. + if (string.IsNullOrEmpty(query)) + { + return new List(); + } + + try + { + const string api = "https://duckduckgo.com/ac/?type=list&q="; + + await using var resultStream = await Http.GetStreamAsync(api + Uri.EscapeDataString(query), token: token).ConfigureAwait(false); + + using var json = await JsonDocument.ParseAsync(resultStream, cancellationToken: token); + + var results = json.RootElement.EnumerateArray().ElementAt(1); + + return results.EnumerateArray().Select(o => o.GetString()).ToList(); + + } + catch (Exception e) when (e is HttpRequestException or {InnerException: TimeoutException}) + { + Log.Exception("|DuckDuckGo.Suggestions|Can't get suggestion from DuckDuckGo", e); + return null; + } + catch (JsonException e) + { + Log.Exception("|DuckDuckGo.Suggestions|can't parse suggestions", e); + return new List(); + } + } + + public override string ToString() + { + return "DuckDuckGo"; + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json index 0efa9c10f0b..12e0566f9ed 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json @@ -26,7 +26,7 @@ "Name": "Web Searches", "Description": "Provide the web search ability", "Author": "qianlifeng", - "Version": "3.0.6", + "Version": "3.0.7", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll", diff --git a/appveyor.yml b/appveyor.yml index 602efba0efe..b84230c434c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: '1.17.1.{build}' +version: '1.17.2.{build}' init: - ps: |