Merge pull request #1476 from LeLocTai/dev

[Program Plugin] Support .url file and Steam/Epic shortcuts
This commit is contained in:
VictoriousRaptor 2022-11-02 05:23:56 +08:00 committed by GitHub
commit e17ec6ac65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 337 additions and 30 deletions

View file

@ -163,15 +163,17 @@ namespace Flow.Launcher.ViewModel
}
}
var loadFullImage = (Path.GetExtension(imagePath) ?? "").Equals(".url", StringComparison.OrdinalIgnoreCase);
if (ImageLoader.CacheContainImage(imagePath))
{
// will get here either when icoPath has value\icon delegate is null\when had exception in delegate
image = ImageLoader.Load(imagePath);
image = ImageLoader.Load(imagePath, loadFullImage);
return;
}
// We need to modify the property not field here to trigger the OnPropertyChanged event
Image = await Task.Run(() => ImageLoader.Load(imagePath)).ConfigureAwait(false);
Image = await Task.Run(() => ImageLoader.Load(imagePath, loadFullImage)).ConfigureAwait(false);
}
public Result Result { get; }

View file

@ -58,6 +58,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="System.Runtime" Version="4.3.1" />
</ItemGroup>

View file

@ -4,6 +4,7 @@
xmlns:system="clr-namespace:System;assembly=mscorlib">
<!-- Program setting -->
<system:String x:Key="flowlauncher_plugin_program_reset">Reset Default</system:String>
<system:String x:Key="flowlauncher_plugin_program_delete">Delete</system:String>
<system:String x:Key="flowlauncher_plugin_program_edit">Edit</system:String>
<system:String x:Key="flowlauncher_plugin_program_add">Add</system:String>
@ -12,7 +13,7 @@
<system:String x:Key="flowlauncher_plugin_program_disable">Disable</system:String>
<system:String x:Key="flowlauncher_plugin_program_location">Location</system:String>
<system:String x:Key="flowlauncher_plugin_program_all_programs">All Programs</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes">File Suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes">File Type</system:String>
<system:String x:Key="flowlauncher_plugin_program_reindex">Reindex</system:String>
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexing</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Index Start Menu</system:String>
@ -35,9 +36,21 @@
<system:String x:Key="flowlauncher_plugin_program_delete_program_source">Are you sure you want to delete the selected program sources?</system:String>
<system:String x:Key="flowlauncher_plugin_program_update">OK</system:String>
<system:String x:Key="flowlauncher_plugin_program_only_index_tip">Flow Launcher will only index files that end with the following suffixes. (Each suffix should split by ';' )</system:String>
<system:String x:Key="flowlauncher_plugin_program_only_index_tip">Program Plugin will only index files with selected suffixes and .url files with selected protocols.</system:String>
<system:String x:Key="flowlauncher_plugin_program_update_file_suffixes">Successfully updated file suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_cannot_empty">File suffixes can't be empty</system:String>
<system:String x:Key="flowlauncher_plugin_protocols_cannot_empty">Protocols can't be empty</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_excutable_types">File Suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_URL_types">URL Protocols</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_custom_urls">Custom URL Protocols</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_custom_file_types">Custom File Suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_tooltip">
Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py)
</system:String>
<system:String x:Key="flowlauncher_plugin_program_protocol_tooltip">
Insert protocols of .url files you want to index. Protocols should be separated by ';'. (ex>ftp;netflix)
</system:String>
<system:String x:Key="flowlauncher_plugin_program_run_as_different_user">Run As Different User</system:String>
<system:String x:Key="flowlauncher_plugin_program_run_as_administrator">Run As Administrator</system:String>

View file

@ -4,10 +4,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
Title="{DynamicResource flowlauncher_plugin_program_suffixes}"
Width="400"
Width="600"
Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
@ -15,9 +17,73 @@
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Grid>
<Window.Resources>
<Style
x:Key="CustomFileTypeTextBox"
BasedOn="{StaticResource DefaultTextBoxStyle}"
TargetType="TextBox">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=CustomFiles, Path=IsChecked}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style
x:Key="CustomURLTypeTextBox"
BasedOn="{StaticResource DefaultTextBoxStyle}"
TargetType="TextBox">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=CustomProtocol, Path=IsChecked}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="SettingGroupBoxSuffixToolTip" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource Color00B}" />
<Setter Property="BorderBrush" Value="{DynamicResource Color03B}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Margin" Value="0,5,0,0" />
<Setter Property="Padding" Value="15,15,15,15" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=tbSuffixes, Path=IsFocused}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=tbSuffixes, Path=IsFocused}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="SettingGroupBoxURLToolTip" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource Color00B}" />
<Setter Property="BorderBrush" Value="{DynamicResource Color03B}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Margin" Value="0,5,0,0" />
<Setter Property="Padding" Value="15,15,15,15" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=tbProtocols, Path=IsFocused}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=tbProtocols, Path=IsFocused}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid x:Name="WindowArea">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
<RowDefinition Height="80" />
</Grid.RowDefinitions>
@ -55,7 +121,9 @@
</Grid>
</StackPanel>
<StackPanel Margin="26,12,26,0">
<StackPanel Margin="0,0,0,12">
<TextBlock
Grid.Column="0"
Margin="0,0,0,0"
@ -65,10 +133,77 @@
TextAlignment="Left" />
</StackPanel>
<TextBlock
Margin="0,0,0,10"
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_program_only_index_tip}"
TextWrapping="Wrap" />
<TextBox x:Name="tbSuffixes" Margin="0,20,0,20" />
<Border Style="{DynamicResource SettingGroupBoxURLToolTip}">
<TextBlock
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_program_protocol_tooltip}"
TextWrapping="Wrap" />
</Border>
<Border Style="{DynamicResource SettingGroupBoxSuffixToolTip}">
<TextBlock
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_program_suffixes_tooltip}"
TextWrapping="Wrap" />
</Border>
<Grid Margin="0,20,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="250" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="0,0,0,0">
<TextBlock
Margin="0,0,0,8"
FontSize="16"
FontWeight="SemiBold"
Text="{DynamicResource flowlauncher_plugin_program_suffixes_excutable_types}" />
<CheckBox Name="apprefMS" Margin="10,0,0,0" IsChecked="{Binding SuffixesStatus[appref-ms]}">appref-ms</CheckBox>
<CheckBox Name="exe" Margin="10,0,0,0" IsChecked="{Binding SuffixesStatus[exe]}">exe</CheckBox>
<CheckBox Name="lnk" Margin="10,0,0,0" IsChecked="{Binding SuffixesStatus[lnk]}">lnk</CheckBox>
<CheckBox
Name="CustomFiles"
Margin="10,0,0,0"
IsChecked="{Binding UseCustomSuffixes}"
Content="{DynamicResource flowlauncher_plugin_program_suffixes_custom_file_types}" />
<TextBox
x:Name="tbSuffixes"
Margin="10,4,0,6"
Style="{StaticResource CustomFileTypeTextBox}" />
</StackPanel>
<Border
Grid.Column="1"
Margin="20,0,0,10"
Padding="20,0,0,0"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="1,0,0,0">
<StackPanel>
<TextBlock
Margin="0,0,0,8"
FontSize="16"
FontWeight="SemiBold"
Text="{DynamicResource flowlauncher_plugin_program_suffixes_URL_types}" />
<CheckBox Name="steam" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[steam]}">Steam Games</CheckBox>
<CheckBox Name="epic" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[epic]}">Epic Games</CheckBox>
<CheckBox Name="http" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[http]}">Http/Https</CheckBox>
<CheckBox
Name="CustomProtocol"
Margin="10,0,0,0"
IsChecked="{Binding UseCustomProtocols}"
Content="{DynamicResource flowlauncher_plugin_program_suffixes_custom_urls}" />
<TextBox
x:Name="tbProtocols"
Margin="10,4,0,6"
Style="{StaticResource CustomURLTypeTextBox}" />
</StackPanel>
</Border>
</Grid>
</StackPanel>
</StackPanel>
<Border
@ -78,10 +213,17 @@
BorderThickness="0,1,0,0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
x:Name="btnCancel"
x:Name="btnReset"
Height="30"
MinWidth="140"
Margin="0,0,5,0"
Click="BtnReset_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_reset}" />
<Button
x:Name="btnCancel"
Height="30"
MinWidth="140"
Margin="5,0,5,0"
Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" />
@ -90,7 +232,7 @@
MinWidth="140"
Margin="5,0,0,0"
HorizontalAlignment="Right"
Click="ButtonBase_OnClick"
Click="BtnAdd_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_update}"
Style="{DynamicResource AccentButtonStyle}" />
</StackPanel>

View file

@ -1,44 +1,76 @@
using System;
using System.Collections.Generic;
using System.Windows;
namespace Flow.Launcher.Plugin.Program
{
/// <summary>
/// ProgramSuffixes.xaml 的交互逻辑
/// </summary>
public partial class ProgramSuffixes
{
private PluginInitContext context;
private Settings _settings;
public Dictionary<string, bool> SuffixesStatus { get; set; }
public Dictionary<string, bool> ProtocolsStatus { get; set; }
public bool UseCustomSuffixes { get; set; }
public bool UseCustomProtocols { get; set; }
public ProgramSuffixes(PluginInitContext context, Settings settings)
{
this.context = context;
InitializeComponent();
_settings = settings;
tbSuffixes.Text = string.Join(Settings.SuffixSeperator.ToString(), _settings.ProgramSuffixes);
SuffixesStatus = new Dictionary<string, bool>(_settings.BuiltinSuffixesStatus);
ProtocolsStatus = new Dictionary<string, bool>(_settings.BuiltinProtocolsStatus);
UseCustomSuffixes = _settings.UseCustomSuffixes;
UseCustomProtocols = _settings.UseCustomProtocols;
InitializeComponent();
tbSuffixes.Text = string.Join(Settings.SuffixSeparator, _settings.CustomSuffixes);
tbProtocols.Text = string.Join(Settings.SuffixSeparator, _settings.CustomProtocols);
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
var suffixes = tbSuffixes.Text.Split(Settings.SuffixSeperator, StringSplitOptions.RemoveEmptyEntries);
if (suffixes.Length == 0)
private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
{
var suffixes = tbSuffixes.Text.Split(Settings.SuffixSeparator, StringSplitOptions.RemoveEmptyEntries);
var protocols = tbProtocols.Text.Split(Settings.SuffixSeparator, StringSplitOptions.RemoveEmptyEntries);
if (suffixes.Length == 0 && UseCustomSuffixes)
{
string warning = context.API.GetTranslation("flowlauncher_plugin_program_suffixes_cannot_empty");
MessageBox.Show(warning);
return;
}
_settings.ProgramSuffixes = suffixes;
if (protocols.Length == 0 && UseCustomProtocols)
{
string warning = context.API.GetTranslation("flowlauncher_plugin_protocols_cannot_empty");
MessageBox.Show(warning);
return;
}
string msg = context.API.GetTranslation("flowlauncher_plugin_program_update_file_suffixes");
MessageBox.Show(msg);
_settings.CustomSuffixes = suffixes;
_settings.CustomProtocols = protocols;
_settings.BuiltinSuffixesStatus = new Dictionary<string, bool>(SuffixesStatus);
_settings.BuiltinProtocolsStatus = new Dictionary<string, bool>(ProtocolsStatus);
_settings.UseCustomSuffixes = UseCustomSuffixes;
_settings.UseCustomProtocols = UseCustomProtocols;
DialogResult = true;
}
private void BtnReset_OnClick(object sender, RoutedEventArgs e)
{
apprefMS.IsChecked = true;
exe.IsChecked = true;
lnk.IsChecked = true;
CustomFiles.IsChecked = false;
steam.IsChecked = true;
epic.IsChecked = true;
http.IsChecked = false;
CustomProtocol.IsChecked = false;
}
}
}
}

View file

@ -16,7 +16,10 @@ using System.Collections;
using System.Diagnostics;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using System.Threading.Channels;
using Flow.Launcher.Infrastructure.Image;
using IniParser;
namespace Flow.Launcher.Plugin.Program.Programs
{
@ -36,6 +39,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string Location => ParentDirectory;
private const string ShortcutExtension = "lnk";
private const string UrlExtension = "url";
private const string ExeExtension = "exe";
private static readonly Win32 Default = new Win32()
@ -287,6 +291,45 @@ namespace Flow.Launcher.Plugin.Program.Programs
#endif
}
private static Win32 UrlProgram(string path)
{
var program = Win32Program(path);
program.Valid = false;
try
{
var parser = new FileIniDataParser();
var data = parser.ReadFile(path);
var urlSection = data["InternetShortcut"];
var url = urlSection?["URL"];
if (String.IsNullOrEmpty(url))
{
return program;
}
foreach(var protocol in Main._settings.GetProtocols())
{
if(url.StartsWith(protocol))
{
program.LnkResolvedPath = url;
program.Valid = true;
break;
}
}
var iconPath = urlSection?["IconFile"];
if (!String.IsNullOrEmpty(iconPath))
{
program.IcoPath = iconPath;
}
}
catch (Exception e)
{
// Many files do not have the required fields, so no logging is done.
}
return program;
}
private static Win32 ExeProgram(string path)
{
try
@ -343,6 +386,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
ExeExtension => ExeProgram(x),
ShortcutExtension => LnkProgram(x),
UrlExtension => UrlProgram(x),
_ => Win32Program(x)
});
@ -365,6 +409,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
.Select(x => Extension(x) switch
{
ShortcutExtension => LnkProgram(x),
UrlExtension => UrlProgram(x),
_ => Win32Program(x)
}).Where(x => x.Valid);
return programs;
@ -504,7 +549,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var programs = Enumerable.Empty<Win32>();
var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes);
var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.GetSuffixes());
programs = programs.Concat(unregistered);
@ -512,13 +557,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (settings.EnableRegistrySource)
{
var appPaths = AppPathsPrograms(settings.ProgramSuffixes);
var appPaths = AppPathsPrograms(settings.GetSuffixes());
autoIndexPrograms = autoIndexPrograms.Concat(appPaths);
}
if (settings.EnableStartMenuSource)
{
var startMenu = StartMenuPrograms(settings.ProgramSuffixes);
var startMenu = StartMenuPrograms(settings.GetSuffixes());
autoIndexPrograms = autoIndexPrograms.Concat(startMenu);
}

View file

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Plugin.Program
{
@ -9,17 +11,87 @@ namespace Flow.Launcher.Plugin.Program
public DateTime LastIndexTime { get; set; }
public List<ProgramSource> ProgramSources { get; set; } = new List<ProgramSource>();
public List<DisabledProgramSource> DisabledProgramSources { get; set; } = new List<DisabledProgramSource>();
public string[] ProgramSuffixes { get; set; } = {"appref-ms", "exe", "lnk"};
public string[] CustomSuffixes { get; set; } = Array.Empty<string>();
public string[] CustomProtocols { get; set; } = Array.Empty<string>();
public Dictionary<string, bool> BuiltinSuffixesStatus { get; set; } = new Dictionary<string, bool>{
{ "exe", true }, { "appref-ms", true }, { "lnk", true }
};
public Dictionary<string, bool> BuiltinProtocolsStatus { get; set; } = new Dictionary<string, bool>{
{ "steam", true }, { "epic", true }, { "http", false }
};
[JsonIgnore]
public Dictionary<string, string> BuiltinProtocols { get; set; } = new Dictionary<string, string>{
{ "steam", $"steam://run/{SuffixSeparator}steam://rungameid/" }, { "epic", "com.epicgames.launcher://apps/" }, { "http", $"http://{SuffixSeparator}https://"}
};
public bool UseCustomSuffixes { get; set; } = false;
public bool UseCustomProtocols { get; set; } = false;
public string[] GetSuffixes()
{
List<string> extensions = new List<string>();
foreach (var item in BuiltinSuffixesStatus)
{
if (item.Value)
{
extensions.Add(item.Key);
}
}
if (BuiltinProtocolsStatus.Values.Any(x => x == true) || UseCustomProtocols)
{
extensions.Add("url");
}
if (UseCustomSuffixes)
{
return extensions.Concat(CustomSuffixes).DistinctBy(x => x.ToLower()).ToArray();
}
else
{
return extensions.DistinctBy(x => x.ToLower()).ToArray();
}
}
public string[] GetProtocols()
{
List<string> protocols = new List<string>();
foreach (var item in BuiltinProtocolsStatus)
{
if (item.Value)
{
if (BuiltinProtocols.TryGetValue(item.Key, out string ps))
{
var tmp = ps.Split(SuffixSeparator, StringSplitOptions.RemoveEmptyEntries);
foreach (var protocol in tmp)
{
protocols.Add(protocol);
}
}
}
}
if (UseCustomProtocols)
{
return protocols.Concat(CustomProtocols).DistinctBy(x => x.ToLower()).ToArray();
}
else
{
return protocols.DistinctBy(x => x.ToLower()).ToArray();
}
}
public bool EnableStartMenuSource { get; set; } = true;
public bool EnableDescription { get; set; } = false;
public bool HideAppsPath { get; set; } = true;
public bool EnableRegistrySource { get; set; } = true;
public string CustomizedExplorer { get; set; } = Explorer;
public string CustomizedArgs { get; set; } = ExplorerArgs;
internal const char SuffixSeperator = ';';
internal const char SuffixSeparator = ';';
internal const string Explorer = "explorer";

View file

@ -12,7 +12,7 @@ namespace Flow.Launcher.Plugin.Program
var text = value as string[];
if (text != null)
{
return string.Join(";", text);
return string.Join(Settings.SuffixSeparator, text);
}
else
{