diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index b1c09024f..bb7ec6817 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -73,8 +73,7 @@ namespace Flow.Launcher.Infrastructure.Image public bool ContainsKey(string key) { - var contains = Data.ContainsKey(key) && Data[key] != null; - return contains; + return Data.ContainsKey(key) && Data[key].imageSource != null; } public int CacheSize() diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 91eeb183d..94132b27f 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -50,14 +50,18 @@ namespace Flow.Launcher.Infrastructure.Logger return valid; } - + [MethodImpl(MethodImplOptions.Synchronized)] public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") { +#if DEBUG + throw exception; +#else var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName); ExceptionInternal(classNameWithMethod, message, exception); +#endif } private static string CheckClassAndMessageAndReturnFullClassWithMethod(string className, string message, diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index b7b52ac35..698802ba3 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -14,10 +14,10 @@ - 1.3.1 - 1.3.1 - 1.3.1 - 1.3.1 + 1.4.0 + 1.4.0 + 1.4.0 + 1.4.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -64,4 +64,4 @@ - \ No newline at end of file + diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index 072196605..2f9d06d81 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -9,7 +9,7 @@ d:DataContext="{d:DesignInstance vm:ResultsViewModel}" MaxHeight="{Binding MaxHeight}" SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" - SelectedItem="{Binding SelectedItem, Mode=OneWayToSource}" + SelectedItem="{Binding SelectedItem, Mode=TwoWay}" HorizontalContentAlignment="Stretch" ItemsSource="{Binding Results}" Margin="{Binding Margin}" Visibility="{Binding Visbility}" diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a062f59dc..8195c745a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -19,9 +17,8 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Storage; -using System.Windows.Media; -using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; +using System.Threading.Tasks.Dataflow; namespace Flow.Launcher.ViewModel { @@ -48,6 +45,8 @@ namespace Flow.Launcher.ViewModel private bool _saved; private readonly Internationalization _translator = InternationalizationManager.Instance; + private BufferBlock _resultsUpdateQueue; + private Task _resultsViewUpdateTask; #endregion @@ -75,8 +74,11 @@ namespace Flow.Launcher.ViewModel _selectedResults = Results; InitializeKeyCommands(); + + RegisterViewUpdate(); RegisterResultsUpdatedEvent(); + SetHotkey(_settings.Hotkey, OnHotkey); SetCustomPluginHotkey(); SetOpenResultModifiers(); @@ -89,15 +91,51 @@ namespace Flow.Launcher.ViewModel var plugin = (IResultUpdated) pair.Plugin; plugin.ResultsUpdated += (s, e) => { - Task.Run(() => - { - PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); - UpdateResultView(e.Results, pair.Metadata, e.Query); - }, _updateToken); + PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); + if (e.Query.Search == _lastQuery.Search) + _resultsUpdateQueue.Post(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, _updateToken)); }; } } + private void RegisterViewUpdate() + { + _resultsUpdateQueue = new BufferBlock(); + _resultsViewUpdateTask = + Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); + + + async Task updateAction() + { + var queue = new Dictionary(); + while (await _resultsUpdateQueue.OutputAvailableAsync()) + { + queue.Clear(); + await Task.Delay(20); + while (_resultsUpdateQueue.TryReceive(out var item)) + { + if (!item.Token.IsCancellationRequested) + queue[item.ID] = item; + } + + UpdateResultView(queue.Values); + } + } + + ; + + void continueAction(Task t) + { +#if DEBUG + throw t.Exception; +#else + Log.Error($"Error happen in task dealing with viewupdate for results. {t.Exception}"); + _resultsViewUpdateTask = + Task.Run(updateAction).ContinueWith(continueAction, TaskContinuationOptions.OnlyOnFaulted); +#endif + } + } + private void InitializeKeyCommands() { @@ -195,12 +233,13 @@ namespace Flow.Launcher.ViewModel public ResultsViewModel Results { get; private set; } public ResultsViewModel ContextMenu { get; private set; } public ResultsViewModel History { get; private set; } + private string _lastQueryText; private string _queryText; public string QueryText { - get { return _queryText; } + get => _queryText; set { _queryText = value; @@ -315,9 +354,20 @@ namespace Flow.Launcher.ViewModel { var filtered = results.Where ( - r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() - || StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() - ).ToList(); + r => + { + var match = StringMatcher.FuzzySearch(query, r.Title); + if (!match.IsSearchPrecisionScoreMet()) + { + match = StringMatcher.FuzzySearch(query, r.SubTitle); + } + + if (!match.IsSearchPrecisionScoreMet()) return false; + + r.Score = match.Score; + return true; + + }).ToList(); ContextMenu.AddResults(filtered, id); } else @@ -371,112 +421,128 @@ namespace Flow.Launcher.ViewModel private void QueryResults() { - if (!string.IsNullOrEmpty(QueryText)) + _updateSource?.Cancel(); + + if (string.IsNullOrWhiteSpace(QueryText)) { - _updateSource?.Cancel(); - var currentUpdateSource = new CancellationTokenSource(); - _updateSource = currentUpdateSource; - var currentCancellationToken = _updateSource.Token; - _updateToken = currentCancellationToken; + Results.Clear(); + Results.Visbility = Visibility.Collapsed; + return; + } - ProgressBarVisibility = Visibility.Hidden; - _isQueryRunning = true; - var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); - if (query != null) + _updateSource?.Dispose(); + + var currentUpdateSource = new CancellationTokenSource(); + _updateSource = currentUpdateSource; + var currentCancellationToken = _updateSource.Token; + _updateToken = currentCancellationToken; + + ProgressBarVisibility = Visibility.Hidden; + _isQueryRunning = true; + + var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins); + + // handle the exclusiveness of plugin using action keyword + RemoveOldQueryResults(query); + + _lastQuery = query; + + var plugins = PluginManager.ValidPluginsForQuery(query); + + Task.Run(async () => { - // handle the exclusiveness of plugin using action keyword - RemoveOldQueryResults(query); + if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign) + { + // Wait 45 millisecond for query change in global query + // if query changes, return so that it won't be calculated + await Task.Delay(45, currentCancellationToken); + if (currentCancellationToken.IsCancellationRequested) + return; + } - _lastQuery = query; - Task.Delay(200, currentCancellationToken).ContinueWith(_ => + _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet - if (currentUpdateSource == _updateSource && _isQueryRunning) + if (!currentCancellationToken.IsCancellationRequested && _isQueryRunning) { ProgressBarVisibility = Visibility.Visible; } }, currentCancellationToken); - var plugins = PluginManager.ValidPluginsForQuery(query); - Task.Run(async () => + Task[] tasks = new Task[plugins.Count]; + try { - // so looping will stop once it was cancelled - - Task[] tasks = new Task[plugins.Count]; - try + for (var i = 0; i < plugins.Count; i++) { - for (var i = 0; i < plugins.Count; i++) + if (!plugins[i].Metadata.Disabled) { - if (!plugins[i].Metadata.Disabled) - { - tasks[i] = QueryTask(plugins[i], query, currentCancellationToken); - } - else - { - tasks[i] = Task.CompletedTask; // Avoid Null - } + tasks[i] = QueryTask(plugins[i]); + } + else + { + tasks[i] = Task.CompletedTask; // Avoid Null } - - // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first - await Task.WhenAll(tasks); - } - catch (OperationCanceledException) - { - // nothing to do here } - // this should happen once after all queries are done so progress bar should continue - // until the end of all querying - _isQueryRunning = false; + // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first + await Task.WhenAll(tasks); + } + catch (OperationCanceledException) + { + // nothing to do here + } + + if (currentCancellationToken.IsCancellationRequested) + return; + + // this should happen once after all queries are done so progress bar should continue + // until the end of all querying + _isQueryRunning = false; + if (!currentCancellationToken.IsCancellationRequested) + { + // update to hidden if this is still the current query + ProgressBarVisibility = Visibility.Hidden; + } + + // Local function + async Task QueryTask(PluginPair plugin) + { + // Since it is wrapped within a Task.Run, the synchronous context is null + // Task.Yield will force it to run in ThreadPool + await Task.Yield(); + + var results = await PluginManager.QueryForPlugin(plugin, query, currentCancellationToken); if (!currentCancellationToken.IsCancellationRequested) - { - // update to hidden if this is still the current query - ProgressBarVisibility = Visibility.Hidden; - } - - // Local function - async Task QueryTask(PluginPair plugin, Query query, CancellationToken token) - { - // Since it is wrapped within a Task.Run, the synchronous context is null - // Task.Yield will force it to run in ThreadPool - await Task.Yield(); - - var results = await PluginManager.QueryForPlugin(plugin, query, token); - if (!currentCancellationToken.IsCancellationRequested) - UpdateResultView(results, plugin.Metadata, query); - } - }, currentCancellationToken).ContinueWith( - t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), - TaskContinuationOptions.OnlyOnFaulted); - } - } - else - { - Results.Clear(); - Results.Visbility = Visibility.Collapsed; - } + _resultsUpdateQueue.Post(new ResultsForUpdate(results, plugin.Metadata, query, + currentCancellationToken)); + } + }, currentCancellationToken) + .ContinueWith(t => Log.Exception("|MainViewModel|Plugins Query Exceptions", t.Exception), + TaskContinuationOptions.OnlyOnFaulted); } + private void RemoveOldQueryResults(Query query) { string lastKeyword = _lastQuery.ActionKeyword; + string keyword = query.ActionKeyword; if (string.IsNullOrEmpty(lastKeyword)) { if (!string.IsNullOrEmpty(keyword)) { - Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); + Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata); } } else { if (string.IsNullOrEmpty(keyword)) { - Results.RemoveResultsFor(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); + Results.KeepResultsExcept(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); } else if (lastKeyword != keyword) { - Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); + Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata); } } } @@ -554,7 +620,6 @@ namespace Flow.Launcher.ViewModel return selected; } - private bool HistorySelected() { var selected = SelectedResults == History; @@ -683,30 +748,47 @@ namespace Flow.Launcher.ViewModel /// /// To avoid deadlock, this method should not called from main thread /// - public void UpdateResultView(List list, PluginMetadata metadata, Query originQuery) + public void UpdateResultView(IEnumerable resultsForUpdates) { - foreach (var result in list) + if (!resultsForUpdates.Any()) + return; + CancellationToken token; + + try { - if (_topMostRecord.IsTopMost(result)) + // Don't know why sometimes even resultsForUpdates is empty, the method won't return; + token = resultsForUpdates.Select(r => r.Token).Distinct().SingleOrDefault(); + } +#if DEBUG + catch + { + throw new ArgumentException("Unacceptable token"); + } +#else + catch + { + token = default; + } +#endif + + + foreach (var metaResults in resultsForUpdates) + { + foreach (var result in metaResults.Results) { - result.Score = int.MaxValue; - } - else - { - var priorityScore = metadata.Priority * 150; - result.Score += _userSelectedRecord.GetSelectedCount(result) * 5 + priorityScore; + if (_topMostRecord.IsTopMost(result)) + { + result.Score = int.MaxValue; + } + else + { + var priorityScore = metaResults.Metadata.Priority * 150; + result.Score += _userSelectedRecord.GetSelectedCount(result) * 5 + priorityScore; + } } } - if (originQuery.RawQuery == _lastQuery.RawQuery) - { - Results.AddResults(list, metadata.ID); - } - - if (Results.Visbility != Visibility.Visible && list.Count > 0) - { - Results.Visbility = Visibility.Visible; - } + Results.AddResults(resultsForUpdates, token); } #endregion diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 4c65f2b9f..c91bbb107 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -1,9 +1,7 @@ using System; -using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; -using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -106,14 +104,10 @@ namespace Flow.Launcher.ViewModel } if (ImageLoader.CacheContainImage(imagePath)) - { // will get here either when icoPath has value\icon delegate is null\when had exception in delegate return ImageLoader.Load(imagePath); - } - else - { - return await Task.Run(() => ImageLoader.Load(imagePath)); - } + + return await Task.Run(() => ImageLoader.Load(imagePath)); } public Result Result { get; } diff --git a/Flow.Launcher/ViewModel/ResultsForUpdate.cs b/Flow.Launcher/ViewModel/ResultsForUpdate.cs new file mode 100644 index 000000000..be48f53c1 --- /dev/null +++ b/Flow.Launcher/ViewModel/ResultsForUpdate.cs @@ -0,0 +1,35 @@ +using Flow.Launcher.Plugin; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace Flow.Launcher.ViewModel +{ + public class ResultsForUpdate + { + public List Results { get; } + + public PluginMetadata Metadata { get; } + public string ID { get; } + + public Query Query { get; } + public CancellationToken Token { get; } + + public ResultsForUpdate(List results, string resultID, CancellationToken token) + { + Results = results; + ID = resultID; + Token = token; + } + + public ResultsForUpdate(List results, PluginMetadata metadata, Query query, CancellationToken token) + { + Results = results; + Metadata = metadata; + Query = query; + Token = token; + ID = metadata.ID; + } + } +} diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index d30854180..1b8dd602d 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; @@ -17,7 +20,6 @@ namespace Flow.Launcher.ViewModel public ResultCollection Results { get; } - private readonly object _addResultsLock = new object(); private readonly object _collectionLock = new object(); private readonly Settings _settings; private int MaxResults => _settings?.MaxResultsToShow ?? 6; @@ -116,17 +118,20 @@ namespace Flow.Launcher.ViewModel public void Clear() { - Results.Clear(); + lock (_collectionLock) + Results.RemoveAll(); } - public void RemoveResultsExcept(PluginMetadata metadata) + public void KeepResultsFor(PluginMetadata metadata) { - Results.RemoveAll(r => r.Result.PluginID != metadata.ID); + lock (_collectionLock) + Results.Update(Results.Where(r => r.Result.PluginID == metadata.ID).ToList()); } - public void RemoveResultsFor(PluginMetadata metadata) + public void KeepResultsExcept(PluginMetadata metadata) { - Results.RemoveAll(r => r.Result.PluginID == metadata.ID); + lock (_collectionLock) + Results.Update(Results.Where(r => r.Result.PluginID != metadata.ID).ToList()); } /// @@ -134,70 +139,99 @@ namespace Flow.Launcher.ViewModel /// public void AddResults(List newRawResults, string resultId) { - lock (_addResultsLock) + lock (_collectionLock) { var newResults = NewResults(newRawResults, resultId); - // update UI in one run, so it can avoid UI flickering - Results.Update(newResults); - - if (Results.Count > 0) + // https://social.msdn.microsoft.com/Forums/vstudio/en-US/5ff71969-f183-4744-909d-50f7cd414954/binding-a-tabcontrols-selectedindex-not-working?forum=wpf + // fix selected index flow + var updateTask = Task.Run(() => { + // update UI in one run, so it can avoid UI flickering + + Results.Update(newResults); + if (Results.Any()) + SelectedItem = Results[0]; + }); + if (!updateTask.Wait(300)) + { + updateTask.Dispose(); + throw new TimeoutException("Update result use too much time."); + } + + } + + if (Visbility != Visibility.Visible && Results.Count > 0) + { + Margin = new Thickness { Top = 8 }; + SelectedIndex = 0; + Visbility = Visibility.Visible; + } + else + { + Margin = new Thickness { Top = 0 }; + Visbility = Visibility.Collapsed; + } + } + /// + /// To avoid deadlock, this method should not called from main thread + /// + public void AddResults(IEnumerable resultsForUpdates, CancellationToken token) + { + var newResults = NewResults(resultsForUpdates); + if (token.IsCancellationRequested) + return; + lock (_collectionLock) + { + // update UI in one run, so it can avoid UI flickering + + Results.Update(newResults, token); + if (Results.Any()) + SelectedItem = Results[0]; + } + + switch (Visbility) + { + case Visibility.Collapsed when Results.Count > 0: Margin = new Thickness { Top = 8 }; SelectedIndex = 0; - } - else - { + Visbility = Visibility.Visible; + break; + case Visibility.Visible when Results.Count == 0: Margin = new Thickness { Top = 0 }; - } + Visbility = Visibility.Collapsed; + break; } + } private List NewResults(List newRawResults, string resultId) { - var results = Results.ToList(); + if (newRawResults.Count == 0) + return Results.ToList(); + + var results = Results as IEnumerable; + var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)).ToList(); - var oldResults = results.Where(r => r.Result.PluginID == resultId).ToList(); - // Find the same results in A (old results) and B (new newResults) - var sameResults = oldResults - .Where(t1 => newResults.Any(x => x.Result.Equals(t1.Result))) - .ToList(); + return results.Where(r => r.Result.PluginID != resultId) + .Concat(results.Intersect(newResults).Union(newResults)) + .OrderByDescending(r => r.Result.Score) + .ToList(); + } - // remove result of relative complement of B in A - foreach (var result in oldResults.Except(sameResults)) - { - results.Remove(result); - } + private List NewResults(IEnumerable resultsForUpdates) + { + if (!resultsForUpdates.Any()) + return Results.ToList(); - // update result with B's score and index position - foreach (var sameResult in sameResults) - { - int oldIndex = results.IndexOf(sameResult); - int oldScore = results[oldIndex].Result.Score; - var newResult = newResults[newResults.IndexOf(sameResult)]; - int newScore = newResult.Result.Score; - if (newScore != oldScore) - { - var oldResult = results[oldIndex]; + var results = Results as IEnumerable; - oldResult.Result.Score = newScore; - oldResult.Result.OriginQuery = newResult.Result.OriginQuery; - - results.RemoveAt(oldIndex); - int newIndex = InsertIndexOf(newScore, results); - results.Insert(newIndex, oldResult); - } - } - - // insert result in relative complement of A in B - foreach (var result in newResults.Except(sameResults)) - { - int newIndex = InsertIndexOf(result.Result.Score, results); - results.Insert(newIndex, result); - } - - return results; + return results.Where(r => r != null && !resultsForUpdates.Any(u => u.Metadata.ID == r.Result.PluginID)) + .Concat( + resultsForUpdates.SelectMany(u => u.Results, (u, r) => new ResultViewModel(r, _settings))) + .OrderByDescending(rv => rv.Result.Score) + .ToList(); } #endregion @@ -234,58 +268,71 @@ namespace Flow.Launcher.ViewModel public class ResultCollection : ObservableCollection { + private long editTime = 0; - public void RemoveAll(Predicate predicate) + private bool _suppressNotifying = false; + + private CancellationToken _token; + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { - CheckReentrancy(); - - for (int i = Count - 1; i >= 0; i--) + if (!_suppressNotifying) { - if (predicate(this[i])) - { - RemoveAt(i); - } + base.OnCollectionChanged(e); } } + public void BulkAddRange(IEnumerable resultViews) + { + // suppress notifying before adding all element + _suppressNotifying = true; + foreach (var item in resultViews) + { + Add(item); + } + _suppressNotifying = false; + // manually update event + // wpf use directx / double buffered already, so just reset all won't cause ui flickering + if (_token.IsCancellationRequested) + return; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + public void AddRange(IEnumerable Items) + { + foreach (var item in Items) + { + if (_token.IsCancellationRequested) + return; + Add(item); + } + } + public void RemoveAll() + { + ClearItems(); + } + /// /// Update the results collection with new results, try to keep identical results /// /// - public void Update(List newItems) + public void Update(List newItems, CancellationToken token = default) { - int newCount = newItems.Count; - int oldCount = Items.Count; - int location = newCount > oldCount ? oldCount : newCount; + _token = token; + if (Count == 0 && newItems.Count == 0 || _token.IsCancellationRequested) + return; - for (int i = 0; i < location; i++) + if (editTime < 10 || newItems.Count < 30) { - ResultViewModel oldResult = this[i]; - ResultViewModel newResult = newItems[i]; - if (!oldResult.Equals(newResult)) - { // result is not the same update it in the current index - this[i] = newResult; - } - else if (oldResult.Result.Score != newResult.Result.Score) - { - this[i].Result.Score = newResult.Result.Score; - } - } - - - if (newCount >= oldCount) - { - for (int i = oldCount; i < newCount; i++) - { - Add(newItems[i]); - } + if (Count != 0) ClearItems(); + AddRange(newItems); + editTime++; + return; } else { - for (int i = oldCount - 1; i >= newCount; i--) - { - RemoveAt(i); - } + Clear(); + BulkAddRange(newItems); + editTime++; } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json index aa44c4413..68ab9f562 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json @@ -7,7 +7,7 @@ "Name": "Explorer", "Description": "Search and manage files and folders. Explorer utilises Windows Index Search", "Author": "Jeremy Wu", - "Version": "1.2.6", + "Version": "1.3.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll", diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs index 40579e6e5..1d473a82a 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs @@ -37,8 +37,17 @@ namespace Flow.Launcher.Plugin.PluginsManager Settings = viewModel.Settings; contextMenu = new ContextMenu(Context); pluginManager = new PluginsManager(Context, Settings); - await pluginManager.UpdateManifest(); - lastUpdateTime = DateTime.Now; + var updateManifestTask = pluginManager.UpdateManifest(); + if (await Task.WhenAny(updateManifestTask, Task.Delay(500)) == updateManifestTask) + { + lastUpdateTime = DateTime.Now; + } + else + { + context.API.ShowMsg("Plugin Manifest Download Fail.", + @"Please check internet transmission with Github.com. + You may not be able to Install and Update Plugin.", pluginManager.icoPath); + } } public List LoadContextMenus(Result selectedResult) @@ -61,7 +70,7 @@ namespace Flow.Launcher.Plugin.PluginsManager return search switch { - var s when s.StartsWith(Settings.HotKeyInstall) => pluginManager.RequestInstallOrUpdate(s), + var s when s.StartsWith(Settings.HotKeyInstall) => await pluginManager.RequestInstallOrUpdate(s, token), var s when s.StartsWith(Settings.HotkeyUninstall) => pluginManager.RequestUninstall(s), var s when s.StartsWith(Settings.HotkeyUpdate) => pluginManager.RequestUpdate(s), _ => pluginManager.GetDefaultHotKeys().Where(hotkey => @@ -93,4 +102,4 @@ namespace Flow.Launcher.Plugin.PluginsManager lastUpdateTime = DateTime.Now; } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 68df5bc1f..0f5e6d9e8 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -36,7 +37,7 @@ namespace Flow.Launcher.Plugin.PluginsManager } } - private readonly string icoPath = "Images\\pluginsmanager.png"; + internal readonly string icoPath = "Images\\pluginsmanager.png"; internal PluginsManager(PluginInitContext context, Settings settings) { @@ -64,27 +65,27 @@ namespace Flow.Launcher.Plugin.PluginsManager return false; } }, - new Result() + new Result() + { + Title = Settings.HotkeyUninstall, + IcoPath = icoPath, + Action = _ => { - Title = Settings.HotkeyUninstall, - IcoPath = icoPath, - Action = _ => - { - Context.API.ChangeQuery("pm uninstall "); - return false; - } - }, - new Result() - { - Title = Settings.HotkeyUpdate, - IcoPath = icoPath, - Action = _ => - { - Context.API.ChangeQuery("pm update "); - return false; - } + Context.API.ChangeQuery("pm uninstall "); + return false; } - }; + }, + new Result() + { + Title = Settings.HotkeyUpdate, + IcoPath = icoPath, + Action = _ => + { + Context.API.ChangeQuery("pm update "); + return false; + } + } + }; } internal async Task InstallOrUpdate(UserPlugin plugin) @@ -137,7 +138,8 @@ namespace Flow.Launcher.Plugin.PluginsManager catch (Exception e) { Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), - string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), plugin.Name)); + string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), + plugin.Name)); Log.Exception("PluginsManager", "An error occured while downloading plugin", e, "InstallOrUpdate"); @@ -164,7 +166,8 @@ namespace Flow.Launcher.Plugin.PluginsManager from existingPlugin in Context.API.GetAllPlugins() join pluginFromManifest in pluginsManifest.UserPlugins on existingPlugin.Metadata.ID equals pluginFromManifest.ID - where existingPlugin.Metadata.Version.CompareTo(pluginFromManifest.Version) < 0 // if current version precedes manifest version + where existingPlugin.Metadata.Version.CompareTo(pluginFromManifest.Version) < + 0 // if current version precedes manifest version select new { @@ -214,22 +217,29 @@ namespace Flow.Launcher.Plugin.PluginsManager Task.Run(async delegate { - Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), + Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); - await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath).ConfigureAwait(false); + await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath) + .ConfigureAwait(false); - Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_download_success")); + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), + Context.API.GetTranslation("plugin_pluginsmanager_download_success")); Install(x.PluginNewUserPlugin, downloadToFilePath); Context.API.RestartApp(); }).ContinueWith(t => { - Log.Exception("PluginsManager", $"Update failed for {x.Name}", t.Exception.InnerException, "RequestUpdate"); - Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), - string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), x.Name)); + Log.Exception("PluginsManager", $"Update failed for {x.Name}", + t.Exception.InnerException, "RequestUpdate"); + Context.API.ShowMsg( + Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"), + string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), + x.Name)); }, TaskContinuationOptions.OnlyOnFaulted); return true; @@ -264,8 +274,21 @@ namespace Flow.Launcher.Plugin.PluginsManager .ToList(); } - internal List RequestInstallOrUpdate(string searchName) + private Task _downloadManifestTask = Task.CompletedTask; + + internal async ValueTask> RequestInstallOrUpdate(string searchName, CancellationToken token) { + if (!pluginsManifest.UserPlugins.Any() && + _downloadManifestTask.Status != TaskStatus.Running) + { + _downloadManifestTask = pluginsManifest.DownloadManifest(); + } + + await _downloadManifestTask; + + if (token.IsCancellationRequested) + return null; + var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty).Trim(); var results = @@ -406,4 +429,4 @@ namespace Flow.Launcher.Plugin.PluginsManager return new List(); } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index 60ab7ab18..7f3f4a5cf 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -6,7 +6,7 @@ "Name": "Plugins Manager", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Author": "Jeremy Wu", - "Version": "1.5.0", + "Version": "1.6.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Program/plugin.json b/Plugins/Flow.Launcher.Plugin.Program/plugin.json index 6c2c18e47..1dedd9909 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Program/plugin.json @@ -4,7 +4,7 @@ "Name": "Program", "Description": "Search programs in Flow.Launcher", "Author": "qianlifeng", - "Version": "1.2.3", + "Version": "1.3.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Program.dll", diff --git a/Plugins/Flow.Launcher.Plugin.Sys/plugin.json b/Plugins/Flow.Launcher.Plugin.Sys/plugin.json index cf8ed6041..75d7073b8 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": "1.1.2", + "Version": "1.2.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Sys.dll", diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json index c036fbf8b..2696ab622 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json @@ -25,7 +25,7 @@ "Name": "Web Searches", "Description": "Provide the web search ability", "Author": "qianlifeng", - "Version": "1.2.1", + "Version": "1.3.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll",