mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge pull request #2082 from VictoriousRaptor/quicklook
Add external preview (QuickLook) support
This commit is contained in:
commit
ea65c8fcbb
9 changed files with 300 additions and 66 deletions
1
.github/actions/spelling/expect.txt
vendored
1
.github/actions/spelling/expect.txt
vendored
|
|
@ -98,6 +98,7 @@ Português
|
|||
Português (Brasil)
|
||||
Italiano
|
||||
Slovenský
|
||||
quicklook
|
||||
Tiếng Việt
|
||||
Droplex
|
||||
Preinstalled
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
40
Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs
Normal file
40
Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ namespace Flow.Launcher
|
|||
if (_settings.UseAnimation)
|
||||
await Task.Delay(100);
|
||||
|
||||
if (_settings.HideWhenDeactivated)
|
||||
if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible)
|
||||
{
|
||||
_viewModel.Hide();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue