66using System . Text . Json ;
77using System . Threading ;
88using System . Threading . Tasks ;
9+ using System . Windows ;
910using CommunityToolkit . Mvvm . DependencyInjection ;
1011using Flow . Launcher . Core . ExternalPlugins ;
1112using Flow . Launcher . Infrastructure ;
@@ -24,6 +25,8 @@ public static class PluginManager
2425 {
2526 private static readonly string ClassName = nameof ( PluginManager ) ;
2627
28+ private static readonly Settings FlowSettings = Ioc . Default . GetRequiredService < Settings > ( ) ;
29+
2730 private static IEnumerable < PluginPair > _contextMenuPlugins ;
2831 private static IEnumerable < PluginPair > _homePlugins ;
2932
@@ -547,6 +550,177 @@ public static async Task UninstallPluginAsync(PluginMetadata plugin, bool remove
547550 await UninstallPluginAsync ( plugin , removePluginFromSettings , removePluginSettings , true ) ;
548551 }
549552
553+ public static async Task InstallPluginAndCheckRestartAsync ( UserPlugin newPlugin )
554+ {
555+ if ( API . ShowMsgBox (
556+ string . Format (
557+ API . GetTranslation ( "InstallPromptSubtitle" ) ,
558+ newPlugin . Name , newPlugin . Author , Environment . NewLine ) ,
559+ API . GetTranslation ( "InstallPromptTitle" ) ,
560+ button : MessageBoxButton . YesNo ) != MessageBoxResult . Yes ) return ;
561+
562+ try
563+ {
564+ // at minimum should provide a name, but handle plugin that is not downloaded from plugins manifest and is a url download
565+ var downloadFilename = string . IsNullOrEmpty ( newPlugin . Version )
566+ ? $ "{ newPlugin . Name } -{ Guid . NewGuid ( ) } .zip"
567+ : $ "{ newPlugin . Name } -{ newPlugin . Version } .zip";
568+
569+ var filePath = Path . Combine ( Path . GetTempPath ( ) , downloadFilename ) ;
570+
571+ using var cts = new CancellationTokenSource ( ) ;
572+
573+ if ( ! newPlugin . IsFromLocalInstallPath )
574+ {
575+ await DownloadFileAsync (
576+ $ "{ API . GetTranslation ( "DownloadingPlugin" ) } { newPlugin . Name } ",
577+ newPlugin . UrlDownload , filePath , cts ) ;
578+ }
579+ else
580+ {
581+ filePath = newPlugin . LocalInstallPath ;
582+ }
583+
584+ // check if user cancelled download before installing plugin
585+ if ( cts . IsCancellationRequested )
586+ {
587+ return ;
588+ }
589+ else
590+ {
591+ if ( ! File . Exists ( filePath ) )
592+ {
593+ throw new FileNotFoundException ( $ "Plugin { newPlugin . ID } zip file not found at { filePath } ", filePath ) ;
594+ }
595+
596+ API . InstallPlugin ( newPlugin , filePath ) ;
597+
598+ if ( ! newPlugin . IsFromLocalInstallPath )
599+ {
600+ File . Delete ( filePath ) ;
601+ }
602+ }
603+ }
604+ catch ( Exception e )
605+ {
606+ API . LogException ( ClassName , "Failed to install plugin" , e ) ;
607+ API . ShowMsgError ( API . GetTranslation ( "ErrorInstallingPlugin" ) ) ;
608+ return ; // don’t restart on failure
609+ }
610+
611+ if ( FlowSettings . AutoRestartAfterChanging )
612+ {
613+ API . RestartApp ( ) ;
614+ }
615+ else
616+ {
617+ API . ShowMsg (
618+ API . GetTranslation ( "installbtn" ) ,
619+ string . Format (
620+ API . GetTranslation (
621+ "InstallSuccessNoRestart" ) ,
622+ newPlugin . Name ) ) ;
623+ }
624+ }
625+
626+ public static async Task UninstallPluginAndCheckRestartAsync ( PluginMetadata oldPlugin )
627+ {
628+ if ( API . ShowMsgBox (
629+ string . Format (
630+ API . GetTranslation ( "UninstallPromptSubtitle" ) ,
631+ oldPlugin . Name , oldPlugin . Author , Environment . NewLine ) ,
632+ API . GetTranslation ( "UninstallPromptTitle" ) ,
633+ button : MessageBoxButton . YesNo ) != MessageBoxResult . Yes ) return ;
634+
635+ var removePluginSettings = API . ShowMsgBox (
636+ API . GetTranslation ( "KeepPluginSettingsSubtitle" ) ,
637+ API . GetTranslation ( "KeepPluginSettingsTitle" ) ,
638+ button : MessageBoxButton . YesNo ) == MessageBoxResult . No ;
639+
640+ try
641+ {
642+ await API . UninstallPluginAsync ( oldPlugin , removePluginSettings ) ;
643+ }
644+ catch ( Exception e )
645+ {
646+ API . LogException ( ClassName , "Failed to uninstall plugin" , e ) ;
647+ API . ShowMsgError ( API . GetTranslation ( "ErrorUninstallingPlugin" ) ) ;
648+ return ; // don’t restart on failure
649+ }
650+
651+ if ( FlowSettings . AutoRestartAfterChanging )
652+ {
653+ API . RestartApp ( ) ;
654+ }
655+ else
656+ {
657+ API . ShowMsg (
658+ API . GetTranslation ( "uninstallbtn" ) ,
659+ string . Format (
660+ API . GetTranslation (
661+ "UninstallSuccessNoRestart" ) ,
662+ oldPlugin . Name ) ) ;
663+ }
664+ }
665+
666+ public static async Task UpdatePluginAndCheckRestartAsync ( UserPlugin newPlugin , PluginMetadata oldPlugin )
667+ {
668+ if ( API . ShowMsgBox (
669+ string . Format (
670+ API . GetTranslation ( "UpdatePromptSubtitle" ) ,
671+ oldPlugin . Name , oldPlugin . Author , Environment . NewLine ) ,
672+ API . GetTranslation ( "UpdatePromptTitle" ) ,
673+ button : MessageBoxButton . YesNo ) != MessageBoxResult . Yes ) return ;
674+
675+ try
676+ {
677+ var filePath = Path . Combine ( Path . GetTempPath ( ) , $ "{ newPlugin . Name } -{ newPlugin . Version } .zip") ;
678+
679+ using var cts = new CancellationTokenSource ( ) ;
680+
681+ if ( ! newPlugin . IsFromLocalInstallPath )
682+ {
683+ await DownloadFileAsync (
684+ $ "{ API . GetTranslation ( "DownloadingPlugin" ) } { newPlugin . Name } ",
685+ newPlugin . UrlDownload , filePath , cts ) ;
686+ }
687+ else
688+ {
689+ filePath = newPlugin . LocalInstallPath ;
690+ }
691+
692+ // check if user cancelled download before installing plugin
693+ if ( cts . IsCancellationRequested )
694+ {
695+ return ;
696+ }
697+ else
698+ {
699+ await API . UpdatePluginAsync ( oldPlugin , newPlugin , filePath ) ;
700+ }
701+ }
702+ catch ( Exception e )
703+ {
704+ API . LogException ( ClassName , "Failed to update plugin" , e ) ;
705+ API . ShowMsgError ( API . GetTranslation ( "ErrorUpdatingPlugin" ) ) ;
706+ return ; // don’t restart on failure
707+ }
708+
709+ if ( FlowSettings . AutoRestartAfterChanging )
710+ {
711+ API . RestartApp ( ) ;
712+ }
713+ else
714+ {
715+ API . ShowMsg (
716+ API . GetTranslation ( "updatebtn" ) ,
717+ string . Format (
718+ API . GetTranslation (
719+ "UpdateSuccessNoRestart" ) ,
720+ newPlugin . Name ) ) ;
721+ }
722+ }
723+
550724 #endregion
551725
552726 #region Internal functions
@@ -694,6 +868,41 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool remo
694868 }
695869 }
696870
871+ internal static async Task DownloadFileAsync ( string prgBoxTitle , string downloadUrl , string filePath , CancellationTokenSource cts , bool deleteFile = true , bool showProgress = true )
872+ {
873+ if ( deleteFile && File . Exists ( filePath ) )
874+ File . Delete ( filePath ) ;
875+
876+ if ( showProgress )
877+ {
878+ var exceptionHappened = false ;
879+ await API . ShowProgressBoxAsync ( prgBoxTitle ,
880+ async ( reportProgress ) =>
881+ {
882+ if ( reportProgress == null )
883+ {
884+ // when reportProgress is null, it means there is expcetion with the progress box
885+ // so we record it with exceptionHappened and return so that progress box will close instantly
886+ exceptionHappened = true ;
887+ return ;
888+ }
889+ else
890+ {
891+ await API . HttpDownloadAsync ( downloadUrl , filePath , reportProgress , cts . Token ) . ConfigureAwait ( false ) ;
892+ }
893+ } , cts . Cancel ) ;
894+
895+ // if exception happened while downloading and user does not cancel downloading,
896+ // we need to redownload the plugin
897+ if ( exceptionHappened && ( ! cts . IsCancellationRequested ) )
898+ await API . HttpDownloadAsync ( downloadUrl , filePath , token : cts . Token ) . ConfigureAwait ( false ) ;
899+ }
900+ else
901+ {
902+ await API . HttpDownloadAsync ( downloadUrl , filePath , token : cts . Token ) . ConfigureAwait ( false ) ;
903+ }
904+ }
905+
697906 #endregion
698907 }
699908}
0 commit comments