diff --git a/src/appstream.h b/src/appstream.h index 90b71a45..e20c24af 100644 --- a/src/appstream.h +++ b/src/appstream.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include diff --git a/src/as-component.c b/src/as-component.c index fdd34a07..1dff64da 100644 --- a/src/as-component.c +++ b/src/as-component.c @@ -32,6 +32,7 @@ #include "as-context-private.h" #include "as-icon-private.h" #include "as-screenshot-private.h" +#include "as-promotional-private.h" #include "as-bundle-private.h" #include "as-release-list-private.h" #include "as-translation-private.h" @@ -95,6 +96,7 @@ typedef struct { GPtrArray *extends; /* of utf8 */ GPtrArray *addons; /* of AsComponent */ GPtrArray *screenshots; /* of AsScreenshot elements */ + GPtrArray *promotionals; /* of AsPromotional elements */ GPtrArray *provided; /* of AsProvided */ GPtrArray *bundles; /* of AsBundle */ GPtrArray *suggestions; /* of AsSuggested elements */ @@ -426,6 +428,7 @@ as_component_init (AsComponent *cpt) priv->categories = g_ptr_array_new_with_free_func (g_free); priv->compulsory_for_desktops = g_ptr_array_new_with_free_func (g_free); priv->screenshots = g_ptr_array_new_with_free_func (g_object_unref); + priv->promotionals = g_ptr_array_new_with_free_func (g_object_unref); priv->provided = g_ptr_array_new_with_free_func (g_object_unref); priv->bundles = g_ptr_array_new_with_free_func (g_object_unref); priv->extends = g_ptr_array_new_with_free_func (g_free); @@ -489,6 +492,7 @@ as_component_finalize (GObject *object) g_ptr_array_unref (priv->compulsory_for_desktops); g_ptr_array_unref (priv->screenshots); + g_ptr_array_unref (priv->promotionals); g_ptr_array_unref (priv->provided); g_ptr_array_unref (priv->bundles); g_ptr_array_unref (priv->extends); @@ -626,6 +630,37 @@ as_component_add_screenshot (AsComponent *cpt, AsScreenshot *sshot) g_ptr_array_add (priv->screenshots, g_object_ref (sshot)); } +/** + * as_component_add_promotional: + * @cpt: a #AsComponent instance. + * @promotional: The #AsPromotional to add + * + * Add an #AsPromotional to this component. + **/ +void +as_component_add_promotional (AsComponent *cpt, AsPromotional *promotional) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + g_return_if_fail (promotional != NULL); + + g_ptr_array_add (priv->promotionals, g_object_ref (promotional)); +} + +/** + * as_component_get_promotionals: + * @cpt: a #AsComponent instance. + * + * Get a list of all associated promotional images/videos. + * + * Returns: (element-type AsPromotional) (transfer none): an array of #AsPromotional instances + */ +GPtrArray * +as_component_get_promotionals (AsComponent *cpt) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + return priv->promotionals; +} + /** * as_component_get_releases_plain: * @cpt: a #AsComponent instance. @@ -4752,6 +4787,22 @@ as_component_load_from_xml (AsComponent *cpt, AsContext *ctx, xmlNode *node, GEr as_component_add_reference (cpt, reference); } + } else if (tag_id == AS_TAG_PROMOTIONALS) { + xmlNode *iter2; + + for (iter2 = iter->children; iter2 != NULL; iter2 = iter2->next) { + if (iter2->type != XML_ELEMENT_NODE) + continue; + if (g_strcmp0 ((const gchar *) iter2->name, "promotional") == 0) { + g_autoptr(AsPromotional) promotional = as_promotional_new (); + if (as_promotional_load_from_xml (promotional, + ctx, + iter2, + NULL)) + as_component_add_promotional (cpt, promotional); + } + } + } else if (as_context_get_internal_mode (ctx)) { g_autofree gchar *content = as_xml_get_node_value (iter); /* internal information */ @@ -5210,6 +5261,17 @@ as_component_to_xml_node (AsComponent *cpt, AsContext *ctx, xmlNode *root) } } + /* promotionals */ + if (priv->promotionals->len > 0) { + xmlNode *pnode = as_xml_add_node (cnode, "promotionals"); + + for (guint i = 0; i < priv->promotionals->len; i++) { + AsPromotional *promo = AS_PROMOTIONAL ( + g_ptr_array_index (priv->promotionals, i)); + as_promotional_to_xml_node (promo, ctx, pnode); + } + } + /* custom node */ as_xml_add_custom_node (cnode, "custom", priv->custom); @@ -5790,6 +5852,13 @@ as_component_load_from_yaml (AsComponent *cpt, AsContext *ctx, struct fy_node *r as_component_add_reference (cpt, reference); } + } else if (field_id == AS_TAG_PROMOTIONALS) { + AS_YAML_SEQUENCE_FOREACH (n, value_n) { + g_autoptr(AsPromotional) promotional = as_promotional_new (); + if (as_promotional_load_from_yaml (promotional, ctx, n, NULL)) + as_component_add_promotional (cpt, promotional); + } + } else if (field_id == AS_TAG_CUSTOM) { as_component_yaml_parse_custom (cpt, value_n); } else if (field_id == AS_TAG_REVIEWS) { @@ -6448,6 +6517,20 @@ as_component_emit_yaml (AsComponent *cpt, AsContext *ctx, struct fy_emitter *emi as_yaml_sequence_end (emitter); } + /* Promotionals */ + if (priv->promotionals->len > 0) { + as_yaml_emit_scalar (emitter, "Promotionals"); + as_yaml_sequence_start (emitter); + + for (guint i = 0; i < priv->promotionals->len; i++) { + AsPromotional *promo = AS_PROMOTIONAL ( + g_ptr_array_index (priv->promotionals, i)); + as_promotional_emit_yaml (promo, ctx, emitter); + } + + as_yaml_sequence_end (emitter); + } + /* Custom fields */ as_component_yaml_emit_custom (cpt, emitter); diff --git a/src/as-component.h b/src/as-component.h index 2ebb36dc..7fa7a708 100644 --- a/src/as-component.h +++ b/src/as-component.h @@ -31,6 +31,7 @@ #include "as-provided.h" #include "as-icon.h" #include "as-screenshot.h" +#include "as-promotional.h" #include "as-release-list.h" #include "as-developer.h" #include "as-translation.h" @@ -263,6 +264,9 @@ void as_component_sort_screenshots (AsComponent *cpt, const gchar *style, gboolean prioritize_style); +GPtrArray *as_component_get_promotionals (AsComponent *cpt); +void as_component_add_promotional (AsComponent *cpt, AsPromotional *promotional); + GPtrArray *as_component_get_keywords (AsComponent *cpt); void as_component_set_keywords (AsComponent *cpt, GPtrArray *new_keywords, diff --git a/src/as-promotional-private.h b/src/as-promotional-private.h new file mode 100644 index 00000000..b4ee7ffd --- /dev/null +++ b/src/as-promotional-private.h @@ -0,0 +1,53 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2024 Matthias Klumpp + * + * Licensed under the GNU Lesser General Public License Version 2.1 + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the license, or + * (at your option) any later version. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#ifndef __AS_PROMOTIONAL_PRIVATE_H +#define __AS_PROMOTIONAL_PRIVATE_H + +#include "as-macros-private.h" +#include "as-promotional.h" +#include "as-xml.h" +#include "as-yaml.h" + +AS_BEGIN_PRIVATE_DECLS + +AS_INTERNAL_VISIBLE +void as_promotional_set_context_locale (AsPromotional *promotional, const gchar *locale); + +gboolean as_promotional_load_from_xml (AsPromotional *promotional, + AsContext *ctx, + xmlNode *node, + GError **error); +void as_promotional_to_xml_node (AsPromotional *promotional, AsContext *ctx, xmlNode *root); + +gboolean as_promotional_load_from_yaml (AsPromotional *promotional, + AsContext *ctx, + struct fy_node *node, + GError **error); +void as_promotional_emit_yaml (AsPromotional *promotional, + AsContext *ctx, + struct fy_emitter *emitter); + +gint as_promotional_get_position (AsPromotional *promotional); +void as_promotional_set_position (AsPromotional *promotional, gint pos); + +AS_END_PRIVATE_DECLS + +#endif /* __AS_PROMOTIONAL_PRIVATE_H */ diff --git a/src/as-promotional.c b/src/as-promotional.c new file mode 100644 index 00000000..f1d45614 --- /dev/null +++ b/src/as-promotional.c @@ -0,0 +1,802 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2024 Matthias Klumpp + * + * Licensed under the GNU Lesser General Public License Version 2.1 + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the license, or + * (at your option) any later version. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +/** + * SECTION:as-promotional + * @short_description: Object representing a single promotional banner + * + * Promotional banners are hero or banner images and videos used in + * application stores and other contexts to present an application + * attractively. They are distinct from screenshots in that they are + * not required to be actual screen captures. + * + * The @contains_text attribute must be set explicitly to indicate whether + * the promotional media contains text integral to understanding it. Its + * absence triggers a validator warning. + * + * See also: #AsImage, #AsVideo, #AsScreenshot + */ + +#include "as-promotional.h" +#include "as-promotional-private.h" + +#include "as-utils.h" +#include "as-utils-private.h" +#include "as-context-private.h" +#include "as-image-private.h" +#include "as-video-private.h" + +typedef struct { + AsPromotionalMediaKind media_kind; + AsPromotionalContainsText contains_text; + GRefString *environment; + GHashTable *caption; + + GPtrArray *images; + GPtrArray *images_lang; + GPtrArray *videos; + GPtrArray *videos_lang; + + gint position; + AsContext *context; +} AsPromotionalPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (AsPromotional, as_promotional, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (as_promotional_get_instance_private (o)) + +/** + * as_promotional_init: + **/ +static void +as_promotional_init (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + + priv->position = -1; + priv->media_kind = AS_PROMOTIONAL_MEDIA_KIND_IMAGE; + priv->contains_text = AS_PROMOTIONAL_CONTAINS_TEXT_UNKNOWN; + priv->caption = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) as_ref_string_release, + g_free); + priv->images = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->images_lang = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->videos = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->videos_lang = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); +} + +/** + * as_promotional_finalize: + **/ +static void +as_promotional_finalize (GObject *object) +{ + AsPromotional *promotional = AS_PROMOTIONAL (object); + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + + g_ptr_array_unref (priv->images); + g_ptr_array_unref (priv->images_lang); + g_ptr_array_unref (priv->videos); + g_ptr_array_unref (priv->videos_lang); + g_hash_table_unref (priv->caption); + as_ref_string_release (priv->environment); + if (priv->context != NULL) + g_object_unref (priv->context); + + G_OBJECT_CLASS (as_promotional_parent_class)->finalize (object); +} + +/** + * as_promotional_class_init: + **/ +static void +as_promotional_class_init (AsPromotionalClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = as_promotional_finalize; +} + +/** + * as_promotional_get_media_kind: + * @promotional: a #AsPromotional instance. + * + * Gets the promotional media kind. + * + * Returns: a #AsPromotionalMediaKind + **/ +AsPromotionalMediaKind +as_promotional_get_media_kind (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return priv->media_kind; +} + +/** + * as_promotional_get_contains_text: + * @promotional: a #AsPromotional instance. + * + * Gets whether this promotional item contains text that is integral + * to understanding it. + * + * Returns: a #AsPromotionalContainsText + **/ +AsPromotionalContainsText +as_promotional_get_contains_text (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return priv->contains_text; +} + +/** + * as_promotional_set_contains_text: + * @promotional: a #AsPromotional instance. + * @contains_text: the #AsPromotionalContainsText value. + * + * Sets whether this promotional item contains text integral to understanding it. + * This attribute must be set explicitly; its absence triggers a validator warning. + **/ +void +as_promotional_set_contains_text (AsPromotional *promotional, + AsPromotionalContainsText contains_text) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + priv->contains_text = contains_text; +} + +/** + * as_promotional_get_environment: + * @promotional: a #AsPromotional instance. + * + * Get the GUI environment ID associated with this promotional, if any. + * E.g. "plasma-mobile" or "gnome:dark". + * + * Returns: (nullable): The GUI environment ID, or %NULL if none set. + **/ +const gchar * +as_promotional_get_environment (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return priv->environment; +} + +/** + * as_promotional_set_environment: + * @promotional: a #AsPromotional instance. + * @env_id: (nullable): the GUI environment ID, e.g. "plasma-mobile" or "gnome:dark" + * + * Sets the GUI environment ID of this promotional. + **/ +void +as_promotional_set_environment (AsPromotional *promotional, const gchar *env_id) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + as_ref_string_assign_safe (&priv->environment, env_id); +} + +/** + * as_promotional_get_active_locale: + * + * Get the current active locale, which is used to get localized messages. + */ +static const gchar * +as_promotional_get_active_locale (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + const gchar *locale; + + /* ensure we have a context */ + if (priv->context == NULL) { + g_autoptr(AsContext) context = as_context_new (); + as_promotional_set_context (promotional, context); + } + + locale = as_context_get_locale (priv->context); + if (locale == NULL) + return "C"; + else + return locale; +} + +/** + * as_promotional_get_images: + * @promotional: a #AsPromotional instance. + * + * Gets the images for this promotional. Only images valid for the current + * language are returned. + * + * Returns: (transfer none) (element-type AsImage): an array + **/ +GPtrArray * +as_promotional_get_images (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + if (priv->images_lang->len == 0) + return as_promotional_get_images_all (promotional); + return priv->images_lang; +} + +/** + * as_promotional_get_image: + * @promotional: a #AsPromotional instance. + * @width: target width + * @height: target height + * @scale: the target scaling factor. + * + * Gets the AsImage closest to the target size. The #AsImage may not actually + * be the requested size. Only images for the current active locale (or fallback) + * are considered. + * + * Returns: (transfer none) (nullable): an #AsImage, or %NULL + **/ +AsImage * +as_promotional_get_image (AsPromotional *promotional, guint width, guint height, guint scale) +{ + AsImage *im_best = NULL; + gint64 best_size = G_MAXINT64; + GPtrArray *images; + + g_return_val_if_fail (AS_IS_PROMOTIONAL (promotional), NULL); + g_return_val_if_fail (scale >= 1, NULL); + + images = as_promotional_get_images (promotional); + for (guint current_scale = scale; current_scale > 0; current_scale--) { + guint64 scaled_width; + guint64 scaled_height; + + scaled_width = (guint64) width * current_scale; + scaled_height = (guint64) height * current_scale; + + for (guint i = 0; i < images->len; i++) { + gint64 tmp; + AsImage *im = g_ptr_array_index (images, i); + guint im_scale = as_image_get_scale (im); + + if (im_scale != current_scale) + continue; + + tmp = ABS ((gint64) (scaled_width * scaled_height) - + (gint64) (as_image_get_width (im) * as_image_get_height (im))); + if (tmp < best_size) { + best_size = tmp; + im_best = im; + } + } + } + + return im_best; +} + +/** + * as_promotional_add_image: + * @promotional: a #AsPromotional instance. + * @image: a #AsImage instance. + * + * Adds an image to the promotional. + **/ +void +as_promotional_add_image (AsPromotional *promotional, AsImage *image) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + g_ptr_array_add (priv->images, g_object_ref (image)); + + if (as_utils_locale_is_compatible (as_image_get_locale (image), + as_promotional_get_active_locale (promotional))) + g_ptr_array_add (priv->images_lang, g_object_ref (image)); +} + +/** + * as_promotional_clear_images: + * @promotional: a #AsPromotional instance. + * + * Remove all images associated with this promotional. + **/ +void +as_promotional_clear_images (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + + g_ptr_array_remove_range (priv->images, 0, priv->images->len); + g_ptr_array_remove_range (priv->images_lang, 0, priv->images_lang->len); +} + +/** + * as_promotional_get_videos_all: + * @promotional: a #AsPromotional instance. + * + * Returns an array of all videos, regardless of locale. + * + * Returns: (transfer none) (element-type AsVideo): an array + **/ +GPtrArray * +as_promotional_get_videos_all (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return priv->videos; +} + +/** + * as_promotional_get_videos: + * @promotional: a #AsPromotional instance. + * + * Gets the videos for this promotional. Only videos valid for the current + * language selection are returned. + * + * Returns: (transfer none) (element-type AsVideo): an array + **/ +GPtrArray * +as_promotional_get_videos (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + if (priv->videos_lang->len == 0) + return priv->videos; + return priv->videos_lang; +} + +/** + * as_promotional_add_video: + * @promotional: a #AsPromotional instance. + * @video: a #AsVideo instance. + * + * Adds a video to the promotional. + **/ +void +as_promotional_add_video (AsPromotional *promotional, AsVideo *video) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + priv->media_kind = AS_PROMOTIONAL_MEDIA_KIND_VIDEO; + g_ptr_array_add (priv->videos, g_object_ref (video)); + + if (as_utils_locale_is_compatible (as_video_get_locale (video), + as_promotional_get_active_locale (promotional))) + g_ptr_array_add (priv->videos_lang, g_object_ref (video)); +} + +/** + * as_promotional_get_caption: + * @promotional: a #AsPromotional instance. + * + * Gets the promotional caption. + * + * Returns: (nullable): the caption, or %NULL if not set. + **/ +const gchar * +as_promotional_get_caption (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return as_context_localized_ht_get (priv->context, + priv->caption, + NULL /* locale override */); +} + +/** + * as_promotional_set_caption: + * @promotional: a #AsPromotional instance. + * @caption: the caption text. + * @locale: (nullable): the locale, or %NULL for the current locale. + * + * Sets a caption on the promotional. + **/ +void +as_promotional_set_caption (AsPromotional *promotional, + const gchar *caption, + const gchar *locale) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + as_context_localized_ht_set (priv->context, priv->caption, caption, locale); +} + +/** + * as_promotional_is_valid: + * @promotional: a #AsPromotional instance. + * + * Performs a quick validation on this promotional. + * + * Returns: %TRUE if the promotional is a valid #AsPromotional + **/ +gboolean +as_promotional_is_valid (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return priv->images->len > 0 || priv->videos->len > 0; +} + +/** + * as_promotional_rebuild_suitable_media_list: + * + * Rebuild lists of images or videos suitable for the selected locale. + */ +static void +as_promotional_rebuild_suitable_media_list (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + gboolean all_locale = FALSE; + const gchar *active_locale = as_promotional_get_active_locale (promotional); + + all_locale = as_context_get_locale_use_all (priv->context); + + g_ptr_array_unref (priv->images_lang); + priv->images_lang = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + for (guint i = 0; i < priv->images->len; i++) { + AsImage *img = AS_IMAGE (g_ptr_array_index (priv->images, i)); + if (!all_locale && + !as_utils_locale_is_compatible (as_image_get_locale (img), active_locale)) + continue; + g_ptr_array_add (priv->images_lang, g_object_ref (img)); + } + + g_ptr_array_unref (priv->videos_lang); + priv->videos_lang = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + for (guint i = 0; i < priv->videos->len; i++) { + AsVideo *vid = AS_VIDEO (g_ptr_array_index (priv->videos, i)); + if (!all_locale && + !as_utils_locale_is_compatible (as_video_get_locale (vid), active_locale)) + continue; + g_ptr_array_add (priv->videos_lang, g_object_ref (vid)); + } +} + +/** + * as_promotional_set_context_locale: + * @promotional: a #AsPromotional instance. + * @locale: the new locale. + * + * Set the active locale on the context associated with this promotional, + * creating a new context if none exists yet. + */ +void +as_promotional_set_context_locale (AsPromotional *promotional, const gchar *locale) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + + if (priv->context == NULL) { + g_autoptr(AsContext) context = as_context_new (); + as_promotional_set_context (promotional, context); + } + as_context_set_locale (priv->context, locale); + as_promotional_rebuild_suitable_media_list (promotional); +} + +/** + * as_promotional_get_images_all: + * @promotional: a #AsPromotional instance. + * + * Returns an array of all images, regardless of size and language. + * + * Returns: (transfer none) (element-type AsImage): an array + **/ +GPtrArray * +as_promotional_get_images_all (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return priv->images; +} + +/** + * as_promotional_get_context: + * @promotional: a #AsPromotional instance. + * + * Returns the #AsContext associated with this promotional. + * + * Returns: (transfer none) (nullable): the #AsContext used by this promotional. + **/ +AsContext * +as_promotional_get_context (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return priv->context; +} + +/** + * as_promotional_set_context: + * @promotional: a #AsPromotional instance. + * @context: the #AsContext. + * + * Sets the document context this promotional is associated with. + **/ +void +as_promotional_set_context (AsPromotional *promotional, AsContext *context) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + if (priv->context != NULL) + g_object_unref (priv->context); + + if (context == NULL) + priv->context = NULL; + else + priv->context = g_object_ref (context); + + as_promotional_rebuild_suitable_media_list (promotional); +} + +/** + * as_promotional_get_position: + * @promotional: a #AsPromotional instance. + * + * Gets the ordering priority of this promotional. + * + * Returns: the position, or -1 if unknown. + **/ +gint +as_promotional_get_position (AsPromotional *promotional) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + return priv->position; +} + +/** + * as_promotional_set_position: + * @promotional: a #AsPromotional instance. + * @pos: the position. + * + * Sets the ordering priority of this promotional in the promotionals list. + **/ +void +as_promotional_set_position (AsPromotional *promotional, gint pos) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + priv->position = pos; +} + +/** + * as_promotional_load_from_xml: + * @promotional: a #AsPromotional instance. + * @ctx: the AppStream document context. + * @node: the XML node. + * @error: a #GError. + * + * Loads data from an XML node. + **/ +gboolean +as_promotional_load_from_xml (AsPromotional *promotional, + AsContext *ctx, + xmlNode *node, + GError **error) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + xmlNode *iter; + g_autofree gchar *prop = NULL; + + /* contains-text attribute */ + prop = as_xml_get_prop_value (node, "contains-text"); + if (g_strcmp0 (prop, "true") == 0) + priv->contains_text = AS_PROMOTIONAL_CONTAINS_TEXT_YES; + else if (g_strcmp0 (prop, "false") == 0) + priv->contains_text = AS_PROMOTIONAL_CONTAINS_TEXT_NO; + else + priv->contains_text = AS_PROMOTIONAL_CONTAINS_TEXT_UNKNOWN; + g_clear_pointer (&prop, g_free); + + /* environment attribute */ + as_ref_string_assign_transfer (&priv->environment, + as_xml_get_prop_value_refstr (node, "environment")); + + /* promotional media children */ + for (iter = node->children; iter != NULL; iter = iter->next) { + const gchar *node_name; + if (iter->type != XML_ELEMENT_NODE) + continue; + node_name = (const gchar *) iter->name; + + if (g_strcmp0 (node_name, "image") == 0) { + g_autoptr(AsImage) image = as_image_new (); + if (as_image_load_from_xml (image, ctx, iter, NULL)) + as_promotional_add_image (promotional, image); + } else if (g_strcmp0 (node_name, "video") == 0) { + g_autoptr(AsVideo) video = as_video_new (); + if (as_video_load_from_xml (video, ctx, iter, NULL)) + as_promotional_add_video (promotional, video); + } else if (g_strcmp0 (node_name, "caption") == 0) { + g_autofree gchar *content = NULL; + g_autofree gchar *lang = NULL; + + content = as_xml_get_node_value (iter); + if (content == NULL) + continue; + + lang = as_xml_get_node_locale_match (ctx, iter); + if (lang != NULL) + as_promotional_set_caption (promotional, content, lang); + } + } + + /* propagate context last so the image list for the selected locale is rebuilt properly */ + as_promotional_set_context (promotional, ctx); + + return TRUE; +} + +/** + * as_promotional_to_xml_node: + * @promotional: a #AsPromotional instance. + * @ctx: the AppStream document context. + * @root: XML node to attach the new nodes to. + * + * Serializes the data to an XML node. + **/ +void +as_promotional_to_xml_node (AsPromotional *promotional, AsContext *ctx, xmlNode *root) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + xmlNode *subnode; + + subnode = as_xml_add_node (root, "promotional"); + + if (priv->contains_text == AS_PROMOTIONAL_CONTAINS_TEXT_YES) + as_xml_add_text_prop (subnode, "contains-text", "true"); + else if (priv->contains_text == AS_PROMOTIONAL_CONTAINS_TEXT_NO) + as_xml_add_text_prop (subnode, "contains-text", "false"); + + if (priv->environment != NULL) + as_xml_add_text_prop (subnode, "environment", priv->environment); + + as_xml_add_localized_text_node (subnode, "caption", priv->caption); + + if (priv->media_kind == AS_PROMOTIONAL_MEDIA_KIND_IMAGE) { + for (guint i = 0; i < priv->images->len; i++) { + AsImage *image = AS_IMAGE (g_ptr_array_index (priv->images, i)); + as_image_to_xml_node (image, ctx, subnode); + } + } else if (priv->media_kind == AS_PROMOTIONAL_MEDIA_KIND_VIDEO) { + for (guint i = 0; i < priv->videos->len; i++) { + AsVideo *video = AS_VIDEO (g_ptr_array_index (priv->videos, i)); + as_video_to_xml_node (video, ctx, subnode); + } + } +} + +/** + * as_promotional_load_from_yaml: + * @promotional: a #AsPromotional instance. + * @ctx: the AppStream document context. + * @node: the YAML node. + * @error: a #GError. + * + * Loads data from a YAML field. + **/ +gboolean +as_promotional_load_from_yaml (AsPromotional *promotional, + AsContext *ctx, + struct fy_node *node, + GError **error) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + + AS_YAML_MAPPING_FOREACH (npair, node) { + const gchar *key = as_yaml_node_get_key0 (npair); + struct fy_node *nval = fy_node_pair_value (npair); + + if (g_strcmp0 (key, "contains-text") == 0) { + const gchar *value = as_yaml_node_get_value0 (npair); + if (g_strcmp0 (value, "true") == 0) + priv->contains_text = AS_PROMOTIONAL_CONTAINS_TEXT_YES; + else if (g_strcmp0 (value, "false") == 0) + priv->contains_text = AS_PROMOTIONAL_CONTAINS_TEXT_NO; + else + priv->contains_text = AS_PROMOTIONAL_CONTAINS_TEXT_UNKNOWN; + } else if (g_strcmp0 (key, "environment") == 0) { + as_ref_string_assign_transfer (&priv->environment, + as_yaml_node_get_value_refstr (npair)); + } else if (g_strcmp0 (key, "caption") == 0) { + as_yaml_set_localized_table (ctx, nval, priv->caption); + } else if (g_strcmp0 (key, "source-image") == 0) { + g_autoptr(AsImage) image = as_image_new (); + if (as_image_load_from_yaml (image, ctx, nval, AS_IMAGE_KIND_SOURCE, NULL)) + as_promotional_add_image (promotional, image); + } else if (g_strcmp0 (key, "thumbnails") == 0) { + AS_YAML_SEQUENCE_FOREACH (in, nval) { + g_autoptr(AsImage) image = as_image_new (); + if (as_image_load_from_yaml (image, + ctx, + in, + AS_IMAGE_KIND_THUMBNAIL, + NULL)) + as_promotional_add_image (promotional, image); + } + } else if (g_strcmp0 (key, "videos") == 0) { + AS_YAML_SEQUENCE_FOREACH (vn, nval) { + g_autoptr(AsVideo) video = as_video_new (); + if (as_video_load_from_yaml (video, ctx, vn, NULL)) + as_promotional_add_video (promotional, video); + } + } else { + as_yaml_print_unknown ("promotional", key, -1); + } + } + + /* propagate context last so the image list for the selected locale is rebuilt properly */ + as_promotional_set_context (promotional, ctx); + + return TRUE; +} + +/** + * as_promotional_emit_yaml: + * @promotional: a #AsPromotional instance. + * @ctx: the AppStream document context. + * @emitter: The YAML emitter to emit data on. + * + * Emit YAML data for this object. + **/ +void +as_promotional_emit_yaml (AsPromotional *promotional, + AsContext *ctx, + struct fy_emitter *emitter) +{ + AsPromotionalPrivate *priv = GET_PRIVATE (promotional); + AsImage *source_img = NULL; + + as_yaml_mapping_start (emitter); + + if (priv->contains_text == AS_PROMOTIONAL_CONTAINS_TEXT_YES) + as_yaml_emit_entry (emitter, "contains-text", "true"); + else if (priv->contains_text == AS_PROMOTIONAL_CONTAINS_TEXT_NO) + as_yaml_emit_entry (emitter, "contains-text", "false"); + + if (priv->environment != NULL) + as_yaml_emit_entry (emitter, "environment", priv->environment); + + as_yaml_emit_localized_entry (emitter, "caption", priv->caption); + + if (priv->media_kind == AS_PROMOTIONAL_MEDIA_KIND_IMAGE) { + as_yaml_emit_scalar (emitter, "thumbnails"); + as_yaml_sequence_start (emitter); + for (guint i = 0; i < priv->images->len; i++) { + AsImage *img = AS_IMAGE (g_ptr_array_index (priv->images, i)); + + if (as_image_get_kind (img) == AS_IMAGE_KIND_SOURCE) { + source_img = img; + continue; + } + + as_image_emit_yaml (img, ctx, emitter); + } + as_yaml_sequence_end (emitter); + + if (source_img != NULL) { + as_yaml_emit_scalar (emitter, "source-image"); + as_image_emit_yaml (source_img, ctx, emitter); + } + } else if (priv->media_kind == AS_PROMOTIONAL_MEDIA_KIND_VIDEO) { + as_yaml_emit_scalar (emitter, "videos"); + as_yaml_sequence_start (emitter); + for (guint i = 0; i < priv->videos->len; i++) { + AsVideo *video = AS_VIDEO (g_ptr_array_index (priv->videos, i)); + as_video_emit_yaml (video, ctx, emitter); + } + as_yaml_sequence_end (emitter); + } + + as_yaml_mapping_end (emitter); +} + +/** + * as_promotional_new: + * + * Creates a new #AsPromotional. + * + * Returns: (transfer full): a #AsPromotional + **/ +AsPromotional * +as_promotional_new (void) +{ + AsPromotional *promotional; + promotional = g_object_new (AS_TYPE_PROMOTIONAL, NULL); + return AS_PROMOTIONAL (promotional); +} diff --git a/src/as-promotional.h b/src/as-promotional.h new file mode 100644 index 00000000..052bd265 --- /dev/null +++ b/src/as-promotional.h @@ -0,0 +1,121 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2024 Matthias Klumpp + * + * Licensed under the GNU Lesser General Public License Version 2.1 + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the license, or + * (at your option) any later version. + * + * This library 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ + +#if !defined(__APPSTREAM_H) && !defined(AS_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __AS_PROMOTIONAL_H +#define __AS_PROMOTIONAL_H + +#include + +#include "as-context.h" +#include "as-image.h" +#include "as-video.h" + +G_BEGIN_DECLS + +#define AS_TYPE_PROMOTIONAL (as_promotional_get_type ()) +G_DECLARE_DERIVABLE_TYPE (AsPromotional, as_promotional, AS, PROMOTIONAL, GObject) + +struct _AsPromotionalClass { + GObjectClass parent_class; + /*< private >*/ + void (*_as_reserved1) (void); + void (*_as_reserved2) (void); + void (*_as_reserved3) (void); + void (*_as_reserved4) (void); + void (*_as_reserved5) (void); + void (*_as_reserved6) (void); +}; + +/** + * AsPromotionalMediaKind: + * @AS_PROMOTIONAL_MEDIA_KIND_UNKNOWN: Media kind is unknown + * @AS_PROMOTIONAL_MEDIA_KIND_IMAGE: The promotional contains images + * @AS_PROMOTIONAL_MEDIA_KIND_VIDEO: The promotional contains videos + * + * The media kind contained in this promotional. + **/ +typedef enum { + AS_PROMOTIONAL_MEDIA_KIND_UNKNOWN, + AS_PROMOTIONAL_MEDIA_KIND_IMAGE, + AS_PROMOTIONAL_MEDIA_KIND_VIDEO, + /*< private >*/ + AS_PROMOTIONAL_MEDIA_KIND_LAST +} AsPromotionalMediaKind; + +/** + * AsPromotionalContainsText: + * @AS_PROMOTIONAL_CONTAINS_TEXT_UNKNOWN: Whether this promotional contains text is unset + * @AS_PROMOTIONAL_CONTAINS_TEXT_YES: The promotional image/video contains text + * @AS_PROMOTIONAL_CONTAINS_TEXT_NO: The promotional image/video does not contain text + * + * Whether this promotional item contains text that is integral to understanding it. + * This attribute must be set explicitly; its absence triggers a validator warning. + **/ +typedef enum { + AS_PROMOTIONAL_CONTAINS_TEXT_UNKNOWN, + AS_PROMOTIONAL_CONTAINS_TEXT_YES, + AS_PROMOTIONAL_CONTAINS_TEXT_NO, + /*< private >*/ + AS_PROMOTIONAL_CONTAINS_TEXT_LAST +} AsPromotionalContainsText; + +gboolean as_promotional_is_valid (AsPromotional *promotional); + +AsPromotional *as_promotional_new (void); + +AsPromotionalMediaKind as_promotional_get_media_kind (AsPromotional *promotional); + +AsPromotionalContainsText as_promotional_get_contains_text (AsPromotional *promotional); +void as_promotional_set_contains_text (AsPromotional *promotional, + AsPromotionalContainsText contains_text); + +const gchar *as_promotional_get_environment (AsPromotional *promotional); +void as_promotional_set_environment (AsPromotional *promotional, + const gchar *env_id); + +AsContext *as_promotional_get_context (AsPromotional *promotional); +void as_promotional_set_context (AsPromotional *promotional, + AsContext *context); + +const gchar *as_promotional_get_caption (AsPromotional *promotional); +void as_promotional_set_caption (AsPromotional *promotional, + const gchar *caption, + const gchar *locale); + +GPtrArray *as_promotional_get_images_all (AsPromotional *promotional); +GPtrArray *as_promotional_get_images (AsPromotional *promotional); +AsImage *as_promotional_get_image (AsPromotional *promotional, + guint width, + guint height, + guint scale); +void as_promotional_add_image (AsPromotional *promotional, AsImage *image); +void as_promotional_clear_images (AsPromotional *promotional); + +GPtrArray *as_promotional_get_videos_all (AsPromotional *promotional); +GPtrArray *as_promotional_get_videos (AsPromotional *promotional); +void as_promotional_add_video (AsPromotional *promotional, AsVideo *video); + +G_END_DECLS + +#endif /* __AS_PROMOTIONAL_H */ diff --git a/src/as-tag-xml.gperf b/src/as-tag-xml.gperf index d09d8a8f..f7c48703 100644 --- a/src/as-tag-xml.gperf +++ b/src/as-tag-xml.gperf @@ -49,6 +49,7 @@ name_variant_suffix, AS_TAG_NAME_VARIANT_SUFFIX tags, AS_TAG_TAGS branding, AS_TAG_BRANDING references, AS_TAG_REFERENCES +promotionals, AS_TAG_PROMOTIONALS p, AS_TAG_P li, AS_TAG_LI ul, AS_TAG_UL diff --git a/src/as-tag-yaml.gperf b/src/as-tag-yaml.gperf index 6bdb5793..ddd4f158 100644 --- a/src/as-tag-yaml.gperf +++ b/src/as-tag-yaml.gperf @@ -51,3 +51,4 @@ NameVariantSuffix, AS_TAG_NAME_VARIANT_SUFFIX Tags, AS_TAG_TAGS Branding, AS_TAG_BRANDING References, AS_TAG_REFERENCES +Promotionals, AS_TAG_PROMOTIONALS diff --git a/src/as-tag.h b/src/as-tag.h index c360886f..d43ebd2f 100644 --- a/src/as-tag.h +++ b/src/as-tag.h @@ -75,6 +75,7 @@ AS_BEGIN_PRIVATE_DECLS * @AS_TAG_TAGS: `tags` / `Tags` * @AS_TAG_BRANDING: `branding` / `Branding` * @AS_TAG_REFERENCES: `references` / `References` + * @AS_TAG_PROMOTIONALS: `promotionals` / `Promotionals` * @AS_TAG_P: Description markup `p` * @AS_TAG_LI: Description markup `li` * @AS_TAG_OL: Description markup `ol` @@ -126,6 +127,7 @@ typedef enum { AS_TAG_TAGS, AS_TAG_BRANDING, AS_TAG_REFERENCES, + AS_TAG_PROMOTIONALS, AS_TAG_P, AS_TAG_LI, AS_TAG_UL, diff --git a/src/as-validator-issue-tag.h b/src/as-validator-issue-tag.h index 1e897d11..82f2f210 100644 --- a/src/as-validator-issue-tag.h +++ b/src/as-validator-issue-tag.h @@ -363,6 +363,22 @@ static AsValidatorIssueTag as_validator_issue_tag_list[] = { N_("No screenshot is marked as default.") }, + { "promotional-contains-text-missing", + AS_ISSUE_SEVERITY_WARNING, + /* TRANSLATORS: `contains-text` is an AppStream XML property. Please do not translate it. */ + N_("A `promotional` element is missing the `contains-text` attribute. Please explicitly set it to `true` if the image contains text, or `false` otherwise.") + }, + + { "promotional-image-not-landscape", + AS_ISSUE_SEVERITY_INFO, + N_("The promotional source image does not appear to be in landscape orientation (width > height). Promotional images should typically be wider than they are tall.") + }, + + { "promotional-no-media", + AS_ISSUE_SEVERITY_ERROR, + N_("A `promotional` element must contain at least one image or video to be useful. Please add an `` to it.") + }, + { "relation-invalid-tag", AS_ISSUE_SEVERITY_WARNING, N_("Found an unknown tag in a requires/recommends group. This is likely an error, because a component relation of this type is unknown.") diff --git a/src/as-validator.c b/src/as-validator.c index 40f69f8b..1ba54e84 100644 --- a/src/as-validator.c +++ b/src/as-validator.c @@ -1930,6 +1930,145 @@ as_validator_check_screenshots (AsValidator *validator, xmlNode *node, AsCompone as_validator_add_issue (validator, node, "screenshot-default-missing", NULL); } +/** + * as_validator_check_promotionals: + * + * Validate a "promotionals" tag. + **/ +static void +as_validator_check_promotionals (AsValidator *validator, xmlNode *node, AsComponent *cpt) +{ + for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) { + gboolean image_found = FALSE; + gboolean video_found = FALSE; + g_autofree gchar *contains_text_str = NULL; + g_autofree gchar *env_style = NULL; + + if (iter->type != XML_ELEMENT_NODE) + continue; + + if (g_strcmp0 ((const gchar *) iter->name, "promotional") != 0) { + as_validator_add_issue ( + validator, + iter, + "invalid-child-tag-name", + _("Found: %s - Allowed: %s"), (const gchar *) iter->name, "promotional"); + continue; + } + + /* contains-text must be explicit */ + contains_text_str = as_xml_get_prop_value (iter, "contains-text"); + if (contains_text_str == NULL) { + as_validator_add_issue (validator, + iter, + "promotional-contains-text-missing", + NULL); + } + + /* validate environment attribute */ + env_style = as_xml_get_prop_value (iter, "environment"); + if (env_style != NULL && !as_utils_is_gui_environment_style (env_style)) { + as_validator_add_issue (validator, + iter, + "screenshot-invalid-env-style", + "%s", + env_style); + } + + for (xmlNode *iter2 = iter->children; iter2 != NULL; iter2 = iter2->next) { + const gchar *node_name2; + if (iter2->type != XML_ELEMENT_NODE) + continue; + node_name2 = (const gchar *) iter2->name; + + if (g_strcmp0 (node_name2, "image") == 0) { + g_autofree gchar *image_url = as_xml_get_node_value (iter2); + g_autofree gchar *image_kind_str = as_xml_get_prop_value (iter2, "type"); + g_autofree gchar *img_width_str = as_xml_get_prop_value (iter2, "width"); + g_autofree gchar *img_height_str = as_xml_get_prop_value (iter2, "height"); + + image_found = TRUE; + + /* check landscape orientation for source images */ + if (g_strcmp0 (image_kind_str, "source") == 0 || + image_kind_str == NULL) { + if (img_width_str != NULL && img_height_str != NULL) { + gint64 w = g_ascii_strtoll (img_width_str, NULL, 10); + gint64 h = g_ascii_strtoll (img_height_str, NULL, 10); + if (w > 0 && h > 0 && w <= h) { + as_validator_add_issue ( + validator, + iter2, + "promotional-image-not-landscape", + NULL); + } + } + } + + if (!as_validate_is_url (image_url)) { + as_validator_add_issue (validator, + iter2, + "web-url-expected", + "%s", + image_url); + continue; + } + if (!as_validate_is_secure_url (image_url)) { + as_validator_add_issue (validator, + iter2, + "screenshot-media-url-not-secure", + "%s", + image_url); + } + as_validator_check_web_url (validator, + iter2, + image_url, + "screenshot-image-not-found"); + } else if (g_strcmp0 (node_name2, "video") == 0) { + g_autofree gchar *video_url = as_xml_get_node_value (iter2); + video_found = TRUE; + + if (!as_validate_is_url (video_url)) { + as_validator_add_issue (validator, + iter2, + "web-url-expected", + "%s", + video_url); + continue; + } + if (!as_validate_is_secure_url (video_url)) { + as_validator_add_issue (validator, + iter2, + "screenshot-media-url-not-secure", + "%s", + video_url); + } + as_validator_check_web_url (validator, + iter2, + video_url, + "screenshot-video-not-found"); + } else if (g_strcmp0 (node_name2, "caption") == 0) { + /* captions are fine */ + } else { + as_validator_add_issue ( + validator, + iter2, + "invalid-child-tag-name", + _("Found: %s - Allowed: %s"), + (const gchar *) iter2->name, + "caption; image; video"); + } + } + + if (!image_found && !video_found) { + as_validator_add_issue (validator, + iter, + "promotional-no-media", + NULL); + } + } +} + /** * as_validator_check_relations: **/ @@ -3496,6 +3635,9 @@ as_validator_validate_component_node (AsValidator *validator, AsContext *ctx, xm } else if (g_strcmp0 (node_name, "references") == 0) { as_validator_check_appear_once (validator, iter, found_tags, FALSE); as_validator_check_references (validator, iter); + } else if (g_strcmp0 (node_name, "promotionals") == 0) { + as_validator_check_appear_once (validator, iter, found_tags, FALSE); + as_validator_check_promotionals (validator, iter, cpt); } else if (g_strcmp0 (node_name, "name_variant_suffix") == 0) { as_validator_check_appear_once (validator, iter, found_tags, FALSE); } else if (g_strcmp0 (node_name, "custom") == 0) { diff --git a/src/meson.build b/src/meson.build index e1416cd2..4ee91f13 100644 --- a/src/meson.build +++ b/src/meson.build @@ -44,6 +44,7 @@ aslib_src = [ 'as-release-list.c', 'as-review.c', 'as-screenshot.c', + 'as-promotional.c', 'as-spdx.c', 'as-suggested.c', 'as-system-info.c', @@ -84,6 +85,7 @@ aslib_pub_headers = [ 'as-release-list.h', 'as-review.h', 'as-screenshot.h', + 'as-promotional.h', 'as-spdx.h', 'as-suggested.h', 'as-system-info.h', @@ -130,6 +132,7 @@ aslib_priv_headers = [ 'as-release-list-private.h', 'as-review-private.h', 'as-screenshot-private.h', + 'as-promotional-private.h', 'as-stemmer.h', 'as-spdx-data.h', 'as-tag.h', diff --git a/tests/test-xmldata.c b/tests/test-xmldata.c index d5feddeb..fc9f47f3 100644 --- a/tests/test-xmldata.c +++ b/tests/test-xmldata.c @@ -25,6 +25,7 @@ #include "appstream.h" #include "as-component-private.h" #include "as-screenshot-private.h" +#include "as-promotional-private.h" #include "as-xml.h" #include "as-test-utils.h" @@ -2437,6 +2438,79 @@ test_xml_rw_references (void) g_assert_true (as_xml_test_compare_xml (res, xmldata_tags)); } +/** + * test_xml_rw_promotionals: + * + * Test reading and writing the "promotionals" tag. + */ +static void +test_xml_rw_promotionals (void) +{ + /* clang-format off */ + static const gchar *xmldata_promotionals = + "\n" + " org.example.PromotionalsTest\n" + " \n" + " \n" + " Hero banner\n" + " https://example.org/hero.png\n" + " https://example.org/hero_small.png\n" + " \n" + " \n" + " Feature highlight\n" + " https://example.org/feature.png\n" + " \n" + " \n" + "\n"; + /* clang-format on */ + + g_autoptr(AsComponent) cpt = NULL; + g_autofree gchar *res = NULL; + GPtrArray *promos; + AsPromotional *promo1; + AsPromotional *promo2; + GPtrArray *images; + AsImage *img; + + /* read */ + cpt = as_xml_test_read_data (xmldata_promotionals, AS_FORMAT_STYLE_METAINFO); + g_assert_cmpstr (as_component_get_id (cpt), ==, "org.example.PromotionalsTest"); + + promos = as_component_get_promotionals (cpt); + g_assert_nonnull (promos); + g_assert_cmpint (promos->len, ==, 2); + + promo1 = AS_PROMOTIONAL (g_ptr_array_index (promos, 0)); + promo2 = AS_PROMOTIONAL (g_ptr_array_index (promos, 1)); + + /* promo1 checks */ + g_assert_cmpint (as_promotional_get_contains_text (promo1), + ==, + AS_PROMOTIONAL_CONTAINS_TEXT_NO); + g_assert_cmpint (as_promotional_get_media_kind (promo1), + ==, + AS_PROMOTIONAL_MEDIA_KIND_IMAGE); + as_promotional_set_context_locale (promo1, "C"); + g_assert_cmpstr (as_promotional_get_caption (promo1), ==, "Hero banner"); + images = as_promotional_get_images_all (promo1); + g_assert_cmpint (images->len, ==, 2); + img = AS_IMAGE (g_ptr_array_index (images, 0)); + g_assert_cmpint (as_image_get_width (img), ==, 1920); + g_assert_cmpint (as_image_get_height (img), ==, 1080); + + /* promo2 checks */ + g_assert_cmpint (as_promotional_get_contains_text (promo2), + ==, + AS_PROMOTIONAL_CONTAINS_TEXT_YES); + g_assert_cmpstr (as_promotional_get_environment (promo2), ==, "gnome"); + as_promotional_set_context_locale (promo2, "C"); + g_assert_cmpstr (as_promotional_get_caption (promo2), ==, "Feature highlight"); + + /* write and compare */ + res = as_xml_test_serialize (cpt, AS_FORMAT_STYLE_METAINFO); + g_assert_true (as_xml_test_compare_xml (res, xmldata_promotionals)); +} + /** * main: */ @@ -2512,6 +2586,7 @@ main (int argc, char **argv) g_test_add_func ("/XML/ReadWrite/Developer", test_xml_rw_developer); g_test_add_func ("/XML/ReadWrite/ExternalReleases", test_xml_rw_external_releases); g_test_add_func ("/XML/ReadWrite/References", test_xml_rw_references); + g_test_add_func ("/XML/ReadWrite/Promotionals", test_xml_rw_promotionals); ret = g_test_run (); g_free (datadir);