Merge branch 'dev' into move-exception-message-to-result

This commit is contained in:
Garulf 2023-12-10 02:20:00 -05:00 committed by GitHub
commit ba45069d88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1277 additions and 1046 deletions

View file

@ -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
@ -78,7 +74,6 @@ WCA_ACCENT_POLICY
HGlobal
dopusrt
firefox
Firefox
msedge
svgc
ime
@ -87,7 +82,6 @@ txb
btn
otf
searchplugin
Noresult
wpftk
mkv
flac
@ -108,4 +102,6 @@ Preinstalled
errormetadatafile
noresult
pluginsmanager
alreadyexists
alreadyexists
JsonRPC
JsonRPCV2

View file

@ -118,3 +118,6 @@
# UWP
[Uu][Ww][Pp]
# version suffix <word>v#
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))

View file

@ -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

View file

@ -54,7 +54,7 @@
<ItemGroup>
<PackageReference Include="Droplex" Version="1.7.0" />
<PackageReference Include="FSharp.Core" Version="7.0.400" />
<PackageReference Include="FSharp.Core" Version="7.0.401" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
<PackageReference Include="StreamJsonRpc" Version="2.16.36" />

View file

@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.Plugin
public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error);
public record JsonRPCQueryResponseModel(int Id,
[property: JsonPropertyName("result")] List<JsonRPCResult> Result,
IReadOnlyDictionary<string, object> SettingsChanges = null,
IReadOnlyDictionary<string, object> SettingsChange = null,
string DebugMessage = "",
JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error);

View file

@ -94,7 +94,7 @@ namespace Flow.Launcher.Core.Plugin
results.AddRange(queryResponseModel.Result);
Settings?.UpdateSettings(queryResponseModel.SettingsChanges);
Settings?.UpdateSettings(queryResponseModel.SettingsChange);
return results;
}
@ -126,14 +126,15 @@ namespace Flow.Launcher.Core.Plugin
private async Task InitSettingAsync()
{
if (!File.Exists(SettingConfigurationPath))
return;
JsonRpcConfigurationModel configuration = null;
if (File.Exists(SettingConfigurationPath))
{
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
configuration =
deserializer.Deserialize<JsonRpcConfigurationModel>(
await File.ReadAllTextAsync(SettingConfigurationPath));
}
var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
var configuration =
deserializer.Deserialize<JsonRpcConfigurationModel>(
await File.ReadAllTextAsync(SettingConfigurationPath));
Settings ??= new JsonRPCPluginSettings
{

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
@ -10,16 +11,16 @@ namespace Flow.Launcher.Core.Plugin
{
public class JsonRPCPluginSettings
{
public required JsonRpcConfigurationModel Configuration { get; init; }
public required JsonRpcConfigurationModel? Configuration { get; init; }
public required string SettingPath { get; init; }
public Dictionary<string, FrameworkElement> SettingControls { get; } = new();
public IReadOnlyDictionary<string, object> Inner => Settings;
protected Dictionary<string, object> Settings { get; set; }
protected ConcurrentDictionary<string, object> Settings { get; set; }
public required IPublicAPI API { get; init; }
private JsonStorage<Dictionary<string, object>> _storage;
private JsonStorage<ConcurrentDictionary<string, object>> _storage;
// maybe move to resource?
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
@ -33,9 +34,14 @@ namespace Flow.Launcher.Core.Plugin
public async Task InitializeAsync()
{
_storage = new JsonStorage<Dictionary<string, object>>(SettingPath);
_storage = new JsonStorage<ConcurrentDictionary<string, object>>(SettingPath);
Settings = await _storage.LoadAsync();
if (Settings != null)
{
return;
}
foreach (var (type, attributes) in Configuration.Body)
{
if (attributes.Name == null)
@ -58,10 +64,7 @@ namespace Flow.Launcher.Core.Plugin
foreach (var (key, value) in settings)
{
if (Settings.ContainsKey(key))
{
Settings[key] = value;
}
Settings[key] = value;
if (SettingControls.TryGetValue(key, out var control))
{
@ -82,6 +85,7 @@ namespace Flow.Launcher.Core.Plugin
}
}
}
Save();
}
public async Task SaveAsync()

View file

@ -11,6 +11,8 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using ISavable = Flow.Launcher.Plugin.ISavable;
using Flow.Launcher.Plugin.SharedCommands;
using System.Text.Json;
namespace Flow.Launcher.Core.Plugin
{
@ -27,9 +29,9 @@ namespace Flow.Launcher.Core.Plugin
public static IPublicAPI API { private set; get; }
// todo happlebao, this should not be public, the indicator function should be embeded
public static PluginsSettings Settings;
private static PluginsSettings Settings;
private static List<PluginMetadata> _metadatas;
private static List<string> _modifiedPlugins = new List<string>();
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
@ -343,5 +345,156 @@ namespace Flow.Launcher.Core.Plugin
RemoveActionKeyword(id, oldActionKeyword);
}
}
private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
{
var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
var unzippedFilesCount = Directory.GetFiles(unzippedParentFolderPath).Length;
// adjust path depending on how the plugin is zipped up
// the recommended should be to zip up the folder not the contents
if (unzippedFolderCount == 1 && unzippedFilesCount == 0)
// folder is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/pluginFolderName/
return Directory.GetDirectories(unzippedParentFolderPath)[0];
if (unzippedFilesCount > 1)
// content is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/
return unzippedParentFolderPath;
return string.Empty;
}
private static bool SameOrLesserPluginVersionExists(string metadataPath)
{
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
return AllPlugins.Any(x => x.Metadata.ID == newMetadata.ID
&& newMetadata.Version.CompareTo(x.Metadata.Version) <= 0);
}
#region Public functions
public static bool PluginModified(string uuid)
{
return _modifiedPlugins.Contains(uuid);
}
/// <summary>
/// Update a plugin to new version, from a zip file. Will Delete zip after updating.
/// </summary>
public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
{
InstallPlugin(newVersion, zipFilePath, checkModified:false);
UninstallPlugin(existingVersion, removeSettings:false, checkModified:false);
_modifiedPlugins.Add(existingVersion.ID);
}
/// <summary>
/// Install a plugin. Will Delete zip after updating.
/// </summary>
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
{
InstallPlugin(plugin, zipFilePath, true);
}
/// <summary>
/// Uninstall a plugin.
/// </summary>
public static void UninstallPlugin(PluginMetadata plugin, bool removeSettings = true)
{
UninstallPlugin(plugin, removeSettings, true);
}
#endregion
#region Internal functions
internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
{
if (checkModified && PluginModified(plugin.ID))
{
// Distinguish exception from installing same or less version
throw new ArgumentException($"Plugin {plugin.Name} {plugin.ID} has been modified.", nameof(plugin));
}
// Unzip plugin files to temp folder
var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);
File.Delete(zipFilePath);
var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
var metadataJsonFilePath = string.Empty;
if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName)))
metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName);
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
{
throw new FileNotFoundException($"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist");
}
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
{
throw new InvalidOperationException($"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}");
}
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
var defaultPluginIDs = new List<string>
{
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
"572be03c74c642baae319fc283e561a8", // Explorer
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
"791FC278BA414111B8D1886DFE447410", // Program
"D409510CD0D2481F853690A07E6DC426", // Shell
"CEA08895D2544B019B2E9C5009600DF4", // Sys
"0308FD86DE0A4DEE8D62B9B535370992", // URL
"565B73353DBF4806919830B9202EE3BF", // WebSearch
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
};
// Treat default plugin differently, it needs to be removable along with each flow release
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
? DataLocation.PluginsDirectory
: Constant.PreinstalledDirectory;
var newPluginPath = Path.Combine(installDirectory, folderName);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath);
Directory.Delete(tempFolderPluginPath, true);
if (checkModified)
{
_modifiedPlugins.Add(plugin.ID);
}
}
internal static void UninstallPlugin(PluginMetadata plugin, bool removeSettings, bool checkModified)
{
if (checkModified && PluginModified(plugin.ID))
{
throw new ArgumentException($"Plugin {plugin.Name} has been modified");
}
if (removeSettings)
{
Settings.Plugins.Remove(plugin.ID);
AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
}
// Marked for deletion. Will be deleted on next start up
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
if (checkModified)
{
_modifiedPlugins.Add(plugin.ID);
}
}
#endregion
}
}

View file

@ -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);
}

View file

@ -53,6 +53,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MemoryPack" Version="1.10.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.7.30" />
<PackageReference Include="NLog" Version="4.7.10" />
<PackageReference Include="NLog.Schema" Version="4.7.10" />

View file

@ -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<Dictionary<(string, bool), int>> _storage;
private static readonly ConcurrentDictionary<string, string> 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<Dictionary<(string, bool), int>>("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<ConcurrentDictionary<(string, bool), int>> 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<BitmapImage> 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;
}
}

View file

@ -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.
/// <summary>
/// Stroage object using binary data
/// Normally, it has better performance, but not readable
/// </summary>
/// <remarks>
/// It utilize MemoryPack, which means the object must be MemoryPackSerializable
/// https://github.com/Cysharp/MemoryPack
/// </remarks>
public class BinaryStorage<T>
{
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<T> 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<T> 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<T>(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
}

View file

@ -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
};
}
}

View file

@ -54,7 +54,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
</ItemGroup>
</Project>

View file

@ -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);
}
}
}

View file

@ -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 ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers()
})
.ConfigureAwait(false);

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
@ -56,7 +56,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.10" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
</ItemGroup>
</Project>

View file

@ -45,7 +45,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Data.OleDb" Version="7.0.0" />
<PackageReference Include="System.Data.OleDb" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="tlbimp-Microsoft.Search.Interop" Version="1.0.0" />
</ItemGroup>

View file

@ -13,6 +13,10 @@ namespace Flow.Launcher.Plugin.PluginsManager
Context = context;
}
private readonly GlyphInfo sourcecodeGlyph = new("/Resources/#Segoe Fluent Icons","\uE943");
private readonly GlyphInfo issueGlyph = new("/Resources/#Segoe Fluent Icons", "\ued15");
private readonly GlyphInfo manifestGlyph = new("/Resources/#Segoe Fluent Icons", "\uea37");
public List<Result> LoadContextMenus(Result selectedResult)
{
if(selectedResult.ContextData is not UserPlugin pluginManifestInfo)
@ -36,6 +40,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_gotosourcecode_title"),
SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_gotosourcecode_subtitle"),
IcoPath = "Images\\sourcecode.png",
Glyph = sourcecodeGlyph,
Action = _ =>
{
Context.API.OpenUrl(pluginManifestInfo.UrlSourceCode);
@ -47,6 +52,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_newissue_title"),
SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_newissue_subtitle"),
IcoPath = "Images\\request.png",
Glyph = issueGlyph,
Action = _ =>
{
// standard UrlSourceCode format in PluginsManifest's plugins.json file: https://github.com/jjw24/Flow.Launcher.Plugin.Putty/tree/master
@ -63,6 +69,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
Title = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_pluginsmanifest_title"),
SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_pluginsmanifest_subtitle"),
IcoPath = "Images\\manifestsite.png",
Glyph = manifestGlyph,
Action = _ =>
{
Context.API.OpenUrl("https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest");

View file

@ -36,8 +36,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>
</Project>

View file

@ -8,7 +8,9 @@
<system:String x:Key="plugin_pluginsmanager_download_success">Successfully downloaded {0}</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_uninstall_prompt_no_restart">{0} by {1} {2}{2}Would you like to uninstall this plugin?</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_prompt_no_restart">{0} by {1} {2}{2}Would you like to install this plugin?</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>
@ -18,19 +20,25 @@
<system:String x:Key="plugin_pluginsmanager_install_error_duplicate">Error: A plugin which has the same or greater version with {0} already exists.</system:String>
<system:String x:Key="plugin_pluginsmanager_install_error_title">Error installing plugin</system:String>
<system:String x:Key="plugin_pluginsmanager_install_error_subtitle">Error occurred while trying to install {0}</system:String>
<system:String x:Key="plugin_pluginsmanager_uninstall_error_title">Error uninstalling plugin</system:String>
<system:String x:Key="plugin_pluginsmanager_update_noresult_title">No update available</system:String>
<system:String x:Key="plugin_pluginsmanager_update_noresult_subtitle">All plugins are up to date</system:String>
<system:String x:Key="plugin_pluginsmanager_update_prompt">{0} by {1} {2}{3}Would you like to update this plugin? After the update Flow will automatically restart.</system:String>
<system:String x:Key="plugin_pluginsmanager_update_prompt_no_restart">{0} by {1} {2}{2}Would you like to update this plugin?</system:String>
<system:String x:Key="plugin_pluginsmanager_update_title">Plugin Update</system:String>
<system:String x:Key="plugin_pluginsmanager_update_exists">This plugin has an update, would you like to see it?</system:String>
<system:String x:Key="plugin_pluginsmanager_update_alreadyexists">This plugin is already installed</system:String>
<system:String x:Key="plugin_pluginsmanager_update_failed_title">Plugin Manifest Download Failed</system:String>
<system:String x:Key="plugin_pluginsmanager_update_failed_subtitle">Please check if you can connect to github.com. This error means you may not be able to install or update plugins.</system:String>
<system:String x:Key="plugin_pluginsmanager_update_success_restart">Plugin {0} successfully updated. Restarting Flow, please wait...</system:String>
<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 -->
<system:String x:Key="plugin_pluginsmanager_install_success_no_restart">Plugin {0} successfully installed. Please restart Flow.</system:String>
<system:String x:Key="plugin_pluginsmanager_uninstall_success_no_restart">Plugin {0} successfully uninstalled. Please restart Flow.</system:String>
<system:String x:Key="plugin_pluginsmanager_update_success_no_restart">Plugin {0} successfully updated. Please restart Flow.</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_modified_error">Plugin {0} has already been modified. Please restart Flow before making any further changes.</system:String>
<!-- 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>
@ -48,4 +56,5 @@
<!-- Settings menu items -->
<system:String x:Key="plugin_pluginsmanager_plugin_settings_unknown_source">Install from unknown source warning</system:String>
<system:String x:Key="plugin_pluginsmanager_plugin_settings_auto_restart">Automatically restart Flow Launcher after installing/uninstalling/updating plugins</system:String>
</ResourceDictionary>

View file

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@ -117,9 +116,19 @@ namespace Flow.Launcher.Plugin.PluginsManager
return;
}
var message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_prompt"),
plugin.Name, plugin.Author,
Environment.NewLine, Environment.NewLine);
string message;
if (Settings.AutoRestartAfterChanging)
{
message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_prompt"),
plugin.Name, plugin.Author,
Environment.NewLine, Environment.NewLine);
}
else
{
message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_prompt_no_restart"),
plugin.Name, plugin.Author,
Environment.NewLine);
}
if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_install_title"),
MessageBoxButton.YesNo) == MessageBoxResult.No)
@ -130,36 +139,48 @@ namespace Flow.Launcher.Plugin.PluginsManager
? $"{plugin.Name}-{Guid.NewGuid()}.zip"
: $"{plugin.Name}-{plugin.Version}.zip";
var filePath = Path.Combine(DataLocation.PluginsDirectory, downloadFilename);
var filePath = Path.Combine(Path.GetTempPath(), downloadFilename);
try
{
await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_download_success"), plugin.Name));
await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
Install(plugin, filePath);
}
catch (Exception e)
catch (HttpRequestException e)
{
if (e is HttpRequestException)
MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_download_error"),
Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"));
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
plugin.Name));
Log.Exception("PluginsManager", "An error occurred while downloading plugin", e, "InstallOrUpdate");
Context.API.ShowMsgError(string.Format(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), plugin.Name),
Context.API.GetTranslation("plugin_pluginsmanager_download_error"));
Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);
return;
}
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_installing_plugin"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_success_restart"), plugin.Name));
Context.API.RestartApp();
catch (Exception e)
{
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
plugin.Name));
Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);
return;
}
if (Settings.AutoRestartAfterChanging)
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_installing_plugin"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_success_restart"),
plugin.Name));
Context.API.RestartApp();
}
else
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_installing_plugin"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_success_no_restart"),
plugin.Name));
}
}
internal async ValueTask<List<Result>> RequestUpdateAsync(string search, CancellationToken token, bool usePrimaryUrlOnly = false)
@ -172,6 +193,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
on existingPlugin.Metadata.ID equals pluginFromManifest.ID
where existingPlugin.Metadata.Version.CompareTo(pluginFromManifest.Version) <
0 // if current version precedes manifest version
&& !PluginManager.PluginModified(existingPlugin.Metadata.ID)
select
new
{
@ -204,36 +226,57 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = x.IcoPath,
Action = e =>
{
string message = string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_update_prompt"),
x.Name, x.Author,
Environment.NewLine, Environment.NewLine);
string message;
if (Settings.AutoRestartAfterChanging)
{
message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_update_prompt"),
x.Name, x.Author,
Environment.NewLine, Environment.NewLine);
}
else
{
message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_update_prompt_no_restart"),
x.Name, x.Author,
Environment.NewLine);
}
if (MessageBox.Show(message,
Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
Uninstall(x.PluginExistingMetadata, false);
var downloadToFilePath = Path.Combine(DataLocation.PluginsDirectory,
var downloadToFilePath = Path.Combine(Path.GetTempPath(),
$"{x.Name}-{x.NewVersion}.zip");
Task.Run(async delegate
_ = Task.Run(async delegate
{
if (File.Exists(downloadToFilePath))
{
File.Delete(downloadToFilePath);
}
await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath)
.ConfigureAwait(false);
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_download_success"), x.Name));
PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin, downloadToFilePath);
Install(x.PluginNewUserPlugin, downloadToFilePath);
Context.API.RestartApp();
if (Settings.AutoRestartAfterChanging)
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_update_success_restart"),
x.Name));
Context.API.RestartApp();
}
else
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_update_success_no_restart"),
x.Name));
}
}).ContinueWith(t =>
{
Log.Exception("PluginsManager", $"Update failed for {x.Name}",
t.Exception.InnerException, "RequestUpdate");
t.Exception.InnerException);
Context.API.ShowMsg(
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(
@ -348,12 +391,13 @@ namespace Flow.Launcher.Plugin.PluginsManager
var results =
PluginsManifest
.UserPlugins
.Where(x => !PluginExists(x.ID) && !PluginManager.PluginModified(x.ID))
.Select(x =>
new Result
{
Title = $"{x.Name} by {x.Author}",
SubTitle = x.Description,
IcoPath = icoPath,
IcoPath = x.IcoPath,
Action = e =>
{
if (e.SpecialKeyState.CtrlPressed)
@ -375,78 +419,29 @@ namespace Flow.Launcher.Plugin.PluginsManager
private void Install(UserPlugin plugin, string downloadedFilePath)
{
if (!File.Exists(downloadedFilePath))
return;
var tempFolderPath = Path.Combine(Path.GetTempPath(), "flowlauncher");
var tempFolderPluginPath = Path.Combine(tempFolderPath, "plugin");
if (Directory.Exists(tempFolderPath))
Directory.Delete(tempFolderPath, true);
Directory.CreateDirectory(tempFolderPath);
var zipFilePath = Path.Combine(tempFolderPath, Path.GetFileName(downloadedFilePath));
File.Copy(downloadedFilePath, zipFilePath);
File.Delete(downloadedFilePath);
Utilities.UnZip(zipFilePath, tempFolderPluginPath, true);
var pluginFolderPath = Utilities.GetContainingFolderPathAfterUnzip(tempFolderPluginPath);
var metadataJsonFilePath = string.Empty;
if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName)))
metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName);
if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
throw new FileNotFoundException($"Plugin {plugin.ID} zip file not found at {downloadedFilePath}", downloadedFilePath);
try
{
MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_install_errormetadatafile"),
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"));
throw new FileNotFoundException(
string.Format("Unable to find plugin.json from the extracted zip file, or this path {0} does not exist", pluginFolderPath));
PluginManager.InstallPlugin(plugin, downloadedFilePath);
File.Delete(downloadedFilePath);
}
if (SameOrLesserPluginVersionExists(metadataJsonFilePath))
catch (FileNotFoundException e)
{
MessageBox.Show(string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_duplicate"), plugin.Name),
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"));
throw new InvalidOperationException(
string.Format("A plugin with the same ID and version already exists, " +
"or the version is greater than this downloaded plugin {0}",
plugin.Name));
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
Context.API.GetTranslation("plugin_pluginsmanager_install_errormetadatafile"));
Log.Exception("Flow.Launcher.Plugin.PluginsManager", e.Message, e);
}
catch (InvalidOperationException e)
{
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_duplicate"), plugin.Name));
Log.Exception("Flow.Launcher.Plugin.PluginsManager", e.Message, e);
}
catch (ArgumentException e) {
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"), plugin.Name));
Log.Exception("Flow.Launcher.Plugin.PluginsManager", e.Message, e);
}
var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";
var defaultPluginIDs = new List<string>
{
"0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
"CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
"572be03c74c642baae319fc283e561a8", // Explorer
"6A122269676E40EB86EB543B945932B9", // PluginIndicator
"9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
"b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
"791FC278BA414111B8D1886DFE447410", // Program
"D409510CD0D2481F853690A07E6DC426", // Shell
"CEA08895D2544B019B2E9C5009600DF4", // Sys
"0308FD86DE0A4DEE8D62B9B535370992", // URL
"565B73353DBF4806919830B9202EE3BF", // WebSearch
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
};
// Treat default plugin differently, it needs to be removable along with each flow release
var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
? DataLocation.PluginsDirectory
: Constant.PreinstalledDirectory;
var newPluginPath = Path.Combine(installDirectory, folderName);
FilesFolders.CopyAll(pluginFolderPath, newPluginPath);
Directory.Delete(pluginFolderPath, true);
}
internal List<Result> RequestUninstall(string search)
@ -461,10 +456,19 @@ namespace Flow.Launcher.Plugin.PluginsManager
IcoPath = x.Metadata.IcoPath,
Action = e =>
{
string message = string.Format(
Context.API.GetTranslation("plugin_pluginsmanager_uninstall_prompt"),
x.Metadata.Name, x.Metadata.Author,
Environment.NewLine, Environment.NewLine);
string message;
if (Settings.AutoRestartAfterChanging)
{
message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_prompt"),
x.Metadata.Name, x.Metadata.Author,
Environment.NewLine, Environment.NewLine);
}
else
{
message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_prompt_no_restart"),
x.Metadata.Name, x.Metadata.Author,
Environment.NewLine);
}
if (MessageBox.Show(message,
Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"),
@ -472,7 +476,16 @@ namespace Flow.Launcher.Plugin.PluginsManager
{
Application.Current.MainWindow.Hide();
Uninstall(x.Metadata);
Context.API.RestartApp();
if (Settings.AutoRestartAfterChanging)
{
Context.API.RestartApp();
}
else
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"),
string.Format(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_success_no_restart"),
x.Metadata.Name));
}
return true;
}
@ -484,24 +497,18 @@ namespace Flow.Launcher.Plugin.PluginsManager
return Search(results, search);
}
private void Uninstall(PluginMetadata plugin, bool removedSetting = true)
private void Uninstall(PluginMetadata plugin)
{
if (removedSetting)
try
{
PluginManager.Settings.Plugins.Remove(plugin.ID);
PluginManager.AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID);
PluginManager.UninstallPlugin(plugin, removeSettings:true);
}
catch (ArgumentException e)
{
Log.Exception("Flow.Launcher.Plugin.PluginsManager", e.Message, e);
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_error_title"),
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"));
}
// Marked for deletion. Will be deleted on next start up
using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, "NeedDelete.txt"));
}
private bool SameOrLesserPluginVersionExists(string metadataPath)
{
var newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataPath));
return Context.API.GetAllPlugins()
.Any(x => x.Metadata.ID == newMetadata.ID
&& newMetadata.Version.CompareTo(x.Metadata.Version) <= 0);
}
}
}

View file

@ -9,5 +9,7 @@
internal const string UpdateCommand = "update";
public bool WarnFromUnknownSource { get; set; } = true;
public bool AutoRestartAfterChanging { get; set; } = true;
}
}

View file

@ -17,5 +17,11 @@
get => Settings.WarnFromUnknownSource;
set => Settings.WarnFromUnknownSource = value;
}
public bool AutoRestartAfterChanging
{
get => Settings.AutoRestartAfterChanging;
set => Settings.AutoRestartAfterChanging = value;
}
}
}

View file

@ -12,10 +12,22 @@
<ColumnDefinition Width="400" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<CheckBox
Grid.Column="0"
Grid.Row="0"
Padding="8,0,0,0"
Content="{DynamicResource plugin_pluginsmanager_plugin_settings_unknown_source}"
IsChecked="{Binding WarnFromUnknownSource}" />
<CheckBox
Grid.Column="0"
Grid.Row="1"
Margin="0,20,0,0"
Padding="8,0,0,0"
Content="{DynamicResource plugin_pluginsmanager_plugin_settings_auto_restart}"
IsChecked="{Binding AutoRestartAfterChanging}" />
</Grid>
</UserControl>

View file

@ -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<Win32[]> _win32Storage;
private static BinaryStorage<UWP.Application[]> _uwpStorage;
private static BinaryStorage<UWPApp[]> _uwpStorage;
private static readonly List<Result> 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<List<Result>> QueryAsync(Query query, CancellationToken token)
@ -76,12 +74,12 @@ namespace Flow.Launcher.Plugin.Program
_settings = context.API.LoadSettingJsonStorage<Settings>();
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[]>("Win32");
_win32s = _win32Storage.TryLoad(Array.Empty<Win32>());
_uwpStorage = new BinaryStorage<UWP.Application[]>("UWP");
_uwps = _uwpStorage.TryLoad(Array.Empty<UWP.Application>());
_win32s = await _win32Storage.TryLoadAsync(Array.Empty<Win32>());
_uwpStorage = new BinaryStorage<UWPApp[]>("UWP");
_uwps = await _uwpStorage.TryLoadAsync(Array.Empty<UWPApp>());
});
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);
}
}

View file

@ -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<Application>();
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<Application>();
// 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<string, PackageVersion> 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<PackageVersion, string> smallLogoNameFromVersion = new()
{
{
PackageVersion.Windows10, "Square44x44Logo"
},
{
PackageVersion.Windows81, "Square30x30Logo"
},
{
PackageVersion.Windows8, "SmallLogo"
},
};
private static readonly Dictionary<PackageVersion, string> 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<Application>();
}
#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<Application>();
}
}
public static bool SupportUWP()
{
var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major;
return support;
}
private static IEnumerable<Package> 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<Package>();
}
}
private static Channel<byte> PackageChangeChannel = Channel.CreateBounded<byte>(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<Result> ContextMenus(IPublicAPI api)
{
var contextMenus = new List<Result>
{
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
}
}
}

View file

@ -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<UWPApp>();
/// <summary>
/// For serialization
/// </summary>
[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<UWPApp>();
// 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<string, PackageVersion> 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<PackageVersion, string> smallLogoNameFromVersion = new()
{
{ PackageVersion.Windows10, "Square44x44Logo" },
{ PackageVersion.Windows81, "Square30x30Logo" },
{ PackageVersion.Windows8, "SmallLogo" },
};
private static readonly Dictionary<PackageVersion, string> 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<UWPApp>();
}
#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<UWPApp>();
}
}
public static bool SupportUWP()
{
var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major;
return support;
}
private static IEnumerable<Package> 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<Package>();
}
}
private static Channel<byte> PackageChangeChannel = Channel.CreateBounded<byte>(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<Result> ContextMenus(IPublicAPI api)
{
var contextMenus = new List<Result>
{
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();
}
}
}

View file

@ -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<Win32>
[MemoryPackable]
public partial class Win32 : IProgram, IEquatable<Win32>
{
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; }
/// <summary>
@ -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<string> EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true)
private static IEnumerable<string> EnumerateProgramsInDir(string directory, string[] suffixes,
bool recursive = true)
{
if (!Directory.Exists(directory))
return Enumerable.Empty<string>();
@ -448,7 +458,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
private static IEnumerable<Win32> UnregisteredPrograms(List<string> directories, string[] suffixes, string[] protocols)
private static IEnumerable<Win32> UnregisteredPrograms(List<string> 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<Win32> PATHPrograms(string[] suffixes, string[] protocols, List<string> commonParents)
private static IEnumerable<Win32> PATHPrograms(string[] suffixes, string[] protocols,
List<string> 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();
}
}

View file

@ -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<ProgramSource> 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<ProgramSource> { selectedProgramSource }, true); // sync status in win32, uwp and disabled
ProgramSettingDisplay.SetProgramSourcesStatus(new List<ProgramSource> { selectedProgramSource },
true); // sync status in win32, uwp and disabled
ProgramSettingDisplay.RemoveDisabledFromSettings();
}
else
{
ProgramSettingDisplay.SetProgramSourcesStatus(new List<ProgramSource> { selectedProgramSource }, false);
ProgramSettingDisplay.SetProgramSourcesStatus(new List<ProgramSource> { 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<ProgramSource>()
.ToList();
.SelectedItems.Cast<ProgramSource>()
.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<ProgramSource>()
.ToList();
.SelectedItems.Cast<ProgramSource>()
.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;