/* bz-favorite-button.c * * Copyright 2025 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #include "config.h" #include #include #include "bz-entry.h" #include "bz-favorite-button.h" #include "bz-state-info.h" #include "env.h" #include "error.h" #include "global-net.h" struct _BzFavoriteButton { GtkButton parent_instance; BzEntry *entry; BzStateInfo *state; GtkBox *box; GtkImage *icon; GtkRevealer *count_revealer; GtkLabel *count_label; GtkStack *stack; GtkWidget *login_popover; gboolean is_favorited; }; G_DEFINE_FINAL_TYPE (BzFavoriteButton, bz_favorite_button, GTK_TYPE_BUTTON) enum { PROP_0, PROP_ENTRY, PROP_STATE, LAST_PROP }; static GParamSpec *props[LAST_PROP] = { 0 }; static DexFuture * fetch_favorite_status_fiber (BzFavoriteButton *button); static DexFuture * toggle_favorite_fiber (BzFavoriteButton *button); static void button_clicked_cb (BzFavoriteButton *self, GtkButton *button); static void update_icon (BzFavoriteButton *self); static void update_count (BzFavoriteButton *self); static void on_login_button_clicked (GtkButton *button, GtkPopover *popover); static void show_login_popover (BzFavoriteButton *self); static gboolean is_positive (gpointer object, int value); static char * format_favorites_count (gpointer object, int count); static void bz_favorite_button_dispose (GObject *object) { BzFavoriteButton *self = BZ_FAVORITE_BUTTON (object); g_clear_object (&self->entry); g_clear_object (&self->state); g_clear_pointer (&self->login_popover, gtk_widget_unparent); G_OBJECT_CLASS (bz_favorite_button_parent_class)->dispose (object); } static void bz_favorite_button_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BzFavoriteButton *self = BZ_FAVORITE_BUTTON (object); switch (prop_id) { case PROP_ENTRY: g_value_set_object (value, bz_favorite_button_get_entry (self)); break; case PROP_STATE: g_value_set_object (value, bz_favorite_button_get_state (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bz_favorite_button_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { BzFavoriteButton *self = BZ_FAVORITE_BUTTON (object); switch (prop_id) { case PROP_ENTRY: bz_favorite_button_set_entry (self, g_value_get_object (value)); break; case PROP_STATE: bz_favorite_button_set_state (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bz_favorite_button_class_init (BzFavoriteButtonClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->dispose = bz_favorite_button_dispose; object_class->get_property = bz_favorite_button_get_property; object_class->set_property = bz_favorite_button_set_property; 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_STATE] = g_param_spec_object ( "state", NULL, NULL, BZ_TYPE_STATE_INFO, 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-favorite-button.ui"); gtk_widget_class_bind_template_child (widget_class, BzFavoriteButton, box); gtk_widget_class_bind_template_child (widget_class, BzFavoriteButton, icon); gtk_widget_class_bind_template_child (widget_class, BzFavoriteButton, count_revealer); gtk_widget_class_bind_template_child (widget_class, BzFavoriteButton, count_label); gtk_widget_class_bind_template_child (widget_class, BzFavoriteButton, stack); gtk_widget_class_bind_template_callback (widget_class, is_positive); gtk_widget_class_bind_template_callback (widget_class, format_favorites_count); gtk_widget_class_bind_template_callback (widget_class, button_clicked_cb); } static void bz_favorite_button_init (BzFavoriteButton *self) { gtk_widget_init_template (GTK_WIDGET (self)); self->is_favorited = FALSE; } GtkWidget * bz_favorite_button_new (void) { return g_object_new (BZ_TYPE_FAVORITE_BUTTON, NULL); } void bz_favorite_button_set_entry (BzFavoriteButton *self, BzEntry *entry) { g_return_if_fail (BZ_IS_FAVORITE_BUTTON (self)); g_return_if_fail (entry == NULL || BZ_IS_ENTRY (entry)); if (g_set_object (&self->entry, entry)) { update_count (self); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENTRY]); if (self->state != NULL && self->entry != NULL) { dex_future_disown (dex_scheduler_spawn ( dex_scheduler_get_default (), bz_get_dex_stack_size (), (DexFiberFunc) fetch_favorite_status_fiber, g_object_ref (self), g_object_unref)); } } } BzEntry * bz_favorite_button_get_entry (BzFavoriteButton *self) { g_return_val_if_fail (BZ_IS_FAVORITE_BUTTON (self), NULL); return self->entry; } void bz_favorite_button_set_state (BzFavoriteButton *self, BzStateInfo *state) { g_return_if_fail (BZ_IS_FAVORITE_BUTTON (self)); g_return_if_fail (state == NULL || BZ_IS_STATE_INFO (state)); if (g_set_object (&self->state, state)) { g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]); if (self->state != NULL && self->entry != NULL) { dex_future_disown (dex_scheduler_spawn ( dex_scheduler_get_default (), bz_get_dex_stack_size (), (DexFiberFunc) fetch_favorite_status_fiber, g_object_ref (self), g_object_unref)); } } } BzStateInfo * bz_favorite_button_get_state (BzFavoriteButton *self) { g_return_val_if_fail (BZ_IS_FAVORITE_BUTTON (self), NULL); return self->state; } static gboolean is_positive (gpointer object, int value) { return value > 0; } static char * format_favorites_count (gpointer object, int count) { if (count >= 1000000) return g_strdup_printf ("%.1fM", count / 1000000.0); else if (count >= 1000) return g_strdup_printf ("%.1fK", count / 1000.0); else return g_strdup_printf ("%d", count); } static void update_icon (BzFavoriteButton *self) { if (self->is_favorited) gtk_image_set_from_icon_name (self->icon, "bookmark-filled-symbolic"); else gtk_image_set_from_icon_name (self->icon, "bookmark-outline-symbolic"); } static void update_count (BzFavoriteButton *self) { int count = 0; if (self->entry != NULL) g_object_get (self->entry, "favorites-count", &count, NULL); gtk_revealer_set_reveal_child (self->count_revealer, count > 0); if (count > 0) { g_autofree char *formatted = format_favorites_count (NULL, count); gtk_label_set_label (self->count_label, formatted); } } static DexFuture * fetch_favorite_status_fiber (BzFavoriteButton *button) { g_autoptr (GError) local_error = NULL; g_autoptr (JsonNode) node = NULL; g_autofree char *request = NULL; BzAuthState *auth_state = NULL; const char *token = NULL; const char *app_id = NULL; if (button->state == NULL || button->entry == NULL) return NULL; auth_state = bz_state_info_get_auth_state (button->state); if (!bz_auth_state_is_authenticated (auth_state)) return NULL; token = bz_auth_state_get_token (auth_state); if (token == NULL) return NULL; g_object_get (button->entry, "id", &app_id, NULL); request = g_strdup_printf ("/favorites/%s", app_id); node = dex_await_boxed ( bz_query_flathub_v2_json_authenticated (request, token), &local_error); if (local_error == NULL && node != NULL && JSON_NODE_HOLDS_VALUE (node)) { button->is_favorited = json_node_get_boolean (node); update_icon (button); } return NULL; } static DexFuture * toggle_favorite_fiber (BzFavoriteButton *button) { g_autoptr (GError) local_error = NULL; g_autofree char *request = NULL; BzAuthState *auth_state = NULL; const char *token = NULL; const char *app_id = NULL; int current_count = 0; if (button->state == NULL || button->entry == NULL) goto err; auth_state = bz_state_info_get_auth_state (button->state); if (!bz_auth_state_is_authenticated (auth_state)) goto err; token = bz_auth_state_get_token (auth_state); if (token == NULL) goto err; g_object_get (button->entry, "id", &app_id, "favorites-count", ¤t_count, NULL); if (button->is_favorited) request = g_strdup_printf ("/favorites/%s/remove", app_id); else request = g_strdup_printf ("/favorites/%s/add", app_id); if (button->is_favorited) dex_await ( bz_query_flathub_v2_json_authenticated_delete (request, token), &local_error); else dex_await ( bz_query_flathub_v2_json_authenticated_post (request, token), &local_error); if (local_error != NULL) { GtkWidget *window = NULL; gtk_stack_set_visible_child_name (button->stack, "content"); window = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_WINDOW); if (window != NULL) bz_show_error_for_widget (window, _ ("Failed to update favorite"), local_error->message); } else { button->is_favorited = !button->is_favorited; g_object_set (button->entry, "favorites-count", button->is_favorited ? current_count + 1 : current_count - 1, NULL); update_icon (button); update_count (button); gtk_stack_set_visible_child_name (button->stack, "content"); } return NULL; err: gtk_stack_set_visible_child_name (button->stack, "content"); return NULL; } static void on_login_button_clicked (GtkButton *button, GtkPopover *popover) { gtk_popover_popdown (popover); } static void show_login_popover (BzFavoriteButton *self) { if (self->login_popover == NULL) { GtkWidget *box; GtkWidget *label; GtkWidget *button; self->login_popover = gtk_popover_new (); gtk_widget_set_parent (self->login_popover, GTK_WIDGET (self)); box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); gtk_widget_set_margin_top (box, 6); gtk_widget_set_margin_bottom (box, 6); gtk_widget_set_margin_start (box, 6); gtk_widget_set_margin_end (box, 6); label = gtk_label_new (_ ("Log in with Flathub to manage favorites")); gtk_label_set_wrap (GTK_LABEL (label), TRUE); gtk_label_set_max_width_chars (GTK_LABEL (label), 17); gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER); gtk_box_append (GTK_BOX (box), label); button = gtk_button_new_with_label (_ ("Log In")); gtk_widget_add_css_class (button, "suggested-action"); gtk_actionable_set_action_name (GTK_ACTIONABLE (button), "app.flathub-login"); g_signal_connect (button, "clicked", G_CALLBACK (on_login_button_clicked), self->login_popover); gtk_box_append (GTK_BOX (box), button); gtk_popover_set_child (GTK_POPOVER (self->login_popover), box); } gtk_popover_popup (GTK_POPOVER (self->login_popover)); } static void button_clicked_cb (BzFavoriteButton *self, GtkButton *button) { BzAuthState *auth_state = NULL; if (self->state == NULL) return; auth_state = bz_state_info_get_auth_state (self->state); if (!bz_auth_state_is_authenticated (auth_state)) { show_login_popover (self); return; } gtk_stack_set_visible_child_name (self->stack, "spinner"); dex_future_disown (dex_scheduler_spawn ( dex_scheduler_get_default (), bz_get_dex_stack_size (), (DexFiberFunc) toggle_favorite_fiber, g_object_ref (self), g_object_unref)); }