Merge Dev

This commit is contained in:
DB p 2022-12-01 16:36:51 +09:00
commit bf9b464be8
23 changed files with 482 additions and 632 deletions

View file

@ -55,7 +55,7 @@
<ItemGroup>
<PackageReference Include="Droplex" Version="1.4.1" />
<PackageReference Include="FSharp.Core" Version="6.0.6" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
</ItemGroup>

View file

@ -241,7 +241,7 @@ namespace Flow.Launcher.Plugin
public string ProgressBarColor { get; set; } = "#26a0da";
/// <summary>
/// Suggests a result's file extension should use full width of the default preview panel.
/// Suggests the preview image of result should use full width of the default preview panel by result's file extension.
/// </summary>
/// <param name="extension">File extension. Dot included.</param>
public static bool ShouldUseBigThumbnail(string extension)

View file

@ -1,30 +0,0 @@
using Flow.Launcher.Plugin.Program.Programs;
using NUnit.Framework;
using System;
using Windows.ApplicationModel;
namespace Flow.Launcher.Test.Plugins
{
[TestFixture]
public class ProgramTest
{
[TestCase("Microsoft.WindowsCamera", "ms-resource:LensSDK/Resources/AppTitle", "ms-resource://Microsoft.WindowsCamera/LensSDK/Resources/AppTitle")]
[TestCase("microsoft.windowscommunicationsapps", "ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName",
"ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName")]
[TestCase("windows.immersivecontrolpanel", "ms-resource:DisplayName", "ms-resource://windows.immersivecontrolpanel/Resources/DisplayName")]
[TestCase("Microsoft.MSPaint", "ms-resource:AppName", "ms-resource://Microsoft.MSPaint/Resources/AppName")]
public void WhenGivenPriReferenceValueShouldReturnCorrectFormat(string packageName, string rawPriReferenceValue, string expectedFormat)
{
// Arrange
var app = new UWP.Application();
// Act
var result = UWP.Application.FormattedPriReferenceValue(packageName, rawPriReferenceValue);
// Assert
Assert.IsTrue(result == expectedFormat,
$"Expected Pri reference format: {expectedFormat}{Environment.NewLine} " +
$"Actual: {result}{Environment.NewLine}");
}
}
}

View file

@ -80,7 +80,7 @@ Global
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|Any CPU

View file

@ -118,6 +118,14 @@
<system:String x:Key="browserMoreThemes">Theme Gallery</system:String>
<system:String x:Key="howToCreateTheme">How to create a theme</system:String>
<system:String x:Key="hiThere">Hi There</system:String>
<system:String x:Key="SampleTitleExplorer">Explorer</system:String>
<system:String x:Key="SampleSubTitleExplorer">Search for files, folders and file contents</system:String>
<system:String x:Key="SampleTitleWebSearch">WebSearch</system:String>
<system:String x:Key="SampleSubTitleWebSearch">Search the web with different search engine support</system:String>
<system:String x:Key="SampleTitleProgram">Program</system:String>
<system:String x:Key="SampleSubTitleProgram">Launch programs as admin or a different user</system:String>
<system:String x:Key="SampleTitleProcessKiller">ProcessKiller</system:String>
<system:String x:Key="SampleSubTitleProcessKiller">Terminate unwanted processes</system:String>
<system:String x:Key="queryBoxFont">Query Box Font</system:String>
<system:String x:Key="resultItemFont">Result Item Font</system:String>
<system:String x:Key="windowMode">Window Mode</system:String>

View file

@ -443,10 +443,9 @@
Margin="2,2,2,0"
Panel.ZIndex="1"
Background="Transparent"
IsItemsHost="true"
IsItemsHost="true"
LastChildFill="False" />
<Border
Grid.Column="1">
<Border Grid.Column="1">
<ContentPresenter Grid.Column="1" ContentSource="SelectedContent" />
</Border>
</Grid>
@ -1011,6 +1010,7 @@
TextAlignment="Left" />
<DockPanel DockPanel.Dock="Right">
<TextBox
Loaded="Plugin_GotFocus"
Name="pluginFilterTxb"
Width="150"
Height="34"
@ -1445,6 +1445,7 @@
DockPanel.Dock="Right"
FontSize="14"
KeyDown="PluginStoreFilterTxb_OnKeyDown"
Loaded="PluginStore_GotFocus"
LostFocus="RefreshPluginStoreEventHandler"
Text=""
TextAlignment="Left"
@ -1795,7 +1796,12 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Border Width="{Binding WindowWidthSize}" Style="{DynamicResource WindowBorderStyle}">
<Border
Width="{Binding WindowWidthSize}"
Margin="0"
SnapsToDevicePixels="True"
Style="{DynamicResource WindowBorderStyle}"
UseLayoutRounding="True">
<Border Style="{DynamicResource WindowRadius}">
<Border.Clip>
<MultiBinding Converter="{StaticResource BorderClipConverter}">
@ -1813,6 +1819,7 @@
<Border Grid.Row="0">
<TextBox
x:Name="QueryTextBox"
IsHitTestVisible="False"
IsReadOnly="True"
Style="{DynamicResource QueryBoxStyle}"
Text="{DynamicResource hiThere}" />
@ -1850,7 +1857,10 @@
</Border>
<ContentControl Grid.Row="2">
<flowlauncher:ResultListBox DataContext="{Binding PreviewResults, Mode=OneTime}" Visibility="Visible" />
<flowlauncher:ResultListBox
DataContext="{Binding PreviewResults, Mode=OneTime}"
IsHitTestVisible="False"
Visibility="Visible" />
</ContentControl>
</Grid>

View file

@ -562,5 +562,15 @@ namespace Flow.Launcher
};
}
private void PluginStore_GotFocus(object sender, RoutedEventArgs e)
{
Keyboard.Focus(pluginStoreFilterTxb);
}
private void Plugin_GotFocus(object sender, RoutedEventArgs e)
{
Keyboard.Focus(pluginFilterTxb);
}
}
}

View file

@ -195,14 +195,32 @@ namespace Flow.Launcher.ViewModel
private async Task LoadImageAsync()
{
// We need to modify the property not field here to trigger the OnPropertyChanged event
Image = await LoadImageInternalAsync(Result.IcoPath, Result.Icon, false).ConfigureAwait(false);
var imagePath = Result.IcoPath;
var iconDelegate = Result.Icon;
if (ImageLoader.CacheContainImage(imagePath, false))
{
image = await LoadImageInternalAsync(imagePath, iconDelegate, false).ConfigureAwait(false);
}
else
{
// We need to modify the property not field here to trigger the OnPropertyChanged event
Image = await LoadImageInternalAsync(imagePath, iconDelegate, false).ConfigureAwait(false);
}
}
private async Task LoadPreviewImageAsync()
{
var imagePath = Result.PreviewImage ?? Result.IcoPath;
PreviewImage = await LoadImageInternalAsync(Result.IcoPath, Result.Icon, true).ConfigureAwait(false);
var iconDelegate = Result.Icon;
if (ImageLoader.CacheContainImage(imagePath, true))
{
previewImage = await LoadImageInternalAsync(imagePath, iconDelegate, true).ConfigureAwait(false);
}
else
{
// We need to modify the property not field here to trigger the OnPropertyChanged event
PreviewImage = await LoadImageInternalAsync(imagePath, iconDelegate, true).ConfigureAwait(false);
}
}
public Result Result { get; }

View file

@ -609,26 +609,26 @@ namespace Flow.Launcher.ViewModel
{
new Result
{
Title = "Explorer",
SubTitle = "Search for files, folders and file contents",
Title = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"),
SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"),
IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png")
},
new Result
{
Title = "WebSearch",
SubTitle = "Search the web with different search engine support",
Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"),
SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"),
IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png")
},
new Result
{
Title = "Program",
SubTitle = "Launch programs as admin or a different user",
Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"),
SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"),
IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png")
},
new Result
{
Title = "ProcessKiller",
SubTitle = "Terminate unwanted processes",
Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"),
SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"),
IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png")
}
};

View file

@ -19,21 +19,28 @@ namespace Flow.Launcher.Plugin.BrowserBookmark.Commands
internal static List<Bookmark> LoadAllBookmarks(Settings setting)
{
var chromeBookmarks = new ChromeBookmarkLoader();
var mozBookmarks = new FirefoxBookmarkLoader();
var edgeBookmarks = new EdgeBookmarkLoader();
var allBookmarks = new List<Bookmark>();
// Add Firefox bookmarks
allBookmarks.AddRange(mozBookmarks.GetBookmarks());
if (setting.LoadChromeBookmark)
{
// Add Chrome bookmarks
var chromeBookmarks = new ChromeBookmarkLoader();
allBookmarks.AddRange(chromeBookmarks.GetBookmarks());
}
// Add Chrome bookmarks
allBookmarks.AddRange(chromeBookmarks.GetBookmarks());
if (setting.LoadFirefoxBookmark)
{
// Add Firefox bookmarks
var mozBookmarks = new FirefoxBookmarkLoader();
allBookmarks.AddRange(mozBookmarks.GetBookmarks());
}
// Add Edge (Chromium) bookmarks
allBookmarks.AddRange(edgeBookmarks.GetBookmarks());
if (setting.LoadEdgeBookmark)
{
// Add Edge (Chromium) bookmarks
var edgeBookmarks = new EdgeBookmarkLoader();
allBookmarks.AddRange(edgeBookmarks.GetBookmarks());
}
foreach (var browser in setting.CustomChromiumBrowsers)
{

View file

@ -21,4 +21,5 @@
<system:String x:Key="flowlauncher_plugin_browserbookmark_browserBookmarkDataDirectory">Data Directory Path</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_addBrowserBookmark">Add</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_removeBrowserBookmark">Delete</system:String>
<system:String x:Key="flowlauncher_plugin_browserbookmark_others">Others</system:String>
</ResourceDictionary>

View file

@ -10,6 +10,10 @@ namespace Flow.Launcher.Plugin.BrowserBookmark.Models
public string BrowserPath { get; set; }
public bool LoadChromeBookmark { get; set; } = true;
public bool LoadFirefoxBookmark { get; set; } = true;
public bool LoadEdgeBookmark { get; set; } = true;
public ObservableCollection<CustomBrowser> CustomChromiumBrowsers { get; set; } = new();
}
}

View file

@ -1,47 +1,66 @@
<UserControl
x:Class="Flow.Launcher.Plugin.BrowserBookmark.Views.SettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
d:DesignHeight="300"
d:DesignWidth="500"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d">
<Grid Margin="10">
x:Class="Flow.Launcher.Plugin.BrowserBookmark.Views.SettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
d:DesignHeight="300"
d:DesignWidth="500"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d">
<Grid Margin="60,0,10,0">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<StackPanel Margin="0,0,0,0" Orientation="Vertical">
<TextBlock Margin="10" Text="{DynamicResource flowlauncher_plugin_browserbookmark_loadBrowserFrom}" />
<ListView
Name="CustomBrowsers"
Height="auto"
Margin="10"
BorderBrush="DarkGray"
BorderThickness="1"
ItemsSource="{Binding Settings.CustomChromiumBrowsers}"
MouseDoubleClick="MouseDoubleClickOnSelectedCustomBrowser"
SelectedItem="{Binding SelectedCustomBrowser}"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name, Mode=OneWay}" Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserName}" />
<GridViewColumn DisplayMemberBinding="{Binding DataDirectoryPath, Mode=OneWay}" Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserBookmarkDataDirectory}" />
</GridView>
</ListView.View>
</ListView>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<StackPanel Margin="0,10,0,10" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10" Text="{DynamicResource flowlauncher_plugin_browserbookmark_loadBrowserFrom}"/>
<CheckBox Margin="0,0,15,0"
Content="Chrome"
IsChecked="{Binding Settings.LoadChromeBookmark}"/>
<CheckBox Margin="0,0,15,0"
Content="Edge"
IsChecked="{Binding Settings.LoadEdgeBookmark}"/>
<CheckBox Margin="0,0,15,0"
Content="Firefox"
IsChecked="{Binding Settings.LoadFirefoxBookmark}"/>
<Button
MinWidth="120"
Margin="10"
Click="NewCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_addBrowserBookmark}" />
<Button
MinWidth="120"
Margin="10"
Click="DeleteCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_removeBrowserBookmark}" />
Margin="0,0,15,0"
Click="Others_Click"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_others}"/>
</StackPanel>
<StackPanel Name="CustomBrowsersList" Visibility="Collapsed">
<ListView
Name="CustomBrowsers"
Height="auto"
Margin="10"
BorderBrush="DarkGray"
BorderThickness="1"
ItemsSource="{Binding Settings.CustomChromiumBrowsers}"
MouseDoubleClick="MouseDoubleClickOnSelectedCustomBrowser"
SelectedItem="{Binding SelectedCustomBrowser}"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name, Mode=OneWay}"
Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserName}"/>
<GridViewColumn DisplayMemberBinding="{Binding DataDirectoryPath, Mode=OneWay}"
Header="{DynamicResource flowlauncher_plugin_browserbookmark_browserBookmarkDataDirectory}"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
MinWidth="130"
Margin="10"
Click="NewCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_addBrowserBookmark}"/>
<Button
MinWidth="120"
Margin="10"
Click="DeleteCustomBrowser"
Content="{DynamicResource flowlauncher_plugin_browserbookmark_removeBrowserBookmark}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>

View file

@ -1,7 +1,8 @@
using System.Windows;
using System.Windows;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
using System.Windows.Input;
using System.ComponentModel;
using System.Windows.Controls;
namespace Flow.Launcher.Plugin.BrowserBookmark.Views
{
@ -59,5 +60,15 @@ namespace Flow.Launcher.Plugin.BrowserBookmark.Views
var window = new CustomBrowserSettingWindow(SelectedCustomBrowser);
window.ShowDialog();
}
private void Others_Click(object sender, RoutedEventArgs e)
{
if (CustomBrowsersList.Visibility == Visibility.Collapsed)
{
CustomBrowsersList.Visibility = Visibility.Visible;
}
else
CustomBrowsersList.Visibility = Visibility.Collapsed;
}
}
}

View file

@ -1,14 +1,16 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<!--Dialogues-->
<!-- Dialogues -->
<system:String x:Key="plugin_pluginsmanager_downloading_plugin">Downloading plugin</system:String>
<system:String x:Key="plugin_pluginsmanager_download_success">Successfully downloaded</system:String>
<system:String x:Key="plugin_pluginsmanager_download_error">Error: Unable to download the plugin</system:String>
<system:String x:Key="plugin_pluginsmanager_uninstall_prompt">{0} by {1} {2}{3}Would you like to uninstall this plugin? After the uninstallation Flow will automatically restart.</system:String>
<system:String x:Key="plugin_pluginsmanager_install_prompt">{0} by {1} {2}{3}Would you like to install this plugin? After the installation Flow will automatically restart.</system:String>
<system:String x:Key="plugin_pluginsmanager_install_title">Plugin Install</system:String>
<system:String x:Key="plugin_pluginsmanager_installing_plugin">Installing Plugin</system:String>
<system:String x:Key="plugin_pluginsmanager_install_from_web">Download and install {0}</system:String>
<system:String x:Key="plugin_pluginsmanager_uninstall_title">Plugin Uninstall</system:String>
<system:String x:Key="plugin_pluginsmanager_install_success_restart">Plugin successfully installed. Restarting Flow, please wait...</system:String>
@ -27,14 +29,14 @@
<system:String x:Key="plugin_pluginsmanager_install_unknown_source_warning_title">Installing from an unknown source</system:String>
<system:String x:Key="plugin_pluginsmanager_install_unknown_source_warning">You are installing this plugin from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning via settings)</system:String>
<!--Controls-->
<!--Plugin Infos-->
<!-- Controls -->
<!-- Plugin Infos -->
<system:String x:Key="plugin_pluginsmanager_plugin_name">Plugins Manager</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_description">Management of installing, uninstalling or updating Flow Launcher plugins</system:String>
<system:String x:Key="plugin_pluginsmanager_unknown_author">Unknown Author</system:String>
<!--Context menu items-->
<!-- Context menu items -->
<system:String x:Key="plugin_pluginsmanager_plugin_contextmenu_openwebsite_title">Open website</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_contextmenu_openwebsite_subtitle">Visit the plugin's website</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_contextmenu_gotosourcecode_title">See source code</system:String>
@ -44,6 +46,6 @@
<system:String x:Key="plugin_pluginsmanager_plugin_contextmenu_pluginsmanifest_title">Go to Flow's plugins repository</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_contextmenu_pluginsmanifest_subtitle">Visit the PluginsManifest repository to see community-made plugin submissions</system:String>
<!--Settings menu items-->
<!-- Settings menu items -->
<system:String x:Key="plugin_pluginsmanager_plugin_settings_unknown_source">Install from unknown source warning</system:String>
</ResourceDictionary>

View file

@ -176,7 +176,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
return;
}
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_install_title"),
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_installing_plugin"),
Context.API.GetTranslation("plugin_pluginsmanager_install_success_restart"));
Context.API.RestartApp();

View file

@ -81,7 +81,9 @@
<!-- Dialogs -->
<system:String x:Key="flowlauncher_plugin_program_disable_dlgtitle_success">Success</system:String>
<system:String x:Key="flowlauncher_plugin_program_disable_dlgtitle_error">Error</system:String>
<system:String x:Key="flowlauncher_plugin_program_disable_dlgtitle_success_message">Successfully disabled this program from displaying in your query</system:String>
<system:String x:Key="flowlauncher_plugin_program_run_as_administrator_not_supported_message">This app is not intended to be run as administrator</system:String>
<system:String x:Key="flowlauncher_plugin_program_run_failed">Unable to run {0}</system:String>
</ResourceDictionary>

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.Program.Programs;
@ -225,9 +226,9 @@ namespace Flow.Launcher.Plugin.Program
}
catch (Exception)
{
var name = "Plugin: Program";
var message = $"Unable to start: {info.FileName}";
Context.API.ShowMsg(name, message, string.Empty);
var title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error");
var message = string.Format(Context.API.GetTranslation("flowlauncher_plugin_program_run_failed"), info.FileName);
Context.API.ShowMsg(title, string.Format(message, info.FileName), string.Empty);
}
}

View file

@ -1,42 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Flow.Launcher.Plugin.Program.Programs
{
public class ApplicationActivationHelper
{
// Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs
public enum ActivateOptions
{
None = 0x00000000,
DesignMode = 0x00000001,
NoErrorUI = 0x00000002,
NoSplashScreen = 0x00000004,
}
/// ApplicationActivationManager
[ComImport, Guid("2e941141-7f97-4756-ba1d-9decde894a3d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IApplicationActivationManager
{
IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
}
// Application Activation Manager Class
[ComImport, Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
public class ApplicationActivationManager : IApplicationActivationManager
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/]
public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
}
}
}

View file

@ -1,80 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
namespace Flow.Launcher.Plugin.Program.Programs
{
public class AppxPackageHelper
{
// This function returns a list of attributes of applications
public static List<IAppxManifestApplication> GetAppsFromManifest(IStream stream)
{
IAppxFactory appxFactory = (IAppxFactory)new AppxFactory();
List<IAppxManifestApplication> apps = new List<IAppxManifestApplication>();
var reader = appxFactory.CreateManifestReader(stream);
var manifestApps = reader.GetApplications();
while (manifestApps.GetHasCurrent())
{
string appListEntry;
var manifestApp = manifestApps.GetCurrent();
manifestApp.GetStringValue("AppListEntry", out appListEntry);
if (appListEntry != "none")
{
apps.Add(manifestApp);
}
manifestApps.MoveNext();
}
return apps;
}
// Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application
[Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781"), ComImport]
public class AppxFactory
{
}
[Guid("BEB94909-E451-438B-B5A7-D79E767B75D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxFactory
{
void _VtblGap0_2(); // skip 2 methods
IAppxManifestReader CreateManifestReader(IStream inputStream);
}
[Guid("4E1BD148-55A0-4480-A3D1-15544710637C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestReader
{
void _VtblGap0_1(); // skip 1 method
IAppxManifestProperties GetProperties();
void _VtblGap1_5(); // skip 5 methods
IAppxManifestApplicationsEnumerator GetApplications();
}
[Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestApplicationsEnumerator
{
IAppxManifestApplication GetCurrent();
bool GetHasCurrent();
bool MoveNext();
}
[Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestApplication
{
[PreserveSig]
int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
[PreserveSig]
int GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value);
}
[Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAppxManifestProperties
{
[PreserveSig]
int GetBoolValue([MarshalAs(UnmanagedType.LPWStr)]string name, out bool value);
[PreserveSig]
int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
}
}
}

View file

@ -3,22 +3,17 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Xml.Linq;
using Windows.ApplicationModel;
using Windows.Management.Deployment;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger;
using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger;
using System.Threading.Channels;
using System.Xml;
using Windows.ApplicationModel.Core;
namespace Flow.Launcher.Plugin.Program.Programs
{
@ -30,9 +25,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
public string FamilyName { get; }
public string Location { get; set; }
public Application[] Apps { get; set; }
public Application[] Apps { get; set; } = Array.Empty<Application>();
public PackageVersion Version { get; set; }
public UWP(Package package)
{
@ -40,71 +34,133 @@ namespace Flow.Launcher.Plugin.Program.Programs
Name = package.Id.Name;
FullName = package.Id.FullName;
FamilyName = package.Id.FamilyName;
InitializeAppInfo();
}
private void InitializeAppInfo()
public void InitAppsInPackage(Package package)
{
var path = Path.Combine(Location, "AppxManifest.xml");
var namespaces = XmlNamespaces(path);
InitPackageVersion(namespaces);
const uint noAttribute = 0x80;
const Stgm nonExclusiveRead = Stgm.Read | Stgm.ShareDenyNone;
var hResult = SHCreateStreamOnFileEx(path, nonExclusiveRead, noAttribute, false, null, out IStream stream);
if (hResult == Hresult.Ok)
var applist = new List<Application>();
// WinRT
var appListEntries = package.GetAppListEntries();
foreach (var app in appListEntries)
{
List<AppxPackageHelper.IAppxManifestApplication> _apps = AppxPackageHelper.GetAppsFromManifest(stream);
try
{
var tmp = new Application(app, this);
applist.Add(tmp);
}
catch (Exception e)
{
ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" +
"|Unexpected exception occurs when trying to construct a Application from package"
+ $"{FullName} from location {Location}", e);
}
}
Apps = applist.ToArray();
Apps = _apps.Select(x => new Application(x, this))
.Where(a => !string.IsNullOrEmpty(a.UserModelId)
&& !string.IsNullOrEmpty(a.DisplayName))
.ToArray();
try
{
var xmlDoc = GetManifestXml();
if (xmlDoc == null)
{
return;
}
var xmlRoot = xmlDoc.DocumentElement;
var packageVersion = GetPackageVersionFromManifest(xmlRoot);
if (!smallLogoNameFromVersion.TryGetValue(packageVersion, out string logoName) ||
!bigLogoNameFromVersion.TryGetValue(packageVersion, out string bigLogoName))
{
return;
}
var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
namespaceManager.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); // still need a name
namespaceManager.AddNamespace("rescap", "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities");
namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10");
var allowElevationNode = xmlRoot.SelectSingleNode("//rescap:Capability[@Name='allowElevation']", namespaceManager);
bool packageCanElevate = allowElevationNode != null;
var appsNode = xmlRoot.SelectSingleNode("d:Applications", namespaceManager);
foreach (var app in Apps)
{
// According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package
// and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
var id = app.UserModelId.Split('!')[1];
var appNode = appsNode?.SelectSingleNode($"d:Application[@Id='{id}']", namespaceManager);
if (appNode != null)
{
app.CanRunElevated = packageCanElevate || Application.IfAppCanRunElevated(appNode);
// local name to fit all versions
var visualElement = appNode.SelectSingleNode($"*[local-name()='VisualElements']", namespaceManager);
var logoUri = visualElement?.Attributes[logoName]?.Value;
app.LogoPath = app.LogoPathFromUri(logoUri, (64, 64));
var previewUri = visualElement?.Attributes[bigLogoName]?.Value;
app.PreviewImagePath = app.LogoPathFromUri(previewUri, (128, 128));
}
}
}
catch (Exception e)
{
ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" +
"|Unexpected exception occurs when trying to construct a Application from package"
+ $"{FullName} from location {Location}", e);
}
}
private XmlDocument GetManifestXml()
{
var manifest = Path.Combine(Location, "AppxManifest.xml");
try
{
var file = File.ReadAllText(manifest);
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(file);
return xmlDoc;
}
catch (FileNotFoundException e)
{
ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "AppxManifest.xml not found.", e);
return null;
}
catch (Exception e)
{
ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "An unexpected error occured and unable to parse AppxManifest.xml", e);
return null;
}
}
private PackageVersion GetPackageVersionFromManifest(XmlNode xmlRoot)
{
if (xmlRoot != null)
{
var namespaces = xmlRoot.Attributes;
foreach (XmlAttribute ns in namespaces)
{
if (versionFromNamespace.TryGetValue(ns.Value, out var packageVersion))
{
return packageVersion;
}
}
ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" +
"|Trying to get the package version of the UWP program, but an unknown UWP appmanifest version in package "
+ $"{FullName} from location {Location}", new FormatException());
return PackageVersion.Unknown;
}
else
{
var e = Marshal.GetExceptionForHR((int)hResult);
ProgramLogger.LogException($"|UWP|InitializeAppInfo|{path}" +
"|Error caused while trying to get the details of the UWP program", e);
Apps = Array.Empty<Application>();
}
if (stream != null && Marshal.ReleaseComObject(stream) > 0)
{
Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked");
ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" +
"|Can't parse AppManifest.xml of package "
+ $"{FullName} from location {Location}", new ArgumentNullException(nameof(xmlRoot)));
return PackageVersion.Unknown;
}
}
/// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx
private static string[] XmlNamespaces(string path)
private static readonly Dictionary<string, PackageVersion> versionFromNamespace = new()
{
XDocument z = XDocument.Load(path);
if (z.Root != null)
{
var namespaces = z.Root.Attributes().Where(a => a.IsNamespaceDeclaration).GroupBy(
a => a.Name.Namespace == XNamespace.None ? string.Empty : a.Name.LocalName,
a => XNamespace.Get(a.Value)
).Select(
g => g.First().ToString()
).ToArray();
return namespaces;
}
else
{
ProgramLogger.LogException($"|UWP|XmlNamespaces|{path}" +
$"|Error occured while trying to get the XML from {path}", new ArgumentNullException());
return Array.Empty<string>();
}
}
private void InitPackageVersion(string[] namespaces)
{
var versionFromNamespace = new Dictionary<string, PackageVersion>
{
{
"http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10
},
@ -114,23 +170,33 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
"http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8
},
};
};
foreach (var n in versionFromNamespace.Keys)
private static readonly Dictionary<PackageVersion, string> smallLogoNameFromVersion = new()
{
{
if (namespaces.Contains(n))
{
Version = versionFromNamespace[n];
return;
}
}
PackageVersion.Windows10, "Square44x44Logo"
},
{
PackageVersion.Windows81, "Square30x30Logo"
},
{
PackageVersion.Windows8, "SmallLogo"
},
};
ProgramLogger.LogException($"|UWP|XmlNamespaces|{Location}" +
"|Trying to get the package version of the UWP program, but a unknown UWP appmanifest version "
+ $"{FullName} from location {Location} is returned.", new FormatException());
Version = PackageVersion.Unknown;
}
private static readonly Dictionary<PackageVersion, string> bigLogoNameFromVersion = new()
{
{
PackageVersion.Windows10, "Square150x150Logo"
},
{
PackageVersion.Windows81, "Square150x150Logo"
},
{
PackageVersion.Windows8, "Logo"
},
};
public static Application[] All()
{
@ -144,6 +210,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
try
{
u = new UWP(p);
u.InitAppsInPackage(p);
}
#if !DEBUG
catch (Exception e)
@ -194,22 +261,19 @@ namespace Flow.Launcher.Plugin.Program.Programs
var ps = m.FindPackagesForUser(id);
ps = ps.Where(p =>
{
bool valid;
try
{
var f = p.IsFramework;
var d = p.IsDevelopmentMode;
var path = p.InstalledLocation.Path;
valid = !f && !d && !string.IsNullOrEmpty(path);
return !f && !d && !string.IsNullOrEmpty(path);
}
catch (Exception e)
{
ProgramLogger.LogException("UWP", "CurrentUserPackages", $"id", "An unexpected error occured and "
ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{id}", "An unexpected error occured and "
+ $"unable to verify if package is valid", e);
return false;
}
return valid;
});
return ps;
}
@ -248,7 +312,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
PackageChangeChannel.Reader.TryRead(out _);
await Task.Run(Main.IndexUwpPrograms);
}
}
}
@ -279,23 +343,27 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
private string _uid = string.Empty;
public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); }
public string DisplayName { get; set; }
public string Description { get; set; }
public string UserModelId { get; set; }
public string BackgroundColor { get; set; }
public string EntryPoint { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string UserModelId { get; set; } = string.Empty;
//public string BackgroundColor { get; set; } = string.Empty; // preserve for future use
public string Name => DisplayName;
public string Location => Package.Location;
public string Location { get; set; } = string.Empty;
public bool Enabled { get; set; }
public bool CanRunElevated { get; set; }
public bool Enabled { get; set; } = false;
public bool CanRunElevated { get; set; } = false;
public string LogoPath { get; set; } = string.Empty;
public string PreviewImagePath { get; set; } = string.Empty;
public string LogoUri { get; set; }
public string LogoPath { get; set; }
public UWP Package { get; set; }
public Application() { }
public Application(AppListEntry appListEntry, UWP package)
{
UserModelId = appListEntry.AppUserModelId;
UniqueIdentifier = appListEntry.AppUserModelId;
DisplayName = appListEntry.DisplayInfo.DisplayName;
Description = appListEntry.DisplayInfo.Description;
Location = package.Location;
Enabled = true;
}
public Result Result(string query, IPublicAPI api)
{
@ -335,8 +403,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
var result = new Result
{
Title = title,
SubTitle = Main._settings.HideAppsPath ? string.Empty : Package.Location,
Icon = Logo,
SubTitle = Main._settings.HideAppsPath ? string.Empty : Location,
IcoPath = LogoPath,
Score = matchResult.Score,
TitleHighlightData = matchResult.MatchData,
ContextData = this,
@ -349,20 +417,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
!e.SpecialKeyState.WinPressed
);
if (elevated && CanRunElevated)
bool shouldRunElevated = elevated && CanRunElevated;
_ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false);
if (elevated && !shouldRunElevated)
{
LaunchElevated();
}
else
{
Launch(api);
if (elevated)
{
var title = "Plugin: Program";
var message = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator_not_supported_message");
api.ShowMsg(title, message, string.Empty);
}
var title = api.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error");
var message = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator_not_supported_message");
api.ShowMsg(title, message, string.Empty);
}
return true;
@ -382,7 +443,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
Action = _ =>
{
Main.Context.API.OpenDirectory(Package.Location);
Main.Context.API.OpenDirectory(Location);
return true;
},
@ -397,7 +458,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"),
Action = _ =>
{
LaunchElevated();
Task.Run(() => Launch(true)).ConfigureAwait(false);
return true;
},
IcoPath = "Images/cmd.png"
@ -407,192 +468,61 @@ namespace Flow.Launcher.Plugin.Program.Programs
return contextMenus;
}
private async void Launch(IPublicAPI api)
private void Launch(bool elevated = false)
{
var appManager = new ApplicationActivationHelper.ApplicationActivationManager();
const string noArgs = "";
const ApplicationActivationHelper.ActivateOptions noFlags = ApplicationActivationHelper.ActivateOptions.None;
await Task.Run(() =>
{
try
{
_ = appManager.ActivateApplication(UserModelId, noArgs, noFlags, out _);
}
catch (Exception)
{
var name = "Plugin: Program";
var message = $"Can't start UWP: {DisplayName}";
api.ShowMsg(name, message, string.Empty);
}
});
}
private void LaunchElevated()
{
string command = "shell:AppsFolder\\" + UniqueIdentifier;
string command = "shell:AppsFolder\\" + UserModelId;
command = Environment.ExpandEnvironmentVariables(command.Trim());
var info = new ProcessStartInfo(command)
{
UseShellExecute = true, Verb = "runas",
UseShellExecute = true,
Verb = elevated ? "runas" : ""
};
Main.StartProcess(Process.Start, info);
}
public Application(AppxPackageHelper.IAppxManifestApplication manifestApp, UWP package)
internal static bool IfAppCanRunElevated(XmlNode appNode)
{
// This is done because we cannot use the keyword 'out' along with a property
// According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package
// and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
manifestApp.GetAppUserModelId(out string tmpUserModelId);
manifestApp.GetAppUserModelId(out string tmpUniqueIdentifier);
manifestApp.GetStringValue("DisplayName", out string tmpDisplayName);
manifestApp.GetStringValue("Description", out string tmpDescription);
manifestApp.GetStringValue("BackgroundColor", out string tmpBackgroundColor);
manifestApp.GetStringValue("EntryPoint", out string tmpEntryPoint);
UserModelId = tmpUserModelId;
UniqueIdentifier = tmpUniqueIdentifier;
DisplayName = tmpDisplayName;
Description = tmpDescription;
BackgroundColor = tmpBackgroundColor;
EntryPoint = tmpEntryPoint;
Package = package;
DisplayName = ResourceFromPri(package.FullName, package.Name, DisplayName);
Description = ResourceFromPri(package.FullName, package.Name, Description);
LogoUri = LogoUriFromManifest(manifestApp);
LogoPath = LogoPathFromUri(LogoUri);
Enabled = true;
CanRunElevated = CanApplicationRunElevated();
return appNode?.Attributes["EntryPoint"]?.Value == "Windows.FullTrustApplication" ||
appNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL";
}
private bool CanApplicationRunElevated()
{
if (EntryPoint == "Windows.FullTrustApplication")
{
return true;
}
var manifest = Package.Location + "\\AppxManifest.xml";
if (File.Exists(manifest))
{
var file = File.ReadAllText(manifest);
if (file.Contains("TrustLevel=\"mediumIL\"", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
internal string ResourceFromPri(string packageFullName, string packageName, string rawReferenceValue)
{
if (string.IsNullOrWhiteSpace(rawReferenceValue) || !rawReferenceValue.StartsWith("ms-resource:"))
return rawReferenceValue;
var formattedPriReference = FormattedPriReferenceValue(packageName, rawReferenceValue);
var outBuffer = new StringBuilder(128);
string source = $"@{{{packageFullName}? {formattedPriReference}}}";
var capacity = (uint)outBuffer.Capacity;
var hResult = SHLoadIndirectString(source, outBuffer, capacity, IntPtr.Zero);
if (hResult == Hresult.Ok)
{
var loaded = outBuffer.ToString();
if (!string.IsNullOrEmpty(loaded))
{
return loaded;
}
else
{
ProgramLogger.LogException($"|UWP|ResourceFromPri|{Package.Location}|Can't load null or empty result "
+ $"pri {source} in uwp location {Package.Location}", new NullReferenceException());
return string.Empty;
}
}
else
{
var e = Marshal.GetExceptionForHR((int)hResult);
ProgramLogger.LogException($"|UWP|ResourceFromPri|{Package.Location}|Load pri failed {source} with HResult {hResult} and location {Package.Location}", e);
return string.Empty;
}
}
public static string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue)
{
const string prefix = "ms-resource:";
if (string.IsNullOrWhiteSpace(rawPriReferenceValue) || !rawPriReferenceValue.StartsWith(prefix))
return rawPriReferenceValue;
string key = rawPriReferenceValue.Substring(prefix.Length);
if (key.StartsWith("//"))
return $"{prefix}{key}";
if (!key.StartsWith("/"))
{
key = $"/{key}";
}
if (!key.ToLower().Contains("resources"))
{
key = $"/Resources{key}";
}
return $"{prefix}//{packageName}{key}";
}
internal string LogoUriFromManifest(AppxPackageHelper.IAppxManifestApplication app)
{
var logoKeyFromVersion = new Dictionary<PackageVersion, string>
{
{
PackageVersion.Windows10, "Square44x44Logo"
},
{
PackageVersion.Windows81, "Square30x30Logo"
},
{
PackageVersion.Windows8, "SmallLogo"
},
};
if (logoKeyFromVersion.ContainsKey(Package.Version))
{
var key = logoKeyFromVersion[Package.Version];
_ = app.GetStringValue(key, out string logoUri);
return logoUri;
}
else
{
return string.Empty;
}
}
internal string LogoPathFromUri(string uri)
internal string LogoPathFromUri(string uri, (int, int) desiredSize)
{
// all https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
// windows 10 https://msdn.microsoft.com/en-us/library/windows/apps/dn934817.aspx
// windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size
// windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx
string path = Path.Combine(Package.Location, uri);
var logoPath = TryToFindLogo(uri, path);
if (String.IsNullOrEmpty(logoPath))
if (string.IsNullOrWhiteSpace(uri))
{
// TODO: Don't know why, just keep it at the moment
// Maybe on older version of Windows 10?
// for C:\Windows\MiracastView etc
return TryToFindLogo(uri, Path.Combine(Package.Location, "Assets", uri));
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
$"|{UserModelId} 's logo uri is null or empty: {Location}", new ArgumentException("uri"));
return string.Empty;
}
string path = Path.Combine(Location, uri);
var pxCount = desiredSize.Item1 * desiredSize.Item2;
var logoPath = TryToFindLogo(uri, path, pxCount);
if (logoPath == string.Empty)
{
var tmp = Path.Combine(Location, "Assets", uri);
if (!path.Equals(tmp, StringComparison.OrdinalIgnoreCase))
{
// TODO: Don't know why, just keep it at the moment
// Maybe on older version of Windows 10?
// for C:\Windows\MiracastView etc
return TryToFindLogo(uri, tmp, pxCount);
}
}
return logoPath;
string TryToFindLogo(string uri, string path)
string TryToFindLogo(string uri, string path, int px)
{
var extension = Path.GetExtension(path);
if (extension != null)
@ -604,42 +534,38 @@ namespace Flow.Launcher.Plugin.Program.Programs
var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44
var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets
if (String.IsNullOrEmpty(logoNamePrefix) || String.IsNullOrEmpty(logoDir) || !Directory.Exists(logoDir))
if (String.IsNullOrEmpty(logoNamePrefix) || !Directory.Exists(logoDir))
{
// Known issue: Edge always triggers it since logo is not at uri
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
$"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Package.Location}", new FileNotFoundException());
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
$"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Location}", new FileNotFoundException());
return string.Empty;
}
var files = Directory.EnumerateFiles(logoDir);
var logos = Directory.EnumerateFiles(logoDir, $"{logoNamePrefix}*{extension}");
// Currently we don't care which one to choose
// Just ignore all qualifiers
// select like logo.[xxx_yyy].png
// https://learn.microsoft.com/en-us/windows/uwp/app-resources/tailor-resources-lang-scale-contrast
var logos = files.Where(file =>
Path.GetFileName(file)?.StartsWith(logoNamePrefix, StringComparison.OrdinalIgnoreCase) ?? false
&& extension.Equals(Path.GetExtension(file), StringComparison.OrdinalIgnoreCase)
);
var selected = logos.FirstOrDefault();
var closest = selected;
int min = int.MaxValue;
foreach(var logo in logos)
foreach (var logo in logos)
{
var imageStream = File.OpenRead(logo);
var decoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.None);
var height = decoder.Frames[0].PixelHeight;
var width = decoder.Frames[0].PixelWidth;
int pixelCountDiff = Math.Abs(height * width - 1936); // 44*44=1936
if(pixelCountDiff < min)
int pixelCountDiff = Math.Abs(height * width - px);
if (pixelCountDiff < min)
{
// try to find the closest to 44x44 logo
// try to find the closest to desired size
closest = logo;
if (pixelCountDiff == 0)
break; // found 44x44
break; // found
min = pixelCountDiff;
}
}
@ -651,111 +577,112 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
else
{
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
$"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Package.Location}", new FileNotFoundException());
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
$"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Location}", new FileNotFoundException());
return string.Empty;
}
}
else
{
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
$"|Unable to find extension from {uri} for {UserModelId} " +
$"in package location {Package.Location}", new FileNotFoundException());
$"in package location {Location}", new FileNotFoundException());
return string.Empty;
}
}
}
public ImageSource Logo()
{
var logo = ImageFromPath(LogoPath);
var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package?
#region logo legacy
// preserve for potential future use
// todo magic! temp fix for cross thread object
plated.Freeze();
return plated;
}
//public ImageSource Logo()
//{
// var logo = ImageFromPath(LogoPath);
// var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package?
// // todo magic! temp fix for cross thread object
// plated.Freeze();
// return plated;
//}
//private BitmapImage ImageFromPath(string path)
//{
// if (File.Exists(path))
// {
// var image = new BitmapImage();
// image.BeginInit();
// image.UriSource = new Uri(path);
// image.CacheOption = BitmapCacheOption.OnLoad;
// image.EndInit();
// image.Freeze();
// return image;
// }
// else
// {
// ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Avaliable" : path)}" +
// $"|Unable to get logo for {UserModelId} from {path} and" +
// $" located in {Location}", new FileNotFoundException());
// return new BitmapImage(new Uri(Constant.MissingImgIcon));
// }
//}
private BitmapImage ImageFromPath(string path)
{
// TODO: Consider using infrastructure.image.imageloader?
if (File.Exists(path))
{
var image = new BitmapImage();
image.BeginInit();
image.UriSource = new Uri(path);
image.CacheOption = BitmapCacheOption.OnLoad;
image.EndInit();
image.Freeze();
return image;
}
else
{
ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Avaliable" : path)}" +
$"|Unable to get logo for {UserModelId} from {path} and" +
$" located in {Package.Location}", new FileNotFoundException());
return new BitmapImage(new Uri(Constant.MissingImgIcon));
}
}
//private ImageSource PlatedImage(BitmapImage image)
//{
// if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent")
// {
// var width = image.Width;
// var height = image.Height;
// var x = 0;
// var y = 0;
private ImageSource PlatedImage(BitmapImage image)
{
if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent")
{
var width = image.Width;
var height = image.Height;
var x = 0;
var y = 0;
// var group = new DrawingGroup();
var group = new DrawingGroup();
// var converted = ColorConverter.ConvertFromString(BackgroundColor);
// if (converted != null)
// {
// var color = (Color)converted;
// var brush = new SolidColorBrush(color);
// var pen = new Pen(brush, 1);
// var backgroundArea = new Rect(0, 0, width, width);
// var rectabgle = new RectangleGeometry(backgroundArea);
// var rectDrawing = new GeometryDrawing(brush, pen, rectabgle);
// group.Children.Add(rectDrawing);
var converted = ColorConverter.ConvertFromString(BackgroundColor);
if (converted != null)
{
var color = (Color)converted;
var brush = new SolidColorBrush(color);
var pen = new Pen(brush, 1);
var backgroundArea = new Rect(0, 0, width, width);
var rectabgle = new RectangleGeometry(backgroundArea);
var rectDrawing = new GeometryDrawing(brush, pen, rectabgle);
group.Children.Add(rectDrawing);
// var imageArea = new Rect(x, y, image.Width, image.Height);
// var imageDrawing = new ImageDrawing(image, imageArea);
// group.Children.Add(imageDrawing);
var imageArea = new Rect(x, y, image.Width, image.Height);
var imageDrawing = new ImageDrawing(image, imageArea);
group.Children.Add(imageDrawing);
// // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
// var visual = new DrawingVisual();
// var context = visual.RenderOpen();
// context.DrawDrawing(group);
// context.Close();
// const int dpiScale100 = 96;
// var bitmap = new RenderTargetBitmap(
// Convert.ToInt32(width), Convert.ToInt32(height),
// dpiScale100, dpiScale100,
// PixelFormats.Pbgra32
// );
// bitmap.Render(visual);
// return bitmap;
// }
// else
// {
// ProgramLogger.LogException($"|UWP|PlatedImage|{Location}" +
// $"|Unable to convert background string {BackgroundColor} " +
// $"to color for {Location}", new InvalidOperationException());
// http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
var visual = new DrawingVisual();
var context = visual.RenderOpen();
context.DrawDrawing(group);
context.Close();
const int dpiScale100 = 96;
var bitmap = new RenderTargetBitmap(
Convert.ToInt32(width), Convert.ToInt32(height),
dpiScale100, dpiScale100,
PixelFormats.Pbgra32
);
bitmap.Render(visual);
return bitmap;
}
else
{
ProgramLogger.LogException($"|UWP|PlatedImage|{Package.Location}" +
$"|Unable to convert background string {BackgroundColor} " +
$"to color for {Package.Location}", new InvalidOperationException());
return new BitmapImage(new Uri(Constant.MissingImgIcon));
}
}
else
{
// todo use windows theme as background
return image;
}
}
// return new BitmapImage(new Uri(Constant.MissingImgIcon));
// }
// }
// else
// {
// // todo use windows theme as background
// return image;
// }
//}
#endregion
public override string ToString()
{
return $"{DisplayName}: {Description}";
@ -786,27 +713,5 @@ namespace Flow.Launcher.Plugin.Program.Programs
Windows8,
Unknown
}
[Flags]
private enum Stgm : uint
{
Read = 0x0,
ShareExclusive = 0x10,
ShareDenyNone = 0x40
}
private enum Hresult : uint
{
Ok = 0x0000,
}
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern Hresult SHCreateStreamOnFileEx(string fileName, Stgm grfMode, uint attributes, bool create,
IStream reserved, out IStream stream);
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern Hresult SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf,
IntPtr ppvReserved);
}
}

View file

@ -143,10 +143,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
FileName = FullPath,
WorkingDirectory = ParentDirectory,
UseShellExecute = true,
Verb = runAsAdmin ? "runas" : null
Verb = runAsAdmin ? "runas" : ""
};
Task.Run(() => Main.StartProcess(Process.Start, info));
_ = Task.Run(() => Main.StartProcess(Process.Start, info));
return true;
}

View file

@ -270,6 +270,10 @@ And you can download <a href="https://github.com/Flow-Launcher/Flow.Launcher/dis
<a href="https://appwrite.io">
<img src='https://appwrite.io/images-ee/press/logo-1.svg' width="25%"/>
</a>
<a href="https://github.com/itsonlyfrans">
<img src='https://avatars.githubusercontent.com/u/46535667?v=4' width="10%"/>
</a>
</p>
<p align="left">