diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index df1464048..3dba35f8d 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -35,5 +35,7 @@ namespace Flow.Launcher.Infrastructure public const string DefaultTheme = "Darker"; public const string Themes = "Themes"; + + public const string Website = "https://flow-launcher.github.io"; } } diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 8fe910c0c..74e335d32 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -111,4 +111,4 @@ namespace Flow.Launcher.Infrastructure.Http } } } -} \ No newline at end of file +} diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml index 32f9e9a6e..e47f0e779 100644 --- a/Flow.Launcher/SettingWindow.xaml +++ b/Flow.Launcher/SettingWindow.xaml @@ -429,8 +429,8 @@ - - + + diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index 853925852..c122f8037 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -213,7 +213,7 @@ namespace Flow.Launcher.ViewModel #region plugin - public static string Plugin => "http://www.wox.one/plugin"; + public static string Plugin => @"https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest"; public PluginViewModel SelectedPlugin { get; set; } public IList PluginViewModels @@ -450,7 +450,7 @@ namespace Flow.Launcher.ViewModel #region about - public string Github => _updater.GitHubRepository; + public string Website => Constant.Website; public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest"; public static string Version => Constant.Version; public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs index 76cb0f86b..7bc357be4 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs @@ -10,12 +10,9 @@ namespace Flow.Launcher.Plugin.PluginsManager { private PluginInitContext Context { get; set; } - private Settings Settings { get; set; } - - public ContextMenu(PluginInitContext context, Settings settings) + public ContextMenu(PluginInitContext context) { Context = context; - Settings = settings; } public List LoadContextMenus(Result selectedResult) @@ -58,7 +55,7 @@ namespace Flow.Launcher.Plugin.PluginsManager ? pluginManifestInfo.UrlSourceCode.Replace("/tree/master", "/issues/new/choose") : pluginManifestInfo.UrlSourceCode; - SharedCommands.SearchWeb.NewBrowserWindow(link); + SharedCommands.SearchWeb.NewTabInBrowser(link); return true; } }, @@ -69,7 +66,7 @@ namespace Flow.Launcher.Plugin.PluginsManager IcoPath = selectedResult.IcoPath, Action = _ => { - SharedCommands.SearchWeb.NewBrowserWindow("https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest"); + SharedCommands.SearchWeb.NewTabInBrowser("https://github.com/Flow-Launcher/Flow.Launcher.PluginsManifest"); return true; } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs index 43f92e7b9..716a424ff 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs @@ -1,9 +1,12 @@ using Flow.Launcher.Infrastructure.Storage; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.PluginsManager.ViewModels; using Flow.Launcher.Plugin.PluginsManager.Views; using System.Collections.Generic; +using System.Linq; using System.Windows.Controls; +using Flow.Launcher.Infrastructure; +using System; +using System.Threading.Tasks; namespace Flow.Launcher.Plugin.PluginsManager { @@ -17,6 +20,10 @@ namespace Flow.Launcher.Plugin.PluginsManager private IContextMenu contextMenu; + internal PluginsManager pluginManager; + + private DateTime lastUpdateTime; + public Control CreateSettingPanel() { return new PluginsManagerSettings(viewModel); @@ -27,7 +34,9 @@ namespace Flow.Launcher.Plugin.PluginsManager Context = context; viewModel = new SettingsViewModel(context); Settings = viewModel.Settings; - contextMenu = new ContextMenu(Context, Settings); + contextMenu = new ContextMenu(Context); + pluginManager = new PluginsManager(Context, Settings); + lastUpdateTime = DateTime.Now; } public List LoadContextMenus(Result selectedResult) @@ -39,17 +48,29 @@ namespace Flow.Launcher.Plugin.PluginsManager { var search = query.Search.ToLower(); - var pluginManager = new PluginsManager(Context, Settings); + if (string.IsNullOrWhiteSpace(search)) + return pluginManager.GetDefaultHotKeys(); - if (!string.IsNullOrEmpty(search) - && ($"{Settings.HotkeyUninstall} ".StartsWith(search) || search.StartsWith($"{Settings.HotkeyUninstall} "))) - return pluginManager.RequestUninstall(search); + if ((DateTime.Now - lastUpdateTime).TotalHours > 12) // 12 hours + { + Task.Run(async () => + { + await pluginManager.UpdateManifest(); + lastUpdateTime = DateTime.Now; + }); + } - if (!string.IsNullOrEmpty(search) - && ($"{Settings.HotkeyUpdate} ".StartsWith(search) || search.StartsWith($"{Settings.HotkeyUpdate} "))) - return pluginManager.RequestUpdate(search); - - return pluginManager.RequestInstallOrUpdate(search); + return search switch + { + var s when s.StartsWith(Settings.HotKeyInstall) => pluginManager.RequestInstallOrUpdate(s), + var s when s.StartsWith(Settings.HotkeyUninstall) => pluginManager.RequestUninstall(s), + var s when s.StartsWith(Settings.HotkeyUpdate) => pluginManager.RequestUpdate(s), + _ => pluginManager.GetDefaultHotKeys().Where(hotkey => + { + hotkey.Score = StringMatcher.FuzzySearch(search, hotkey.Title).Score; + return hotkey.Score > 0; + }).ToList() + }; } public void Save() diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs index 13a5ae2ca..814e0764d 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Models/PluginsManifest.cs @@ -1,8 +1,8 @@ using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; -using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; namespace Flow.Launcher.Plugin.PluginsManager.Models @@ -13,18 +13,17 @@ namespace Flow.Launcher.Plugin.PluginsManager.Models internal PluginsManifest() { - DownloadManifest().Wait(); + Task.Run(async () => await DownloadManifest()).Wait(); } - private async Task DownloadManifest() + internal async Task DownloadManifest() { - var json = string.Empty; try { - json = await Http.Get( - "https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json"); + await using var jsonStream = await Http.GetStreamAsync("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json") + .ConfigureAwait(false); - UserPlugins = JsonConvert.DeserializeObject>(json); + UserPlugins = await JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); } catch (Exception e) { diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 90f3277fb..9635648d4 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -7,20 +7,22 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using System.Windows; namespace Flow.Launcher.Plugin.PluginsManager { internal class PluginsManager { - private readonly PluginsManifest pluginsManifest; + private PluginsManifest pluginsManifest; private PluginInitContext Context { get; set; } private Settings Settings { get; set; } private bool shouldHideWindow = true; - private bool ShouldHideWindow + + private bool ShouldHideWindow { set { shouldHideWindow = value; } get @@ -42,18 +44,63 @@ namespace Flow.Launcher.Plugin.PluginsManager Context = context; Settings = settings; } + + internal async Task UpdateManifest() + { + await pluginsManifest.DownloadManifest(); + } + + internal List GetDefaultHotKeys() + { + return new List() + { + new Result() + { + Title = Settings.HotKeyInstall, + IcoPath = icoPath, + Action = _ => + { + Context.API.ChangeQuery("pm install "); + return false; + } + }, + new Result() + { + Title = Settings.HotkeyUninstall, + IcoPath = icoPath, + Action = _ => + { + Context.API.ChangeQuery("pm uninstall "); + return false; + } + }, + new Result() + { + Title = Settings.HotkeyUpdate, + IcoPath = icoPath, + Action = _ => + { + Context.API.ChangeQuery("pm update "); + return false; + } + } + }; + } + internal void InstallOrUpdate(UserPlugin plugin) { if (PluginExists(plugin.ID)) { - if (Context.API.GetAllPlugins().Any(x => x.Metadata.ID == plugin.ID && x.Metadata.Version != plugin.Version)) + if (Context.API.GetAllPlugins() + .Any(x => x.Metadata.ID == plugin.ID && x.Metadata.Version != plugin.Version)) { if (MessageBox.Show(Context.API.GetTranslation("plugin_pluginsmanager_update_exists"), - Context.API.GetTranslation("plugin_pluginsmanager_update_title"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) - Context - .API - .ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.HotkeyUpdate} {plugin.Name}"); + Context.API.GetTranslation("plugin_pluginsmanager_update_title"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + Context + .API + .ChangeQuery( + $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.HotkeyUpdate} {plugin.Name}"); Application.Current.MainWindow.Show(); shouldHideWindow = false; @@ -66,10 +113,11 @@ namespace Flow.Launcher.Plugin.PluginsManager } var message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_prompt"), - plugin.Name, plugin.Author, - Environment.NewLine, Environment.NewLine); + plugin.Name, plugin.Author, + Environment.NewLine, Environment.NewLine); - if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_install_title"), MessageBoxButton.YesNo) == MessageBoxResult.No) + if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_install_title"), + MessageBoxButton.YesNo) == MessageBoxResult.No) return; var filePath = Path.Combine(DataLocation.PluginsDirectory, $"{plugin.Name}-{plugin.Version}.zip"); @@ -77,30 +125,34 @@ namespace Flow.Launcher.Plugin.PluginsManager try { Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); + Context.API.GetTranslation("plugin_pluginsmanager_please_wait")); Http.Download(plugin.UrlDownload, filePath); Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_download_success")); + Context.API.GetTranslation("plugin_pluginsmanager_download_success")); } catch (Exception e) { Context.API.ShowMsg(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), - Context.API.GetTranslation("plugin_pluginsmanager_download_success")); + Context.API.GetTranslation("plugin_pluginsmanager_download_success")); Log.Exception("PluginsManager", "An error occured while downloading plugin", e, "PluginDownload"); } - Application.Current.Dispatcher.Invoke(() => { Install(plugin, filePath); Context.API.RestartApp(); }); + Application.Current.Dispatcher.Invoke(() => + { + Install(plugin, filePath); + Context.API.RestartApp(); + }); } internal List RequestUpdate(string search) { var autocompletedResults = AutoCompleteReturnAllResults(search, - Settings.HotkeyUpdate, - "Update", - "Select a plugin to update"); + Settings.HotkeyUpdate, + "Update", + "Select a plugin to update"); if (autocompletedResults.Any()) return autocompletedResults; @@ -108,63 +160,68 @@ namespace Flow.Launcher.Plugin.PluginsManager var uninstallSearch = search.Replace(Settings.HotkeyUpdate, string.Empty).TrimStart(); - var resultsForUpdate = - from existingPlugin in Context.API.GetAllPlugins() - join pluginFromManifest in pluginsManifest.UserPlugins - on existingPlugin.Metadata.ID equals pluginFromManifest.ID - where existingPlugin.Metadata.Version != pluginFromManifest.Version - select - new - { - pluginFromManifest.Name, - pluginFromManifest.Author, - CurrentVersion = existingPlugin.Metadata.Version, - NewVersion = pluginFromManifest.Version, - existingPlugin.Metadata.IcoPath, - PluginExistingMetadata = existingPlugin.Metadata, - PluginNewUserPlugin = pluginFromManifest - }; + var resultsForUpdate = + from existingPlugin in Context.API.GetAllPlugins() + join pluginFromManifest in pluginsManifest.UserPlugins + on existingPlugin.Metadata.ID equals pluginFromManifest.ID + where existingPlugin.Metadata.Version.CompareTo(pluginFromManifest.Version) < 0 // if current version precedes manifest version + select + new + { + pluginFromManifest.Name, + pluginFromManifest.Author, + CurrentVersion = existingPlugin.Metadata.Version, + NewVersion = pluginFromManifest.Version, + existingPlugin.Metadata.IcoPath, + PluginExistingMetadata = existingPlugin.Metadata, + PluginNewUserPlugin = pluginFromManifest + }; if (!resultsForUpdate.Any()) - return new List { - new Result - { - Title = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_title"), - SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_subtitle"), - IcoPath = icoPath - }}; + return new List + { + new Result + { + Title = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_title"), + SubTitle = Context.API.GetTranslation("plugin_pluginsmanager_update_noresult_subtitle"), + IcoPath = icoPath + } + }; var results = resultsForUpdate - .Select(x => - new Result - { - Title = $"{x.Name} by {x.Author}", - SubTitle = $"Update from version {x.CurrentVersion} to {x.NewVersion}", - IcoPath = x.IcoPath, - Action = e => - { - string message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_update_prompt"), - x.Name, x.Author, - Environment.NewLine, Environment.NewLine); + .Select(x => + new Result + { + Title = $"{x.Name} by {x.Author}", + SubTitle = $"Update from version {x.CurrentVersion} to {x.NewVersion}", + IcoPath = x.IcoPath, + Action = e => + { + string message = string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_update_prompt"), + x.Name, x.Author, + Environment.NewLine, Environment.NewLine); - if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_update_title"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) - { - Uninstall(x.PluginExistingMetadata); + if (MessageBox.Show(message, + Context.API.GetTranslation("plugin_pluginsmanager_update_title"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + Uninstall(x.PluginExistingMetadata); - var downloadToFilePath = Path.Combine(DataLocation.PluginsDirectory, $"{x.Name}-{x.NewVersion}.zip"); - Http.Download(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath); - Install(x.PluginNewUserPlugin, downloadToFilePath); + var downloadToFilePath = Path.Combine(DataLocation.PluginsDirectory, + $"{x.Name}-{x.NewVersion}.zip"); + Http.Download(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath); + Install(x.PluginNewUserPlugin, downloadToFilePath); - Context.API.RestartApp(); + Context.API.RestartApp(); - return true; - } + return true; + } - return false; - } - }); + return false; + } + }); return Search(results, uninstallSearch); } @@ -180,39 +237,41 @@ namespace Flow.Launcher.Plugin.PluginsManager return results.ToList(); return results - .Where(x => - { - var matchResult = StringMatcher.FuzzySearch(searchName, x.Title); - if (matchResult.IsSearchPrecisionScoreMet()) - x.Score = matchResult.Score; + .Where(x => + { + var matchResult = StringMatcher.FuzzySearch(searchName, x.Title); + if (matchResult.IsSearchPrecisionScoreMet()) + x.Score = matchResult.Score; - return matchResult.IsSearchPrecisionScoreMet(); - }) - .ToList(); + return matchResult.IsSearchPrecisionScoreMet(); + }) + .ToList(); } internal List RequestInstallOrUpdate(string searchName) { + var searchNameWithoutKeyword = searchName.Replace(Settings.HotKeyInstall, string.Empty).Trim(); + var results = pluginsManifest - .UserPlugins - .Select(x => - new Result - { - Title = $"{x.Name} by {x.Author}", - SubTitle = x.Description, - IcoPath = icoPath, - Action = e => + .UserPlugins + .Select(x => + new Result { - Application.Current.MainWindow.Hide(); - InstallOrUpdate(x); + Title = $"{x.Name} by {x.Author}", + SubTitle = x.Description, + IcoPath = icoPath, + Action = e => + { + Application.Current.MainWindow.Hide(); + InstallOrUpdate(x); - return ShouldHideWindow; - }, - ContextData = x - }); + return ShouldHideWindow; + }, + ContextData = x + }); - return Search(results, searchName); + return Search(results, searchNameWithoutKeyword); } private void Install(UserPlugin plugin, string downloadedFilePath) @@ -253,10 +312,10 @@ namespace Flow.Launcher.Plugin.PluginsManager internal List RequestUninstall(string search) { - var autocompletedResults = AutoCompleteReturnAllResults(search, - Settings.HotkeyUninstall, - "Uninstall", - "Select a plugin to uninstall"); + var autocompletedResults = AutoCompleteReturnAllResults(search, + Settings.HotkeyUninstall, + "Uninstall", + "Select a plugin to uninstall"); if (autocompletedResults.Any()) return autocompletedResults; @@ -264,32 +323,34 @@ namespace Flow.Launcher.Plugin.PluginsManager var uninstallSearch = search.Replace(Settings.HotkeyUninstall, string.Empty).TrimStart(); var results = Context.API - .GetAllPlugins() - .Select(x => - new Result - { - Title = $"{x.Metadata.Name} by {x.Metadata.Author}", - SubTitle = x.Metadata.Description, - IcoPath = x.Metadata.IcoPath, - Action = e => - { - string message = string.Format(Context.API.GetTranslation("plugin_pluginsmanager_uninstall_prompt"), - x.Metadata.Name, x.Metadata.Author, - Environment.NewLine, Environment.NewLine); + .GetAllPlugins() + .Select(x => + new Result + { + Title = $"{x.Metadata.Name} by {x.Metadata.Author}", + SubTitle = x.Metadata.Description, + IcoPath = x.Metadata.IcoPath, + Action = e => + { + string message = string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_uninstall_prompt"), + x.Metadata.Name, x.Metadata.Author, + Environment.NewLine, Environment.NewLine); - if (MessageBox.Show(message, Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"), - MessageBoxButton.YesNo) == MessageBoxResult.Yes) - { - Application.Current.MainWindow.Hide(); - Uninstall(x.Metadata); - Context.API.RestartApp(); + if (MessageBox.Show(message, + Context.API.GetTranslation("plugin_pluginsmanager_uninstall_title"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + Application.Current.MainWindow.Hide(); + Uninstall(x.Metadata); + Context.API.RestartApp(); - return true; - } + return true; + } - return false; - } - }); + return false; + } + }); return Search(results, uninstallSearch); } @@ -317,8 +378,9 @@ namespace Flow.Launcher.Plugin.PluginsManager Action = e => { Context - .API - .ChangeQuery($"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {hotkey} "); + .API + .ChangeQuery( + $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {hotkey} "); return false; } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs index e2e8d22e5..9c5b0d29f 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs @@ -6,6 +6,7 @@ namespace Flow.Launcher.Plugin.PluginsManager { internal class Settings { + internal string HotKeyInstall { get; set; } = "install"; internal string HotkeyUninstall { get; set; } = "uninstall"; internal string HotkeyUpdate { get; set; } = "update"; diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index e970e5a8e..d94af71a1 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -6,7 +6,7 @@ "Name": "Plugins Manager", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Author": "Jeremy Wu", - "Version": "1.3.0", + "Version": "1.3.1", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", diff --git a/README.md b/README.md index 5f3f7e1a7..02f488758 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Windows may complain about security due to code not being signed, this will be c - Open flow's search window: Alt+Space is the default hotkey. - Open context menu: Ctrl+O/Shift+Enter. - Cancel/Return to previous screen: Esc. -- Install/Uninstall/Update plugins: in the search window, type `pm`/`pm uninstall`/`pm update` + the plugin name. +- Install/Uninstall/Update plugins: in the search window, type `pm install`/`pm uninstall`/`pm update` + the plugin name. - Saved user settings are located: - If using roaming: `%APPDATA%\FlowLauncher` - If using portable, by default: `%localappdata%\FlowLauncher\app-\UserData`