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

/* bz-age-rating-dialog.c
 *
 * Copyright 2021 Endless OS Foundation LLC
 * Copyright 2025 Alexander Vanhee
 *
 * Author: Philip Withnall <pwithnall@endlessos.org> (GNOME Software)
 * Adapted for Bazaar by 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 <appstream.h>
#include <glib/gi18n.h>
#include <locale.h>

#include "bz-age-rating-attribute.h"
#include "bz-age-rating-dialog.h"
#include "bz-context-row.h"
#include "bz-lozenge.h"

struct _BzAgeRatingDialog
{
  AdwDialog parent_instance;

  BzEntry    *entry;
  BzLozenge  *lozenge;
  GtkListBox *list;
};

G_DEFINE_FINAL_TYPE (BzAgeRatingDialog, bz_age_rating_dialog, ADW_TYPE_DIALOG)

enum
{
  PROP_0,
  PROP_ENTRY,
  LAST_PROP
};

static GParamSpec *props[LAST_PROP] = { NULL };

typedef enum
{
  BZ_AGE_RATING_GROUP_TYPE_DRUGS,
  BZ_AGE_RATING_GROUP_TYPE_LANGUAGE,
  BZ_AGE_RATING_GROUP_TYPE_MONEY,
  BZ_AGE_RATING_GROUP_TYPE_SEX,
  BZ_AGE_RATING_GROUP_TYPE_SOCIAL,
  BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
} BzAgeRatingGroupType;

#define BZ_AGE_RATING_GROUP_TYPE_COUNT (BZ_AGE_RATING_GROUP_TYPE_VIOLENCE + 1)

typedef struct
{
  GList *attributes;
} BzAgeRatingGroup;

typedef void (*AttributeCallback) (const gchar         *attribute,
                                   AsContentRatingValue value,
                                   gpointer             user_data);

static const struct
{
  const gchar         *id;
  BzAgeRatingGroupType group_type;
  const gchar         *title;
  const gchar         *unknown_description;
  const gchar         *icon_name;
  const gchar         *icon_name_negative;
} attribute_details[] = {
  {        "violence-cartoon", BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Cartoon Violence"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding cartoon violence"),
   "violence-symbolic",              "violence-none-symbolic"                },
  {        "violence-fantasy", BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Fantasy Violence"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding fantasy violence"),
   "violence-symbolic",              "violence-none-symbolic"                },
  {      "violence-realistic", BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Realistic Violence"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding realistic violence"),
   "violence-symbolic",              "violence-none-symbolic"                },
  {      "violence-bloodshed", BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Violence Depicting Bloodshed"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding bloodshed"),
   "violence-symbolic",              "violence-none-symbolic"                },
  {         "violence-sexual", BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Sexual Violence"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding sexual violence"),
   "violence-symbolic",              "violence-none-symbolic"                },
  {           "drugs-alcohol",    BZ_AGE_RATING_GROUP_TYPE_DRUGS,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Alcohol"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding references to alcohol"),
   "alcohol-use-symbolic",           "alcohol-use-none-symbolic"             },
  {         "drugs-narcotics",    BZ_AGE_RATING_GROUP_TYPE_DRUGS,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Narcotics"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding references to illicit drugs"),
   "drug-use-symbolic",              "drug-use-none-symbolic"                },
  {           "drugs-tobacco",    BZ_AGE_RATING_GROUP_TYPE_DRUGS,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Tobacco"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding references to tobacco products"),
   "smoking-symbolic",               "smoking-none-symbolic"                 },
  {              "sex-nudity",      BZ_AGE_RATING_GROUP_TYPE_SEX,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Nudity"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding nudity of any sort"),
   "nudity-symbolic",                "nudity-none-symbolic"                  },
  {              "sex-themes",      BZ_AGE_RATING_GROUP_TYPE_SEX,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Sexual Themes"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding references to or depictions of sexual nature"),
   "nudity-symbolic",                "nudity-none-symbolic"                  },
  {      "language-profanity", BZ_AGE_RATING_GROUP_TYPE_LANGUAGE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Profanity"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding profanity of any kind"),
   "strong-language-symbolic",       "strong-language-none-symbolic"         },
  {          "language-humor", BZ_AGE_RATING_GROUP_TYPE_LANGUAGE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Inappropriate Humor"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding inappropriate humor"),
   "strong-language-symbolic",       "strong-language-none-symbolic"         },
  { "language-discrimination",   BZ_AGE_RATING_GROUP_TYPE_SOCIAL,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Discrimination"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding discriminatory language of any kind"),
   "strong-language-symbolic",       "strong-language-none-symbolic"         },
  {       "money-advertising",    BZ_AGE_RATING_GROUP_TYPE_MONEY,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Advertising"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding advertising of any kind"),
   "advertising-symbolic",           "advertising-none-symbolic"             },
  {          "money-gambling",    BZ_AGE_RATING_GROUP_TYPE_MONEY,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Gambling"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding gambling of any kind"),
   "gambling-symbolic",              "gambling-none-symbolic"                },
  {        "money-purchasing",    BZ_AGE_RATING_GROUP_TYPE_MONEY,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Purchasing"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding the ability to spend money"),
   "money-symbolic",                 "money-none-symbolic"                   },
  {             "social-chat",   BZ_AGE_RATING_GROUP_TYPE_SOCIAL,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Chat Between Users"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding ways to chat with other users"),
   "messaging-symbolic",             "messaging-none-symbolic"               },
  {            "social-audio",   BZ_AGE_RATING_GROUP_TYPE_SOCIAL,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Audio Chat Between Users"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding ways to talk with other users"),
   "audio-chat-symbolic",            "audio-chat-none-symbolic"              },
  {         "social-contacts",   BZ_AGE_RATING_GROUP_TYPE_SOCIAL,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Contact Details"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding sharing of social network usernames or email addresses"),
   "contacts-symbolic",                                  NULL                },
  {             "social-info",   BZ_AGE_RATING_GROUP_TYPE_SOCIAL,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Identifying Information"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding sharing of user information with third parties"),
   "social-info-symbolic",                                  NULL             },
  {         "social-location",   BZ_AGE_RATING_GROUP_TYPE_SOCIAL,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Location Sharing"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding sharing of physical location with other users"),
   "location-services-active-symbolic", "location-services-disabled-symbolic" },
  {        "sex-prostitution",      BZ_AGE_RATING_GROUP_TYPE_SEX,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Prostitution"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding references to prostitution"),
   "nudity-symbolic",                "nudity-none-symbolic"                  },
  {            "sex-adultery",      BZ_AGE_RATING_GROUP_TYPE_SEX,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Adultery"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding references to adultery"),
   "nudity-symbolic",                "nudity-none-symbolic"                  },
  {          "sex-appearance",      BZ_AGE_RATING_GROUP_TYPE_SEX,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Sexualized Characters"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding sexualized characters"),
   "nudity-symbolic",                "nudity-none-symbolic"                  },
  {        "violence-worship", BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Desecration"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding references to desecration"),
   "violence-symbolic",              "violence-none-symbolic"                },
  {    "violence-desecration", BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Human Remains"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding visible dead human remains"),
   "human-remains-symbolic",                                  NULL           },
  {        "violence-slavery", BZ_AGE_RATING_GROUP_TYPE_VIOLENCE,
   /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */
   N_ ("Slavery"),
   /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */
   N_ ("No information regarding references to slavery"),
   "violence-symbolic",              "violence-none-symbolic"                },
};

static const gchar         *content_rating_attribute_get_icon_name (const gchar *attribute,
                                                                    gboolean     negative_version);
static const gchar         *content_rating_attribute_get_title (const gchar *attribute);
static const gchar         *content_rating_attribute_get_unknown_description (const gchar *attribute);
static BzAgeRatingGroupType content_rating_attribute_get_group_type (const gchar *attribute);
static const gchar         *content_rating_group_get_description (BzAgeRatingGroupType group_type);
static const gchar         *content_rating_group_get_icon_name (BzAgeRatingGroupType group_type,
                                                                gboolean             negative_version);
static const gchar         *content_rating_group_get_title (BzAgeRatingGroupType group_type);
static BzImportance         content_rating_value_get_importance (AsContentRatingValue value);
static gint                 attributes_compare (BzAgeRatingAttribute *attr1,
                                                BzAgeRatingAttribute *attr2);
static void                 collect_attribute (const gchar         *attribute,
                                               AsContentRatingValue value,
                                               gpointer             user_data);
static void                 process_attributes (AsContentRating  *content_rating,
                                                gboolean          show_worst_only,
                                                AttributeCallback callback,
                                                gpointer          user_data);
static gchar               *format_age_short (AsContentRatingSystem system,
                                              guint                 age);
static void                 update_lozenge (BzAgeRatingDialog *self,
                                            AsContentRating   *content_rating);
static void                 update_list (BzAgeRatingDialog *self);

static void
bz_age_rating_dialog_set_property (GObject      *object,
                                   guint         prop_id,
                                   const GValue *value,
                                   GParamSpec   *pspec)
{
  BzAgeRatingDialog *self = NULL;

  self = BZ_AGE_RATING_DIALOG (object);

  switch (prop_id)
    {
    case PROP_ENTRY:
      self->entry = g_value_dup_object (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bz_age_rating_dialog_constructed (GObject *object)
{
  BzAgeRatingDialog *self = NULL;

  self = BZ_AGE_RATING_DIALOG (object);

  G_OBJECT_CLASS (bz_age_rating_dialog_parent_class)->constructed (object);

  if (self->entry != NULL)
    update_list (self);
}

static void
bz_age_rating_dialog_dispose (GObject *object)
{
  BzAgeRatingDialog *self = NULL;

  self = BZ_AGE_RATING_DIALOG (object);

  g_clear_object (&self->entry);

  gtk_widget_dispose_template (GTK_WIDGET (self), BZ_TYPE_AGE_RATING_DIALOG);
  G_OBJECT_CLASS (bz_age_rating_dialog_parent_class)->dispose (object);
}

static void
bz_age_rating_dialog_class_init (BzAgeRatingDialogClass *klass)
{
  GObjectClass   *object_class = NULL;
  GtkWidgetClass *widget_class = NULL;

  object_class = G_OBJECT_CLASS (klass);
  widget_class = GTK_WIDGET_CLASS (klass);

  object_class->set_property = bz_age_rating_dialog_set_property;
  object_class->constructed  = bz_age_rating_dialog_constructed;
  object_class->dispose      = bz_age_rating_dialog_dispose;

  props[PROP_ENTRY] =
      g_param_spec_object (
          "entry",
          NULL, NULL,
          BZ_TYPE_ENTRY,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, LAST_PROP, props);

  g_type_ensure (BZ_TYPE_LOZENGE);

  gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-age-rating-dialog.ui");

  gtk_widget_class_bind_template_child (widget_class, BzAgeRatingDialog, lozenge);
  gtk_widget_class_bind_template_child (widget_class, BzAgeRatingDialog, list);
}

static void
bz_age_rating_dialog_init (BzAgeRatingDialog *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));
}

BzAgeRatingDialog *
bz_age_rating_dialog_new (BzEntry *entry)
{
  return g_object_new (BZ_TYPE_AGE_RATING_DIALOG,
                       "entry", entry,
                       NULL);
}

static const gchar *
content_rating_attribute_get_icon_name (const gchar *attribute,
                                        gboolean     negative_version)
{
  for (gsize i = 0; i < G_N_ELEMENTS (attribute_details); i++)
    {
      if (g_str_equal (attribute, attribute_details[i].id))
        {
          if (negative_version && attribute_details[i].icon_name_negative != NULL)
            return attribute_details[i].icon_name_negative;
          return attribute_details[i].icon_name;
        }
    }

  g_assert_not_reached ();
}

static const gchar *
content_rating_attribute_get_title (const gchar *attribute)
{
  for (gsize i = 0; i < G_N_ELEMENTS (attribute_details); i++)
    {
      if (g_str_equal (attribute, attribute_details[i].id))
        return _ (attribute_details[i].title);
    }

  g_assert_not_reached ();
}

static const gchar *
content_rating_attribute_get_unknown_description (const gchar *attribute)
{
  for (gsize i = 0; i < G_N_ELEMENTS (attribute_details); i++)
    {
      if (g_str_equal (attribute, attribute_details[i].id))
        return _ (attribute_details[i].unknown_description);
    }

  g_assert_not_reached ();
}

static BzAgeRatingGroupType
content_rating_attribute_get_group_type (const gchar *attribute)
{
  for (gsize i = 0; i < G_N_ELEMENTS (attribute_details); i++)
    {
      if (g_str_equal (attribute, attribute_details[i].id))
        return attribute_details[i].group_type;
    }

  g_assert_not_reached ();
}

static const gchar *
content_rating_group_get_description (BzAgeRatingGroupType group_type)
{
  switch (group_type)
    {
    case BZ_AGE_RATING_GROUP_TYPE_DRUGS:
      return _ ("Does not include references to drugs");
    case BZ_AGE_RATING_GROUP_TYPE_LANGUAGE:
      return _ ("Does not include swearing, profanity, and other kinds of strong language");
    case BZ_AGE_RATING_GROUP_TYPE_MONEY:
      return _ ("Does not include ads or monetary transactions");
    case BZ_AGE_RATING_GROUP_TYPE_SEX:
      return _ ("Does not include sex or nudity");
    case BZ_AGE_RATING_GROUP_TYPE_SOCIAL:
      return _ ("Does not include uncontrolled chat functionality");
    case BZ_AGE_RATING_GROUP_TYPE_VIOLENCE:
      return _ ("Does not include violence");
    default:
      g_assert_not_reached ();
    }
}

static const gchar *
content_rating_group_get_icon_name (BzAgeRatingGroupType group_type,
                                    gboolean             negative_version)
{
  switch (group_type)
    {
    case BZ_AGE_RATING_GROUP_TYPE_DRUGS:
      return negative_version ? "smoking-none-symbolic" : "smoking-symbolic";
    case BZ_AGE_RATING_GROUP_TYPE_LANGUAGE:
      return negative_version ? "strong-language-none-symbolic" : "strong-language-symbolic";
    case BZ_AGE_RATING_GROUP_TYPE_MONEY:
      return negative_version ? "money-none-symbolic" : "money-symbolic";
    case BZ_AGE_RATING_GROUP_TYPE_SEX:
      return negative_version ? "nudity-none-symbolic" : "nudity-symbolic";
    case BZ_AGE_RATING_GROUP_TYPE_SOCIAL:
      return negative_version ? "messaging-none-symbolic" : "messaging-symbolic";
    case BZ_AGE_RATING_GROUP_TYPE_VIOLENCE:
      return negative_version ? "violence-none-symbolic" : "violence-symbolic";
    default:
      g_assert_not_reached ();
    }
}

static const gchar *
content_rating_group_get_title (BzAgeRatingGroupType group_type)
{
  switch (group_type)
    {
    case BZ_AGE_RATING_GROUP_TYPE_DRUGS:
      return _ ("Drugs");
    case BZ_AGE_RATING_GROUP_TYPE_LANGUAGE:
      return _ ("Strong Language");
    case BZ_AGE_RATING_GROUP_TYPE_MONEY:
      return _ ("Money");
    case BZ_AGE_RATING_GROUP_TYPE_SEX:
      return _ ("Nudity");
    case BZ_AGE_RATING_GROUP_TYPE_SOCIAL:
      return _ ("Social");
    case BZ_AGE_RATING_GROUP_TYPE_VIOLENCE:
      return _ ("Violence");
    default:
      g_assert_not_reached ();
    }
}

static BzImportance
content_rating_value_get_importance (AsContentRatingValue value)
{
  switch (value)
    {
    case AS_CONTENT_RATING_VALUE_NONE:
      return BZ_IMPORTANCE_UNIMPORTANT;
    case AS_CONTENT_RATING_VALUE_UNKNOWN:
      return BZ_IMPORTANCE_NEUTRAL;
    case AS_CONTENT_RATING_VALUE_MILD:
      return BZ_IMPORTANCE_INFORMATION;
    case AS_CONTENT_RATING_VALUE_MODERATE:
      return BZ_IMPORTANCE_WARNING;
    case AS_CONTENT_RATING_VALUE_INTENSE:
      return BZ_IMPORTANCE_IMPORTANT;
    case AS_CONTENT_RATING_VALUE_LAST:
    default:
      return BZ_IMPORTANCE_NEUTRAL;
    }
}

static gint
attributes_compare (BzAgeRatingAttribute *attr1,
                    BzAgeRatingAttribute *attr2)
{
  BzImportance importance1 = 0;
  BzImportance importance2 = 0;
  const gchar *id1         = NULL;
  const gchar *id2         = NULL;

  importance1 = bz_age_rating_attribute_get_importance (attr1);
  importance2 = bz_age_rating_attribute_get_importance (attr2);

  if (importance1 != importance2)
    {
      if (importance1 == BZ_IMPORTANCE_NEUTRAL &&
          importance2 == BZ_IMPORTANCE_UNIMPORTANT)
        return -1;
      if (importance1 == BZ_IMPORTANCE_UNIMPORTANT &&
          importance2 == BZ_IMPORTANCE_NEUTRAL)
        return 1;

      return importance2 - importance1;
    }
  else
    {
      id1 = bz_age_rating_attribute_get_id (attr1);
      id2 = bz_age_rating_attribute_get_id (attr2);
      return g_strcmp0 (id1, id2);
    }
}

static void
collect_attribute (const gchar         *attribute,
                   AsContentRatingValue value,
                   gpointer             user_data)
{
  BzAgeRatingGroup     *groups      = NULL;
  BzAgeRatingGroupType  group_type  = 0;
  BzImportance          rating      = 0;
  const gchar          *icon_name   = NULL;
  const gchar          *title       = NULL;
  const gchar          *description = NULL;
  BzAgeRatingAttribute *attr        = NULL;

  groups     = user_data;
  group_type = content_rating_attribute_get_group_type (attribute);
  rating     = content_rating_value_get_importance (value);
  icon_name  = content_rating_attribute_get_icon_name (attribute, value == AS_CONTENT_RATING_VALUE_NONE);
  title      = content_rating_attribute_get_title (attribute);

  if (value == AS_CONTENT_RATING_VALUE_UNKNOWN)
    description = content_rating_attribute_get_unknown_description (attribute);
  else
    description = as_content_rating_attribute_get_description (attribute, value);

  attr                          = g_object_new (BZ_TYPE_AGE_RATING_ATTRIBUTE,
                                                "id", attribute,
                                                "icon-name", icon_name,
                                                "importance", rating,
                                                "title", title,
                                                "description", description,
                                                NULL);
  groups[group_type].attributes = g_list_insert_sorted (groups[group_type].attributes,
                                                        attr,
                                                        (GCompareFunc) attributes_compare);
}

static AsContentRatingValue
get_rating_value (AsContentRating *content_rating,
                  const gchar     *id)
{
  if (content_rating == NULL)
    return AS_CONTENT_RATING_VALUE_UNKNOWN;

  return as_content_rating_get_value (content_rating, id);
}

static void
process_attributes (AsContentRating  *content_rating,
                    gboolean          show_worst_only,
                    AttributeCallback callback,
                    gpointer          user_data)
{
  g_autofree const gchar **rating_ids   = NULL;
  AsContentRatingValue     value_bad    = AS_CONTENT_RATING_VALUE_NONE;
  guint                    age_bad      = 0;
  guint                    rating_age   = 0;
  AsContentRatingValue     rating_value = 0;

  const gchar *const violence_group[] = {
    "violence-bloodshed",
    "violence-realistic",
    "violence-fantasy",
    "violence-cartoon",
    NULL
  };
  const gchar *const social_group[] = {
    "social-audio",
    "social-chat",
    "social-contacts",
    "social-info",
    NULL
  };
  const gchar *const coalesce_groups[] = {
    "sex-themes",
    "sex-homosexuality",
    NULL
  };

  rating_ids = as_content_rating_get_all_rating_ids ();

  for (gsize i = 0; rating_ids[i] != NULL; i++)
    {
      rating_value = get_rating_value (content_rating, rating_ids[i]);
      rating_age   = as_content_rating_attribute_to_csm_age (rating_ids[i], rating_value);

      if (rating_age > age_bad)
        age_bad = rating_age;
      if (rating_value > value_bad)
        value_bad = rating_value;
    }

  if (show_worst_only && (value_bad == AS_CONTENT_RATING_VALUE_NONE || age_bad == 0))
    {
      callback (NULL, AS_CONTENT_RATING_VALUE_UNKNOWN, user_data);
      return;
    }

  for (gsize i = 0; rating_ids[i] != NULL; i++)
    {
      if (g_strv_contains (violence_group, rating_ids[i]) ||
          g_strv_contains (social_group, rating_ids[i]))
        continue;

      rating_value = get_rating_value (content_rating, rating_ids[i]);
      rating_age   = as_content_rating_attribute_to_csm_age (rating_ids[i], rating_value);

      if (show_worst_only && rating_age < age_bad)
        continue;

      if (g_strv_contains (coalesce_groups + 1, rating_ids[i]) &&
          as_content_rating_attribute_to_csm_age (coalesce_groups[0],
                                                  get_rating_value (content_rating,
                                                                    coalesce_groups[0])) >= rating_age)
        continue;

      callback (rating_ids[i], rating_value, user_data);
    }

  for (gsize i = 0; violence_group[i] != NULL; i++)
    {
      rating_value = get_rating_value (content_rating, violence_group[i]);
      rating_age   = as_content_rating_attribute_to_csm_age (violence_group[i], rating_value);

      if (show_worst_only && rating_age < age_bad)
        continue;

      callback (violence_group[i], rating_value, user_data);
    }

  for (gsize i = 0; social_group[i] != NULL; i++)
    {
      rating_value = get_rating_value (content_rating, social_group[i]);
      rating_age   = as_content_rating_attribute_to_csm_age (social_group[i], rating_value);

      if (show_worst_only && rating_age < age_bad)
        continue;

      callback (social_group[i], rating_value, user_data);
    }
}

static gchar *
format_age_short (AsContentRatingSystem system,
                  guint                 age)
{
  if (age < 3)
    age = 3;

  /* Translators: Age rating format, e.g. "12+" for ages 12 and up */
  return g_strdup_printf (_ ("%d+"), age);
}

static void
update_lozenge (BzAgeRatingDialog *self,
                AsContentRating   *content_rating)
{
  const gchar          *locale     = NULL;
  AsContentRatingSystem system     = 0;
  guint                 age        = G_MAXUINT;
  g_autofree gchar     *age_text   = NULL;
  g_autofree gchar     *title_text = NULL;
  g_autofree gchar     *a11y_desc  = NULL;
  BzImportance          importance = BZ_IMPORTANCE_NEUTRAL;
  gboolean              is_unknown = FALSE;

  locale = setlocale (LC_MESSAGES, NULL);
  system = as_content_rating_system_from_locale (locale);

  if (content_rating != NULL)
    age = as_content_rating_get_minimum_age (content_rating);

  if (age != G_MAXUINT)
    age_text = format_age_short (system, age);

  if (content_rating != NULL && age_text == NULL && age == 0)
    age_text = g_strdup (C_ ("Age rating", "All"));

  if (age_text == NULL ||
      (content_rating != NULL &&
       g_strcmp0 (as_content_rating_get_kind (content_rating), "oars-1.0") != 0 &&
       g_strcmp0 (as_content_rating_get_kind (content_rating), "oars-1.1") != 0))
    {
      g_clear_pointer (&age_text, g_free);
      age_text   = g_strdup ("?");
      importance = BZ_IMPORTANCE_NEUTRAL;
    }
  else
    {
      if (age >= 18)
        importance = BZ_IMPORTANCE_IMPORTANT;
      else if (age >= 15)
        importance = BZ_IMPORTANCE_WARNING;
      else if (age >= 12)
        importance = BZ_IMPORTANCE_INFORMATION;
      else
        importance = BZ_IMPORTANCE_NEUTRAL;
    }

  if (self->entry == NULL)
    {
      title_text = g_strdup (_ ("Age Rating"));
    }
  else
    {
      is_unknown = (content_rating == NULL ||
                    (g_strcmp0 (as_content_rating_get_kind (content_rating), "oars-1.0") != 0 &&
                     g_strcmp0 (as_content_rating_get_kind (content_rating), "oars-1.1") != 0) ||
                    age == G_MAXUINT);

      if (is_unknown)
        {
          title_text = g_strdup_printf (_ ("%s has an unknown age rating"),
                                        bz_entry_get_title (self->entry));
        }
      else
        {
          if (age <= 3)
            title_text = g_strdup_printf (_ ("%s is suitable for everyone"),
                                          bz_entry_get_title (self->entry));
          else if (age <= 5)
            title_text = g_strdup_printf (_ ("%s is suitable for young children"),
                                          bz_entry_get_title (self->entry));
          else if (age <= 12)
            title_text = g_strdup_printf (_ ("%s is suitable for children"),
                                          bz_entry_get_title (self->entry));
          else if (age <= 18)
            title_text = g_strdup_printf (_ ("%s is suitable for teenagers"),
                                          bz_entry_get_title (self->entry));
          else if (age < G_MAXUINT)
            title_text = g_strdup_printf (_ ("%s is suitable for adults"),
                                          bz_entry_get_title (self->entry));
          else
            title_text = g_strdup_printf (_ ("%s is suitable for %s"),
                                          bz_entry_get_title (self->entry),
                                          age_text);
        }
    }

  bz_lozenge_set_label (self->lozenge, age_text);
  bz_lozenge_set_title (self->lozenge, title_text);
  bz_lozenge_set_importance (self->lozenge, importance);

  a11y_desc = g_strdup_printf ("%s. %s", age_text, title_text);
  gtk_accessible_update_property (GTK_ACCESSIBLE (self),
                                  GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, a11y_desc,
                                  -1);
}

static void
update_list (BzAgeRatingDialog *self)
{
  AsContentRating      *content_rating                                  = NULL;
  BzAgeRatingGroup      groups[BZ_AGE_RATING_GROUP_TYPE_COUNT]          = { 0 };
  guint                 attr_count                                      = 0;
  BzAgeRatingAttribute *attr                                            = NULL;
  AdwActionRow         *row                                             = NULL;
  BzImportance          max_importance                                  = 0;
  BzImportance          attr_importance                                 = 0;
  BzImportance          row_importances[BZ_AGE_RATING_GROUP_TYPE_COUNT] = { 0 };
  GtkWidget            *rows[BZ_AGE_RATING_GROUP_TYPE_COUNT]            = { NULL };
  const gchar          *group_icon                                      = NULL;
  const gchar          *group_title                                     = NULL;
  const gchar          *group_description                               = NULL;
  const gchar          *attr_description                                = NULL;
  g_autofree gchar     *description                                     = NULL;
  g_autoptr (GList) l                                                   = NULL;
  g_autofree gchar *tmp                                                 = NULL;

  content_rating = bz_entry_get_content_rating (self->entry);
  update_lozenge (self, content_rating);

  process_attributes (content_rating, FALSE, collect_attribute, groups);

  for (gsize i = 0; i < BZ_AGE_RATING_GROUP_TYPE_COUNT; i++)
    {
      if (groups[i].attributes == NULL)
        continue;

      attr_count = g_list_length (groups[i].attributes);
      row        = NULL;

      if (attr_count == 1)
        {
          attr               = (BzAgeRatingAttribute *) groups[i].attributes->data;
          row                = bz_context_row_new (bz_age_rating_attribute_get_icon_name (attr),
                                                   bz_age_rating_attribute_get_importance (attr),
                                                   bz_age_rating_attribute_get_title (attr),
                                                   bz_age_rating_attribute_get_description (attr));
          row_importances[i] = bz_age_rating_attribute_get_importance (attr);
        }
      else
        {
          max_importance = BZ_IMPORTANCE_UNIMPORTANT;

          for (l = groups[i].attributes; l != NULL; l = l->next)
            {
              attr            = (BzAgeRatingAttribute *) l->data;
              attr_importance = bz_age_rating_attribute_get_importance (attr);
              if (attr_importance > max_importance)
                max_importance = attr_importance;
            }

          if (max_importance == BZ_IMPORTANCE_UNIMPORTANT)
            {
              group_icon        = content_rating_group_get_icon_name (i, TRUE);
              group_title       = content_rating_group_get_title (i);
              group_description = content_rating_group_get_description (i);
              row               = bz_context_row_new (group_icon, BZ_IMPORTANCE_UNIMPORTANT, group_title, group_description);
            }
          else
            {
              group_icon  = content_rating_group_get_icon_name (i, FALSE);
              group_title = content_rating_group_get_title (i);
              g_clear_pointer (&description, g_free);

              for (l = groups[i].attributes; l != NULL; l = l->next)
                {
                  attr            = (BzAgeRatingAttribute *) l->data;
                  attr_importance = bz_age_rating_attribute_get_importance (attr);

                  if (attr_importance == BZ_IMPORTANCE_UNIMPORTANT)
                    continue;

                  attr_description = bz_age_rating_attribute_get_description (attr);

                  if (description == NULL)
                    {
                      description = g_strdup (attr_description);
                    }
                  else
                    {
                      tmp = g_strdup_printf (_ ("%s • %s"), description, attr_description);
                      g_clear_pointer (&description, g_free);
                      description = g_steal_pointer (&tmp);
                    }
                }

              row = bz_context_row_new (group_icon, max_importance, group_title, description);
            }

          row_importances[i] = max_importance;
        }

      rows[i] = GTK_WIDGET (row);
      g_list_free_full (g_steal_pointer (&groups[i].attributes), g_object_unref);
    }

  for (gint level = BZ_IMPORTANCE_IMPORTANT; level >= BZ_IMPORTANCE_UNIMPORTANT; level--)
    {
      for (gsize i = 0; i < BZ_AGE_RATING_GROUP_TYPE_COUNT; i++)
        {
          if (rows[i] == NULL || row_importances[i] != level)
            continue;

          gtk_list_box_append (self->list, rows[i]);
        }
    }
}