mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into 250331-NewPluginsPage
This commit is contained in:
commit
adbd262292
31 changed files with 492 additions and 239 deletions
|
|
@ -22,7 +22,7 @@ namespace Flow.Launcher.Core.Configuration
|
|||
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private UpdateManager NewUpdateManager()
|
||||
private static UpdateManager NewUpdateManager()
|
||||
{
|
||||
var applicationFolderName = Constant.ApplicationDirectory
|
||||
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
|
||||
|
|
@ -81,20 +81,16 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
public void RemoveShortcuts()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
|
||||
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
|
||||
}
|
||||
|
||||
public void RemoveUninstallerEntry()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.RemoveUninstallerRegistryEntry();
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.RemoveUninstallerRegistryEntry();
|
||||
}
|
||||
|
||||
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
||||
|
|
@ -110,12 +106,10 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
public void CreateShortcuts()
|
||||
{
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
|
||||
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
|
||||
}
|
||||
|
||||
public void CreateUninstallerEntry()
|
||||
|
|
@ -129,18 +123,14 @@ namespace Flow.Launcher.Core.Configuration
|
|||
subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String);
|
||||
}
|
||||
|
||||
using (var portabilityUpdater = NewUpdateManager())
|
||||
{
|
||||
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||
}
|
||||
using var portabilityUpdater = NewUpdateManager();
|
||||
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
|
||||
}
|
||||
|
||||
internal void IndicateDeletion(string filePathTodelete)
|
||||
private static void IndicateDeletion(string filePathTodelete)
|
||||
{
|
||||
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);
|
||||
using (var _ = File.CreateText(deleteFilePath))
|
||||
{
|
||||
}
|
||||
using var _ = File.CreateText(deleteFilePath);
|
||||
}
|
||||
|
||||
///<summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
|
|
@ -17,11 +18,11 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
||||
|
||||
private static DateTime lastFetchedAt = DateTime.MinValue;
|
||||
private static TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
|
||||
private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
public static List<UserPlugin> UserPlugins { get; private set; }
|
||||
|
||||
public static async Task<bool> UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
|
||||
public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -43,7 +44,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Http request failed", e);
|
||||
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(nameof(PluginsManifest), "Http request failed", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
public record UserPlugin
|
||||
{
|
||||
public string ID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string Website { get; set; }
|
||||
public string UrlDownload { get; set; }
|
||||
public string UrlSourceCode { get; set; }
|
||||
public string LocalInstallPath { get; set; }
|
||||
public string IcoPath { get; set; }
|
||||
public DateTime? LatestReleaseDate { get; set; }
|
||||
public DateTime? DateAdded { get; set; }
|
||||
|
||||
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!;
|
||||
public required IPublicAPI API { get; init; }
|
||||
|
||||
private static readonly string ClassName = nameof(JsonRPCPluginSettings);
|
||||
|
||||
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
|
||||
|
||||
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
|
||||
|
|
@ -122,12 +124,26 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
await _storage.SaveAsync();
|
||||
try
|
||||
{
|
||||
await _storage.SaveAsync();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_storage.Save();
|
||||
try
|
||||
{
|
||||
_storage.Save();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin settings to path: {SettingPath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public bool NeedCreateSettingPanel()
|
||||
|
|
|
|||
|
|
@ -454,16 +454,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
#region Public functions
|
||||
|
||||
public static bool PluginModified(string uuid)
|
||||
public static bool PluginModified(string id)
|
||||
{
|
||||
return _modifiedPlugins.Contains(uuid);
|
||||
return _modifiedPlugins.Contains(id);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
|
||||
/// unless it's a local path installation
|
||||
/// </summary>
|
||||
public static async Task UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(newVersion, zipFilePath, checkModified:false);
|
||||
|
|
@ -471,17 +466,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
_modifiedPlugins.Add(existingVersion.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
|
||||
/// </summary>
|
||||
public static void InstallPlugin(UserPlugin plugin, string zipFilePath)
|
||||
{
|
||||
InstallPlugin(plugin, zipFilePath, checkModified: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstall a plugin.
|
||||
/// </summary>
|
||||
public static async Task UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false)
|
||||
{
|
||||
await UninstallPluginAsync(plugin, removePluginFromSettings, removePluginSettings, true);
|
||||
|
|
@ -525,20 +514,20 @@ namespace Flow.Launcher.Core.Plugin
|
|||
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
|
||||
};
|
||||
{
|
||||
"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)
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ using System.Net;
|
|||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using JetBrains.Annotations;
|
||||
using Squirrel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
|
|
@ -15,8 +17,8 @@ using Flow.Launcher.Infrastructure.Http;
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Squirrel;
|
||||
|
||||
namespace Flow.Launcher.Core
|
||||
{
|
||||
|
|
@ -51,7 +53,7 @@ namespace Flow.Launcher.Core
|
|||
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
|
||||
var currentVersion = Version.Parse(Constant.Version);
|
||||
|
||||
Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>");
|
||||
Log.Info($"|Updater.UpdateApp|Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
|
||||
|
||||
if (newReleaseVersion <= currentVersion)
|
||||
{
|
||||
|
|
@ -70,7 +72,7 @@ namespace Flow.Launcher.Core
|
|||
|
||||
if (DataLocation.PortableDataLocationInUse())
|
||||
{
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}";
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
|
||||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||
|
|
@ -122,7 +124,7 @@ namespace Flow.Launcher.Core
|
|||
}
|
||||
|
||||
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||
private static async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
|
||||
{
|
||||
var uri = new Uri(repository);
|
||||
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
|
||||
|
|
@ -144,12 +146,22 @@ namespace Flow.Launcher.Core
|
|||
return manager;
|
||||
}
|
||||
|
||||
public string NewVersionTips(string version)
|
||||
private static string NewVersionTips(string version)
|
||||
{
|
||||
var translator = InternationalizationManager.Instance;
|
||||
var translator = Ioc.Default.GetRequiredService<Internationalization>();
|
||||
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
|
||||
|
||||
return tips;
|
||||
}
|
||||
|
||||
private static string Formatted<T>(T t)
|
||||
{
|
||||
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
public static class Helper
|
||||
{
|
||||
static Helper()
|
||||
{
|
||||
jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
|
||||
/// </summary>
|
||||
|
|
@ -36,55 +28,5 @@ namespace Flow.Launcher.Infrastructure
|
|||
throw new NullReferenceException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
|
||||
{
|
||||
if (!Directory.Exists(dataDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
}
|
||||
|
||||
foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
|
||||
{
|
||||
var data = Path.GetFileName(bundledDataPath);
|
||||
var dataPath = Path.Combine(dataDirectory, data.NonNull());
|
||||
if (!File.Exists(dataPath))
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
|
||||
var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
|
||||
if (time1 != time2)
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions jsonFormattedSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
public static string Formatted<T>(this T t)
|
||||
{
|
||||
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using static Flow.Launcher.Infrastructure.Http.Http;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
|
|
@ -28,7 +26,6 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
public const int SmallIconSize = 64;
|
||||
public const int FullIconSize = 256;
|
||||
|
||||
|
||||
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
||||
|
||||
public static async Task InitializeAsync()
|
||||
|
|
@ -61,7 +58,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
});
|
||||
}
|
||||
|
||||
public static async Task Save()
|
||||
public static async Task SaveAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
|
||||
|
|
@ -71,12 +68,22 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
.Select(x => x.Key)
|
||||
.ToList());
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception($"|ImageLoader.SaveAsync|Failed to save image cache to file", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WaitSaveAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
storageLock.Release();
|
||||
}
|
||||
|
||||
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
|
|
@ -173,7 +180,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
|
||||
{
|
||||
// Download image from url
|
||||
await using var resp = await GetStreamAsync(uriResult);
|
||||
await using var resp = await Http.Http.GetStreamAsync(uriResult);
|
||||
await using var buffer = new MemoryStream();
|
||||
await resp.CopyToAsync(buffer);
|
||||
buffer.Seek(0, SeekOrigin.Begin);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using MemoryPack;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
|
|
@ -21,7 +22,7 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
public BinaryStorage(string filename, string directoryPath = null)
|
||||
{
|
||||
directoryPath ??= DataLocation.CacheDirectory;
|
||||
Helper.ValidateDirectory(directoryPath);
|
||||
FilesFolders.ValidateDirectory(directoryPath);
|
||||
|
||||
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,51 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
|
||||
{
|
||||
private static readonly string ClassName = "FlowLauncherJsonStorage";
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public FlowLauncherJsonStorage()
|
||||
{
|
||||
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
|
||||
Helper.ValidateDirectory(directoryPath);
|
||||
FilesFolders.ValidateDirectory(directoryPath);
|
||||
|
||||
var filename = typeof(T).Name;
|
||||
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
|
||||
}
|
||||
|
||||
public new void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Save();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public new async Task SaveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await base.SaveAsync();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save FL settings to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.IO;
|
|||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
|
|
@ -37,7 +38,7 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
FilePath = filePath;
|
||||
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
|
||||
|
||||
Helper.ValidateDirectory(DirectoryPath);
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
}
|
||||
|
||||
public async Task<T> LoadAsync()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
|
|
@ -8,13 +12,19 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
// Use assembly name to check which plugin is using this storage
|
||||
public readonly string AssemblyName;
|
||||
|
||||
private static readonly string ClassName = "PluginJsonStorage";
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public PluginJsonStorage()
|
||||
{
|
||||
// C# related, add python related below
|
||||
var dataType = typeof(T);
|
||||
AssemblyName = dataType.Assembly.GetName().Name;
|
||||
DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName);
|
||||
Helper.ValidateDirectory(DirectoryPath);
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
|
||||
}
|
||||
|
|
@ -23,5 +33,29 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public new void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Save();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
|
||||
public new async Task SaveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await base.SaveAsync();
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to save plugin settings to path: {FilePath}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,16 @@ namespace Flow.Launcher.Infrastructure
|
|||
return PInvoke.SetForegroundWindow(new(handle));
|
||||
}
|
||||
|
||||
public static bool IsForegroundWindow(Window window)
|
||||
{
|
||||
return IsForegroundWindow(GetWindowHandle(window));
|
||||
}
|
||||
|
||||
internal static bool IsForegroundWindow(HWND handle)
|
||||
{
|
||||
return handle.Equals(PInvoke.GetForegroundWindow());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task Switching
|
||||
|
|
@ -354,10 +364,20 @@ namespace Flow.Launcher.Infrastructure
|
|||
// No installed English layout found
|
||||
if (enHKL == HKL.Null) return;
|
||||
|
||||
// Get the current foreground window
|
||||
var hwnd = PInvoke.GetForegroundWindow();
|
||||
// When application is exiting, the Application.Current will be null
|
||||
if (Application.Current == null) return;
|
||||
|
||||
// Get the FL main window
|
||||
var hwnd = GetWindowHandle(Application.Current.MainWindow, true);
|
||||
if (hwnd == HWND.Null) return;
|
||||
|
||||
// Check if the FL main window is the current foreground window
|
||||
if (!IsForegroundWindow(hwnd))
|
||||
{
|
||||
var result = PInvoke.SetForegroundWindow(hwnd);
|
||||
if (!result) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
// Get the current foreground window thread ID
|
||||
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
|
||||
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
|
|
@ -367,12 +387,10 @@ namespace Flow.Launcher.Infrastructure
|
|||
// the IME mode instead of switching to another layout.
|
||||
var currentLayout = PInvoke.GetKeyboardLayout(threadId);
|
||||
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
|
||||
foreach (var langTag in ImeLanguageTags)
|
||||
foreach (var imeLangTag in ImeLanguageTags)
|
||||
{
|
||||
if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var langTag = GetLanguageTag(currentLangId);
|
||||
if (langTag.StartsWith(imeLangTag, StringComparison.OrdinalIgnoreCase)) return;
|
||||
}
|
||||
|
||||
// Backup current keyboard layout
|
||||
|
|
@ -488,5 +506,16 @@ namespace Flow.Launcher.Infrastructure
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Notification
|
||||
|
||||
public static bool IsNotificationSupported()
|
||||
{
|
||||
// Notifications only supported on Windows 10 19041+
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
Environment.OSVersion.Version.Build >= 19041;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -344,5 +344,62 @@ namespace Flow.Launcher.Plugin
|
|||
/// Stop the loading bar in main window
|
||||
/// </summary>
|
||||
public void StopLoadingBar();
|
||||
|
||||
/// <summary>
|
||||
/// Update the plugin manifest
|
||||
/// </summary>
|
||||
/// <param name="usePrimaryUrlOnly">
|
||||
/// FL has multiple urls to download the plugin manifest. Set this to true to only use the primary url.
|
||||
/// </param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns>True if the manifest is updated successfully, false otherwise</returns>
|
||||
public Task<bool> UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the plugin manifest
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IReadOnlyList<UserPlugin> GetPluginManifest();
|
||||
|
||||
/// <summary>
|
||||
/// Check if the plugin has been modified.
|
||||
/// If this plugin is updated, installed or uninstalled and users do not restart the app,
|
||||
/// it will be marked as modified
|
||||
/// </summary>
|
||||
/// <param name="id">Plugin id</param>
|
||||
/// <returns></returns>
|
||||
public bool PluginModified(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
|
||||
/// unless it's a local path installation
|
||||
/// </summary>
|
||||
/// <param name="pluginMetadata">The metadata of the old plugin to update</param>
|
||||
/// <param name="plugin">The new plugin to update</param>
|
||||
/// <param name="zipFilePath">
|
||||
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Install a plugin. By default will remove the zip file if installation is from url,
|
||||
/// unless it's a local path installation
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin to install</param>
|
||||
/// <param name="zipFilePath">
|
||||
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
|
||||
/// </param>
|
||||
public void InstallPlugin(UserPlugin plugin, string zipFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Uninstall a plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginMetadata">The metadata of the plugin to uninstall</param>
|
||||
/// <param name="removePluginSettings">
|
||||
/// Plugin has their own settings. If this is set to true, the plugin settings will be removed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,5 +318,51 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
{
|
||||
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a directory, creating it if it doesn't exist
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
public static void ValidateDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a data directory, synchronizing it by ensuring all files from a bundled source directory exist in it.
|
||||
/// If files are missing or outdated, they are copied from the bundled directory to the data directory.
|
||||
/// </summary>
|
||||
/// <param name="bundledDataDirectory"></param>
|
||||
/// <param name="dataDirectory"></param>
|
||||
public static void ValidateDataDirectory(string bundledDataDirectory, string dataDirectory)
|
||||
{
|
||||
if (!Directory.Exists(dataDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
}
|
||||
|
||||
foreach (var bundledDataPath in Directory.GetFiles(bundledDataDirectory))
|
||||
{
|
||||
var data = Path.GetFileName(bundledDataPath);
|
||||
if (data == null) continue;
|
||||
var dataPath = Path.Combine(dataDirectory, data);
|
||||
if (!File.Exists(dataPath))
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var time1 = new FileInfo(bundledDataPath).LastWriteTimeUtc;
|
||||
var time2 = new FileInfo(dataPath).LastWriteTimeUtc;
|
||||
if (time1 != time2)
|
||||
{
|
||||
File.Copy(bundledDataPath, dataPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
Flow.Launcher.Plugin/UserPlugin.cs
Normal file
80
Flow.Launcher.Plugin/UserPlugin.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// User Plugin Model for Flow Launcher
|
||||
/// </summary>
|
||||
public record UserPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier of the plugin
|
||||
/// </summary>
|
||||
public string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the plugin
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Description of the plugin
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Author of the plugin
|
||||
/// </summary>
|
||||
public string Author { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the plugin
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Allow language of the plugin <see cref="AllowedLanguage"/>
|
||||
/// </summary>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Website of the plugin
|
||||
/// </summary>
|
||||
public string Website { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to download the plugin
|
||||
/// </summary>
|
||||
public string UrlDownload { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL to the source code of the plugin
|
||||
/// </summary>
|
||||
public string UrlSourceCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local path where the plugin is installed
|
||||
/// </summary>
|
||||
public string LocalInstallPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Icon path of the plugin
|
||||
/// </summary>
|
||||
public string IcoPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date when the plugin was last updated
|
||||
/// </summary>
|
||||
public DateTime? LatestReleaseDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date when the plugin was added to the local system
|
||||
/// </summary>
|
||||
public DateTime? DateAdded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the plugin is installed from a local path
|
||||
/// </summary>
|
||||
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -179,8 +179,6 @@ namespace Flow.Launcher
|
|||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
HotKeyMapper.Initialize();
|
||||
|
||||
// main windows needs initialized before theme change because of blur settings
|
||||
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
|
||||
|
||||
|
|
@ -304,6 +302,14 @@ namespace Flow.Launcher
|
|||
return;
|
||||
}
|
||||
|
||||
// If we call Environment.Exit(0), the application dispose will be called before _mainWindow.Close()
|
||||
// Accessing _mainWindow?.Dispatcher will cause the application stuck
|
||||
// So here we need to check it and just return so that we will not acees _mainWindow?.Dispatcher
|
||||
if (!_mainWindow.CanClose)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ using System.Windows.Threading;
|
|||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.Image;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
|
@ -32,6 +34,13 @@ namespace Flow.Launcher
|
|||
{
|
||||
public partial class MainWindow : IDisposable
|
||||
{
|
||||
#region Public Property
|
||||
|
||||
// Window Event: Close Event
|
||||
public bool CanClose { get; set; } = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
// Dependency Injection
|
||||
|
|
@ -45,8 +54,6 @@ namespace Flow.Launcher
|
|||
private readonly ContextMenu _contextMenu = new();
|
||||
private readonly MainViewModel _viewModel;
|
||||
|
||||
// Window Event: Close Event
|
||||
private bool _canClose = false;
|
||||
// Window Event: Key Event
|
||||
private bool _isArrowKeyPressed = false;
|
||||
|
||||
|
|
@ -168,6 +175,9 @@ namespace Flow.Launcher
|
|||
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
|
||||
_viewModel.QueryTextCursorMovedToEnd = false;
|
||||
|
||||
// Initialize hotkey mapper after window is loaded
|
||||
HotKeyMapper.Initialize();
|
||||
|
||||
// View model property changed event
|
||||
_viewModel.PropertyChanged += (o, e) =>
|
||||
{
|
||||
|
|
@ -279,15 +289,16 @@ namespace Flow.Launcher
|
|||
|
||||
private async void OnClosing(object sender, CancelEventArgs e)
|
||||
{
|
||||
if (!_canClose)
|
||||
if (!CanClose)
|
||||
{
|
||||
_notifyIcon.Visible = false;
|
||||
App.API.SaveAppAllSettings();
|
||||
e.Cancel = true;
|
||||
await ImageLoader.WaitSaveAsync();
|
||||
await PluginManager.DisposePluginsAsync();
|
||||
Notification.Uninstall();
|
||||
// After plugins are all disposed, we can close the main window
|
||||
_canClose = true;
|
||||
CanClose = true;
|
||||
// Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ namespace Flow.Launcher
|
|||
{
|
||||
internal static class Notification
|
||||
{
|
||||
internal static bool legacy = Environment.OSVersion.Version.Build < 19041;
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
|
||||
internal static bool legacy = !Win32Helper.IsNotificationSupported();
|
||||
|
||||
internal static void Uninstall()
|
||||
{
|
||||
if (!legacy)
|
||||
|
|
@ -25,7 +25,6 @@ namespace Flow.Launcher
|
|||
});
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
|
||||
private static void ShowInternal(string title, string subTitle, string iconPath = null)
|
||||
{
|
||||
// Handle notification for win7/8/early win10
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ using Flow.Launcher.Plugin.SharedCommands;
|
|||
using Flow.Launcher.ViewModel;
|
||||
using JetBrains.Annotations;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
|
|
@ -37,6 +38,8 @@ namespace Flow.Launcher
|
|||
private readonly Internationalization _translater;
|
||||
private readonly MainViewModel _mainVM;
|
||||
|
||||
private readonly object _saveSettingsLock = new();
|
||||
|
||||
#region Constructor
|
||||
|
||||
public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM)
|
||||
|
|
@ -57,21 +60,28 @@ namespace Flow.Launcher
|
|||
_mainVM.ChangeQueryText(query, requery);
|
||||
}
|
||||
|
||||
public void RestartApp()
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
|
||||
public async void RestartApp()
|
||||
{
|
||||
_mainVM.Hide();
|
||||
|
||||
// we must manually save
|
||||
// We must manually save
|
||||
// UpdateManager.RestartApp() will call Environment.Exit(0)
|
||||
// which will cause ungraceful exit
|
||||
SaveAppAllSettings();
|
||||
|
||||
// Wait for all image caches to be saved before restarting
|
||||
await ImageLoader.WaitSaveAsync();
|
||||
|
||||
// Restart requires Squirrel's Update.exe to be present in the parent folder,
|
||||
// it is only published from the project's release pipeline. When debugging without it,
|
||||
// the project may not restart or just terminates. This is expected.
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
|
||||
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||
|
||||
public void ShowMainWindow() => _mainVM.Show();
|
||||
|
||||
public void HideMainWindow() => _mainVM.Hide();
|
||||
|
|
@ -85,10 +95,13 @@ namespace Flow.Launcher
|
|||
|
||||
public void SaveAppAllSettings()
|
||||
{
|
||||
PluginManager.Save();
|
||||
_mainVM.Save();
|
||||
_settings.Save();
|
||||
_ = ImageLoader.Save();
|
||||
lock (_saveSettingsLock)
|
||||
{
|
||||
_settings.Save();
|
||||
PluginManager.Save();
|
||||
_mainVM.Save();
|
||||
}
|
||||
_ = ImageLoader.SaveAsync();
|
||||
}
|
||||
|
||||
public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();
|
||||
|
|
@ -342,6 +355,22 @@ namespace Flow.Launcher
|
|||
|
||||
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action cancelProgress = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, cancelProgress);
|
||||
|
||||
public Task<bool> UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) =>
|
||||
PluginsManifest.UpdateManifestAsync(usePrimaryUrlOnly, token);
|
||||
|
||||
public IReadOnlyList<UserPlugin> GetPluginManifest() => PluginsManifest.UserPlugins;
|
||||
|
||||
public bool PluginModified(string id) => PluginManager.PluginModified(id);
|
||||
|
||||
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) =>
|
||||
PluginManager.UpdatePluginAsync(pluginMetadata, plugin, zipFilePath);
|
||||
|
||||
public void InstallPlugin(UserPlugin plugin, string zipFilePath) =>
|
||||
PluginManager.InstallPlugin(plugin, zipFilePath);
|
||||
|
||||
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) =>
|
||||
PluginManager.UninstallPluginAsync(pluginMetadata, removePluginSettings);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
|
@ -14,7 +13,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
|
|||
public string FilterText { get; set; } = string.Empty;
|
||||
|
||||
public IList<PluginStoreItemViewModel> ExternalPlugins =>
|
||||
PluginsManifest.UserPlugins?.Select(p => new PluginStoreItemViewModel(p))
|
||||
App.API.GetPluginManifest()?.Select(p => new PluginStoreItemViewModel(p))
|
||||
.OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease)
|
||||
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated)
|
||||
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.None)
|
||||
|
|
@ -24,7 +23,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
|
|||
[RelayCommand]
|
||||
private async Task RefreshExternalPluginsAsync()
|
||||
{
|
||||
if (await PluginsManifest.UpdateManifestAsync())
|
||||
if (await App.API.UpdatePluginManifestAsync())
|
||||
{
|
||||
OnPropertyChanged(nameof(ExternalPlugins));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Plugin;
|
||||
using SemanticVersioning;
|
||||
using Version = SemanticVersioning.Version;
|
||||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Flow.Launcher.Plugin.PluginsManager
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
@ -37,4 +35,8 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using Flow.Launcher.Plugin.PluginsManager.ViewModels;
|
||||
using Flow.Launcher.Plugin.PluginsManager.Views;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Controls;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Flow.Launcher.Plugin.PluginsManager.ViewModels;
|
||||
using Flow.Launcher.Plugin.PluginsManager.Views;
|
||||
|
||||
namespace Flow.Launcher.Plugin.PluginsManager
|
||||
{
|
||||
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n
|
||||
{
|
||||
internal PluginInitContext Context { get; set; }
|
||||
internal static PluginInitContext Context { get; set; }
|
||||
|
||||
internal Settings Settings;
|
||||
|
||||
|
|
@ -35,7 +33,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
contextMenu = new ContextMenu(Context);
|
||||
pluginManager = new PluginsManager(Context, Settings);
|
||||
|
||||
await PluginsManifest.UpdateManifestAsync();
|
||||
await Context.API.UpdatePluginManifestAsync();
|
||||
}
|
||||
|
||||
public List<Result> LoadContextMenus(Result selectedResult)
|
||||
|
|
@ -56,7 +54,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
Settings.UpdateCommand => await pluginManager.RequestUpdateAsync(query.SecondToEndSearch, token, query.IsReQuery),
|
||||
_ => pluginManager.GetDefaultHotKeys().Where(hotkey =>
|
||||
{
|
||||
hotkey.Score = StringMatcher.FuzzySearch(query.Search, hotkey.Title).Score;
|
||||
hotkey.Score = Context.API.FuzzySearch(query.Search, hotkey.Title).Score;
|
||||
return hotkey.Score > 0;
|
||||
}).ToList()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -12,12 +6,15 @@ using System.Net.Http;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Plugin.PluginsManager
|
||||
{
|
||||
internal class PluginsManager
|
||||
{
|
||||
private const string zip = "zip";
|
||||
private const string ZipSuffix = "zip";
|
||||
|
||||
private static readonly string ClassName = nameof(PluginsManager);
|
||||
|
||||
private PluginInitContext Context { get; set; }
|
||||
|
||||
|
|
@ -51,7 +48,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
{
|
||||
return new List<Result>()
|
||||
{
|
||||
new Result()
|
||||
new()
|
||||
{
|
||||
Title = Settings.InstallCommand,
|
||||
IcoPath = icoPath,
|
||||
|
|
@ -63,7 +60,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return false;
|
||||
}
|
||||
},
|
||||
new Result()
|
||||
new()
|
||||
{
|
||||
Title = Settings.UninstallCommand,
|
||||
IcoPath = icoPath,
|
||||
|
|
@ -75,7 +72,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return false;
|
||||
}
|
||||
},
|
||||
new Result()
|
||||
new()
|
||||
{
|
||||
Title = Settings.UpdateCommand,
|
||||
IcoPath = icoPath,
|
||||
|
|
@ -169,7 +166,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
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);
|
||||
Context.API.LogException(ClassName, "An error occurred while downloading plugin", e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -179,7 +176,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
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);
|
||||
Context.API.LogException(ClassName, "An error occurred while downloading plugin", e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -237,7 +234,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
internal async ValueTask<List<Result>> RequestUpdateAsync(string search, CancellationToken token,
|
||||
bool usePrimaryUrlOnly = false)
|
||||
{
|
||||
await PluginsManifest.UpdateManifestAsync(token, usePrimaryUrlOnly);
|
||||
await Context.API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
|
||||
|
||||
var pluginFromLocalPath = null as UserPlugin;
|
||||
var updateFromLocalPath = false;
|
||||
|
|
@ -250,7 +247,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
|
||||
var updateSource = !updateFromLocalPath
|
||||
? PluginsManifest.UserPlugins
|
||||
? Context.API.GetPluginManifest()
|
||||
: new List<UserPlugin> { pluginFromLocalPath };
|
||||
|
||||
var resultsForUpdate = (
|
||||
|
|
@ -260,7 +257,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
|
||||
StringComparison.InvariantCulture) <
|
||||
0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
|
||||
&& !PluginManager.PluginModified(existingPlugin.Metadata.ID)
|
||||
&& !Context.API.PluginModified(existingPlugin.Metadata.ID)
|
||||
select
|
||||
new
|
||||
{
|
||||
|
|
@ -276,7 +273,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
if (!resultsForUpdate.Any())
|
||||
return new List<Result>
|
||||
{
|
||||
new Result
|
||||
new()
|
||||
{
|
||||
Title = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_title"),
|
||||
SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_subtitle"),
|
||||
|
|
@ -341,7 +338,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
else
|
||||
{
|
||||
await PluginManager.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin,
|
||||
await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlugin,
|
||||
downloadToFilePath);
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
|
|
@ -366,7 +363,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
}
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
Log.Exception("PluginsManager", $"Update failed for {x.Name}",
|
||||
Context.API.LogException(ClassName, $"Update failed for {x.Name}",
|
||||
t.Exception.InnerException);
|
||||
Context.API.ShowMsg(
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
|
||||
|
|
@ -433,12 +430,12 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
if (cts.IsCancellationRequested)
|
||||
return;
|
||||
else
|
||||
await PluginManager.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
|
||||
await Context.API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin,
|
||||
downloadToFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Exception("PluginsManager", $"Update failed for {plugin.Name}", ex.InnerException);
|
||||
Context.API.LogException(ClassName, $"Update failed for {plugin.Name}", ex.InnerException);
|
||||
Context.API.ShowMsg(
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
|
||||
string.Format(
|
||||
|
|
@ -486,7 +483,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
return results
|
||||
.Where(x =>
|
||||
{
|
||||
var matchResult = StringMatcher.FuzzySearch(searchName, x.Title);
|
||||
var matchResult = Context.API.FuzzySearch(searchName, x.Title);
|
||||
if (matchResult.IsSearchPrecisionScoreMet())
|
||||
x.Score = matchResult.Score;
|
||||
|
||||
|
|
@ -498,7 +495,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
internal List<Result> InstallFromWeb(string url)
|
||||
{
|
||||
var filename = url.Split("/").Last();
|
||||
var name = filename.Split(string.Format(".{0}", zip)).First();
|
||||
var name = filename.Split(string.Format(".{0}", ZipSuffix)).First();
|
||||
|
||||
var plugin = new UserPlugin
|
||||
{
|
||||
|
|
@ -552,7 +549,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
|
||||
return new List<Result>
|
||||
{
|
||||
new Result
|
||||
new()
|
||||
{
|
||||
Title = $"{plugin.Name} by {plugin.Author}",
|
||||
SubTitle = plugin.Description,
|
||||
|
|
@ -602,19 +599,18 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
internal async ValueTask<List<Result>> RequestInstallOrUpdateAsync(string search, CancellationToken token,
|
||||
bool usePrimaryUrlOnly = false)
|
||||
{
|
||||
await PluginsManifest.UpdateManifestAsync(token, usePrimaryUrlOnly);
|
||||
await Context.API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
|
||||
|
||||
if (Uri.IsWellFormedUriString(search, UriKind.Absolute)
|
||||
&& search.Split('.').Last() == zip)
|
||||
&& search.Split('.').Last() == ZipSuffix)
|
||||
return InstallFromWeb(search);
|
||||
|
||||
if (FilesFolders.IsZipFilePath(search, checkFileExists: true))
|
||||
return InstallFromLocalPath(search);
|
||||
|
||||
var results =
|
||||
PluginsManifest
|
||||
.UserPlugins
|
||||
.Where(x => !PluginExists(x.ID) && !PluginManager.PluginModified(x.ID))
|
||||
Context.API.GetPluginManifest()
|
||||
.Where(x => !PluginExists(x.ID) && !Context.API.PluginModified(x.ID))
|
||||
.Select(x =>
|
||||
new Result
|
||||
{
|
||||
|
|
@ -647,7 +643,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
|
||||
try
|
||||
{
|
||||
PluginManager.InstallPlugin(plugin, downloadedFilePath);
|
||||
Context.API.InstallPlugin(plugin, downloadedFilePath);
|
||||
|
||||
if (!plugin.IsFromLocalInstallPath)
|
||||
File.Delete(downloadedFilePath);
|
||||
|
|
@ -656,21 +652,21 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
{
|
||||
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);
|
||||
Context.API.LogException(ClassName, 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);
|
||||
Context.API.LogException(ClassName, 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);
|
||||
Context.API.LogException(ClassName, e.Message, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -740,11 +736,11 @@ namespace Flow.Launcher.Plugin.PluginsManager
|
|||
Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_subtitle"),
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_title"),
|
||||
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
|
||||
await PluginManager.UninstallPluginAsync(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings);
|
||||
await Context.API.UninstallPluginAsync(plugin, removePluginSettings);
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
Log.Exception("Flow.Launcher.Plugin.PluginsManager", e.Message, e);
|
||||
Context.API.LogException(ClassName, e.Message, e);
|
||||
Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_error_title"),
|
||||
Context.API.GetTranslation("plugin_pluginsmanager_plugin_modified_error"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Flow.Launcher.Core.ExternalPlugins;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Controls;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.Program.Programs;
|
||||
using Flow.Launcher.Plugin.Program.Views;
|
||||
using Flow.Launcher.Plugin.Program.Views.Models;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Path = System.IO.Path;
|
||||
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
|
||||
|
|
@ -191,7 +191,7 @@ namespace Flow.Launcher.Plugin.Program
|
|||
|
||||
await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", async () =>
|
||||
{
|
||||
Helper.ValidateDirectory(Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
|
||||
FilesFolders.ValidateDirectory(Context.CurrentPluginMetadata.PluginCacheDirectoryPath);
|
||||
|
||||
static void MoveFile(string sourcePath, string destinationPath)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Controls;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Plugin.WebSearch
|
||||
|
|
@ -180,7 +179,7 @@ namespace Flow.Launcher.Plugin.WebSearch
|
|||
|
||||
// Default images directory is in the WebSearch's application folder
|
||||
DefaultImagesDirectory = Path.Combine(pluginDirectory, Images);
|
||||
Helper.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory);
|
||||
FilesFolders.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory);
|
||||
|
||||
// Custom images directory is in the WebSearch's data location folder
|
||||
CustomImagesDirectory = Path.Combine(_context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "CustomIcons");
|
||||
|
|
|
|||
Loading…
Reference in a new issue