@@ -95,6 +95,7 @@ public PackageRow? SelectedPackage
9595 public RelayCommand RefreshCommand { get ; }
9696 public RelayCommand AddFromGitHubCommand { get ; }
9797 public RelayCommand CreateBundleCommand { get ; }
98+ public RelayCommand InstallBundleFromFileCommand { get ; }
9899 public RelayCommand < CatalogPackageVersion > ChooseVersionCommand { get ; }
99100 public RelayCommand ToggleLayoutCommand { get ; }
100101 public RelayCommand < string > OpenLinkCommand { get ; }
@@ -114,6 +115,7 @@ public PackagesViewModel(UserSettingsService settingsService, CatalogService cat
114115 RefreshCommand = new RelayCommand ( RefreshAsync ) ;
115116 AddFromGitHubCommand = new RelayCommand ( AddFromGitHubAsync ) ;
116117 CreateBundleCommand = new RelayCommand ( CreateBundleAsync ) ;
118+ InstallBundleFromFileCommand = new RelayCommand ( InstallBundleFromFileAsync ) ;
117119 ChooseVersionCommand = new RelayCommand < CatalogPackageVersion > ( ChooseVersionAsync ) ;
118120 ToggleLayoutCommand = new RelayCommand ( ( ) => IsGridView = ! IsGridView ) ;
119121 OpenLinkCommand = new RelayCommand < string > ( url => { if ( ! string . IsNullOrWhiteSpace ( url ) ) ExternalLink . Open ( url ! ) ; } ) ;
@@ -594,6 +596,16 @@ private async Task CreateBundleAsync()
594596 WriteIndented = true ,
595597 DefaultIgnoreCondition = JsonIgnoreCondition . WhenWritingNull ,
596598 } ) ;
599+
600+ // Local: write the bundle JSON to a file the user keeps — no submission.
601+ if ( draft . Destination == BundleDestination . SaveToFile )
602+ {
603+ var savedPath = await Dialogs . SaveBundleFileAsync ( bundle . Id , json ) ;
604+ if ( savedPath is not null )
605+ _shell . SetStatus ( L . Tr ( "packages.status.bundleSaved" , draft . Name , draft . Packages . Count , savedPath ) , StatusKind . Success ) ;
606+ return ;
607+ }
608+
597609 var body = "### Bundle submission\n \n Add this entry to `src/BasisPM.Server/seed/bundles.json`:\n \n ```json\n " + json + "\n ```\n " ;
598610 var url = "https://github.com/BasisVR/BasisPackageManager/issues/new?labels=bundle-submission"
599611 + "&title=" + Uri . EscapeDataString ( "Add bundle: " + draft . Name )
@@ -602,6 +614,37 @@ private async Task CreateBundleAsync()
602614 _shell . SetStatus ( L . Tr ( "packages.status.openingIssue" , draft . Name , draft . Packages . Count ) , StatusKind . Success ) ;
603615 }
604616
617+ /// <summary>Reads a local bundle file (as saved by "Save to file") and installs its packages into a chosen project.</summary>
618+ private async Task InstallBundleFromFileAsync ( )
619+ {
620+ var path = await Dialogs . OpenBundleFileAsync ( ) ;
621+ if ( string . IsNullOrEmpty ( path ) ) return ;
622+
623+ Bundle ? bundle ;
624+ try
625+ {
626+ var json = await File . ReadAllTextAsync ( path ) ;
627+ bundle = JsonSerializer . Deserialize < Bundle > ( json , new JsonSerializerOptions { PropertyNameCaseInsensitive = true } ) ;
628+ }
629+ catch ( Exception ex )
630+ {
631+ _shell . SetStatus ( L . Tr ( "packages.status.bundleFileInvalid" , ex . Message ) , StatusKind . Error ) ;
632+ return ;
633+ }
634+
635+ if ( bundle is null || bundle . Packages . Count == 0 )
636+ {
637+ _shell . SetStatus ( L . Tr ( "packages.status.bundleFileEmpty" ) , StatusKind . Error ) ;
638+ return ;
639+ }
640+
641+ var target = await _shell . ChooseInstallTargetAsync ( L . Tr ( "shell.bundle.pickerLabel" , bundle . Name , bundle . Packages . Count ) ) ;
642+ if ( target is null ) return ;
643+
644+ _shell . SetActiveInstall ( target ) ;
645+ await AddBundleAsync ( bundle , target ) ;
646+ }
647+
605648 /// <summary>Adds every package in a bundle to the target project's manifest (used by the bundle deep link).</summary>
606649 public async Task AddBundleAsync ( Bundle bundle , BasisInstall target )
607650 {
0 commit comments