mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into update_all
This commit is contained in:
commit
be47d613e4
18 changed files with 936 additions and 890 deletions
10
.github/actions/spelling/expect.txt
vendored
10
.github/actions/spelling/expect.txt
vendored
|
|
@ -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
|
||||
|
|
|
|||
3
.github/actions/spelling/patterns.txt
vendored
3
.github/actions/spelling/patterns.txt
vendored
|
|
@ -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_]))
|
||||
|
|
|
|||
7
.github/workflows/spelling.yml
vendored
7
.github/workflows/spelling.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -212,7 +212,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new FlowPluginException(metadata, e);
|
||||
Result r = new()
|
||||
{
|
||||
Title = $"{metadata.Name}: Failed to respond!",
|
||||
SubTitle = "Select this result for more info",
|
||||
IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon,
|
||||
PluginDirectory = metadata.PluginDirectory,
|
||||
ActionKeywordAssigned = query.ActionKeyword,
|
||||
PluginID = metadata.ID,
|
||||
OriginQuery = query,
|
||||
Action = _ => { throw new FlowPluginException(metadata, e);},
|
||||
Score = -100
|
||||
};
|
||||
results.Add(r);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,12 +53,13 @@
|
|||
<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" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.13.0" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
|
||||
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->
|
||||
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@
|
|||
|
||||
public bool WarnFromUnknownSource { get; set; } = true;
|
||||
|
||||
public bool AutoRestartAfterChanging { get; set; } = false;
|
||||
public bool AutoRestartAfterChanging { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
752
Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs
Normal file
752
Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue