From 83553244d73024b51b7a5bdc6aee40a0ca369ded Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sat, 11 Nov 2023 18:00:03 -0600 Subject: [PATCH 01/14] use memorypack instead of binaryformatter --- .../Flow.Launcher.Infrastructure.csproj | 1 + .../Image/ImageLoader.cs | 70 +- .../Storage/BinaryStorage.cs | 83 +- Flow.Launcher/App.xaml.cs | 13 +- Plugins/Flow.Launcher.Plugin.Program/Main.cs | 35 +- .../Programs/UWP.cs | 737 ----------------- .../Programs/UWPPackage.cs | 752 ++++++++++++++++++ .../Programs/Win32.cs | 46 +- .../Views/ProgramSetting.xaml.cs | 35 +- 9 files changed, 899 insertions(+), 873 deletions(-) delete mode 100644 Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs create mode 100644 Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 2f5259039..b24f069c1 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -53,6 +53,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 203c5646a..add6d4e92 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -15,6 +16,7 @@ namespace Flow.Launcher.Infrastructure.Image public static class ImageLoader { private static readonly ImageCache ImageCache = new(); + private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1); private static BinaryStorage> _storage; private static readonly ConcurrentDictionary GuidToKey = new(); private static IImageHashGenerator _hashGenerator; @@ -25,24 +27,18 @@ namespace Flow.Launcher.Infrastructure.Image public const int FullIconSize = 256; - private static readonly string[] ImageExtensions = - { - ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" - }; + private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" }; - public static void Initialize() + public static async Task InitializeAsync() { _storage = new BinaryStorage>("Image"); _hashGenerator = new ImageHashGenerator(); - var usage = LoadStorageToConcurrentDictionary(); + var usage = await LoadStorageToConcurrentDictionaryAsync(); ImageCache.Initialize(usage.ToDictionary(x => x.Key, x => x.Value)); - foreach (var icon in new[] - { - Constant.DefaultIcon, Constant.MissingImgIcon - }) + foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon }) { ImageSource img = new BitmapImage(new Uri(icon)); img.Freeze(); @@ -58,29 +54,41 @@ namespace Flow.Launcher.Infrastructure.Image await LoadAsync(path, isFullImage); } }); - Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); + Log.Info( + $"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); }); } - public static void Save() + public static async Task Save() { - lock (_storage) + await storageLock.WaitAsync(); + + try { - _storage.Save(ImageCache.Data + _storage.SaveAsync(ImageCache.Data .ToDictionary( x => x.Key, x => x.Value.usage)); } + finally + { + storageLock.Release(); + } } - private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary() + private static async Task> LoadStorageToConcurrentDictionaryAsync() { - lock (_storage) + await storageLock.WaitAsync(); + try { - var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>()); + var loaded = await _storage.TryLoadAsync(new Dictionary<(string, bool), int>()); return new ConcurrentDictionary<(string, bool), int>(loaded); } + finally + { + storageLock.Release(); + } } private class ImageResult @@ -129,6 +137,7 @@ namespace Flow.Launcher.Infrastructure.Image ImageCache[path, loadFullImage] = image; return new ImageResult(image, ImageType.ImageFile); } + if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) { var imageSource = new BitmapImage(new Uri(path)); @@ -158,6 +167,7 @@ namespace Flow.Launcher.Infrastructure.Image return imageResult; } + private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult) { // Download image from url @@ -173,6 +183,7 @@ namespace Flow.Launcher.Infrastructure.Image image.DecodePixelHeight = SmallIconSize; image.DecodePixelWidth = SmallIconSize; } + image.StreamSource = buffer; image.EndInit(); image.StreamSource = null; @@ -188,8 +199,8 @@ namespace Flow.Launcher.Infrastructure.Image if (Directory.Exists(path)) { /* Directories can also have thumbnails instead of shell icons. - * Generating thumbnails for a bunch of folder results while scrolling - * could have a big impact on performance and Flow.Launcher responsibility. + * Generating thumbnails for a bunch of folder results while scrolling + * could have a big impact on performance and Flow.Launcher responsibility. * - Solution: just load the icon */ type = ImageType.Folder; @@ -208,9 +219,9 @@ namespace Flow.Launcher.Infrastructure.Image } else { - /* Although the documentation for GetImage on MSDN indicates that + /* Although the documentation for GetImage on MSDN indicates that * if a thumbnail is available it will return one, this has proved to not - * be the case in many situations while testing. + * be the case in many situations while testing. * - Solution: explicitly pass the ThumbnailOnly flag */ image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly); @@ -236,7 +247,8 @@ namespace Flow.Launcher.Infrastructure.Image return new ImageResult(image, type); } - private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, int size = SmallIconSize) + private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, + int size = SmallIconSize) { return WindowsThumbnailProvider.GetThumbnail( path, @@ -261,17 +273,19 @@ namespace Flow.Launcher.Infrastructure.Image var img = imageResult.ImageSource; if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache) - { // we need to get image hash + { + // we need to get image hash string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null; if (hash != null) { - if (GuidToKey.TryGetValue(hash, out string key)) - { // image already exists + { + // image already exists img = ImageCache[key, loadFullImage] ?? img; } else - { // new guid + { + // new guid GuidToKey[hash] = path; } @@ -289,7 +303,7 @@ namespace Flow.Launcher.Infrastructure.Image BitmapImage image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; - image.UriSource = new Uri(path); + image.UriSource = new Uri(path); image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; image.EndInit(); @@ -314,8 +328,10 @@ namespace Flow.Launcher.Infrastructure.Image resizedHeight.EndInit(); return resizedHeight; } + return resizedWidth; } + return image; } } diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index ea2d42773..a679643fd 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -4,67 +4,65 @@ using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters; using System.Runtime.Serialization.Formatters.Binary; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; +using MemoryPack; namespace Flow.Launcher.Infrastructure.Storage { -#pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete. /// /// Stroage object using binary data /// Normally, it has better performance, but not readable /// + /// + /// It utilize MemoryPack, which means the object must be MemoryPackSerializable + /// https://github.com/Cysharp/MemoryPack + /// public class BinaryStorage { + const string DirectoryName = "Cache"; + + const string FileSuffix = ".cache"; + public BinaryStorage(string filename) { - const string directoryName = "Cache"; - var directoryPath = Path.Combine(DataLocation.DataDirectory(), directoryName); + var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); Helper.ValidateDirectory(directoryPath); - const string fileSuffix = ".cache"; - FilePath = Path.Combine(directoryPath, $"{filename}{fileSuffix}"); + FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); } public string FilePath { get; } - public T TryLoad(T defaultData) + public async ValueTask TryLoadAsync(T defaultData) { if (File.Exists(FilePath)) { if (new FileInfo(FilePath).Length == 0) { Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>"); - Save(defaultData); + await SaveAsync(defaultData); return defaultData; } - using (var stream = new FileStream(FilePath, FileMode.Open)) - { - var d = Deserialize(stream, defaultData); - return d; - } + await using var stream = new FileStream(FilePath, FileMode.Open); + var d = await DeserializeAsync(stream, defaultData); + return d; } else { Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data"); - Save(defaultData); + await SaveAsync(defaultData); return defaultData; } } - private T Deserialize(FileStream stream, T defaultData) + private async ValueTask DeserializeAsync(Stream stream, T defaultData) { - //http://stackoverflow.com/questions/2120055/binaryformatter-deserialize-gives-serializationexception - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; - BinaryFormatter binaryFormatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple - }; - try { - var t = ((T)binaryFormatter.Deserialize(stream)).NonNull(); + var t = await MemoryPackSerializer.DeserializeAsync(stream); return t; } catch (System.Exception e) @@ -72,47 +70,12 @@ namespace Flow.Launcher.Infrastructure.Storage Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); return defaultData; } - finally - { - AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve; - } } - private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + public async ValueTask SaveAsync(T data) { - Assembly ayResult = null; - string sShortAssemblyName = args.Name.Split(',')[0]; - Assembly[] ayAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (Assembly ayAssembly in ayAssemblies) - { - if (sShortAssemblyName == ayAssembly.FullName.Split(',')[0]) - { - ayResult = ayAssembly; - break; - } - } - return ayResult; - } - - public void Save(T data) - { - using (var stream = new FileStream(FilePath, FileMode.Create)) - { - BinaryFormatter binaryFormatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple - }; - - try - { - binaryFormatter.Serialize(stream, data); - } - catch (SerializationException e) - { - Log.Exception($"|BinaryStorage.Save|serialize error for file <{FilePath}>", e); - } - } + await using var stream = new FileStream(FilePath, FileMode.Create); + await MemoryPackSerializer.SerializeAsync(stream, data); } } -#pragma warning restore SYSLIB0011 } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 295dd3e7a..f4a17761f 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -52,12 +52,13 @@ namespace Flow.Launcher { _portable.PreStartCleanUpAfterPortabilityUpdate(); - Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------"); + Log.Info( + "|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------"); Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}"); RegisterAppDomainExceptions(); RegisterDispatcherUnhandledException(); - ImageLoader.Initialize(); + var imageLoadertask = ImageLoader.InitializeAsync(); _settingsVM = new SettingWindowViewModel(_updater, _portable); _settings = _settingsVM.Settings; @@ -78,6 +79,8 @@ namespace Flow.Launcher Http.Proxy = _settings.Proxy; await PluginManager.InitializePluginsAsync(API); + await imageLoadertask; + var window = new MainWindow(_settings, _mainVM); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); @@ -103,7 +106,8 @@ namespace Flow.Launcher AutoUpdates(); API.SaveAppAllSettings(); - Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); + Log.Info( + "|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); }); } @@ -122,7 +126,8 @@ namespace Flow.Launcher // but if it fails (permissions, etc) then don't keep retrying // this also gives the user a visual indication in the Settings widget _settings.StartFlowLauncherOnSystemStartup = false; - Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message); + Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), + e.Message); } } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index ac23534b1..c02573ed8 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -15,24 +15,22 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Plugin.Program { - public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable, IDisposable + public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable, + IDisposable { internal static Win32[] _win32s { get; set; } - internal static UWP.Application[] _uwps { get; set; } + internal static UWPApp[] _uwps { get; set; } internal static Settings _settings { get; set; } internal static PluginInitContext Context { get; private set; } private static BinaryStorage _win32Storage; - private static BinaryStorage _uwpStorage; + private static BinaryStorage _uwpStorage; private static readonly List emptyResults = new(); - private static readonly MemoryCacheOptions cacheOptions = new() - { - SizeLimit = 1560 - }; + private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 }; private static MemoryCache cache = new(cacheOptions); static Main() @@ -41,8 +39,8 @@ namespace Flow.Launcher.Plugin.Program public void Save() { - _win32Storage.Save(_win32s); - _uwpStorage.Save(_uwps); + _win32Storage.SaveAsync(_win32s); + _uwpStorage.SaveAsync(_uwps); } public async Task> QueryAsync(Query query, CancellationToken token) @@ -76,12 +74,12 @@ namespace Flow.Launcher.Plugin.Program _settings = context.API.LoadSettingJsonStorage(); - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () => + await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", async () => { _win32Storage = new BinaryStorage("Win32"); - _win32s = _win32Storage.TryLoad(Array.Empty()); - _uwpStorage = new BinaryStorage("UWP"); - _uwps = _uwpStorage.TryLoad(Array.Empty()); + _win32s = await _win32Storage.TryLoadAsync(Array.Empty()); + _uwpStorage = new BinaryStorage("UWP"); + _uwps = await _uwpStorage.TryLoadAsync(Array.Empty()); }); Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); @@ -104,7 +102,7 @@ namespace Flow.Launcher.Plugin.Program static void WatchProgramUpdate() { Win32.WatchProgramUpdate(_settings); - _ = UWP.WatchPackageChange(); + _ = UWPPackage.WatchPackageChange(); } } @@ -113,16 +111,16 @@ namespace Flow.Launcher.Plugin.Program var win32S = Win32.All(_settings); _win32s = win32S; ResetCache(); - _win32Storage.Save(_win32s); + _win32Storage.SaveAsync(_win32s); _settings.LastIndexTime = DateTime.Now; } public static void IndexUwpPrograms() { - var applications = UWP.All(_settings); + var applications = UWPPackage.All(_settings); _uwps = applications; ResetCache(); - _uwpStorage.Save(_uwps); + _uwpStorage.SaveAsync(_uwps); _settings.LastIndexTime = DateTime.Now; } @@ -228,7 +226,8 @@ namespace Flow.Launcher.Plugin.Program catch (Exception) { 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); + 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); } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs deleted file mode 100644 index d5924ba28..000000000 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs +++ /dev/null @@ -1,737 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Security.Principal; -using System.Threading.Tasks; -using System.Windows.Media.Imaging; -using Windows.ApplicationModel; -using Windows.Management.Deployment; -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Plugin.Program.Logger; -using Flow.Launcher.Plugin.SharedModels; -using System.Threading.Channels; -using System.Xml; -using Windows.ApplicationModel.Core; -using System.Windows.Input; - -namespace Flow.Launcher.Plugin.Program.Programs -{ - [Serializable] - public class UWP - { - public string Name { get; } - public string FullName { get; } - public string FamilyName { get; } - public string Location { get; set; } - - public Application[] Apps { get; set; } = Array.Empty(); - - - public UWP(Package package) - { - Location = package.InstalledLocation.Path; - Name = package.Id.Name; - FullName = package.Id.FullName; - FamilyName = package.Id.FamilyName; - } - - public void InitAppsInPackage(Package package) - { - var apps = new List(); - // WinRT - var appListEntries = package.GetAppListEntries(); - foreach (var app in appListEntries) - { - try - { - var tmp = new Application(app, this); - apps.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 = apps.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)); - // use small logo or may have a big margin - var previewUri = visualElement?.Attributes[logoName]?.Value; - app.PreviewImagePath = app.LogoPathFromUri(previewUri, (256, 256)); - } - } - } - 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 occurred 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 app-manifest version in package " - + $"{FullName} from location {Location}", new FormatException()); - return PackageVersion.Unknown; - } - else - { - ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" + - "|Can't parse AppManifest.xml of package " - + $"{FullName} from location {Location}", new ArgumentNullException(nameof(xmlRoot))); - return PackageVersion.Unknown; - } - } - - private static readonly Dictionary versionFromNamespace = new() - { - { - "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 - }, - { - "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81 - }, - { - "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 - }, - }; - - private static readonly Dictionary smallLogoNameFromVersion = new() - { - { - PackageVersion.Windows10, "Square44x44Logo" - }, - { - PackageVersion.Windows81, "Square30x30Logo" - }, - { - PackageVersion.Windows8, "SmallLogo" - }, - }; - - private static readonly Dictionary bigLogoNameFromVersion = new() - { - { - PackageVersion.Windows10, "Square150x150Logo" - }, - { - PackageVersion.Windows81, "Square150x150Logo" - }, - { - PackageVersion.Windows8, "Logo" - }, - }; - - public static Application[] All(Settings settings) - { - var support = SupportUWP(); - if (support && settings.EnableUWP) - { - var applications = CurrentUserPackages().AsParallel().SelectMany(p => - { - UWP u; - try - { - u = new UWP(p); - u.InitAppsInPackage(p); - } -#if !DEBUG - catch (Exception e) - { - ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and unable to convert Package to UWP for {p.Id.FullName}", e); - return Array.Empty(); - } -#endif -#if DEBUG //make developer aware and implement handling - catch - { - throw; - } -#endif - return u.Apps; - }).ToArray(); - - var updatedListWithoutDisabledApps = applications - .Where(t1 => !Main._settings.DisabledProgramSources - .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)); - - return updatedListWithoutDisabledApps.ToArray(); - } - else - { - return Array.Empty(); - } - } - - public static bool SupportUWP() - { - var windows10 = new Version(10, 0); - var support = Environment.OSVersion.Version.Major >= windows10.Major; - return support; - } - - private static IEnumerable CurrentUserPackages() - { - var user = WindowsIdentity.GetCurrent().User; - - if (user != null) - { - var userId = user.Value; - PackageManager packageManager; - try - { - packageManager = new PackageManager(); - } - catch - { - // Bug from https://github.com/microsoft/CsWinRT, using Microsoft.Windows.SDK.NET.Ref 10.0.19041.0. - // Only happens on the first time, so a try catch can fix it. - packageManager = new PackageManager(); - } - var packages = packageManager.FindPackagesForUser(userId); - packages = packages.Where(p => - { - try - { - var f = p.IsFramework; - var d = p.IsDevelopmentMode; - var path = p.InstalledLocation.Path; - return !f && !d && !string.IsNullOrEmpty(path); - } - catch (Exception e) - { - ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{p.Id.FullName}", "An unexpected error occurred and " - + $"unable to verify if package is valid", e); - return false; - } - }); - return packages; - } - else - { - return Array.Empty(); - } - } - - private static Channel PackageChangeChannel = Channel.CreateBounded(1); - - public static async Task WatchPackageChange() - { - if (Environment.OSVersion.Version.Major >= 10) - { - var catalog = PackageCatalog.OpenForCurrentUser(); - catalog.PackageInstalling += (_, args) => - { - if (args.IsComplete) - PackageChangeChannel.Writer.TryWrite(default); - }; - catalog.PackageUninstalling += (_, args) => - { - if (args.IsComplete) - PackageChangeChannel.Writer.TryWrite(default); - }; - catalog.PackageUpdating += (_, args) => - { - if (args.IsComplete) - PackageChangeChannel.Writer.TryWrite(default); - }; - - while (await PackageChangeChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) - { - await Task.Delay(3000).ConfigureAwait(false); - PackageChangeChannel.Reader.TryRead(out _); - await Task.Run(Main.IndexUwpPrograms); - } - - } - } - - public override string ToString() - { - return FamilyName; - } - - public override bool Equals(object obj) - { - if (obj is UWP uwp) - { - return FamilyName.Equals(uwp.FamilyName); - } - else - { - return false; - } - } - - public override int GetHashCode() - { - return FamilyName.GetHashCode(); - } - - [Serializable] - public class Application : IProgram - { - private string _uid = string.Empty; - public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } - 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 { get; set; } = string.Empty; - - 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 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) - { - string title; - MatchResult matchResult; - - // We suppose Name won't be null - if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description)) - { - title = Name; - matchResult = StringMatcher.FuzzySearch(query, Name); - } - else - { - title = $"{Name}: {Description}"; - var nameMatch = StringMatcher.FuzzySearch(query, Name); - var descriptionMatch = StringMatcher.FuzzySearch(query, Description); - if (descriptionMatch.Score > nameMatch.Score) - { - for (int i = 0; i < descriptionMatch.MatchData.Count; i++) - { - descriptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " - } - matchResult = descriptionMatch; - } - else - { - matchResult = nameMatch; - } - } - - if (!matchResult.IsSearchPrecisionScoreMet()) - return null; - - var result = new Result - { - Title = title, - AutoCompleteText = Name, - SubTitle = Main._settings.HideAppsPath ? string.Empty : Location, - IcoPath = LogoPath, - Preview = new Result.PreviewInfo - { - IsMedia = false, - PreviewImagePath = PreviewImagePath, - Description = Description - }, - Score = matchResult.Score, - TitleHighlightData = matchResult.MatchData, - ContextData = this, - Action = e => - { - // Ctrl + Enter to open containing folder - bool openFolder = e.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control; - if (openFolder) - { - Main.Context.API.OpenDirectory(Location); - return true; - } - - // Ctrl + Shift + Enter to run elevated - bool elevated = e.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); - - bool shouldRunElevated = elevated && CanRunElevated; - _ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false); - if (elevated && !shouldRunElevated) - { - 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; - } - }; - - - return result; - } - - public List ContextMenus(IPublicAPI api) - { - var contextMenus = new List - { - new Result - { - Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), - Action = _ => - { - Main.Context.API.OpenDirectory(Location); - - return true; - }, - IcoPath = "Images/folder.png", - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe838"), - } - }; - - if (CanRunElevated) - { - contextMenus.Add(new Result - { - Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), - Action = _ => - { - Task.Run(() => Launch(true)).ConfigureAwait(false); - return true; - }, - IcoPath = "Images/cmd.png", - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ef") - }); - } - - return contextMenus; - } - - private void Launch(bool elevated = false) - { - string command = "shell:AppsFolder\\" + UserModelId; - command = Environment.ExpandEnvironmentVariables(command.Trim()); - - var info = new ProcessStartInfo(command) - { - UseShellExecute = true, - Verb = elevated ? "runas" : "" - }; - - Main.StartProcess(Process.Start, info); - } - - internal static bool IfAppCanRunElevated(XmlNode appNode) - { - // 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 - - return appNode?.Attributes["EntryPoint"]?.Value == "Windows.FullTrustApplication" || - appNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL"; - } - - 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 - - if (string.IsNullOrWhiteSpace(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, int px) - { - var extension = Path.GetExtension(path); - if (extension != null) - { - //if (File.Exists(path)) - //{ - // return path; // shortcut, avoid enumerating files - //} - - var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44 - var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets - if (String.IsNullOrEmpty(logoNamePrefix) || !Directory.Exists(logoDir)) - { - // Known issue: Edge always triggers it since logo is not at uri - 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 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 - - // todo select from file name like pt run - var selected = logos.FirstOrDefault(); - var closest = selected; - int min = int.MaxValue; - 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 - px); - if (pixelCountDiff < min) - { - // try to find the closest to desired size - closest = logo; - if (pixelCountDiff == 0) - break; // found - min = pixelCountDiff; - } - } - - selected = closest; - if (!string.IsNullOrEmpty(selected)) - { - return selected; - } - else - { - 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|{Location}" + - $"|Unable to find extension from {uri} for {UserModelId} " + - $"in package location {Location}", new FileNotFoundException()); - return string.Empty; - } - } - } - - - #region logo legacy - // preserve for potential future use - - //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 Available" : path)}" + - // $"|Unable to get logo for {UserModelId} from {path} and" + - // $" located in {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; - - // 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 rectangle = new RectangleGeometry(backgroundArea); - // var rectDrawing = new GeometryDrawing(brush, pen, rectangle); - // group.Children.Add(rectDrawing); - - // 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()); - - // return new BitmapImage(new Uri(Constant.MissingImgIcon)); - // } - // } - // else - // { - // // todo use windows theme as background - // return image; - // } - //} - - #endregion - public override string ToString() - { - return $"{DisplayName}: {Description}"; - } - - public override bool Equals(object obj) - { - if (obj is Application other) - { - return UniqueIdentifier == other.UniqueIdentifier; - } - else - { - return false; - } - } - - public override int GetHashCode() - { - return UniqueIdentifier.GetHashCode(); - } - } - - public enum PackageVersion - { - Windows10, - Windows81, - Windows8, - Unknown - } - } -} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs new file mode 100644 index 000000000..3fb22b39d --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs @@ -0,0 +1,752 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Principal; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using Windows.ApplicationModel; +using Windows.Management.Deployment; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin.Program.Logger; +using Flow.Launcher.Plugin.SharedModels; +using System.Threading.Channels; +using System.Xml; +using Windows.ApplicationModel.Core; +using System.Windows.Input; +using MemoryPack; + +namespace Flow.Launcher.Plugin.Program.Programs +{ + [MemoryPackable] + public partial class UWPPackage + { + public string Name { get; } + public string FullName { get; } + public string FamilyName { get; } + public string Location { get; set; } + + public UWPApp[] Apps { get; set; } = Array.Empty(); + + + /// + /// For serialization + /// + [MemoryPackConstructor] + private UWPPackage() + { + } + + public UWPPackage(Package package) + { + Location = package.InstalledLocation.Path; + Name = package.Id.Name; + FullName = package.Id.FullName; + FamilyName = package.Id.FamilyName; + } + + public void InitAppsInPackage(Package package) + { + var apps = new List(); + // WinRT + var appListEntries = package.GetAppListEntries(); + foreach (var app in appListEntries) + { + try + { + var tmp = new UWPApp(app, this); + apps.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 = apps.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 || UWPApp.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)); + // use small logo or may have a big margin + var previewUri = visualElement?.Attributes[logoName]?.Value; + app.PreviewImagePath = app.LogoPathFromUri(previewUri, (256, 256)); + } + } + } + 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 occurred 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 app-manifest version in package " + + $"{FullName} from location {Location}", new FormatException()); + return PackageVersion.Unknown; + } + else + { + ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" + + "|Can't parse AppManifest.xml of package " + + $"{FullName} from location {Location}", + new ArgumentNullException(nameof(xmlRoot))); + return PackageVersion.Unknown; + } + } + + private static readonly Dictionary versionFromNamespace = new() + { + { "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 }, + { "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81 }, + { "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 }, + }; + + private static readonly Dictionary smallLogoNameFromVersion = new() + { + { PackageVersion.Windows10, "Square44x44Logo" }, + { PackageVersion.Windows81, "Square30x30Logo" }, + { PackageVersion.Windows8, "SmallLogo" }, + }; + + private static readonly Dictionary bigLogoNameFromVersion = new() + { + { PackageVersion.Windows10, "Square150x150Logo" }, + { PackageVersion.Windows81, "Square150x150Logo" }, + { PackageVersion.Windows8, "Logo" }, + }; + + public static UWPApp[] All(Settings settings) + { + var support = SupportUWP(); + if (support && settings.EnableUWP) + { + var applications = CurrentUserPackages().AsParallel().SelectMany(p => + { + UWPPackage u; + try + { + u = new UWPPackage(p); + u.InitAppsInPackage(p); + } +#if !DEBUG + catch (Exception e) + { + ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and unable to convert Package to UWP for {p.Id.FullName}", e); + return Array.Empty(); + } +#endif +#if DEBUG //make developer aware and implement handling + catch + { + throw; + } +#endif + return u.Apps; + }).ToArray(); + + var updatedListWithoutDisabledApps = applications + .Where(t1 => !Main._settings.DisabledProgramSources + .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)); + + return updatedListWithoutDisabledApps.ToArray(); + } + else + { + return Array.Empty(); + } + } + + public static bool SupportUWP() + { + var windows10 = new Version(10, 0); + var support = Environment.OSVersion.Version.Major >= windows10.Major; + return support; + } + + private static IEnumerable CurrentUserPackages() + { + var user = WindowsIdentity.GetCurrent().User; + + if (user != null) + { + var userId = user.Value; + PackageManager packageManager; + try + { + packageManager = new PackageManager(); + } + catch + { + // Bug from https://github.com/microsoft/CsWinRT, using Microsoft.Windows.SDK.NET.Ref 10.0.19041.0. + // Only happens on the first time, so a try catch can fix it. + packageManager = new PackageManager(); + } + + var packages = packageManager.FindPackagesForUser(userId); + packages = packages.Where(p => + { + try + { + var f = p.IsFramework; + var d = p.IsDevelopmentMode; + var path = p.InstalledLocation.Path; + return !f && !d && !string.IsNullOrEmpty(path); + } + catch (Exception e) + { + ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{p.Id.FullName}", + "An unexpected error occurred and " + + $"unable to verify if package is valid", e); + return false; + } + }); + return packages; + } + else + { + return Array.Empty(); + } + } + + private static Channel PackageChangeChannel = Channel.CreateBounded(1); + + public static async Task WatchPackageChange() + { + if (Environment.OSVersion.Version.Major >= 10) + { + var catalog = PackageCatalog.OpenForCurrentUser(); + catalog.PackageInstalling += (_, args) => + { + if (args.IsComplete) + PackageChangeChannel.Writer.TryWrite(default); + }; + catalog.PackageUninstalling += (_, args) => + { + if (args.IsComplete) + PackageChangeChannel.Writer.TryWrite(default); + }; + catalog.PackageUpdating += (_, args) => + { + if (args.IsComplete) + PackageChangeChannel.Writer.TryWrite(default); + }; + + while (await PackageChangeChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) + { + await Task.Delay(3000).ConfigureAwait(false); + PackageChangeChannel.Reader.TryRead(out _); + await Task.Run(Main.IndexUwpPrograms); + } + } + } + + public override string ToString() + { + return FamilyName; + } + + public override bool Equals(object obj) + { + if (obj is UWPPackage uwp) + { + return FamilyName.Equals(uwp.FamilyName); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + return FamilyName.GetHashCode(); + } + + + public enum PackageVersion + { + Windows10, + Windows81, + Windows8, + Unknown + } + } + + [MemoryPackable] + public partial class UWPApp : IProgram + { + private string _uid = string.Empty; + + public string UniqueIdentifier + { + get => _uid; + set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); + } + + 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 { get; set; } = string.Empty; + + 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; + + [MemoryPackConstructor] + private UWPApp() + { + } + + public UWPApp(AppListEntry appListEntry, UWPPackage 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) + { + string title; + MatchResult matchResult; + + // We suppose Name won't be null + if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description)) + { + title = Name; + matchResult = StringMatcher.FuzzySearch(query, Name); + } + else + { + title = $"{Name}: {Description}"; + var nameMatch = StringMatcher.FuzzySearch(query, Name); + var descriptionMatch = StringMatcher.FuzzySearch(query, Description); + if (descriptionMatch.Score > nameMatch.Score) + { + for (int i = 0; i < descriptionMatch.MatchData.Count; i++) + { + descriptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": " + } + + matchResult = descriptionMatch; + } + else + { + matchResult = nameMatch; + } + } + + if (!matchResult.IsSearchPrecisionScoreMet()) + return null; + + var result = new Result + { + Title = title, + AutoCompleteText = Name, + SubTitle = Main._settings.HideAppsPath ? string.Empty : Location, + IcoPath = LogoPath, + Preview = new Result.PreviewInfo + { + IsMedia = false, PreviewImagePath = PreviewImagePath, Description = Description + }, + Score = matchResult.Score, + TitleHighlightData = matchResult.MatchData, + ContextData = this, + Action = e => + { + // Ctrl + Enter to open containing folder + bool openFolder = e.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control; + if (openFolder) + { + Main.Context.API.OpenDirectory(Location); + return true; + } + + // Ctrl + Shift + Enter to run elevated + bool elevated = e.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); + + bool shouldRunElevated = elevated && CanRunElevated; + _ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false); + if (elevated && !shouldRunElevated) + { + 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; + } + }; + + + return result; + } + + public List ContextMenus(IPublicAPI api) + { + var contextMenus = new List + { + new Result + { + Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), + Action = _ => + { + Main.Context.API.OpenDirectory(Location); + + return true; + }, + IcoPath = "Images/folder.png", + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe838"), + } + }; + + if (CanRunElevated) + { + contextMenus.Add(new Result + { + Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), + Action = _ => + { + Task.Run(() => Launch(true)).ConfigureAwait(false); + return true; + }, + IcoPath = "Images/cmd.png", + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ef") + }); + } + + return contextMenus; + } + + private void Launch(bool elevated = false) + { + string command = "shell:AppsFolder\\" + UserModelId; + command = Environment.ExpandEnvironmentVariables(command.Trim()); + + var info = new ProcessStartInfo(command) { UseShellExecute = true, Verb = elevated ? "runas" : "" }; + + Main.StartProcess(Process.Start, info); + } + + internal static bool IfAppCanRunElevated(XmlNode appNode) + { + // 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 + + return appNode?.Attributes["EntryPoint"]?.Value == "Windows.FullTrustApplication" || + appNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL"; + } + + 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 + + if (string.IsNullOrWhiteSpace(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, int px) + { + var extension = Path.GetExtension(path); + if (extension != null) + { + //if (File.Exists(path)) + //{ + // return path; // shortcut, avoid enumerating files + //} + + var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44 + var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets + if (String.IsNullOrEmpty(logoNamePrefix) || !Directory.Exists(logoDir)) + { + // Known issue: Edge always triggers it since logo is not at uri + 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 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 + + // todo select from file name like pt run + var selected = logos.FirstOrDefault(); + var closest = selected; + int min = int.MaxValue; + 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 - px); + if (pixelCountDiff < min) + { + // try to find the closest to desired size + closest = logo; + if (pixelCountDiff == 0) + break; // found + min = pixelCountDiff; + } + } + + selected = closest; + if (!string.IsNullOrEmpty(selected)) + { + return selected; + } + else + { + 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|{Location}" + + $"|Unable to find extension from {uri} for {UserModelId} " + + $"in package location {Location}", new FileNotFoundException()); + return string.Empty; + } + } + } + + + #region logo legacy + + // preserve for potential future use + + //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 Available" : path)}" + + // $"|Unable to get logo for {UserModelId} from {path} and" + + // $" located in {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; + + // 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 rectangle = new RectangleGeometry(backgroundArea); + // var rectDrawing = new GeometryDrawing(brush, pen, rectangle); + // group.Children.Add(rectDrawing); + + // 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()); + + // return new BitmapImage(new Uri(Constant.MissingImgIcon)); + // } + // } + // else + // { + // // todo use windows theme as background + // return image; + // } + //} + + #endregion + + public override string ToString() + { + return $"{DisplayName}: {Description}"; + } + + public override bool Equals(object obj) + { + if (obj is UWPApp other) + { + return UniqueIdentifier == other.UniqueIdentifier; + } + else + { + return false; + } + } + + public override int GetHashCode() + { + return UniqueIdentifier.GetHashCode(); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index 20f489c30..7d08d3670 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -16,14 +16,21 @@ using System.Threading.Channels; using Flow.Launcher.Plugin.Program.Views.Models; using IniParser; using System.Windows.Input; +using MemoryPack; namespace Flow.Launcher.Plugin.Program.Programs { - [Serializable] - public class Win32 : IProgram, IEquatable + [MemoryPackable] + public partial class Win32 : IProgram, IEquatable { public string Name { get; set; } - public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison + + public string UniqueIdentifier + { + get => _uid; + set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); + } // For path comparison + public string IcoPath { get; set; } /// @@ -96,7 +103,8 @@ namespace Flow.Launcher.Plugin.Program.Programs bool useLocalizedName = !string.IsNullOrEmpty(LocalizedName) && !Name.Equals(LocalizedName); string resultName = useLocalizedName ? LocalizedName : Name; - if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || resultName.Equals(Description)) + if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || + resultName.Equals(Description)) { title = resultName; matchResult = StringMatcher.FuzzySearch(query, resultName); @@ -113,6 +121,7 @@ namespace Flow.Launcher.Plugin.Program.Programs { descriptionMatch.MatchData[i] += resultName.Length + 2; // 2 is ": " } + matchResult = descriptionMatch; } else @@ -129,10 +138,12 @@ namespace Flow.Launcher.Plugin.Program.Programs { candidates.Add(ExecutableName); } + if (useLocalizedName) { candidates.Add(Name); } + matchResult = Match(query, candidates); if (matchResult == null) { @@ -209,9 +220,7 @@ namespace Flow.Launcher.Plugin.Program.Programs { var info = new ProcessStartInfo { - FileName = FullPath, - WorkingDirectory = ParentDirectory, - UseShellExecute = true + FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true }; Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info)); @@ -424,7 +433,8 @@ namespace Flow.Launcher.Plugin.Program.Programs } } - private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true) + private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes, + bool recursive = true) { if (!Directory.Exists(directory)) return Enumerable.Empty(); @@ -448,7 +458,8 @@ namespace Flow.Launcher.Plugin.Program.Programs } } - private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes, string[] protocols) + private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes, + string[] protocols) { // Disabled custom sources are not in DisabledProgramSources var paths = directories.AsParallel() @@ -466,14 +477,15 @@ namespace Flow.Launcher.Plugin.Program.Programs .Distinct(); var startupPaths = GetStartupPaths(); - + var programs = ExceptDisabledSource(allPrograms) .Where(x => !startupPaths.Any(startup => FilesFolders.PathContains(startup, x))) .Select(x => GetProgramFromPath(x, protocols)); return programs; } - private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols, List commonParents) + private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols, + List commonParents) { var pathEnv = Environment.GetEnvironmentVariable("Path"); if (String.IsNullOrEmpty(pathEnv)) @@ -515,7 +527,8 @@ namespace Flow.Launcher.Plugin.Program.Programs toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p))); var programs = ExceptDisabledSource(toFilter) - .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid).ToList(); // ToList due to disposing issue + .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid) + .ToList(); // ToList due to disposing issue return programs; } @@ -616,7 +629,10 @@ namespace Flow.Launcher.Plugin.Program.Programs .SelectMany(g => { // is shortcut and in start menu - var startMenu = g.Where(g => g.LnkResolvedPath != null && startMenuPaths.Any(x => FilesFolders.PathContains(x, g.FullPath))).ToList(); + var startMenu = g.Where(g => + g.LnkResolvedPath != null && + startMenuPaths.Any(x => FilesFolders.PathContains(x, g.FullPath))) + .ToList(); if (startMenu.Any()) return startMenu.Take(1); @@ -756,6 +772,7 @@ namespace Flow.Launcher.Plugin.Program.Programs while (reader.TryRead(out _)) { } + await Task.Run(Main.IndexWin32Programs); } } @@ -766,6 +783,7 @@ namespace Flow.Launcher.Plugin.Program.Programs { throw new ArgumentException("Path Not Exist"); } + var watcher = new FileSystemWatcher(directory); watcher.Created += static (_, _) => indexQueue.Writer.TryWrite(default); @@ -804,8 +822,10 @@ namespace Flow.Launcher.Plugin.Program.Programs parents.Remove(source); } } + result.AddRange(parents.Select(x => x.Location)); } + return result.DistinctBy(x => x.ToLowerInvariant()).ToList(); } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs index 156f33ebc..9879993fe 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs @@ -87,9 +87,9 @@ namespace Flow.Launcher.Plugin.Program.Views } } - public bool ShowUWPCheckbox => UWP.SupportUWP(); + public bool ShowUWPCheckbox => UWPPackage.SupportUWP(); - public ProgramSetting(PluginInitContext context, Settings settings, Win32[] win32s, UWP.Application[] uwps) + public ProgramSetting(PluginInitContext context, Settings settings, Win32[] win32s, UWPApp[] uwps) { this.context = context; _settings = settings; @@ -149,9 +149,9 @@ namespace Flow.Launcher.Plugin.Program.Views private void DeleteProgramSources(List itemsToDelete) { itemsToDelete.ForEach(t1 => _settings.ProgramSources - .Remove(_settings.ProgramSources - .Where(x => x.UniqueIdentifier == t1.UniqueIdentifier) - .FirstOrDefault())); + .Remove(_settings.ProgramSources + .Where(x => x.UniqueIdentifier == t1.UniqueIdentifier) + .FirstOrDefault())); itemsToDelete.ForEach(x => ProgramSettingDisplayList.Remove(x)); ReIndexing(); @@ -182,16 +182,20 @@ namespace Flow.Launcher.Plugin.Program.Views { if (selectedProgramSource.Enabled) { - ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, true); // sync status in win32, uwp and disabled + ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, + true); // sync status in win32, uwp and disabled ProgramSettingDisplay.RemoveDisabledFromSettings(); } else { - ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, false); + ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, + false); ProgramSettingDisplay.StoreDisabledInSettings(); } + ReIndexing(); } + programSourceView.SelectedIndex = selectedIndex; } } @@ -233,7 +237,8 @@ namespace Flow.Launcher.Plugin.Program.Views foreach (string directory in directories) { if (Directory.Exists(directory) - && !ProgramSettingDisplayList.Any(x => x.UniqueIdentifier.Equals(directory, System.StringComparison.OrdinalIgnoreCase))) + && !ProgramSettingDisplayList.Any(x => + x.UniqueIdentifier.Equals(directory, System.StringComparison.OrdinalIgnoreCase))) { var source = new ProgramSource(directory); @@ -262,8 +267,8 @@ namespace Flow.Launcher.Plugin.Program.Views private void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e) { var selectedItems = programSourceView - .SelectedItems.Cast() - .ToList(); + .SelectedItems.Cast() + .ToList(); if (selectedItems.Count == 0) { @@ -274,7 +279,8 @@ namespace Flow.Launcher.Plugin.Program.Views if (IsAllItemsUserAdded(selectedItems)) { - var msg = string.Format(context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source")); + var msg = string.Format( + context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source")); if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) { @@ -364,8 +370,8 @@ namespace Flow.Launcher.Plugin.Program.Views private void programSourceView_SelectionChanged(object sender, SelectionChangedEventArgs e) { var selectedItems = programSourceView - .SelectedItems.Cast() - .ToList(); + .SelectedItems.Cast() + .ToList(); if (IsAllItemsUserAdded(selectedItems)) { @@ -400,7 +406,8 @@ namespace Flow.Launcher.Plugin.Program.Views ListView listView = sender as ListView; GridView gView = listView.View as GridView; - var workingWidth = listView.ActualWidth - SystemParameters.VerticalScrollBarWidth; // take into account vertical scrollbar + var workingWidth = + listView.ActualWidth - SystemParameters.VerticalScrollBarWidth; // take into account vertical scrollbar var col1 = 0.25; var col2 = 0.15; var col3 = 0.60; From ab7685e9ea9bf58a4f1bf3c519ca7cebd1ca1880 Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Sat, 18 Nov 2023 21:45:06 -0500 Subject: [PATCH 02/14] Show a result error instead of popping up dialog --- Flow.Launcher.Core/Plugin/PluginManager.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index f8c9a3f17..31ded2baf 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -210,7 +210,18 @@ namespace Flow.Launcher.Core.Plugin } catch (Exception e) { - throw new FlowPluginException(metadata, e); + Result r = new() + { + Title = $"{metadata.Name}: {e.GetType().Name}", + SubTitle = "ERROR: There was an error loading this plugin!", + IcoPath = "Images\\app_error.png", + PluginDirectory = metadata.PluginDirectory, + ActionKeywordAssigned = query.ActionKeyword, + PluginID = metadata.ID, + OriginQuery = query, + Action = _ => { throw new FlowPluginException(metadata, e);} + }; + results.Add(r); } return results; } From 798d30ea27aa24da41e569da4b4d21bd76e36a3d Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:04:29 +0800 Subject: [PATCH 03/14] Ignore modifier key when using key + number to launch result - close #2191 - close #2425 --- Flow.Launcher/ViewModel/MainViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c832c258d..7dcd2f4d2 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -285,7 +285,8 @@ namespace Flow.Launcher.ViewModel } var hideWindow = await result.ExecuteAsync(new ActionContext { - SpecialKeyState = GlobalHotkey.CheckModifiers() + // not null means pressing modifier key + number, should ignore the modifier key + SpecialKeyState = index is not null ? new SpecialKeyState() : GlobalHotkey.CheckModifiers() }) .ConfigureAwait(false); From 6625e911829a42c0d4115ef6a8c5db2dad8438df Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Mon, 20 Nov 2023 23:20:59 +0800 Subject: [PATCH 04/14] Use default SpecialKeyState --- Flow.Launcher.Plugin/ActionContext.cs | 7 +++++++ Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Plugin/ActionContext.cs b/Flow.Launcher.Plugin/ActionContext.cs index d6ba4894e..e31c8e31d 100644 --- a/Flow.Launcher.Plugin/ActionContext.cs +++ b/Flow.Launcher.Plugin/ActionContext.cs @@ -50,5 +50,12 @@ namespace Flow.Launcher.Plugin (AltPressed ? ModifierKeys.Alt : ModifierKeys.None) | (WinPressed ? ModifierKeys.Windows : ModifierKeys.None); } + + public static readonly SpecialKeyState Default = new () { + CtrlPressed = false, + ShiftPressed = false, + AltPressed = false, + WinPressed = false + }; } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 7dcd2f4d2..61bf0c4dc 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -286,7 +286,7 @@ namespace Flow.Launcher.ViewModel var hideWindow = await result.ExecuteAsync(new ActionContext { // not null means pressing modifier key + number, should ignore the modifier key - SpecialKeyState = index is not null ? new SpecialKeyState() : GlobalHotkey.CheckModifiers() + SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() }) .ConfigureAwait(false); From 6e385a3d7c622e438afd0747a690b11e634ab739 Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Sat, 25 Nov 2023 01:20:43 +0800 Subject: [PATCH 05/14] Revert "Bump System.Drawing.Common from 7.0.0 to 8.0.0" --- .../Flow.Launcher.Infrastructure.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 8124a95de..2f5259039 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -58,7 +58,7 @@ - + From a84e509aabbb726c81bf547d0ea48d35df933caa Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:15:17 -0500 Subject: [PATCH 06/14] Use proper error icon constant --- Flow.Launcher.Core/Plugin/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 31ded2baf..a297de63e 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -214,7 +214,7 @@ namespace Flow.Launcher.Core.Plugin { Title = $"{metadata.Name}: {e.GetType().Name}", SubTitle = "ERROR: There was an error loading this plugin!", - IcoPath = "Images\\app_error.png", + IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon, PluginDirectory = metadata.PluginDirectory, ActionKeywordAssigned = query.ActionKeyword, PluginID = metadata.ID, From 0e226d7a5b60f61b5bc68d6c72647bd1050334f1 Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:15:29 -0500 Subject: [PATCH 07/14] Reword title and subtitle --- Flow.Launcher.Core/Plugin/PluginManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index a297de63e..7454b5a94 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -212,8 +212,8 @@ namespace Flow.Launcher.Core.Plugin { Result r = new() { - Title = $"{metadata.Name}: {e.GetType().Name}", - SubTitle = "ERROR: There was an error loading this plugin!", + Title = $"{metadata.Name}: Failed to respond!", + SubTitle = "Select this result for more info", IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon, PluginDirectory = metadata.PluginDirectory, ActionKeywordAssigned = query.ActionKeyword, From f684883d7250996dd8656e40b711697d875ad4d8 Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:15:58 -0500 Subject: [PATCH 08/14] Insure result is never in front of relevant results --- Flow.Launcher.Core/Plugin/PluginManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 7454b5a94..eec906807 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -219,7 +219,8 @@ namespace Flow.Launcher.Core.Plugin ActionKeywordAssigned = query.ActionKeyword, PluginID = metadata.ID, OriginQuery = query, - Action = _ => { throw new FlowPluginException(metadata, e);} + Action = _ => { throw new FlowPluginException(metadata, e);}, + Score = -100 }; results.Add(r); } From fd9e8a59e116a8eb53dc1126066287c8cd803be7 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sat, 25 Nov 2023 23:57:05 -0600 Subject: [PATCH 09/14] fix build --- Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs index 3fb22b39d..654897cc5 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs @@ -214,7 +214,7 @@ namespace Flow.Launcher.Plugin.Program.Programs catch (Exception e) { ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and unable to convert Package to UWP for {p.Id.FullName}", e); - return Array.Empty(); + return Array.Empty(); } #endif #if DEBUG //make developer aware and implement handling From 44fb863f075a4227b3d4b026340ab9d90040b270 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 26 Nov 2023 09:33:34 -0600 Subject: [PATCH 10/14] minor fix jsonrpc errorstream and expect.txt --- .github/actions/spelling/expect.txt | 4 ---- Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index d0fee9559..f2be7fb3b 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,7 +1,6 @@ crowdin DWM workflows -Wpf wpf actionkeyword stackoverflow @@ -20,9 +19,7 @@ Prioritise Segoe Google Customise -UWP uwp -Uwp Bokmal Bokm uninstallation @@ -61,7 +58,6 @@ popup ptr pluginindicator TobiasSekan -Img img resx bak diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs index 24d06d975..a476f06e9 100644 --- a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -17,7 +17,7 @@ namespace Flow.Launcher.Core.Plugin protected abstract ProcessStartInfo StartInfo { get; set; } - public Process ClientProcess { get; set; } + protected Process ClientProcess { get; set; } public override async Task InitAsync(PluginInitContext context) { @@ -33,6 +33,8 @@ namespace Flow.Launcher.Core.Plugin SetupPipe(ClientProcess); + ErrorStream = ClientProcess.StandardError; + await base.InitAsync(context); } From 1bd16cccaf67ceeafb6bf76febfd2348a2e0fcd4 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 26 Nov 2023 09:37:43 -0600 Subject: [PATCH 11/14] remove duplicate expect --- .github/actions/spelling/expect.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index f2be7fb3b..0d4dde36b 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -74,7 +74,6 @@ WCA_ACCENT_POLICY HGlobal dopusrt firefox -Firefox msedge svgc ime @@ -83,7 +82,6 @@ txb btn otf searchplugin -Noresult wpftk mkv flac From c8753b29ec4f8001b1743fdb960bef4105dcf526 Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:50:23 +0800 Subject: [PATCH 12/14] Set default value of `AutoRestartAfterChanging` to `true` --- Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs index f23ff71f0..811bec50c 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs @@ -10,6 +10,6 @@ public bool WarnFromUnknownSource { get; set; } = true; - public bool AutoRestartAfterChanging { get; set; } = false; + public bool AutoRestartAfterChanging { get; set; } = true; } } From 44af8e59f9b319c345cbc09f9bd900a8df52011a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 23:00:55 +0000 Subject: [PATCH 13/14] Bump System.Data.OleDb from 7.0.0 to 8.0.0 Bumps [System.Data.OleDb](https://github.com/dotnet/runtime) from 7.0.0 to 8.0.0. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v7.0.0...v8.0.0) --- updated-dependencies: - dependency-name: System.Data.OleDb dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .../Flow.Launcher.Plugin.Explorer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj index 1c0bdaad7..6d1497327 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj @@ -45,7 +45,7 @@ - + From 7a603f5504b22edd4b959a7b81fef5ff47afc692 Mon Sep 17 00:00:00 2001 From: VictoriousRaptor <10308169+VictoriousRaptor@users.noreply.github.com> Date: Wed, 6 Dec 2023 22:42:24 +0800 Subject: [PATCH 14/14] Fix spell check - fix crash - fix missing dict --- .github/actions/spelling/expect.txt | 4 +++- .github/actions/spelling/patterns.txt | 3 +++ .github/workflows/spelling.yml | 7 +++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 0d4dde36b..8e29be550 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -102,4 +102,6 @@ Preinstalled errormetadatafile noresult pluginsmanager -alreadyexists \ No newline at end of file +alreadyexists +JsonRPC +JsonRPCV2 diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt index 903714aef..f29f57ad5 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -118,3 +118,6 @@ # UWP [Uu][Ww][Pp] + +# version suffix v# +(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_])) diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 97d3cccb3..7aaa9296a 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -73,7 +73,7 @@ jobs: steps: - name: check-spelling id: spelling - uses: check-spelling/check-spelling@v0.0.22 + uses: check-spelling/check-spelling@prerelease with: suppress_push_for_open_pull_request: 1 checkout: true @@ -91,10 +91,9 @@ jobs: extra_dictionaries: cspell:software-terms/dict/softwareTerms.txt cspell:win32/src/win32.txt - cspell:php/src/php.txt cspell:filetypes/filetypes.txt cspell:csharp/csharp.txt - cspell:dotnet/src/dotnet.txt + cspell:dotnet/dict/dotnet.txt cspell:python/src/common/extra.txt cspell:python/src/python/python-lib.txt cspell:aws/aws.txt @@ -130,7 +129,7 @@ jobs: if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request') steps: - name: comment - uses: check-spelling/check-spelling@v0.0.22 + uses: check-spelling/check-spelling@prerelease with: checkout: true spell_check_this: check-spelling/spell-check-this@main