*
* 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
*/
#define G_LOG_DOMAIN "BAZAAR::FLATHUB"
#define COLLECTION_FETCH_SIZE 192
#define CATEGORY_FETCH_SIZE 48
#define QUALITY_MODERATION_PAGE_SIZE 300
#define KEYWORD_SEARCH_PAGE_SIZE 48
#include <json-glib/json-glib.h>
#include <libdex.h>
#include "bz-flathub-category.h"
#include "bz-flathub-curated-selection.h"
#include "bz-flathub-state.h"
#include "bz-serializable.h"
#include "env.h"
#include "global-net.h"
#include "io.h"
#include "util.h"
struct _BzFlathubState
{
GObject parent_instance;
char *for_day;
BzApplicationMapFactory *map_factory;
char *app_of_the_day;
GtkStringList *apps_of_the_week;
GListStore *categories;
GListStore *curated_selections;
DexFuture *initializing;
};
typedef enum
{
QUALITY_MODE_NONE,
QUALITY_MODE_FIRST,
QUALITY_MODE_RANDOM
} QualityMode;
static void
serializable_iface_init (BzSerializableInterface *iface);
G_DEFINE_FINAL_TYPE_WITH_CODE (
BzFlathubState,
bz_flathub_state,
G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (BZ_TYPE_SERIALIZABLE, serializable_iface_init))
static GListModel *bz_flathub_state_dup_apps_of_the_day_week (BzFlathubState *self);
enum
{
PROP_0,
PROP_FOR_DAY,
PROP_MAP_FACTORY,
PROP_APP_OF_THE_DAY,
PROP_APPS_OF_THE_WEEK,
PROP_APPS_OF_THE_DAY_WEEK,
PROP_CATEGORIES,
PROP_CURATED_SELECTIONS,
LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };
static DexFuture *
initialize_fiber (GWeakRef *wr);
static DexFuture *
initialize_finally (DexFuture *future,
GWeakRef *wr);
static void
notify_all (BzFlathubState *self);
static void
clear (BzFlathubState *self);
static void
bz_flathub_state_dispose (GObject *object)
{
BzFlathubState *self = BZ_FLATHUB_STATE (object);
dex_clear (&self->initializing);
g_clear_pointer (&self->map_factory, g_object_unref);
clear (self);
G_OBJECT_CLASS (bz_flathub_state_parent_class)->dispose (object);
}
static void
bz_flathub_state_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
BzFlathubState *self = BZ_FLATHUB_STATE (object);
switch (prop_id)
{
case PROP_FOR_DAY:
g_value_set_string (value, bz_flathub_state_get_for_day (self));
break;
case PROP_MAP_FACTORY:
g_value_set_object (value, bz_flathub_state_get_map_factory (self));
break;
case PROP_APP_OF_THE_DAY:
g_value_set_string (value, bz_flathub_state_get_app_of_the_day (self));
break;
case PROP_APPS_OF_THE_WEEK:
g_value_take_object (value, bz_flathub_state_dup_apps_of_the_week (self));
break;
case PROP_APPS_OF_THE_DAY_WEEK:
g_value_take_object (value, bz_flathub_state_dup_apps_of_the_day_week (self));
break;
case PROP_CATEGORIES:
g_value_set_object (value, bz_flathub_state_get_categories (self));
break;
case PROP_CURATED_SELECTIONS:
g_value_set_object (value, bz_flathub_state_get_curated_selections (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_flathub_state_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
BzFlathubState *self = BZ_FLATHUB_STATE (object);
switch (prop_id)
{
case PROP_FOR_DAY:
dex_future_disown (bz_flathub_state_set_for_day (self, g_value_get_string (value)));
break;
case PROP_MAP_FACTORY:
bz_flathub_state_set_map_factory (self, g_value_get_object (value));
break;
case PROP_APP_OF_THE_DAY:
case PROP_APPS_OF_THE_WEEK:
case PROP_APPS_OF_THE_DAY_WEEK:
case PROP_CATEGORIES:
case PROP_CURATED_SELECTIONS:
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_flathub_state_class_init (BzFlathubStateClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->set_property = bz_flathub_state_set_property;
object_class->get_property = bz_flathub_state_get_property;
object_class->dispose = bz_flathub_state_dispose;
props[PROP_FOR_DAY] =
g_param_spec_string (
"for-day",
NULL, NULL, NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_MAP_FACTORY] =
g_param_spec_object (
"map-factory",
NULL, NULL,
BZ_TYPE_APPLICATION_MAP_FACTORY,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_APP_OF_THE_DAY] =
g_param_spec_string (
"app-of-the-day",
NULL, NULL, NULL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_APPS_OF_THE_WEEK] =
g_param_spec_object (
"apps-of-the-week",
NULL, NULL,
G_TYPE_LIST_MODEL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_APPS_OF_THE_DAY_WEEK] =
g_param_spec_object (
"apps-of-the-day-week",
NULL, NULL,
G_TYPE_LIST_MODEL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_CATEGORIES] =
g_param_spec_object (
"categories",
NULL, NULL,
G_TYPE_LIST_MODEL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_CURATED_SELECTIONS] =
g_param_spec_object (
"curated-selections",
NULL, NULL,
G_TYPE_LIST_MODEL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, LAST_PROP, props);
}
static void
bz_flathub_state_init (BzFlathubState *self)
{
}
static void
bz_flathub_state_real_serialize (BzSerializable *serializable,
GVariantBuilder *builder)
{
BzFlathubState *self = BZ_FLATHUB_STATE (serializable);
if (self->initializing != NULL &&
dex_future_is_pending (self->initializing))
return;
if (self->for_day != NULL)
g_variant_builder_add (builder, "{sv}", "for-day", g_variant_new_string (self->for_day));
if (self->app_of_the_day != NULL)
g_variant_builder_add (builder, "{sv}", "app-of-the-day", g_variant_new_string (self->app_of_the_day));
if (self->apps_of_the_week != NULL)
{
guint n_items = 0;
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->apps_of_the_week));
if (n_items > 0)
{
g_autoptr (GVariantBuilder) sub_builder = NULL;
sub_builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
for (guint i = 0; i < n_items; i++)
{
const char *string = NULL;
string = gtk_string_list_get_string (self->apps_of_the_week, i);
g_variant_builder_add (sub_builder, "s", string);
}
g_variant_builder_add (builder, "{sv}", "apps-of-the-week", g_variant_builder_end (sub_builder));
}
}
if (self->categories != NULL)
{
guint n_items = 0;
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->categories));
if (n_items > 0)
{
g_autoptr (GVariantBuilder) sub_builder = NULL;
sub_builder = g_variant_builder_new (G_VARIANT_TYPE ("av"));
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzFlathubCategory) category = NULL;
g_autoptr (GVariantBuilder) category_builder = NULL;
category = g_list_model_get_item (G_LIST_MODEL (self->categories), i);
category_builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);
bz_serializable_serialize (BZ_SERIALIZABLE (category), category_builder);
g_variant_builder_add (sub_builder, "v", g_variant_builder_end (category_builder));
}
g_variant_builder_add (builder, "{sv}", "categories", g_variant_builder_end (sub_builder));
}
}
if (self->curated_selections != NULL)
{
guint n_items = 0;
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->curated_selections));
if (n_items > 0)
{
g_autoptr (GVariantBuilder) sub_builder = NULL;
sub_builder = g_variant_builder_new (G_VARIANT_TYPE ("av"));
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzFlathubCuratedSelection) selection = NULL;
g_autoptr (GVariantBuilder) selection_builder = NULL;
const char *theme_key = NULL;
const char *slot = NULL;
GtkStringList *apps = NULL;
selection = g_list_model_get_item (G_LIST_MODEL (self->curated_selections), i);
theme_key = bz_flathub_curated_selection_get_theme_key (selection);
slot = bz_flathub_curated_selection_get_slot (selection);
apps = bz_flathub_curated_selection_get_apps (selection);
selection_builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);
if (theme_key != NULL)
g_variant_builder_add (selection_builder, "{sv}", "theme-key", g_variant_new_string (theme_key));
if (slot != NULL)
g_variant_builder_add (selection_builder, "{sv}", "slot", g_variant_new_string (slot));
if (apps != NULL)
{
guint n_apps = 0;
n_apps = g_list_model_get_n_items (G_LIST_MODEL (apps));
if (n_apps > 0)
{
g_autoptr (GVariantBuilder) apps_builder = NULL;
apps_builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
for (guint j = 0; j < n_apps; j++)
{
const char *app_id = NULL;
app_id = gtk_string_list_get_string (apps, j);
g_variant_builder_add (apps_builder, "s", app_id);
}
g_variant_builder_add (selection_builder, "{sv}", "apps", g_variant_builder_end (apps_builder));
}
}
g_variant_builder_add (sub_builder, "v", g_variant_builder_end (selection_builder));
}
g_variant_builder_add (builder, "{sv}", "curated-selections", g_variant_builder_end (sub_builder));
}
}
}
static gboolean
bz_flathub_state_real_deserialize (BzSerializable *serializable,
GVariant *import,
GError **error)
{
BzFlathubState *self = BZ_FLATHUB_STATE (serializable);
gboolean result = FALSE;
g_autoptr (GVariantIter) iter = NULL;
if (self->initializing != NULL &&
!dex_future_is_pending (self->initializing))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_BUSY,
"Cannot perform serialization operations while initializing!");
return FALSE;
}
clear (self);
iter = g_variant_iter_new (import);
for (;;)
{
g_autofree char *key = NULL;
g_autoptr (GVariant) value = NULL;
if (!g_variant_iter_next (iter, "{sv}", &key, &value))
break;
if (g_strcmp0 (key, "for-day") == 0)
self->for_day = g_variant_dup_string (value, NULL);
else if (g_strcmp0 (key, "app-of-the-day") == 0)
self->app_of_the_day = g_variant_dup_string (value, NULL);
else if (g_strcmp0 (key, "apps-of-the-week") == 0)
{
g_autoptr (GtkStringList) list = NULL;
g_autoptr (GVariantIter) list_iter = NULL;
list = gtk_string_list_new (NULL);
list_iter = g_variant_iter_new (value);
for (;;)
{
g_autofree char *id = NULL;
if (!g_variant_iter_next (list_iter, "s", &id))
break;
gtk_string_list_append (list, id);
}
self->apps_of_the_week = g_steal_pointer (&list);
}
else if (g_strcmp0 (key, "categories") == 0)
{
g_autoptr (GListStore) categories = NULL;
g_autoptr (GVariantIter) categories_iter = NULL;
categories = g_list_store_new (BZ_TYPE_FLATHUB_CATEGORY);
categories_iter = g_variant_iter_new (value);
for (;;)
{
g_autoptr (GVariant) category_import = NULL;
g_autoptr (BzFlathubCategory) category = NULL;
if (!g_variant_iter_next (categories_iter, "v", &category_import))
break;
category = bz_flathub_category_new ();
result = bz_serializable_deserialize (
BZ_SERIALIZABLE (category), category_import, error);
if (!result)
return FALSE;
g_object_bind_property (self, "map-factory", category, "map-factory", G_BINDING_SYNC_CREATE);
g_list_store_append (categories, category);
}
self->categories = g_steal_pointer (&categories);
}
else if (g_strcmp0 (key, "curated-selections") == 0)
{
g_autoptr (GListStore) curated_selections = NULL;
g_autoptr (GVariantIter) curated_selections_iter = NULL;
curated_selections = g_list_store_new (BZ_TYPE_FLATHUB_CURATED_SELECTION);
curated_selections_iter = g_variant_iter_new (value);
for (;;)
{
g_autoptr (GVariant) selection_import = NULL;
g_autoptr (BzFlathubCuratedSelection) selection = NULL;
g_autoptr (GVariantIter) selection_iter = NULL;
g_autoptr (GtkStringList) apps = NULL;
g_autofree char *theme_key = NULL;
g_autofree char *slot = NULL;
if (!g_variant_iter_next (curated_selections_iter, "v", &selection_import))
break;
selection_iter = g_variant_iter_new (selection_import);
for (;;)
{
g_autofree char *sel_key = NULL;
g_autoptr (GVariant) sel_val = NULL;
if (!g_variant_iter_next (selection_iter, "{sv}", &sel_key, &sel_val))
break;
if (g_strcmp0 (sel_key, "theme-key") == 0)
theme_key = g_variant_dup_string (sel_val, NULL);
else if (g_strcmp0 (sel_key, "slot") == 0)
slot = g_variant_dup_string (sel_val, NULL);
else if (g_strcmp0 (sel_key, "apps") == 0)
{
g_autoptr (GVariantIter) apps_iter = NULL;
apps = gtk_string_list_new (NULL);
apps_iter = g_variant_iter_new (sel_val);
for (;;)
{
g_autofree char *app_id = NULL;
if (!g_variant_iter_next (apps_iter, "s", &app_id))
break;
gtk_string_list_append (apps, app_id);
}
}
}
selection = bz_flathub_curated_selection_new ();
if (theme_key != NULL)
bz_flathub_curated_selection_set_theme_key (selection, theme_key);
if (slot != NULL)
bz_flathub_curated_selection_set_slot (selection, slot);
if (apps != NULL)
bz_flathub_curated_selection_set_apps (selection, apps);
g_object_bind_property (self, "map-factory", selection, "map-factory", G_BINDING_SYNC_CREATE);
g_list_store_append (curated_selections, selection);
}
self->curated_selections = g_steal_pointer (&curated_selections);
}
}
notify_all (self);
return TRUE;
}
static void
serializable_iface_init (BzSerializableInterface *iface)
{
iface->serialize = bz_flathub_state_real_serialize;
iface->deserialize = bz_flathub_state_real_deserialize;
}
BzFlathubState *
bz_flathub_state_new (void)
{
return g_object_new (BZ_TYPE_FLATHUB_STATE, NULL);
}
const char *
bz_flathub_state_get_for_day (BzFlathubState *self)
{
g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
return self->for_day;
}
BzApplicationMapFactory *
bz_flathub_state_get_map_factory (BzFlathubState *self)
{
g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
return self->map_factory;
}
const char *
bz_flathub_state_get_app_of_the_day (BzFlathubState *self)
{
g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
if (self->initializing != NULL &&
dex_future_is_pending (self->initializing))
return NULL;
return self->app_of_the_day;
}
BzEntryGroup *
bz_flathub_state_dup_app_of_the_day_group (BzFlathubState *self)
{
g_autoptr (GtkStringObject) string = NULL;
g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
if (self->initializing != NULL &&
dex_future_is_pending (self->initializing))
return NULL;
g_return_val_if_fail (self->map_factory != NULL, NULL);
string = gtk_string_object_new (self->app_of_the_day);
return bz_application_map_factory_convert_one (self->map_factory, string);
}
GListModel *
bz_flathub_state_dup_apps_of_the_week (BzFlathubState *self)
{
g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
if (self->initializing != NULL &&
dex_future_is_pending (self->initializing))
return NULL;
if (self->apps_of_the_week != NULL)
{
if (self->map_factory != NULL)
return bz_application_map_factory_generate (
self->map_factory, G_LIST_MODEL (self->apps_of_the_week));
else
return G_LIST_MODEL (g_object_ref (self->apps_of_the_week));
}
else
return NULL;
}
GListModel *
bz_flathub_state_dup_apps_of_the_day_week (BzFlathubState *self)
{
g_autoptr (GtkStringList) combined_list = NULL;
g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
if (self->initializing != NULL &&
dex_future_is_pending (self->initializing))
return NULL;
combined_list = gtk_string_list_new (NULL);
if (self->app_of_the_day != NULL)
gtk_string_list_append (combined_list, self->app_of_the_day);
if (self->apps_of_the_week != NULL)
{
guint n_items = g_list_model_get_n_items (G_LIST_MODEL (self->apps_of_the_week));
for (guint i = 0; i < n_items; i++)
{
const char *app_id = gtk_string_list_get_string (self->apps_of_the_week, i);
gtk_string_list_append (combined_list, app_id);
}
}
if (self->map_factory != NULL)
return bz_application_map_factory_generate (self->map_factory, G_LIST_MODEL (combined_list));
else
return G_LIST_MODEL (g_object_ref (combined_list));
}
GListModel *
bz_flathub_state_get_categories (BzFlathubState *self)
{
g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
if (self->initializing != NULL &&
dex_future_is_pending (self->initializing))
return NULL;
return G_LIST_MODEL (self->categories);
}
GListModel *
bz_flathub_state_get_curated_selections (BzFlathubState *self)
{
g_return_val_if_fail (BZ_IS_FLATHUB_STATE (self), NULL);
if (self->initializing != NULL &&
dex_future_is_pending (self->initializing))
return NULL;
return G_LIST_MODEL (self->curated_selections);
}
DexFuture *
bz_flathub_state_set_for_day (BzFlathubState *self,
const char *for_day)
{
dex_return_error_if_fail (BZ_IS_FLATHUB_STATE (self));
dex_clear (&self->initializing);
clear (self);
if (for_day != NULL)
{
g_autoptr (DexFuture) future = NULL;
self->for_day = g_strdup (for_day);
self->apps_of_the_week = gtk_string_list_new (NULL);
self->categories = g_list_store_new (BZ_TYPE_FLATHUB_CATEGORY);
self->curated_selections = g_list_store_new (BZ_TYPE_FLATHUB_CURATED_SELECTION);
future = dex_scheduler_spawn (
bz_get_io_scheduler (),
bz_get_dex_stack_size (),
(DexFiberFunc) initialize_fiber,
bz_track_weak (self), bz_weak_release);
future = dex_future_finally (
future,
(DexFutureCallback) initialize_finally,
bz_track_weak (self), bz_weak_release);
self->initializing = g_steal_pointer (&future);
return dex_ref (self->initializing);
}
else
{
notify_all (self);
return dex_future_new_false ();
}
}
DexFuture *
bz_flathub_state_update_to_today (BzFlathubState *self)
{
g_autoptr (GDateTime) datetime = NULL;
g_autofree gchar *for_day = NULL;
dex_return_error_if_fail (BZ_IS_FLATHUB_STATE (self));
datetime = g_date_time_new_now_utc ();
for_day = g_date_time_format (datetime, "%F");
g_debug ("Syncing with flathub for day: %s", for_day);
return bz_flathub_state_set_for_day (self, for_day);
}
void
bz_flathub_state_set_map_factory (BzFlathubState *self,
BzApplicationMapFactory *map_factory)
{
g_return_if_fail (BZ_IS_FLATHUB_STATE (self));
g_return_if_fail (map_factory == NULL || BZ_IS_APPLICATION_MAP_FACTORY (map_factory));
g_clear_object (&self->map_factory);
if (map_factory != NULL)
self->map_factory = g_object_ref (map_factory);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MAP_FACTORY]);
}
static const char *
get_de (void)
{
const char *desktop = NULL;
desktop = g_getenv ("XDG_CURRENT_DESKTOP");
if (desktop == NULL)
return "adwaita";
if (g_strstr_len (desktop, -1, "KDE") != NULL)
return "kde";
if (g_strstr_len (desktop, -1, "COSMIC") != NULL)
return "cosmic";
return "adwaita";
}
static void
add_category (BzFlathubState *self,
const char *name,
JsonNode *node,
GHashTable *quality_set,
gboolean is_toplevel_json,
QualityMode quality_mode,
gboolean is_spotlight)
{
JsonObject *object = NULL;
JsonArray *hits_array = NULL;
JsonArray *toplevel_array = NULL;
const char *app = NULL;
g_autoptr (BzFlathubCategory) category = NULL;
g_autoptr (GtkStringList) store = NULL;
g_autoptr (GtkStringList) quality_store = NULL;
g_autoptr (GPtrArray) quality_apps = NULL;
guint app_count = 0;
guint quality_count = 0;
guint random_index = 0;
guint i = 0;
int total_entries = 0;
category = bz_flathub_category_new ();
store = gtk_string_list_new (NULL);
quality_store = gtk_string_list_new (NULL);
bz_flathub_category_set_name (category, name);
bz_flathub_category_set_is_spotlight (category, is_spotlight);
bz_flathub_category_set_applications (category, G_LIST_MODEL (store));
if (quality_mode == QUALITY_MODE_RANDOM)
quality_apps = g_ptr_array_new_with_free_func (g_free);
if (is_toplevel_json)
{
toplevel_array = json_node_get_array (node);
app_count = json_array_get_length (toplevel_array);
for (i = 0; i < app_count; i++)
{
const char *app_id = NULL;
app_id = json_array_get_string_element (toplevel_array, i);
gtk_string_list_append (store, app_id);
if (g_hash_table_contains (quality_set, app_id))
{
if (quality_mode == QUALITY_MODE_RANDOM)
g_ptr_array_add (quality_apps, g_strdup (app_id));
else if (quality_mode == QUALITY_MODE_FIRST)
gtk_string_list_append (quality_store, app_id);
}
}
total_entries = app_count;
}
else
{
object = json_node_get_object (node);
hits_array = json_object_get_array_member (object, "hits");
app_count = json_array_get_length (hits_array);
for (i = 0; i < app_count; i++)
{
JsonObject *element = NULL;
const char *app_id = NULL;
element = json_array_get_object_element (hits_array, i);
app_id = json_object_get_string_member (element, "app_id");
gtk_string_list_append (store, app_id);
if (g_hash_table_contains (quality_set, app_id))
{
if (quality_mode == QUALITY_MODE_RANDOM)
g_ptr_array_add (quality_apps, g_strdup (app_id));
else if (quality_mode == QUALITY_MODE_FIRST)
gtk_string_list_append (quality_store, app_id);
}
}
total_entries = json_object_get_int_member (object, "totalHits");
}
if (quality_mode == QUALITY_MODE_RANDOM && quality_apps != NULL)
{
quality_count = MIN (7, quality_apps->len);
for (i = 0; i < quality_count; i++)
{
random_index = g_random_int_range (0, quality_apps->len);
app = g_ptr_array_index (quality_apps, random_index);
gtk_string_list_append (quality_store, app);
g_ptr_array_remove_index_fast (quality_apps, random_index);
}
}
bz_flathub_category_set_total_entries (category, total_entries);
bz_flathub_category_set_quality_applications (category, G_LIST_MODEL (quality_store));
g_list_store_append (self->categories, category);
}
static void
add_curated_selection (BzFlathubState *self,
JsonObject *object)
{
g_autoptr (BzFlathubCuratedSelection) selection = NULL;
g_autoptr (GtkStringList) apps = NULL;
JsonArray *apps_array = NULL;
guint length = 0;
selection = bz_flathub_curated_selection_new ();
apps = gtk_string_list_new (NULL);
bz_flathub_curated_selection_set_theme_key (selection, json_object_get_string_member (object, "theme_key"));
bz_flathub_curated_selection_set_slot (selection, json_object_get_string_member (object, "slot"));
apps_array = json_object_get_array_member (object, "apps");
length = apps_array != NULL ? json_array_get_length (apps_array) : 0;
for (guint i = 0; i < length; i++)
{
JsonObject *element = NULL;
const char *app_id = NULL;
element = json_array_get_object_element (apps_array, i);
app_id = json_object_get_string_member (element, "app_id");
gtk_string_list_append (apps, app_id);
}
bz_flathub_curated_selection_set_apps (selection, apps);
g_list_store_append (self->curated_selections, selection);
}
static DexFuture *
initialize_fiber (GWeakRef *wr)
{
g_autoptr (BzFlathubState) self = NULL;
g_autoptr (GError) local_error = NULL;
gboolean result = FALSE;
const char *de = get_de ();
g_autoptr (GHashTable) quality_set = NULL;
g_autoptr (DexFuture) aotd_f = NULL;
g_autoptr (DexFuture) aotw_f = NULL;
g_autoptr (DexFuture) curated_f = NULL;
g_autoptr (DexFuture) categories_f = NULL;
g_autoptr (DexFuture) updated_f = NULL;
g_autoptr (DexFuture) added_f = NULL;
g_autoptr (DexFuture) popular_f = NULL;
g_autoptr (DexFuture) trending_f = NULL;
g_autoptr (DexFuture) mobile_f = NULL;
g_autoptr (DexFuture) passing_f = NULL;
g_autoptr (DexFuture) toolkit_f = NULL;
g_autoptr (DexFuture) emulators_f = NULL;
g_autoptr (DexFuture) launchers_f = NULL;
g_autoptr (DexFuture) game_tools_f = NULL;
g_autoptr (DexFuture) game_only_f = NULL;
bz_weak_get_or_return_reject (self, wr);
quality_set = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
#define ADD_REQUEST(_var, ...) \
G_STMT_START \
{ \
g_autofree char *_request = NULL; \
\
_request = g_strdup_printf (__VA_ARGS__); \
(_var) = bz_query_flathub_v2_json_take (g_steal_pointer (&_request)); \
if (!dex_await (dex_ref ((_var)), &local_error)) \
{ \
g_warning ("Failed to complete request to flathub: %s", local_error->message); \
return dex_future_new_for_error (g_steal_pointer (&local_error)); \
} \
} \
G_STMT_END
{
g_autofree char *made_for_route = NULL;
made_for_route = g_strdup_printf ("/made-for/%s", de);
toolkit_f = bz_query_bazaar_json (made_for_route);
if (!dex_await (dex_ref (toolkit_f), &local_error))
{
g_warning ("Failed to complete request to bazaar %s: %s", made_for_route, local_error->message);
g_clear_error (&local_error);
dex_clear (&toolkit_f);
}
}
ADD_REQUEST (passing_f, "/quality-moderation/passing-apps?page=1&page_size=%d", QUALITY_MODERATION_PAGE_SIZE);
ADD_REQUEST (aotd_f, "/app-picks/app-of-the-day/%s", self->for_day);
ADD_REQUEST (aotw_f, "/app-picks/apps-of-the-week/%s", self->for_day);
ADD_REQUEST (curated_f, "/app-picks/curated-app-selections/%s", self->for_day);
ADD_REQUEST (categories_f, "/collection/category");
ADD_REQUEST (updated_f, "/collection/recently-updated?page=0&per_page=%d", COLLECTION_FETCH_SIZE);
ADD_REQUEST (added_f, "/collection/recently-added?page=0&per_page=%d", COLLECTION_FETCH_SIZE);
ADD_REQUEST (popular_f, "/collection/popular?page=0&per_page=%d", COLLECTION_FETCH_SIZE);
ADD_REQUEST (trending_f, "/collection/trending?page=0&per_page=%d", COLLECTION_FETCH_SIZE);
ADD_REQUEST (mobile_f, "/collection/mobile?sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
ADD_REQUEST (game_only_f, "/collection/category/game?exclude_subcategories=emulator&exclude_subcategories=packageManager&exclude_subcategories=utility&exclude_subcategories=network&exclude_subcategories=gameTool&exclude_subcategories=launcherStore&sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
ADD_REQUEST (emulators_f, "/collection/category/game/subcategories?subcategory=emulator&sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
ADD_REQUEST (launchers_f, "/collection/category/game/subcategories?subcategory=packageManager&subcategory=launcherStore&sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
ADD_REQUEST (game_tools_f, "/collection/category/game/subcategories?subcategory=utility&subcategory=network&subcategory=gameTool&sort_by=trending&page=0&per_page=%d", CATEGORY_FETCH_SIZE);
#undef ADD_REQUEST
#define GET_BOXED(_future) g_value_get_boxed (dex_future_get_value ((_future), NULL))
{
JsonObject *object = NULL;
JsonArray *array = NULL;
guint length = 0;
object = json_node_get_object (GET_BOXED (passing_f));
array = json_object_get_array_member (object, "apps");
length = json_array_get_length (array);
for (guint i = 0; i < length; i++)
{
const char *app_id = NULL;
app_id = json_array_get_string_element (array, i);
g_hash_table_replace (quality_set, g_strdup (app_id), NULL);
}
}
{
JsonObject *object = NULL;
object = json_node_get_object (GET_BOXED (aotd_f));
self->app_of_the_day = g_strdup (json_object_get_string_member (object, "app_id"));
}
{
JsonObject *root = NULL;
JsonArray *array = NULL;
guint length = 0;
root = json_node_get_object (GET_BOXED (curated_f));
array = json_object_get_array_member (root, "selections");
length = json_array_get_length (array);
for (guint i = 0; i < length; i++)
add_curated_selection (self, json_array_get_object_element (array, i));
}
{
JsonObject *object = NULL;
JsonArray *array = NULL;
guint length = 0;
object = json_node_get_object (GET_BOXED (aotw_f));
array = json_object_get_array_member (object, "apps");
length = json_array_get_length (array);
for (guint i = 0; i < length; i++)
{
JsonObject *element = NULL;
element = json_array_get_object_element (array, i);
gtk_string_list_append (
self->apps_of_the_week,
json_object_get_string_member (element, "app_id"));
}
}
add_category (self, "trending", GET_BOXED (trending_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
add_category (self, "popular", GET_BOXED (popular_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
add_category (self, "recently-added", GET_BOXED (added_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
add_category (self, "recently-updated", GET_BOXED (updated_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
add_category (self, "mobile", GET_BOXED (mobile_f), quality_set, FALSE, QUALITY_MODE_NONE, TRUE);
add_category (self, "game-only", GET_BOXED (game_only_f), quality_set, FALSE, QUALITY_MODE_NONE, FALSE);
add_category (self, "emulators", GET_BOXED (emulators_f), quality_set, FALSE, QUALITY_MODE_NONE, FALSE);
add_category (self, "launchers", GET_BOXED (launchers_f), quality_set, FALSE, QUALITY_MODE_NONE, FALSE);
add_category (self, "game-tools", GET_BOXED (game_tools_f), quality_set, FALSE, QUALITY_MODE_NONE, FALSE);
{
static const char *categories[] = {
"audiovideo", "development", "education", "game", "graphics",
"network", "office", "science", "system", "utility"
};
g_autoptr (GPtrArray) category_futures = NULL;
category_futures = g_ptr_array_new_with_free_func (dex_unref);
for (guint i = 0; i < G_N_ELEMENTS (categories); i++)
{
g_autofree char *request = NULL;
g_autoptr (DexFuture) future = NULL;
request = g_strdup_printf (
"/collection/category/%s?sort_by=trending&page=0&per_page=%d",
categories[i], CATEGORY_FETCH_SIZE);
future = bz_query_flathub_v2_json_take (g_steal_pointer (&request));
result = dex_await (dex_ref (future), &local_error);
if (!result)
{
g_warning ("Failed to complete request to flathub: %s", local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
g_ptr_array_add (category_futures, dex_ref (future));
}
for (guint i = 0; i < G_N_ELEMENTS (categories); i++)
{
DexFuture *future = NULL;
JsonNode *node = NULL;
future = g_ptr_array_index (category_futures, i);
node = GET_BOXED (future);
add_category (self, categories[i], node, quality_set, FALSE, QUALITY_MODE_FIRST, FALSE);
}
}
if (toolkit_f != NULL)
add_category (self, de, GET_BOXED (toolkit_f), quality_set, TRUE, QUALITY_MODE_RANDOM, FALSE);
return dex_future_new_true ();
}
static DexFuture *
initialize_finally (DexFuture *future,
GWeakRef *wr)
{
g_autoptr (BzFlathubState) self = NULL;
bz_weak_get_or_return_reject (self, wr);
if (dex_future_is_resolved (future))
{
guint n_categories = 0;
guint n_selections = 0;
n_categories = g_list_model_get_n_items (G_LIST_MODEL (self->categories));
for (guint i = 0; i < n_categories; i++)
{
g_autoptr (BzFlathubCategory) category = NULL;
category = g_list_model_get_item (G_LIST_MODEL (self->categories), i);
g_object_bind_property (self, "map-factory", category, "map-factory", G_BINDING_SYNC_CREATE);
}
n_selections = g_list_model_get_n_items (G_LIST_MODEL (self->curated_selections));
for (guint i = 0; i < n_selections; i++)
{
g_autoptr (BzFlathubCuratedSelection) selection = NULL;
selection = g_list_model_get_item (G_LIST_MODEL (self->curated_selections), i);
g_object_bind_property (self, "map-factory", selection, "map-factory", G_BINDING_SYNC_CREATE);
}
g_debug ("Done syncing flathub state; notifying property listeners...");
notify_all (self);
}
else
clear (self);
return dex_ref (future);
}
static DexFuture *
search_collection_fiber (char *route)
{
g_autoptr (GError) local_error = NULL;
g_autoptr (GtkStringList) results = NULL;
g_autoptr (JsonNode) node = NULL;
g_autofree char *request = NULL;
JsonObject *object = NULL;
JsonArray *array = NULL;
guint length = 0;
request = g_strdup_printf ("%s&page=1&per_page=%d&locale=en",
route, KEYWORD_SEARCH_PAGE_SIZE);
node = dex_await_boxed (
bz_query_flathub_v2_json_take (
g_steal_pointer (&request)),
&local_error);
if (node == NULL)
return dex_future_new_for_error (g_steal_pointer (&local_error));
results = gtk_string_list_new (NULL);
object = json_node_get_object (node);
array = json_object_get_array_member (object, "hits");
length = json_array_get_length (array);
for (guint i = 0; i < length; i++)
{
JsonObject *element = NULL;
const char *app_id = NULL;
element = json_array_get_object_element (array, i);
app_id = json_object_get_string_member (element, "app_id");
gtk_string_list_append (results, app_id);
}
return dex_future_new_take_object (g_steal_pointer (&results));
}
static DexFuture *
search_collection_finally (DexFuture *future,
GWeakRef *wr)
{
g_autoptr (BzFlathubState) self = NULL;
const GValue *value = NULL;
GListModel *model = NULL;
bz_weak_get_or_return_reject (self, wr);
value = dex_future_get_value (future, NULL);
if (value == NULL)
return dex_ref (future);
model = g_value_get_object (value);
if (self->map_factory != NULL)
return dex_future_new_take_object (
bz_application_map_factory_generate (self->map_factory, model));
return dex_ref (future);
}
DexFuture *
bz_flathub_state_search_collection (BzFlathubState *self,
const char *route)
{
g_autoptr (DexFuture) future = NULL;
dex_return_error_if_fail (BZ_IS_FLATHUB_STATE (self));
dex_return_error_if_fail (route != NULL);
future = dex_scheduler_spawn (
bz_get_io_scheduler (),
bz_get_dex_stack_size (),
(DexFiberFunc) search_collection_fiber,
g_strdup (route),
g_free);
future = dex_future_finally (
future,
(DexFutureCallback) search_collection_finally,
bz_track_weak (self),
bz_weak_release);
return g_steal_pointer (&future);
}
static void
notify_all (BzFlathubState *self)
{
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FOR_DAY]);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APP_OF_THE_DAY]);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APPS_OF_THE_WEEK]);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APPS_OF_THE_DAY_WEEK]);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CATEGORIES]);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CURATED_SELECTIONS]);
}
static void
clear (BzFlathubState *self)
{
g_clear_pointer (&self->for_day, g_free);
g_clear_pointer (&self->app_of_the_day, g_free);
g_clear_pointer (&self->apps_of_the_week, g_object_unref);
g_clear_pointer (&self->categories, g_object_unref);
g_clear_pointer (&self->curated_selections, g_object_unref);
}