From 130832d69c19357ee4c0a1785c331e1c47dc80a3 Mon Sep 17 00:00:00 2001 From: Phaedrus Leeds Date: Wed, 3 Feb 2021 18:41:44 -0800 Subject: [PATCH 1/4] src: Move time-to-string function to gs-common This will allow it to be reused in later commits. Also change it to be more in line with how time spans are displayed in the mock-ups for the version history widget, e.g. "5 weeks ago" instead of "25 May 2012". This code is based on code in gnome-shell's js/misc/util.js. --- src/gs-common.c | 66 ++++++++++++++++++++++++++++++ src/gs-common.h | 1 + src/gs-updates-page.c | 95 ++----------------------------------------- src/meson.build | 1 + 4 files changed, 71 insertions(+), 92 deletions(-) diff --git a/src/gs-common.c b/src/gs-common.c index 404f9ae93..79ff95c4a 100644 --- a/src/gs-common.c +++ b/src/gs-common.c @@ -14,6 +14,12 @@ #include "gs-common.h" +#ifdef HAVE_GSETTINGS_DESKTOP_SCHEMAS +#include +#endif + +#include + #define SPINNER_DELAY 500 static gboolean @@ -654,3 +660,63 @@ gs_utils_reboot_notify (GsAppList *list) g_notification_set_priority (n, G_NOTIFICATION_PRIORITY_URGENT); g_application_send_notification (g_application_get_default (), "restart-required", n); } + +/** + * gs_utils_time_to_string: + * @unix_time_seconds: Time since the epoch in seconds + * + * Converts a time to a string such as "5 minutes ago" or "2 weeks ago" + * + * Returns: (transfer full): the time string, or %NULL if @unix_time_seconds is + * not valid + */ +gchar * +gs_utils_time_to_string (gint64 unix_time_seconds) +{ + gint minutes_ago, hours_ago, days_ago; + gint weeks_ago, months_ago, years_ago; + g_autoptr(GDateTime) date_time = NULL; + g_autoptr(GDateTime) now = NULL; + GTimeSpan timespan; + + if (unix_time_seconds <= 0) + return NULL; + date_time = g_date_time_new_from_unix_local (unix_time_seconds); + now = g_date_time_new_now_local (); + timespan = g_date_time_difference (now, date_time); + + minutes_ago = (gint) (timespan / G_TIME_SPAN_MINUTE); + hours_ago = (gint) (timespan / G_TIME_SPAN_HOUR); + days_ago = (gint) (timespan / G_TIME_SPAN_DAY); + weeks_ago = days_ago / 7; + months_ago = days_ago / 30; + years_ago = weeks_ago / 52; + + if (minutes_ago < 5) { + /* TRANSLATORS: something happened less than 5 minutes ago */ + return g_strdup (_("Just now")); + } else if (hours_ago < 1) + return g_strdup_printf (ngettext ("%d minute ago", + "%d minutes ago", minutes_ago), + minutes_ago); + else if (days_ago < 1) + return g_strdup_printf (ngettext ("%d hour ago", + "%d hours ago", hours_ago), + hours_ago); + else if (days_ago < 15) + return g_strdup_printf (ngettext ("%d day ago", + "%d days ago", days_ago), + days_ago); + else if (weeks_ago < 8) + return g_strdup_printf (ngettext ("%d week ago", + "%d weeks ago", weeks_ago), + weeks_ago); + else if (years_ago < 1) + return g_strdup_printf (ngettext ("%d month ago", + "%d months ago", months_ago), + months_ago); + else + return g_strdup_printf (ngettext ("%d year ago", + "%d years ago", years_ago), + years_ago); +} diff --git a/src/gs-common.h b/src/gs-common.h index 63c8e86bf..cbd6dc61c 100644 --- a/src/gs-common.h +++ b/src/gs-common.h @@ -47,5 +47,6 @@ gchar *gs_utils_build_unique_id_kind (AsComponentKind kind, gboolean gs_utils_list_has_component_fuzzy (GsAppList *list, GsApp *app); void gs_utils_reboot_notify (GsAppList *list); +gchar *gs_utils_time_to_string (gint64 unix_time_seconds); G_END_DECLS diff --git a/src/gs-updates-page.c b/src/gs-updates-page.c index 37919fc53..0b51283ee 100644 --- a/src/gs-updates-page.c +++ b/src/gs-updates-page.c @@ -23,12 +23,6 @@ #include "gs-upgrade-banner.h" #include "gs-application.h" -#ifdef HAVE_GSETTINGS_DESKTOP_SCHEMAS -#include -#endif - -#include - typedef enum { GS_UPDATES_PAGE_FLAG_NONE = 0, GS_UPDATES_PAGE_FLAG_HAS_UPDATES = 1 << 0, @@ -176,91 +170,13 @@ _get_num_updates (GsUpdatesPage *self) return count; } -static GDateTime * -time_next_midnight (void) -{ - GDateTime *next_midnight; - GTimeSpan since_midnight; - g_autoptr(GDateTime) now = NULL; - - now = g_date_time_new_now_local (); - since_midnight = g_date_time_get_hour (now) * G_TIME_SPAN_HOUR + - g_date_time_get_minute (now) * G_TIME_SPAN_MINUTE + - g_date_time_get_second (now) * G_TIME_SPAN_SECOND + - g_date_time_get_microsecond (now); - next_midnight = g_date_time_add (now, G_TIME_SPAN_DAY - since_midnight); - - return next_midnight; -} - static gchar * gs_updates_page_last_checked_time_string (GsUpdatesPage *self) { -#ifdef HAVE_GSETTINGS_DESKTOP_SCHEMAS - GDesktopClockFormat clock_format; -#endif - const gchar *format_string; - gchar *time_string; - gboolean use_24h_time = FALSE; - gint64 tmp; - gint days_ago; - g_autoptr(GDateTime) last_checked = NULL; - g_autoptr(GDateTime) midnight = NULL; - - g_settings_get (self->settings, "check-timestamp", "x", &tmp); - if (tmp == 0) - return NULL; - last_checked = g_date_time_new_from_unix_local (tmp); - - midnight = time_next_midnight (); - days_ago = (gint) (g_date_time_difference (midnight, last_checked) / G_TIME_SPAN_DAY); - -#ifdef HAVE_GSETTINGS_DESKTOP_SCHEMAS - clock_format = g_settings_get_enum (self->desktop_settings, "clock-format"); - use_24h_time = (clock_format == G_DESKTOP_CLOCK_FORMAT_24H || self->ampm_available == FALSE); -#endif - - if (days_ago < 1) { // today - if (use_24h_time) { - /* TRANSLATORS: Time in 24h format */ - format_string = _("%R"); - } else { - /* TRANSLATORS: Time in 12h format */ - format_string = _("%l:%M %p"); - } - } else if (days_ago < 2) { // yesterday - if (use_24h_time) { - /* TRANSLATORS: This is the word "Yesterday" followed by a - time string in 24h format. i.e. "Yesterday, 14:30" */ - format_string = _("Yesterday, %R"); - } else { - /* TRANSLATORS: This is the word "Yesterday" followed by a - time string in 12h format. i.e. "Yesterday, 2:30 PM" */ - format_string = _("Yesterday, %l:%M %p"); - } - } else if (days_ago < 3) { - format_string = _("Two days ago"); - } else if (days_ago < 4) { - format_string = _("Three days ago"); - } else if (days_ago < 5) { - format_string = _("Four days ago"); - } else if (days_ago < 6) { - format_string = _("Five days ago"); - } else if (days_ago < 7) { - format_string = _("Six days ago"); - } else if (days_ago < 8) { - format_string = _("One week ago"); - } else if (days_ago < 15) { - format_string = _("Two weeks ago"); - } else { - /* TRANSLATORS: This is the date string with: day number, month name, year. - i.e. "25 May 2012" */ - format_string = _("%e %B %Y"); - } + gint64 last_checked; - time_string = g_date_time_format (last_checked, format_string); - - return time_string; + g_settings_get (self->settings, "check-timestamp", "x", &last_checked); + return gs_utils_time_to_string (last_checked); } static const gchar * @@ -1440,8 +1356,6 @@ gs_updates_page_class_init (GsUpdatesPageClass *klass) static void gs_updates_page_init (GsUpdatesPage *self) { - const char *ampm; - gtk_widget_init_template (GTK_WIDGET (self)); self->state = GS_UPDATES_PAGE_STATE_STARTUP; @@ -1454,9 +1368,6 @@ gs_updates_page_init (GsUpdatesPage *self) self->sizegroup_button = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); self->sizegroup_header = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL); - ampm = nl_langinfo (AM_STR); - if (ampm != NULL && *ampm != '\0') - self->ampm_available = TRUE; } GsUpdatesPage * diff --git a/src/meson.build b/src/meson.build index 364b86387..7e4cf4f66 100644 --- a/src/meson.build +++ b/src/meson.build @@ -241,6 +241,7 @@ if get_option('tests') glib, gmodule, goa, + gsettings_desktop_schemas, gtk, json_glib, libgnomesoftware_dep, -- GitLab From aba78e905791e2201155f25909ee7dc7cbe25543 Mon Sep 17 00:00:00 2001 From: Phaedrus Leeds Date: Tue, 9 Feb 2021 14:56:10 -0800 Subject: [PATCH 2/4] gs-details-page: Fix a copy-paste error --- src/gs-details-page.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui index a654049fa..fce2ca3af 100644 --- a/src/gs-details-page.ui +++ b/src/gs-details-page.ui @@ -813,7 +813,7 @@ - + -- GitLab From d74129c1e03c85a9fc81c2010c94d21109ac5c43 Mon Sep 17 00:00:00 2001 From: Phaedrus Leeds Date: Mon, 8 Feb 2021 12:21:10 -0800 Subject: [PATCH 3/4] Add version history info into GsApp This will be used by follow-up commits. --- lib/gs-app.c | 42 +++++++++++++++++++++++++++ lib/gs-app.h | 3 ++ plugins/core/gs-appstream.c | 57 +++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/lib/gs-app.c b/lib/gs-app.c index fdec16795..d3cd67546 100644 --- a/lib/gs-app.c +++ b/lib/gs-app.c @@ -127,6 +127,7 @@ typedef struct GsPluginAction pending_action; GsAppPermissions permissions; gboolean is_update_downloaded; + GPtrArray *version_history; /* (element-type AsRelease) */ } GsAppPrivate; enum { @@ -4584,6 +4585,7 @@ gs_app_dispose (GObject *object) g_clear_pointer (&priv->reviews, g_ptr_array_unref); g_clear_pointer (&priv->provided, g_ptr_array_unref); g_clear_pointer (&priv->icons, g_ptr_array_unref); + g_clear_pointer (&priv->version_history, g_ptr_array_unref); G_OBJECT_CLASS (gs_app_parent_class)->dispose (object); } @@ -5121,3 +5123,43 @@ gs_app_set_update_permissions (GsApp *app, GsAppPermissions update_permissions) g_return_if_fail (GS_IS_APP (app)); priv->update_permissions = update_permissions; } + +/** + * gs_app_get_version_history: + * @app: a #GsApp + * + * Gets the list of past releases for an application (including the latest + * one). + * + * Returns: (element-type AsRelease) (transfer none): a list + * + * Since: 40 + **/ +GPtrArray * +gs_app_get_version_history (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->version_history; +} + +/** + * gs_app_set_version_history: + * @app: a #GsApp + * @version_history: (element-type AsRelease): a set of entries representing + * the version history + * + * Set the list of past releases for an application (including the latest one). + * + * Since: 40 + **/ +void +gs_app_set_version_history (GsApp *app, GPtrArray *version_history) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (version_history != NULL); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_ptr_array (&priv->version_history, version_history); +} diff --git a/lib/gs-app.h b/lib/gs-app.h index 6b7dd5b46..f215151e5 100644 --- a/lib/gs-app.h +++ b/lib/gs-app.h @@ -466,5 +466,8 @@ void gs_app_set_permissions (GsApp *app, GsAppPermissions gs_app_get_update_permissions (GsApp *app); void gs_app_set_update_permissions (GsApp *app, GsAppPermissions update_permissions); +GPtrArray *gs_app_get_version_history (GsApp *app); +void gs_app_set_version_history (GsApp *app, + GPtrArray *version_history); G_END_DECLS diff --git a/plugins/core/gs-appstream.c b/plugins/core/gs-appstream.c index b0fe2e42f..a273abf7f 100644 --- a/plugins/core/gs-appstream.c +++ b/plugins/core/gs-appstream.c @@ -587,6 +587,59 @@ gs_appstream_refine_app_updates (GsPlugin *plugin, return TRUE; } +static gboolean +gs_appstream_refine_add_version_history (GsApp *app, XbNode *component, GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) version_history = NULL; /* (element-type AsRelease) */ + g_autoptr(GPtrArray) releases = NULL; /* (element-type XbNode) */ + + /* get all components */ + releases = xb_node_query (component, "releases/*", 0, &error_local); + if (releases == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + return TRUE; + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) + return TRUE; + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + version_history = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + for (guint i = 0; i < releases->len; i++) { + XbNode *release_node = g_ptr_array_index (releases, i); + const gchar *version = xb_node_get_attr (release, "version"); + g_autoptr(XbNode) description_node = NULL; + g_autofree gchar *description = NULL; + guint64 timestamp; + + /* ignore releases with no version */ + if (version == NULL) + continue; + + timestamp = xb_node_query_attr_as_uint (release_node, "timestamp", NULL); + + /* include updates with or without a description */ + description_node = xb_node_query_first (release_node, "description", NULL); + if (description_node != NULL) + description = gs_appstream_format_description (description_node, NULL); + + release = as_release_new (); + as_release_set_version (release, version); + as_release_set_timestamp (release, timestamp); + if (description != NULL) + as_release_set_description (release, description, NULL); + + g_ptr_array_add (version_history, g_steal_pointer (&release)); + } + + if (version_history->len > 0) + gs_app_set_version_history (app, version_history); + + /* success */ + return TRUE; +} + /** * _gs_utils_locale_has_translations: * @locale: A locale, e.g. `en_GB` or `uz_UZ.utf8@cyrillic` @@ -923,6 +976,10 @@ gs_appstream_refine_app (GsPlugin *plugin, if (timestamp != G_MAXUINT64) gs_app_set_release_date (app, timestamp); + /* set the version history */ + if (!gs_appstream_refine_add_version_history (app, component, error)) + return FALSE; + /* copy all the metadata */ if (!gs_appstream_copy_metadata (app, component, error)) return FALSE; -- GitLab From 88ff93c936d65a4c0081351922e1aa234984963b Mon Sep 17 00:00:00 2001 From: Phaedrus Leeds Date: Wed, 3 Feb 2021 12:45:34 -0800 Subject: [PATCH 4/4] Add version history to app details pages Instead of showing the Version and Release Date in the table of info on app details pages, show it as seen in the mock-ups (#1111). For now, leave the "Last Updated" information since the latest available version isn't necessarily the installed version, so this UI doesn't duplicate that information. Helps: #1111 --- plugins/core/gs-appstream.c | 10 ++- po/POTFILES.in | 3 + src/gnome-software.gresource.xml | 2 + src/gs-app-version-history-dialog.c | 109 +++++++++++++++++++++++ src/gs-app-version-history-dialog.h | 24 +++++ src/gs-app-version-history-dialog.ui | 51 +++++++++++ src/gs-app-version-history-row.c | 104 ++++++++++++++++++++++ src/gs-app-version-history-row.h | 27 ++++++ src/gs-app-version-history-row.ui | 63 +++++++++++++ src/gs-common.c | 1 + src/gs-details-page.c | 85 +++++++++++------- src/gs-details-page.ui | 127 ++++++++++++--------------- src/meson.build | 2 + 13 files changed, 498 insertions(+), 110 deletions(-) create mode 100644 src/gs-app-version-history-dialog.c create mode 100644 src/gs-app-version-history-dialog.h create mode 100644 src/gs-app-version-history-dialog.ui create mode 100644 src/gs-app-version-history-row.c create mode 100644 src/gs-app-version-history-row.h create mode 100644 src/gs-app-version-history-row.ui diff --git a/plugins/core/gs-appstream.c b/plugins/core/gs-appstream.c index a273abf7f..6fd38cc4c 100644 --- a/plugins/core/gs-appstream.c +++ b/plugins/core/gs-appstream.c @@ -608,16 +608,19 @@ gs_appstream_refine_add_version_history (GsApp *app, XbNode *component, GError * version_history = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); for (guint i = 0; i < releases->len; i++) { XbNode *release_node = g_ptr_array_index (releases, i); - const gchar *version = xb_node_get_attr (release, "version"); + const gchar *version = xb_node_get_attr (release_node, "version"); g_autoptr(XbNode) description_node = NULL; g_autofree gchar *description = NULL; guint64 timestamp; + g_autoptr(AsRelease) release = NULL; + g_autofree char *timestamp_xpath = NULL; /* ignore releases with no version */ if (version == NULL) continue; - timestamp = xb_node_query_attr_as_uint (release_node, "timestamp", NULL); + timestamp_xpath = g_strdup_printf ("releases/release[%u]", i+1); + timestamp = xb_node_query_attr_as_uint (component, timestamp_xpath, "timestamp", NULL); /* include updates with or without a description */ description_node = xb_node_query_first (release_node, "description", NULL); @@ -626,7 +629,8 @@ gs_appstream_refine_add_version_history (GsApp *app, XbNode *component, GError * release = as_release_new (); as_release_set_version (release, version); - as_release_set_timestamp (release, timestamp); + if (timestamp != G_MAXUINT64) + as_release_set_timestamp (release, timestamp); if (description != NULL) as_release_set_description (release, description, NULL); diff --git a/po/POTFILES.in b/po/POTFILES.in index c119b8628..9f92fa4f5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -6,6 +6,9 @@ src/gnome-software.ui lib/gs-app.c src/gs-app-addon-row.c src/gs-app-addon-row.ui +src/gs-app-version-history-dialog.ui +src/gs-app-version-history-row.c +src/gs-app-version-history-row.ui src/gs-application.c src/gs-app-row.c src/gs-app-row.ui diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml index 459ecf821..d0f7ea95c 100644 --- a/src/gnome-software.gresource.xml +++ b/src/gnome-software.gresource.xml @@ -3,6 +3,8 @@ gnome-software.ui gs-app-addon-row.ui + gs-app-version-history-dialog.ui + gs-app-version-history-row.ui gs-app-row.ui gs-basic-auth-dialog.ui gs-category-page.ui diff --git a/src/gs-app-version-history-dialog.c b/src/gs-app-version-history-dialog.c new file mode 100644 index 000000000..439ad2946 --- /dev/null +++ b/src/gs-app-version-history-dialog.c @@ -0,0 +1,109 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Matthew Leeds + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-app-version-history-dialog.h" + +#include "gnome-software-private.h" +#include "gs-common.h" +#include "gs-app-version-history-row.h" +#include + +struct _GsAppVersionHistoryDialog +{ + GtkDialog parent_instance; + GsApp *app; + GtkWidget *listbox; +}; + +G_DEFINE_TYPE (GsAppVersionHistoryDialog, gs_app_version_history_dialog, GTK_TYPE_DIALOG) + +static void +populate_version_history (GsAppVersionHistoryDialog *dialog, + GsApp *app) +{ + GPtrArray *version_history; + + /* remove previous */ + gs_container_remove_all (GTK_CONTAINER (dialog->listbox)); + + version_history = gs_app_get_version_history (app); + if (version_history == NULL) { + GtkWidget *row; + row = gs_app_version_history_row_new (); + gs_app_version_history_row_set_info (GS_APP_VERSION_HISTORY_ROW (row), + gs_app_get_version (app), + gs_app_get_release_date (app), NULL); + gtk_list_box_insert (GTK_LIST_BOX (dialog->listbox), row, -1); + gtk_widget_show (row); + return; + } + + /* add each */ + for (guint i = 0; i < version_history->len; i++) { + GtkWidget *row; + AsRelease *version = g_ptr_array_index (version_history, i); + + row = gs_app_version_history_row_new (); + gs_app_version_history_row_set_info (GS_APP_VERSION_HISTORY_ROW (row), + as_release_get_version (version), + as_release_get_timestamp (version), + as_release_get_description (version)); + + gtk_list_box_insert (GTK_LIST_BOX (dialog->listbox), row, -1); + gtk_widget_show (row); + } +} + +static void +list_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *header = NULL; + if (before != NULL) + header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, header); +} + +static void +gs_app_version_history_dialog_init (GsAppVersionHistoryDialog *dialog) +{ + gtk_widget_init_template (GTK_WIDGET (dialog)); + + gtk_list_box_set_header_func (GTK_LIST_BOX (dialog->listbox), + list_header_func, + dialog, + NULL); +} + +static void +gs_app_version_history_dialog_class_init (GsAppVersionHistoryDialogClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-app-version-history-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsAppVersionHistoryDialog, listbox); +} + +GtkWidget * +gs_app_version_history_dialog_new (GtkWindow *parent, GsApp *app) +{ + GsAppVersionHistoryDialog *dialog; + + dialog = g_object_new (GS_TYPE_APP_VERSION_HISTORY_DIALOG, + "use-header-bar", TRUE, + "transient-for", parent, + "modal", TRUE, + NULL); + populate_version_history (dialog, app); + + return GTK_WIDGET (dialog); +} diff --git a/src/gs-app-version-history-dialog.h b/src/gs-app-version-history-dialog.h new file mode 100644 index 000000000..6abed18e2 --- /dev/null +++ b/src/gs-app-version-history-dialog.h @@ -0,0 +1,24 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Matthew Leeds + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_APP_VERSION_HISTORY_DIALOG (gs_app_version_history_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (GsAppVersionHistoryDialog, gs_app_version_history_dialog, GS, APP_VERSION_HISTORY_DIALOG, GtkDialog) + +GtkWidget *gs_app_version_history_dialog_new (GtkWindow *parent, + GsApp *app); + +G_END_DECLS diff --git a/src/gs-app-version-history-dialog.ui b/src/gs-app-version-history-dialog.ui new file mode 100644 index 000000000..b8527a041 --- /dev/null +++ b/src/gs-app-version-history-dialog.ui @@ -0,0 +1,51 @@ + + + + + diff --git a/src/gs-app-version-history-row.c b/src/gs-app-version-history-row.c new file mode 100644 index 000000000..e01e6987e --- /dev/null +++ b/src/gs-app-version-history-row.c @@ -0,0 +1,104 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Matthew Leeds + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include + +#include "gs-app-version-history-row.h" + +#include "gs-common.h" + +struct _GsAppVersionHistoryRow +{ + GtkListBoxRow parent_instance; + + GtkWidget *version_number_label; + GtkWidget *version_date_label; + GtkWidget *version_description_label; +}; + +G_DEFINE_TYPE (GsAppVersionHistoryRow, gs_app_version_history_row, GTK_TYPE_LIST_BOX_ROW) + +static void +gs_app_version_history_row_class_init (GsAppVersionHistoryRowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-app-version-history-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsAppVersionHistoryRow, version_number_label); + gtk_widget_class_bind_template_child (widget_class, GsAppVersionHistoryRow, version_date_label); + gtk_widget_class_bind_template_child (widget_class, GsAppVersionHistoryRow, version_description_label); +} + +static void +gs_app_version_history_row_init (GsAppVersionHistoryRow *row) +{ + gtk_widget_set_has_window (GTK_WIDGET (row), FALSE); + gtk_widget_init_template (GTK_WIDGET (row)); +} + +void +gs_app_version_history_row_set_info (GsAppVersionHistoryRow *row, + const char *version_number, + guint64 version_date, + const char *version_description) +{ + g_autofree char *version_date_string = NULL; + g_autofree char *version_date_string_tooltip = NULL; + + if (version_number == NULL || *version_number == '\0') + return; + + if (version_description != NULL && *version_description != '\0') { + g_autofree char *version_tmp = NULL; + version_tmp = g_strdup_printf (_("New in Version %s"), version_number); + gtk_label_set_label (GTK_LABEL (row->version_number_label), version_tmp); + gtk_label_set_label (GTK_LABEL (row->version_description_label), version_description); + } else { + g_autofree char *version_tmp = NULL; + const gchar *version_description_fallback; + version_tmp = g_strdup_printf (_("Version %s"), version_number); + gtk_label_set_label (GTK_LABEL (row->version_number_label), version_tmp); + version_description_fallback = _("No details for this release"); + gtk_label_set_label (GTK_LABEL (row->version_description_label), version_description_fallback); + gtk_style_context_add_class (gtk_widget_get_style_context (row->version_description_label), "dim-label"); + } + + if (version_date != 0) { + g_autoptr(GDateTime) date_time = NULL; + const gchar *format_string; + + /* this is the date in the form of "x weeks ago" or "y months ago" */ + version_date_string = gs_utils_time_to_string ((gint64) version_date); + + /* TRANSLATORS: This is the date string with: day number, month name, year. + i.e. "25 May 2012" */ + format_string = _("%e %B %Y"); + date_time = g_date_time_new_from_unix_local (version_date); + version_date_string_tooltip = g_date_time_format (date_time, format_string); + } + + if (version_date_string == NULL) + gtk_widget_set_visible (row->version_date_label, FALSE); + else + gtk_label_set_label (GTK_LABEL (row->version_date_label), version_date_string); + + if (version_date_string_tooltip != NULL) + gtk_widget_set_tooltip_text (row->version_date_label, version_date_string_tooltip); +} + +GtkWidget * +gs_app_version_history_row_new (void) +{ + GsAppVersionHistoryRow *row; + + row = g_object_new (GS_TYPE_APP_VERSION_HISTORY_ROW, NULL); + return GTK_WIDGET (row); +} diff --git a/src/gs-app-version-history-row.h b/src/gs-app-version-history-row.h new file mode 100644 index 000000000..0e91ac6e2 --- /dev/null +++ b/src/gs-app-version-history-row.h @@ -0,0 +1,27 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Matthew Leeds + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include + +#include "gnome-software-private.h" + +G_BEGIN_DECLS + +#define GS_TYPE_APP_VERSION_HISTORY_ROW (gs_app_version_history_row_get_type ()) + +G_DECLARE_FINAL_TYPE (GsAppVersionHistoryRow, gs_app_version_history_row, GS, APP_VERSION_HISTORY_ROW, GtkListBoxRow) + +GtkWidget *gs_app_version_history_row_new (void); +void gs_app_version_history_row_set_info (GsAppVersionHistoryRow *row, + const char *version_number, + guint64 version_date, + const char *version_description); + +G_END_DECLS diff --git a/src/gs-app-version-history-row.ui b/src/gs-app-version-history-row.ui new file mode 100644 index 000000000..97b016dbb --- /dev/null +++ b/src/gs-app-version-history-row.ui @@ -0,0 +1,63 @@ + + + + + diff --git a/src/gs-common.c b/src/gs-common.c index 79ff95c4a..dd1ba3f51 100644 --- a/src/gs-common.c +++ b/src/gs-common.c @@ -681,6 +681,7 @@ gs_utils_time_to_string (gint64 unix_time_seconds) if (unix_time_seconds <= 0) return NULL; + date_time = g_date_time_new_from_unix_local (unix_time_seconds); now = g_date_time_new_now_local (); timespan = g_date_time_difference (now, date_time); diff --git a/src/gs-details-page.c b/src/gs-details-page.c index 0dd4fa297..099ff06d0 100644 --- a/src/gs-details-page.c +++ b/src/gs-details-page.c @@ -19,6 +19,8 @@ #include "gs-details-page.h" #include "gs-app-addon-row.h" +#include "gs-app-version-history-row.h" +#include "gs-app-version-history-dialog.h" #include "gs-description-box.h" #include "gs-history-dialog.h" #include "gs-origin-popover-row.h" @@ -111,16 +113,16 @@ struct _GsDetailsPage GtkWidget *label_details_size_download_value; GtkWidget *label_details_updated_title; GtkWidget *label_details_updated_value; - GtkWidget *label_details_version_title; - GtkWidget *label_details_version_value; - GtkWidget *label_details_released_title; - GtkWidget *label_details_released_value; GtkWidget *label_details_permissions_title; GtkWidget *button_details_permissions_value; GtkWidget *label_failed; GtkWidget *label_license_nonfree_details; GtkWidget *label_licenses_intro; GtkWidget *list_box_addons; + GtkWidget *list_box_version_history; + GtkWidget *box_version_history_frame; + GtkWidget *row_latest_version; + GtkWidget *version_history_button; GtkWidget *box_reviews; GtkWidget *box_details_screenshot_fallback; GtkWidget *histogram; @@ -1109,6 +1111,7 @@ gs_details_page_refresh_all (GsDetailsPage *self) guint64 user_integration_bf; gboolean show_support_box = FALSE; g_autofree gchar *origin = NULL; + GPtrArray *version_history; /* change widgets */ tmp = gs_app_get_name (self->app); @@ -1200,34 +1203,26 @@ gs_details_page_refresh_all (GsDetailsPage *self) gtk_widget_set_visible (self->label_details_channel_value, FALSE); } - /* set version */ - tmp = gs_app_get_version (self->app); - if (tmp != NULL){ - gtk_label_set_label (GTK_LABEL (self->label_details_version_value), tmp); + /* set version history */ + version_history = gs_app_get_version_history (self->app); + if (version_history == NULL) { + const char *version = gs_app_get_version (self->app); + if (version == NULL || *version == '\0') + gtk_widget_set_visible (self->box_version_history_frame, FALSE); + else + gs_app_version_history_row_set_info (GS_APP_VERSION_HISTORY_ROW (self->row_latest_version), + version, gs_app_get_release_date (self->app), NULL); } else { - /* TRANSLATORS: this is where the version is not known */ - gtk_label_set_label (GTK_LABEL (self->label_details_version_value), C_("version", "Unknown")); + AsRelease *latest_version = g_ptr_array_index (version_history, 0); + gs_app_version_history_row_set_info (GS_APP_VERSION_HISTORY_ROW (self->row_latest_version), + as_release_get_version (latest_version), + as_release_get_timestamp (latest_version), + as_release_get_description (latest_version)); } /* refresh size information */ gs_details_page_refresh_size (self); - /* set the released date */ - if (gs_app_get_release_date (self->app)) { - g_autoptr(GDateTime) dt = NULL; - g_autofree gchar *released_str = NULL; - - dt = g_date_time_new_from_unix_utc ((gint64) gs_app_get_release_date (self->app)); - released_str = g_date_time_format (dt, "%x"); - - gtk_label_set_label (GTK_LABEL (self->label_details_released_value), released_str); - gtk_widget_set_visible (self->label_details_released_title, TRUE); - gtk_widget_set_visible (self->label_details_released_value, TRUE); - } else { - gtk_widget_set_visible (self->label_details_released_title, FALSE); - gtk_widget_set_visible (self->label_details_released_value, FALSE); - } - /* set the updated date */ updated = gs_app_get_install_date (self->app); if (updated == GS_APP_INSTALL_DATE_UNSET) { @@ -1421,14 +1416,10 @@ gs_details_page_refresh_all (GsDetailsPage *self) case AS_COMPONENT_KIND_REPOSITORY: gtk_widget_set_visible (self->label_details_license_title, FALSE); gtk_widget_set_visible (self->box_details_license_value, FALSE); - gtk_widget_set_visible (self->label_details_version_title, FALSE); - gtk_widget_set_visible (self->label_details_version_value, FALSE); break; default: gtk_widget_set_visible (self->label_details_license_title, TRUE); gtk_widget_set_visible (self->box_details_license_value, TRUE); - gtk_widget_set_visible (self->label_details_version_title, TRUE); - gtk_widget_set_visible (self->label_details_version_value, TRUE); break; } @@ -1465,6 +1456,26 @@ list_sort_func (GtkListBoxRow *a, gs_app_get_name (a2)); } +static void +version_history_list_row_activated_cb (GtkListBox *list_box, + GtkListBoxRow *row, + GsDetailsPage *self) +{ + GtkWidget *dialog; + + /* Only the row with the arrow is clickable */ + if (GS_IS_APP_VERSION_HISTORY_ROW (row)) + return; + + dialog = gs_app_version_history_dialog_new (gs_shell_get_window (self->shell), + self->app); + gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog)); + + /* just destroy */ + g_signal_connect_swapped (dialog, "response", + G_CALLBACK (gtk_widget_destroy), dialog); +} + static void gs_details_page_addon_selected_cb (GsAppAddonRow *row, GParamSpec *pspec, GsDetailsPage *self); static void gs_details_page_addon_remove_cb (GsAppAddonRow *row, gpointer user_data); @@ -2882,14 +2893,14 @@ gs_details_page_class_init (GsDetailsPageClass *klass) gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_size_installed_value); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_updated_title); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_updated_value); - gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_version_title); - gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_version_value); - gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_released_title); - gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_released_value); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_permissions_title); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_permissions_value); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_failed); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, list_box_addons); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, list_box_version_history); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_version_history_frame); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, row_latest_version); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, version_history_button); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_reviews); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_screenshot_fallback); gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, histogram); @@ -2948,6 +2959,12 @@ gs_details_page_init (GsDetailsPage *self) list_sort_func, self, NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box_version_history), + list_header_func, + self, NULL); + g_signal_connect (self->list_box_version_history, "row-activated", + G_CALLBACK (version_history_list_row_activated_cb), self); + gtk_style_context_add_class (gtk_widget_get_style_context (self->button_details_permissions_value), "content-rating-permissions"); } diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui index fce2ca3af..e4f65d527 100644 --- a/src/gs-details-page.ui +++ b/src/gs-details-page.ui @@ -444,6 +444,60 @@ + + + True + in + fill + start + + + + True + none + + + + + + + + True + True + + + True + horizontal + center + 12 + 12 + + + True + 0 + 0.5 + Version History + + + + + True + go-next-symbolic + 6 + + + + + + + + + + False @@ -713,77 +767,6 @@ - - - True - Version - 0 - 0.5 - True - - - - 0 - 1 - - - - - True - True - True - 0.12.3 - True - end - 0 - 0.5 - - - - - - 1 - 1 - - - - - True - Released - 0 - 0.5 - True - - - - 0 - 2 - - - - - True - True - True - 0.12.3 - True - end - 0 - 0.5 - - - - - - 1 - 2 - - - True @@ -1262,7 +1245,6 @@ True - @@ -1275,7 +1257,6 @@ True - diff --git a/src/meson.build b/src/meson.build index 7e4cf4f66..49d708c6e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -17,6 +17,8 @@ gdbus_src = gnome.gdbus_codegen( gnome_software_sources = [ 'gs-app-addon-row.c', + 'gs-app-version-history-dialog.c', + 'gs-app-version-history-row.c', 'gs-application.c', 'gs-app-row.c', 'gs-app-tile.c', -- GitLab