/* bz-bundle-install-dialog.c * * Copyright 2026 Eva M * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include "bz-app-permissions.h" #include "bz-app-size-dialog.h" #include "bz-application-map-factory.h" #include "bz-application.h" #include "bz-async-texture.h" #include "bz-bundle-install-dialog.h" #include "bz-flatpak-repo.h" #include "bz-release.h" #include "bz-safety-dialog.h" #include "bz-state-info.h" #include "context-tile-callbacks.h" #include "env.h" #include "safety-calculator.h" #include "template-callbacks.h" #include "util.h" struct _BzBundleInstallDialog { AdwDialog parent_instance; BzStateInfo *state; BzEntry *entry; BzFlatpakRepo *runtime_repo; AdwNavigationView *nav_view; GtkStack *main_stack; AdwCarousel *carousel; GtkWidget *page_progress; GtkWidget *page_finish; GtkProgressBar *progress_bar; AdwStatusPage *error_status; GtkImage *safety_icon; GtkImage *bundle_image; guint pulse_source_id; }; G_DEFINE_FINAL_TYPE (BzBundleInstallDialog, bz_bundle_install_dialog, ADW_TYPE_DIALOG); enum { PROP_0, PROP_STATE, PROP_ENTRY, PROP_RUNTIME_REPO, LAST_PROP }; static GParamSpec *props[LAST_PROP] = { 0 }; static DexFuture * install_fiber (GWeakRef *wr); static void bz_bundle_install_dialog_dispose (GObject *object) { BzBundleInstallDialog *self = BZ_BUNDLE_INSTALL_DIALOG (object); g_clear_handle_id (&self->pulse_source_id, g_source_remove); g_clear_pointer (&self->state, g_object_unref); g_clear_pointer (&self->entry, g_object_unref); g_clear_pointer (&self->runtime_repo, g_object_unref); G_OBJECT_CLASS (bz_bundle_install_dialog_parent_class)->dispose (object); } static void bz_bundle_install_dialog_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BzBundleInstallDialog *self = BZ_BUNDLE_INSTALL_DIALOG (object); switch (prop_id) { case PROP_STATE: g_value_set_object (value, bz_bundle_install_dialog_get_state (self)); break; case PROP_ENTRY: g_value_set_object (value, bz_bundle_install_dialog_get_entry (self)); break; case PROP_RUNTIME_REPO: g_value_set_object (value, bz_bundle_install_dialog_get_runtime_repo (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bz_bundle_install_dialog_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { BzBundleInstallDialog *self = BZ_BUNDLE_INSTALL_DIALOG (object); switch (prop_id) { case PROP_STATE: bz_bundle_install_dialog_set_state (self, g_value_get_object (value)); break; case PROP_ENTRY: bz_bundle_install_dialog_set_entry (self, g_value_get_object (value)); break; case PROP_RUNTIME_REPO: bz_bundle_install_dialog_set_runtime_repo (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static gboolean pulse_progress_bar (gpointer user_data) { BzBundleInstallDialog *self = BZ_BUNDLE_INSTALL_DIALOG (user_data); gtk_progress_bar_pulse (self->progress_bar); return G_SOURCE_CONTINUE; } static char * get_version (gpointer object, GListModel *version_history) { g_autoptr (BzRelease) release = NULL; if (version_history == NULL || g_list_model_get_n_items (version_history) == 0) return NULL; release = g_list_model_get_item (version_history, 0); return g_strdup (bz_release_get_version (release)); } static char * get_disk_title (gpointer object, guint64 installed_size) { g_autofree char *size_str = NULL; if (installed_size == 0) return g_strdup (_ ("Unknown install size")); size_str = g_format_size (installed_size); return g_strdup_printf (_ ("About %s to install"), size_str); } static char * get_safety_subtitle (gpointer object, BzEntry *entry) { g_autoptr (GListModel) model = NULL; g_autoptr (GString) result = NULL; guint n_items = 0; guint n_picked = 0; guint n_total = 0; if (entry == NULL) return g_strdup (_ ("N/A")); model = bz_safety_calculator_analyze_entry (entry); n_items = g_list_model_get_n_items (model); result = g_string_new (NULL); for (guint i = 0; i < n_items; i++) { g_autoptr (BzSafetyRow) row = g_list_model_get_item (model, i); BzImportance importance = BZ_IMPORTANCE_UNIMPORTANT; g_autofree char *title = NULL; g_object_get (row, "importance", &importance, "title", &title, NULL); if (importance <= BZ_IMPORTANCE_UNIMPORTANT || title == NULL) continue; n_total++; if (n_picked < 2) { if (n_picked > 0) g_string_append (result, ", "); g_string_append (result, title); n_picked++; } } if (n_picked == 0) return g_strdup (_ ("No special permissions")); if (n_total > 2) g_string_append (result, ", …"); return g_string_free (g_steal_pointer (&result), FALSE); } static char * get_safety_icon_name (gpointer object, BzEntry *entry) { BzImportance importance; if (entry == NULL) return g_strdup ("app-safety-unknown-symbolic"); importance = bz_safety_calculator_calculate_rating (entry); switch (importance) { case BZ_IMPORTANCE_UNIMPORTANT: case BZ_IMPORTANCE_NEUTRAL: case BZ_IMPORTANCE_INFORMATION: return g_strdup ("app-safety-ok-symbolic"); case BZ_IMPORTANCE_WARNING: return g_strdup ("permissions-warning-symbolic"); case BZ_IMPORTANCE_IMPORTANT: return g_strdup ("app-safety-unsafe-symbolic"); default: return g_strdup ("app-safety-unknown-symbolic"); } } static char * get_repo_blurb (gpointer object, const char *comment, const char *description) { if (comment != NULL && *comment != '\0') return g_strdup (comment); if (description != NULL && *description != '\0') return g_strdup (description); return g_strdup (""); } static char * format_url (gpointer object, const char *url) { g_autoptr (GUri) uri = NULL; const char *host = NULL; if (url == NULL || (uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL)) == NULL || (host = g_uri_get_host (uri)) == NULL) return g_strdup (url); return g_strdup (host); } static GtkAlign get_header_halign (gpointer object, BzFlatpakRepo *runtime_repo) { return runtime_repo != NULL ? GTK_ALIGN_FILL : GTK_ALIGN_CENTER; } static GtkOrientation get_header_orientation (gpointer object, BzFlatpakRepo *runtime_repo) { return runtime_repo != NULL ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL; } static void install_cb (BzBundleInstallDialog *self, GtkButton *button) { if (self->entry == NULL || self->state == NULL) return; adw_carousel_scroll_to (self->carousel, self->page_progress, TRUE); self->pulse_source_id = g_timeout_add (80, pulse_progress_bar, self); dex_future_disown (dex_scheduler_spawn ( dex_scheduler_get_default (), bz_get_dex_stack_size (), (DexFiberFunc) install_fiber, bz_track_weak (self), bz_weak_release)); } static void run_cb (BzBundleInstallDialog *self, GtkButton *button) { const char *id = NULL; if (self->entry == NULL) return; id = bz_entry_get_id (self->entry); gtk_widget_activate_action (GTK_WIDGET (self), "window.launch-group", "s", id); } static void show_cb (BzBundleInstallDialog *self, GtkButton *button) { const char *id = NULL; if (self->entry == NULL) return; id = bz_entry_get_id (self->entry); adw_dialog_close (ADW_DIALOG (gtk_widget_get_ancestor (GTK_WIDGET (self), ADW_TYPE_DIALOG))); gtk_widget_activate_action (GTK_WIDGET (self), "window.show-group", "(sb)", id, TRUE); } static void permission_cb (AdwActionRow *row, BzBundleInstallDialog *self) { AdwNavigationPage *page = NULL; if (self->entry == NULL) return; page = bz_safety_dialog_page_new (self->entry); adw_navigation_view_push (self->nav_view, page); } static void repo_cb (AdwActionRow *row, BzBundleInstallDialog *self) { if (self->runtime_repo != NULL) gtk_uri_launcher_launch ( gtk_uri_launcher_new (bz_flatpak_repo_get_homepage (self->runtime_repo)), NULL, NULL, NULL, NULL); } static void bz_bundle_install_dialog_class_init (BzBundleInstallDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->set_property = bz_bundle_install_dialog_set_property; object_class->get_property = bz_bundle_install_dialog_get_property; object_class->dispose = bz_bundle_install_dialog_dispose; props[PROP_STATE] = g_param_spec_object ( "state", NULL, NULL, BZ_TYPE_STATE_INFO, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_ENTRY] = g_param_spec_object ( "entry", NULL, NULL, BZ_TYPE_ENTRY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_RUNTIME_REPO] = g_param_spec_object ( "runtime-repo", NULL, NULL, BZ_TYPE_FLATPAK_REPO, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_PROP, props); gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-bundle-install-dialog.ui"); bz_widget_class_bind_all_util_callbacks (widget_class); bz_widget_class_bind_all_context_tile_callbacks (widget_class); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, nav_view); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, main_stack); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, carousel); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, page_progress); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, page_finish); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, progress_bar); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, error_status); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, safety_icon); gtk_widget_class_bind_template_child (widget_class, BzBundleInstallDialog, bundle_image); gtk_widget_class_bind_template_callback (widget_class, install_cb); gtk_widget_class_bind_template_callback (widget_class, run_cb); gtk_widget_class_bind_template_callback (widget_class, show_cb); gtk_widget_class_bind_template_callback (widget_class, repo_cb); gtk_widget_class_bind_template_callback (widget_class, permission_cb); gtk_widget_class_bind_template_callback (widget_class, get_version); gtk_widget_class_bind_template_callback (widget_class, get_disk_title); gtk_widget_class_bind_template_callback (widget_class, get_safety_subtitle); gtk_widget_class_bind_template_callback (widget_class, get_safety_icon_name); gtk_widget_class_bind_template_callback (widget_class, get_repo_blurb); gtk_widget_class_bind_template_callback (widget_class, format_url); gtk_widget_class_bind_template_callback (widget_class, get_header_halign); gtk_widget_class_bind_template_callback (widget_class, get_header_orientation); } static void bz_bundle_install_dialog_init (BzBundleInstallDialog *self) { gtk_widget_init_template (GTK_WIDGET (self)); } static void bz_bundle_install_dialog_update_visible_page (BzBundleInstallDialog *self) { if (self->entry == NULL) return; if (bz_entry_is_installed (self->entry)) gtk_stack_set_visible_child_name (self->main_stack, "installed"); else gtk_stack_set_visible_child_name (self->main_stack, "carousel"); } BzBundleInstallDialog * bz_bundle_install_dialog_new (void) { return g_object_new (BZ_TYPE_BUNDLE_INSTALL_DIALOG, NULL); } BzStateInfo * bz_bundle_install_dialog_get_state (BzBundleInstallDialog *self) { g_return_val_if_fail (BZ_IS_BUNDLE_INSTALL_DIALOG (self), NULL); return self->state; } BzEntry * bz_bundle_install_dialog_get_entry (BzBundleInstallDialog *self) { g_return_val_if_fail (BZ_IS_BUNDLE_INSTALL_DIALOG (self), NULL); return self->entry; } BzFlatpakRepo * bz_bundle_install_dialog_get_runtime_repo (BzBundleInstallDialog *self) { g_return_val_if_fail (BZ_IS_BUNDLE_INSTALL_DIALOG (self), NULL); return self->runtime_repo; } void bz_bundle_install_dialog_set_state (BzBundleInstallDialog *self, BzStateInfo *state) { g_return_if_fail (BZ_IS_BUNDLE_INSTALL_DIALOG (self)); g_return_if_fail (state == NULL || BZ_IS_STATE_INFO (state)); if (state == self->state) return; g_clear_pointer (&self->state, g_object_unref); if (state != NULL) self->state = g_object_ref (state); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]); } void bz_bundle_install_dialog_set_entry (BzBundleInstallDialog *self, BzEntry *entry) { BzImportance importance = 0; const char *style = NULL; g_return_if_fail (BZ_IS_BUNDLE_INSTALL_DIALOG (self)); g_return_if_fail (entry == NULL || BZ_IS_ENTRY (entry)); if (entry == self->entry) return; g_clear_pointer (&self->entry, g_object_unref); if (entry != NULL) self->entry = g_object_ref (entry); bz_bundle_install_dialog_update_visible_page (self); if (self->entry != NULL) { importance = bz_safety_calculator_calculate_rating (self->entry); style = bz_safety_style_for_importance (importance); gtk_widget_add_css_class (GTK_WIDGET (self->safety_icon), style); } g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENTRY]); } void bz_bundle_install_dialog_set_runtime_repo (BzBundleInstallDialog *self, BzFlatpakRepo *runtime_repo) { g_return_if_fail (BZ_IS_BUNDLE_INSTALL_DIALOG (self)); g_return_if_fail (runtime_repo == NULL || BZ_IS_FLATPAK_REPO (runtime_repo)); if (runtime_repo == self->runtime_repo) return; g_set_object (&self->runtime_repo, runtime_repo); if (self->bundle_image != NULL) { const char *icon_url = NULL; icon_url = runtime_repo != NULL ? bz_flatpak_repo_get_icon (runtime_repo) : NULL; if (icon_url != NULL) { g_autoptr (BzAsyncTexture) async_tex = NULL; async_tex = bz_async_texture_new (g_file_new_for_uri (icon_url), NULL); gtk_image_set_from_paintable (self->bundle_image, GDK_PAINTABLE (async_tex)); } else gtk_image_set_from_icon_name (self->bundle_image, "application-x-executable"); } g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RUNTIME_REPO]); } static DexFuture * install_fiber (GWeakRef *wr) { g_autoptr (BzBundleInstallDialog) self = NULL; g_autoptr (GError) local_error = NULL; g_autoptr (BzTransaction) transaction = NULL; g_autoptr (BzTransactionManager) ts_manager = NULL; gboolean success = FALSE; g_autofree char *error_message = NULL; bz_weak_get_or_return_reject (self, wr); if (self->entry == NULL || self->state == NULL) return dex_future_new_false (); transaction = bz_transaction_new_full ( &self->entry, 1, NULL, 0, NULL, 0); ts_manager = g_object_ref (bz_state_info_get_transaction_manager (self->state)); dex_await ( bz_transaction_manager_add ( ts_manager, transaction), &local_error); g_object_get (transaction, "success", &success, "error", &error_message, NULL); g_clear_handle_id (&self->pulse_source_id, g_source_remove); if (local_error != NULL || !success) { const char *description = local_error != NULL ? local_error->message : error_message; adw_status_page_set_description (self->error_status, description); gtk_stack_set_visible_child_name (self->main_stack, "error"); } else adw_carousel_scroll_to (self->carousel, self->page_finish, TRUE); return dex_future_new_true (); } /* End of bz-bundle-install-dialog.c */