Add first-run permission summary dialog before launch
This commit is contained in:
@@ -1765,6 +1765,14 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_first_run_prompted(&self, id: i64, prompted: bool) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE appimages SET first_run_prompted = ?2 WHERE id = ?1",
|
||||
params![id, prompted as i32],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Launch statistics ---
|
||||
|
||||
pub fn get_top_launched(&self, limit: i32) -> SqlResult<Vec<(String, u64)>> {
|
||||
|
||||
@@ -101,79 +101,41 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
let launch_args_raw = record.launch_args.clone();
|
||||
let db_launch = db.clone();
|
||||
let toast_launch = toast_overlay.clone();
|
||||
let first_run_prompted = record.first_run_prompted;
|
||||
let db_perm = db.clone();
|
||||
launch_button.connect_clicked(move |btn| {
|
||||
btn.set_sensitive(false);
|
||||
let btn_ref = btn.clone();
|
||||
let path = path.clone();
|
||||
let app_name = app_name_launch.clone();
|
||||
let db_launch = db_launch.clone();
|
||||
let toast_ref = toast_launch.clone();
|
||||
// Check if first-run permission dialog should be shown
|
||||
if !first_run_prompted {
|
||||
let btn_ref = btn.clone();
|
||||
let path = path.clone();
|
||||
let app_name = app_name_launch.clone();
|
||||
let app_name_dialog = app_name.clone();
|
||||
let db_launch = db_launch.clone();
|
||||
let toast_ref = toast_launch.clone();
|
||||
let launch_args_raw = launch_args_raw.clone();
|
||||
let db_perm = db_perm.clone();
|
||||
crate::ui::permission_dialog::show_permission_dialog(
|
||||
btn,
|
||||
&app_name_dialog,
|
||||
record_id,
|
||||
&db_perm,
|
||||
move || {
|
||||
do_launch(
|
||||
&btn_ref, record_id, &path, &app_name,
|
||||
launcher::parse_launch_args(launch_args_raw.as_deref()),
|
||||
&db_launch, &toast_ref,
|
||||
);
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let launch_args = launcher::parse_launch_args(launch_args_raw.as_deref());
|
||||
glib::spawn_future_local(async move {
|
||||
let path_bg = path.clone();
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let appimage_path = std::path::Path::new(&path_bg);
|
||||
launcher::launch_appimage(
|
||||
&Database::open().expect("DB open"),
|
||||
record_id,
|
||||
appimage_path,
|
||||
"gui_detail",
|
||||
&launch_args,
|
||||
&[],
|
||||
)
|
||||
}).await;
|
||||
|
||||
btn_ref.set_sensitive(true);
|
||||
match result {
|
||||
Ok(launcher::LaunchResult::Started { pid, method }) => {
|
||||
log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());
|
||||
|
||||
// App survived startup - do Wayland analysis after a delay
|
||||
let db_wayland = db_launch.clone();
|
||||
let path_clone = path.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
glib::timeout_future(std::time::Duration::from_secs(3)).await;
|
||||
let analysis_result = gio::spawn_blocking(move || {
|
||||
wayland::analyze_running_process(pid)
|
||||
}).await;
|
||||
if let Ok(Ok(analysis)) = analysis_result {
|
||||
let status_str = analysis.as_status_str();
|
||||
log::info!(
|
||||
"Runtime Wayland: {} -> {} (pid={}, env: {:?})",
|
||||
path_clone, analysis.status_label(), analysis.pid, analysis.env_vars,
|
||||
);
|
||||
db_wayland.update_runtime_wayland_status(record_id, status_str).ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
|
||||
log::error!("App crashed on launch (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
|
||||
widgets::show_crash_dialog(&btn_ref, &app_name, exit_code, &stderr);
|
||||
if let Some(app) = gtk::gio::Application::default() {
|
||||
notification::send_system_notification(
|
||||
&app,
|
||||
&format!("crash-{}", record_id),
|
||||
&format!("{} crashed", app_name),
|
||||
&stderr.chars().take(200).collect::<String>(),
|
||||
gtk::gio::NotificationPriority::Urgent,
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(launcher::LaunchResult::Failed(msg)) => {
|
||||
log::error!("Failed to launch: {}", msg);
|
||||
let toast = adw::Toast::builder()
|
||||
.title(&format!("Could not launch: {}", msg))
|
||||
.timeout(5)
|
||||
.build();
|
||||
toast_ref.add_toast(toast);
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Launch task panicked");
|
||||
}
|
||||
}
|
||||
});
|
||||
do_launch(
|
||||
btn, record_id, &path, &app_name_launch,
|
||||
launch_args, &db_launch, &toast_launch,
|
||||
);
|
||||
});
|
||||
header.pack_end(&launch_button);
|
||||
|
||||
// Check for Update button
|
||||
let update_button = gtk::Button::builder()
|
||||
@@ -2553,3 +2515,82 @@ fn show_uninstall_dialog(
|
||||
dialog.present(Some(toast_overlay));
|
||||
}
|
||||
|
||||
fn do_launch(
|
||||
btn: >k::Button,
|
||||
record_id: i64,
|
||||
path: &str,
|
||||
app_name: &str,
|
||||
launch_args: Vec<String>,
|
||||
db: &Rc<Database>,
|
||||
toast_overlay: &adw::ToastOverlay,
|
||||
) {
|
||||
btn.set_sensitive(false);
|
||||
let btn_ref = btn.clone();
|
||||
let path = path.to_string();
|
||||
let app_name = app_name.to_string();
|
||||
let db_launch = db.clone();
|
||||
let toast_ref = toast_overlay.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let path_bg = path.clone();
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let appimage_path = std::path::Path::new(&path_bg);
|
||||
launcher::launch_appimage(
|
||||
&Database::open().expect("DB open"),
|
||||
record_id,
|
||||
appimage_path,
|
||||
"gui_detail",
|
||||
&launch_args,
|
||||
&[],
|
||||
)
|
||||
}).await;
|
||||
|
||||
btn_ref.set_sensitive(true);
|
||||
match result {
|
||||
Ok(launcher::LaunchResult::Started { pid, method }) => {
|
||||
log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());
|
||||
|
||||
let db_wayland = db_launch.clone();
|
||||
let path_clone = path.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
glib::timeout_future(std::time::Duration::from_secs(3)).await;
|
||||
let analysis_result = gio::spawn_blocking(move || {
|
||||
wayland::analyze_running_process(pid)
|
||||
}).await;
|
||||
if let Ok(Ok(analysis)) = analysis_result {
|
||||
let status_str = analysis.as_status_str();
|
||||
log::info!(
|
||||
"Runtime Wayland: {} -> {} (pid={}, env: {:?})",
|
||||
path_clone, analysis.status_label(), analysis.pid, analysis.env_vars,
|
||||
);
|
||||
db_wayland.update_runtime_wayland_status(record_id, status_str).ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
|
||||
log::error!("App crashed on launch (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
|
||||
widgets::show_crash_dialog(&btn_ref, &app_name, exit_code, &stderr);
|
||||
if let Some(app) = gtk::gio::Application::default() {
|
||||
notification::send_system_notification(
|
||||
&app,
|
||||
&format!("crash-{}", record_id),
|
||||
&format!("{} crashed", app_name),
|
||||
&stderr.chars().take(200).collect::<String>(),
|
||||
gtk::gio::NotificationPriority::Urgent,
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(launcher::LaunchResult::Failed(msg)) => {
|
||||
log::error!("Failed to launch: {}", msg);
|
||||
let toast = adw::Toast::builder()
|
||||
.title(&format!("Could not launch: {}", msg))
|
||||
.timeout(5)
|
||||
.build();
|
||||
toast_ref.add_toast(toast);
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Launch task panicked");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod duplicate_dialog;
|
||||
pub mod fuse_wizard;
|
||||
pub mod integration_dialog;
|
||||
pub mod library_view;
|
||||
pub mod permission_dialog;
|
||||
pub mod preferences;
|
||||
pub mod security_report;
|
||||
pub mod update_dialog;
|
||||
|
||||
83
src/ui/permission_dialog.rs
Normal file
83
src/ui/permission_dialog.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use adw::prelude::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::core::database::Database;
|
||||
use crate::core::launcher;
|
||||
use crate::i18n::i18n;
|
||||
|
||||
/// Show a first-run permission summary before launching an AppImage.
|
||||
/// Returns a dialog that the caller should present. The `on_proceed` callback
|
||||
/// is called if the user chooses to continue.
|
||||
pub fn show_permission_dialog(
|
||||
parent: &impl IsA<gtk::Widget>,
|
||||
app_name: &str,
|
||||
record_id: i64,
|
||||
db: &Rc<Database>,
|
||||
on_proceed: impl Fn() + 'static,
|
||||
) {
|
||||
let dialog = adw::AlertDialog::builder()
|
||||
.heading(&format!("Launch {}?", app_name))
|
||||
.body(&i18n(
|
||||
"This AppImage will run with your full user permissions. \
|
||||
It can access your files, network, and desktop.",
|
||||
))
|
||||
.build();
|
||||
|
||||
let extra = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(8)
|
||||
.margin_top(12)
|
||||
.build();
|
||||
|
||||
let access_label = gtk::Label::builder()
|
||||
.label(&i18n("This app will have access to:"))
|
||||
.xalign(0.0)
|
||||
.build();
|
||||
access_label.add_css_class("heading");
|
||||
extra.append(&access_label);
|
||||
|
||||
let items = [
|
||||
"Your home directory and files",
|
||||
"Network and internet access",
|
||||
"Display server (Wayland/X11)",
|
||||
"System D-Bus and services",
|
||||
];
|
||||
for item in &items {
|
||||
let label = gtk::Label::builder()
|
||||
.label(&format!(" - {}", i18n(item)))
|
||||
.xalign(0.0)
|
||||
.build();
|
||||
extra.append(&label);
|
||||
}
|
||||
|
||||
// Show firejail option if available
|
||||
if launcher::has_firejail() {
|
||||
let sandbox_note = gtk::Label::builder()
|
||||
.label(&i18n(
|
||||
"Firejail is available on your system. You can configure sandboxing in the app's system tab.",
|
||||
))
|
||||
.wrap(true)
|
||||
.xalign(0.0)
|
||||
.margin_top(8)
|
||||
.build();
|
||||
sandbox_note.add_css_class("dim-label");
|
||||
extra.append(&sandbox_note);
|
||||
}
|
||||
|
||||
dialog.set_extra_child(Some(&extra));
|
||||
|
||||
dialog.add_response("cancel", &i18n("Cancel"));
|
||||
dialog.add_response("proceed", &i18n("Launch"));
|
||||
dialog.set_response_appearance("proceed", adw::ResponseAppearance::Suggested);
|
||||
dialog.set_default_response(Some("proceed"));
|
||||
dialog.set_close_response("cancel");
|
||||
|
||||
let db_ref = db.clone();
|
||||
dialog.connect_response(Some("proceed"), move |_dlg, _response| {
|
||||
// Mark as prompted so we don't show again
|
||||
db_ref.set_first_run_prompted(record_id, true).ok();
|
||||
on_proceed();
|
||||
});
|
||||
|
||||
dialog.present(Some(parent));
|
||||
}
|
||||
Reference in New Issue
Block a user