using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using System.Windows.Media; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Core.Storage; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.ViewModel; using JetBrains.Annotations; using Squirrel; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher { public class PublicAPIInstance : IPublicAPI, IRemovable { private static readonly string ClassName = nameof(PublicAPIInstance); private readonly Settings _settings; private readonly MainViewModel _mainVM; // Must use getter to avoid accessing Application.Current.Resources.MergedDictionaries so earlier in theme constructor private Theme _theme; private Theme Theme => _theme ??= Ioc.Default.GetRequiredService(); // Must use getter to avoid circular dependency private Updater _updater; private Updater Updater => _updater ??= Ioc.Default.GetRequiredService(); private readonly object _saveSettingsLock = new(); #region Constructor public PublicAPIInstance(Settings settings, MainViewModel mainVM) { _settings = settings; _mainVM = mainVM; GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback; WebRequest.RegisterPrefix("data", new DataWebRequestFactory()); } #endregion #region Public API public void ChangeQuery(string query, bool requery = false) { _mainVM.ChangeQueryText(query, requery); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] public async void RestartApp() { _mainVM.Hide(); // 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); } public void ShowMainWindow() => _mainVM.Show(); public void FocusQueryTextBox() => _mainVM.FocusQueryTextBox(); public void HideMainWindow() => _mainVM.Hide(); public bool IsMainWindowVisible() => _mainVM.MainWindowVisibilityStatus; public event VisibilityChangedEventHandler VisibilityChanged { add => _mainVM.VisibilityChanged += value; remove => _mainVM.VisibilityChanged -= value; } public void CheckForNewUpdate() => _ = Updater.UpdateAppAsync(false); public void SaveAppAllSettings() { lock (_saveSettingsLock) { _settings.Save(); PluginManager.Save(); _mainVM.Save(); } _ = ImageLoader.SaveAsync(); } public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync(); public void ShowMsgError(string title, string subTitle = "") => ShowMsg(title, subTitle, Constant.ErrorIcon, true); public void ShowMsg(string title, string subTitle = "", string iconPath = "") => ShowMsg(title, subTitle, iconPath, true); public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) { Notification.Show(title, subTitle, iconPath); } public void OpenSettingDialog() { Application.Current.Dispatcher.Invoke(() => { SettingWindow sw = SingletonWindowOpener.Open(); }); } public void ShellRun(string cmd, string filename = "cmd.exe") { var args = filename == "cmd.exe" ? $"/C {cmd}" : $"{cmd}"; var startInfo = ShellCommand.SetProcessStartInfo(filename, arguments: args, createNoWindow: true); ShellCommand.Execute(startInfo); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] public async void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true) { if (string.IsNullOrEmpty(stringToCopy)) { return; } var isFile = File.Exists(stringToCopy); if (directCopy && (isFile || Directory.Exists(stringToCopy))) { // Sometimes the clipboard is locked and cannot be accessed, // we need to retry a few times before giving up var exception = await RetryActionOnSTAThreadAsync(() => { var paths = new StringCollection { stringToCopy }; Clipboard.SetFileDropList(paths); }); if (exception == null) { if (showDefaultNotification) { ShowMsg( $"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}", GetTranslation("completedSuccessfully")); } } else { LogException(nameof(PublicAPIInstance), "Failed to copy file/folder to clipboard", exception); ShowMsgError(GetTranslation("failedToCopy")); } } else { // Sometimes the clipboard is locked and cannot be accessed, // we need to retry a few times before giving up var exception = await RetryActionOnSTAThreadAsync(() => { // We should use SetText instead of SetDataObject to avoid the clipboard being locked by other applications Clipboard.SetText(stringToCopy); }); if (exception == null) { if (showDefaultNotification) { ShowMsg( $"{GetTranslation("copy")} {GetTranslation("textTitle")}", GetTranslation("completedSuccessfully")); } } else { LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception); ShowMsgError(GetTranslation("failedToCopy")); } } } private static async Task RetryActionOnSTAThreadAsync(Action action, int retryCount = 6, int retryDelay = 150) { for (var i = 0; i < retryCount; i++) { try { await Win32Helper.StartSTATaskAsync(action).ConfigureAwait(false); break; } catch (Exception e) { if (i == retryCount - 1) { return e; } await Task.Delay(retryDelay); } } return null; } public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible; public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; public string GetTranslation(string key) => Internationalization.GetTranslation(key); public List GetAllPlugins() => PluginManager.AllPlugins.ToList(); public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); public Task HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url, token); public Task HttpGetStreamAsync(string url, CancellationToken token = default) => Http.GetStreamAsync(url, token); public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default) =>Http.DownloadAsync(url, filePath, reportProgress, token); public void AddActionKeyword(string pluginId, string newActionKeyword) => PluginManager.AddActionKeyword(pluginId, newActionKeyword); public bool ActionKeywordAssigned(string actionKeyword) => PluginManager.ActionKeywordRegistered(actionKeyword); public void RemoveActionKeyword(string pluginId, string oldActionKeyword) => PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword); public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") => Log.Debug(className, message, methodName); public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") => Log.Info(className, message, methodName); public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") => Log.Warn(className, message, methodName); public void LogError(string className, string message, [CallerMemberName] string methodName = "") => Log.Error(className, message, methodName); public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName); private readonly ConcurrentDictionary _pluginJsonStorages = new(); public void RemovePluginSettings(string assemblyName) { foreach (var keyValuePair in _pluginJsonStorages) { var key = keyValuePair.Key; var value = keyValuePair.Value; var name = value.GetType().GetField("AssemblyName")?.GetValue(value)?.ToString(); if (name == assemblyName) { _pluginJsonStorages.TryRemove(key, out var _); } } } public void SavePluginSettings() { foreach (var value in _pluginJsonStorages.Values) { var savable = value as ISavable; savable?.Save(); } } public T LoadSettingJsonStorage() where T : new() { var type = typeof(T); if (!_pluginJsonStorages.ContainsKey(type)) _pluginJsonStorages[type] = new PluginJsonStorage(); return ((PluginJsonStorage)_pluginJsonStorages[type]).Load(); } public void SaveSettingJsonStorage() where T : new() { var type = typeof(T); if (!_pluginJsonStorages.ContainsKey(type)) _pluginJsonStorages[type] = new PluginJsonStorage(); ((PluginJsonStorage)_pluginJsonStorages[type]).Save(); } public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null) { try { using var explorer = new Process(); var explorerInfo = _settings.CustomExplorer; var explorerPath = explorerInfo.Path.Trim().ToLowerInvariant(); var targetPath = FileNameOrFilePath is null ? DirectoryPath : Path.IsPathRooted(FileNameOrFilePath) ? FileNameOrFilePath : Path.Combine(DirectoryPath, FileNameOrFilePath); if (Path.GetFileNameWithoutExtension(explorerPath) == "explorer") { // Windows File Manager explorer.StartInfo = new ProcessStartInfo { FileName = targetPath, UseShellExecute = true }; } else { // Custom File Manager explorer.StartInfo = new ProcessStartInfo { FileName = explorerInfo.Path.Replace("%d", DirectoryPath), UseShellExecute = true, Arguments = FileNameOrFilePath is null ? explorerInfo.DirectoryArgument.Replace("%d", DirectoryPath) : explorerInfo.FileArgument .Replace("%d", DirectoryPath) .Replace("%f", targetPath) }; } explorer.Start(); } catch (Win32Exception ex) when (ex.NativeErrorCode == 2) { LogError(ClassName, "File Manager not found"); ShowMsgBox( string.Format(GetTranslation("fileManagerNotFound"), ex.Message), GetTranslation("fileManagerNotFoundTitle"), MessageBoxButton.OK, MessageBoxImage.Error ); } catch (Exception ex) { LogException(ClassName, "Failed to open folder", ex); ShowMsgBox( string.Format(GetTranslation("folderOpenError"), ex.Message), GetTranslation("errorTitle"), MessageBoxButton.OK, MessageBoxImage.Error ); } } private void OpenUri(Uri uri, bool? inPrivate = null) { if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) { var browserInfo = _settings.CustomBrowser; var path = browserInfo.Path == "*" ? "" : browserInfo.Path; if (browserInfo.OpenInTab) { uri.AbsoluteUri.OpenInBrowserTab(path, inPrivate ?? browserInfo.EnablePrivate, browserInfo.PrivateArg); } else { uri.AbsoluteUri.OpenInBrowserWindow(path, inPrivate ?? browserInfo.EnablePrivate, browserInfo.PrivateArg); } } else { Process.Start(new ProcessStartInfo() { FileName = uri.AbsoluteUri, UseShellExecute = true })?.Dispose(); return; } } public void OpenUrl(string url, bool? inPrivate = null) { OpenUri(new Uri(url), inPrivate); } public void OpenUrl(Uri url, bool? inPrivate = null) { OpenUri(url, inPrivate); } public void OpenAppUri(string appUri) { OpenUri(new Uri(appUri)); } public void OpenAppUri(Uri appUri) { OpenUri(appUri); } public void ToggleGameMode() { _mainVM.ToggleGameMode(); } public void SetGameMode(bool value) { _mainVM.GameModeStatus = value; } public bool IsGameModeOn() { return _mainVM.GameModeStatus; } private readonly List> _globalKeyboardHandlers = new(); public void RegisterGlobalKeyboardCallback(Func callback) => _globalKeyboardHandlers.Add(callback); public void RemoveGlobalKeyboardCallback(Func callback) => _globalKeyboardHandlers.Remove(callback); public void ReQuery(bool reselect = true) => _mainVM.ReQuery(reselect); public void BackToQueryResults() => _mainVM.BackToQueryResults(); public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK) => MessageBoxEx.Show(messageBoxText, caption, button, icon, defaultResult); public Task ShowProgressBoxAsync(string caption, Func, Task> reportProgressAsync, Action cancelProgress = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, cancelProgress); public List GetAvailableThemes() => Theme.GetAvailableThemes(); public ThemeData GetCurrentTheme() => Theme.GetCurrentTheme(); public bool SetCurrentTheme(ThemeData theme) => Theme.ChangeTheme(theme.FileNameWithoutExtension); private readonly ConcurrentDictionary<(string, string, Type), object> _pluginBinaryStorages = new(); public void RemovePluginCaches(string cacheDirectory) { foreach (var keyValuePair in _pluginBinaryStorages) { var key = keyValuePair.Key; var currentCacheDirectory = key.Item2; if (cacheDirectory == currentCacheDirectory) { _pluginBinaryStorages.TryRemove(key, out var _); } } } public void SavePluginCaches() { foreach (var value in _pluginBinaryStorages.Values) { var savable = value as ISavable; savable?.Save(); } } public async Task LoadCacheBinaryStorageAsync(string cacheName, string cacheDirectory, T defaultData) where T : new() { var type = typeof(T); if (!_pluginBinaryStorages.ContainsKey((cacheName, cacheDirectory, type))) _pluginBinaryStorages[(cacheName, cacheDirectory, type)] = new PluginBinaryStorage(cacheName, cacheDirectory); return await ((PluginBinaryStorage)_pluginBinaryStorages[(cacheName, cacheDirectory, type)]).TryLoadAsync(defaultData); } public async Task SaveCacheBinaryStorageAsync(string cacheName, string cacheDirectory) where T : new() { var type = typeof(T); if (!_pluginBinaryStorages.ContainsKey((cacheName, cacheDirectory, type))) _pluginBinaryStorages[(cacheName, cacheDirectory, type)] = new PluginBinaryStorage(cacheName, cacheDirectory); await ((PluginBinaryStorage)_pluginBinaryStorages[(cacheName, cacheDirectory, type)]).SaveAsync(); } public ValueTask LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true) => ImageLoader.LoadAsync(path, loadFullImage, cacheImage); public Task UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) => PluginsManifest.UpdateManifestAsync(usePrimaryUrlOnly, token); public IReadOnlyList 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); public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "") => Stopwatch.Debug(className, message, action, methodName); public Task StopwatchLogDebugAsync(string className, string message, Func action, [CallerMemberName] string methodName = "") => Stopwatch.DebugAsync(className, message, action, methodName); public long StopwatchLogInfo(string className, string message, Action action, [CallerMemberName] string methodName = "") => Stopwatch.Info(className, message, action, methodName); public Task StopwatchLogInfoAsync(string className, string message, Func action, [CallerMemberName] string methodName = "") => Stopwatch.InfoAsync(className, message, action, methodName); #endregion #region Private Methods private bool KListener_hookedKeyboardCallback(KeyEvent keyevent, int vkcode, SpecialKeyState state) { var continueHook = true; foreach (var x in _globalKeyboardHandlers) { continueHook &= x((int)keyevent, vkcode, state); } return continueHook; } #endregion } }