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

/* bz-window.c
 *
 * Copyright 2025 Adam Masciola
 *
 * 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
 */

// This file is an utter mess
#include "config.h"

#include <bzvala.h>
#include <glib/gi18n.h>

#include "bz-addons-dialog.h"
#include "bz-application.h"
#include "bz-bulk-install-dialog.h"
#include "bz-curated-view.h"
#include "bz-entry-group-util.h"
#include "bz-entry-group.h"
#include "bz-flathub-page.h"
#include "bz-flatpak-entry.h"
#include "bz-full-view.h"
#include "bz-library-page.h"
#include "bz-progress-bar.h"
#include "bz-screenshot-page.h"
#include "bz-search-bar.h"
#include "bz-search-page.h"
#include "bz-transaction-dialog.h"
#include "bz-transaction-manager.h"
#include "bz-update-history-data-point.h"
#include "bz-update-history-dialog.h"
#include "bz-update-info.h"
#include "bz-user-data-page.h"
#include "bz-window.h"
#include "env.h"
#include "error.h"
#include "hooks.h"
#include "io.h"
#include "template-callbacks.h"
#include "util.h"

struct _BzWindow
{
  AdwApplicationWindow parent_instance;

  BzStateInfo *state;

  GtkEventController *key_controller;

  BzScreenshotPage *screenshot_page;

  gboolean breakpoint_applied;

  /* Template widgets */
  AdwNavigationView *navigation_view;
  BzSearchPage      *search_page;
  BzLibraryPage     *library_page;
  AdwToastOverlay   *toasts;
  AdwViewStack      *main_view_stack;
  GtkStack          *main_stack;
  GtkOverlay        *window_overlay;
  GtkDropTarget     *drop_target;
};

G_DEFINE_FINAL_TYPE (BzWindow, bz_window, ADW_TYPE_APPLICATION_WINDOW)

enum
{
  PROP_0,

  PROP_STATE,
  PROP_COMPACT,

  LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };

BZ_DEFINE_DATA (
    transact,
    Transact,
    {
      GWeakRef     *self;
      BzEntry      *entry;
      BzEntryGroup *group;
      gboolean      remove;
      gboolean      auto_confirm;
      GtkWidget    *source;
    },
    BZ_RELEASE_DATA (self, bz_weak_release);
    BZ_RELEASE_DATA (entry, g_object_unref);
    BZ_RELEASE_DATA (group, g_object_unref);
    BZ_RELEASE_DATA (source, g_object_unref))

static DexFuture *
update_fiber (BzWeakRef  *wr,
              GListModel *entries);

static DexFuture *
transact_fiber (TransactData *data);

BZ_DEFINE_DATA (
    bulk_install,
    BulkInstall,
    {
      GWeakRef   *self;
      GListModel *groups;
    },
    BZ_RELEASE_DATA (self, bz_weak_release);
    BZ_RELEASE_DATA (groups, g_object_unref))

static DexFuture *
bulk_install_fiber (BulkInstallData *data);

static DexFuture *
transact (BzEntry   *entry,
          gboolean   remove,
          GtkWidget *source);

static void
try_transact (BzWindow     *self,
              BzEntry      *entry,
              BzEntryGroup *group,
              gboolean      remove,
              gboolean      auto_confirm,
              GtkWidget    *source);

static void
search (BzWindow   *self,
        const char *text);

static void
bulk_install (BzWindow *self,
              BzEntry **installs,
              guint     n_installs);

static void
set_page (BzWindow *self);

static BzFullView *
create_full_view (BzWindow     *self,
                  BzEntryGroup *group);

static void
emit_hook_disown (BzWindow     *self,
                  BzHookSignal  signal,
                  BzEntryGroup *group);

static void
bz_window_dispose (GObject *object)
{
  BzWindow *self = BZ_WINDOW (object);

  g_clear_object (&self->state);

  G_OBJECT_CLASS (bz_window_parent_class)->dispose (object);
}

static void
bz_window_get_property (GObject    *object,
                        guint       prop_id,
                        GValue     *value,
                        GParamSpec *pspec)
{
  BzWindow *self = BZ_WINDOW (object);

  switch (prop_id)
    {
    case PROP_STATE:
      g_value_set_object (value, self->state);
      break;
    case PROP_COMPACT:
      g_value_set_boolean (value, self->breakpoint_applied);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bz_window_set_property (GObject      *object,
                        guint         prop_id,
                        const GValue *value,
                        GParamSpec   *pspec)
{
  // BzWindow *self = BZ_WINDOW (object);

  switch (prop_id)
    {
    case PROP_STATE:
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static char *
list_length (gpointer    object,
             GListModel *model)
{
  if (model == NULL)
    return g_strdup (0);

  return g_strdup_printf ("%u", g_list_model_get_n_items (model));
}

static void
update_cb (BzWindow   *self,
           GListModel *entries,
           GtkWidget  *widget)
{
  g_autoptr (BzWeakRef) wr = NULL;

  if (g_list_model_get_n_items (entries) == 0)
    return;

  wr = bz_weak_ref_new (self);

  dex_future_disown (dex_scheduler_spawnv (
      dex_scheduler_get_default (), bz_get_dex_stack_size (),
      G_CALLBACK (update_fiber),
      2, BZ_TYPE_WEAK_REF, wr, G_TYPE_OBJECT, entries));
}

static DexFuture *
update_fiber (BzWeakRef  *wr,
              GListModel *entries)
{
  g_autoptr (BzWindow) self              = NULL;
  g_autoptr (BzTransaction) transaction  = NULL;
  g_autoptr (GPtrArray) updates          = NULL;
  g_autoptr (GHashTable) update_ids      = NULL;
  GListModel *available_updates          = NULL;
  g_autoptr (GPtrArray) permission_infos = NULL;
  guint n_updates                        = 0;

  bz_weak_get_or_return_reject (self, &wr->ref);

  n_updates  = g_list_model_get_n_items (entries);
  updates    = g_ptr_array_new_full (n_updates, g_object_unref);
  update_ids = g_hash_table_new (g_str_hash, g_str_equal);
  for (guint i = 0; i < n_updates; i++)
    {
      BzEntry *entry = NULL;

      entry = g_list_model_get_item (entries, i);
      g_ptr_array_add (updates, entry);
      g_hash_table_add (update_ids, (gpointer) bz_entry_get_id (entry));
    }

  available_updates = bz_state_info_get_available_updates (self->state);

  permission_infos = g_ptr_array_new_with_free_func (g_object_unref);
  if (available_updates != NULL)
    {
      guint n_available = 0;

      n_available = g_list_model_get_n_items (available_updates);

      for (guint i = 0; i < n_available; i++)
        {
          g_autoptr (BzUpdatePermissionInfo) info = NULL;
          BzEntry          *info_entry            = NULL;
          const char       *info_id               = NULL;
          BzAppPermissions *delta                 = NULL;

          info       = g_list_model_get_item (available_updates, i);
          info_entry = bz_update_permission_info_get_entry (info);
          info_id    = bz_entry_get_id (info_entry);
          delta      = bz_update_permission_info_get_additional_permissions (info);

          if (delta == NULL || bz_app_permissions_is_empty (delta))
            continue;

          if (g_hash_table_contains (update_ids, info_id))
            g_ptr_array_add (permission_infos, g_object_ref (info));
        }
    }

  if (permission_infos->len > 0)
    {
      gboolean confirmed = FALSE;

      confirmed = dex_await_boolean (
          bz_new_permissions_dialog_present_and_wait (
              GTK_WIDGET (self),
              (BzUpdatePermissionInfo **) permission_infos->pdata,
              permission_infos->len),
          NULL);

      if (!confirmed)
        return dex_future_new_false ();
    }

  transaction = bz_transaction_new_full (NULL, 0, (BzEntry **) updates->pdata,
                                         updates->len, NULL, 0);

  dex_future_disown (bz_transaction_manager_add (
      bz_state_info_get_transaction_manager (self->state),
      transaction));

  if (G_IS_LIST_STORE (available_updates))
    {
      GListStore *store       = NULL;
      guint       n_available = 0;

      store       = G_LIST_STORE (available_updates);
      n_available = g_list_model_get_n_items (available_updates);

      for (guint i = n_available; i > 0; i--)
        {
          g_autoptr (BzUpdatePermissionInfo) available_info = NULL;
          const char *available_id                          = NULL;

          available_info = g_list_model_get_item (available_updates, i - 1);
          available_id   = bz_entry_get_id (bz_update_permission_info_get_entry (available_info));

          if (g_hash_table_contains (update_ids, available_id))
            g_list_store_remove (store, i - 1);
        }
    }

  g_object_notify (G_OBJECT (self->state), "available-updates");
  return dex_future_new_true ();
}

static void
page_toggled_cb (BzWindow       *self,
                 GParamSpec     *pspec,
                 AdwToggleGroup *toggles)
{
  set_page (self);
}

static void
browse_flathub_cb (BzWindow      *self,
                   BzCuratedView *widget)
{
  adw_view_stack_set_visible_child_name (self->main_view_stack, "flathub");
}

static void
open_search_cb (BzWindow     *self,
                BzSearchPage *widget)
{
  adw_view_stack_set_visible_child_name (self->main_view_stack, "search");
}

static void
breakpoint_apply_cb (BzWindow      *self,
                     AdwBreakpoint *breakpoint)
{
  self->breakpoint_applied = TRUE;

  gtk_widget_add_css_class (GTK_WIDGET (self), "narrow");
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COMPACT]);
}

static void
breakpoint_unapply_cb (BzWindow      *self,
                       AdwBreakpoint *breakpoint)
{
  self->breakpoint_applied = FALSE;

  gtk_widget_remove_css_class (GTK_WIDGET (self), "narrow");
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COMPACT]);
}

static void
sync_cb (BzWindow  *self,
         GtkButton *button)
{
  g_action_group_activate_action (
      G_ACTION_GROUP (g_application_get_default ()),
      "sync-remotes", NULL);
}

static void
transactions_clear_cb (BzWindow  *self,
                       GtkButton *button)
{
  bz_transaction_manager_clear_finished (
      bz_state_info_get_transaction_manager (self->state));
}

static void
action_escape (GtkWidget  *widget,
               const char *action_name,
               GVariant   *parameter)
{
  BzWindow   *self    = BZ_WINDOW (widget);
  GListModel *stack   = NULL;
  guint       n_pages = 0;

  if (adw_application_window_get_visible_dialog (ADW_APPLICATION_WINDOW (self)) != NULL)
    return;

  if (self->screenshot_page != NULL)
    {
      if (!bz_screenshot_page_is_closing (self->screenshot_page))
        {
          bz_screenshot_page_close (self->screenshot_page);
          return;
        }
    }

  stack   = adw_navigation_view_get_navigation_stack (self->navigation_view);
  n_pages = g_list_model_get_n_items (stack);

  adw_navigation_view_pop (self->navigation_view);
  if (n_pages <= 2)
    set_page (self);
}

static char *
format_progress (gpointer object,
                 double   value)
{
  return g_strdup_printf ("%.0f%%", 100.0 * value);
}

static char *
format_title (gpointer    object,
              const char *title)
{
  // Translators: Please treat "Bazaar" as a brand name and avoid translating.
  if (title == NULL || *title == '\0' || g_strcmp0 (title, _ ("Bazaar")) == 0)
    return g_strdup (_ ("Bazaar"));
  /* Translators: Please treat "Bazaar" as a brand name and avoid translating. %s is the title of the current page. */
  return g_strdup_printf (_ ("Bazaar — %s"), title);
}

static gboolean
drop_accept_cb (BzWindow      *self,
                GdkDrop       *drop,
                GtkDropTarget *target)
{
  return gdk_content_formats_contain_gtype (gdk_drop_get_formats (drop), G_TYPE_FILE) &&
         !bz_state_info_get_busy (self->state) &&
         self->screenshot_page == NULL &&
         adw_application_window_get_visible_dialog (ADW_APPLICATION_WINDOW (self)) == NULL;
}

static gboolean
drop_cb (BzWindow      *self,
         const GValue  *value,
         double         x,
         double         y,
         GtkDropTarget *target)
{
  GFile           *file = NULL;
  g_autofree char *uri  = NULL;

  file = G_FILE (g_value_get_object (value));
  uri  = g_file_get_uri (file);

  gtk_widget_activate_action (GTK_WIDGET (self), "app.open-location", "s", uri);

  return TRUE;
}

static BzEntryGroup *
resolve_group_from_parameter (BzWindow *self,
                              GVariant *parameter,
                              gboolean *auto_confirm)
{
  const char *id = NULL;

  g_variant_get (parameter, "(&sb)", &id, auto_confirm);

  return bz_application_map_factory_convert_one (
      bz_state_info_get_application_factory (self->state),
      gtk_string_object_new (id));
}

static void
action_install_group (GtkWidget  *widget,
                      const char *action_name,
                      GVariant   *parameter)
{
  BzWindow *self                 = BZ_WINDOW (widget);
  g_autoptr (BzEntryGroup) group = NULL;
  gboolean auto_confirm          = FALSE;

  group = resolve_group_from_parameter (self, parameter, &auto_confirm);
  if (group != NULL)
    try_transact (self, NULL, group, FALSE, auto_confirm, NULL);
}

static void
action_remove_group (GtkWidget  *widget,
                     const char *action_name,
                     GVariant   *parameter)
{
  BzWindow *self                 = BZ_WINDOW (widget);
  g_autoptr (BzEntryGroup) group = NULL;
  gboolean auto_confirm          = FALSE;

  group = resolve_group_from_parameter (self, parameter, &auto_confirm);
  if (group != NULL)
    try_transact (self, NULL, group, TRUE, auto_confirm, NULL);
}

static void
action_cancel_group (GtkWidget  *widget,
                     const char *action_name,
                     GVariant   *parameter)
{
  BzWindow             *self     = BZ_WINDOW (widget);
  const char           *id       = NULL;
  BzTransactionManager *manager  = NULL;
  BzBackend            *backend  = NULL;
  GListModel           *trackers = NULL;
  guint                 n_items  = 0;

  id      = g_variant_get_string (parameter, NULL);
  manager = bz_state_info_get_transaction_manager (self->state);
  if (manager == NULL)
    return;

  backend = bz_state_info_get_backend (self->state);
  if (backend == NULL)
    return;

  trackers = bz_transaction_manager_get_all_trackers (manager);
  n_items  = g_list_model_get_n_items (trackers);

  for (guint i = 0; i < n_items; i++)
    {
      g_autoptr (BzTransactionEntryTracker) tracker = NULL;
      BzEntry    *entry                             = NULL;
      const char *entry_id                          = NULL;

      tracker = g_list_model_get_item (trackers, i);
      entry   = bz_transaction_entry_tracker_get_entry (tracker);
      if (entry == NULL)
        continue;

      entry_id = bz_entry_get_id (entry);
      if (g_strcmp0 (entry_id, id) == 0)
        {
          if (bz_backend_cancel_task_for_entry (backend, entry))
            g_object_set (tracker, "status", BZ_TRANSACTION_ENTRY_STATUS_CANCELLED, NULL);
          break;
        }
    }
}

static void
parent_ui_entry_show_group_cb (BzAddonsDialog *dialog,
                               GParamSpec     *pspec,
                               BzWindow       *self)
{
  BzEntry *entry                 = NULL;
  g_autoptr (BzEntryGroup) group = NULL;

  entry = bz_addons_dialog_get_parent_entry (dialog);
  if (entry == NULL)
    return;

  group = bz_application_map_factory_convert_one (
      bz_state_info_get_application_factory (bz_window_get_state_info (self)),
      gtk_string_object_new (bz_entry_get_id (entry)));
  if (group == NULL)
    return;

  bz_window_show_group (self, group);
}

static void
action_show_group (GtkWidget  *widget,
                   const char *action_name,
                   GVariant   *parameter)
{
  BzWindow   *self               = BZ_WINDOW (widget);
  const char *id                 = NULL;
  gboolean    animate            = FALSE;
  g_autoptr (BzEntryGroup) group = NULL;

  g_variant_get (parameter, "(&sb)", &id, &animate);

  group = bz_application_map_factory_convert_one (
      bz_state_info_get_application_factory (self->state),
      gtk_string_object_new (id));

  if (group == NULL)
    return;

  if (bz_entry_group_is_addon (group))
    {
      AdwDialog *dialog = NULL;

      dialog = bz_addons_dialog_new_single (group);
      g_signal_connect_object (dialog, "notify::parent-ui-entry",
                               G_CALLBACK (parent_ui_entry_show_group_cb),
                               self, 0);
      parent_ui_entry_show_group_cb (BZ_ADDONS_DIALOG (dialog), NULL, self);
      adw_dialog_present (dialog, GTK_WIDGET (self));
    }
  else
    {
      adw_navigation_view_set_animate_transitions (self->navigation_view, animate);
      bz_window_show_group (self, group);
      adw_navigation_view_set_animate_transitions (self->navigation_view, TRUE);
    }
}

static void
action_addons_group (GtkWidget  *widget,
                     const char *action_name,
                     GVariant   *parameter)
{
  BzWindow   *self               = BZ_WINDOW (widget);
  const char *id                 = NULL;
  g_autoptr (BzEntryGroup) group = NULL;
  AdwDialog *addons_dialog       = NULL;

  id    = g_variant_get_string (parameter, NULL);
  group = bz_application_map_factory_convert_one (
      bz_state_info_get_application_factory (self->state),
      gtk_string_object_new (id));

  if (group == NULL)
    return;

  addons_dialog = bz_addons_dialog_new (group);
  adw_dialog_present (addons_dialog, GTK_WIDGET (self));
}

static void
action_bulk_install (GtkWidget  *widget,
                     const char *action_name,
                     GVariant   *parameter)
{
  BzWindow *self                = BZ_WINDOW (widget);
  g_autoptr (GListStore) ids    = NULL;
  g_autoptr (GListModel) groups = NULL;
  GVariantIter iter             = { 0 };
  const char  *id               = NULL;

  ids = g_list_store_new (GTK_TYPE_STRING_OBJECT);

  g_variant_iter_init (&iter, parameter);
  while (g_variant_iter_next (&iter, "&s", &id))
    {
      g_autoptr (GtkStringObject) string = gtk_string_object_new (id);
      g_list_store_append (ids, string);
    }

  groups = bz_application_map_factory_generate (
      bz_state_info_get_application_factory (self->state),
      G_LIST_MODEL (ids));

  if (groups != NULL && g_list_model_get_n_items (groups) > 0)
    bz_window_bulk_install (self, groups);
}

static void
action_user_data (GtkWidget  *widget,
                  const char *action_name,
                  GVariant   *parameter)
{
  BzWindow          *self           = BZ_WINDOW (widget);
  AdwNavigationPage *user_data_page = NULL;

  user_data_page = ADW_NAVIGATION_PAGE (bz_user_data_page_new (self->state));
  adw_navigation_view_push (self->navigation_view, user_data_page);
}

static void
action_open_flathub_page (GtkWidget  *widget,
                          const char *action_name,
                          GVariant   *parameter)
{
  BzWindow *self = BZ_WINDOW (widget);

  adw_navigation_view_pop_to_tag (self->navigation_view, "main");
  adw_view_stack_set_visible_child_name (self->main_view_stack, "flathub");
}

static void
action_open_library (GtkWidget  *widget,
                     const char *action_name,
                     GVariant   *parameter)
{
  BzWindow *self = BZ_WINDOW (widget);

  adw_navigation_view_pop_to_tag (self->navigation_view, "main");
  adw_view_stack_set_visible_child_name (self->main_view_stack, "installed");
  bz_library_page_reset_search (self->library_page);
}

static DexFuture *
launch_group_fiber (BzEntryGroup *group)
{
  g_autoptr (GError) local_error = NULL;
  g_autoptr (GListStore) store   = NULL;
  GtkWidget   *window            = NULL;
  BzStateInfo *state             = NULL;

  state  = bz_state_info_get_default ();
  window = GTK_WIDGET (gtk_application_get_active_window (
      GTK_APPLICATION (g_application_get_default ())));

  store = dex_await_object (
      bz_entry_group_dup_all_into_store (group), &local_error);
  if (store == NULL)
    {
      if (window != NULL)
        bz_show_error_for_widget (window, _ ("Failed to launch application"), local_error->message);
      return dex_future_new_for_error (g_steal_pointer (&local_error));
    }

  for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (store)); i++)
    {
      g_autoptr (BzEntry) entry = NULL;
      const char *ref           = NULL;
      gboolean    result        = FALSE;

      entry = g_list_model_get_item (G_LIST_MODEL (store), i);

      if (!BZ_IS_FLATPAK_ENTRY (entry) || !bz_entry_is_installed (entry))
        continue;

      {
        const char *remote_repo_name = NULL;

        remote_repo_name = bz_entry_get_remote_repo_name (entry);
        if (g_strcmp0 (remote_repo_name, COSMIC_APPLET_REMOTE_NAME) == 0)
          {
            adw_dialog_present (ADW_DIALOG (bz_cosmic_place_applet_dialog_new ()), window);
            return dex_future_new_false ();
          }
      }

      ref = bz_flatpak_entry_get_addon_extension_of_ref (BZ_FLATPAK_ENTRY (entry));
      if (ref != NULL)
        {
          g_auto (GStrv) parts             = NULL;
          BzApplicationMapFactory *factory = NULL;
          g_autoptr (BzEntryGroup) parent  = NULL;

          parts = g_strsplit (ref, "/", -1);
          if (parts[0] != NULL && parts[1] != NULL)
            {
              factory = bz_state_info_get_application_factory (state);
              parent  = bz_application_map_factory_convert_one (
                  factory, gtk_string_object_new (parts[1]));
              if (parent != NULL)
                return launch_group_fiber (parent);
            }
        }

      result = bz_flatpak_entry_launch (
          BZ_FLATPAK_ENTRY (entry),
          BZ_FLATPAK_INSTANCE (bz_state_info_get_backend (state)),
          &local_error);
      if (!result && window != NULL)
        bz_show_error_for_widget (window, _ ("Failed to launch application"), local_error->message);
      return result ? dex_future_new_true () : dex_future_new_for_error (g_steal_pointer (&local_error));
    }

  return dex_future_new_false ();
}

static void
action_launch_group (GtkWidget  *widget,
                     const char *action_name,
                     GVariant   *parameter)
{
  BzWindow   *self               = BZ_WINDOW (widget);
  const char *id                 = NULL;
  g_autoptr (BzEntryGroup) group = NULL;

  id    = g_variant_get_string (parameter, NULL);
  group = bz_application_map_factory_convert_one (
      bz_state_info_get_application_factory (self->state),
      gtk_string_object_new (id));

  if (group == NULL)
    return;

  dex_future_disown (dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) launch_group_fiber,
      g_object_ref (group),
      g_object_unref));
}

static void
action_activate_hook_app (GtkWidget  *widget,
                          const char *action_name,
                          GVariant   *parameter)
{
  BzWindow   *self               = BZ_WINDOW (widget);
  const char *id                 = NULL;
  g_autoptr (BzEntryGroup) group = NULL;

  id    = g_variant_get_string (parameter, NULL);
  group = bz_entry_group_new_manual (id, NULL, NULL);

  emit_hook_disown (self, BZ_HOOK_SIGNAL_ARTICLE_APP, group);
}

static void
action_open_user_data_folder (GtkWidget  *widget,
                              const char *action_name,
                              GVariant   *parameter)
{
  const char      *id                  = NULL;
  g_autofree char *path                = NULL;
  g_autoptr (GFile) file               = NULL;
  g_autoptr (GtkFileLauncher) launcher = NULL;

  id = g_variant_get_string (parameter, NULL);
  if (id == NULL)
    return;

  path     = bz_dup_user_data_path (id);
  file     = g_file_new_for_path (path);
  launcher = gtk_file_launcher_new (file);

  gtk_file_launcher_launch (launcher, GTK_WINDOW (widget), NULL, NULL, NULL);
}

static void
open_uri_finish_cb (GObject      *source,
                    GAsyncResult *result,
                    gpointer      user_data)
{
  g_autoptr (BzWindow) self      = BZ_WINDOW (user_data);
  GtkUriLauncher *launcher       = GTK_URI_LAUNCHER (source);
  g_autoptr (GError) local_error = NULL;

  if (!gtk_uri_launcher_launch_finish (launcher, result, &local_error) &&
      !g_error_matches (local_error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
    bz_show_error_for_widget (GTK_WIDGET (self), _ ("Could not open link"), local_error->message);
}

static void
action_open_uri (GtkWidget  *widget,
                 const char *action_name,
                 GVariant   *parameter)
{
  BzWindow   *self                    = BZ_WINDOW (widget);
  const char *uri                     = g_variant_get_string (parameter, NULL);
  g_autoptr (GtkUriLauncher) launcher = NULL;

  if (uri == NULL || *uri == '\0')
    return;

  launcher = gtk_uri_launcher_new (uri);
  gtk_uri_launcher_launch (launcher, GTK_WINDOW (self), NULL, open_uri_finish_cb, g_object_ref (self));
}

static void
bz_window_class_init (BzWindowClass *klass)
{
  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->dispose      = bz_window_dispose;
  object_class->get_property = bz_window_get_property;
  object_class->set_property = bz_window_set_property;

  props[PROP_STATE] =
      g_param_spec_object (
          "state",
          NULL, NULL,
          BZ_TYPE_STATE_INFO,
          G_PARAM_READABLE);

  props[PROP_COMPACT] =
      g_param_spec_boolean (
          "compact",
          NULL, NULL, FALSE,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, LAST_PROP, props);

  g_type_ensure (BZ_TYPE_SEARCH_BAR);
  g_type_ensure (BZ_TYPE_SEARCH_PAGE);
  g_type_ensure (BZ_TYPE_PROGRESS_BAR);
  g_type_ensure (BZ_TYPE_CURATED_VIEW);
  g_type_ensure (BZ_TYPE_FULL_VIEW);
  g_type_ensure (BZ_TYPE_ENTRY);
  g_type_ensure (BZ_TYPE_LIBRARY_PAGE);
  g_type_ensure (BZ_TYPE_FLATHUB_PAGE);

  gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-window.ui");
  bz_widget_class_bind_all_util_callbacks (widget_class);

  gtk_widget_class_bind_template_child (widget_class, BzWindow, navigation_view);
  gtk_widget_class_bind_template_child (widget_class, BzWindow, toasts);
  gtk_widget_class_bind_template_child (widget_class, BzWindow, search_page);
  gtk_widget_class_bind_template_child (widget_class, BzWindow, library_page);
  gtk_widget_class_bind_template_child (widget_class, BzWindow, main_view_stack);
  gtk_widget_class_bind_template_child (widget_class, BzWindow, main_stack);
  gtk_widget_class_bind_template_child (widget_class, BzWindow, window_overlay);
  gtk_widget_class_bind_template_child (widget_class, BzWindow, drop_target);
  gtk_widget_class_bind_template_callback (widget_class, list_length);
  gtk_widget_class_bind_template_callback (widget_class, update_cb);
  gtk_widget_class_bind_template_callback (widget_class, page_toggled_cb);
  gtk_widget_class_bind_template_callback (widget_class, breakpoint_apply_cb);
  gtk_widget_class_bind_template_callback (widget_class, breakpoint_unapply_cb);
  gtk_widget_class_bind_template_callback (widget_class, sync_cb);
  gtk_widget_class_bind_template_callback (widget_class, transactions_clear_cb);
  gtk_widget_class_bind_template_callback (widget_class, browse_flathub_cb);
  gtk_widget_class_bind_template_callback (widget_class, open_search_cb);
  gtk_widget_class_bind_template_callback (widget_class, format_progress);
  gtk_widget_class_bind_template_callback (widget_class, format_title);
  gtk_widget_class_bind_template_callback (widget_class, drop_accept_cb);
  gtk_widget_class_bind_template_callback (widget_class, drop_cb);

  gtk_widget_class_install_action (widget_class, "escape", NULL, action_escape);
  gtk_widget_class_install_action (widget_class, "window.user-data", NULL, action_user_data);
  gtk_widget_class_install_action (widget_class, "window.open-library", NULL, action_open_library);
  gtk_widget_class_install_action (widget_class, "window.open-flathub-page", NULL, action_open_flathub_page);

  gtk_widget_class_install_action (widget_class, "window.install-group", "(sb)", action_install_group);
  gtk_widget_class_install_action (widget_class, "window.remove-group", "(sb)", action_remove_group);
  gtk_widget_class_install_action (widget_class, "window.cancel-group", "s", action_cancel_group);
  gtk_widget_class_install_action (widget_class, "window.show-group", "(sb)", action_show_group);
  gtk_widget_class_install_action (widget_class, "window.addons-group", "s", action_addons_group);
  gtk_widget_class_install_action (widget_class, "window.bulk-install", NULL, action_bulk_install);
  gtk_widget_class_install_action (widget_class, "window.launch-group", "s", action_launch_group);
  gtk_widget_class_install_action (widget_class, "window.activate-hook-app", "s", action_activate_hook_app);
  gtk_widget_class_install_action (widget_class, "window.open-user-data-folder", "s", action_open_user_data_folder);
  gtk_widget_class_install_action (widget_class, "window.open-uri", "s", action_open_uri);

  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_d, GDK_CONTROL_MASK, "window.open-library", NULL);
  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_w, GDK_CONTROL_MASK, "window.close", NULL);
  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_e, GDK_CONTROL_MASK, "window.open-flathub-page", NULL);
}

static gboolean
key_pressed (BzWindow              *self,
             guint                  keyval,
             guint                  keycode,
             GdkModifierType        state,
             GtkEventControllerKey *controller)
{
  gunichar    unichar            = 0;
  char        buf[32]            = { 0 };
  const char *visible_child_name = NULL;
  gboolean    was_deeper         = FALSE;

  /* Ignore if this is a modifier-shortcut of some sort */
  if (state & ~(GDK_NO_MODIFIER_MASK | GDK_SHIFT_MASK))
    return FALSE;

  if (self->screenshot_page != NULL)
    return FALSE;

  unichar = gdk_keyval_to_unicode (keyval);
  if (unichar == 0 || !g_unichar_isgraph (unichar))
    return FALSE;
  g_unichar_to_utf8 (unichar, buf);

  was_deeper = g_list_model_get_n_items (
                   adw_navigation_view_get_navigation_stack (self->navigation_view)) > 1;

  adw_navigation_view_pop_to_tag (self->navigation_view, "main");

  visible_child_name = adw_view_stack_get_visible_child_name (self->main_view_stack);
  if (!was_deeper && g_strcmp0 (visible_child_name, "installed") == 0)
    return bz_library_page_ensure_active (self->library_page, buf);
  else
    {
      adw_view_stack_set_visible_child_name (self->main_view_stack, "search");
      return bz_search_page_ensure_active (self->search_page, buf);
    }
}

static void
bz_window_init (BzWindow *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));

#ifdef DEVELOPMENT_BUILD
  gtk_widget_add_css_class (GTK_WIDGET (self), "devel");
#endif

  adw_view_stack_set_visible_child_name (self->main_view_stack, "flathub");

  self->key_controller = gtk_event_controller_key_new ();
  g_signal_connect_swapped (self->key_controller,
                            "key-pressed",
                            G_CALLBACK (key_pressed),
                            self);
  gtk_widget_add_controller (GTK_WIDGET (self), self->key_controller);
  gtk_drop_target_set_gtypes (self->drop_target, (GType[]){ G_TYPE_FILE }, 1);
}

static void
app_busy_changed (BzWindow    *self,
                  GParamSpec  *pspec,
                  BzStateInfo *info)
{
  bz_search_page_refresh (self->search_page);
  set_page (self);
}

static void
has_inputs_changed (BzWindow          *self,
                    GParamSpec        *pspec,
                    BzContentProvider *provider)
{
  if (!bz_content_provider_get_has_inputs (provider) &&
      g_strcmp0 (adw_view_stack_get_visible_child_name (self->main_view_stack), "browse") == 0)
    adw_view_stack_set_visible_child_name (self->main_view_stack, "flathub");
}

static DexFuture *
transact_fiber (TransactData *data)
{
  g_autoptr (GError) local_error        = NULL;
  g_autoptr (BzEntry) selected_entry    = NULL;
  g_autoptr (DexFuture) transact_future = NULL;
  g_autofree char *id_dup               = NULL;
  BzMainConfig    *config               = NULL;
  GListModel      *hooks                = NULL;
  gboolean         delete_user_data     = FALSE;
  GdkDisplay      *display              = NULL;
  GdkSeat         *seat                 = NULL;
  GdkDevice       *keyboard             = NULL;
  GdkModifierType  modifiers            = GDK_NO_MODIFIER_MASK;

  // Get ID early before any async operations
  if (data->group != NULL)
    id_dup = g_strdup (bz_entry_group_get_id (data->group));
  else
    id_dup = g_strdup (bz_entry_get_id (data->entry));

  /* Prevent Bazaar from being removed by itself */
  if (data->remove)
    {
      const char *bazaar_id = NULL;

      bazaar_id = g_application_get_application_id (g_application_get_default ());
      if (g_strcmp0 (id_dup, bazaar_id) == 0)
        {
          GtkWidget *window = NULL;
          window            = GTK_WIDGET (gtk_application_get_active_window (GTK_APPLICATION (g_application_get_default ())));
          // Translators: Please treat "Bazaar" as a brand name and avoid translating.
          bz_show_error_for_widget (window, _ ("You can't remove Bazaar from Bazaar!"), _ ("You can't remove Bazaar from Bazaar!"));
          return dex_future_new_false ();
        }
    }

  config = bz_state_info_get_main_config (bz_state_info_get_default ());
  if (config != NULL)
    hooks = bz_main_config_get_hooks (config);

#define RUN_HOOK(_signal)                                               \
  G_STMT_START                                                          \
  {                                                                     \
    if (hooks != NULL &&                                                \
        !dex_await (                                                    \
            bz_run_hook_emission (                                      \
                hooks, (_signal),                                       \
                data->remove                                            \
                    ? BZ_HOOK_TRANSACTION_TYPE_REMOVAL                  \
                    : BZ_HOOK_TRANSACTION_TYPE_INSTALL,                 \
                id_dup, NULL),                                          \
            &local_error))                                              \
      return dex_future_new_for_error (g_steal_pointer (&local_error)); \
  }                                                                     \
  G_STMT_END

  RUN_HOOK (BZ_HOOK_SIGNAL_BEFORE_TRANSACTION);

  display  = gdk_display_get_default ();
  seat     = gdk_display_get_default_seat (display);
  keyboard = gdk_seat_get_keyboard (seat);
  if (keyboard != NULL)
    modifiers = gdk_device_get_modifier_state (keyboard);

  if (modifiers & GDK_SHIFT_MASK)
    /* Holding shift while invoking a transaction skips the dialog and assumes
       the first valid entry */
    {
      if (data->group != NULL)
        {
          g_autoptr (GListModel) store = NULL;
          guint n_items                = 0;

          store = dex_await_object (
              bz_entry_group_dup_all_into_store (data->group),
              &local_error);
          if (store == NULL)
            return dex_future_new_for_error (g_steal_pointer (&local_error));

          n_items = g_list_model_get_n_items (store);
          for (guint i = 0; i < n_items; i++)
            {
              g_autoptr (BzEntry) entry = NULL;
              gboolean installed        = FALSE;

              entry     = g_list_model_get_item (store, i);
              installed = bz_entry_is_installed (entry);
              if ((data->remove && installed) ||
                  (!data->remove && !installed))
                {
                  selected_entry = g_steal_pointer (&entry);
                  break;
                }
            }
          if (selected_entry == NULL)
            return dex_future_new_false ();
        }
      else
        selected_entry = g_object_ref (data->entry);
    }
  else
    {
      g_autoptr (BzTransactionDialogResult) dialog_result = NULL;

      // Show the dialog
      dialog_result = dex_await_object (
          bz_transaction_dialog_show (
              GTK_WIDGET (gtk_application_get_active_window (
                  GTK_APPLICATION (g_application_get_default ()))),
              data->entry,
              data->group,
              data->remove,
              data->auto_confirm),
          &local_error);

      if (dialog_result == NULL)
        return dex_future_new_for_error (g_steal_pointer (&local_error));
      if (!bz_transaction_dialog_result_get_confirmed (dialog_result))
        return dex_future_new_false ();

      selected_entry = g_object_ref (
          bz_transaction_dialog_result_get_selected_entry (dialog_result));
      delete_user_data = bz_transaction_dialog_result_get_delete_user_data (
          dialog_result);
    }

  // Perform the transaction
  transact_future = transact (
      selected_entry,
      data->remove,
      data->source);

  if (!dex_await (g_steal_pointer (&transact_future), &local_error))
    return dex_future_new_for_error (g_steal_pointer (&local_error));

  // Handle user data deletion
  if (delete_user_data)
    {
      if (data->group != NULL)
        bz_entry_group_reap_user_data (data->group);
      else
        dex_future_disown (bz_reap_user_data_dex (id_dup));
    }

  RUN_HOOK (BZ_HOOK_SIGNAL_AFTER_TRANSACTION);
#undef RUN_HOOK

  return dex_future_new_true ();
}

BzWindow *
bz_window_new (BzStateInfo *state)
{
  BzWindow     *window = NULL;
  BzMainConfig *config = NULL;

  g_return_val_if_fail (BZ_IS_STATE_INFO (state), NULL);

  window        = g_object_new (BZ_TYPE_WINDOW, NULL);
  window->state = g_object_ref (state);

  config = bz_state_info_get_main_config (state);
  if (config != NULL && bz_main_config_get_start_on_curated (config))
    {
      BzContentProvider *curated_provider = NULL;

      curated_provider = bz_state_info_get_curated_provider (state);
      if (curated_provider != NULL &&
          g_list_model_get_n_items (G_LIST_MODEL (curated_provider)) > 0)
        adw_view_stack_set_visible_child_name (window->main_view_stack, "browse");
    }

  g_signal_connect_object (state,
                           "notify::busy",
                           G_CALLBACK (app_busy_changed),
                           window, G_CONNECT_SWAPPED);

  /* these seem unsafe but BzApplication never
   * changes the objects we are connecting to
   */
  g_signal_connect_object (bz_state_info_get_curated_provider (state),
                           "notify::has-inputs",
                           G_CALLBACK (has_inputs_changed),
                           window, G_CONNECT_SWAPPED);

  g_object_notify_by_pspec (G_OBJECT (window), props[PROP_STATE]);

  set_page (window);
  return window;
}

void
bz_window_search (BzWindow   *self,
                  const char *text)
{
  g_return_if_fail (BZ_IS_WINDOW (self));
  search (self, text);
}

void
bz_window_show_entry (BzWindow *self,
                      BzEntry  *entry)
{
  g_autoptr (BzEntryGroup) group = NULL;

  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (BZ_IS_ENTRY (entry));

  group = bz_entry_group_new_for_single_entry (entry);
  bz_window_show_group (self, group);
}

void
bz_window_install_entry (BzWindow *self,
                         BzEntry  *entry)
{
  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (BZ_IS_ENTRY (entry));

  try_transact (self, entry, NULL, FALSE, FALSE, NULL);
}

static void
full_view_page_hidden_cb (AdwNavigationPage *page,
                          BzWindow          *self)
{
  adw_navigation_page_set_child (page, NULL);
}

static void
full_view_page_shown_cb (AdwNavigationPage *page,
                         BzWindow          *self)
{
  BzEntryGroup *group     = NULL;
  GtkWidget    *existing  = NULL;
  BzFullView   *full_view = NULL;

  existing = adw_navigation_page_get_child (page);
  if (BZ_IS_FULL_VIEW (existing))
    return;

  group = BZ_ENTRY_GROUP (g_object_get_data (G_OBJECT (page), "bz-entry-group"));
  if (group == NULL)
    return;

  full_view = create_full_view (self, group);
  adw_navigation_page_set_child (page, GTK_WIDGET (full_view));
}

void
bz_window_show_group (BzWindow     *self,
                      BzEntryGroup *group)
{
  BzFullView        *full_view = NULL;
  AdwNavigationPage *page      = NULL;
  const char        *title     = NULL;

  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (BZ_IS_ENTRY_GROUP (group));

  full_view = create_full_view (self, group);

  title = bz_entry_group_get_title (group);
  page  = adw_navigation_page_new (GTK_WIDGET (full_view), title != NULL ? title : "");

  g_object_set_data_full (
      G_OBJECT (page), "bz-entry-group",
      g_object_ref (group), g_object_unref);

  g_signal_connect_object (page, "showing", G_CALLBACK (full_view_page_shown_cb), self, 0);
  g_signal_connect_object (page, "hidden", G_CALLBACK (full_view_page_hidden_cb), self, 0);

  adw_navigation_view_push (self->navigation_view, page);
}

void
bz_window_add_toast (BzWindow *self,
                     AdwToast *toast)
{
  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (ADW_IS_TOAST (toast));

  adw_toast_overlay_add_toast (self->toasts, toast);
}

void
bz_window_push_page (BzWindow *self, AdwNavigationPage *page)
{
  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (ADW_IS_NAVIGATION_PAGE (page));

  adw_navigation_view_push (self->navigation_view, page);
}

void
bz_window_open_screenshot_page (BzWindow         *self,
                                BzScreenshotPage *page)
{
  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (BZ_IS_SCREENSHOT_PAGE (page));

  if (self->screenshot_page != NULL)
    return;

  self->screenshot_page = page;
  g_signal_connect_swapped (page, "destroy",
                            G_CALLBACK (g_nullify_pointer),
                            &self->screenshot_page);
  gtk_overlay_add_overlay (self->window_overlay, GTK_WIDGET (page));
}

void
bz_window_bulk_install (BzWindow   *self,
                        GListModel *groups)
{
  g_autoptr (BulkInstallData) data = NULL;

  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (G_IS_LIST_MODEL (groups));

  data         = bulk_install_data_new ();
  data->self   = bz_track_weak (self);
  data->groups = g_object_ref (groups);

  dex_future_disown (dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) bulk_install_fiber,
      bulk_install_data_ref (data),
      bulk_install_data_unref));
}

void
bz_window_show_update_history (BzWindow *self,
                               GVariant *history)
{
  g_autoptr (GListStore) store = NULL;
  GVariantIter iter            = { 0 };
  const char  *id              = NULL;
  const char  *old_version     = NULL;
  const char  *new_version     = NULL;
  AdwDialog   *dialog          = NULL;

  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (history != NULL);

  store = g_list_store_new (BZ_TYPE_UPDATE_HISTORY_DATA_POINT);

  g_variant_iter_init (&iter, history);
  while (g_variant_iter_next (&iter, "(&s&s&s)", &id, &old_version, &new_version))
    {
      g_autoptr (BzEntryGroup) group             = NULL;
      g_autoptr (BzUpdateHistoryDataPoint) point = NULL;

      group = bz_application_map_factory_convert_one (
          bz_state_info_get_application_factory (self->state),
          gtk_string_object_new (id));

      point = g_object_new (BZ_TYPE_UPDATE_HISTORY_DATA_POINT,
                            "group", group,
                            "old-version", old_version,
                            "new-version", new_version,
                            NULL);
      g_list_store_append (store, point);
    }

  dialog = bz_update_history_dialog_new (G_LIST_MODEL (store));
  adw_dialog_present (dialog, GTK_WIDGET (self));
}

BzStateInfo *
bz_window_get_state_info (BzWindow *self)
{
  g_return_val_if_fail (BZ_IS_WINDOW (self), NULL);
  return self->state;
}

static DexFuture *
transact (BzEntry   *entry,
          gboolean   remove,
          GtkWidget *source)
{
  g_autoptr (BzTransaction) transaction = NULL;

  if (remove)
    transaction = bz_transaction_new_full (
        NULL, 0,
        NULL, 0,
        &entry, 1);
  else
    transaction = bz_transaction_new_full (
        &entry, 1,
        NULL, 0,
        NULL, 0);

  return bz_transaction_manager_add (
      bz_state_info_get_transaction_manager (bz_state_info_get_default ()),
      transaction);
}

static void
try_transact (BzWindow     *self,
              BzEntry      *entry,
              BzEntryGroup *group,
              gboolean      remove,
              gboolean      auto_confirm,
              GtkWidget    *source)
{
  g_autoptr (TransactData) data = NULL;

  g_return_if_fail (entry != NULL || group != NULL);
  if (bz_state_info_get_busy (self->state))
    {
      adw_toast_overlay_add_toast (
          self->toasts,
          adw_toast_new_format (_ ("Can't do that right now!")));
      return;
    }

  data               = transact_data_new ();
  data->self         = bz_track_weak (self);
  data->entry        = bz_object_maybe_ref (entry);
  data->group        = bz_object_maybe_ref (group);
  data->remove       = remove;
  data->auto_confirm = auto_confirm;
  data->source       = bz_object_maybe_ref (source);

  dex_future_disown (dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) transact_fiber,
      transact_data_ref (data), transact_data_unref));
}

static void
bulk_install (BzWindow *self,
              BzEntry **installs,
              guint     n_installs)
{
  g_autoptr (BzTransaction) transaction = NULL;

  g_return_if_fail (BZ_IS_WINDOW (self));
  g_return_if_fail (installs != NULL);
  g_return_if_fail (n_installs > 0);

  if (bz_state_info_get_busy (self->state))
    {
      adw_toast_overlay_add_toast (
          self->toasts,
          adw_toast_new_format (_ ("Can't do that right now!")));
      return;
    }

  transaction = bz_transaction_new_full (
      installs, n_installs,
      NULL, 0,
      NULL, 0);

  dex_future_disown (bz_transaction_manager_add (
      bz_state_info_get_transaction_manager (self->state),
      transaction));
}

static DexFuture *
bulk_install_fiber (BulkInstallData *data)
{
  g_autoptr (BzWindow) self                    = NULL;
  g_autoptr (GError) local_error               = NULL;
  g_autoptr (BzBulkInstallDialogResult) result = NULL;
  GListModel          *entries                 = NULL;
  guint                n_installs              = 0;
  g_autofree BzEntry **installs_buf            = NULL;

  bz_weak_get_or_return_reject (self, data->self);

  result = dex_await_object (
      bz_bulk_install_dialog_show (GTK_WIDGET (self), data->groups),
      &local_error);

  if (result == NULL)
    return dex_future_new_for_error (g_steal_pointer (&local_error));

  if (!bz_bulk_install_dialog_result_get_confirmed (result))
    return dex_future_new_false ();

  entries    = bz_bulk_install_dialog_result_get_entries (result);
  n_installs = g_list_model_get_n_items (entries);
  if (n_installs == 0)
    return dex_future_new_false ();

  installs_buf = g_malloc_n (n_installs, sizeof (*installs_buf));
  for (guint i = 0; i < n_installs; i++)
    installs_buf[i] = g_list_model_get_item (entries, i);

  bulk_install (self, installs_buf, n_installs);
  for (guint i = 0; i < n_installs; i++)
    g_object_unref (installs_buf[i]);

  return dex_future_new_true ();
}

static void
search (BzWindow   *self,
        const char *initial)
{
  if (initial != NULL && *initial != '\0')
    bz_search_page_set_text (self->search_page, initial);

  adw_view_stack_set_visible_child_name (self->main_view_stack, "search");
  adw_navigation_view_pop_to_tag (self->navigation_view, "main");
  gtk_widget_grab_focus (GTK_WIDGET (self->search_page));
}

static void
set_page (BzWindow *self)
{
  if (self->state == NULL)
    return;

  if (bz_state_info_get_busy (self->state))
    {
      gtk_stack_set_visible_child_name (self->main_stack, "loading");
      adw_navigation_view_pop_to_tag (self->navigation_view, "main");
    }
  else
    gtk_stack_set_visible_child_name (self->main_stack, "main");
}

static BzFullView *
create_full_view (BzWindow     *self,
                  BzEntryGroup *group)
{
  BzFullView *full_view = NULL;

  full_view = BZ_FULL_VIEW (bz_full_view_new ());
  g_object_set (full_view, "state", self->state, NULL);
  bz_full_view_set_entry_group (full_view, group);
  g_signal_connect_object (full_view, "update", G_CALLBACK (update_cb), self, G_CONNECT_SWAPPED);

  return full_view;
}

static void
emit_hook_disown (BzWindow     *self,
                  BzHookSignal  signal,
                  BzEntryGroup *group)
{
  BzMainConfig *config = NULL;
  GListModel   *hooks  = NULL;

  if (self->state == NULL)
    return;

  config = bz_state_info_get_main_config (self->state);
  if (config == NULL)
    return;

  hooks = bz_main_config_get_hooks (config);
  if (hooks == NULL)
    return;

  dex_future_disown (bz_run_hook_emission (
      hooks, signal, 0, NULL, group));
}