diff --git a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs index 64c4cd627..79d6d7605 100644 --- a/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs +++ b/Flow.Launcher.Core/ExternalPlugins/UserPlugin.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Flow.Launcher.Core.ExternalPlugins { @@ -13,9 +13,11 @@ namespace Flow.Launcher.Core.ExternalPlugins public string Website { get; set; } public string UrlDownload { get; set; } public string UrlSourceCode { get; set; } + public string LocalInstallPath { get; set; } public string IcoPath { get; set; } public DateTime? LatestReleaseDate { get; set; } public DateTime? DateAdded { get; set; } + public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath); } } diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 7f4d4ea35..e7dfb31c0 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -380,7 +380,8 @@ namespace Flow.Launcher.Core.Plugin /// - /// Update a plugin to new version, from a zip file. Will Delete zip after updating. + /// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url, + /// unless it's a local path installation /// public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) { @@ -390,11 +391,11 @@ namespace Flow.Launcher.Core.Plugin } /// - /// Install a plugin. Will Delete zip after updating. + /// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation /// public static void InstallPlugin(UserPlugin plugin, string zipFilePath) { - InstallPlugin(plugin, zipFilePath, true); + InstallPlugin(plugin, zipFilePath, checkModified: true); } /// @@ -420,7 +421,9 @@ namespace Flow.Launcher.Core.Plugin // Unzip plugin files to temp folder var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath); - File.Delete(zipFilePath); + + if(!plugin.IsFromLocalInstallPath) + File.Delete(zipFilePath); var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath); diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 7bb8fe200..61a9da400 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -31,6 +31,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings public string SelectPrevPageHotkey { get; set; } = $"PageDown"; public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O"; public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; + public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; + public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; public string Language { @@ -280,42 +282,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings { get { - var list = new List - { - new("Up", "HotkeyLeftRightDesc"), - new("Down", "HotkeyLeftRightDesc"), - new("Left", "HotkeyUpDownDesc"), - new("Right", "HotkeyUpDownDesc"), - new("Escape", "HotkeyESCDesc"), - new("F5", "ReloadPluginHotkey"), - new("Alt+Home", "HotkeySelectFirstResult"), - new("Alt+End", "HotkeySelectLastResult"), - new("Ctrl+R", "HotkeyRequery"), - new("Ctrl+H", "ToggleHistoryHotkey"), - new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"), - new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"), - new("Ctrl+OemPlus", "QuickHeightHotkey"), - new("Ctrl+OemMinus", "QuickHeightHotkey"), - new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"), - new("Shift+Enter", "OpenContextMenuHotkey"), - new("Enter", "HotkeyRunDesc"), - new("Ctrl+Enter", "OpenContainFolderHotkey"), - new("Alt+Enter", "HotkeyOpenResult"), - new("Ctrl+F12", "ToggleGameModeHotkey"), - new("Ctrl+Shift+C", "CopyFilePathHotkey"), - - new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1), - new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2), - new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3), - new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4), - new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5), - new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6), - new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7), - new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8), - new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9), - new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10) - }; + var list = FixedHotkeys(); + // Customizeable hotkeys if(!string.IsNullOrEmpty(Hotkey)) list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = "")); if(!string.IsNullOrEmpty(PreviewHotkey)) @@ -340,7 +309,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = "")); if(!string.IsNullOrEmpty(SelectPrevPageHotkey)) list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = "")); + if (!string.IsNullOrEmpty(CycleHistoryUpHotkey)) + list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = "")); + if (!string.IsNullOrEmpty(CycleHistoryDownHotkey)) + list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = "")); + // Custom Query Hotkeys foreach (var customPluginHotkey in CustomPluginHotkeys) { if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey)) @@ -350,6 +324,45 @@ namespace Flow.Launcher.Infrastructure.UserSettings return list; } } + + private List FixedHotkeys() + { + return new List + { + new("Up", "HotkeyLeftRightDesc"), + new("Down", "HotkeyLeftRightDesc"), + new("Left", "HotkeyUpDownDesc"), + new("Right", "HotkeyUpDownDesc"), + new("Escape", "HotkeyESCDesc"), + new("F5", "ReloadPluginHotkey"), + new("Alt+Home", "HotkeySelectFirstResult"), + new("Alt+End", "HotkeySelectLastResult"), + new("Ctrl+R", "HotkeyRequery"), + new("Ctrl+H", "ToggleHistoryHotkey"), + new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"), + new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"), + new("Ctrl+OemPlus", "QuickHeightHotkey"), + new("Ctrl+OemMinus", "QuickHeightHotkey"), + new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"), + new("Shift+Enter", "OpenContextMenuHotkey"), + new("Enter", "HotkeyRunDesc"), + new("Ctrl+Enter", "OpenContainFolderHotkey"), + new("Alt+Enter", "HotkeyOpenResult"), + new("Ctrl+F12", "ToggleGameModeHotkey"), + new("Ctrl+Shift+C", "CopyFilePathHotkey"), + + new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1), + new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2), + new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3), + new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4), + new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5), + new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6), + new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7), + new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8), + new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9), + new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10) + }; + } } public enum LastQueryMode diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 4137ca8d0..dd8c4b112 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; #pragma warning disable IDE0005 using System.Windows; #pragma warning restore IDE0005 @@ -200,6 +201,24 @@ namespace Flow.Launcher.Plugin.SharedCommands } } + /// + /// This checks whether a given string is a zip file path. + /// By default does not check if the zip file actually exist on disk, can do so by + /// setting checkFileExists = true. + /// + public static bool IsZipFilePath(string querySearchString, bool checkFileExists = false) + { + if (IsLocationPathString(querySearchString) && querySearchString.Split('.').Last() == "zip") + { + if (checkFileExists) + return FileExists(querySearchString); + + return true; + } + + return false; + } + /// /// This checks whether a given string is a directory path or network location string. /// It does not check if location actually exists. diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index e9d37433f..80cb74729 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -191,11 +191,15 @@ namespace Flow.Launcher.Test.Plugins [TestCase(@"\c:\", false)] [TestCase(@"cc:\", false)] [TestCase(@"\\\SomeNetworkLocation\", false)] + [TestCase(@"\\SomeNetworkLocation\", true)] [TestCase("RandomFile", false)] [TestCase(@"c:\>*", true)] [TestCase(@"c:\>", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation", true)] + [TestCase(@"c:\SomeLocation\SomeOtherLocation\SomeFile.exe", true)] + [TestCase(@"\\SomeNetworkLocation\SomeFile.exe", true)] + public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) { // When, Given diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index fd24f825e..9ab05555c 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -188,6 +188,8 @@ Select Previous Item Next Page Previous Page + Cycle Previous Query + Cycle Next Query Open Context Menu Open Setting Window Copy File Path diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 2e6e973a7..2315d99ec 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -25,6 +25,7 @@ LocationChanged="OnLocationChanged" Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" PreviewKeyDown="OnKeyDown" + PreviewKeyUp="OnKeyUp" ResizeMode="NoResize" ShowInTaskbar="False" SizeToContent="Height" @@ -197,6 +198,14 @@ Key="{Binding SelectPrevPageHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}" Command="{Binding SelectPrevPageCommand}" Modifiers="{Binding SelectPrevPageHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" /> + + diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 76e587cdc..9c5ed46c0 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -47,6 +47,7 @@ namespace Flow.Launcher private MainViewModel _viewModel; private bool _animating; MediaPlayer animationSound = new MediaPlayer(); + private bool isArrowKeyPressed = false; #endregion @@ -109,9 +110,11 @@ namespace Flow.Launcher private void OnInitialized(object sender, EventArgs e) { } - private void OnLoaded(object sender, RoutedEventArgs _) { + // MouseEventHandler + PreviewMouseMove += MainPreviewMouseMove; + CheckFirstLaunch(); HideStartup(); // show notify icon when flowlauncher is hidden @@ -406,6 +409,7 @@ namespace Flow.Launcher if (_animating) return; + isArrowKeyPressed = true; _animating = true; UpdatePosition(); @@ -494,6 +498,7 @@ namespace Flow.Launcher windowsb.Completed += (_, _) => _animating = false; _settings.WindowLeft = Left; _settings.WindowTop = Top; + isArrowKeyPressed = false; if (QueryTextBox.Text.Length == 0) { @@ -644,10 +649,12 @@ namespace Flow.Launcher switch (e.Key) { case Key.Down: + isArrowKeyPressed = true; _viewModel.SelectNextItemCommand.Execute(null); e.Handled = true; break; case Key.Up: + isArrowKeyPressed = true; _viewModel.SelectPrevItemCommand.Execute(null); e.Handled = true; break; @@ -698,7 +705,21 @@ namespace Flow.Launcher } } + private void OnKeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Up || e.Key == Key.Down) + { + isArrowKeyPressed = false; + } + } + private void MainPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) + { + if (isArrowKeyPressed) + { + e.Handled = true; // Ignore Mouse Hover when press Arrowkeys + } + } public void PreviewReset() { _viewModel.ResetPreview(); diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml index 07831aca8..2698e8609 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml @@ -139,6 +139,26 @@ Type="Inside"> + + + + + + = 0 && _settings.SettingWindowLeft >= 0) - { - Top = _settings.SettingWindowTop; - Left = _settings.SettingWindowLeft; - } - else + if (_settings.SettingWindowTop == null) { Top = WindowTop(); Left = WindowLeft(); } + else + { + Top = _settings.SettingWindowTop; + Left = _settings.SettingWindowLeft; + } WindowState = _settings.SettingWindowState; } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 67efc6a86..01dc67b98 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -42,6 +42,7 @@ namespace Flow.Launcher.ViewModel private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; private readonly FlowLauncherJsonStorage _topMostRecordStorage; private readonly History _history; + private int lastHistoryIndex = 1; private readonly UserSelectedRecord _userSelectedRecord; private readonly TopMostRecord _topMostRecord; @@ -83,6 +84,12 @@ namespace Flow.Launcher.ViewModel case nameof(Settings.AutoCompleteHotkey): OnPropertyChanged(nameof(AutoCompleteHotkey)); break; + case nameof(Settings.CycleHistoryUpHotkey): + OnPropertyChanged(nameof(CycleHistoryUpHotkey)); + break; + case nameof(Settings.CycleHistoryDownHotkey): + OnPropertyChanged(nameof(CycleHistoryDownHotkey)); + break; case nameof(Settings.AutoCompleteHotkey2): OnPropertyChanged(nameof(AutoCompleteHotkey2)); break; @@ -256,6 +263,32 @@ namespace Flow.Launcher.ViewModel } } + [RelayCommand] + public void ReverseHistory() + { + if (_history.Items.Count > 0) + { + ChangeQueryText(_history.Items[_history.Items.Count - lastHistoryIndex].Query.ToString()); + if (lastHistoryIndex < _history.Items.Count) + { + lastHistoryIndex++; + } + } + } + + [RelayCommand] + public void ForwardHistory() + { + if (_history.Items.Count > 0) + { + ChangeQueryText(_history.Items[_history.Items.Count - lastHistoryIndex].Query.ToString()); + if (lastHistoryIndex > 1) + { + lastHistoryIndex--; + } + } + } + [RelayCommand] private void LoadContextMenu() { @@ -346,6 +379,7 @@ namespace Flow.Launcher.ViewModel { _userSelectedRecord.Add(result); _history.Add(result.OriginQuery.RawQuery); + lastHistoryIndex = 1; } if (hideWindow) @@ -394,7 +428,18 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void SelectPrevItem() { - SelectedResults.SelectPrevResult(); + if (_history.Items.Count > 0 + && QueryText == string.Empty + && SelectedIsFromQueryResults()) + { + lastHistoryIndex = 1; + ReverseHistory(); + } + else + { + SelectedResults.SelectPrevResult(); + } + } [RelayCommand] @@ -690,6 +735,8 @@ namespace Flow.Launcher.ViewModel public string SelectPrevPageHotkey => VerifyOrSetDefaultHotkey(Settings.SelectPrevPageHotkey, ""); public string OpenContextMenuHotkey => VerifyOrSetDefaultHotkey(Settings.OpenContextMenuHotkey, "Ctrl+O"); public string SettingWindowHotkey => VerifyOrSetDefaultHotkey(Settings.SettingWindowHotkey, "Ctrl+I"); + public string CycleHistoryUpHotkey => VerifyOrSetDefaultHotkey(Settings.CycleHistoryUpHotkey, "Alt+Up"); + public string CycleHistoryDownHotkey => VerifyOrSetDefaultHotkey(Settings.CycleHistoryDownHotkey, "Alt+Down"); public string Image => Constant.QueryTextBoxIconImagePath; @@ -1116,6 +1163,7 @@ namespace Flow.Launcher.ViewModel public async void Hide() { + lastHistoryIndex = 1; // Trick for no delay MainWindowOpacity = 0; lastContextMenuResult = new Result(); diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/zipfolder.png b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/zipfolder.png new file mode 100644 index 000000000..5d3ed0ace Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.PluginsManager/Images/zipfolder.png differ diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml index a89d9df21..de6a3a2fb 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml @@ -26,7 +26,6 @@ {0} by {1} {2}{3}Would you like to update this plugin? After the update Flow will automatically restart. {0} by {1} {2}{2}Would you like to update this plugin? Plugin Update - This plugin has an update, would you like to see it? This plugin is already installed Plugin Manifest Download Failed Please check if you can connect to github.com. This error means you may not be able to install or update plugins. diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 8cd58ac52..15cbda7f2 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -3,11 +3,9 @@ using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; @@ -99,13 +97,12 @@ namespace Flow.Launcher.Plugin.PluginsManager if (Context.API.GetAllPlugins() .Any(x => x.Metadata.ID == plugin.ID && x.Metadata.Version.CompareTo(plugin.Version) < 0)) { - 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.UpdateCommand} {plugin.Name}"); + var updateDetail = !plugin.IsFromLocalInstallPath ? plugin.Name : plugin.LocalInstallPath; + + Context + .API + .ChangeQuery( + $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.UpdateCommand} {updateDetail}"); var mainWindow = Application.Current.MainWindow; mainWindow.Show(); @@ -147,12 +144,17 @@ namespace Flow.Launcher.Plugin.PluginsManager try { - if (File.Exists(filePath)) + if (!plugin.IsFromLocalInstallPath) { - File.Delete(filePath); - } + if (File.Exists(filePath)) + File.Delete(filePath); - await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false); + await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false); + } + else + { + filePath = plugin.LocalInstallPath; + } Install(plugin, filePath); } @@ -193,24 +195,38 @@ namespace Flow.Launcher.Plugin.PluginsManager { await PluginsManifest.UpdateManifestAsync(token, usePrimaryUrlOnly); + var pluginFromLocalPath = null as UserPlugin; + var updateFromLocalPath = false; + + if (FilesFolders.IsZipFilePath(search, checkFileExists: true)) + { + pluginFromLocalPath = Utilities.GetPluginInfoFromZip(search); + pluginFromLocalPath.LocalInstallPath = search; + updateFromLocalPath = true; + } + + var updateSource = !updateFromLocalPath + ? PluginsManifest.UserPlugins + : new List { pluginFromLocalPath }; + var resultsForUpdate = ( from existingPlugin in Context.API.GetAllPlugins() - join pluginFromManifest in PluginsManifest.UserPlugins - on existingPlugin.Metadata.ID equals pluginFromManifest.ID - where String.Compare(existingPlugin.Metadata.Version, pluginFromManifest.Version, + join pluginUpdateSource in updateSource + on existingPlugin.Metadata.ID equals pluginUpdateSource.ID + where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version, StringComparison.InvariantCulture) < - 0 // if current version precedes manifest version + 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest) && !PluginManager.PluginModified(existingPlugin.Metadata.ID) select new { - pluginFromManifest.Name, - pluginFromManifest.Author, + pluginUpdateSource.Name, + pluginUpdateSource.Author, CurrentVersion = existingPlugin.Metadata.Version, - NewVersion = pluginFromManifest.Version, + NewVersion = pluginUpdateSource.Version, existingPlugin.Metadata.IcoPath, PluginExistingMetadata = existingPlugin.Metadata, - PluginNewUserPlugin = pluginFromManifest + PluginNewUserPlugin = pluginUpdateSource }).ToList(); if (!resultsForUpdate.Any()) @@ -261,13 +277,21 @@ namespace Flow.Launcher.Plugin.PluginsManager _ = Task.Run(async delegate { - if (File.Exists(downloadToFilePath)) + if (!x.PluginNewUserPlugin.IsFromLocalInstallPath) { - File.Delete(downloadToFilePath); - } + if (File.Exists(downloadToFilePath)) + { + File.Delete(downloadToFilePath); + } - await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath) - .ConfigureAwait(false); + await Http.DownloadAsync(x.PluginNewUserPlugin.UrlDownload, downloadToFilePath) + .ConfigureAwait(false); + } + else + { + downloadToFilePath = x.PluginNewUserPlugin.LocalInstallPath; + } + PluginManager.UpdatePlugin(x.PluginExistingMetadata, x.PluginNewUserPlugin, downloadToFilePath); @@ -396,7 +420,7 @@ namespace Flow.Launcher.Plugin.PluginsManager results = results.Prepend(updateAllResult); } - return Search(results, search); + return !updateFromLocalPath ? Search(results, search) : results.ToList(); } internal bool PluginExists(string id) @@ -470,6 +494,42 @@ namespace Flow.Launcher.Plugin.PluginsManager return new List { result }; } + internal List InstallFromLocalPath(string localPath) + { + var plugin = Utilities.GetPluginInfoFromZip(localPath); + + plugin.LocalInstallPath = localPath; + + return new List + { + new Result + { + Title = $"{plugin.Name} by {plugin.Author}", + SubTitle = plugin.Description, + IcoPath = plugin.IcoPath, + Action = e => + { + if (Settings.WarnFromUnknownSource) + { + if (!InstallSourceKnown(plugin.Website) + && MessageBox.Show(string.Format( + Context.API.GetTranslation("plugin_pluginsmanager_install_unknown_source_warning"), + Environment.NewLine), + Context.API.GetTranslation( + "plugin_pluginsmanager_install_unknown_source_warning_title"), + MessageBoxButton.YesNo) == MessageBoxResult.No) + return false; + } + + Application.Current.MainWindow.Hide(); + _ = InstallOrUpdateAsync(plugin); + + return ShouldHideWindow; + } + } + }; + } + private bool InstallSourceKnown(string url) { var author = url.Split('/')[3]; @@ -489,6 +549,9 @@ namespace Flow.Launcher.Plugin.PluginsManager && search.Split('.').Last() == zip) return InstallFromWeb(search); + if (FilesFolders.IsZipFilePath(search, checkFileExists: true)) + return InstallFromLocalPath(search); + var results = PluginsManifest .UserPlugins @@ -522,10 +585,13 @@ namespace Flow.Launcher.Plugin.PluginsManager if (!File.Exists(downloadedFilePath)) throw new FileNotFoundException($"Plugin {plugin.ID} zip file not found at {downloadedFilePath}", downloadedFilePath); + try { PluginManager.InstallPlugin(plugin, downloadedFilePath); - File.Delete(downloadedFilePath); + + if (!plugin.IsFromLocalInstallPath) + File.Delete(downloadedFilePath); } catch (FileNotFoundException e) { diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs index 792891ad1..9800fa020 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs @@ -1,5 +1,10 @@ -using ICSharpCode.SharpZipLib.Zip; +using Flow.Launcher.Core.ExternalPlugins; +using Flow.Launcher.Infrastructure.UserSettings; +using ICSharpCode.SharpZipLib.Zip; +using Newtonsoft.Json; using System.IO; +using System.IO.Compression; +using System.Linq; namespace Flow.Launcher.Plugin.PluginsManager { @@ -55,5 +60,28 @@ namespace Flow.Launcher.Plugin.PluginsManager return string.Empty; } + + internal static UserPlugin GetPluginInfoFromZip(string filePath) + { + var plugin = null as UserPlugin; + + using (ZipArchive archive = System.IO.Compression.ZipFile.OpenRead(filePath)) + { + var pluginJsonPath = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json").ToString(); + ZipArchiveEntry pluginJsonEntry = archive.GetEntry(pluginJsonPath); + + if (pluginJsonEntry != null) + { + using (StreamReader reader = new StreamReader(pluginJsonEntry.Open())) + { + string pluginJsonContent = reader.ReadToEnd(); + plugin = JsonConvert.DeserializeObject(pluginJsonContent); + plugin.IcoPath = "Images\\zipfolder.png"; + } + } + } + + return plugin; + } } }