Merge branch 'dev' into binary_storage_api

This commit is contained in:
Jack Ye 2025-04-04 16:41:08 +08:00 committed by GitHub
commit 8cd81b74f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 490 additions and 223 deletions

View file

@ -22,7 +22,7 @@ namespace Flow.Launcher.Core.Configuration
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private UpdateManager NewUpdateManager() private static UpdateManager NewUpdateManager()
{ {
var applicationFolderName = Constant.ApplicationDirectory var applicationFolderName = Constant.ApplicationDirectory
.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None) .Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None)
@ -81,20 +81,16 @@ namespace Flow.Launcher.Core.Configuration
public void RemoveShortcuts() public void RemoveShortcuts()
{ {
using (var portabilityUpdater = NewUpdateManager()) using var portabilityUpdater = NewUpdateManager();
{ portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu);
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop);
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
}
} }
public void RemoveUninstallerEntry() public void RemoveUninstallerEntry()
{ {
using (var portabilityUpdater = NewUpdateManager()) using var portabilityUpdater = NewUpdateManager();
{ portabilityUpdater.RemoveUninstallerRegistryEntry();
portabilityUpdater.RemoveUninstallerRegistryEntry();
}
} }
public void MoveUserDataFolder(string fromLocation, string toLocation) public void MoveUserDataFolder(string fromLocation, string toLocation)
@ -110,12 +106,10 @@ namespace Flow.Launcher.Core.Configuration
public void CreateShortcuts() public void CreateShortcuts()
{ {
using (var portabilityUpdater = NewUpdateManager()) using var portabilityUpdater = NewUpdateManager();
{ portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false);
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false);
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
}
} }
public void CreateUninstallerEntry() public void CreateUninstallerEntry()
@ -129,18 +123,14 @@ namespace Flow.Launcher.Core.Configuration
subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String); subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String);
} }
using (var portabilityUpdater = NewUpdateManager()) using var portabilityUpdater = NewUpdateManager();
{ _ = portabilityUpdater.CreateUninstallerRegistryEntry();
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
}
} }
internal void IndicateDeletion(string filePathTodelete) private static void IndicateDeletion(string filePathTodelete)
{ {
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile); var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);
using (var _ = File.CreateText(deleteFilePath)) using var _ = File.CreateText(deleteFilePath);
{
}
} }
///<summary> ///<summary>

View file

@ -1,15 +1,14 @@
using Flow.Launcher.Infrastructure.Logger; using System;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Forms; using System.Windows.Forms;
using Flow.Launcher.Core.Resource;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.ExternalPlugins.Environments namespace Flow.Launcher.Core.ExternalPlugins.Environments
{ {
@ -43,8 +42,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal IEnumerable<PluginPair> Setup() internal IEnumerable<PluginPair> Setup()
{ {
// If no plugin is using the language, return empty list
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase))) if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
{
return new List<PluginPair>(); return new List<PluginPair>();
}
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath)) if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
{ {
@ -56,24 +58,55 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
} }
var noRuntimeMessage = string.Format( var noRuntimeMessage = string.Format(
InternationalizationManager.Instance.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"), API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
Language, Language,
EnvName, EnvName,
Environment.NewLine Environment.NewLine
); );
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{ {
var msg = string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName); var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
string selectedFile;
selectedFile = GetFileFromDialog(msg, FileDialogFilter); var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
if (!string.IsNullOrEmpty(selectedFile)) if (!string.IsNullOrEmpty(selectedFile))
{
PluginsSettingsFilePath = selectedFile; PluginsSettingsFilePath = selectedFile;
}
// Nothing selected because user pressed cancel from the file dialog window // Nothing selected because user pressed cancel from the file dialog window
if (string.IsNullOrEmpty(selectedFile)) else
InstallEnvironment(); {
var forceDownloadMessage = string.Format(
API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
Language,
EnvName,
Environment.NewLine
);
// Let users select valid path or choose to download
while (string.IsNullOrEmpty(selectedFile))
{
if (API.ShowMsgBox(forceDownloadMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
// Continue select file
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
}
else
{
// User selected no, break the loop
break;
}
}
if (!string.IsNullOrEmpty(selectedFile))
{
PluginsSettingsFilePath = selectedFile;
}
else
{
InstallEnvironment();
}
}
} }
else else
{ {
@ -86,7 +119,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
} }
else else
{ {
API.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language)); API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
Log.Error("PluginsLoader", Log.Error("PluginsLoader",
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.", $"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment"); $"{Language}Environment");
@ -99,13 +132,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath) private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
{ {
if (expectedPath == currentPath) if (expectedPath == currentPath) return;
return;
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s)); FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
InstallEnvironment(); InstallEnvironment();
} }
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata); internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
@ -126,7 +157,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
return pluginPairs; return pluginPairs;
} }
private string GetFileFromDialog(string title, string filter = "") private static string GetFileFromDialog(string title, string filter = "")
{ {
var dlg = new OpenFileDialog var dlg = new OpenFileDialog
{ {
@ -140,7 +171,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
var result = dlg.ShowDialog(); var result = dlg.ShowDialog();
return result == DialogResult.OK ? dlg.FileName : string.Empty; return result == DialogResult.OK ? dlg.FileName : string.Empty;
} }
/// <summary> /// <summary>
@ -183,31 +213,33 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
else else
{ {
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName)) if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
{
settings.PluginSettings.PythonExecutablePath settings.PluginSettings.PythonExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath); = GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
}
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName)) if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
{
settings.PluginSettings.NodeExecutablePath settings.PluginSettings.NodeExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath); = GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
}
} }
} }
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName) private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
{ {
if (string.IsNullOrEmpty(filePath)) if (string.IsNullOrEmpty(filePath)) return false;
return false;
// DataLocation.PortableDataPath returns the current portable path, this determines if an out // DataLocation.PortableDataPath returns the current portable path, this determines if an out
// of date path is also a portable path. // of date path is also a portable path.
var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}"; var portableAppEnvLocation = Path.Combine("UserData", DataLocation.PluginEnvironments, pluginEnvironmentName);
return filePath.Contains(portableAppEnvLocation); return filePath.Contains(portableAppEnvLocation);
} }
private static bool IsUsingRoamingPath(string filePath) private static bool IsUsingRoamingPath(string filePath)
{ {
if (string.IsNullOrEmpty(filePath)) if (string.IsNullOrEmpty(filePath)) return false;
return false;
return filePath.StartsWith(DataLocation.RoamingDataPath); return filePath.StartsWith(DataLocation.RoamingDataPath);
} }
@ -217,8 +249,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
var index = filePath.IndexOf(DataLocation.PluginEnvironments); var index = filePath.IndexOf(DataLocation.PluginEnvironments);
// get the substring after "Environments" because we can not determine it dynamically // get the substring after "Environments" because we can not determine it dynamically
var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count()); var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}"; return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
} }
} }
} }

View file

@ -1,10 +1,10 @@
using Droplex; using System.Collections.Generic;
using System.IO;
using Droplex;
using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using System.Collections.Generic;
using System.IO;
namespace Flow.Launcher.Core.ExternalPlugins.Environments namespace Flow.Launcher.Core.ExternalPlugins.Environments
{ {
@ -22,7 +22,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string FileDialogFilter => "Python|pythonw.exe"; internal override string FileDialogFilter => "Python|pythonw.exe";
internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; } internal override string PluginsSettingsFilePath
{
get => PluginSettings.PythonExecutablePath;
set => PluginSettings.PythonExecutablePath = value;
}
internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }

View file

@ -1,10 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using Droplex;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin;
using System.IO; using System.IO;
using Droplex;
using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.ExternalPlugins.Environments namespace Flow.Launcher.Core.ExternalPlugins.Environments
{ {
@ -19,7 +19,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } internal override string PluginsSettingsFilePath
{
get => PluginSettings.NodeExecutablePath;
set => PluginSettings.NodeExecutablePath = value;
}
internal TypeScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal TypeScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }

View file

@ -1,10 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using Droplex;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin;
using System.IO; using System.IO;
using Droplex;
using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Core.ExternalPlugins.Environments namespace Flow.Launcher.Core.ExternalPlugins.Environments
{ {
@ -19,7 +19,11 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } internal override string PluginsSettingsFilePath
{
get => PluginSettings.NodeExecutablePath;
set => PluginSettings.NodeExecutablePath = value;
}
internal TypeScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } internal TypeScriptV2Environment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }

View file

@ -23,6 +23,8 @@ namespace Flow.Launcher.Core.Plugin
protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!; protected ConcurrentDictionary<string, object?> Settings { get; set; } = null!;
public required IPublicAPI API { get; init; } public required IPublicAPI API { get; init; }
private static readonly string ClassName = nameof(JsonRPCPluginSettings);
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!; private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin"); private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
@ -122,12 +124,26 @@ namespace Flow.Launcher.Core.Plugin
public async Task SaveAsync() 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() 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() public bool NeedCreateSettingPanel()

View file

@ -1,20 +1,19 @@
using Flow.Launcher.Core.ExternalPlugins; using System;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using ISavable = Flow.Launcher.Plugin.ISavable;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using System.Text.Json; using ISavable = Flow.Launcher.Plugin.ISavable;
using Flow.Launcher.Core.Resource;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace Flow.Launcher.Core.Plugin namespace Flow.Launcher.Core.Plugin
{ {

View file

@ -4,10 +4,12 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using JetBrains.Annotations; using CommunityToolkit.Mvvm.DependencyInjection;
using Squirrel;
using Flow.Launcher.Core.Resource; using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
@ -15,8 +17,8 @@ using Flow.Launcher.Infrastructure.Http;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using System.Text.Json.Serialization; using JetBrains.Annotations;
using System.Threading; using Squirrel;
namespace Flow.Launcher.Core namespace Flow.Launcher.Core
{ {
@ -51,7 +53,7 @@ namespace Flow.Launcher.Core
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = Version.Parse(Constant.Version); 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) if (newReleaseVersion <= currentVersion)
{ {
@ -70,7 +72,7 @@ namespace Flow.Launcher.Core
if (DataLocation.PortableDataLocationInUse()) 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)); FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
if (!FilesFolders.VerifyBothFolderFilesEqual(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"), _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 // 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 uri = new Uri(repository);
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
@ -144,12 +146,22 @@ namespace Flow.Launcher.Core
return manager; 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); var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
return tips; return tips;
} }
private static string Formatted<T>(T t)
{
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
{
WriteIndented = true
});
return formatted;
}
} }
} }

View file

@ -1,19 +1,11 @@
#nullable enable #nullable enable
using System; using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Infrastructure namespace Flow.Launcher.Infrastructure
{ {
public static class Helper public static class Helper
{ {
static Helper()
{
jsonFormattedSerializerOptions.Converters.Add(new JsonStringEnumConverter());
}
/// <summary> /// <summary>
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy /// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
/// </summary> /// </summary>
@ -36,55 +28,5 @@ namespace Flow.Launcher.Infrastructure
throw new NullReferenceException(); 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;
}
} }
} }

View file

@ -62,7 +62,7 @@ namespace Flow.Launcher.Infrastructure.Image
}); });
} }
public static async Task Save() public static async Task SaveAsync()
{ {
await storageLock.WaitAsync(); await storageLock.WaitAsync();
@ -72,12 +72,22 @@ namespace Flow.Launcher.Infrastructure.Image
.Select(x => x.Key) .Select(x => x.Key)
.ToList()); .ToList());
} }
catch (System.Exception e)
{
Log.Exception($"|ImageLoader.SaveAsync|Failed to save image cache to file", e);
}
finally finally
{ {
storageLock.Release(); storageLock.Release();
} }
} }
public static async Task WaitSaveAsync()
{
await storageLock.WaitAsync();
storageLock.Release();
}
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync() private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
{ {
await storageLock.WaitAsync(); await storageLock.WaitAsync();

View file

@ -2,6 +2,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using MemoryPack; using MemoryPack;
#nullable enable #nullable enable
@ -33,7 +34,7 @@ namespace Flow.Launcher.Infrastructure.Storage
public BinaryStorage(string filename) public BinaryStorage(string filename)
{ {
DirectoryPath = DataLocation.CacheDirectory; DirectoryPath = DataLocation.CacheDirectory;
Helper.ValidateDirectory(DirectoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
} }

View file

@ -1,17 +1,51 @@
using System.IO; using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage namespace Flow.Launcher.Infrastructure.Storage
{ {
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new() 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() public FlowLauncherJsonStorage()
{ {
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
Helper.ValidateDirectory(directoryPath); FilesFolders.ValidateDirectory(directoryPath);
var filename = typeof(T).Name; var filename = typeof(T).Name;
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); 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);
}
}
} }
} }

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage namespace Flow.Launcher.Infrastructure.Storage
{ {
@ -37,7 +38,7 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = filePath; FilePath = filePath;
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path"); DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
Helper.ValidateDirectory(DirectoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
} }
public async Task<T> LoadAsync() public async Task<T> LoadAsync()

View file

@ -1,5 +1,9 @@
using System.IO; using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Infrastructure.Storage namespace Flow.Launcher.Infrastructure.Storage
{ {
@ -8,14 +12,49 @@ namespace Flow.Launcher.Infrastructure.Storage
// Use assembly name to check which plugin is using this storage // Use assembly name to check which plugin is using this storage
public readonly string AssemblyName; 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() public PluginJsonStorage()
{ {
var dataType = typeof(T); var dataType = typeof(T);
AssemblyName = dataType.Assembly.GetName().Name; AssemblyName = dataType.Assembly.GetName().Name;
DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName); DirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, AssemblyName);
Helper.ValidateDirectory(DirectoryPath); FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
} }
public PluginJsonStorage(T data) : this()
{
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);
}
}
} }
} }

View file

@ -488,5 +488,16 @@ namespace Flow.Launcher.Infrastructure
} }
#endregion #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
} }
} }

View file

@ -318,5 +318,51 @@ namespace Flow.Launcher.Plugin.SharedCommands
{ {
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; 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);
}
}
}
}
} }
} }

View file

@ -138,6 +138,11 @@ namespace Flow.Launcher
{ {
await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () =>
{ {
// Because new message box api uses MessageBoxEx window,
// if it is created and closed before main window is created, it will cause the application to exit.
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
Log.SetLogLevel(_settings.LogLevel); Log.SetLogLevel(_settings.LogLevel);
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate(); Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
@ -299,6 +304,14 @@ namespace Flow.Launcher
return; 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; _disposed = true;
} }

View file

@ -9,6 +9,11 @@
Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable Click no if it's already installed, and you will be prompted to select the folder that contains the {1} executable
</system:String> </system:String>
<system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String> <system:String x:Key="runtimePluginChooseRuntimeExecutable">Please select the {0} executable</system:String>
<system:String x:Key="runtimeExecutableInvalidChooseDownload">
Your selected {0} executable is invalid.
{2}{2}
Click yes if you would like select the {0} executable agian. Click no if you would like to download {1}
</system:String>
<system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String> <system:String x:Key="runtimePluginUnableToSetExecutablePath">Unable to set {0} executable path, please try from Flow's settings (scroll down to the bottom).</system:String>
<system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String> <system:String x:Key="failedToInitializePluginsTitle">Fail to Init Plugins</system:String>
<system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String> <system:String x:Key="failedToInitializePluginsMessage">Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help</system:String>
@ -320,6 +325,9 @@
<system:String x:Key="logfolder">Log Folder</system:String> <system:String x:Key="logfolder">Log Folder</system:String>
<system:String x:Key="clearlogfolder">Clear Logs</system:String> <system:String x:Key="clearlogfolder">Clear Logs</system:String>
<system:String x:Key="clearlogfolderMessage">Are you sure you want to delete all logs?</system:String> <system:String x:Key="clearlogfolderMessage">Are you sure you want to delete all logs?</system:String>
<system:String x:Key="clearcachefolder">Clear Caches</system:String>
<system:String x:Key="clearcachefolderMessage">Are you sure you want to delete all caches?</system:String>
<system:String x:Key="clearfolderfailMessage">Failed to clear part of folders and files. Please see log file for more information</system:String>
<system:String x:Key="welcomewindow">Wizard</system:String> <system:String x:Key="welcomewindow">Wizard</system:String>
<system:String x:Key="userdatapath">User Data Location</system:String> <system:String x:Key="userdatapath">User Data Location</system:String>
<system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String> <system:String x:Key="userdatapathToolTip">User settings and installed plugins are saved in the user data folder. This location may vary depending on whether it's in portable mode or not.</system:String>

View file

@ -32,6 +32,13 @@ namespace Flow.Launcher
{ {
public partial class MainWindow : IDisposable public partial class MainWindow : IDisposable
{ {
#region Public Property
// Window Event: Close Event
public bool CanClose { get; set; } = false;
#endregion
#region Private Fields #region Private Fields
// Dependency Injection // Dependency Injection
@ -45,8 +52,6 @@ namespace Flow.Launcher
private readonly ContextMenu _contextMenu = new(); private readonly ContextMenu _contextMenu = new();
private readonly MainViewModel _viewModel; private readonly MainViewModel _viewModel;
// Window Event: Close Event
private bool _canClose = false;
// Window Event: Key Event // Window Event: Key Event
private bool _isArrowKeyPressed = false; private bool _isArrowKeyPressed = false;
@ -279,7 +284,7 @@ namespace Flow.Launcher
private async void OnClosing(object sender, CancelEventArgs e) private async void OnClosing(object sender, CancelEventArgs e)
{ {
if (!_canClose) if (!CanClose)
{ {
_notifyIcon.Visible = false; _notifyIcon.Visible = false;
App.API.SaveAppAllSettings(); App.API.SaveAppAllSettings();
@ -287,7 +292,7 @@ namespace Flow.Launcher
await PluginManager.DisposePluginsAsync(); await PluginManager.DisposePluginsAsync();
Notification.Uninstall(); Notification.Uninstall();
// After plugins are all disposed, we can close the main window // 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 // Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
Application.Current.Shutdown(); Application.Current.Shutdown();
} }

View file

@ -9,8 +9,8 @@ namespace Flow.Launcher
{ {
internal static class Notification internal static class Notification
{ {
internal static bool legacy = Environment.OSVersion.Version.Build < 19041; internal static bool legacy = !Win32Helper.IsNotificationSupported();
[System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
internal static void Uninstall() internal static void Uninstall()
{ {
if (!legacy) 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) private static void ShowInternal(string title, string subTitle, string iconPath = null)
{ {
// Handle notification for win7/8/early win10 // Handle notification for win7/8/early win10

View file

@ -37,6 +37,8 @@ namespace Flow.Launcher
private readonly Internationalization _translater; private readonly Internationalization _translater;
private readonly MainViewModel _mainVM; private readonly MainViewModel _mainVM;
private readonly object _saveSettingsLock = new();
#region Constructor #region Constructor
public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM) public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM)
@ -57,21 +59,28 @@ namespace Flow.Launcher
_mainVM.ChangeQueryText(query, requery); _mainVM.ChangeQueryText(query, requery);
} }
public void RestartApp() #pragma warning disable VSTHRD100 // Avoid async void methods
public async void RestartApp()
{ {
_mainVM.Hide(); _mainVM.Hide();
// we must manually save // We must manually save
// UpdateManager.RestartApp() will call Environment.Exit(0) // UpdateManager.RestartApp() will call Environment.Exit(0)
// which will cause ungraceful exit // which will cause ungraceful exit
SaveAppAllSettings(); 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, // 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, // 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. // the project may not restart or just terminates. This is expected.
UpdateManager.RestartApp(Constant.ApplicationFileName); UpdateManager.RestartApp(Constant.ApplicationFileName);
} }
#pragma warning restore VSTHRD100 // Avoid async void methods
public void ShowMainWindow() => _mainVM.Show(); public void ShowMainWindow() => _mainVM.Show();
public void HideMainWindow() => _mainVM.Hide(); public void HideMainWindow() => _mainVM.Hide();
@ -85,10 +94,13 @@ namespace Flow.Launcher
public void SaveAppAllSettings() public void SaveAppAllSettings()
{ {
PluginManager.Save(); lock (_saveSettingsLock)
_mainVM.Save(); {
_settings.Save(); _settings.Save();
_ = ImageLoader.Save(); PluginManager.Save();
_mainVM.Save();
}
_ = ImageLoader.SaveAsync();
} }
public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync(); public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();

View file

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core; using Flow.Launcher.Core;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
@ -16,6 +15,8 @@ namespace Flow.Launcher.SettingPages.ViewModels;
public partial class SettingsPaneAboutViewModel : BaseModel public partial class SettingsPaneAboutViewModel : BaseModel
{ {
private static readonly string ClassName = nameof(SettingsPaneAboutViewModel);
private readonly Settings _settings; private readonly Settings _settings;
private readonly Updater _updater; private readonly Updater _updater;
@ -24,7 +25,16 @@ public partial class SettingsPaneAboutViewModel : BaseModel
get get
{ {
var size = GetLogFiles().Sum(file => file.Length); var size = GetLogFiles().Sum(file => file.Length);
return $"{InternationalizationManager.Instance.GetTranslation("clearlogfolder")} ({BytesToReadableString(size)})"; return $"{App.API.GetTranslation("clearlogfolder")} ({BytesToReadableString(size)})";
}
}
public string CacheFolderSize
{
get
{
var size = GetCacheFiles().Sum(file => file.Length);
return $"{App.API.GetTranslation("clearcachefolder")} ({BytesToReadableString(size)})";
} }
} }
@ -42,7 +52,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel
}; };
public string ActivatedTimes => string.Format( public string ActivatedTimes => string.Format(
InternationalizationManager.Instance.GetTranslation("about_activate_times"), App.API.GetTranslation("about_activate_times"),
_settings.ActivateTimes _settings.ActivateTimes
); );
@ -88,14 +98,35 @@ public partial class SettingsPaneAboutViewModel : BaseModel
private void AskClearLogFolderConfirmation() private void AskClearLogFolderConfirmation()
{ {
var confirmResult = App.API.ShowMsgBox( var confirmResult = App.API.ShowMsgBox(
InternationalizationManager.Instance.GetTranslation("clearlogfolderMessage"), App.API.GetTranslation("clearlogfolderMessage"),
InternationalizationManager.Instance.GetTranslation("clearlogfolder"), App.API.GetTranslation("clearlogfolder"),
MessageBoxButton.YesNo MessageBoxButton.YesNo
); );
if (confirmResult == MessageBoxResult.Yes) if (confirmResult == MessageBoxResult.Yes)
{ {
ClearLogFolder(); if (!ClearLogFolder())
{
App.API.ShowMsgBox(App.API.GetTranslation("clearfolderfailMessage"));
}
}
}
[RelayCommand]
private void AskClearCacheFolderConfirmation()
{
var confirmResult = App.API.ShowMsgBox(
App.API.GetTranslation("clearcachefolderMessage"),
App.API.GetTranslation("clearcachefolder"),
MessageBoxButton.YesNo
);
if (confirmResult == MessageBoxResult.Yes)
{
if (!ClearCacheFolder())
{
App.API.ShowMsgBox(App.API.GetTranslation("clearfolderfailMessage"));
}
} }
} }
@ -113,7 +144,6 @@ public partial class SettingsPaneAboutViewModel : BaseModel
App.API.OpenDirectory(parentFolderPath); App.API.OpenDirectory(parentFolderPath);
} }
[RelayCommand] [RelayCommand]
private void OpenLogsFolder() private void OpenLogsFolder()
{ {
@ -121,21 +151,47 @@ public partial class SettingsPaneAboutViewModel : BaseModel
} }
[RelayCommand] [RelayCommand]
private Task UpdateApp() => _updater.UpdateAppAsync(false); private Task UpdateAppAsync() => _updater.UpdateAppAsync(false);
private void ClearLogFolder() private bool ClearLogFolder()
{ {
var success = true;
var logDirectory = GetLogDir(); var logDirectory = GetLogDir();
var logFiles = GetLogFiles(); var logFiles = GetLogFiles();
logFiles.ForEach(f => f.Delete()); logFiles.ForEach(f =>
{
try
{
f.Delete();
}
catch (Exception e)
{
App.API.LogException(ClassName, $"Failed to delete log file: {f.Name}", e);
success = false;
}
});
logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly) logDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
// Do not clean log files of current version
.Where(dir => !Constant.Version.Equals(dir.Name)) .Where(dir => !Constant.Version.Equals(dir.Name))
.ToList() .ToList()
.ForEach(dir => dir.Delete()); .ForEach(dir =>
{
try
{
dir.Delete(true);
}
catch (Exception e)
{
App.API.LogException(ClassName, $"Failed to delete log directory: {dir.Name}", e);
success = false;
}
});
OnPropertyChanged(nameof(LogFolderSize)); OnPropertyChanged(nameof(LogFolderSize));
return success;
} }
private static DirectoryInfo GetLogDir(string version = "") private static DirectoryInfo GetLogDir(string version = "")
@ -148,6 +204,55 @@ public partial class SettingsPaneAboutViewModel : BaseModel
return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList(); return GetLogDir(version).EnumerateFiles("*", SearchOption.AllDirectories).ToList();
} }
private bool ClearCacheFolder()
{
var success = true;
var cacheDirectory = GetCacheDir();
var cacheFiles = GetCacheFiles();
cacheFiles.ForEach(f =>
{
try
{
f.Delete();
}
catch (Exception e)
{
App.API.LogException(ClassName, $"Failed to delete cache file: {f.Name}", e);
success = false;
}
});
cacheDirectory.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
.ToList()
.ForEach(dir =>
{
try
{
dir.Delete(true);
}
catch (Exception e)
{
App.API.LogException(ClassName, $"Failed to delete cache directory: {dir.Name}", e);
success = false;
}
});
OnPropertyChanged(nameof(CacheFolderSize));
return success;
}
private static DirectoryInfo GetCacheDir()
{
return new DirectoryInfo(DataLocation.CacheDirectory);
}
private static List<FileInfo> GetCacheFiles()
{
return GetCacheDir().EnumerateFiles("*", SearchOption.AllDirectories).ToList();
}
private static string BytesToReadableString(long bytes) private static string BytesToReadableString(long bytes)
{ {
const int scale = 1024; const int scale = 1024;
@ -156,8 +261,7 @@ public partial class SettingsPaneAboutViewModel : BaseModel
foreach (string order in orders) foreach (string order in orders)
{ {
if (bytes > max) if (bytes > max) return $"{decimal.Divide(bytes, max):##.##} {order}";
return $"{decimal.Divide(bytes, max):##.##} {order}";
max /= scale; max /= scale;
} }

View file

@ -90,6 +90,10 @@
Margin="0 12 0 0" Margin="0 12 0 0"
Icon="&#xf12b;"> Icon="&#xf12b;">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Button
Margin="0 0 12 0"
Command="{Binding AskClearCacheFolderConfirmationCommand}"
Content="{Binding CacheFolderSize, Mode=OneWay}" />
<Button <Button
Margin="0 0 12 0" Margin="0 0 12 0"
Command="{Binding AskClearLogFolderConfirmationCommand}" Command="{Binding AskClearLogFolderConfirmationCommand}"

View file

@ -28,16 +28,16 @@
Style="{StaticResource PageTitle}" Style="{StaticResource PageTitle}"
Text="{DynamicResource general}" Text="{DynamicResource general}"
TextAlignment="left" /> TextAlignment="left" />
<cc:ExCard
<cc:CardGroup Margin="0 10 0 0"> Title="{DynamicResource startFlowLauncherOnSystemStartup}"
Margin="0 8 0 0"
<cc:Card Title="{DynamicResource startFlowLauncherOnSystemStartup}" Icon="&#xe8fc;"> Icon="&#xe8fc;">
<cc:ExCard.SideContent>
<ui:ToggleSwitch <ui:ToggleSwitch
IsOn="{Binding StartFlowLauncherOnSystemStartup}" IsOn="{Binding StartFlowLauncherOnSystemStartup}"
OffContent="{DynamicResource disable}" OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" /> OnContent="{DynamicResource enable}" />
</cc:Card> </cc:ExCard.SideContent>
<cc:Card <cc:Card
Title="{DynamicResource useLogonTaskForStartup}" Title="{DynamicResource useLogonTaskForStartup}"
Sub="{DynamicResource useLogonTaskForStartupTooltip}" Sub="{DynamicResource useLogonTaskForStartupTooltip}"
@ -47,17 +47,16 @@
OffContent="{DynamicResource disable}" OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" /> OnContent="{DynamicResource enable}" />
</cc:Card> </cc:Card>
</cc:ExCard>
<cc:Card <cc:Card
Title="{DynamicResource hideOnStartup}" Title="{DynamicResource hideOnStartup}"
Sub="{DynamicResource hideOnStartupToolTip}" Icon="&#xed1a;"
Type="InsideFit"> Sub="{DynamicResource hideOnStartupToolTip}">
<ui:ToggleSwitch <ui:ToggleSwitch
IsOn="{Binding Settings.HideOnStartup}" IsOn="{Binding Settings.HideOnStartup}"
OffContent="{DynamicResource disable}" OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" /> OnContent="{DynamicResource enable}" />
</cc:Card> </cc:Card>
</cc:CardGroup>
<cc:Card Title="{DynamicResource hideFlowLauncherWhenLoseFocus}" Margin="0 14 0 0"> <cc:Card Title="{DynamicResource hideFlowLauncherWhenLoseFocus}" Margin="0 14 0 0">
<ui:ToggleSwitch <ui:ToggleSwitch
@ -178,30 +177,6 @@
OnContent="{DynamicResource enable}" /> OnContent="{DynamicResource enable}" />
</cc:Card> </cc:Card>
<cc:CardGroup Margin="0 14 0 0">
<cc:Card
Title="{DynamicResource searchDelay}"
Icon="&#xE961;"
Sub="{DynamicResource searchDelayToolTip}">
<ui:ToggleSwitch
IsOn="{Binding Settings.SearchQueryResultsWithDelay}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource searchDelayTime}"
Icon="&#xE916;"
Sub="{DynamicResource searchDelayTimeToolTip}">
<ComboBox
MaxWidth="200"
DisplayMemberPath="Display"
ItemsSource="{Binding SearchDelayTimes}"
SelectedItem="{Binding SearchDelayTime}"
SelectedValuePath="Value" />
</cc:Card>
</cc:CardGroup>
<cc:CardGroup Margin="0 14 0 0"> <cc:CardGroup Margin="0 14 0 0">
<cc:Card Title="{DynamicResource querySearchPrecision}" Sub="{DynamicResource querySearchPrecisionToolTip}"> <cc:Card Title="{DynamicResource querySearchPrecision}" Sub="{DynamicResource querySearchPrecisionToolTip}">
<ComboBox <ComboBox

View file

@ -6,12 +6,12 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Controls; using System.Windows.Controls;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.Program.Programs; using Flow.Launcher.Plugin.Program.Programs;
using Flow.Launcher.Plugin.Program.Views; using Flow.Launcher.Plugin.Program.Views;
using Flow.Launcher.Plugin.Program.Views.Models; using Flow.Launcher.Plugin.Program.Views.Models;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Path = System.IO.Path; using Path = System.IO.Path;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
@ -184,7 +184,7 @@ namespace Flow.Launcher.Plugin.Program
{ {
var pluginCachePath = Context.CurrentPluginMetadata.PluginCacheDirectoryPath; var pluginCachePath = Context.CurrentPluginMetadata.PluginCacheDirectoryPath;
Helper.ValidateDirectory(pluginCachePath); FilesFolders.ValidateDirectory(pluginCachePath);
static void MoveFile(string sourcePath, string destinationPath) static void MoveFile(string sourcePath, string destinationPath)
{ {

View file

@ -9,9 +9,13 @@ namespace Flow.Launcher.Plugin.Sys
{ {
public const string Keyword = "fltheme"; public const string Keyword = "fltheme";
private readonly Theme _theme;
private readonly PluginInitContext _context; private readonly PluginInitContext _context;
// Do not initialize it in the constructor, because it will cause null reference in
// var dicts = Application.Current.Resources.MergedDictionaries; line of Theme
private Theme theme = null;
private Theme Theme => theme ??= Ioc.Default.GetRequiredService<Theme>();
#region Theme Selection #region Theme Selection
// Theme select codes simplified from SettingsPaneThemeViewModel.cs // Theme select codes simplified from SettingsPaneThemeViewModel.cs
@ -19,24 +23,23 @@ namespace Flow.Launcher.Plugin.Sys
private Theme.ThemeData _selectedTheme; private Theme.ThemeData _selectedTheme;
public Theme.ThemeData SelectedTheme public Theme.ThemeData SelectedTheme
{ {
get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == _theme.GetCurrentTheme()); get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == Theme.GetCurrentTheme());
set set
{ {
_selectedTheme = value; _selectedTheme = value;
_theme.ChangeTheme(value.FileNameWithoutExtension); Theme.ChangeTheme(value.FileNameWithoutExtension);
_ = _theme.RefreshFrameAsync(); _ = Theme.RefreshFrameAsync();
} }
} }
private List<Theme.ThemeData> Themes => _theme.LoadAvailableThemes(); private List<Theme.ThemeData> Themes => Theme.LoadAvailableThemes();
#endregion #endregion
public ThemeSelector(PluginInitContext context) public ThemeSelector(PluginInitContext context)
{ {
_context = context; _context = context;
_theme = Ioc.Default.GetRequiredService<Theme>();
} }
public List<Result> Query(Query query) public List<Result> Query(Query query)

View file

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Controls; using System.Windows.Controls;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
namespace Flow.Launcher.Plugin.WebSearch namespace Flow.Launcher.Plugin.WebSearch
@ -180,7 +179,7 @@ namespace Flow.Launcher.Plugin.WebSearch
// Default images directory is in the WebSearch's application folder // Default images directory is in the WebSearch's application folder
DefaultImagesDirectory = Path.Combine(pluginDirectory, Images); DefaultImagesDirectory = Path.Combine(pluginDirectory, Images);
Helper.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory); FilesFolders.ValidateDataDirectory(bundledImagesDirectory, DefaultImagesDirectory);
// Custom images directory is in the WebSearch's data location folder // Custom images directory is in the WebSearch's data location folder
CustomImagesDirectory = Path.Combine(_context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "CustomIcons"); CustomImagesDirectory = Path.Combine(_context.CurrentPluginMetadata.PluginSettingsDirectoryPath, "CustomIcons");