/* bz-apps-page.c * * 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include "bz-all-apps-page.h" #include "bz-app-tile.h" #include "bz-application.h" #include "bz-apps-page.h" #include "bz-dynamic-list-view.h" #include "bz-entry-group.h" #include "bz-flathub-category.h" #include "bz-state-info.h" #include "bz-subcategory-list.h" #include "env.h" struct _BzAppsPage { AdwNavigationPage parent_instance; char *title; GListModel *applications; GListModel *all_applications; GListModel *carousel_applications; char *subtitle; BzFlathubCategory *category; /* Template widgets */ }; G_DEFINE_FINAL_TYPE (BzAppsPage, bz_apps_page, ADW_TYPE_NAVIGATION_PAGE) enum { PROP_0, PROP_PAGE_TITLE, PROP_APPLICATIONS, PROP_ALL_APPLICATIONS, PROP_CAROUSEL_APPLICATIONS, PROP_PAGE_SUBTITLE, PROP_CATEGORY, PROP_FLATHUB_STATE, LAST_PROP }; static GParamSpec *props[LAST_PROP] = { 0 }; static void bz_apps_page_dispose (GObject *object) { BzAppsPage *self = BZ_APPS_PAGE (object); g_clear_pointer (&self->title, g_free); g_clear_object (&self->applications); g_clear_object (&self->all_applications); g_clear_object (&self->carousel_applications); g_clear_pointer (&self->subtitle, g_free); g_clear_object (&self->category); G_OBJECT_CLASS (bz_apps_page_parent_class)->dispose (object); } static void bz_apps_page_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BzAppsPage *self = BZ_APPS_PAGE (object); switch (prop_id) { case PROP_PAGE_TITLE: g_value_set_string (value, self->title); break; case PROP_APPLICATIONS: g_value_set_object (value, self->applications); break; case PROP_ALL_APPLICATIONS: g_value_set_object (value, self->all_applications); break; case PROP_CAROUSEL_APPLICATIONS: g_value_set_object (value, self->carousel_applications); break; case PROP_PAGE_SUBTITLE: g_value_set_string (value, self->subtitle); break; case PROP_CATEGORY: g_value_set_object (value, self->category); break; case PROP_FLATHUB_STATE: g_value_set_object (value, bz_state_info_get_flathub (bz_state_info_get_default ())); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bz_apps_page_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { BzAppsPage *self = BZ_APPS_PAGE (object); switch (prop_id) { case PROP_PAGE_TITLE: g_clear_pointer (&self->title, g_free); self->title = g_value_dup_string (value); break; case PROP_APPLICATIONS: g_clear_object (&self->applications); self->applications = g_value_dup_object (value); break; case PROP_ALL_APPLICATIONS: g_clear_object (&self->all_applications); self->all_applications = g_value_dup_object (value); break; case PROP_CAROUSEL_APPLICATIONS: g_clear_object (&self->carousel_applications); self->carousel_applications = g_value_dup_object (value); break; case PROP_PAGE_SUBTITLE: g_clear_pointer (&self->subtitle, g_free); self->subtitle = g_value_dup_string (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static gboolean is_not_null (gpointer object, GObject *value) { return value != NULL; } static gboolean is_not_empty_string (gpointer object, const char *str) { return str != NULL && str[0] != '\0'; } static gboolean is_not_empty_list (gpointer object, GListModel *list) { return list != NULL && g_list_model_get_n_items (list) > 0; } static gboolean is_scrolled_down (gpointer object, double value, gboolean carousel_visible) { return value > (carousel_visible ? 400.0 : 50.0); } static void featured_carousel_group_clicked_cb (BzAppsPage *self, BzEntryGroup *group, GtkWidget *carousel) { gtk_widget_activate_action (GTK_WIDGET (self), "window.show-group", "(sb)", bz_entry_group_get_id (group), TRUE); } static void show_all_cb (BzAppsPage *self, GtkButton *button) { GtkWidget *nav_view = NULL; AdwNavigationPage *all_page = NULL; g_autofree char *all_title = NULL; guint n_items = 0; g_return_if_fail (BZ_IS_APPS_PAGE (self)); if (self->all_applications == NULL) return; n_items = g_list_model_get_n_items (self->all_applications); if (n_items == 0) return; nav_view = gtk_widget_get_ancestor (GTK_WIDGET (self), ADW_TYPE_NAVIGATION_VIEW); if (nav_view == NULL) return; all_title = g_strdup_printf (_ ("All \"%s\""), self->title); all_page = bz_all_apps_page_new (all_title, self->all_applications); if (all_page == NULL) return; adw_navigation_view_push (ADW_NAVIGATION_VIEW (nav_view), all_page); } static void bz_apps_page_class_init (BzAppsPageClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->dispose = bz_apps_page_dispose; object_class->get_property = bz_apps_page_get_property; object_class->set_property = bz_apps_page_set_property; props[PROP_PAGE_TITLE] = g_param_spec_string ( "page-title", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); props[PROP_APPLICATIONS] = g_param_spec_object ( "applications", NULL, NULL, G_TYPE_LIST_MODEL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); props[PROP_ALL_APPLICATIONS] = g_param_spec_object ( "all-applications", NULL, NULL, G_TYPE_LIST_MODEL, G_PARAM_READWRITE); props[PROP_CAROUSEL_APPLICATIONS] = g_param_spec_object ( "carousel-applications", NULL, NULL, G_TYPE_LIST_MODEL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); props[PROP_PAGE_SUBTITLE] = g_param_spec_string ( "page-subtitle", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_CATEGORY] = g_param_spec_object ( "category", NULL, NULL, BZ_TYPE_FLATHUB_CATEGORY, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_FLATHUB_STATE] = g_param_spec_object ( "flathub-state", NULL, NULL, BZ_TYPE_FLATHUB_STATE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, LAST_PROP, props); g_type_ensure (BZ_TYPE_APP_TILE); g_type_ensure (BZ_TYPE_SUBCATEGORY_LIST); gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-apps-page.ui"); gtk_widget_class_bind_template_callback (widget_class, is_not_null); gtk_widget_class_bind_template_callback (widget_class, is_not_empty_string); gtk_widget_class_bind_template_callback (widget_class, is_not_empty_list); gtk_widget_class_bind_template_callback (widget_class, is_scrolled_down); gtk_widget_class_bind_template_callback (widget_class, featured_carousel_group_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, show_all_cb); } static void bz_apps_page_init (BzAppsPage *self) { gtk_widget_init_template (GTK_WIDGET (self)); } AdwNavigationPage * bz_apps_page_new (const char *title, GListModel *applications) { return bz_apps_page_new_with_carousel (title, applications, NULL); } AdwNavigationPage * bz_apps_page_new_with_carousel (const char *title, GListModel *applications, GListModel *carousel_applications) { BzAppsPage *apps_page = NULL; apps_page = g_object_new ( BZ_TYPE_APPS_PAGE, "page-title", title, "applications", applications, "carousel-applications", carousel_applications, NULL); adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (apps_page), title); return ADW_NAVIGATION_PAGE (apps_page); } static gboolean filter_by_category (BzEntryGroup *group, const char *category_name) { g_return_val_if_fail (BZ_IS_ENTRY_GROUP (group), FALSE); g_return_val_if_fail (category_name != NULL, FALSE); return bz_entry_group_has_category (group, category_name); } static DexFuture * filter_applications_fiber (BzAppsPage *self) { g_autoptr (GError) error = NULL; g_autoptr (GtkFilterListModel) filtered_model = NULL; GListModel *all_model = NULL; const char *category_name = NULL; BzStateInfo *state_info = NULL; state_info = bz_state_info_get_default (); if (state_info == NULL) return NULL; all_model = bz_state_info_get_filtered_entry_groups (state_info); if (all_model == NULL) return NULL; category_name = g_object_get_data (G_OBJECT (self), "category-name"); if (category_name == NULL) return NULL; filtered_model = gtk_filter_list_model_new ( g_object_ref (all_model), GTK_FILTER (gtk_custom_filter_new ( (GtkCustomFilterFunc) filter_by_category, g_strdup (category_name), g_free))); g_set_object (&self->all_applications, G_LIST_MODEL (filtered_model)); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALL_APPLICATIONS]); return NULL; } static AdwNavigationPage * create_split_page (const char *title, GListModel *model, GListModel *carousel_model) { AdwNavigationPage *apps_page = NULL; guint n_items = g_list_model_get_n_items (model); if (n_items > 48) { g_autoptr (GListModel) limited_model = NULL; limited_model = G_LIST_MODEL (gtk_slice_list_model_new ( g_object_ref (model), 0, 48)); if (carousel_model != NULL && g_list_model_get_n_items (carousel_model) > 0) apps_page = bz_apps_page_new_with_carousel (title, limited_model, carousel_model); else apps_page = bz_apps_page_new (title, limited_model); if (g_set_object (&BZ_APPS_PAGE (apps_page)->all_applications, model)) g_object_notify_by_pspec (G_OBJECT (apps_page), props[PROP_ALL_APPLICATIONS]); } else { if (carousel_model != NULL && g_list_model_get_n_items (carousel_model) > 0) apps_page = bz_apps_page_new_with_carousel (title, model, carousel_model); else apps_page = bz_apps_page_new (title, model); } return apps_page; } static AdwNavigationPage * create_standard_page (const char *title, GListModel *model, GListModel *carousel_model) { if (carousel_model != NULL && g_list_model_get_n_items (carousel_model) > 0) return bz_apps_page_new_with_carousel (title, model, carousel_model); else return bz_apps_page_new (title, model); } static void setup_category_filter (AdwNavigationPage *apps_page, const char *category_name) { if (apps_page == NULL || category_name == NULL || g_strcmp0 (category_name, "adwaita") == 0) return; g_object_set_data_full (G_OBJECT (apps_page), "category-name", g_strdup (category_name), g_free); dex_future_disown (dex_scheduler_spawn ( dex_scheduler_get_default (), bz_get_dex_stack_size (), (DexFiberFunc) filter_applications_fiber, g_object_ref (apps_page), g_object_unref)); } AdwNavigationPage * bz_apps_page_new_from_category (BzFlathubCategory *category) { g_autoptr (GListModel) model = NULL; g_autoptr (GListModel) carousel_model = NULL; AdwNavigationPage *apps_page = NULL; const char *title = NULL; const char *category_name = NULL; g_autofree char *subtitle = NULL; int total_entries = 0; guint n_items = 0; g_return_val_if_fail (BZ_IS_FLATHUB_CATEGORY (category), NULL); model = bz_flathub_category_dup_applications (category); if (model == NULL) return NULL; title = bz_flathub_category_get_display_name (category); category_name = bz_flathub_category_get_name (category); n_items = g_list_model_get_n_items (model); carousel_model = bz_flathub_category_dup_quality_applications (category); total_entries = bz_flathub_category_get_total_entries (category); if (n_items > 48) apps_page = create_split_page (title, model, carousel_model); else apps_page = create_standard_page (title, model, carousel_model); if (total_entries > 0 && !bz_flathub_category_get_is_spotlight (category)) { subtitle = g_strdup_printf (_ ("%d Applications"), total_entries); bz_apps_page_set_subtitle (BZ_APPS_PAGE (apps_page), subtitle); } BZ_APPS_PAGE (apps_page)->category = g_object_ref (category); g_object_notify_by_pspec (G_OBJECT (apps_page), props[PROP_CATEGORY]); if (n_items <= 48) setup_category_filter (apps_page, category_name); return apps_page; } void bz_apps_page_set_subtitle (BzAppsPage *self, const char *subtitle) { g_return_if_fail (BZ_IS_APPS_PAGE (self)); if (g_strcmp0 (self->subtitle, subtitle) == 0) return; g_clear_pointer (&self->subtitle, g_free); if (subtitle != NULL) self->subtitle = g_strdup (subtitle); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PAGE_SUBTITLE]); }