Merge pull request #2082 from VictoriousRaptor/quicklook

Add external preview (QuickLook) support
This commit is contained in:
Jeremy Wu 2024-06-16 13:08:15 +10:00 committed by GitHub
commit ea65c8fcbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 300 additions and 66 deletions

View file

@ -98,6 +98,7 @@ Português
Português (Brasil)
Italiano
Slovenský
quicklook
Tiếng Việt
Droplex
Preinstalled

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.ExternalPlugins;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -90,6 +90,48 @@ namespace Flow.Launcher.Core.Plugin
}).ToArray());
}
public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
}).ToArray());
}
public static async Task CloseExternalPreviewAsync()
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.ClosePreviewAsync(),
_ => Task.CompletedTask,
}).ToArray());
}
public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
{
IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
_ => Task.CompletedTask,
}).ToArray());
}
public static bool UseExternalPreview()
{
return GetPluginsForInterface<IAsyncExternalPreview>().Any(x => !x.Metadata.Disabled);
}
public static bool AllowAlwaysPreview()
{
var plugin = GetPluginsForInterface<IAsyncExternalPreview>().FirstOrDefault(x => !x.Metadata.Disabled);
if (plugin is null)
return false;
return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
}
static PluginManager()
{
// validate user directory

View file

@ -185,7 +185,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
/// when false Alphabet static service will always return empty results
/// </summary>
public bool ShouldUsePinyin { get; set; } = false;
public bool AlwaysPreview { get; set; } = false;
public bool AlwaysStartEn { get; set; } = false;
private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular;

View file

@ -0,0 +1,40 @@
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// This interface is for plugins that wish to provide file preview (external preview)
/// via a third party app instead of the default preview.
/// </summary>
public interface IAsyncExternalPreview : IFeatures
{
/// <summary>
/// Method for opening/showing the preview.
/// </summary>
/// <param name="path">The file path to open the preview for</param>
/// <param name="sendFailToast">Whether to send a toast message notification on failure for the user</param>
public Task OpenPreviewAsync(string path, bool sendFailToast = true);
/// <summary>
/// Method for closing/hiding the preview.
/// </summary>
public Task ClosePreviewAsync();
/// <summary>
/// Method for switching the preview to the next file result.
/// This requires the external preview be already open/showing
/// </summary>
/// <param name="path">The file path to switch the preview for</param>
/// <param name="sendFailToast">Whether to send a toast message notification on failure for the user</param>
public Task SwitchPreviewAsync(string path, bool sendFailToast = true);
/// <summary>
/// Allows the preview plugin to override the AlwaysPreview setting. Typically useful if plugin's preview does not
/// fully work well with being shown together when the query window appears with results.
/// When AlwaysPreview setting is on and this is set to false, the preview will not be shown when query
/// window appears with results, instead the internal preview will be shown.
/// </summary>
/// <returns></returns>
public bool AllowAlwaysPreview();
}
}

View file

@ -270,12 +270,12 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Full image used for preview panel
/// </summary>
public string PreviewImagePath { get; set; }
public string PreviewImagePath { get; set; } = null;
/// <summary>
/// Determines if the preview image should occupy the full width of the preview panel.
/// </summary>
public bool IsMedia { get; set; }
public bool IsMedia { get; set; } = false;
/// <summary>
/// Result description text that is shown at the bottom of the preview panel.
@ -283,12 +283,17 @@ namespace Flow.Launcher.Plugin
/// <remarks>
/// When a value is not set, the <see cref="SubTitle"/> will be used.
/// </remarks>
public string Description { get; set; }
public string Description { get; set; } = null;
/// <summary>
/// Delegate to get the preview panel's image
/// </summary>
public IconDelegate PreviewDelegate { get; set; }
public IconDelegate PreviewDelegate { get; set; } = null;
/// <summary>
/// File path of the result. For third-party programs providing external preview.
/// </summary>
public string FilePath { get; set; } = null;
/// <summary>
/// Default instance of <see cref="PreviewInfo"/>
@ -299,6 +304,7 @@ namespace Flow.Launcher.Plugin
Description = null,
IsMedia = false,
PreviewDelegate = null,
FilePath = null,
};
}
}

View file

@ -427,7 +427,7 @@
VerticalAlignment="Stretch"
Background="Transparent"
ShowsPreview="True"
Visibility="{Binding PreviewVisible, Converter={StaticResource BoolToVisibilityConverter}}">
Visibility="{Binding InternalPreviewVisible, Converter={StaticResource BoolToVisibilityConverter}}">
<GridSplitter.Template>
<ControlTemplate TargetType="{x:Type GridSplitter}">
<Border Style="{DynamicResource PreviewBorderStyle}" />
@ -439,7 +439,7 @@
Grid.Column="2"
VerticalAlignment="Stretch"
Style="{DynamicResource PreviewArea}"
Visibility="{Binding PreviewVisible, Converter={StaticResource BoolToVisibilityConverter}}">
Visibility="{Binding InternalPreviewVisible, Converter={StaticResource BoolToVisibilityConverter}}">
<Border
MinHeight="380"
d:DataContext="{d:DesignInstance vm:ResultViewModel}"

View file

@ -630,7 +630,7 @@ namespace Flow.Launcher
if (_settings.UseAnimation)
await Task.Delay(100);
if (_settings.HideWhenDeactivated)
if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible)
{
_viewModel.Hide();
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@ -407,6 +407,10 @@ namespace Flow.Launcher.ViewModel
}
}
#endregion
#region BasicCommands
[RelayCommand]
private void OpenSetting()
{
@ -581,56 +585,6 @@ namespace Flow.Launcher.ViewModel
Settings.MaxResultsToShow -= 1;
}
[RelayCommand]
public void TogglePreview()
{
if (!PreviewVisible)
{
ShowPreview();
}
else
{
HidePreview();
}
ContextMenu.IsPreviewOn = PreviewVisible;
History.IsPreviewOn = PreviewVisible;
Results.IsPreviewOn = PreviewVisible;
}
private void ShowPreview()
{
ResultAreaColumn = 1;
PreviewVisible = true;
Results.SelectedItem?.LoadPreviewImage();
}
private void HidePreview()
{
ResultAreaColumn = 3;
PreviewVisible = false;
}
public void ResetPreview()
{
if (Settings.AlwaysPreview == true)
{
ShowPreview();
}
else
{
HidePreview();
}
}
private void UpdatePreview()
{
if (PreviewVisible)
{
Results.SelectedItem?.LoadPreviewImage();
}
}
/// <summary>
/// we need move cursor to end when we manually changed query
/// but we don't want to move cursor to end when query is updated from TextBox
@ -805,10 +759,186 @@ namespace Flow.Launcher.ViewModel
public string Image => Constant.QueryTextBoxIconImagePath;
public bool StartWithEnglishMode => Settings.AlwaysStartEn;
#endregion
public bool PreviewVisible { get; set; } = false;
#region Preview
public int ResultAreaColumn { get; set; } = 1;
public bool InternalPreviewVisible
{
get
{
if (ResultAreaColumn == ResultAreaColumnPreviewShown)
return true;
if (ResultAreaColumn == ResultAreaColumnPreviewHidden)
return false;
#if DEBUG
throw new NotImplementedException("ResultAreaColumn should match ResultAreaColumnPreviewShown/ResultAreaColumnPreviewHidden value");
#else
Log.Error("MainViewModel", "ResultAreaColumnPreviewHidden/ResultAreaColumnPreviewShown int value not implemented", "InternalPreviewVisible");
#endif
return false;
}
}
private static readonly int ResultAreaColumnPreviewShown = 1;
private static readonly int ResultAreaColumnPreviewHidden = 3;
public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown;
// This is not a reliable indicator of whether external preview is visible due to the
// ability of manually closing/exiting the external preview program which, does not inform flow that
// preview is no longer available.
public bool ExternalPreviewVisible { get; set; } = false;
private void ShowPreview()
{
var useExternalPreview = PluginManager.UseExternalPreview();
switch (useExternalPreview)
{
case true
when CanExternalPreviewSelectedResult(out var path):
// Internal preview may still be on when user switches to external
if (InternalPreviewVisible)
HideInternalPreview();
OpenExternalPreview(path);
break;
case true
when !CanExternalPreviewSelectedResult(out var _):
if (ExternalPreviewVisible)
CloseExternalPreview();
ShowInternalPreview();
break;
case false:
ShowInternalPreview();
break;
}
}
private void HidePreview()
{
if (PluginManager.UseExternalPreview())
CloseExternalPreview();
if (InternalPreviewVisible)
HideInternalPreview();
}
[RelayCommand]
private void TogglePreview()
{
if (InternalPreviewVisible || ExternalPreviewVisible)
{
HidePreview();
}
else
{
ShowPreview();
}
}
private void ToggleInternalPreview()
{
if (!InternalPreviewVisible)
{
ShowInternalPreview();
}
else
{
HideInternalPreview();
}
}
private void OpenExternalPreview(string path, bool sendFailToast = true)
{
_ = PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
ExternalPreviewVisible = true;
}
private void CloseExternalPreview()
{
_ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
ExternalPreviewVisible = false;
}
private void SwitchExternalPreview(string path, bool sendFailToast = true)
{
_ = PluginManager.SwitchExternalPreviewAsync(path,sendFailToast).ConfigureAwait(false);
}
private void ShowInternalPreview()
{
ResultAreaColumn = ResultAreaColumnPreviewShown;
Results.SelectedItem?.LoadPreviewImage();
}
private void HideInternalPreview()
{
ResultAreaColumn = ResultAreaColumnPreviewHidden;
}
public void ResetPreview()
{
switch (Settings.AlwaysPreview)
{
case true
when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path):
OpenExternalPreview(path);
break;
case true:
ShowInternalPreview();
break;
case false:
HidePreview();
break;
}
}
private void UpdatePreview()
{
switch (PluginManager.UseExternalPreview())
{
case true
when CanExternalPreviewSelectedResult(out var path):
if (ExternalPreviewVisible)
{
SwitchExternalPreview(path, false);
}
else if (InternalPreviewVisible)
{
HideInternalPreview();
OpenExternalPreview(path);
}
break;
case true
when !CanExternalPreviewSelectedResult(out var _):
if (ExternalPreviewVisible)
{
CloseExternalPreview();
ShowInternalPreview();
}
break;
case false
when InternalPreviewVisible:
Results.SelectedItem?.LoadPreviewImage();
break;
}
}
private bool CanExternalPreviewSelectedResult(out string path)
{
path = Results.SelectedItem?.Result?.Preview.FilePath;
return !string.IsNullOrEmpty(path);
}
#endregion
@ -1232,6 +1362,9 @@ namespace Flow.Launcher.ViewModel
lastContextMenuResult = new Result();
lastContextMenuResults = new List<Result>();
if (ExternalPreviewVisible)
CloseExternalPreview();
if (!SelectedIsFromQueryResults())
{
SelectedResults = Results;

View file

@ -102,6 +102,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
CopyText = path,
Preview = new Result.PreviewInfo
{
FilePath = path,
},
Action = c =>
{
if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt)
@ -192,6 +196,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Score = 500,
ProgressBar = progressValue,
ProgressBarColor = progressBarColor,
Preview = new Result.PreviewInfo
{
FilePath = path,
},
Action = _ =>
{
OpenFolder(path);
@ -261,10 +269,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false)
{
Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath))
? new Result.PreviewInfo { IsMedia = true, PreviewImagePath = filePath, }
: Result.PreviewInfo.Default;
bool isMedia = IsMedia(Path.GetExtension(filePath));
var title = Path.GetFileName(filePath);
@ -275,7 +280,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Title = title,
SubTitle = Path.GetDirectoryName(filePath),
IcoPath = filePath,
Preview = preview,
Preview = new Result.PreviewInfo
{
IsMedia = isMedia,
PreviewImagePath = isMedia ? filePath : null,
FilePath = filePath,
},
AutoCompleteText = GetAutoCompleteText(title, query, filePath, ResultType.File),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
Score = score,