mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
- Use DynamicData SourceList with automatic sorting by score descending - Add ReplaceResults() with EditDiff for minimal UI updates (reduces flickering) - Keep previous results visible while typing until new results arrive - Add ImageLoader with Windows Shell API (IShellItemImageFactory) for exe/ico icons - Use AlphaFormat.Unpremul to correctly render transparent icons without white borders - Query all plugins in parallel and merge/sort results globally
208 lines
6.3 KiB
C#
208 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Flow.Launcher.Core.Plugin;
|
|
using Flow.Launcher.Infrastructure.Logger;
|
|
using Flow.Launcher.Infrastructure.UserSettings;
|
|
using Flow.Launcher.Plugin;
|
|
|
|
namespace Flow.Launcher.Avalonia.ViewModel;
|
|
|
|
/// <summary>
|
|
/// MainViewModel for Avalonia - minimal implementation for plugin queries.
|
|
/// </summary>
|
|
public partial class MainViewModel : ObservableObject
|
|
{
|
|
private static readonly string ClassName = nameof(MainViewModel);
|
|
private readonly Settings _settings;
|
|
private CancellationTokenSource? _queryTokenSource;
|
|
private bool _pluginsReady;
|
|
|
|
public event Action? HideRequested;
|
|
public event Action? ShowRequested;
|
|
|
|
[ObservableProperty]
|
|
private bool _mainWindowVisibility = true;
|
|
|
|
[ObservableProperty]
|
|
private string _queryText = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
private bool _isQueryRunning;
|
|
|
|
[ObservableProperty]
|
|
private bool _hasResults;
|
|
|
|
[ObservableProperty]
|
|
private ResultsViewModel _results;
|
|
|
|
public Settings Settings => _settings;
|
|
|
|
public MainViewModel(Settings settings)
|
|
{
|
|
_settings = settings;
|
|
_results = new ResultsViewModel(settings);
|
|
}
|
|
|
|
public void OnPluginsReady()
|
|
{
|
|
_pluginsReady = true;
|
|
Log.Info(ClassName, "Plugins ready");
|
|
if (!string.IsNullOrWhiteSpace(QueryText))
|
|
_ = QueryAsync();
|
|
}
|
|
|
|
public void RequestHide() => HideRequested?.Invoke();
|
|
|
|
/// <summary>
|
|
/// Toggle the main window visibility. Called by global hotkey.
|
|
/// </summary>
|
|
public void ToggleFlowLauncher()
|
|
{
|
|
Log.Info(ClassName, $"ToggleFlowLauncher called, currently visible: {MainWindowVisibility}");
|
|
if (MainWindowVisibility)
|
|
{
|
|
Hide();
|
|
}
|
|
else
|
|
{
|
|
Show();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show the main window.
|
|
/// </summary>
|
|
public void Show()
|
|
{
|
|
MainWindowVisibility = true;
|
|
ShowRequested?.Invoke();
|
|
Log.Info(ClassName, "Show requested");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hide the main window.
|
|
/// </summary>
|
|
public void Hide()
|
|
{
|
|
MainWindowVisibility = false;
|
|
QueryText = "";
|
|
HideRequested?.Invoke();
|
|
Log.Info(ClassName, "Hide requested");
|
|
}
|
|
|
|
partial void OnQueryTextChanged(string value) => _ = QueryAsync();
|
|
|
|
private async Task QueryAsync()
|
|
{
|
|
_queryTokenSource?.Cancel();
|
|
_queryTokenSource = new CancellationTokenSource();
|
|
var token = _queryTokenSource.Token;
|
|
var queryText = QueryText.Trim();
|
|
|
|
// Only clear results when query is empty
|
|
if (string.IsNullOrWhiteSpace(queryText))
|
|
{
|
|
Results.Clear();
|
|
HasResults = false;
|
|
IsQueryRunning = false;
|
|
return;
|
|
}
|
|
|
|
if (!_pluginsReady)
|
|
{
|
|
IsQueryRunning = false;
|
|
return;
|
|
}
|
|
|
|
IsQueryRunning = true;
|
|
|
|
try
|
|
{
|
|
var query = QueryBuilder.Build(queryText, PluginManager.NonGlobalPlugins);
|
|
if (query == null) { HasResults = false; return; }
|
|
|
|
var plugins = PluginManager.ValidPluginsForQuery(query, dialogJump: false)
|
|
.Where(p => !p.Metadata.Disabled).ToList();
|
|
|
|
if (plugins.Count == 0) { HasResults = false; return; }
|
|
|
|
// Query all plugins in parallel and collect results
|
|
var tasks = plugins.Select(p => QueryPluginAsync(p, query, token));
|
|
var pluginResults = await Task.WhenAll(tasks);
|
|
|
|
if (token.IsCancellationRequested) return;
|
|
|
|
// Flatten, sort by score, take top N, and replace all at once
|
|
var allResults = pluginResults
|
|
.SelectMany(r => r)
|
|
.OrderByDescending(r => r.Score)
|
|
.Take(_settings.MaxResultsToShow)
|
|
.ToList();
|
|
|
|
// Replace results with minimal UI updates (EditDiff)
|
|
await global::Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
Results.ReplaceResults(allResults);
|
|
HasResults = Results.Results.Count > 0;
|
|
});
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception e) { Log.Exception(ClassName, "Query error", e); }
|
|
finally { if (!token.IsCancellationRequested) IsQueryRunning = false; }
|
|
}
|
|
|
|
private async Task<List<ResultViewModel>> QueryPluginAsync(PluginPair plugin, Query query, CancellationToken token)
|
|
{
|
|
var resultList = new List<ResultViewModel>();
|
|
|
|
try
|
|
{
|
|
var delay = plugin.Metadata.SearchDelayTime ?? _settings.SearchDelayTime;
|
|
if (delay > 0) await Task.Delay(delay, token);
|
|
if (token.IsCancellationRequested) return resultList;
|
|
|
|
var results = await PluginManager.QueryForPluginAsync(plugin, query, token);
|
|
if (token.IsCancellationRequested || results == null || results.Count == 0) return resultList;
|
|
|
|
foreach (var r in results)
|
|
{
|
|
resultList.Add(new ResultViewModel
|
|
{
|
|
Title = r.Title ?? "",
|
|
SubTitle = r.SubTitle ?? "",
|
|
IconPath = r.IcoPath ?? plugin.Metadata.IcoPath ?? "",
|
|
Score = r.Score,
|
|
PluginResult = r
|
|
});
|
|
}
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception e) { Log.Exception(ClassName, $"Plugin {plugin.Metadata.Name} error", e); }
|
|
|
|
return resultList;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Esc() { Hide(); }
|
|
|
|
[RelayCommand]
|
|
private async Task OpenResultAsync()
|
|
{
|
|
var result = Results.SelectedItem?.PluginResult;
|
|
if (result == null) return;
|
|
try
|
|
{
|
|
if (await result.ExecuteAsync(new ActionContext { SpecialKeyState = SpecialKeyState.Default }))
|
|
HideRequested?.Invoke();
|
|
}
|
|
catch (Exception e) { Log.Exception(ClassName, "Execute error", e); }
|
|
}
|
|
|
|
[RelayCommand] private void SelectNextItem() => Results.SelectNextItem();
|
|
[RelayCommand] private void SelectPrevItem() => Results.SelectPrevItem();
|
|
}
|