*
* Copyright 2025 Adam Masciola, 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 <bzvala.h>
#include "bz-addon-tile.h"
#include "bz-addons-dialog.h"
#include "bz-app-size-dialog.h"
#include "bz-application-map-factory.h"
#include "bz-application.h"
#include "bz-appstream-description-render.h"
#include "bz-context-tile.h"
#include "bz-entry-group.h"
#include "bz-fading-clamp.h"
#include "bz-flatpak-entry.h"
#include "bz-install-controls.h"
#include "bz-license-dialog.h"
#include "bz-result.h"
#include "bz-share-list.h"
#include "bz-state-info.h"
#include "bz-stats-dialog.h"
#include "bz-window.h"
#include "context-tile-callbacks.h"
#include "template-callbacks.h"
#include "util.h"
struct _BzAddonsDialog
{
AdwDialog parent_instance;
GListModel *addon_groups;
BzEntryGroup *selected_group;
BzResult *selected_ui_entry;
DexFuture *selected_ui_future;
BzResult *parent_ui_entry;
DexFuture *parent_ui_future;
BzStateInfo *state;
AdwAnimation *width_animation;
AdwAnimation *height_animation;
gboolean has_mapped;
AdwNavigationView *navigation_view;
GtkToggleButton *description_toggle;
AdwClamp *full_view_clamp;
AdwClamp *list_clamp;
GtkCustomSorter *sorter;
};
G_DEFINE_FINAL_TYPE (BzAddonsDialog, bz_addons_dialog, ADW_TYPE_DIALOG)
enum
{
PROP_0,
PROP_ADDON_GROUPS,
PROP_SELECTED_GROUP,
PROP_SELECTED_UI_ENTRY,
PROP_PARENT_UI_ENTRY,
PROP_STATE,
LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };
static char *format_parent_title (gpointer object, const char *title);
static int get_description_max_height (gpointer object, gboolean active);
static char *get_description_toggle_text (gpointer object, gboolean active);
static void size_cb (BzAddonsDialog *self, GtkButton *button);
static void license_cb (BzAddonsDialog *self, GtkButton *button);
static void dl_stats_cb (BzAddonsDialog *self, GtkButton *button);
static void other_source_download_cb (BzAddonsDialog *self, BzEntry *entry);
static void animate_to_size (BzAddonsDialog *self);
static void on_visible_page_tag_changed (AdwNavigationView *nav_view, GParamSpec *pspec, BzAddonsDialog *self);
static DexFuture *on_parent_ui_entry_resolved (DexFuture *future, GWeakRef *wr);
static DexFuture *on_selected_ui_entry_resolved (DexFuture *future, GWeakRef *wr);
static void set_selected_group (BzAddonsDialog *self, BzEntryGroup *group);
static void tile_activated_cb (BzAddonTile *tile);
static int sort_func (BzEntryGroup *a, BzEntryGroup *b, BzAddonsDialog *self);
static void
bz_addons_dialog_dispose (GObject *object)
{
BzAddonsDialog *self = BZ_ADDONS_DIALOG (object);
dex_clear (&self->selected_ui_future);
dex_clear (&self->parent_ui_future);
g_clear_object (&self->addon_groups);
g_clear_object (&self->selected_group);
g_clear_object (&self->selected_ui_entry);
g_clear_object (&self->parent_ui_entry);
g_clear_object (&self->state);
g_clear_object (&self->width_animation);
g_clear_object (&self->height_animation);
G_OBJECT_CLASS (bz_addons_dialog_parent_class)->dispose (object);
}
static void
bz_addons_dialog_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
BzAddonsDialog *self = BZ_ADDONS_DIALOG (object);
switch (prop_id)
{
case PROP_ADDON_GROUPS:
g_value_set_object (value, self->addon_groups);
break;
case PROP_SELECTED_GROUP:
g_value_set_object (value, self->selected_group);
break;
case PROP_SELECTED_UI_ENTRY:
g_value_set_object (value, self->selected_ui_entry);
break;
case PROP_PARENT_UI_ENTRY:
g_value_set_object (value, self->parent_ui_entry);
break;
case PROP_STATE:
g_value_set_object (value, bz_state_info_get_default ());
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_addons_dialog_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
BzAddonsDialog *self = BZ_ADDONS_DIALOG (object);
switch (prop_id)
{
case PROP_ADDON_GROUPS:
g_clear_object (&self->addon_groups);
self->addon_groups = g_value_dup_object (value);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ADDON_GROUPS]);
break;
case PROP_SELECTED_GROUP:
g_clear_object (&self->selected_group);
self->selected_group = g_value_dup_object (value);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_GROUP]);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_addons_dialog_class_init (BzAddonsDialogClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->dispose = bz_addons_dialog_dispose;
object_class->get_property = bz_addons_dialog_get_property;
object_class->set_property = bz_addons_dialog_set_property;
props[PROP_ADDON_GROUPS] =
g_param_spec_object (
"addon-groups",
NULL, NULL,
G_TYPE_LIST_MODEL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_SELECTED_GROUP] =
g_param_spec_object (
"selected-group",
NULL, NULL,
BZ_TYPE_ENTRY_GROUP,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_SELECTED_UI_ENTRY] =
g_param_spec_object (
"selected-ui-entry",
NULL, NULL,
BZ_TYPE_RESULT,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_PARENT_UI_ENTRY] =
g_param_spec_object (
"parent-ui-entry",
NULL, NULL,
BZ_TYPE_RESULT,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_STATE] =
g_param_spec_object (
"state",
NULL, NULL,
BZ_TYPE_STATE_INFO,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, LAST_PROP, props);
g_type_ensure (BZ_TYPE_ADDON_TILE);
g_type_ensure (BZ_TYPE_APPSTREAM_DESCRIPTION_RENDER);
g_type_ensure (BZ_TYPE_CONTEXT_TILE);
g_type_ensure (BZ_TYPE_ENTRY);
g_type_ensure (BZ_TYPE_ENTRY_GROUP);
g_type_ensure (BZ_TYPE_FADING_CLAMP);
g_type_ensure (BZ_TYPE_FLATPAK_ENTRY);
g_type_ensure (BZ_TYPE_INSTALL_CONTROLS);
g_type_ensure (BZ_TYPE_RESULT);
g_type_ensure (BZ_TYPE_SHARE_LIST);
g_type_ensure (BZ_TYPE_OTHER_SOURCES_LIST);
gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-addons-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, BzAddonsDialog, navigation_view);
gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, description_toggle);
gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, full_view_clamp);
gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, list_clamp);
gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, sorter);
gtk_widget_class_bind_template_callback (widget_class, tile_activated_cb);
gtk_widget_class_bind_template_callback (widget_class, on_visible_page_tag_changed);
gtk_widget_class_bind_template_callback (widget_class, format_parent_title);
gtk_widget_class_bind_template_callback (widget_class, get_description_max_height);
gtk_widget_class_bind_template_callback (widget_class, get_description_toggle_text);
gtk_widget_class_bind_template_callback (widget_class, license_cb);
gtk_widget_class_bind_template_callback (widget_class, size_cb);
gtk_widget_class_bind_template_callback (widget_class, dl_stats_cb);
gtk_widget_class_bind_template_callback (widget_class, other_source_download_cb);
}
static void
bz_addons_dialog_init (BzAddonsDialog *self)
{
AdwAnimationTarget *width_target = NULL;
AdwAnimationTarget *height_target = NULL;
gtk_widget_init_template (GTK_WIDGET (self));
width_target = adw_property_animation_target_new (G_OBJECT (self), "content-width");
self->width_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 300, width_target);
height_target = adw_property_animation_target_new (G_OBJECT (self), "content-height");
self->height_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 300, height_target);
g_signal_connect_swapped (self, "map", G_CALLBACK (animate_to_size), self);
gtk_custom_sorter_set_sort_func (self->sorter, (GCompareDataFunc) sort_func, self, NULL);
}
AdwDialog *
bz_addons_dialog_new (BzEntryGroup *group)
{
GListModel *ids = NULL;
GListModel *groups = NULL;
BzApplicationMapFactory *factory = NULL;
BzAddonsDialog *self = NULL;
ids = bz_entry_group_get_addon_group_ids (group);
if (ids != NULL)
{
factory = bz_state_info_get_application_factory (bz_state_info_get_default ());
if (factory != NULL)
groups = bz_application_map_factory_generate (factory, ids);
}
self = g_object_new (
BZ_TYPE_ADDONS_DIALOG,
"addon-groups", groups,
NULL);
self->parent_ui_entry = bz_entry_group_dup_ui_entry (group);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARENT_UI_ENTRY]);
if (groups == NULL || g_list_model_get_n_items (groups) == 0)
adw_navigation_view_replace (self->navigation_view,
(AdwNavigationPage *[]){ adw_navigation_view_find_page (self->navigation_view, "empty") },
1);
else if (g_list_model_get_n_items (groups) == 1)
{
g_autoptr (BzEntryGroup) single = g_list_model_get_item (groups, 0);
AdwNavigationPage *full_view_page = NULL;
set_selected_group (self, single);
full_view_page = adw_navigation_view_find_page (self->navigation_view, "full-view");
adw_navigation_view_replace (self->navigation_view, &full_view_page, 1);
}
return ADW_DIALOG (self);
}
AdwDialog *
bz_addons_dialog_new_single (BzEntryGroup *group)
{
BzAddonsDialog *self = NULL;
AdwNavigationPage *full_view = NULL;
self = g_object_new (BZ_TYPE_ADDONS_DIALOG, NULL);
set_selected_group (self, group);
full_view = adw_navigation_view_find_page (self->navigation_view, "full-view");
adw_navigation_view_replace (self->navigation_view, &full_view, 1);
return ADW_DIALOG (self);
}
BzEntry *
bz_addons_dialog_get_parent_entry (BzAddonsDialog *self)
{
g_return_val_if_fail (BZ_IS_ADDONS_DIALOG (self), NULL);
if (self->parent_ui_entry == NULL || !bz_result_get_resolved (self->parent_ui_entry))
return NULL;
return bz_result_get_object (self->parent_ui_entry);
}
static char *
format_parent_title (gpointer object,
const char *title)
{
if (title == NULL || *title == '\0')
return g_strdup ("");
return g_strdup_printf (_ ("Add-on for %s"), title);
}
static int
get_description_max_height (gpointer object,
gboolean active)
{
return active ? 10000 : 170;
}
static char *
get_description_toggle_text (gpointer object,
gboolean active)
{
return g_strdup (active ? _ ("Show Less") : _ ("Show More"));
}
static void
size_cb (BzAddonsDialog *self,
GtkButton *button)
{
AdwNavigationPage *page = NULL;
if (self->selected_group == NULL)
return;
page = bz_app_size_page_new (self->selected_group);
adw_navigation_view_push (self->navigation_view, page);
}
static void
license_cb (BzAddonsDialog *self,
GtkButton *button)
{
AdwNavigationPage *page = NULL;
BzEntry *ui_entry = NULL;
if (self->selected_ui_entry == NULL)
return;
ui_entry = bz_result_get_object (self->selected_ui_entry);
if (ui_entry == NULL)
return;
page = bz_license_page_new (ui_entry);
adw_navigation_view_push (self->navigation_view, page);
}
static void
dl_stats_cb (BzAddonsDialog *self,
GtkButton *button)
{
BzStatsDialog *bin = NULL;
AdwNavigationPage *page = NULL;
BzEntry *ui_entry = NULL;
if (self->selected_ui_entry == NULL)
return;
ui_entry = bz_result_get_object (self->selected_ui_entry);
if (ui_entry == NULL)
return;
bin = BZ_STATS_DIALOG (bz_stats_dialog_new (NULL, NULL, 0));
page = adw_navigation_page_new (GTK_WIDGET (bin), _ ("Download Stats"));
adw_navigation_page_set_tag (page, "stats");
g_object_bind_property (ui_entry, "download-stats", bin, "model", G_BINDING_SYNC_CREATE);
g_object_bind_property (ui_entry, "total-downloads", bin, "total-downloads", G_BINDING_SYNC_CREATE);
adw_navigation_view_push (self->navigation_view, page);
bz_stats_dialog_animate_open (bin);
}
static void
other_source_download_cb (BzAddonsDialog *self,
BzEntry *entry)
{
GtkWidget *window = NULL;
window = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
if (BZ_IS_WINDOW (window))
bz_window_install_entry (BZ_WINDOW (window), entry);
}
static void
animate_to_size (BzAddonsDialog *self)
{
const char *tag = NULL;
int target_width = 0;
int target_height = 0;
int nat = 0;
int cur_width = 0;
int measure_for = 0;
int cur_w = 0;
int cur_h = 0;
int delta_w = 0;
int delta_h = 0;
int delta = 0;
guint duration = 0;
tag = adw_navigation_view_get_visible_page_tag (self->navigation_view);
if (g_strcmp0 (tag, "list") == 0)
{
cur_width = gtk_widget_get_width (GTK_WIDGET (self));
target_width = 500;
measure_for = MAX (-1, MIN (target_width, cur_width) - 48);
gtk_widget_measure (GTK_WIDGET (self->list_clamp), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL);
target_height = CLAMP (nat + 50, 150, 600);
}
else if (g_strcmp0 (tag, "full-view") == 0)
{
cur_width = gtk_widget_get_width (GTK_WIDGET (self));
target_width = 500;
measure_for = MAX (-1, MIN (target_width, cur_width) - 48);
gtk_widget_measure (GTK_WIDGET (self->full_view_clamp), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL);
target_height = CLAMP (nat + 50, 300, 700);
}
else if (g_strcmp0 (tag, "app-size") == 0)
{
target_width = 500;
target_height = 300;
}
else if (g_strcmp0 (tag, "license") == 0)
{
cur_width = gtk_widget_get_width (GTK_WIDGET (self));
target_width = 400;
measure_for = target_width - 48;
gtk_widget_measure (GTK_WIDGET (self->navigation_view), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL);
target_height = CLAMP (nat, 300, 700);
}
else if (g_strcmp0 (tag, "stats") == 0)
{
target_width = 1250;
target_height = 750;
}
else if (g_strcmp0 (tag, "empty") == 0)
{
target_width = 500;
target_height = 500;
}
else
return;
if (!self->has_mapped)
{
self->has_mapped = TRUE;
adw_dialog_set_content_width (ADW_DIALOG (self), target_width);
adw_dialog_set_content_height (ADW_DIALOG (self), target_height);
return;
}
cur_w = adw_dialog_get_content_width (ADW_DIALOG (self));
cur_h = adw_dialog_get_content_height (ADW_DIALOG (self));
delta_w = ABS (target_width - cur_w);
delta_h = ABS (target_height - cur_h);
delta = MAX (delta_w, delta_h);
duration = (guint) CLAMP (delta * 0.6, 200, (target_width < cur_w || target_height < cur_h) ? 300 : 600);
adw_timed_animation_set_duration (ADW_TIMED_ANIMATION (self->width_animation), duration);
adw_timed_animation_set_duration (ADW_TIMED_ANIMATION (self->height_animation), duration);
adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->width_animation), cur_w);
adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->width_animation), target_width);
adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->height_animation), cur_h);
adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->height_animation), target_height);
adw_animation_play (self->width_animation);
adw_animation_play (self->height_animation);
}
static void
on_visible_page_tag_changed (AdwNavigationView *nav_view,
GParamSpec *pspec,
BzAddonsDialog *self)
{
g_idle_add_once ((GSourceOnceFunc) animate_to_size, self);
}
static DexFuture *
on_parent_ui_entry_resolved (DexFuture *future,
GWeakRef *wr)
{
g_autoptr (BzAddonsDialog) self = NULL;
bz_weak_get_or_return_reject (self, wr);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARENT_UI_ENTRY]);
return dex_future_new_for_boolean (TRUE);
}
static DexFuture *
on_selected_ui_entry_resolved (DexFuture *future,
GWeakRef *wr)
{
g_autoptr (BzAddonsDialog) self = NULL;
const GValue *value = NULL;
g_autoptr (BzEntry) ui_entry = NULL;
const char *ref = NULL;
g_auto (GStrv) parts = NULL;
BzApplicationMapFactory *factory = NULL;
GtkStringObject *item = NULL;
BzEntryGroup *parent_group = NULL;
bz_weak_get_or_return_reject (self, wr);
value = dex_future_get_value (future, NULL);
if (value == NULL || !G_VALUE_HOLDS_OBJECT (value))
return dex_future_new_for_boolean (TRUE);
ui_entry = g_value_dup_object (value);
if (ui_entry == NULL || !BZ_IS_FLATPAK_ENTRY (ui_entry))
return dex_future_new_for_boolean (TRUE);
ref = bz_flatpak_entry_get_addon_extension_of_ref (BZ_FLATPAK_ENTRY (ui_entry));
if (ref == NULL)
return dex_future_new_for_boolean (TRUE);
parts = g_strsplit (ref, "/", -1);
if (parts[0] == NULL || parts[1] == NULL)
return dex_future_new_for_boolean (TRUE);
factory = bz_state_info_get_application_factory (bz_state_info_get_default ());
item = gtk_string_object_new (parts[1]);
parent_group = bz_application_map_factory_convert_one (factory, item);
if (parent_group == NULL)
return dex_future_new_for_boolean (TRUE);
g_clear_object (&self->parent_ui_entry);
self->parent_ui_entry = bz_entry_group_dup_ui_entry (parent_group);
if (self->parent_ui_entry == NULL)
return dex_future_new_for_boolean (TRUE);
if (bz_result_get_resolved (self->parent_ui_entry))
{
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARENT_UI_ENTRY]);
}
else
{
g_autoptr (DexFuture) parent_future = NULL;
GWeakRef *parent_wr = NULL;
parent_future = bz_result_dup_future (self->parent_ui_entry);
parent_wr = bz_track_weak (self);
parent_future = dex_future_then (
parent_future,
(DexFutureCallback) on_parent_ui_entry_resolved,
parent_wr,
bz_weak_release);
dex_clear (&self->parent_ui_future);
self->parent_ui_future = g_steal_pointer (&parent_future);
}
return dex_future_new_for_boolean (TRUE);
}
static void
set_selected_group (BzAddonsDialog *self,
BzEntryGroup *group)
{
dex_clear (&self->selected_ui_future);
dex_clear (&self->parent_ui_future);
g_clear_object (&self->selected_group);
g_clear_object (&self->selected_ui_entry);
g_clear_object (&self->parent_ui_entry);
gtk_toggle_button_set_active (self->description_toggle, FALSE);
if (group == NULL)
return;
self->selected_group = g_object_ref (group);
self->selected_ui_entry = bz_entry_group_dup_ui_entry (group);
if (self->selected_ui_entry == NULL)
goto notify;
if (bz_result_get_resolved (self->selected_ui_entry))
{
g_autoptr (BzEntry) entry = NULL;
g_autoptr (DexFuture) object_future = NULL;
GWeakRef *wr = NULL;
entry = g_object_ref (bz_result_get_object (self->selected_ui_entry));
object_future = dex_future_new_for_object (entry);
wr = bz_track_weak (self);
dex_unref (on_selected_ui_entry_resolved (object_future, wr));
bz_weak_release (wr);
}
else
{
g_autoptr (DexFuture) ui_future = NULL;
GWeakRef *wr = NULL;
ui_future = bz_result_dup_future (self->selected_ui_entry);
wr = bz_track_weak (self);
ui_future = dex_future_then (
ui_future,
(DexFutureCallback) on_selected_ui_entry_resolved,
wr,
bz_weak_release);
self->selected_ui_future = g_steal_pointer (&ui_future);
}
notify:
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_GROUP]);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_UI_ENTRY]);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARENT_UI_ENTRY]);
}
static void
tile_activated_cb (BzAddonTile *tile)
{
BzAddonsDialog *self = NULL;
BzEntryGroup *group = NULL;
self = BZ_ADDONS_DIALOG (gtk_widget_get_ancestor (GTK_WIDGET (tile), BZ_TYPE_ADDONS_DIALOG));
if (self == NULL)
return;
group = bz_addon_tile_get_group (tile);
if (group == NULL)
return;
set_selected_group (self, group);
adw_navigation_view_push_by_tag (self->navigation_view, "full-view");
}
static int
sort_func (BzEntryGroup *a,
BzEntryGroup *b,
BzAddonsDialog *self)
{
const char *desc_a = NULL;
const char *desc_b = NULL;
gboolean has_a = FALSE;
gboolean has_b = FALSE;
int result = 0;
desc_a = bz_entry_group_get_description (a);
desc_b = bz_entry_group_get_description (b);
has_a = desc_a != NULL && *desc_a != '\0';
has_b = desc_b != NULL && *desc_b != '\0';
if (has_a != has_b)
result = has_b - has_a;
else
result = g_utf8_collate (bz_entry_group_get_title (a),
bz_entry_group_get_title (b));
return result;
}