From efdc814bc2a04fd5a4d460ee4d8e4d07ba3fa895 Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:08:03 +0200 Subject: [PATCH 1/7] Rework Add-ons dialog --- po/POTFILES.in | 4 + src/bazaar.gresource.xml | 1 + src/bz-addon-tile.blp | 89 ++++ src/bz-addon-tile.c | 239 ++++++++++ src/bz-addon-tile.h | 42 ++ src/bz-addons-dialog.blp | 360 +++++++++++++-- src/bz-addons-dialog.c | 772 +++++++++++++++++++++----------- src/bz-addons-dialog.h | 8 +- src/bz-app-size-dialog.blp | 8 +- src/bz-app-size-dialog.c | 35 +- src/bz-app-size-dialog.h | 5 +- src/bz-application.c | 117 ++--- src/bz-context-tile-callbacks.c | 434 ++++++++++++++++++ src/bz-context-tile-callbacks.h | 26 ++ src/bz-entry-group.c | 253 +++++++---- src/bz-entry-group.h | 10 + src/bz-full-view.c | 418 +---------------- src/bz-library-page.c | 3 + src/bz-license-dialog.blp | 3 +- src/bz-license-dialog.c | 30 +- src/bz-license-dialog.h | 5 +- src/bz-share-list.c | 17 +- src/bz-stats-dialog.blp | 7 +- src/bz-stats-dialog.c | 12 +- src/bz-stats-dialog.h | 4 +- src/bz-window.c | 81 +--- src/meson.build | 3 + 27 files changed, 2028 insertions(+), 958 deletions(-) create mode 100644 src/bz-addon-tile.blp create mode 100644 src/bz-addon-tile.c create mode 100644 src/bz-addon-tile.h create mode 100644 src/bz-context-tile-callbacks.c create mode 100644 src/bz-context-tile-callbacks.h diff --git a/po/POTFILES.in b/po/POTFILES.in index e41a4de1..6654b642 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -2,6 +2,8 @@ # Please keep this file sorted alphabetically. data/io.github.kolunmi.Bazaar.desktop.in data/io.github.kolunmi.Bazaar.metainfo.xml.in +src/bz-addon-tile.blp +src/bz-addon-tile.c src/bz-addons-dialog.blp src/bz-addons-dialog.c src/bz-age-rating-dialog.blp @@ -24,6 +26,8 @@ src/bz-comet-overlay.c src/bz-content-provider.c src/bz-context-tile.blp src/bz-context-tile.c +src/bz-context-tile-callbacks.blp +src/bz-context-tile-callbacks.c src/bz-curated-view.blp src/bz-curated-view.c src/bz-data-graph.c diff --git a/src/bazaar.gresource.xml b/src/bazaar.gresource.xml index 71c22f2e..be515610 100644 --- a/src/bazaar.gresource.xml +++ b/src/bazaar.gresource.xml @@ -16,6 +16,7 @@ countries.gvariant + bz-addon-tile.ui bz-addons-dialog.ui bz-donations-dialog.ui bz-age-rating-dialog.ui diff --git a/src/bz-addon-tile.blp b/src/bz-addon-tile.blp new file mode 100644 index 00000000..9bbfaa6a --- /dev/null +++ b/src/bz-addon-tile.blp @@ -0,0 +1,89 @@ +using Gtk 4.0; + +template $BzAddonTile: $BzListTile { + accessibility { + labelled-by: title_label; + described-by: description_label; + } + + child: Box { + orientation: horizontal; + spacing: 10; + height-request: 64; + + Box { + orientation: vertical; + valign: center; + spacing: 4; + margin-start: 10; + + Label title_label { + xalign: 0.0; + ellipsize: end; + single-line-mode: true; + has-tooltip: true; + tooltip-text: bind template.group as <$BzEntryGroup>.id; + label: bind template.group as <$BzEntryGroup>.title; + } + + Label description_label { + halign: start; + label: bind template.group as <$BzEntryGroup>.description; + visible: bind eol_label.visible inverted; + xalign: 0.0; + ellipsize: end; + single-line-mode: true; + styles ["dim-label", "caption"] + } + + Label eol_label{ + visible: bind $invert_boolean($is_null(template.group as <$BzEntryGroup>.eol) as ) as ; + wrap: true; + wrap-mode: word_char; + ellipsize: end; + vexpand: true; + lines: 2; + single-line-mode: true; + halign: start; + hexpand: true; + label: _("Stopped Receiving Updates"); + + styles [ + "warning", + ] + } + } + + Box { + orientation: horizontal; + spacing: 8; + margin-end: 8; + hexpand: true; + halign: end; + + Button install_remove_button { + styles ["flat"] + width-request: 32; + height-request: 32; + valign: center; + has-tooltip: true; + visible: bind $invert_boolean($logical_and($is_zero(template.group as <$BzEntryGroup>.removable) as , $is_zero(template.group as <$BzEntryGroup>.installable) as ) as ) as ; + tooltip-text: bind $get_install_remove_tooltip(template.group as <$BzEntryGroup>.removable) as ; + sensitive: bind $switch_bool( + template.group as <$BzEntryGroup>.removable, + $invert_boolean($is_zero(template.group as <$BzEntryGroup>.removable-and-available) as ) as , + $invert_boolean($is_zero(template.group as <$BzEntryGroup>.installable-and-available) as ) as , + ) as ; + icon-name: bind $get_install_remove_icon(template.group as <$BzEntryGroup>.removable) as ; + clicked => $install_remove_cb() swapped; + } + + Image { + pixel-size: 14; + icon-name: "go-next-symbolic"; + margin-end: 4; + styles ["dimmed"] + } + } + }; +} \ No newline at end of file diff --git a/src/bz-addon-tile.c b/src/bz-addon-tile.c new file mode 100644 index 00000000..b92a2cbe --- /dev/null +++ b/src/bz-addon-tile.c @@ -0,0 +1,239 @@ +/* bz-addon-tile.c + * + * Copyright 2026 Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#include "bz-addon-tile.h" +#include "bz-entry-group.h" +#include "bz-state-info.h" +#include "bz-window.h" + +struct _BzAddonTile +{ + BzListTile parent_instance; + + BzEntryGroup *group; + + GtkButton *install_remove_button; +}; + +G_DEFINE_FINAL_TYPE (BzAddonTile, bz_addon_tile, BZ_TYPE_LIST_TILE) + +enum +{ + PROP_0, + + PROP_GROUP, + + LAST_PROP +}; +static GParamSpec *props[LAST_PROP] = { 0 }; + +static void +install_remove_cb (BzAddonTile *self, + GtkButton *button) +{ + int removable = 0; + + if (self->group == NULL) + return; + + removable = bz_entry_group_get_removable (self->group); + + if (removable > 0) + gtk_widget_activate_action (GTK_WIDGET (self), "window.remove-group", "(sb)", + bz_entry_group_get_id (self->group), FALSE); + else + gtk_widget_activate_action (GTK_WIDGET (self), "window.install-group", "(sb)", + bz_entry_group_get_id (self->group), TRUE); +} + +static void +bz_addon_tile_dispose (GObject *object) +{ + BzAddonTile *self = BZ_ADDON_TILE (object); + + g_clear_object (&self->group); + + G_OBJECT_CLASS (bz_addon_tile_parent_class)->dispose (object); +} + +static void +bz_addon_tile_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + BzAddonTile *self = BZ_ADDON_TILE (object); + + switch (prop_id) + { + case PROP_GROUP: + g_value_set_object (value, bz_addon_tile_get_group (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +bz_addon_tile_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + BzAddonTile *self = BZ_ADDON_TILE (object); + + switch (prop_id) + { + case PROP_GROUP: + bz_addon_tile_set_group (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static gboolean +is_zero (gpointer object, + int value) +{ + return value == 0; +} + +static gboolean +invert_boolean (gpointer object, + gboolean value) +{ + return !value; +} + +static gboolean +is_null (gpointer object, + GObject *value) +{ + return value == NULL; +} + +static gboolean +logical_and (gpointer object, + gboolean value1, + gboolean value2) +{ + return value1 && value2; +} + +static char * +get_install_remove_tooltip (gpointer object, + int removable) +{ + if (removable > 0) + return g_strdup (_ ("Uninstall")); + else + return g_strdup (_ ("Install")); +} + +static char * +get_install_remove_icon (gpointer object, + int removable) +{ + if (removable > 0) + return g_strdup ("user-trash-symbolic"); + else + return g_strdup ("document-save-symbolic"); +} + +static gboolean +switch_bool (gpointer object, + gboolean condition, + gboolean true_value, + gboolean false_value) +{ + return condition ? true_value : false_value; +} + +static void +bz_addon_tile_class_init (BzAddonTileClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = bz_addon_tile_dispose; + object_class->get_property = bz_addon_tile_get_property; + object_class->set_property = bz_addon_tile_set_property; + + props[PROP_GROUP] = + g_param_spec_object ( + "group", + NULL, NULL, + BZ_TYPE_ENTRY_GROUP, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + g_type_ensure (BZ_TYPE_LIST_TILE); + g_type_ensure (BZ_TYPE_ENTRY_GROUP); + + gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-addon-tile.ui"); + gtk_widget_class_bind_template_child (widget_class, BzAddonTile, install_remove_button); + gtk_widget_class_bind_template_callback (widget_class, is_zero); + gtk_widget_class_bind_template_callback (widget_class, invert_boolean); + gtk_widget_class_bind_template_callback (widget_class, is_null); + gtk_widget_class_bind_template_callback (widget_class, logical_and); + gtk_widget_class_bind_template_callback (widget_class, get_install_remove_tooltip); + gtk_widget_class_bind_template_callback (widget_class, get_install_remove_icon); + gtk_widget_class_bind_template_callback (widget_class, switch_bool); + gtk_widget_class_bind_template_callback (widget_class, install_remove_cb); + + gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_BUTTON); +} + +static void +bz_addon_tile_init (BzAddonTile *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget * +bz_addon_tile_new (void) +{ + return g_object_new (BZ_TYPE_ADDON_TILE, NULL); +} + +void +bz_addon_tile_set_group (BzAddonTile *self, + BzEntryGroup *group) +{ + g_return_if_fail (BZ_IS_ADDON_TILE (self)); + g_return_if_fail (group == NULL || BZ_IS_ENTRY_GROUP (group)); + + g_clear_object (&self->group); + if (group != NULL) + self->group = g_object_ref (group); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_GROUP]); +} + +BzEntryGroup * +bz_addon_tile_get_group (BzAddonTile *self) +{ + g_return_val_if_fail (BZ_IS_ADDON_TILE (self), NULL); + return self->group; +} diff --git a/src/bz-addon-tile.h b/src/bz-addon-tile.h new file mode 100644 index 00000000..e84bd0ce --- /dev/null +++ b/src/bz-addon-tile.h @@ -0,0 +1,42 @@ +/* bz-addon-tile.h + * + * Copyright 2026 Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "bz-entry-group.h" +#include "bz-list-tile.h" +#include + +G_BEGIN_DECLS + +#define BZ_TYPE_ADDON_TILE (bz_addon_tile_get_type ()) +G_DECLARE_FINAL_TYPE (BzAddonTile, bz_addon_tile, BZ, ADDON_TILE, BzListTile) + +GtkWidget * +bz_addon_tile_new (void); + +void +bz_addon_tile_set_group (BzAddonTile *self, + BzEntryGroup *group); + +BzEntryGroup * +bz_addon_tile_get_group (BzAddonTile *self); + +G_END_DECLS diff --git a/src/bz-addons-dialog.blp b/src/bz-addons-dialog.blp index f2d32cc5..ecb1b79f 100644 --- a/src/bz-addons-dialog.blp +++ b/src/bz-addons-dialog.blp @@ -2,41 +2,333 @@ using Gtk 4.0; using Adw 1; template $BzAddonsDialog: Adw.Dialog { - content-width: 550; - content-height: 500; - - child: Adw.ToolbarView { - [top] - Adw.HeaderBar { - title-widget: Label { - styles [ - "heading", - ] - - label: _("Manage Add-Ons"); - }; - } - - content: ScrolledWindow { - propagate-natural-height: true; - vexpand: true; - hscrollbar-policy: never; - - child: Adw.Clamp { - maximum-size: 500; - tightening-threshold: 500; - - child: Box { - orientation: vertical; - margin-top: 24; - margin-bottom: 24; - margin-start: 12; - margin-end: 12; - - Adw.PreferencesGroup addons_group { + content-width: 500; + content-height: 550; + + child: Adw.ToastOverlay { + child:Adw.NavigationView navigation_view { + notify::visible-page-tag => $on_visible_page_tag_changed(); + + Adw.NavigationPage { + tag: "list"; + title: _("Manage Add-Ons"); + + child: Adw.ToolbarView { + [top] + Adw.HeaderBar { + title-widget: Label { + styles ["heading"] + label: _("Manage Add-Ons"); + }; + } + + content: ScrolledWindow { + propagate-natural-height: true; + vexpand: true; + hscrollbar-policy: never; + + child: Adw.Clamp list_clamp { + maximum-size: 450; + tightening-threshold: 500; + margin-start: 6; + margin-end: 6; + margin-bottom: 12; + + child: ListView { + styles [ + "navigation-sidebar", + "installed-list-view", + ] + + model: NoSelection { + model: bind template.addon-groups; + }; + + factory: BuilderListItemFactory { + template ListItem { + activatable: false; + selectable: false; + focusable: false; + + child: $BzAddonTile { + group: bind template.item as <$BzEntryGroup>; + activated => $tile_activated_cb(); + }; + } + }; + }; + }; + }; + }; + } + + Adw.NavigationPage { + tag: "full-view"; + + child: Adw.ToolbarView { + [top] + Adw.HeaderBar { + show-title: false; } + + content: ScrolledWindow { + vexpand: true; + hscrollbar-policy: never; + + child: Adw.Clamp { + maximum-size: 450; + tightening-threshold: 500; + margin-start: 12; + margin-end: 12; + + child: Box full_view_clamp { + orientation: vertical; + spacing: 4; + margin-top: 12; + + Box { + halign: center; + spacing: 8; + orientation: vertical; + margin-bottom: 12; + + Image { + pixel-size: 96; + paintable: bind template.parent-ui-entry as <$BzResult>.object as <$BzEntry>.icon-paintable; + visible: bind $invert_boolean($is_null(template.parent-ui-entry as <$BzResult>.object as <$BzEntry>.icon-paintable) as ) as ; + + styles ["icon-dropshadow"] + } + + Box { + orientation: vertical; + valign: center; + halign: center; + spacing: 2; + margin-bottom: 8; + + Label { + label: bind template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.title; + justify: center; + max-width-chars: 25; + wrap: true; + wrap-mode: word_char; + + styles ["title-3"] + } + + Label { + label: bind $format_parent_title(template.parent-ui-entry as <$BzResult>.object as <$BzEntry>.title) as ; + justify: center; + + styles ["dim-label"] + } + } + } + + Box { + homogeneous: true; + margin-bottom: 12; + orientation: horizontal; + halign: fill; + + styles ["app-context-bar"] + + $BzContextTile { + label: bind $get_size_label($is_zero(template.selected-group as <$BzEntryGroup>.removable) as ) as ; + clicked => $size_cb(template); + has-tooltip: true; + tooltip-text: bind $format_size_tooltip(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.size) as ; + lozenge-style: "grey"; + sensitive: bind $invert_boolean($is_zero($get_size_type(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>, $is_zero(template.selected-group as <$BzEntryGroup>.removable) as ) as ) as ) as ; + + lozenge-child: Label { + justify: center; + label: bind $format_size($get_size_type(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>, $is_zero(template.selected-group as <$BzEntryGroup>.removable) as ) as ) as ; + lines: 3; + ellipsize: end; + halign: center; + wrap: true; + xalign: 0.5; + use-markup: true; + }; + } + + $BzContextTile { + label: bind $get_license_label(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>) as ; + clicked => $license_cb(template); + lozenge-style: bind $bool_to_string(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.is-floss, "grey", "warning") as ; + has-tooltip: true; + tooltip-text: bind $format_license_tooltip(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>) as ; + + lozenge-child: Box { + spacing: 6; + + Image { + icon-name: bind $get_license_icon(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.is-floss, 0) as ; + } + + Image { + icon-name: bind $get_license_icon(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.is-floss, 1) as ; + } + }; + } + + $BzContextTile { + can-target: bind $invert_boolean($is_null(template.selected-ui-entry) as ) as ; + sensitive: bind $invert_boolean($is_null(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.recent-downloads) as ) as ; + label: _("Downloads/Month"); + clicked => $dl_stats_cb(template); + has-tooltip: bind $invert_boolean($is_null(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.recent-downloads) as ) as ; + tooltip-text: bind $format_recent_downloads_tooltip(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.recent-downloads) as ; + lozenge-style: "grey"; + + lozenge-child: Label { + justify: center; + label: bind $format_recent_downloads(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.recent-downloads) as ; + halign: center; + use-markup: true; + }; + } + } + + Box { + visible: bind $invert_boolean($is_null(template.selected-group as <$BzEntryGroup>.eol) as ) as ; + orientation: vertical; + spacing: 8; + + styles [ + "card", + "colored", + "warning", + ] + + Label { + label: _("Stopped Receiving Core Updates"); + margin-top: 8; + margin-start: 8; + margin-end: 8; + wrap: true; + wrap-mode: word_char; + justify: center; + + styles [ + "title-4", + ] + } + + Label { + label: _("This add-on uses a runtime that no longer receives updates or security fixes. It may become unsafe to use."); + margin-bottom: 8; + margin-start: 8; + margin-end: 8; + wrap: true; + wrap-mode: word_char; + justify: center; + } + } + + Label { + visible: bind $logical_and($invert_boolean($is_null(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.description) as ) as , $is_null(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.long-description) as ) as ; + label: bind template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.description; + wrap: true; + wrap-mode: word_char; + justify: center; + halign: center; + margin-bottom: 6; + + styles ["dimmed"] + } + + Stack { + transition-type: crossfade; + halign: center; + margin-top: 12; + margin-bottom: 16; + visible-child-name: bind $get_install_stack_page(template.selected-group as <$BzEntryGroup>.installable, template.selected-group as <$BzEntryGroup>.removable) as ; + + StackPage { + name: "install"; + child: Button { + styles ["pill", "suggested-action"] + label: _("Install"); + sensitive: bind $invert_boolean($is_zero(template.selected-group as <$BzEntryGroup>.installable-and-available) as ) as ; + clicked => $install_cb(); + }; + } + + StackPage { + name: "open"; + child: Box { + spacing: 8; + + Button { + styles ["pill"] + label: _("Open"); + sensitive: bind $invert_boolean($is_zero(template.selected-group as <$BzEntryGroup>.removable-and-available) as ) as ; + clicked => $run_cb(); + } + + Button { + styles ["pill", "destructive-action"] + label: _("Remove"); + sensitive: bind $invert_boolean($is_zero(template.selected-group as <$BzEntryGroup>.removable-and-available) as ) as ; + clicked => $remove_cb(); + } + }; + } + + StackPage { + name: "empty"; + child: Adw.Bin {}; + } + } + + Label { + visible: bind $logical_and($invert_boolean($is_null(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.description) as ) as , $invert_boolean($is_null(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.long-description) as ) as ) as ; + label: bind template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.description; + wrap: true; + wrap-mode: word_char; + xalign: 0; + halign: fill; + margin-top: 12; + margin-bottom: 4; + + styles ["title-4"] + } + + $BzFadingClamp fading_clamp { + visible: bind $invert_boolean($is_null(template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.long-description) as ) as ; + max-height: bind $get_description_max_height(description_toggle.active) as ; + min-max-height: 170; + + child: $BzAppstreamDescriptionRender { + appstream-description: bind template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.long-description; + }; + } + + ToggleButton description_toggle { + styles ["circular"] + visible: bind fading_clamp.will-change; + halign: center; + + child: Label { + label: bind $get_description_toggle_text(description_toggle.active) as ; + margin-start: 16; + margin-end: 16; + }; + margin-bottom: 12; + } + + $BzShareList { + urls: bind template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.share-urls; + margin-bottom: 24; + } + }; + }; + }; }; - }; + } }; }; -} +} \ No newline at end of file diff --git a/src/bz-addons-dialog.c b/src/bz-addons-dialog.c index 03f7239c..1500582a 100644 --- a/src/bz-addons-dialog.c +++ b/src/bz-addons-dialog.c @@ -1,6 +1,6 @@ /* bz-addons-dialog.c * - * Copyright 2025 Adam Masciola + * Copyright 2025 Adam Masciola, Alexander Vanhee * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,26 +18,49 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +#include "config.h" + #include +#include "bz-addon-tile.h" #include "bz-addons-dialog.h" -#include "bz-entry.h" -#include "bz-env.h" +#include "bz-app-size-dialog.h" +#include "bz-application-map-factory.h" +#include "bz-application.h" +#include "bz-appstream-description-render.h" +#include "bz-context-tile-callbacks.h" +#include "bz-context-tile.h" +#include "bz-entry-group.h" +#include "bz-fading-clamp.h" #include "bz-flatpak-entry.h" +#include "bz-install-controls.h" +#include "bz-license-dialog.h" #include "bz-result.h" +#include "bz-share-list.h" +#include "bz-state-info.h" +#include "bz-stats-dialog.h" +#include "bz-template-callbacks.h" #include "bz-util.h" struct _BzAddonsDialog { AdwDialog parent_instance; - BzResult *entry; - GListModel *model; + GListModel *addon_groups; + BzEntryGroup *selected_group; + BzResult *selected_ui_entry; + DexFuture *selected_ui_future; + BzResult *parent_ui_entry; + DexFuture *parent_ui_future; - DexFuture *task; + AdwAnimation *width_animation; + AdwAnimation *height_animation; /* Template widgets */ - AdwPreferencesGroup *addons_group; + AdwNavigationView *navigation_view; + GtkToggleButton *description_toggle; + AdwClamp *full_view_clamp; + AdwClamp *list_clamp; }; G_DEFINE_FINAL_TYPE (BzAddonsDialog, bz_addons_dialog, ADW_TYPE_DIALOG) @@ -46,360 +69,589 @@ enum { PROP_0, - PROP_ENTRY, - PROP_MODEL, + PROP_ADDON_GROUPS, + PROP_SELECTED_GROUP, + PROP_SELECTED_UI_ENTRY, + PROP_PARENT_UI_ENTRY, LAST_PROP }; static GParamSpec *props[LAST_PROP] = { 0 }; -enum -{ - SIGNAL_TRANSACT, - - LAST_SIGNAL, -}; -static guint signals[LAST_SIGNAL]; +static char *format_parent_title (gpointer object, const char *title); +static int get_description_max_height (gpointer object, gboolean active); +static char *get_description_toggle_text (gpointer object, gboolean active); +static void size_cb (BzAddonsDialog *self, GtkButton *button); +static void license_cb (BzAddonsDialog *self, GtkButton *button); +static void dl_stats_cb (BzAddonsDialog *self, GtkButton *button); +static void animate_to_size (BzAddonsDialog *self); +static void on_visible_page_tag_changed (AdwNavigationView *nav_view, GParamSpec *pspec, BzAddonsDialog *self); +static char *get_install_stack_page (gpointer object, int installable, int removable); +static void install_cb (GtkButton *button, BzAddonsDialog *self); +static void remove_cb (GtkButton *button, BzAddonsDialog *self); +static void run_cb (GtkButton *button, BzAddonsDialog *self); +static DexFuture *on_parent_ui_entry_resolved (DexFuture *future, GWeakRef *wr); +static DexFuture *on_selected_ui_entry_resolved (DexFuture *future, GWeakRef *wr); +static void set_selected_group (BzAddonsDialog *self, BzEntryGroup *group); +static void tile_activated_cb (BzAddonTile *tile); static void -transact_cb (BzAddonsDialog *self, - GtkButton *button) +bz_addons_dialog_dispose (GObject *object) { - BzEntry *entry = NULL; + BzAddonsDialog *self = BZ_ADDONS_DIALOG (object); - entry = g_object_get_data (G_OBJECT (button), "entry"); - if (entry == NULL) - return; + dex_clear (&self->selected_ui_future); + dex_clear (&self->parent_ui_future); + g_clear_object (&self->addon_groups); + g_clear_object (&self->selected_group); + g_clear_object (&self->selected_ui_entry); + g_clear_object (&self->parent_ui_entry); + g_clear_object (&self->width_animation); + g_clear_object (&self->height_animation); - g_signal_emit (self, signals[SIGNAL_TRANSACT], 0, entry); + G_OBJECT_CLASS (bz_addons_dialog_parent_class)->dispose (object); } static void -update_button_for_entry (GtkButton *button, - BzEntry *entry) +bz_addons_dialog_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) { - gboolean installed = FALSE; - gboolean holding = FALSE; - const char *icon_name; - const char *tooltip_text; - - g_object_get (entry, - "installed", &installed, - "holding", &holding, - NULL); + BzAddonsDialog *self = BZ_ADDONS_DIALOG (object); - if (installed) + switch (prop_id) { - icon_name = "user-trash-symbolic"; - tooltip_text = _ ("Remove"); + case PROP_ADDON_GROUPS: + g_value_set_object (value, self->addon_groups); + break; + case PROP_SELECTED_GROUP: + g_value_set_object (value, self->selected_group); + break; + case PROP_SELECTED_UI_ENTRY: + g_value_set_object (value, self->selected_ui_entry); + break; + case PROP_PARENT_UI_ENTRY: + g_value_set_object (value, self->parent_ui_entry); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } - else +} + +static void +bz_addons_dialog_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + BzAddonsDialog *self = BZ_ADDONS_DIALOG (object); + + switch (prop_id) { - icon_name = "folder-download-symbolic"; - tooltip_text = _ ("Install"); + case PROP_ADDON_GROUPS: + g_clear_object (&self->addon_groups); + self->addon_groups = g_value_dup_object (value); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ADDON_GROUPS]); + break; + case PROP_SELECTED_GROUP: + g_clear_object (&self->selected_group); + self->selected_group = g_value_dup_object (value); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_GROUP]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } - - gtk_button_set_icon_name (button, icon_name); - gtk_widget_set_tooltip_text (GTK_WIDGET (button), tooltip_text); - gtk_widget_set_sensitive (GTK_WIDGET (button), !holding); } static void -entry_notify_cb (BzEntry *entry, - GParamSpec *pspec, - AdwActionRow *action_row) +bz_addons_dialog_class_init (BzAddonsDialogClass *klass) { - g_autofree char *title = NULL; - g_autofree char *description = NULL; - GtkButton *action_button = NULL; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); - action_button = g_object_get_data (G_OBJECT (action_row), "button"); - if (action_button == NULL) - return; + object_class->dispose = bz_addons_dialog_dispose; + object_class->get_property = bz_addons_dialog_get_property; + object_class->set_property = bz_addons_dialog_set_property; - g_object_get (entry, - "title", &title, - "description", &description, - NULL); + props[PROP_ADDON_GROUPS] = + g_param_spec_object ( + "addon-groups", + NULL, NULL, + G_TYPE_LIST_MODEL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); - adw_preferences_row_set_title (ADW_PREFERENCES_ROW (action_row), title); - adw_action_row_set_subtitle (action_row, description); + props[PROP_SELECTED_GROUP] = + g_param_spec_object ( + "selected-group", + NULL, NULL, + BZ_TYPE_ENTRY_GROUP, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); - update_button_for_entry (action_button, entry); + props[PROP_SELECTED_UI_ENTRY] = + g_param_spec_object ( + "selected-ui-entry", + NULL, NULL, + BZ_TYPE_RESULT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_PARENT_UI_ENTRY] = + g_param_spec_object ( + "parent-ui-entry", + NULL, NULL, + BZ_TYPE_RESULT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + g_type_ensure (BZ_TYPE_ADDON_TILE); + g_type_ensure (BZ_TYPE_APPSTREAM_DESCRIPTION_RENDER); + g_type_ensure (BZ_TYPE_CONTEXT_TILE); + g_type_ensure (BZ_TYPE_ENTRY); + g_type_ensure (BZ_TYPE_ENTRY_GROUP); + g_type_ensure (BZ_TYPE_FADING_CLAMP); + g_type_ensure (BZ_TYPE_FLATPAK_ENTRY); + g_type_ensure (BZ_TYPE_INSTALL_CONTROLS); + g_type_ensure (BZ_TYPE_RESULT); + g_type_ensure (BZ_TYPE_SHARE_LIST); + + gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-addons-dialog.ui"); + + bz_widget_class_bind_all_util_callbacks (widget_class); + bz_widget_class_bind_all_context_tile_callbacks (widget_class); + + gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, navigation_view); + gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, description_toggle); + gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, full_view_clamp); + gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, list_clamp); + + gtk_widget_class_bind_template_callback (widget_class, tile_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_visible_page_tag_changed); + gtk_widget_class_bind_template_callback (widget_class, format_parent_title); + gtk_widget_class_bind_template_callback (widget_class, get_description_max_height); + gtk_widget_class_bind_template_callback (widget_class, get_description_toggle_text); + gtk_widget_class_bind_template_callback (widget_class, get_install_stack_page); + gtk_widget_class_bind_template_callback (widget_class, install_cb); + gtk_widget_class_bind_template_callback (widget_class, remove_cb); + gtk_widget_class_bind_template_callback (widget_class, run_cb); + + gtk_widget_class_bind_template_callback (widget_class, license_cb); + gtk_widget_class_bind_template_callback (widget_class, size_cb); + gtk_widget_class_bind_template_callback (widget_class, dl_stats_cb); } -static AdwActionRow * -make_action_row (BzAddonsDialog *self, - BzEntry *entry) +static void +bz_addons_dialog_init (BzAddonsDialog *self) { - AdwActionRow *action_row = NULL; - const char *flatpak_version = NULL; - const char *title = NULL; - const char *description = NULL; - g_autofree char *title_text = NULL; - GtkButton *action_button = NULL; + AdwAnimationTarget *width_target = NULL; + AdwAnimationTarget *height_target = NULL; - action_row = ADW_ACTION_ROW (adw_action_row_new ()); - adw_preferences_row_set_use_markup (ADW_PREFERENCES_ROW (action_row), FALSE); + gtk_widget_init_template (GTK_WIDGET (self)); - flatpak_version = bz_flatpak_entry_get_flatpak_version (BZ_FLATPAK_ENTRY (entry)); - title = bz_entry_get_title (entry); - description = bz_entry_get_description (entry); + width_target = adw_property_animation_target_new (G_OBJECT (self), "content-width"); + self->width_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 200, width_target); + adw_timed_animation_set_easing (ADW_TIMED_ANIMATION (self->width_animation), ADW_EASE_IN_OUT_CUBIC); - title_text = g_strdup_printf ("%s %s", title, flatpak_version); - adw_preferences_row_set_title (ADW_PREFERENCES_ROW (action_row), title_text); - adw_preferences_row_set_use_markup (ADW_PREFERENCES_ROW (action_row), TRUE); + height_target = adw_property_animation_target_new (G_OBJECT (self), "content-height"); + self->height_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 200, height_target); + adw_timed_animation_set_easing (ADW_TIMED_ANIMATION (self->height_animation), ADW_EASE_IN_OUT_CUBIC); +} - adw_action_row_set_subtitle (action_row, description); +AdwDialog * +bz_addons_dialog_new (BzEntryGroup *group) +{ + GListModel *ids = NULL; + GListModel *groups = NULL; + BzApplicationMapFactory *factory = NULL; + BzAddonsDialog *self = NULL; - action_button = GTK_BUTTON (gtk_button_new ()); - gtk_widget_set_valign (GTK_WIDGET (action_button), GTK_ALIGN_CENTER); - gtk_widget_add_css_class (GTK_WIDGET (action_button), "flat"); - g_object_set_data_full (G_OBJECT (action_button), "entry", g_object_ref (entry), g_object_unref); - g_signal_connect_swapped (action_button, "clicked", - G_CALLBACK (transact_cb), self); + ids = bz_entry_group_get_addon_group_ids (group); + if (ids != NULL) + { + factory = bz_state_info_get_application_factory (bz_state_info_get_default ()); + if (factory != NULL) + groups = bz_application_map_factory_generate (factory, ids); + } - update_button_for_entry (action_button, entry); + self = g_object_new ( + BZ_TYPE_ADDONS_DIALOG, + "addon-groups", groups, + NULL); - g_object_set_data (G_OBJECT (action_row), "button", action_button); + if (groups != NULL && g_list_model_get_n_items (groups) == 1) + { + g_autoptr (BzEntryGroup) single = g_list_model_get_item (groups, 0); + AdwNavigationPage *full_view_page = NULL; - adw_action_row_add_suffix (action_row, GTK_WIDGET (action_button)); - adw_action_row_set_activatable_widget (action_row, GTK_WIDGET (action_button)); + set_selected_group (self, single); - g_signal_connect_object (entry, "notify::installed", - G_CALLBACK (entry_notify_cb), - action_row, G_CONNECT_DEFAULT); - g_signal_connect_object (entry, "notify::holding", - G_CALLBACK (entry_notify_cb), - action_row, G_CONNECT_DEFAULT); + adw_navigation_view_push_by_tag (self->navigation_view, "full-view"); + full_view_page = adw_navigation_view_find_page (self->navigation_view, "full-view"); + adw_navigation_page_set_can_pop (full_view_page, FALSE); + } + else + g_idle_add_once ((GSourceOnceFunc) animate_to_size, self); - return action_row; + return ADW_DIALOG (self); } -static gint -cmp_future (DexFuture *a, - DexFuture *b) +AdwDialog * +bz_addons_dialog_new_single (BzEntryGroup *group) { - const GValue *a_val = NULL; - const GValue *b_val = NULL; - BzEntry *a_entry = NULL; - BzEntry *b_entry = NULL; + BzAddonsDialog *self = NULL; + AdwNavigationPage *full_view = NULL; - a_val = dex_future_get_value (a, NULL); - b_val = dex_future_get_value (b, NULL); + self = g_object_new (BZ_TYPE_ADDONS_DIALOG, NULL); - if (a_val == NULL || b_val == NULL) - return 0; + set_selected_group (self, group); - a_entry = g_value_get_object (a_val); - b_entry = g_value_get_object (b_val); + adw_navigation_view_push_by_tag (self->navigation_view, "full-view"); - return strcasecmp (bz_entry_get_title (a_entry), - bz_entry_get_title (b_entry)); + full_view = adw_navigation_view_find_page (self->navigation_view, "full-view"); + if (full_view != NULL) + adw_navigation_page_set_can_pop (full_view, FALSE); + + return ADW_DIALOG (self); } -static DexFuture * -populate_addons_fiber (GWeakRef *wr) +static char * +format_parent_title (gpointer object, + const char *title) { - g_autoptr (BzAddonsDialog) self = NULL; - guint n_results = 0; - g_autoptr (GPtrArray) futures = NULL; + if (title == NULL || *title == '\0') + return g_strdup (""); - bz_weak_get_or_return_reject (self, wr); + return g_strdup_printf (_ ("Add-on for %s"), title); +} - n_results = g_list_model_get_n_items (self->model); - futures = g_ptr_array_new_with_free_func (dex_unref); - for (guint i = 0; i < n_results; i++) - { - g_autoptr (BzResult) result = NULL; - g_autoptr (DexFuture) future = NULL; +static int +get_description_max_height (gpointer object, + gboolean active) +{ + return active ? 10000 : 170; +} - result = g_list_model_get_item (self->model, i); - future = bz_result_dup_future (result); - if (future != NULL) - g_ptr_array_add (futures, g_steal_pointer (&future)); - } - dex_await ( - dex_future_allv ( - (DexFuture *const *) futures->pdata, - futures->len), - NULL); - g_ptr_array_sort_values (futures, (GCompareFunc) cmp_future); +static char * +get_description_toggle_text (gpointer object, + gboolean active) +{ + return g_strdup (active ? _ ("Show Less") : _ ("Show More")); +} - for (guint i = 0; i < futures->len; i++) - { - DexFuture *future = NULL; - const GValue *value = NULL; - BzEntry *entry = NULL; - const char *id = NULL; - AdwActionRow *action_row = NULL; - - future = g_ptr_array_index (futures, i); - value = dex_future_get_value (future, NULL); - if (value == NULL) - continue; - entry = g_value_get_object (value); - - id = bz_entry_get_id (entry); - if (strstr (id, ".Debug") != NULL || - strstr (id, ".Locale") != NULL) - continue; - - action_row = make_action_row (self, entry); - if (action_row != NULL) - adw_preferences_group_add (self->addons_group, GTK_WIDGET (action_row)); - } +static void +size_cb (BzAddonsDialog *self, + GtkButton *button) +{ + AdwNavigationPage *page = NULL; - return dex_future_new_true (); + if (self->selected_group == NULL) + return; + + page = bz_app_size_page_new (self->selected_group); + adw_navigation_view_push (self->navigation_view, page); } static void -populate_addons (BzAddonsDialog *self) +license_cb (BzAddonsDialog *self, + GtkButton *button) { - if (self->model == NULL || - self->addons_group == NULL) + AdwNavigationPage *page = NULL; + BzEntry *ui_entry = NULL; + + if (self->selected_ui_entry == NULL) return; - dex_clear (&self->task); - self->task = dex_scheduler_spawn ( - dex_scheduler_get_default (), - bz_get_dex_stack_size (), - (DexFiberFunc) populate_addons_fiber, - bz_track_weak (self), - bz_weak_release); + ui_entry = bz_result_get_object (self->selected_ui_entry); + if (ui_entry == NULL) + return; + + page = bz_license_page_new (ui_entry); + adw_navigation_view_push (self->navigation_view, page); } static void -bz_addons_dialog_dispose (GObject *object) +dl_stats_cb (BzAddonsDialog *self, + GtkButton *button) { - BzAddonsDialog *self = BZ_ADDONS_DIALOG (object); + BzStatsDialog *bin = NULL; + AdwNavigationPage *page = NULL; + BzEntry *ui_entry = NULL; - g_clear_object (&self->entry); - g_clear_object (&self->model); - dex_clear (&self->task); + if (self->selected_ui_entry == NULL) + return; - G_OBJECT_CLASS (bz_addons_dialog_parent_class)->dispose (object); + ui_entry = bz_result_get_object (self->selected_ui_entry); + if (ui_entry == NULL) + return; + + bin = BZ_STATS_DIALOG (bz_stats_dialog_new (NULL, NULL, 0)); + page = adw_navigation_page_new (GTK_WIDGET (bin), _ ("Download Stats")); + adw_navigation_page_set_tag (page, "stats"); + + g_object_bind_property (ui_entry, "download-stats", bin, "model", G_BINDING_SYNC_CREATE); + g_object_bind_property (ui_entry, "total-downloads", bin, "total-downloads", G_BINDING_SYNC_CREATE); + + adw_navigation_view_push (self->navigation_view, page); + bz_stats_dialog_animate_open (bin); } static void -bz_addons_dialog_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) +animate_to_size (BzAddonsDialog *self) { - BzAddonsDialog *self = BZ_ADDONS_DIALOG (object); + const char *tag = NULL; + int target_width = 0; + int target_height = 0; + int nat = 0; + int cur_width = 0; + int measure_for = 0; - switch (prop_id) + tag = adw_navigation_view_get_visible_page_tag (self->navigation_view); + + if (g_strcmp0 (tag, "list") == 0) { - case PROP_ENTRY: - g_value_set_object (value, self->entry); - break; - case PROP_MODEL: - g_value_set_object (value, self->model); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + cur_width = gtk_widget_get_width (GTK_WIDGET (self)); + target_width = 500; + measure_for = MIN (target_width, cur_width) - 48; + gtk_widget_measure (GTK_WIDGET (self->list_clamp), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL); + target_height = CLAMP (nat + 50, 300, 600); + } + else if (g_strcmp0 (tag, "full-view") == 0) + { + cur_width = gtk_widget_get_width (GTK_WIDGET (self)); + target_width = 500; + measure_for = MIN (target_width, cur_width) - 48; + gtk_widget_measure (GTK_WIDGET (self->full_view_clamp), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL); + target_height = CLAMP (nat + 50, 300, 700); } + else if (g_strcmp0 (tag, "app-size") == 0) + { + target_width = 500; + target_height = 300; + } + else if (g_strcmp0 (tag, "license") == 0) + { + cur_width = gtk_widget_get_width (GTK_WIDGET (self)); + target_width = 400; + measure_for = target_width - 48; + gtk_widget_measure (GTK_WIDGET (self->navigation_view), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL); + target_height = CLAMP (nat + 0, 300, 700); + } + else if (g_strcmp0 (tag, "stats") == 0) + { + target_width = 1250; + target_height = 750; + } + else + return; + + adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->width_animation), adw_dialog_get_content_width (ADW_DIALOG (self))); + adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->width_animation), target_width); + adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->height_animation), adw_dialog_get_content_height (ADW_DIALOG (self))); + adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->height_animation), target_height); + adw_animation_play (self->width_animation); + adw_animation_play (self->height_animation); } static void -bz_addons_dialog_set_property (GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec) +on_visible_page_tag_changed (AdwNavigationView *nav_view, + GParamSpec *pspec, + BzAddonsDialog *self) { - BzAddonsDialog *self = BZ_ADDONS_DIALOG (object); + g_idle_add_once ((GSourceOnceFunc) animate_to_size, self); +} - switch (prop_id) - { - case PROP_ENTRY: - g_clear_object (&self->entry); - self->entry = g_value_dup_object (value); - break; - case PROP_MODEL: - g_clear_object (&self->model); - self->model = g_value_dup_object (value); - populate_addons (self); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - } +static char * +get_install_stack_page (gpointer object, + int installable, + int removable) +{ + if (removable > 0) + return g_strdup ("open"); + else if (installable > 0) + return g_strdup ("install"); + else + return g_strdup ("empty"); } static void -bz_addons_dialog_constructed (GObject *object) +install_cb (GtkButton *button, + BzAddonsDialog *self) { - BzAddonsDialog *self = BZ_ADDONS_DIALOG (object); - - G_OBJECT_CLASS (bz_addons_dialog_parent_class)->constructed (object); + if (self->selected_group == NULL) + return; + gtk_widget_activate_action (GTK_WIDGET (self), "window.install-group", "(sb)", + bz_entry_group_get_id (self->selected_group), TRUE); +} - if (self->model && self->addons_group) - populate_addons (self); +static void +remove_cb (GtkButton *button, + BzAddonsDialog *self) +{ + if (self->selected_group == NULL) + return; + gtk_widget_activate_action (GTK_WIDGET (self), "window.remove-group", "(sb)", + bz_entry_group_get_id (self->selected_group), TRUE); } static void -bz_addons_dialog_class_init (BzAddonsDialogClass *klass) +run_cb (GtkButton *button, + BzAddonsDialog *self) { - GObjectClass *object_class = G_OBJECT_CLASS (klass); - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + BzEntry *entry = NULL; + entry = bz_result_get_object (self->parent_ui_entry); - object_class->dispose = bz_addons_dialog_dispose; - object_class->constructed = bz_addons_dialog_constructed; - object_class->get_property = bz_addons_dialog_get_property; - object_class->set_property = bz_addons_dialog_set_property; + gtk_widget_activate_action (GTK_WIDGET (self), "window.launch-group", "s", + bz_entry_get_id (entry)); +} - props[PROP_ENTRY] = - g_param_spec_object ( - "entry", - NULL, NULL, - BZ_TYPE_ENTRY, - G_PARAM_READWRITE); +static DexFuture * +on_parent_ui_entry_resolved (DexFuture *future, + GWeakRef *wr) +{ + g_autoptr (BzAddonsDialog) self = NULL; - props[PROP_MODEL] = - g_param_spec_object ( - "model", - NULL, NULL, - G_TYPE_LIST_MODEL, - G_PARAM_READWRITE); + bz_weak_get_or_return_reject (self, wr); - g_object_class_install_properties (object_class, LAST_PROP, props); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARENT_UI_ENTRY]); - signals[SIGNAL_TRANSACT] = - g_signal_new ( - "transact", - G_OBJECT_CLASS_TYPE (klass), - G_SIGNAL_RUN_FIRST, - 0, - NULL, NULL, - g_cclosure_marshal_VOID__OBJECT, - G_TYPE_NONE, 1, - BZ_TYPE_ENTRY); - g_signal_set_va_marshaller ( - signals[SIGNAL_TRANSACT], - G_TYPE_FROM_CLASS (klass), - g_cclosure_marshal_VOID__OBJECTv); + return dex_future_new_for_boolean (TRUE); +} - gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-addons-dialog.ui"); - gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, addons_group); +static DexFuture * +on_selected_ui_entry_resolved (DexFuture *future, + GWeakRef *wr) +{ + g_autoptr (BzAddonsDialog) self = NULL; + const GValue *value = NULL; + g_autoptr (BzEntry) ui_entry = NULL; + const char *ref = NULL; + g_auto (GStrv) parts = NULL; + BzApplicationMapFactory *factory = NULL; + GtkStringObject *item = NULL; + BzEntryGroup *parent_group = NULL; + + bz_weak_get_or_return_reject (self, wr); + + value = dex_future_get_value (future, NULL); + if (value == NULL || !G_VALUE_HOLDS_OBJECT (value)) + return dex_future_new_for_boolean (TRUE); + + ui_entry = g_value_dup_object (value); + if (ui_entry == NULL || !BZ_IS_FLATPAK_ENTRY (ui_entry)) + return dex_future_new_for_boolean (TRUE); + + ref = bz_flatpak_entry_get_addon_extension_of_ref (BZ_FLATPAK_ENTRY (ui_entry)); + if (ref == NULL) + return dex_future_new_for_boolean (TRUE); + + parts = g_strsplit (ref, "/", -1); + if (parts[0] == NULL || parts[1] == NULL) + return dex_future_new_for_boolean (TRUE); + + factory = bz_state_info_get_application_factory (bz_state_info_get_default ()); + item = gtk_string_object_new (parts[1]); + parent_group = bz_application_map_factory_convert_one (factory, item); + + if (parent_group == NULL) + return dex_future_new_for_boolean (TRUE); + + g_clear_object (&self->parent_ui_entry); + self->parent_ui_entry = bz_entry_group_dup_ui_entry (parent_group); + + if (self->parent_ui_entry == NULL) + return dex_future_new_for_boolean (TRUE); + + if (bz_result_get_resolved (self->parent_ui_entry)) + { + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARENT_UI_ENTRY]); + } + else + { + g_autoptr (DexFuture) parent_future = NULL; + GWeakRef *parent_wr = NULL; + + parent_future = bz_result_dup_future (self->parent_ui_entry); + parent_wr = bz_track_weak (self); + parent_future = dex_future_then ( + parent_future, + (DexFutureCallback) on_parent_ui_entry_resolved, + parent_wr, + bz_weak_release); + dex_clear (&self->parent_ui_future); + self->parent_ui_future = g_steal_pointer (&parent_future); + } + + return dex_future_new_for_boolean (TRUE); } static void -bz_addons_dialog_init (BzAddonsDialog *self) +set_selected_group (BzAddonsDialog *self, + BzEntryGroup *group) { - gtk_widget_init_template (GTK_WIDGET (self)); + dex_clear (&self->selected_ui_future); + dex_clear (&self->parent_ui_future); + g_clear_object (&self->selected_group); + g_clear_object (&self->selected_ui_entry); + g_clear_object (&self->parent_ui_entry); + + gtk_toggle_button_set_active (self->description_toggle, FALSE); + + if (group == NULL) + return; + + self->selected_group = g_object_ref (group); + self->selected_ui_entry = bz_entry_group_dup_ui_entry (group); + + if (self->selected_ui_entry == NULL) + goto notify; + + if (bz_result_get_resolved (self->selected_ui_entry)) + { + g_autoptr (BzEntry) entry = g_object_ref (bz_result_get_object (self->selected_ui_entry)); + g_autoptr (DexFuture) object_future = NULL; + GWeakRef *wr = NULL; + + object_future = dex_future_new_for_object (entry); + wr = bz_track_weak (self); + dex_unref (on_selected_ui_entry_resolved (object_future, wr)); + bz_weak_release (wr); + } + else + { + g_autoptr (DexFuture) ui_future = NULL; + GWeakRef *wr = NULL; + + ui_future = bz_result_dup_future (self->selected_ui_entry); + wr = bz_track_weak (self); + ui_future = dex_future_then ( + ui_future, + (DexFutureCallback) on_selected_ui_entry_resolved, + wr, + bz_weak_release); + self->selected_ui_future = g_steal_pointer (&ui_future); + } + +notify: + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_GROUP]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_UI_ENTRY]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARENT_UI_ENTRY]); } -AdwDialog * -bz_addons_dialog_new (BzEntry *entry, - GListModel *model) +static void +tile_activated_cb (BzAddonTile *tile) { - BzAddonsDialog *addons_dialog = NULL; + BzAddonsDialog *self = NULL; + BzEntryGroup *group = NULL; - addons_dialog = g_object_new ( - BZ_TYPE_ADDONS_DIALOG, - "entry", entry, - "model", model, - NULL); + self = BZ_ADDONS_DIALOG (gtk_widget_get_ancestor (GTK_WIDGET (tile), BZ_TYPE_ADDONS_DIALOG)); + if (self == NULL) + return; + + group = bz_addon_tile_get_group (tile); + if (group == NULL) + return; + + set_selected_group (self, group); - return ADW_DIALOG (addons_dialog); + adw_navigation_view_push_by_tag (self->navigation_view, "full-view"); } diff --git a/src/bz-addons-dialog.h b/src/bz-addons-dialog.h index 66f76e3c..be5ebba0 100644 --- a/src/bz-addons-dialog.h +++ b/src/bz-addons-dialog.h @@ -22,7 +22,7 @@ #include -#include "bz-entry.h" +#include "bz-entry-group.h" G_BEGIN_DECLS @@ -30,7 +30,9 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (BzAddonsDialog, bz_addons_dialog, BZ, ADDONS_DIALOG, AdwDialog) AdwDialog * -bz_addons_dialog_new (BzEntry *entry, - GListModel *model); +bz_addons_dialog_new (BzEntryGroup *group); + +AdwDialog * +bz_addons_dialog_new_single (BzEntryGroup *group); G_END_DECLS diff --git a/src/bz-app-size-dialog.blp b/src/bz-app-size-dialog.blp index bc9a971c..4cd9302e 100644 --- a/src/bz-app-size-dialog.blp +++ b/src/bz-app-size-dialog.blp @@ -1,10 +1,7 @@ using Gtk 4.0; using Adw 1; -template $BzAppSizeDialog: Adw.Dialog { - content-height: 500; - content-width: 600; - +template $BzAppSizeDialog: Adw.Bin { child: Adw.ToolbarView { [top] Adw.HeaderBar { @@ -78,8 +75,7 @@ template $BzAppSizeDialog: Adw.Dialog { } Adw.ActionRow { - visible: bind $invert_boolean($is_null(template.group as <$BzEntryGroup>.ui-entry as <$BzResult>.object as <$BzFlatpakEntry>.runtime as <$BzResult>.object) as ) as ; - + visible: bind $invert_boolean($is_null(template.group as <$BzEntryGroup>.ui-entry as <$BzResult>.object as <$BzFlatpakEntry>.runtime as <$BzResult>) as ) as ; [prefix] Label { label: bind $format_size($choose(template.group as <$BzEntryGroup>.ui-entry as <$BzResult>.object as <$BzFlatpakEntry>.runtime as <$BzResult>.object as <$BzEntry>.installed, template.group as <$BzEntryGroup>.ui-entry as <$BzResult>.object as <$BzFlatpakEntry>.runtime as <$BzResult>.object as <$BzEntry>.installed-size as , template.group as <$BzEntryGroup>.ui-entry as <$BzResult>.object as <$BzFlatpakEntry>.runtime as <$BzResult>.object as <$BzEntry>.size as ) as ) as ; diff --git a/src/bz-app-size-dialog.c b/src/bz-app-size-dialog.c index 0983ba0e..f818e1f4 100644 --- a/src/bz-app-size-dialog.c +++ b/src/bz-app-size-dialog.c @@ -28,12 +28,12 @@ struct _BzAppSizeDialog { - AdwDialog parent_instance; + AdwBin parent_instance; BzEntryGroup *group; }; -G_DEFINE_FINAL_TYPE (BzAppSizeDialog, bz_app_size_dialog, ADW_TYPE_DIALOG) +G_DEFINE_FINAL_TYPE (BzAppSizeDialog, bz_app_size_dialog, ADW_TYPE_BIN) enum { @@ -100,7 +100,8 @@ get_runtime_size_title (gpointer object, } static char * -format_size (gpointer object, guint64 value) +format_size (gpointer object, + guint64 value) { g_autofree char *size_str = g_format_size (value); char *space = g_strrstr (size_str, "\xC2\xA0"); @@ -180,12 +181,28 @@ bz_app_size_dialog_init (BzAppSizeDialog *self) AdwDialog * bz_app_size_dialog_new (BzEntryGroup *group) { - BzAppSizeDialog *app_size_dialog = NULL; + BzAppSizeDialog *widget = NULL; + AdwDialog *dialog = NULL; - app_size_dialog = g_object_new ( - BZ_TYPE_APP_SIZE_DIALOG, - "group", group, - NULL); + widget = g_object_new (BZ_TYPE_APP_SIZE_DIALOG, "group", group, NULL); - return ADW_DIALOG (app_size_dialog); + dialog = adw_dialog_new (); + adw_dialog_set_content_height (dialog, 500); + adw_dialog_set_content_width (dialog, 600); + adw_dialog_set_child (dialog, GTK_WIDGET (widget)); + + return dialog; +} + +AdwNavigationPage * +bz_app_size_page_new (BzEntryGroup *group) +{ + BzAppSizeDialog *widget = NULL; + AdwNavigationPage *page = NULL; + + widget = g_object_new (BZ_TYPE_APP_SIZE_DIALOG, "group", group, NULL); + page = adw_navigation_page_new (GTK_WIDGET (widget), _ ("App Size")); + adw_navigation_page_set_tag (page, "app-size"); + + return page; } diff --git a/src/bz-app-size-dialog.h b/src/bz-app-size-dialog.h index accc3f80..1ce45c8d 100644 --- a/src/bz-app-size-dialog.h +++ b/src/bz-app-size-dialog.h @@ -27,9 +27,12 @@ G_BEGIN_DECLS #define BZ_TYPE_APP_SIZE_DIALOG (bz_app_size_dialog_get_type ()) -G_DECLARE_FINAL_TYPE (BzAppSizeDialog, bz_app_size_dialog, BZ, APP_SIZE_DIALOG, AdwDialog) +G_DECLARE_FINAL_TYPE (BzAppSizeDialog, bz_app_size_dialog, BZ, APP_SIZE_DIALOG, AdwBin) AdwDialog * bz_app_size_dialog_new (BzEntryGroup *group); +AdwNavigationPage * +bz_app_size_page_new (BzEntryGroup *group); + G_END_DECLS diff --git a/src/bz-application.c b/src/bz-application.c index 4fb4adb9..a5cc8c16 100644 --- a/src/bz-application.c +++ b/src/bz-application.c @@ -1904,6 +1904,43 @@ watch_backend_notifs_then_loop_cb (DexFuture *future, return g_steal_pointer (&ret_future); } +static BzEntryGroup * +ensure_group_and_add (BzApplication *self, + const char *id, + BzEntry *entry, + BzEntry *eol_runtime, + gboolean ignore_eol, + gboolean installed) +{ + BzEntryGroup *group = NULL; + + group = g_hash_table_lookup (self->ids_to_groups, id); + if (group != NULL) + { + bz_entry_group_add (group, entry, eol_runtime, ignore_eol); + } + else + { + g_autoptr (BzEntryGroup) new_group = NULL; + + g_debug ("Creating new application group for id %s", id); + new_group = bz_entry_group_new (self->entry_factory); + bz_entry_group_add (new_group, entry, eol_runtime, ignore_eol); + + g_list_store_append (self->groups, new_group); + g_hash_table_replace (self->ids_to_groups, g_strdup (id), g_object_ref (new_group)); + + group = new_group; + } + + if (installed && !g_list_store_find (self->installed_apps, group, NULL)) + g_list_store_insert_sorted ( + self->installed_apps, group, + (GCompareDataFunc) cmp_group, NULL); + + return group; +} + static void fiber_replace_entry (BzApplication *self, BzEntry *entry) @@ -1915,6 +1952,7 @@ fiber_replace_entry (BzApplication *self, gboolean installed = FALSE; const char *flatpak_id = NULL; const char *version = NULL; + GHashTable *name_to_addons = NULL; id = bz_entry_get_id (entry); unique_id = bz_entry_get_unique_id (entry); @@ -1924,6 +1962,7 @@ fiber_replace_entry (BzApplication *self, unique_id_checksum == NULL) return; user = bz_flatpak_entry_is_user (BZ_FLATPAK_ENTRY (entry)); + name_to_addons = user ? self->usr_name_to_addons : self->sys_name_to_addons; installed = g_hash_table_contains (self->installed_set, unique_id); bz_entry_set_installed (entry, installed); @@ -1937,11 +1976,7 @@ fiber_replace_entry (BzApplication *self, { GPtrArray *addons = NULL; - addons = g_hash_table_lookup ( - user - ? self->usr_name_to_addons - : self->sys_name_to_addons, - flatpak_id); + addons = g_hash_table_lookup (name_to_addons, flatpak_id); if (addons != NULL) { g_debug ("Appending %d addons to %s", addons->len, unique_id); @@ -1952,23 +1987,17 @@ fiber_replace_entry (BzApplication *self, addon_id = g_ptr_array_index (addons, i); bz_entry_append_addon (entry, addon_id); } - g_hash_table_remove ( - user - ? self->usr_name_to_addons - : self->sys_name_to_addons, - flatpak_id); + g_hash_table_remove (name_to_addons, flatpak_id); addons = NULL; } } if (bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_APPLICATION)) { - BzEntryGroup *group = NULL; - gboolean ignore_eol = FALSE; - const char *runtime_name = NULL; - BzEntry *eol_runtime = NULL; + gboolean ignore_eol = FALSE; + const char *runtime_name = NULL; + BzEntry *eol_runtime = NULL; - group = g_hash_table_lookup (self->ids_to_groups, id); if (self->ignore_eol_set != NULL) ignore_eol = g_hash_table_contains (self->ignore_eol_set, id); @@ -1976,30 +2005,7 @@ fiber_replace_entry (BzApplication *self, if (!ignore_eol && runtime_name != NULL) eol_runtime = g_hash_table_lookup (self->eol_runtimes, runtime_name); - if (group != NULL) - { - bz_entry_group_add (group, entry, eol_runtime, ignore_eol); - if (installed && !g_list_store_find (self->installed_apps, group, NULL)) - g_list_store_insert_sorted ( - self->installed_apps, group, - (GCompareDataFunc) cmp_group, NULL); - } - else - { - g_autoptr (BzEntryGroup) new_group = NULL; - - g_debug ("Creating new application group for id %s", id); - new_group = bz_entry_group_new (self->entry_factory); - bz_entry_group_add (new_group, entry, eol_runtime, ignore_eol); - - g_list_store_append (self->groups, new_group); - g_hash_table_replace (self->ids_to_groups, g_strdup (id), g_object_ref (new_group)); - - if (installed) - g_list_store_insert_sorted ( - self->installed_apps, new_group, - (GCompareDataFunc) cmp_group, NULL); - } + ensure_group_and_add (self, id, entry, eol_runtime, ignore_eol, installed); } if (flatpak_id != NULL && @@ -2029,23 +2035,35 @@ fiber_replace_entry (BzApplication *self, extension_of_what = bz_flatpak_entry_get_addon_extension_of_ref ( BZ_FLATPAK_ENTRY (entry)); + + if (extension_of_what != NULL && + g_str_has_prefix (extension_of_what, "app/")) + { + g_auto (GStrv) parts = NULL; + BzEntryGroup *app_group = NULL; + + ensure_group_and_add (self, id, entry, NULL, FALSE, installed); + + parts = g_strsplit (extension_of_what, "/", -1); + if (parts != NULL && parts[1] != NULL) + { + app_group = g_hash_table_lookup (self->ids_to_groups, parts[1]); + if (app_group != NULL) + bz_entry_group_append_addon_group_id (app_group, id); + } + } + if (extension_of_what != NULL) { GPtrArray *addons = NULL; /* BzFlatpakInstance ensures addons come before applications */ - addons = g_hash_table_lookup ( - user - ? self->usr_name_to_addons - : self->sys_name_to_addons, - extension_of_what); + addons = g_hash_table_lookup (name_to_addons, extension_of_what); if (addons == NULL) { addons = g_ptr_array_new_with_free_func (g_free); g_hash_table_replace ( - user - ? self->usr_name_to_addons - : self->sys_name_to_addons, + name_to_addons, g_strdup (extension_of_what), addons); } g_ptr_array_add (addons, g_strdup (unique_id)); @@ -3112,14 +3130,13 @@ open_generic_id (BzApplication *self, BzEntryGroup *group = NULL; GtkWindow *window = NULL; - group = g_hash_table_lookup (self->ids_to_groups, generic_id); - + group = g_hash_table_lookup (self->ids_to_groups, generic_id); window = gtk_application_get_active_window (GTK_APPLICATION (self)); if (window == NULL) window = new_window (self); if (group != NULL) - bz_window_show_group (BZ_WINDOW (window), group); + gtk_widget_activate_action (GTK_WIDGET (window), "window.show-group", "s", generic_id); else { g_autofree char *message = NULL; diff --git a/src/bz-context-tile-callbacks.c b/src/bz-context-tile-callbacks.c new file mode 100644 index 00000000..0f4866c8 --- /dev/null +++ b/src/bz-context-tile-callbacks.c @@ -0,0 +1,434 @@ +/* bz-context-tile-callbacks.c + * + * Copyright 2026 Eva M, Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include +#include +#include + +#include "bz-context-tile-callbacks.h" +#include "bz-entry.h" +#include "bz-safety-calculator.h" +#include "bz-spdx.h" + +static char * +format_with_small_suffix (char *number, const char *suffix) +{ + char *dot = g_strrstr (number, "."); + + if (dot != NULL) + { + char *end = dot; + while (*(end + 1) != '\0') + end++; + while (end > dot && *end == '0') + *end-- = '\0'; + if (end == dot) + *dot = '\0'; + } + + return g_strdup_printf ("%s\xC2\xA0%s", + number, suffix); +} + +static char * +format_favorites_count (gpointer object, + int favorites_count) +{ + if (favorites_count < 0) + return g_strdup (" "); + return g_strdup_printf ("%d", favorites_count); +} + +static char * +format_recent_downloads (gpointer object, + int value) +{ + double result; + int digits; + + if (value <= 0) + return g_strdup (_ ("---")); + + if (value >= 1000000) + { + result = value / 1000000.0; + digits = (int) log10 (result) + 1; + /* Translators: M is the suffix for millions */ + return g_strdup_printf (_ ("%.*fM"), 3 - digits, result); + } + else if (value >= 1000) + { + result = value / 1000.0; + digits = (int) log10 (result) + 1; + /* Translators: K is the suffix for thousands*/ + return g_strdup_printf (_ ("%.*fK"), 3 - digits, result); + } + else + return g_strdup_printf ("%'d", value); +} + +static char * +format_recent_downloads_tooltip (gpointer object, + int value) +{ + return g_strdup_printf (_ ("%d downloads in the last month"), value); +} + +static char * +format_size (gpointer object, guint64 value) +{ + g_autofree char *size_str = g_format_size (value); + char *space = g_strrstr (size_str, "\xC2\xA0"); + char *decimal = NULL; + int digits = 0; + + if (value == 0) + return g_strdup (_ ("N/A")); + + if (space != NULL) + { + *space = '\0'; + for (char *p = size_str; *p != '\0' && *p != '.'; p++) + if (g_ascii_isdigit (*p)) + digits++; + if (digits >= 3) + { + decimal = g_strrstr (size_str, "."); + if (decimal != NULL) + *decimal = '\0'; + } + return format_with_small_suffix (size_str, space + 2); + } + return g_strdup (size_str); +} + +static char * +get_size_label (gpointer object, + gboolean is_installable, + gboolean runtime_installed, + guint64 runtime_size) +{ + if (is_installable && !runtime_installed && runtime_size > 0) + { + g_autofree char *size_str = g_format_size (runtime_size); + return g_strdup_printf (_ ("+%s runtime"), size_str); + } + + return g_strdup (is_installable ? _ ("Download") : _ ("Installed")); +} + +static guint64 +get_size_type (gpointer object, + BzEntry *entry, + gboolean is_installable) +{ + if (entry == NULL) + return 0; + + return is_installable ? bz_entry_get_size (entry) : bz_entry_get_installed_size (entry); +} + +static char * +format_size_tooltip (gpointer object, guint64 value) +{ + g_autofree char *size_str = NULL; + + if (value == 0) + return g_strdup (_ ("Size information unavailable")); + + size_str = g_format_size (value); + return g_strdup_printf (_ ("Download size of %s"), size_str); +} + +static char * +format_age_rating (gpointer object, + AsContentRating *content_rating) +{ + guint age; + + if (content_rating == NULL) + return g_strdup ("?"); + + age = as_content_rating_get_minimum_age (content_rating); + + if (age < 3) + age = 3; + + /* Translators: Age rating format, e.g. "12+" for ages 12 and up */ + return g_strdup_printf (_ ("%d+"), age); +} + +static char * +get_age_rating_label (gpointer object, + AsContentRating *content_rating) +{ + guint age; + + if (content_rating == NULL) + return g_strdup (_ ("Age Rating")); + + age = as_content_rating_get_minimum_age (content_rating); + + if (age == 0) + return g_strdup (_ ("All Ages")); + else + return g_strdup (_ ("Age Rating")); +} + +static char * +get_age_rating_tooltip (gpointer object, + AsContentRating *content_rating) +{ + guint age; + + if (content_rating == NULL) + return g_strdup (_ ("Age rating information unavailable")); + + age = as_content_rating_get_minimum_age (content_rating); + + if (age == 0) + return g_strdup (_ ("Suitable for all ages")); + + return g_strdup_printf (_ ("Suitable for ages %d and up"), age); +} + +static char * +get_age_rating_style (gpointer object, + AsContentRating *content_rating) +{ + guint age; + + if (content_rating == NULL) + return g_strdup ("grey"); + + age = as_content_rating_get_minimum_age (content_rating); + + if (age >= 18) + return g_strdup ("error"); + else if (age >= 15) + return g_strdup ("orange"); + else if (age >= 12) + return g_strdup ("warning"); + else + return g_strdup ("grey"); +} + +static char * +format_license_tooltip (gpointer object, + BzEntry *entry) +{ + const char *license; + gboolean is_floss = FALSE; + g_autofree char *name = NULL; + + if (entry == NULL) + return g_strdup (_ ("Unknown")); + + g_object_get (entry, "is-floss", &is_floss, "project-license", &license, NULL); + + if (license == NULL || *license == '\0') + return g_strdup (_ ("Unknown")); + + if (is_floss && bz_spdx_is_valid (license)) + { + name = bz_spdx_get_name (license); + return g_strdup_printf (_ ("Free software licensed under %s"), + (name != NULL && *name != '\0') ? name : license); + } + + if (is_floss) + return g_strdup (_ ("Free software")); + + if (bz_spdx_is_proprietary (license)) + return g_strdup (_ ("Proprietary Software")); + + name = bz_spdx_get_name (license); + return g_strdup_printf (_ ("Special License: %s"), + (name != NULL && *name != '\0') ? name : license); +} + +static char * +get_license_label (gpointer object, + BzEntry *entry) +{ + const char *license; + gboolean is_floss = FALSE; + + if (entry == NULL) + return g_strdup (_ ("Unknown")); + + g_object_get (entry, "is-floss", &is_floss, "project-license", &license, NULL); + + if (is_floss) + return g_strdup (_ ("Free")); + + if (license == NULL || *license == '\0') + return g_strdup (_ ("Unknown")); + + if (bz_spdx_is_proprietary (license)) + return g_strdup (_ ("Proprietary")); + + return g_strdup (_ ("Special License")); +} + +static char * +get_license_icon (gpointer object, + gboolean is_floss, + int index) +{ + const char *icons[][2] = { + { "license-symbolic", "proprietary-code-symbolic" }, + { "community-symbolic", "license-symbolic" } + }; + + return g_strdup (icons[is_floss ? 1 : 0][index]); +} + +static char * +get_formfactor_label (gpointer object, + gboolean is_mobile_friendly) +{ + return g_strdup (is_mobile_friendly ? _ ("Adaptive") : _ ("Desktop Only")); +} + +static char * +get_formfactor_tooltip (gpointer object, gboolean is_mobile_friendly) +{ + return g_strdup (is_mobile_friendly ? _ ("Works on desktop, tablets, and phones") + : _ ("May not work on mobile devices")); +} + +static char * +get_safety_rating_icon (gpointer object, + BzEntry *entry, + int index) +{ + char *icon = NULL; + BzImportance importance = 0; + + if (entry == NULL) + return g_strdup ("app-safety-unknown-symbolic"); + + if (index < 0 || index > 2) + return NULL; + + if (index == 0) + { + importance = bz_safety_calculator_calculate_rating (entry); + switch (importance) + { + case BZ_IMPORTANCE_UNIMPORTANT: + case BZ_IMPORTANCE_NEUTRAL: + return g_strdup ("app-safety-ok-symbolic"); + case BZ_IMPORTANCE_INFORMATION: + case BZ_IMPORTANCE_WARNING: + return NULL; + case BZ_IMPORTANCE_IMPORTANT: + return g_strdup ("dialog-warning-symbolic"); + default: + return NULL; + } + } + + icon = bz_safety_calculator_get_top_icon (entry, index - 1); + return icon; +} + +static char * +get_safety_rating_style (gpointer object, + BzEntry *entry) +{ + BzImportance importance; + + if (entry == NULL) + return g_strdup ("grey"); + + importance = bz_safety_calculator_calculate_rating (entry); + + switch (importance) + { + case BZ_IMPORTANCE_UNIMPORTANT: + case BZ_IMPORTANCE_NEUTRAL: + return g_strdup ("grey"); + case BZ_IMPORTANCE_INFORMATION: + return g_strdup ("warning"); + case BZ_IMPORTANCE_WARNING: + return g_strdup ("orange"); + case BZ_IMPORTANCE_IMPORTANT: + return g_strdup ("error"); + default: + return g_strdup ("grey"); + } +} + +static char * +get_safety_rating_label (gpointer object, + BzEntry *entry) +{ + BzImportance importance; + + if (entry == NULL) + return g_strdup (_ ("N/A")); + + importance = bz_safety_calculator_calculate_rating (entry); + + switch (importance) + { + case BZ_IMPORTANCE_UNIMPORTANT: + return g_strdup (_ ("Safe")); + case BZ_IMPORTANCE_NEUTRAL: + return g_strdup (_ ("Low Risk")); + case BZ_IMPORTANCE_INFORMATION: + return g_strdup (_ ("Low Risk")); + case BZ_IMPORTANCE_WARNING: + return g_strdup (_ ("Medium Risk")); + case BZ_IMPORTANCE_IMPORTANT: + return g_strdup (_ ("High Risk")); + default: + return g_strdup (_ ("N/A")); + } +} + +void +bz_widget_class_bind_all_context_tile_callbacks (GtkWidgetClass *widget_class) +{ + g_return_if_fail (GTK_IS_WIDGET_CLASS (widget_class)); + + gtk_widget_class_bind_template_callback (widget_class, format_favorites_count); + gtk_widget_class_bind_template_callback (widget_class, format_recent_downloads); + gtk_widget_class_bind_template_callback (widget_class, format_recent_downloads_tooltip); + gtk_widget_class_bind_template_callback (widget_class, format_size); + gtk_widget_class_bind_template_callback (widget_class, get_size_label); + gtk_widget_class_bind_template_callback (widget_class, get_size_type); + gtk_widget_class_bind_template_callback (widget_class, format_size_tooltip); + gtk_widget_class_bind_template_callback (widget_class, format_age_rating); + gtk_widget_class_bind_template_callback (widget_class, get_age_rating_label); + gtk_widget_class_bind_template_callback (widget_class, get_age_rating_tooltip); + gtk_widget_class_bind_template_callback (widget_class, get_age_rating_style); + gtk_widget_class_bind_template_callback (widget_class, format_license_tooltip); + gtk_widget_class_bind_template_callback (widget_class, get_license_label); + gtk_widget_class_bind_template_callback (widget_class, get_license_icon); + gtk_widget_class_bind_template_callback (widget_class, get_formfactor_label); + gtk_widget_class_bind_template_callback (widget_class, get_formfactor_tooltip); + gtk_widget_class_bind_template_callback (widget_class, get_safety_rating_icon); + gtk_widget_class_bind_template_callback (widget_class, get_safety_rating_style); + gtk_widget_class_bind_template_callback (widget_class, get_safety_rating_label); +} diff --git a/src/bz-context-tile-callbacks.h b/src/bz-context-tile-callbacks.h new file mode 100644 index 00000000..98c26092 --- /dev/null +++ b/src/bz-context-tile-callbacks.h @@ -0,0 +1,26 @@ +/* bz-context-tile-callbacks.h + * + * Copyright 2026 Eva M, Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +void +bz_widget_class_bind_all_context_tile_callbacks (GtkWidgetClass *widget_class); diff --git a/src/bz-entry-group.c b/src/bz-entry-group.c index 3456e7c2..7e45944c 100644 --- a/src/bz-entry-group.c +++ b/src/bz-entry-group.c @@ -48,6 +48,7 @@ struct _BzEntryGroup char *search_tokens; char *eol; guint64 installed_size; + GtkStringList *addon_group_ids; int n_addons; char *donation_url; BzCategoryFlags categories; @@ -63,6 +64,7 @@ struct _BzEntryGroup int removable_available; gboolean read_only; gboolean searchable; + gboolean is_addon; guint64 user_data_size; @@ -688,6 +690,33 @@ bz_entry_group_get_installed_size (BzEntryGroup *self) return self->installed_size; } +GListModel * +bz_entry_group_get_addon_group_ids (BzEntryGroup *self) +{ + g_return_val_if_fail (BZ_IS_ENTRY_GROUP (self), NULL); + + if (self->addon_group_ids == NULL) + return NULL; + + return G_LIST_MODEL (self->addon_group_ids); +} + +void +bz_entry_group_append_addon_group_id (BzEntryGroup *self, + const char *id) +{ + g_return_if_fail (BZ_IS_ENTRY_GROUP (self)); + g_return_if_fail (id != NULL); + + if (self->addon_group_ids == NULL) + self->addon_group_ids = gtk_string_list_new (NULL); + + if (gtk_string_list_find (self->addon_group_ids, id) != G_MAXUINT) + return; + + gtk_string_list_append (self->addon_group_ids, id); +} + int bz_entry_group_get_n_addons (BzEntryGroup *self) { @@ -825,6 +854,13 @@ bz_entry_group_is_searchable (BzEntryGroup *self) return self->searchable; } +gboolean +bz_entry_group_is_addon (BzEntryGroup *self) +{ + g_return_val_if_fail (BZ_IS_ENTRY_GROUP (self), FALSE); + return self->is_addon; +} + void bz_entry_group_add (BzEntryGroup *self, BzEntry *entry, @@ -854,6 +890,7 @@ bz_entry_group_add (BzEntryGroup *self, guint existing = 0; gboolean is_searchable = FALSE; AsContentRating *content_rating = NULL; + gboolean is_addon = FALSE; g_return_if_fail (BZ_IS_ENTRY_GROUP (self)); g_return_if_fail (BZ_IS_ENTRY (entry)); @@ -861,6 +898,11 @@ bz_entry_group_add (BzEntryGroup *self, locker = g_mutex_locker_new (&self->mutex); + is_addon = bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_ADDON); + + if (is_addon) + self->is_addon = TRUE; + if (self->id == NULL) { self->id = g_strdup (bz_entry_get_id (entry)); @@ -886,24 +928,30 @@ bz_entry_group_add (BzEntryGroup *self, } title = bz_entry_get_title (entry); - developer = bz_entry_get_developer (entry); description = bz_entry_get_description (entry); - mini_icon = bz_entry_get_mini_icon (entry); - search_tokens = bz_entry_get_search_tokens (entry); - is_floss = bz_entry_get_is_foss (entry); - light_accent_color = bz_entry_get_light_accent_color (entry); - dark_accent_color = bz_entry_get_dark_accent_color (entry); - is_flathub = bz_entry_get_is_flathub (entry); - is_verified = bz_entry_is_verified (entry); installed_size = bz_entry_get_installed_size (entry); - donation_url = bz_entry_get_donation_url (entry); - entry_categories = bz_entry_get_category_flags (entry); - content_rating = bz_entry_get_content_rating (entry); + is_flathub = bz_entry_get_is_flathub (entry); + is_floss = bz_entry_get_is_foss (entry); - addons = bz_entry_get_addons (entry); - is_searchable = bz_entry_is_searchable (entry); - if (addons != NULL) - n_addons = g_list_model_get_n_items (addons); + if (is_addon) // You would not see any addon when the filter is on without this. + is_verified = TRUE; + + if (!is_addon) + { + developer = bz_entry_get_developer (entry); + mini_icon = bz_entry_get_mini_icon (entry); + search_tokens = bz_entry_get_search_tokens (entry); + light_accent_color = bz_entry_get_light_accent_color (entry); + dark_accent_color = bz_entry_get_dark_accent_color (entry); + is_verified = bz_entry_is_verified (entry); + donation_url = bz_entry_get_donation_url (entry); + entry_categories = bz_entry_get_category_flags (entry); + content_rating = bz_entry_get_content_rating (entry); + addons = bz_entry_get_addons (entry); + is_searchable = bz_entry_is_searchable (entry); + if (addons != NULL) + n_addons = g_list_model_get_n_items (addons); + } usefulness = bz_entry_calc_usefulness (entry); existing = gtk_string_list_find (self->unique_ids, unique_id); @@ -924,80 +972,84 @@ bz_entry_group_add (BzEntryGroup *self, self->title = g_strdup (title); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); } - if (developer != NULL) - { - g_clear_pointer (&self->developer, g_free); - self->developer = g_strdup (developer); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEVELOPER]); - } if (description != NULL) { g_clear_pointer (&self->description, g_free); self->description = g_strdup (description); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DESCRIPTION]); } - if (mini_icon != NULL) + if (installed_size != self->installed_size) { - g_clear_object (&self->mini_icon); - self->mini_icon = g_object_ref (mini_icon); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MINI_ICON]); + self->installed_size = installed_size; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INSTALLED_SIZE]); } - if (search_tokens != NULL) + if (!!is_flathub != !!self->is_flathub) { - g_clear_pointer (&self->search_tokens, g_free); - self->search_tokens = g_strdup (search_tokens); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SEARCH_TOKENS]); + self->is_flathub = is_flathub; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_FLATHUB]); } if (!!is_floss != !!self->is_floss) { self->is_floss = is_floss; g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_FLOSS]); } - if (light_accent_color != NULL) - { - g_clear_pointer (&self->light_accent_color, g_free); - self->light_accent_color = g_strdup (light_accent_color); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LIGHT_ACCENT_COLOR]); - } - if (dark_accent_color != NULL) - { - g_clear_pointer (&self->dark_accent_color, g_free); - self->dark_accent_color = g_strdup (dark_accent_color); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DARK_ACCENT_COLOR]); - } - if (!!is_flathub != !!self->is_flathub) - { - self->is_flathub = is_flathub; - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_FLATHUB]); - } if (!!is_verified != !!self->is_verified) { self->is_verified = is_verified; g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_VERIFIED]); } - if (installed_size != self->installed_size) - { - self->installed_size = installed_size; - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INSTALLED_SIZE]); - } - if (n_addons != self->n_addons) - { - self->n_addons = n_addons; - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_ADDONS]); - } - if (donation_url != NULL) - { - g_clear_pointer (&self->donation_url, g_free); - self->donation_url = g_strdup (donation_url); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DONATION_URL]); - } - if (entry_categories != BZ_CATEGORY_FLAGS_NONE) + + if (!is_addon) { - self->categories = entry_categories; - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CATEGORIES]); + if (developer != NULL) + { + g_clear_pointer (&self->developer, g_free); + self->developer = g_strdup (developer); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEVELOPER]); + } + if (mini_icon != NULL) + { + g_clear_object (&self->mini_icon); + self->mini_icon = g_object_ref (mini_icon); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MINI_ICON]); + } + if (search_tokens != NULL) + { + g_clear_pointer (&self->search_tokens, g_free); + self->search_tokens = g_strdup (search_tokens); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SEARCH_TOKENS]); + } + if (light_accent_color != NULL) + { + g_clear_pointer (&self->light_accent_color, g_free); + self->light_accent_color = g_strdup (light_accent_color); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LIGHT_ACCENT_COLOR]); + } + if (dark_accent_color != NULL) + { + g_clear_pointer (&self->dark_accent_color, g_free); + self->dark_accent_color = g_strdup (dark_accent_color); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DARK_ACCENT_COLOR]); + } + if (n_addons != self->n_addons) + { + self->n_addons = n_addons; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_ADDONS]); + } + if (donation_url != NULL) + { + g_clear_pointer (&self->donation_url, g_free); + self->donation_url = g_strdup (donation_url); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DONATION_URL]); + } + if (entry_categories != BZ_CATEGORY_FLAGS_NONE) + { + self->categories = entry_categories; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CATEGORIES]); + } + if (content_rating != NULL) + self->content_age_rating = as_content_rating_get_minimum_age (content_rating); } - if (content_rating != NULL) - self->content_age_rating = as_content_rating_get_minimum_age (content_rating); self->max_usefulness = usefulness; } @@ -1014,45 +1066,49 @@ bz_entry_group_add (BzEntryGroup *self, self->title = g_strdup (title); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); } - if (developer != NULL && self->developer == NULL) - { - self->developer = g_strdup (developer); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEVELOPER]); - } if (description != NULL && self->description == NULL) { self->description = g_strdup (description); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DESCRIPTION]); } - if (mini_icon != NULL && self->mini_icon == NULL) - { - self->mini_icon = g_object_ref (mini_icon); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MINI_ICON]); - } - if (search_tokens != NULL && self->search_tokens == NULL) - { - self->search_tokens = g_strdup (search_tokens); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SEARCH_TOKENS]); - } - if (light_accent_color != NULL && self->light_accent_color == NULL) - { - self->light_accent_color = g_strdup (light_accent_color); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LIGHT_ACCENT_COLOR]); - } - if (dark_accent_color != NULL && self->dark_accent_color == NULL) - { - self->dark_accent_color = g_strdup (dark_accent_color); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DARK_ACCENT_COLOR]); - } if (installed_size > 0 && self->installed_size == 0) { self->installed_size = installed_size; g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INSTALLED_SIZE]); } - if (donation_url != NULL && self->donation_url == NULL) + + if (!is_addon) { - self->donation_url = g_strdup (donation_url); - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DONATION_URL]); + if (developer != NULL && self->developer == NULL) + { + self->developer = g_strdup (developer); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEVELOPER]); + } + if (mini_icon != NULL && self->mini_icon == NULL) + { + self->mini_icon = g_object_ref (mini_icon); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MINI_ICON]); + } + if (search_tokens != NULL && self->search_tokens == NULL) + { + self->search_tokens = g_strdup (search_tokens); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SEARCH_TOKENS]); + } + if (light_accent_color != NULL && self->light_accent_color == NULL) + { + self->light_accent_color = g_strdup (light_accent_color); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LIGHT_ACCENT_COLOR]); + } + if (dark_accent_color != NULL && self->dark_accent_color == NULL) + { + self->dark_accent_color = g_strdup (dark_accent_color); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DARK_ACCENT_COLOR]); + } + if (donation_url != NULL && self->donation_url == NULL) + { + self->donation_url = g_strdup (donation_url); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DONATION_URL]); + } } } @@ -1087,10 +1143,9 @@ bz_entry_group_add (BzEntryGroup *self, } } } - if (is_searchable && !self->searchable) - { - self->searchable = TRUE; - } + + if (!is_addon && is_searchable && !self->searchable) + self->searchable = TRUE; } void diff --git a/src/bz-entry-group.h b/src/bz-entry-group.h index 1d9e3b84..ce4dcad6 100644 --- a/src/bz-entry-group.h +++ b/src/bz-entry-group.h @@ -85,6 +85,13 @@ bz_entry_group_get_eol (BzEntryGroup *self); guint64 bz_entry_group_get_installed_size (BzEntryGroup *self); +GListModel * +bz_entry_group_get_addon_group_ids (BzEntryGroup *self); + +void +bz_entry_group_append_addon_group_id (BzEntryGroup *self, + const char *id); + int bz_entry_group_get_n_addons (BzEntryGroup *self); @@ -125,6 +132,9 @@ bz_entry_group_get_removable_and_available (BzEntryGroup *self); gboolean bz_entry_group_is_searchable (BzEntryGroup *self); +gboolean +bz_entry_group_is_addon (BzEntryGroup *self); + guint64 bz_entry_group_get_user_data_size (BzEntryGroup *self); diff --git a/src/bz-full-view.c b/src/bz-full-view.c index 25259afe..f9bfe56a 100644 --- a/src/bz-full-view.c +++ b/src/bz-full-view.c @@ -29,6 +29,7 @@ #include "bz-apps-page.h" #include "bz-appstream-description-render.h" #include "bz-context-tile.h" +#include "bz-context-tile-callbacks.h" #include "bz-developer-badge.h" #include "bz-dynamic-list-view.h" #include "bz-entry-inspector.h" @@ -170,24 +171,6 @@ bz_full_view_set_property (GObject *object, } } -static char * -format_with_small_suffix (char *number, const char *suffix) -{ - char *dot = g_strrstr (number, "."); - - if (dot != NULL) - { - char *end = dot + strlen (dot) - 1; - while (end > dot && *end == '0') - *end-- = '\0'; - if (end == dot) - *dot = '\0'; - } - - return g_strdup_printf ("%s\xC2\xA0%s", - number, suffix); -} - static gboolean is_scrolled_down (gpointer object, double value) @@ -195,274 +178,6 @@ is_scrolled_down (gpointer object, return value > 100.0; } -static char * -format_favorites_count (gpointer object, - int favorites_count) -{ - if (favorites_count < 0) - return g_strdup (" "); - return g_strdup_printf ("%d", favorites_count); -} - -static char * -format_recent_downloads (gpointer object, - int value) -{ - double result; - int digits; - - if (value <= 0) - return g_strdup (_ ("---")); - - if (value >= 1000000) - { - result = value / 1000000.0; - digits = (int) log10 (result) + 1; - /* Translators: M is the suffix for millions */ - return g_strdup_printf (_ ("%.*fM"), 3 - digits, result); - } - else if (value >= 1000) - { - result = value / 1000.0; - digits = (int) log10 (result) + 1; - /* Translators: K is the suffix for thousands*/ - return g_strdup_printf (_ ("%.*fK"), 3 - digits, result); - } - else - return g_strdup_printf ("%'d", value); -} - -static char * -format_recent_downloads_tooltip (gpointer object, - int value) -{ - return g_strdup_printf (_ ("%d downloads in the last month"), value); -} - -static char * -format_size (gpointer object, guint64 value) -{ - g_autofree char *size_str = g_format_size (value); - char *space = g_strrstr (size_str, "\xC2\xA0"); - char *decimal = NULL; - int digits = 0; - - if (value == 0) - return g_strdup (_ ("N/A")); - - if (space != NULL) - { - *space = '\0'; - for (char *p = size_str; *p != '\0' && *p != '.'; p++) - if (g_ascii_isdigit (*p)) - digits++; - if (digits >= 3) - { - decimal = g_strrstr (size_str, "."); - if (decimal != NULL) - *decimal = '\0'; - } - return format_with_small_suffix (size_str, space + 2); - } - return g_strdup (size_str); -} - -static char * -get_size_label (gpointer object, - gboolean is_installable, - gboolean runtime_installed, - guint64 runtime_size) -{ - if (is_installable && !runtime_installed && runtime_size > 0) - { - g_autofree char *size_str = g_format_size (runtime_size); - return g_strdup_printf (_ ("+%s runtime"), size_str); - } - - return g_strdup (is_installable ? _ ("Download") : _ ("Installed")); -} - -static guint64 -get_size_type (gpointer object, - BzEntry *entry, - gboolean is_installable) -{ - if (entry == NULL) - return 0; - - return is_installable ? bz_entry_get_size (entry) : bz_entry_get_installed_size (entry); -} - -static char * -format_size_tooltip (gpointer object, guint64 value) -{ - g_autofree char *size_str = NULL; - - if (value == 0) - return g_strdup (_ ("Size information unavailable")); - - size_str = g_format_size (value); - return g_strdup_printf (_ ("Download size of %s"), size_str); -} - -static char * -format_age_rating (gpointer object, - AsContentRating *content_rating) -{ - guint age; - - if (content_rating == NULL) - return g_strdup ("?"); - - age = as_content_rating_get_minimum_age (content_rating); - - if (age < 3) - age = 3; - - /* Translators: Age rating format, e.g. "12+" for ages 12 and up */ - return g_strdup_printf (_ ("%d+"), age); -} - -static char * -get_age_rating_label (gpointer object, - AsContentRating *content_rating) -{ - guint age; - - if (content_rating == NULL) - return g_strdup (_ ("Age Rating")); - - age = as_content_rating_get_minimum_age (content_rating); - - if (age == 0) - return g_strdup (_ ("All Ages")); - else - return g_strdup (_ ("Age Rating")); -} - -static char * -get_age_rating_tooltip (gpointer object, - AsContentRating *content_rating) -{ - guint age; - - if (content_rating == NULL) - return g_strdup (_ ("Age rating information unavailable")); - - age = as_content_rating_get_minimum_age (content_rating); - - if (age == 0) - return g_strdup (_ ("Suitable for all ages")); - - return g_strdup_printf (_ ("Suitable for ages %d and up"), age); -} - -static char * -get_age_rating_style (gpointer object, - AsContentRating *content_rating) -{ - guint age; - - if (content_rating == NULL) - return g_strdup ("grey"); - - age = as_content_rating_get_minimum_age (content_rating); - - if (age >= 18) - return g_strdup ("error"); - else if (age >= 15) - return g_strdup ("orange"); - else if (age >= 12) - return g_strdup ("warning"); - else - return g_strdup ("grey"); -} - -static char * -format_license_tooltip (gpointer object, - BzEntry *entry) -{ - const char *license; - gboolean is_floss = FALSE; - g_autofree char *name = NULL; - - if (entry == NULL) - return g_strdup (_ ("Unknown")); - - g_object_get (entry, "is-floss", &is_floss, "project-license", &license, NULL); - - if (license == NULL || *license == '\0') - return g_strdup (_ ("Unknown")); - - if (is_floss && bz_spdx_is_valid (license)) - { - name = bz_spdx_get_name (license); - return g_strdup_printf (_ ("Free software licensed under %s"), - (name != NULL && *name != '\0') ? name : license); - } - - if (is_floss) - return g_strdup (_ ("Free software")); - - if (bz_spdx_is_proprietary (license)) - return g_strdup (_ ("Proprietary Software")); - - name = bz_spdx_get_name (license); - return g_strdup_printf (_ ("Special License: %s"), - (name != NULL && *name != '\0') ? name : license); -} - -static char * -get_license_label (gpointer object, - BzEntry *entry) -{ - const char *license; - gboolean is_floss = FALSE; - - if (entry == NULL) - return g_strdup (_ ("Unknown")); - - g_object_get (entry, "is-floss", &is_floss, "project-license", &license, NULL); - - if (is_floss) - return g_strdup (_ ("Free")); - - if (license == NULL || *license == '\0') - return g_strdup (_ ("Unknown")); - - if (bz_spdx_is_proprietary (license)) - return g_strdup (_ ("Proprietary")); - - return g_strdup (_ ("Special License")); -} - -static char * -get_license_icon (gpointer object, - gboolean is_floss, - int index) -{ - const char *icons[][2] = { - { "license-symbolic", "proprietary-code-symbolic" }, - { "community-symbolic", "license-symbolic" } - }; - - return g_strdup (icons[is_floss ? 1 : 0][index]); -} - -static char * -get_formfactor_label (gpointer object, - gboolean is_mobile_friendly) -{ - return g_strdup (is_mobile_friendly ? _ ("Adaptive") : _ ("Desktop Only")); -} - -static char * -get_formfactor_tooltip (gpointer object, gboolean is_mobile_friendly) -{ - return g_strdup (is_mobile_friendly ? _ ("Works on desktop, tablets, and phones") - : _ ("May not work on mobile devices")); -} - static char * format_as_link (gpointer object, const char *value) @@ -519,97 +234,6 @@ format_leftover_label (gpointer object, const char *name, guint64 size) return g_strdup_printf (_ ("%s is not installed, but it still has %s of data present."), name, formatted_size); } -static char * -get_safety_rating_icon (gpointer object, - BzEntry *entry, - int index) -{ - char *icon = NULL; - BzImportance importance = 0; - - if (entry == NULL) - return g_strdup ("app-safety-unknown-symbolic"); - - if (index < 0 || index > 2) - return NULL; - - if (index == 0) - { - importance = bz_safety_calculator_calculate_rating (entry); - switch (importance) - { - case BZ_IMPORTANCE_UNIMPORTANT: - case BZ_IMPORTANCE_NEUTRAL: - return g_strdup ("app-safety-ok-symbolic"); - case BZ_IMPORTANCE_INFORMATION: - case BZ_IMPORTANCE_WARNING: - return NULL; - case BZ_IMPORTANCE_IMPORTANT: - return g_strdup ("dialog-warning-symbolic"); - default: - return NULL; - } - } - - icon = bz_safety_calculator_get_top_icon (entry, index - 1); - return icon; -} - -static char * -get_safety_rating_style (gpointer object, - BzEntry *entry) -{ - BzImportance importance; - - if (entry == NULL) - return g_strdup ("grey"); - - importance = bz_safety_calculator_calculate_rating (entry); - - switch (importance) - { - case BZ_IMPORTANCE_UNIMPORTANT: - case BZ_IMPORTANCE_NEUTRAL: - return g_strdup ("grey"); - case BZ_IMPORTANCE_INFORMATION: - return g_strdup ("warning"); - case BZ_IMPORTANCE_WARNING: - return g_strdup ("orange"); - case BZ_IMPORTANCE_IMPORTANT: - return g_strdup ("error"); - default: - return g_strdup ("grey"); - } -} - -static char * -get_safety_rating_label (gpointer object, - BzEntry *entry) -{ - BzImportance importance; - - if (entry == NULL) - return g_strdup (_ ("N/A")); - - importance = bz_safety_calculator_calculate_rating (entry); - - switch (importance) - { - case BZ_IMPORTANCE_UNIMPORTANT: - return g_strdup (_ ("Safe")); - case BZ_IMPORTANCE_NEUTRAL: - return g_strdup (_ ("Low Risk")); - case BZ_IMPORTANCE_INFORMATION: - return g_strdup (_ ("Low Risk")); - case BZ_IMPORTANCE_WARNING: - return g_strdup (_ ("Medium Risk")); - case BZ_IMPORTANCE_IMPORTANT: - return g_strdup (_ ("High Risk")); - default: - return g_strdup (_ ("N/A")); - } -} - static gpointer filter_own_app_id (BzEntry *entry, GtkStringList *app_ids) { @@ -788,22 +412,27 @@ static void dl_stats_cb (BzFullView *self, GtkButton *button) { - AdwDialog *dialog = NULL; - BzEntry *ui_entry = NULL; + AdwDialog *dialog = NULL; + AdwBreakpointBin *bin = NULL; + BzEntry *ui_entry = NULL; if (self->group == NULL) return; ui_entry = bz_result_get_object (self->ui_entry); - dialog = bz_stats_dialog_new (NULL, NULL, 0); + bin = bz_stats_dialog_new (NULL, NULL, 0); + dialog = adw_dialog_new (); + adw_dialog_set_content_width (dialog, 1250); + adw_dialog_set_content_height (dialog, 750); + adw_dialog_set_child (dialog, GTK_WIDGET (bin)); - g_object_bind_property (ui_entry, "download-stats", dialog, "model", G_BINDING_SYNC_CREATE); - g_object_bind_property (ui_entry, "download-stats-per-country", dialog, "country-model", G_BINDING_SYNC_CREATE); - g_object_bind_property (ui_entry, "total-downloads", dialog, "total-downloads", G_BINDING_SYNC_CREATE); + g_object_bind_property (ui_entry, "download-stats", bin, "model", G_BINDING_SYNC_CREATE); + g_object_bind_property (ui_entry, "download-stats-per-country", bin, "country-model", G_BINDING_SYNC_CREATE); + g_object_bind_property (ui_entry, "total-downloads", bin, "total-downloads", G_BINDING_SYNC_CREATE); adw_dialog_present (dialog, GTK_WIDGET (self)); - bz_stats_dialog_animate_open (BZ_STATS_DIALOG (dialog)); + bz_stats_dialog_animate_open (BZ_STATS_DIALOG (bin)); } static void @@ -1056,32 +685,14 @@ bz_full_view_class_init (BzFullViewClass *klass) gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-full-view.ui"); bz_widget_class_bind_all_util_callbacks (widget_class); - + bz_widget_class_bind_all_context_tile_callbacks (widget_class); gtk_widget_class_bind_template_child (widget_class, BzFullView, stack); gtk_widget_class_bind_template_child (widget_class, BzFullView, main_scroll); gtk_widget_class_bind_template_child (widget_class, BzFullView, shadow_overlay); gtk_widget_class_bind_template_child (widget_class, BzFullView, description_toggle); gtk_widget_class_bind_template_callback (widget_class, is_scrolled_down); - gtk_widget_class_bind_template_callback (widget_class, format_favorites_count); - gtk_widget_class_bind_template_callback (widget_class, format_recent_downloads); - gtk_widget_class_bind_template_callback (widget_class, format_recent_downloads_tooltip); - gtk_widget_class_bind_template_callback (widget_class, format_size); - gtk_widget_class_bind_template_callback (widget_class, get_size_label); - gtk_widget_class_bind_template_callback (widget_class, format_size_tooltip); gtk_widget_class_bind_template_callback (widget_class, age_rating_cb); - gtk_widget_class_bind_template_callback (widget_class, format_age_rating); - gtk_widget_class_bind_template_callback (widget_class, get_age_rating_label); - gtk_widget_class_bind_template_callback (widget_class, get_age_rating_tooltip); - gtk_widget_class_bind_template_callback (widget_class, get_age_rating_style); gtk_widget_class_bind_template_callback (widget_class, format_as_link); - gtk_widget_class_bind_template_callback (widget_class, format_license_tooltip); - gtk_widget_class_bind_template_callback (widget_class, get_license_label); - gtk_widget_class_bind_template_callback (widget_class, get_license_icon); - gtk_widget_class_bind_template_callback (widget_class, get_formfactor_label); - gtk_widget_class_bind_template_callback (widget_class, get_formfactor_tooltip); - gtk_widget_class_bind_template_callback (widget_class, get_safety_rating_icon); - gtk_widget_class_bind_template_callback (widget_class, get_safety_rating_style); - gtk_widget_class_bind_template_callback (widget_class, get_safety_rating_label); gtk_widget_class_bind_template_callback (widget_class, has_link); gtk_widget_class_bind_template_callback (widget_class, format_leftover_label); gtk_widget_class_bind_template_callback (widget_class, format_other_apps_label); @@ -1093,7 +704,6 @@ bz_full_view_class_init (BzFullViewClass *klass) gtk_widget_class_bind_template_callback (widget_class, dl_stats_cb); gtk_widget_class_bind_template_callback (widget_class, screenshot_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, size_cb); - gtk_widget_class_bind_template_callback (widget_class, get_size_type); gtk_widget_class_bind_template_callback (widget_class, formfactor_cb); gtk_widget_class_bind_template_callback (widget_class, safety_cb); gtk_widget_class_bind_template_callback (widget_class, update_cb); diff --git a/src/bz-library-page.c b/src/bz-library-page.c index af1f7cb1..a152101b 100644 --- a/src/bz-library-page.c +++ b/src/bz-library-page.c @@ -609,6 +609,9 @@ filter (BzEntryGroup *group, const char *title = NULL; const char *text = NULL; + if (bz_entry_group_is_addon (group)) + return FALSE; + id = bz_entry_group_get_id (group); title = bz_entry_group_get_title (group); diff --git a/src/bz-license-dialog.blp b/src/bz-license-dialog.blp index f0e1b5b3..7f96a1d1 100644 --- a/src/bz-license-dialog.blp +++ b/src/bz-license-dialog.blp @@ -1,8 +1,7 @@ using Gtk 4.0; using Adw 1; -template $BzLicenseDialog: Adw.Dialog { - content-width: 400; +template $BzLicenseDialog: Adw.Bin { child: Adw.ToolbarView { [top] diff --git a/src/bz-license-dialog.c b/src/bz-license-dialog.c index a17d22b4..63a15bd7 100644 --- a/src/bz-license-dialog.c +++ b/src/bz-license-dialog.c @@ -20,6 +20,7 @@ #include "config.h" +#include #include #include "bz-entry.h" @@ -30,12 +31,12 @@ struct _BzLicenseDialog { - AdwDialog parent_instance; + AdwBin parent_instance; BzEntry *entry; }; -G_DEFINE_FINAL_TYPE (BzLicenseDialog, bz_license_dialog, ADW_TYPE_DIALOG) +G_DEFINE_FINAL_TYPE (BzLicenseDialog, bz_license_dialog, ADW_TYPE_BIN) enum { @@ -289,8 +290,27 @@ bz_license_dialog_init (BzLicenseDialog *self) AdwDialog * bz_license_dialog_new (BzEntry *entry) { - return g_object_new (BZ_TYPE_LICENSE_DIALOG, - "entry", entry, - NULL); + BzLicenseDialog *widget = NULL; + AdwDialog *dialog = NULL; + + widget = g_object_new (BZ_TYPE_LICENSE_DIALOG, "entry", entry, NULL); + + dialog = adw_dialog_new (); + adw_dialog_set_content_width (dialog, 400); + adw_dialog_set_child (dialog, GTK_WIDGET (widget)); + + return dialog; } +AdwNavigationPage * +bz_license_page_new (BzEntry *entry) +{ + BzLicenseDialog *widget = NULL; + AdwNavigationPage *page = NULL; + + widget = g_object_new (BZ_TYPE_LICENSE_DIALOG, "entry", entry, NULL); + page = adw_navigation_page_new (GTK_WIDGET (widget), _ ("License")); + adw_navigation_page_set_tag (page, "license"); + + return page; +} diff --git a/src/bz-license-dialog.h b/src/bz-license-dialog.h index 9ede55a0..df6d9200 100644 --- a/src/bz-license-dialog.h +++ b/src/bz-license-dialog.h @@ -27,9 +27,12 @@ G_BEGIN_DECLS #define BZ_TYPE_LICENSE_DIALOG (bz_license_dialog_get_type ()) -G_DECLARE_FINAL_TYPE (BzLicenseDialog, bz_license_dialog, BZ, LICENSE_DIALOG, AdwDialog) +G_DECLARE_FINAL_TYPE (BzLicenseDialog, bz_license_dialog, BZ, LICENSE_DIALOG, AdwBin) AdwDialog * bz_license_dialog_new (BzEntry *entry); +AdwNavigationPage * +bz_license_page_new (BzEntry *entry); + G_END_DECLS diff --git a/src/bz-share-list.c b/src/bz-share-list.c index 69206230..05e91f68 100644 --- a/src/bz-share-list.c +++ b/src/bz-share-list.c @@ -51,6 +51,7 @@ copy_cb (BzShareList *self, const char *link = NULL; GdkClipboard *clipboard = NULL; AdwToast *toast = NULL; + GtkWidget *ancestor = NULL; GtkRoot *root = NULL; link = g_object_get_data (G_OBJECT (button), "url"); @@ -58,13 +59,19 @@ copy_cb (BzShareList *self, clipboard = gdk_display_get_clipboard (gdk_display_get_default ()); gdk_clipboard_set_text (clipboard, link); - root = gtk_widget_get_root (GTK_WIDGET (self)); - if (root && BZ_IS_WINDOW (root)) + toast = adw_toast_new (_ ("Copied!")); + adw_toast_set_timeout (toast, 1); + + ancestor = gtk_widget_get_ancestor (GTK_WIDGET (self), ADW_TYPE_TOAST_OVERLAY); + if (ancestor != NULL) { - toast = adw_toast_new (_ ("Copied!")); - adw_toast_set_timeout (toast, 1); - bz_window_add_toast (BZ_WINDOW (root), toast); + adw_toast_overlay_add_toast (ADW_TOAST_OVERLAY (ancestor), toast); + return; } + + root = gtk_widget_get_root (GTK_WIDGET (self)); + if (root != NULL && BZ_IS_WINDOW (root)) + bz_window_add_toast (BZ_WINDOW (root), toast); } static void diff --git a/src/bz-stats-dialog.blp b/src/bz-stats-dialog.blp index 035af2bb..55836f9d 100644 --- a/src/bz-stats-dialog.blp +++ b/src/bz-stats-dialog.blp @@ -1,17 +1,16 @@ using Gtk 4.0; using Adw 1; -template $BzStatsDialog: Adw.Dialog { +template $BzStatsDialog: Adw.BreakpointBin { width-request: 360; height-request: 450; - content-width: 1250; - content-height: 750; child: Adw.ToolbarView { bottom-bar-style: raised_border; [top] Adw.HeaderBar { title-widget: Adw.ViewSwitcher switcher_title { + visible: bind $invert_boolean($is_null(template.country-model) as ) as ; stack: stack; policy: wide; }; @@ -79,6 +78,7 @@ template $BzStatsDialog: Adw.Dialog { } Adw.ViewSwitcherBar switcher_bar { + visible: bind $invert_boolean($is_null(template.country-model) as ) as ; stack: stack; } }; @@ -90,7 +90,6 @@ template $BzStatsDialog: Adw.Dialog { setters { switcher_title.stack: null; switcher_bar.reveal: true; - template.content-height: 300; } } } \ No newline at end of file diff --git a/src/bz-stats-dialog.c b/src/bz-stats-dialog.c index e6684278..ebc093df 100644 --- a/src/bz-stats-dialog.c +++ b/src/bz-stats-dialog.c @@ -24,10 +24,11 @@ #include "bz-data-graph.h" #include "bz-stats-dialog.h" #include "bz-world-map.h" +#include "bz-template-callbacks.h" struct _BzStatsDialog { - AdwDialog parent_instance; + AdwBreakpointBin parent_instance; GListModel *model; GListModel *country_model; @@ -38,7 +39,7 @@ struct _BzStatsDialog BzWorldMap *world_map; }; -G_DEFINE_FINAL_TYPE (BzStatsDialog, bz_stats_dialog, ADW_TYPE_DIALOG) +G_DEFINE_FINAL_TYPE (BzStatsDialog, bz_stats_dialog, ADW_TYPE_BREAKPOINT_BIN) enum { @@ -166,6 +167,9 @@ bz_stats_dialog_class_init (BzStatsDialogClass *klass) g_type_ensure (BZ_TYPE_WORLD_MAP); gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-stats-dialog.ui"); + + bz_widget_class_bind_all_util_callbacks (widget_class); + gtk_widget_class_bind_template_callback (widget_class, format_total_downloads); gtk_widget_class_bind_template_child (widget_class, BzStatsDialog, graph); gtk_widget_class_bind_template_child (widget_class, BzStatsDialog, world_map); @@ -177,7 +181,7 @@ bz_stats_dialog_init (BzStatsDialog *self) gtk_widget_init_template (GTK_WIDGET (self)); } -AdwDialog * +AdwBreakpointBin * bz_stats_dialog_new (GListModel *model, GListModel *country_model, int total_downloads) @@ -191,7 +195,7 @@ bz_stats_dialog_new (GListModel *model, "total-downloads", total_downloads, NULL); - return ADW_DIALOG (stats_dialog); + return ADW_BREAKPOINT_BIN (stats_dialog); } void diff --git a/src/bz-stats-dialog.h b/src/bz-stats-dialog.h index 9af2970f..f1b45973 100644 --- a/src/bz-stats-dialog.h +++ b/src/bz-stats-dialog.h @@ -25,9 +25,9 @@ G_BEGIN_DECLS #define BZ_TYPE_STATS_DIALOG (bz_stats_dialog_get_type ()) -G_DECLARE_FINAL_TYPE (BzStatsDialog, bz_stats_dialog, BZ, STATS_DIALOG, AdwDialog) +G_DECLARE_FINAL_TYPE (BzStatsDialog, bz_stats_dialog, BZ, STATS_DIALOG, AdwBreakpointBin) -AdwDialog * +AdwBreakpointBin * bz_stats_dialog_new (GListModel *model, GListModel *country_model, int total_downloads); diff --git a/src/bz-window.c b/src/bz-window.c index 6dbb8105..bc2ace68 100644 --- a/src/bz-window.c +++ b/src/bz-window.c @@ -436,72 +436,18 @@ action_show_group (GtkWidget *widget, bz_state_info_get_application_factory (self->state), gtk_string_object_new (id)); - if (group != NULL) - bz_window_show_group (self, group); -} - -static gboolean -test_has_addons (BzEntry *entry) -{ - GListModel *model = NULL; - - model = bz_entry_get_addons (entry); - return model != NULL && g_list_model_get_n_items (model) > 0; -} - -static void -addon_transact_cb (BzWindow *self, - BzEntry *entry, - BzAddonsDialog *dialog) -{ - gboolean installed = FALSE; - - g_object_get (entry, "installed", &installed, NULL); - - try_transact (self, entry, NULL, installed, TRUE, NULL); -} - -static DexFuture * -addons_fiber (BzEntryGroup *group) -{ - g_autoptr (GError) local_error = NULL; - g_autoptr (BzEntry) entry = NULL; - g_autoptr (GListModel) model = NULL; - BzStateInfo *state = NULL; - GtkWidget *window = NULL; - AdwDialog *addons_dialog = NULL; - - state = bz_state_info_get_default (); - if (state == NULL) - return NULL; - - window = GTK_WIDGET (gtk_application_get_active_window ( - GTK_APPLICATION (g_application_get_default ()))); + if (group == NULL) + return; - entry = bz_entry_group_find_entry (group, test_has_addons, - window, &local_error); - if (entry == NULL) + if (bz_entry_group_is_addon (group)) { - if (local_error != NULL) - bz_show_error_for_widget (window, - _ ("Failed to load add-ons"), - local_error->message); - return NULL; - } - - model = bz_application_map_factory_generate ( - bz_state_info_get_entry_factory (state), - bz_entry_get_addons (entry)); - - addons_dialog = bz_addons_dialog_new (entry, model); - g_signal_connect_swapped ( - addons_dialog, "transact", - G_CALLBACK (addon_transact_cb), window); - gtk_widget_set_size_request (GTK_WIDGET (addons_dialog), 350, -1); + AdwDialog *dialog = NULL; - adw_dialog_present (addons_dialog, window); - - return NULL; + dialog =bz_addons_dialog_new_single (group); + adw_dialog_present (dialog, GTK_WIDGET (self)); + } + else + bz_window_show_group (self, group); } static void @@ -512,6 +458,7 @@ action_addons_group (GtkWidget *widget, BzWindow *self = BZ_WINDOW (widget); const char *id = NULL; g_autoptr (BzEntryGroup) group = NULL; + AdwDialog *addons_dialog = NULL; id = g_variant_get_string (parameter, NULL); group = bz_application_map_factory_convert_one ( @@ -521,12 +468,8 @@ action_addons_group (GtkWidget *widget, if (group == NULL) return; - dex_future_disown (dex_scheduler_spawn ( - dex_scheduler_get_default (), - bz_get_dex_stack_size (), - (DexFiberFunc) addons_fiber, - g_object_ref (group), - g_object_unref)); + addons_dialog = bz_addons_dialog_new (group); + adw_dialog_present (addons_dialog, GTK_WIDGET (self)); } static void diff --git a/src/meson.build b/src/meson.build index f5e819e1..9c7930f0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -52,6 +52,7 @@ gdbus_src = gnome.gdbus_codegen( ) bz_sources = files( + 'bz-addon-tile.c', 'bz-addons-dialog.c', 'bz-age-rating-dialog.c', 'bz-all-apps-page.c', @@ -72,6 +73,7 @@ bz_sources = files( 'bz-content-provider.c', 'bz-context-row.c', 'bz-context-tile.c', + 'bz-context-tile-callbacks.c', 'bz-curated-app-tile.c', 'bz-curated-view.c', 'bz-data-graph.c', @@ -259,6 +261,7 @@ bz_deps += [ generated_gobjects ] blueprints = custom_target('blueprints', input: files( + 'bz-addon-tile.blp', 'bz-addons-dialog.blp', 'bz-donations-dialog.blp', 'bz-age-rating-dialog.blp', From b32540a577dc0b7c6d24542f8e58dc015bc66576 Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:26:42 +0200 Subject: [PATCH 2/7] Fix outlines and tweak anim speed --- src/bz-addons-dialog.blp | 12 ++++++++++-- src/bz-addons-dialog.c | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/bz-addons-dialog.blp b/src/bz-addons-dialog.blp index ecb1b79f..205609c1 100644 --- a/src/bz-addons-dialog.blp +++ b/src/bz-addons-dialog.blp @@ -243,13 +243,17 @@ template $BzAddonsDialog: Adw.Dialog { Stack { transition-type: crossfade; halign: center; - margin-top: 12; - margin-bottom: 16; + margin-top: 6; + margin-bottom: 8; visible-child-name: bind $get_install_stack_page(template.selected-group as <$BzEntryGroup>.installable, template.selected-group as <$BzEntryGroup>.removable) as ; StackPage { name: "install"; child: Button { + margin-top: 6; + margin-bottom: 6; + margin-start: 6; + margin-end: 6; styles ["pill", "suggested-action"] label: _("Install"); sensitive: bind $invert_boolean($is_zero(template.selected-group as <$BzEntryGroup>.installable-and-available) as ) as ; @@ -261,6 +265,10 @@ template $BzAddonsDialog: Adw.Dialog { name: "open"; child: Box { spacing: 8; + margin-top: 6; + margin-bottom: 6; + margin-start: 6; + margin-end: 6; Button { styles ["pill"] diff --git a/src/bz-addons-dialog.c b/src/bz-addons-dialog.c index 1500582a..b633db6c 100644 --- a/src/bz-addons-dialog.c +++ b/src/bz-addons-dialog.c @@ -249,11 +249,11 @@ bz_addons_dialog_init (BzAddonsDialog *self) gtk_widget_init_template (GTK_WIDGET (self)); width_target = adw_property_animation_target_new (G_OBJECT (self), "content-width"); - self->width_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 200, width_target); + self->width_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 300, width_target); adw_timed_animation_set_easing (ADW_TIMED_ANIMATION (self->width_animation), ADW_EASE_IN_OUT_CUBIC); height_target = adw_property_animation_target_new (G_OBJECT (self), "content-height"); - self->height_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 200, height_target); + self->height_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 300, height_target); adw_timed_animation_set_easing (ADW_TIMED_ANIMATION (self->height_animation), ADW_EASE_IN_OUT_CUBIC); } From 05b5d4aee5da821a60a7c07e13a3bd23fe56b4ea Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:33:35 +0200 Subject: [PATCH 3/7] fix title missing warnings --- src/bz-addons-dialog.blp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bz-addons-dialog.blp b/src/bz-addons-dialog.blp index 205609c1..b2cfa83e 100644 --- a/src/bz-addons-dialog.blp +++ b/src/bz-addons-dialog.blp @@ -64,6 +64,7 @@ template $BzAddonsDialog: Adw.Dialog { Adw.NavigationPage { tag: "full-view"; + title: bind try { template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.title, _("Add-on Page") }; child: Adw.ToolbarView { [top] From 40adc496a2417c4ff1ec6a4bb5a8def7e7a42322 Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:08:20 +0200 Subject: [PATCH 4/7] Fix pop issues --- src/bz-addons-dialog.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/bz-addons-dialog.c b/src/bz-addons-dialog.c index b633db6c..304b7372 100644 --- a/src/bz-addons-dialog.c +++ b/src/bz-addons-dialog.c @@ -284,10 +284,8 @@ bz_addons_dialog_new (BzEntryGroup *group) AdwNavigationPage *full_view_page = NULL; set_selected_group (self, single); - - adw_navigation_view_push_by_tag (self->navigation_view, "full-view"); full_view_page = adw_navigation_view_find_page (self->navigation_view, "full-view"); - adw_navigation_page_set_can_pop (full_view_page, FALSE); + adw_navigation_view_replace (self->navigation_view, &full_view_page, 1); } else g_idle_add_once ((GSourceOnceFunc) animate_to_size, self); @@ -305,11 +303,8 @@ bz_addons_dialog_new_single (BzEntryGroup *group) set_selected_group (self, group); - adw_navigation_view_push_by_tag (self->navigation_view, "full-view"); - full_view = adw_navigation_view_find_page (self->navigation_view, "full-view"); - if (full_view != NULL) - adw_navigation_page_set_can_pop (full_view, FALSE); + adw_navigation_view_replace (self->navigation_view, &full_view, 1); return ADW_DIALOG (self); } From 6465654629389b44078ecbb700200520c0104531 Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:55:02 +0200 Subject: [PATCH 5/7] Fixes --- src/bz-addons-dialog.blp | 5 ++- src/bz-addons-dialog.c | 95 +++++++++++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/src/bz-addons-dialog.blp b/src/bz-addons-dialog.blp index b2cfa83e..02a21f07 100644 --- a/src/bz-addons-dialog.blp +++ b/src/bz-addons-dialog.blp @@ -41,7 +41,10 @@ template $BzAddonsDialog: Adw.Dialog { ] model: NoSelection { - model: bind template.addon-groups; + model: SortListModel { + sorter: CustomSorter sorter {}; + model: bind template.addon-groups; + }; }; factory: BuilderListItemFactory { diff --git a/src/bz-addons-dialog.c b/src/bz-addons-dialog.c index 304b7372..cf687480 100644 --- a/src/bz-addons-dialog.c +++ b/src/bz-addons-dialog.c @@ -61,6 +61,7 @@ struct _BzAddonsDialog GtkToggleButton *description_toggle; AdwClamp *full_view_clamp; AdwClamp *list_clamp; + GtkCustomSorter *sorter; }; G_DEFINE_FINAL_TYPE (BzAddonsDialog, bz_addons_dialog, ADW_TYPE_DIALOG) @@ -78,22 +79,23 @@ enum }; static GParamSpec *props[LAST_PROP] = { 0 }; -static char *format_parent_title (gpointer object, const char *title); -static int get_description_max_height (gpointer object, gboolean active); -static char *get_description_toggle_text (gpointer object, gboolean active); -static void size_cb (BzAddonsDialog *self, GtkButton *button); -static void license_cb (BzAddonsDialog *self, GtkButton *button); -static void dl_stats_cb (BzAddonsDialog *self, GtkButton *button); -static void animate_to_size (BzAddonsDialog *self); -static void on_visible_page_tag_changed (AdwNavigationView *nav_view, GParamSpec *pspec, BzAddonsDialog *self); -static char *get_install_stack_page (gpointer object, int installable, int removable); -static void install_cb (GtkButton *button, BzAddonsDialog *self); -static void remove_cb (GtkButton *button, BzAddonsDialog *self); -static void run_cb (GtkButton *button, BzAddonsDialog *self); +static char *format_parent_title (gpointer object, const char *title); +static int get_description_max_height (gpointer object, gboolean active); +static char *get_description_toggle_text (gpointer object, gboolean active); +static void size_cb (BzAddonsDialog *self, GtkButton *button); +static void license_cb (BzAddonsDialog *self, GtkButton *button); +static void dl_stats_cb (BzAddonsDialog *self, GtkButton *button); +static void animate_to_size (BzAddonsDialog *self); +static void on_visible_page_tag_changed (AdwNavigationView *nav_view, GParamSpec *pspec, BzAddonsDialog *self); +static char *get_install_stack_page (gpointer object, int installable, int removable); +static void install_cb (GtkButton *button, BzAddonsDialog *self); +static void remove_cb (GtkButton *button, BzAddonsDialog *self); +static void run_cb (GtkButton *button, BzAddonsDialog *self); static DexFuture *on_parent_ui_entry_resolved (DexFuture *future, GWeakRef *wr); static DexFuture *on_selected_ui_entry_resolved (DexFuture *future, GWeakRef *wr); -static void set_selected_group (BzAddonsDialog *self, BzEntryGroup *group); -static void tile_activated_cb (BzAddonTile *tile); +static void set_selected_group (BzAddonsDialog *self, BzEntryGroup *group); +static void tile_activated_cb (BzAddonTile *tile); +static int sort_func (BzEntryGroup *a, BzEntryGroup *b, BzAddonsDialog *self); static void bz_addons_dialog_dispose (GObject *object) @@ -224,6 +226,7 @@ bz_addons_dialog_class_init (BzAddonsDialogClass *klass) gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, description_toggle); gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, full_view_clamp); gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, list_clamp); + gtk_widget_class_bind_template_child (widget_class, BzAddonsDialog, sorter); gtk_widget_class_bind_template_callback (widget_class, tile_activated_cb); gtk_widget_class_bind_template_callback (widget_class, on_visible_page_tag_changed); @@ -248,13 +251,14 @@ bz_addons_dialog_init (BzAddonsDialog *self) gtk_widget_init_template (GTK_WIDGET (self)); - width_target = adw_property_animation_target_new (G_OBJECT (self), "content-width"); - self->width_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 300, width_target); - adw_timed_animation_set_easing (ADW_TIMED_ANIMATION (self->width_animation), ADW_EASE_IN_OUT_CUBIC); - + width_target = adw_property_animation_target_new (G_OBJECT (self), "content-width"); + self->width_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 300, width_target); height_target = adw_property_animation_target_new (G_OBJECT (self), "content-height"); self->height_animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 0, 300, height_target); - adw_timed_animation_set_easing (ADW_TIMED_ANIMATION (self->height_animation), ADW_EASE_IN_OUT_CUBIC); + + g_signal_connect_swapped (self, "map", G_CALLBACK (animate_to_size), self); + + gtk_custom_sorter_set_sort_func (self->sorter, (GCompareDataFunc) sort_func, self, NULL); } AdwDialog * @@ -287,8 +291,6 @@ bz_addons_dialog_new (BzEntryGroup *group) full_view_page = adw_navigation_view_find_page (self->navigation_view, "full-view"); adw_navigation_view_replace (self->navigation_view, &full_view_page, 1); } - else - g_idle_add_once ((GSourceOnceFunc) animate_to_size, self); return ADW_DIALOG (self); } @@ -399,6 +401,12 @@ animate_to_size (BzAddonsDialog *self) int nat = 0; int cur_width = 0; int measure_for = 0; + int cur_w = 0; + int cur_h = 0; + int delta_w = 0; + int delta_h = 0; + int delta = 0; + guint duration = 0; tag = adw_navigation_view_get_visible_page_tag (self->navigation_view); @@ -406,7 +414,7 @@ animate_to_size (BzAddonsDialog *self) { cur_width = gtk_widget_get_width (GTK_WIDGET (self)); target_width = 500; - measure_for = MIN (target_width, cur_width) - 48; + measure_for = MAX (-1, MIN (target_width, cur_width) - 48); gtk_widget_measure (GTK_WIDGET (self->list_clamp), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL); target_height = CLAMP (nat + 50, 300, 600); } @@ -414,7 +422,7 @@ animate_to_size (BzAddonsDialog *self) { cur_width = gtk_widget_get_width (GTK_WIDGET (self)); target_width = 500; - measure_for = MIN (target_width, cur_width) - 48; + measure_for = MAX (-1, MIN (target_width, cur_width) - 48); gtk_widget_measure (GTK_WIDGET (self->full_view_clamp), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL); target_height = CLAMP (nat + 50, 300, 700); } @@ -429,7 +437,7 @@ animate_to_size (BzAddonsDialog *self) target_width = 400; measure_for = target_width - 48; gtk_widget_measure (GTK_WIDGET (self->navigation_view), GTK_ORIENTATION_VERTICAL, measure_for, NULL, &nat, NULL, NULL); - target_height = CLAMP (nat + 0, 300, 700); + target_height = CLAMP (nat, 300, 700); } else if (g_strcmp0 (tag, "stats") == 0) { @@ -439,9 +447,20 @@ animate_to_size (BzAddonsDialog *self) else return; - adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->width_animation), adw_dialog_get_content_width (ADW_DIALOG (self))); + cur_w = adw_dialog_get_content_width (ADW_DIALOG (self)); + cur_h = adw_dialog_get_content_height (ADW_DIALOG (self)); + delta_w = ABS (target_width - cur_w); + delta_h = ABS (target_height - cur_h); + delta = MAX (delta_w, delta_h); + + duration = (guint) CLAMP (delta * 0.6, 200, (target_width < cur_w || target_height < cur_h) ? 300 : 600); + + adw_timed_animation_set_duration (ADW_TIMED_ANIMATION (self->width_animation), duration); + adw_timed_animation_set_duration (ADW_TIMED_ANIMATION (self->height_animation), duration); + + adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->width_animation), cur_w); adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->width_animation), target_width); - adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->height_animation), adw_dialog_get_content_height (ADW_DIALOG (self))); + adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->height_animation), cur_h); adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->height_animation), target_height); adw_animation_play (self->width_animation); adw_animation_play (self->height_animation); @@ -650,3 +669,27 @@ tile_activated_cb (BzAddonTile *tile) adw_navigation_view_push_by_tag (self->navigation_view, "full-view"); } + +static int +sort_func (BzEntryGroup *a, + BzEntryGroup *b, + BzAddonsDialog *self) +{ + const char *desc_a = NULL; + const char *desc_b = NULL; + gboolean has_a = FALSE; + gboolean has_b = FALSE; + int result = 0; + + desc_a = bz_entry_group_get_description (a); + desc_b = bz_entry_group_get_description (b); + has_a = desc_a != NULL && *desc_a != '\0'; + has_b = desc_b != NULL && *desc_b != '\0'; + if (has_a != has_b) + result = has_b - has_a; + else + result = g_utf8_collate (bz_entry_group_get_title (a), + bz_entry_group_get_title (b)); + + return result; +} From 404a38dab4ba63e23168315771ed39eaa223c1e5 Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:14:50 +0200 Subject: [PATCH 6/7] Fix empty dialog when opening quickly after app start Addon group IDs were dropped the first time addons came in because they came before applications, only getting saved the second time when the initial cache got replaced and entry groups for apps already existed --- src/bz-application.c | 46 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/bz-application.c b/src/bz-application.c index a5cc8c16..06bb47ed 100644 --- a/src/bz-application.c +++ b/src/bz-application.c @@ -97,6 +97,8 @@ struct _BzApplication GHashTable *installed_set; GHashTable *sys_name_to_addons; GHashTable *usr_name_to_addons; + GHashTable *sys_ref_to_addon_group_ids; + GHashTable *usr_ref_to_addon_group_ids; GListStore *groups; GListStore *installed_apps; GListStore *search_biases_backing; @@ -388,6 +390,8 @@ bz_application_dispose (GObject *object) g_clear_pointer (&self->sys_name_to_addons, g_hash_table_unref); g_clear_pointer (&self->txt_blocked_id_sets, g_ptr_array_unref); g_clear_pointer (&self->usr_name_to_addons, g_hash_table_unref); + g_clear_pointer (&self->sys_ref_to_addon_group_ids, g_hash_table_unref); + g_clear_pointer (&self->usr_ref_to_addon_group_ids, g_hash_table_unref); g_weak_ref_clear (&self->main_window); G_OBJECT_CLASS (bz_application_parent_class)->dispose (object); @@ -1994,9 +1998,12 @@ fiber_replace_entry (BzApplication *self, if (bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_APPLICATION)) { - gboolean ignore_eol = FALSE; - const char *runtime_name = NULL; - BzEntry *eol_runtime = NULL; + gboolean ignore_eol = FALSE; + const char *runtime_name = NULL; + BzEntry *eol_runtime = NULL; + BzEntryGroup *group = NULL; + GHashTable *ref_to_addon_group_ids = NULL; + GPtrArray *pending = NULL; if (self->ignore_eol_set != NULL) ignore_eol = g_hash_table_contains (self->ignore_eol_set, id); @@ -2005,7 +2012,18 @@ fiber_replace_entry (BzApplication *self, if (!ignore_eol && runtime_name != NULL) eol_runtime = g_hash_table_lookup (self->eol_runtimes, runtime_name); - ensure_group_and_add (self, id, entry, eol_runtime, ignore_eol, installed); + group = ensure_group_and_add (self, id, entry, eol_runtime, ignore_eol, installed); + + ref_to_addon_group_ids = user + ? self->usr_ref_to_addon_group_ids + : self->sys_ref_to_addon_group_ids; + pending = g_hash_table_lookup (ref_to_addon_group_ids, id); + if (pending != NULL) + { + for (guint i = 0; i < pending->len; i++) + bz_entry_group_append_addon_group_id (group, g_ptr_array_index (pending, i)); + g_hash_table_remove (ref_to_addon_group_ids, id); + } } if (flatpak_id != NULL && @@ -2050,6 +2068,22 @@ fiber_replace_entry (BzApplication *self, app_group = g_hash_table_lookup (self->ids_to_groups, parts[1]); if (app_group != NULL) bz_entry_group_append_addon_group_id (app_group, id); + else + { + GHashTable *ref_to_addon_group_ids = user + ? self->usr_ref_to_addon_group_ids + : self->sys_ref_to_addon_group_ids; + GPtrArray *pending = NULL; + + pending = g_hash_table_lookup (ref_to_addon_group_ids, parts[1]); + if (pending == NULL) + { + pending = g_ptr_array_new_with_free_func (g_free); + g_hash_table_replace (ref_to_addon_group_ids, + g_strdup (parts[1]), pending); + } + g_ptr_array_add (pending, g_strdup (id)); + } } } @@ -2952,6 +2986,10 @@ init_service_struct (BzApplication *self, g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref); self->usr_name_to_addons = g_hash_table_new_full ( g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref); + self->sys_ref_to_addon_group_ids = g_hash_table_new_full ( + g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref); + self->usr_ref_to_addon_group_ids = g_hash_table_new_full ( + g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref); self->entry_factory = bz_application_map_factory_new ( (GtkMapListModelMapFunc) map_ids_to_entries, From 9781965607a051bbe0a460e7ac49beabb8281cc5 Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:49:29 +0200 Subject: [PATCH 7/7] Fix style issues --- src/bz-addons-dialog.c | 3 ++- src/bz-application.c | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/bz-addons-dialog.c b/src/bz-addons-dialog.c index cf687480..8adc1d04 100644 --- a/src/bz-addons-dialog.c +++ b/src/bz-addons-dialog.c @@ -621,10 +621,11 @@ set_selected_group (BzAddonsDialog *self, if (bz_result_get_resolved (self->selected_ui_entry)) { - g_autoptr (BzEntry) entry = g_object_ref (bz_result_get_object (self->selected_ui_entry)); + g_autoptr (BzEntry) entry = NULL; g_autoptr (DexFuture) object_future = NULL; GWeakRef *wr = NULL; + entry = g_object_ref (bz_result_get_object (self->selected_ui_entry)); object_future = dex_future_new_for_object (entry); wr = bz_track_weak (self); dex_unref (on_selected_ui_entry_resolved (object_future, wr)); diff --git a/src/bz-application.c b/src/bz-application.c index 06bb47ed..3646b45a 100644 --- a/src/bz-application.c +++ b/src/bz-application.c @@ -222,6 +222,14 @@ static DexFuture * watch_backend_notifs_then_loop_cb (DexFuture *future, GWeakRef *wr); +static BzEntryGroup * +ensure_group_and_add (BzApplication *self, + const char *id, + BzEntry *entry, + BzEntry *eol_runtime, + gboolean ignore_eol, + gboolean installed); + static void fiber_replace_entry (BzApplication *self, BzEntry *entry);