Add first-run permission summary dialog before launch
This commit is contained in:
@@ -1765,6 +1765,14 @@ impl Database {
|
|||||||
Ok(())
|
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 ---
|
// --- Launch statistics ---
|
||||||
|
|
||||||
pub fn get_top_launched(&self, limit: i32) -> SqlResult<Vec<(String, u64)>> {
|
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 launch_args_raw = record.launch_args.clone();
|
||||||
let db_launch = db.clone();
|
let db_launch = db.clone();
|
||||||
let toast_launch = toast_overlay.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| {
|
launch_button.connect_clicked(move |btn| {
|
||||||
btn.set_sensitive(false);
|
// Check if first-run permission dialog should be shown
|
||||||
|
if !first_run_prompted {
|
||||||
let btn_ref = btn.clone();
|
let btn_ref = btn.clone();
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
let app_name = app_name_launch.clone();
|
let app_name = app_name_launch.clone();
|
||||||
|
let app_name_dialog = app_name.clone();
|
||||||
let db_launch = db_launch.clone();
|
let db_launch = db_launch.clone();
|
||||||
let toast_ref = toast_launch.clone();
|
let toast_ref = toast_launch.clone();
|
||||||
let launch_args = launcher::parse_launch_args(launch_args_raw.as_deref());
|
let launch_args_raw = launch_args_raw.clone();
|
||||||
glib::spawn_future_local(async move {
|
let db_perm = db_perm.clone();
|
||||||
let path_bg = path.clone();
|
crate::ui::permission_dialog::show_permission_dialog(
|
||||||
let result = gio::spawn_blocking(move || {
|
btn,
|
||||||
let appimage_path = std::path::Path::new(&path_bg);
|
&app_name_dialog,
|
||||||
launcher::launch_appimage(
|
|
||||||
&Database::open().expect("DB open"),
|
|
||||||
record_id,
|
record_id,
|
||||||
appimage_path,
|
&db_perm,
|
||||||
"gui_detail",
|
move || {
|
||||||
&launch_args,
|
do_launch(
|
||||||
&[],
|
&btn_ref, record_id, &path, &app_name,
|
||||||
)
|
launcher::parse_launch_args(launch_args_raw.as_deref()),
|
||||||
}).await;
|
&db_launch, &toast_ref,
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(launcher::LaunchResult::Failed(msg)) => {
|
let launch_args = launcher::parse_launch_args(launch_args_raw.as_deref());
|
||||||
log::error!("Failed to launch: {}", msg);
|
do_launch(
|
||||||
let toast = adw::Toast::builder()
|
btn, record_id, &path, &app_name_launch,
|
||||||
.title(&format!("Could not launch: {}", msg))
|
launch_args, &db_launch, &toast_launch,
|
||||||
.timeout(5)
|
);
|
||||||
.build();
|
|
||||||
toast_ref.add_toast(toast);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
log::error!("Launch task panicked");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
header.pack_end(&launch_button);
|
|
||||||
|
|
||||||
// Check for Update button
|
// Check for Update button
|
||||||
let update_button = gtk::Button::builder()
|
let update_button = gtk::Button::builder()
|
||||||
@@ -2553,3 +2515,82 @@ fn show_uninstall_dialog(
|
|||||||
dialog.present(Some(toast_overlay));
|
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 fuse_wizard;
|
||||||
pub mod integration_dialog;
|
pub mod integration_dialog;
|
||||||
pub mod library_view;
|
pub mod library_view;
|
||||||
|
pub mod permission_dialog;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod security_report;
|
pub mod security_report;
|
||||||
pub mod update_dialog;
|
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