@@ -46,6 +46,9 @@ mod imp {
4646 pub selected_image : RefCell < String > ,
4747 pub prefill_query : RefCell < Option < Query < Option < String > > > > ,
4848 pub url_validation_query : RefCell < Option < Query < bool > > > ,
49+ pub ini_content_query : RefCell < Option < Query < String > > > ,
50+ #[ property( get, set) ]
51+ pub ini_approved : Cell < bool > ,
4952 pub home_row_expander : adw:: ExpanderRow ,
5053 #[ property( get, set, nullable) ]
5154 pub home_folder : RefCell < Option < String > > ,
@@ -481,11 +484,148 @@ impl CreateDistroboxDialog {
481484 url_group. add ( & url_row) ;
482485 content. append ( & url_group) ;
483486
487+ // Create preview expandable row
488+ let preview_expander = adw:: ExpanderRow :: new ( ) ;
489+ preview_expander. set_title ( & gettext ( "File Preview" ) ) ;
490+ preview_expander. set_subtitle ( & gettext ( "Loading..." ) ) ;
491+ preview_expander. set_enable_expansion ( false ) ;
492+ preview_expander. set_visible ( false ) ;
493+
494+ // Create TextView for content display
495+ let text_view = gtk:: TextView :: new ( ) ;
496+ text_view. set_editable ( false ) ;
497+ text_view. set_cursor_visible ( false ) ;
498+ text_view. set_monospace ( true ) ;
499+ text_view. set_wrap_mode ( gtk:: WrapMode :: None ) ;
500+ text_view. set_margin_start ( 12 ) ;
501+ text_view. set_margin_end ( 12 ) ;
502+ text_view. set_margin_top ( 6 ) ;
503+ text_view. set_margin_bottom ( 6 ) ;
504+
505+ // Wrap TextView in ScrolledWindow
506+ let scrolled_window = gtk:: ScrolledWindow :: new ( ) ;
507+ scrolled_window. set_child ( Some ( & text_view) ) ;
508+ scrolled_window. set_min_content_height ( 200 ) ;
509+ scrolled_window. set_max_content_height ( 400 ) ;
510+ scrolled_window. set_vexpand ( true ) ;
511+
512+ // Create a box to hold the scrolled window
513+ let preview_box = gtk:: Box :: new ( gtk:: Orientation :: Vertical , 0 ) ;
514+ preview_box. append ( & scrolled_window) ;
515+ preview_expander. add_row ( & preview_box) ;
516+
517+ // Create security warning InfoBar
518+ let warning_infobar = adw:: Banner :: new ( & gettext ( "Review the file contents carefully before proceeding. Only create containers from trusted sources." ) ) ;
519+ warning_infobar. set_revealed ( false ) ;
520+ warning_infobar. set_button_label ( Some ( & gettext ( "Re-download" ) ) ) ;
521+
522+ // Create approval checkbox
523+ let approval_checkbox = gtk:: CheckButton :: new ( ) ;
524+ approval_checkbox. set_label ( Some ( & gettext ( "I have reviewed the file and trust this source" ) ) ) ;
525+ approval_checkbox. set_margin_start ( 12 ) ;
526+ approval_checkbox. set_margin_end ( 12 ) ;
527+ approval_checkbox. set_margin_top ( 6 ) ;
528+ approval_checkbox. set_margin_bottom ( 6 ) ;
529+ approval_checkbox. set_visible ( false ) ;
530+
531+ content. append ( & preview_expander) ;
532+ content. append ( & warning_infobar) ;
533+ content. append ( & approval_checkbox) ;
534+
484535 // Add create button for URL
485536 let create_btn = self . build_create_btn ( ) ;
486537 create_btn. set_sensitive ( false ) ;
487538 content. append ( & create_btn) ;
488539
540+ // Create ini_content_query for downloading .ini file
541+ let ini_content_query: Query < String > = Query :: new (
542+ "ini-content-download" . to_string ( ) ,
543+ clone ! (
544+ #[ weak( rename_to=this) ]
545+ self ,
546+ #[ upgrade_or_panic]
547+ move || async move {
548+ if let Some ( url_text) = this. assemble_url( ) {
549+ if url_text. is_empty( ) {
550+ return Err ( anyhow:: anyhow!( "URL is empty" ) ) ;
551+ }
552+ this. download_ini_file( & url_text) . await
553+ } else {
554+ Err ( anyhow:: anyhow!( "No URL provided" ) )
555+ }
556+ }
557+ ) ,
558+ ) . with_timeout ( Duration :: from_secs ( 10 ) ) ;
559+
560+ // Wire ini_content_query success handler
561+ ini_content_query. connect_success ( clone ! (
562+ #[ weak( rename_to=this) ]
563+ self ,
564+ #[ weak]
565+ preview_expander,
566+ #[ weak]
567+ text_view,
568+ #[ weak]
569+ warning_infobar,
570+ #[ weak]
571+ approval_checkbox,
572+ move |content| {
573+ // Show preview section
574+ preview_expander. set_visible( true ) ;
575+ preview_expander. set_enable_expansion( true ) ;
576+ preview_expander. set_subtitle( & gettext( "Downloaded successfully" ) ) ;
577+ preview_expander. set_expanded( true ) ; // Auto-expand on first successful download
578+
579+ // Set content in TextView
580+ text_view. buffer( ) . set_text( content) ;
581+
582+ // Show warning and approval checkbox
583+ warning_infobar. set_revealed( true ) ;
584+ approval_checkbox. set_visible( true ) ;
585+
586+ // Show success toast
587+ let toast = adw:: Toast :: new( & gettext( "File downloaded successfully" ) ) ;
588+ this. imp( ) . toast_overlay. add_toast( toast) ;
589+ }
590+ ) ) ;
591+
592+ // Wire ini_content_query loading handler
593+ ini_content_query. connect_loading ( clone ! (
594+ #[ weak]
595+ preview_expander,
596+ move |is_loading| {
597+ if is_loading {
598+ preview_expander. set_visible( true ) ;
599+ preview_expander. set_subtitle( & gettext( "Downloading..." ) ) ;
600+ preview_expander. set_enable_expansion( false ) ;
601+ }
602+ }
603+ ) ) ;
604+
605+ // Wire ini_content_query error handler
606+ ini_content_query. connect_error ( clone ! (
607+ #[ weak( rename_to=this) ]
608+ self ,
609+ #[ weak]
610+ preview_expander,
611+ #[ weak]
612+ warning_infobar,
613+ #[ weak]
614+ approval_checkbox,
615+ move |error| {
616+ preview_expander. set_visible( true ) ;
617+ preview_expander. set_subtitle( & gettext( "Failed to download" ) ) ;
618+ preview_expander. set_enable_expansion( false ) ;
619+ warning_infobar. set_revealed( false ) ;
620+ approval_checkbox. set_visible( false ) ;
621+
622+ let toast = adw:: Toast :: new( & format!( "{}: {}" , gettext( "Download failed" ) , error) ) ;
623+ this. imp( ) . toast_overlay. add_toast( toast) ;
624+ }
625+ ) ) ;
626+
627+ * self . imp ( ) . ini_content_query . borrow_mut ( ) = Some ( ini_content_query. clone ( ) ) ;
628+
489629 // Create URL validation query with debouncing
490630 let url_validation_query: Query < bool > = Query :: new (
491631 "url-validation" . to_string ( ) ,
@@ -510,30 +650,28 @@ impl CreateDistroboxDialog {
510650 #[ weak( rename_to=this) ]
511651 self ,
512652 #[ weak]
513- create_btn,
514- #[ weak]
515653 url_row,
654+ #[ strong]
655+ ini_content_query,
516656 move |is_valid| {
517657 this. set_url_validated( * is_valid) ;
518- create_btn. set_sensitive( * is_valid) ;
519658
520659 if !is_valid {
521660 let toast = adw:: Toast :: new( & gettext( "Could not connect to URL" ) ) ;
522661 this. imp( ) . toast_overlay. add_toast( toast) ;
523662 url_row. add_css_class( "error" ) ;
524663 } else {
525664 url_row. remove_css_class( "error" ) ;
665+ // Trigger download when URL is valid
666+ ini_content_query. refetch( ) ;
526667 }
527668 }
528669 ) ) ;
529670
530671 url_validation_query. connect_error ( clone ! (
531- #[ weak]
532- create_btn,
533672 #[ weak]
534673 url_row,
535674 move |_| {
536- create_btn. set_sensitive( false ) ;
537675 url_row. add_css_class( "error" ) ;
538676 }
539677 ) ) ;
@@ -545,15 +683,28 @@ impl CreateDistroboxDialog {
545683 self ,
546684 #[ weak]
547685 create_btn,
686+ #[ weak]
687+ preview_expander,
688+ #[ weak]
689+ warning_infobar,
690+ #[ weak]
691+ approval_checkbox,
548692 #[ strong]
549693 url_validation_query,
550694 move |entry| {
551695 this. set_assemble_url( Some ( entry. text( ) ) ) ;
552696 this. set_url_validated( false ) ;
697+ this. set_ini_approved( false ) ;
553698 create_btn. set_sensitive( false ) ;
554699 // Clear error CSS when user types
555700 entry. remove_css_class( "error" ) ;
556701
702+ // Reset UI state on URL change
703+ preview_expander. set_visible( false ) ;
704+ warning_infobar. set_revealed( false ) ;
705+ approval_checkbox. set_visible( false ) ;
706+ approval_checkbox. set_active( false ) ;
707+
557708 // Debounced validation
558709 url_validation_query. refetch_with( Query :: debounce( Duration :: from_millis( 500 ) ) ) ;
559710 }
@@ -568,6 +719,34 @@ impl CreateDistroboxDialog {
568719 }
569720 ) ) ;
570721
722+ // Handle approval checkbox changes
723+ approval_checkbox. connect_toggled ( clone ! (
724+ #[ weak( rename_to=this) ]
725+ self ,
726+ #[ weak]
727+ create_btn,
728+ move |checkbox| {
729+ let is_approved = checkbox. is_active( ) ;
730+ this. set_ini_approved( is_approved) ;
731+ // Update create button sensitivity
732+ let is_valid = this. url_validated( ) ;
733+ create_btn. set_sensitive( is_valid && is_approved) ;
734+ }
735+ ) ) ;
736+
737+ // Handle re-download button in warning banner
738+ warning_infobar. connect_button_clicked ( clone ! (
739+ #[ weak]
740+ approval_checkbox,
741+ #[ strong]
742+ ini_content_query,
743+ move |_| {
744+ // Reset approval when re-downloading
745+ approval_checkbox. set_active( false ) ;
746+ ini_content_query. refetch( ) ;
747+ }
748+ ) ) ;
749+
571750 // Handle create click
572751 create_btn. connect_clicked ( clone ! (
573752 #[ weak( rename_to=this) ]
@@ -1003,4 +1182,34 @@ impl CreateDistroboxDialog {
10031182 let output = command_runner. output ( cmd) . await ?;
10041183 Ok ( output. status . success ( ) )
10051184 }
1185+
1186+ async fn download_ini_file ( & self , url : & str ) -> anyhow:: Result < String > {
1187+ // Download the .ini file content using curl
1188+ // CRITICAL: Use self.root_store().command_runner() for Flatpak compatibility
1189+ let command_runner = self . root_store ( ) . command_runner ( ) ;
1190+ let mut cmd = Command :: new ( "curl" ) ;
1191+ cmd. arg ( "-s" ) ; // Silent
1192+ cmd. arg ( "-f" ) ; // Fail on HTTP errors
1193+ cmd. arg ( "-L" ) ; // Follow redirects
1194+ cmd. arg ( "--connect-timeout" ) ;
1195+ cmd. arg ( "10" ) ; // 10 second connection timeout
1196+ cmd. arg ( "--max-time" ) ;
1197+ cmd. arg ( "30" ) ; // 30 second max time
1198+ cmd. arg ( url) ;
1199+
1200+ let output = command_runner. output ( cmd) . await ?;
1201+
1202+ if !output. status . success ( ) {
1203+ return Err ( anyhow:: anyhow!( "Failed to download file: HTTP error" ) ) ;
1204+ }
1205+
1206+ let content = String :: from_utf8 ( output. stdout )
1207+ . map_err ( |_| anyhow:: anyhow!( "Downloaded file is not valid UTF-8" ) ) ?;
1208+
1209+ if content. is_empty ( ) {
1210+ return Err ( anyhow:: anyhow!( "Downloaded file is empty" ) ) ;
1211+ }
1212+
1213+ Ok ( content)
1214+ }
10061215}
0 commit comments