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) Português (Brasil)
Italiano Italiano
Slovenský Slovenský
quicklook
Tiếng Việt Tiếng Việt
Droplex Droplex
Preinstalled Preinstalled

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Core.ExternalPlugins;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -90,6 +90,48 @@ namespace Flow.Launcher.Core.Plugin
}).ToArray()); }).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() static PluginManager()
{ {
// validate user directory // validate user directory

View file

@ -185,7 +185,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
/// when false Alphabet static service will always return empty results /// when false Alphabet static service will always return empty results
/// </summary> /// </summary>
public bool ShouldUsePinyin { get; set; } = false; public bool ShouldUsePinyin { get; set; } = false;
public bool AlwaysPreview { get; set; } = false; public bool AlwaysPreview { get; set; } = false;
public bool AlwaysStartEn { get; set; } = false; public bool AlwaysStartEn { get; set; } = false;
private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular; 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> /// <summary>
/// Full image used for preview panel /// Full image used for preview panel
/// </summary> /// </summary>
public string PreviewImagePath { get; set; } public string PreviewImagePath { get; set; } = null;
/// <summary> /// <summary>
/// Determines if the preview image should occupy the full width of the preview panel. /// Determines if the preview image should occupy the full width of the preview panel.
/// </summary> /// </summary>
public bool IsMedia { get; set; } public bool IsMedia { get; set; } = false;
/// <summary> /// <summary>
/// Result description text that is shown at the bottom of the preview panel. /// Result description text that is shown at the bottom of the preview panel.
@ -283,12 +283,17 @@ namespace Flow.Launcher.Plugin
/// <remarks> /// <remarks>
/// When a value is not set, the <see cref="SubTitle"/> will be used. /// When a value is not set, the <see cref="SubTitle"/> will be used.
/// </remarks> /// </remarks>
public string Description { get; set; } public string Description { get; set; } = null;
/// <summary> /// <summary>
/// Delegate to get the preview panel's image /// Delegate to get the preview panel's image
/// </summary> /// </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> /// <summary>
/// Default instance of <see cref="PreviewInfo"/> /// Default instance of <see cref="PreviewInfo"/>
@ -299,6 +304,7 @@ namespace Flow.Launcher.Plugin
Description = null, Description = null,
IsMedia = false, IsMedia = false,
PreviewDelegate = null, PreviewDelegate = null,
FilePath = null,
}; };
} }
} }

View file

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

View file

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

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -407,6 +407,10 @@ namespace Flow.Launcher.ViewModel
} }
} }
#endregion
#region BasicCommands
[RelayCommand] [RelayCommand]
private void OpenSetting() private void OpenSetting()
{ {
@ -581,56 +585,6 @@ namespace Flow.Launcher.ViewModel
Settings.MaxResultsToShow -= 1; 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> /// <summary>
/// we need move cursor to end when we manually changed query /// 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 /// 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 string Image => Constant.QueryTextBoxIconImagePath;
public bool StartWithEnglishMode => Settings.AlwaysStartEn; 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 #endregion
@ -1232,6 +1362,9 @@ namespace Flow.Launcher.ViewModel
lastContextMenuResult = new Result(); lastContextMenuResult = new Result();
lastContextMenuResults = new List<Result>(); lastContextMenuResults = new List<Result>();
if (ExternalPreviewVisible)
CloseExternalPreview();
if (!SelectedIsFromQueryResults()) if (!SelectedIsFromQueryResults())
{ {
SelectedResults = Results; SelectedResults = Results;

View file

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