mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into move-exception-message-to-result
This commit is contained in:
commit
ba45069d88
30 changed files with 1277 additions and 1046 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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -36,8 +36,4 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,5 +9,7 @@
|
|||
internal const string UpdateCommand = "update";
|
||||
|
||||
public bool WarnFromUnknownSource { get; set; } = true;
|
||||
|
||||
public bool AutoRestartAfterChanging { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,11 @@
|
|||
get => Settings.WarnFromUnknownSource;
|
||||
set => Settings.WarnFromUnknownSource = value;
|
||||
}
|
||||
|
||||
public bool AutoRestartAfterChanging
|
||||
{
|
||||
get => Settings.AutoRestartAfterChanging;
|
||||
set => Settings.AutoRestartAfterChanging = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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