@@ -2,9 +2,11 @@ use adw::prelude::*;
22use adw:: subclass:: prelude:: * ;
33use gtk:: gio:: File ;
44use gtk:: { gio, glib} ;
5+ use std:: cell:: Cell ;
56use tracing:: error;
67
78use crate :: backends:: { self , CreateArgName , CreateArgs , Error } ;
9+ use crate :: fakers:: Command ;
810use crate :: i18n:: gettext;
911use crate :: models:: Container ;
1012use crate :: root_store:: RootStore ;
@@ -34,8 +36,11 @@ mod imp {
3436 pub dialog : adw:: Dialog ,
3537 pub navigation_view : adw:: NavigationView ,
3638 pub toolbar_view : adw:: ToolbarView ,
39+ pub toast_overlay : adw:: ToastOverlay ,
3740 pub content : gtk:: Box ,
3841 pub name_row : adw:: EntryRow ,
42+ #[ property( get, set) ]
43+ pub url_validated : Cell < bool > ,
3944 pub image_row : adw:: ActionRow ,
4045 pub images_model : gtk:: StringList ,
4146 pub selected_image : RefCell < String > ,
@@ -181,6 +186,7 @@ mod imp {
181186 let home_row = self . obj ( ) . build_file_row (
182187 & gettext ( "Select Home Directory" ) ,
183188 FileRowSelection :: Folder ,
189+ None , // No filter for folders
184190 move |path| {
185191 obj. set_home_folder ( Some ( path. display ( ) . to_string ( ) ) ) ;
186192 } ,
@@ -219,6 +225,7 @@ mod imp {
219225
220226 let create_btn = gtk:: Button :: with_label ( & gettext ( "Create" ) ) ;
221227 create_btn. set_halign ( gtk:: Align :: Center ) ;
228+ create_btn. set_sensitive ( false ) ; // Initially disabled until name is valid
222229
223230 let obj = self . obj ( ) ;
224231 create_btn. connect_clicked ( clone ! (
@@ -247,6 +254,18 @@ mod imp {
247254
248255 self . content . append ( & create_btn) ;
249256
257+ // Add name validation for Create button sensitivity
258+ let guided_create_btn = create_btn. clone ( ) ;
259+ self . name_row . connect_changed ( clone ! (
260+ #[ weak]
261+ guided_create_btn,
262+ move |entry| {
263+ let text = entry. text( ) ;
264+ let is_valid = !text. is_empty( ) && backends:: CreateArgName :: new( & text) . is_ok( ) ;
265+ guided_create_btn. set_sensitive( is_valid) ;
266+ }
267+ ) ) ;
268+
250269 // Prefill wiring: debounce name changes to suggest an image when user hasn't interacted
251270 let obj_for_prefill = self . obj ( ) . clone ( ) ;
252271 let name_row = obj_for_prefill. imp ( ) . name_row . clone ( ) ;
@@ -307,10 +326,15 @@ mod imp {
307326 assemble_group
308327 . set_description ( Some ( & gettext ( "Create a container from an assemble file" ) ) ) ;
309328
329+ let ini_filter = gtk:: FileFilter :: new ( ) ;
330+ ini_filter. set_name ( Some ( & gettext ( "INI Files" ) ) ) ;
331+ ini_filter. add_pattern ( "*.ini" ) ;
332+
310333 let obj = self . obj ( ) . clone ( ) ;
311334 let file_row = self . obj ( ) . build_file_row (
312335 & gettext ( "Select Assemble File" ) ,
313336 FileRowSelection :: File ,
337+ Some ( & ini_filter) ,
314338 move |path| {
315339 obj. set_assemble_file ( Some ( path. display ( ) . to_string ( ) ) ) ;
316340 } ,
@@ -358,7 +382,9 @@ mod imp {
358382
359383 let url_row = adw:: EntryRow :: new ( ) ;
360384 url_row. set_title ( & gettext ( "URL" ) ) ;
361- url_row. set_text ( "https://example.com/container.yaml" ) ;
385+ url_row. set_text ( "https://example.com/container.ini" ) ;
386+ url_row. set_show_apply_button ( true ) ;
387+
362388
363389 url_group. add ( & url_row) ;
364390 url_page. append ( & url_group) ;
@@ -372,12 +398,60 @@ mod imp {
372398 create_btn. set_sensitive ( false ) ;
373399 url_page. append ( & create_btn) ;
374400
375- // Enable button when URL is entered
401+ // Store reference for use in multiple closures
402+ let url_create_btn = create_btn. clone ( ) ;
403+ let obj_for_url = self . obj ( ) . clone ( ) ;
404+
376405 url_row. connect_changed ( clone ! (
377406 #[ weak]
378- obj,
407+ obj_for_url,
408+ #[ weak]
409+ url_create_btn,
379410 move |entry| {
380- obj. set_assemble_url( Some ( entry. text( ) ) ) ;
411+ obj_for_url. set_assemble_url( Some ( entry. text( ) ) ) ;
412+ obj_for_url. set_url_validated( false ) ;
413+ url_create_btn. set_sensitive( false ) ;
414+ // Clear error CSS when user types
415+ entry. remove_css_class( "error" ) ;
416+ }
417+ ) ) ;
418+
419+ url_row. connect_apply ( clone ! (
420+ #[ weak]
421+ obj_for_url,
422+ #[ weak]
423+ url_create_btn,
424+ move |entry| {
425+ let url = entry. text( ) . to_string( ) ;
426+ if url. is_empty( ) {
427+ return ;
428+ }
429+
430+ // Reset validation state
431+ obj_for_url. set_url_validated( false ) ;
432+ url_create_btn. set_sensitive( false ) ;
433+
434+ glib:: MainContext :: ref_thread_default( ) . spawn_local( clone!(
435+ #[ weak]
436+ obj_for_url,
437+ #[ weak]
438+ url_create_btn,
439+ #[ weak]
440+ entry,
441+ async move {
442+ let is_valid = obj_for_url. validate_url( & url) . await ;
443+ obj_for_url. set_url_validated( is_valid) ;
444+ url_create_btn. set_sensitive( is_valid) ;
445+
446+ if !is_valid {
447+ let toast = adw:: Toast :: new( & gettext( "Could not connect to URL" ) ) ;
448+ obj_for_url. imp( ) . toast_overlay. add_toast( toast) ;
449+ entry. add_css_class( "error" ) ;
450+ } else {
451+ entry. remove_css_class( "error" ) ;
452+ }
453+ }
454+ ) ) ;
381455 }
382456 ) ) ;
383457
@@ -393,10 +467,6 @@ mod imp {
393467 }
394468 ) ) ;
395469
396- obj. connect_assemble_url_notify ( move |obj| {
397- create_btn. set_sensitive ( obj. assemble_url ( ) . is_some ( ) ) ;
398- } ) ;
399-
400470 // Add pages to view stack
401471 view_stack. add_titled ( & self . content , Some ( "create" ) , "Guided" ) ;
402472 view_stack. add_titled ( & assemble_page, Some ( "assemble-file" ) , "From File" ) ;
@@ -420,9 +490,12 @@ mod imp {
420490 scrolled_window. set_propagate_natural_height ( true ) ;
421491 scrolled_window. set_child ( Some ( & content_box) ) ;
422492
493+ // Wrap in toast overlay for showing notifications
494+ self . toast_overlay . set_child ( Some ( & scrolled_window) ) ;
495+
423496 toolbar_view. add_top_bar ( & header) ;
424497 toolbar_view. set_vexpand ( true ) ;
425- toolbar_view. set_content ( Some ( & scrolled_window ) ) ;
498+ toolbar_view. set_content ( Some ( & self . toast_overlay ) ) ;
426499
427500 let page = adw:: NavigationPage :: new ( toolbar_view, "Create a Distrobox" ) ;
428501 navigation_view. add ( & page) ;
@@ -489,6 +562,7 @@ impl CreateDistroboxDialog {
489562 & self ,
490563 title : & str ,
491564 selection : FileRowSelection ,
565+ filter : Option < & gtk:: FileFilter > ,
492566 cb : impl Fn ( PathBuf ) + Clone + ' static ,
493567 ) -> adw:: ActionRow {
494568 let row = adw:: ActionRow :: new ( ) ;
@@ -500,6 +574,7 @@ impl CreateDistroboxDialog {
500574 row. add_suffix ( & file_icon) ;
501575
502576 let title = title. to_owned ( ) ;
577+ let filter = filter. cloned ( ) ; // Clone the Option<&FileFilter> to Option<FileFilter>
503578 let dialog_cb = clone ! (
504579 #[ weak( rename_to=this) ]
505580 self ,
@@ -535,6 +610,15 @@ impl CreateDistroboxDialog {
535610 ) ;
536611 row. connect_activated ( move |_| {
537612 let file_dialog = gtk:: FileDialog :: builder ( ) . title ( & title) . modal ( true ) . build ( ) ;
613+
614+ // Apply filter if provided
615+ if let Some ( ref f) = filter {
616+ let filters = gio:: ListStore :: new :: < gtk:: FileFilter > ( ) ;
617+ filters. append ( f) ;
618+ file_dialog. set_filters ( Some ( & filters) ) ;
619+ file_dialog. set_default_filter ( Some ( f) ) ;
620+ }
621+
538622 let dialog_cb = dialog_cb. clone ( ) ;
539623 match selection {
540624 FileRowSelection :: File => {
@@ -859,8 +943,34 @@ impl CreateDistroboxDialog {
859943 Err ( backends:: Error :: InvalidField ( field, msg) ) if field == "name" => {
860944 imp. name_row . add_css_class ( "error" ) ;
861945 imp. name_row . set_tooltip_text ( Some ( msg) ) ;
946+ // Show toast for name validation error
947+ let toast = adw:: Toast :: new ( msg) ;
948+ imp. toast_overlay . add_toast ( toast) ;
949+ }
950+ Err ( backends:: Error :: InvalidField ( _, msg) ) => {
951+ // Show toast for other field validation errors
952+ let toast = adw:: Toast :: new ( msg) ;
953+ imp. toast_overlay . add_toast ( toast) ;
862954 }
863955 _ => { }
864956 }
865957 }
958+
959+ async fn validate_url ( & self , url : & str ) -> bool {
960+ // Use curl with HEAD request to validate URL
961+ // CRITICAL: Use self.root_store().command_runner() for Flatpak compatibility
962+ let command_runner = self . root_store ( ) . command_runner ( ) ;
963+ let mut cmd = Command :: new ( "curl" ) ;
964+ cmd. arg ( "-s" ) ; // Silent
965+ cmd. arg ( "-f" ) ; // Fail on HTTP errors
966+ cmd. arg ( "-I" ) ; // HEAD request only
967+ cmd. arg ( "--connect-timeout" ) ;
968+ cmd. arg ( "5" ) ; // 5 second connection timeout
969+ cmd. arg ( url) ;
970+
971+ match command_runner. output ( cmd) . await {
972+ Ok ( output) => output. status . success ( ) ,
973+ Err ( _) => false ,
974+ }
975+ }
866976}
0 commit comments