Skip to content

Commit 77285ea

Browse files
committed
feat: Add .ini file preview with approval for assembly from URL
- Add ini_content_query Query for downloading .ini file contents - Implement download_ini_file() using curl via CommandRunner - Create expandable preview UI with AdwExpanderRow and TextView - Add security warning InfoBar emphasizing trust requirement - Implement approval CheckButton that gates Create button - Auto-expand preview on successful download - Add re-download button that resets approval state - URL changes clear cache and reset approval - Proper error handling with visual feedback and toasts - Maintain Flatpak compatibility
1 parent 8ea84b0 commit 77285ea

1 file changed

Lines changed: 215 additions & 6 deletions

File tree

src/dialogs/create_distrobox_dialog.rs

Lines changed: 215 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)