🏡 index : quine-global-labs/bazaar.git

/* 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 <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include <glib/gi18n.h>

#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 */