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

/* bz-metainfo-preview.c
 *
 * Copyright 2026 Alexander Vanhee
 *
 * 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 "config.h"

#include <glib/gi18n.h>

#include "bz-application-map-factory.h"
#include "bz-application.h"
#include "bz-dynamic-list-view.h"
#include "bz-entry-group.h"
#include "bz-featured-carousel.h"
#include "bz-flathub-category.h"
#include "bz-metainfo-preview.h"
#include "bz-rich-app-tile.h"
#include "bz-state-info.h"
#include "error.h"
#include "util.h"

BZ_DEFINE_DATA (
    open_metainfo,
    OpenMetainfo,
    {
      DexPromise *promise;
      GFile      *metainfo_file;
      GtkWindow  *parent_window;
    },
    BZ_RELEASE_DATA (promise, dex_unref);
    BZ_RELEASE_DATA (metainfo_file, g_object_unref);
    BZ_RELEASE_DATA (parent_window, g_object_unref))

static DexFuture *
on_icon_prompt_finally (DexFuture        *future,
                        OpenMetainfoData *data);

static void
on_icon_chosen (GtkFileDialog    *dialog,
                GAsyncResult     *result,
                OpenMetainfoData *data);

static BzMetainfoPickResult *
bz_metainfo_pick_result_copy (BzMetainfoPickResult *result);

static void
append_quality_apps (BzFlathubState *flathub,
                     const char     *category_name,
                     guint           count,
                     GListStore     *destination);

GType
bz_metainfo_pick_result_get_type (void)
{
  static GType type = 0;

  if (type == 0)
    type = g_boxed_type_register_static (
        "BzMetainfoPickResult",
        (GBoxedCopyFunc) bz_metainfo_pick_result_copy,
        (GBoxedFreeFunc) bz_metainfo_pick_result_free);
  return type;
}

void
bz_metainfo_pick_result_free (BzMetainfoPickResult *result)
{
  g_clear_object (&result->metainfo_file);
  g_clear_object (&result->icon_file);
  g_free (result);
}

DexFuture *
bz_metainfo_preview_open_file (GFile     *metainfo_file,
                               GtkWindow *parent_window)
{
  g_autoptr (DexPromise) promise     = dex_promise_new ();
  g_autoptr (DexFuture) alert_future = NULL;
  AdwDialog        *alert            = NULL;
  OpenMetainfoData *data             = NULL;

  g_assert (metainfo_file != NULL);

  data                = open_metainfo_data_new ();
  data->promise       = dex_ref (promise);
  data->metainfo_file = g_object_ref (metainfo_file);
  data->parent_window = parent_window != NULL ? g_object_ref (parent_window) : NULL;

  alert = adw_alert_dialog_new (NULL, NULL);
  adw_alert_dialog_format_heading (ADW_ALERT_DIALOG (alert), _ ("Add Icon to Metainfo Preview?"));
  adw_alert_dialog_format_body (
      ADW_ALERT_DIALOG (alert),
      _ ("Metainfo files don't include app icons by themselves. Would you like to select one manually?"));
  adw_alert_dialog_add_responses (
      ADW_ALERT_DIALOG (alert),
      "skip", _ ("Skip"),
      "select", _ ("Select Icon"),
      NULL);
  adw_alert_dialog_set_response_appearance (
      ADW_ALERT_DIALOG (alert), "select", ADW_RESPONSE_SUGGESTED);
  adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (alert), "select");
  adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (alert), "skip");

  adw_dialog_present (
      alert, parent_window != NULL ? GTK_WIDGET (parent_window) : NULL);

  alert_future = bz_make_alert_dialog_future (ADW_ALERT_DIALOG (alert));
  alert_future = dex_future_then (
      g_steal_pointer (&alert_future),
      (DexFutureCallback) on_icon_prompt_finally,
      data,
      (GDestroyNotify) open_metainfo_data_unref);
  dex_future_disown (g_steal_pointer (&alert_future));

  return DEX_FUTURE (g_steal_pointer (&promise));
}

static BzMetainfoPickResult *
bz_metainfo_pick_result_copy (BzMetainfoPickResult *result)
{
  BzMetainfoPickResult *copy = NULL;

  copy                = g_new0 (BzMetainfoPickResult, 1);
  copy->metainfo_file = result->metainfo_file ? g_object_ref (result->metainfo_file) : NULL;
  copy->icon_file     = result->icon_file ? g_object_ref (result->icon_file) : NULL;

  return copy;
}

static DexFuture *
on_icon_prompt_finally (DexFuture        *future,
                        OpenMetainfoData *data)
{
  g_autoptr (GError) local_error = NULL;
  const GValue *value            = NULL;
  const char   *response         = NULL;

  value    = dex_future_get_value (future, &local_error);
  response = value != NULL ? g_value_get_string (value) : NULL;

  if (g_strcmp0 (response, "select") == 0)
    {
      g_autoptr (GtkFileDialog) icon_dialog = NULL;
      g_autoptr (GtkFileFilter) filter      = NULL;
      g_autoptr (GListStore) filters        = NULL;

      icon_dialog = gtk_file_dialog_new ();
      gtk_file_dialog_set_title (icon_dialog, _ ("Select Icon"));

      filter = gtk_file_filter_new ();
      gtk_file_filter_set_name (filter, _ ("Image Files"));
      gtk_file_filter_add_pattern (filter, "*.png");
      gtk_file_filter_add_pattern (filter, "*.svg");
      gtk_file_filter_add_pattern (filter, "*.jpg");
      gtk_file_filter_add_pattern (filter, "*.jpeg");

      filters = g_list_store_new (GTK_TYPE_FILE_FILTER);
      g_list_store_append (filters, filter);
      gtk_file_dialog_set_filters (icon_dialog, G_LIST_MODEL (filters));

      gtk_file_dialog_open (
          icon_dialog,
          data->parent_window,
          NULL,
          (GAsyncReadyCallback) on_icon_chosen,
          open_metainfo_data_ref (data));
    }
  else
    {
      BzMetainfoPickResult *pick = NULL;

      pick                = g_new0 (BzMetainfoPickResult, 1);
      pick->metainfo_file = g_object_ref (data->metainfo_file);
      pick->icon_file     = NULL;

      dex_promise_resolve_boxed (
          data->promise,
          bz_metainfo_pick_result_get_type (),
          g_steal_pointer (&pick));
    }

  return dex_future_new_true ();
}

static void
on_icon_chosen (GtkFileDialog    *dialog,
                GAsyncResult     *result,
                OpenMetainfoData *data)
{
  g_autoptr (OpenMetainfoData) owned_data = data;
  g_autoptr (GFile) icon_file             = NULL;
  BzMetainfoPickResult *pick              = NULL;

  icon_file = gtk_file_dialog_open_finish (dialog, result, NULL);

  pick                = g_new0 (BzMetainfoPickResult, 1);
  pick->metainfo_file = g_object_ref (owned_data->metainfo_file);
  pick->icon_file     = g_steal_pointer (&icon_file);

  dex_promise_resolve_boxed (
      owned_data->promise,
      bz_metainfo_pick_result_get_type (),
      g_steal_pointer (&pick));
}

static void
append_quality_apps (BzFlathubState *flathub,
                     const char     *category_name,
                     guint           count,
                     GListStore     *destination)
{
  GListModel *categories   = NULL;
  guint       n_categories = 0;

  if (flathub == NULL)
    return;

  categories   = bz_flathub_state_get_categories (flathub);
  n_categories = categories != NULL ? g_list_model_get_n_items (categories) : 0;

  for (guint i = 0; i < n_categories; i++)
    {
      g_autoptr (BzFlathubCategory) category = NULL;

      category = g_list_model_get_item (categories, i);
      if (g_strcmp0 (bz_flathub_category_get_name (category), category_name) == 0)
        {
          g_autoptr (GListModel) quality_apps = NULL;
          guint n_quality_apps                = 0;
          guint n_pick                        = 0;
          guint offset                        = 0;

          quality_apps   = bz_flathub_category_dup_quality_applications (category);
          n_quality_apps = quality_apps != NULL ? g_list_model_get_n_items (quality_apps) : 0;

          n_pick = MIN (count, n_quality_apps);
          if (n_quality_apps > 0)
            offset = g_random_int_range (0, n_quality_apps);

          for (guint j = 0; j < n_pick; j++)
            {
              g_autoptr (GObject) picked_obj = NULL;
              guint index                    = (offset + j) % n_quality_apps;

              picked_obj = g_list_model_get_item (quality_apps, index);
              if (BZ_IS_ENTRY_GROUP (picked_obj))
                g_list_store_append (destination, BZ_ENTRY_GROUP (picked_obj));
            }
          break;
        }
    }
}

AdwNavigationPage *
create_entry_group_preview_page (BzEntryGroup *group)
{
  g_autoptr (GtkBuilder) builder            = NULL;
  AdwNavigationPage  *page                  = NULL;
  BzFeaturedCarousel *carousel              = NULL;
  GtkWidget          *section_list          = NULL;
  GtkWidget          *rich_section_list     = NULL;
  g_autoptr (GListStore) store              = NULL;
  g_autoptr (GListStore) section_store      = NULL;
  g_autoptr (GListStore) rich_section_store = NULL;
  BzStateInfo    *state                     = NULL;
  BzFlathubState *flathub                   = NULL;

  builder = gtk_builder_new_from_resource (
      "/io/github/kolunmi/Bazaar/bz-metainfo-preview-page.ui");

  page              = ADW_NAVIGATION_PAGE (gtk_builder_get_object (builder, "page"));
  carousel          = BZ_FEATURED_CAROUSEL (gtk_builder_get_object (builder, "carousel"));
  section_list      = GTK_WIDGET (gtk_builder_get_object (builder, "section_list"));
  rich_section_list = GTK_WIDGET (gtk_builder_get_object (builder, "rich_section_list"));

  state   = bz_state_info_get_default ();
  flathub = bz_state_info_get_flathub (state);

  store = g_list_store_new (BZ_TYPE_ENTRY_GROUP);
  g_list_store_append (store, group);
  append_quality_apps (flathub, "utility", 4, store);

  bz_featured_carousel_set_model (carousel, G_LIST_MODEL (store));

  section_store = g_list_store_new (BZ_TYPE_ENTRY_GROUP);
  append_quality_apps (flathub, "utility", 11, section_store);
  g_list_store_insert (
      section_store,
      MIN (1, g_list_model_get_n_items (G_LIST_MODEL (section_store))),
      group);
  g_object_set (section_list, "model", G_LIST_MODEL (section_store), NULL);

  rich_section_store = g_list_store_new (BZ_TYPE_ENTRY_GROUP);
  append_quality_apps (flathub, "utility", 5, rich_section_store);
  g_list_store_insert (
      rich_section_store,
      MIN (1, g_list_model_get_n_items (G_LIST_MODEL (rich_section_store))),
      group);
  g_object_set (rich_section_list, "model", G_LIST_MODEL (rich_section_store), NULL);

  g_object_ref (page);
  return page;
}