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..02a21f07 100644 --- a/src/bz-addons-dialog.blp +++ b/src/bz-addons-dialog.blp @@ -2,41 +2,345 @@ 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: SortListModel { + sorter: CustomSorter sorter {}; + 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"; + title: bind try { template.selected-ui-entry as <$BzResult>.object as <$BzEntry>.title, _("Add-on Page") }; + + 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: 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 ; + clicked => $install_cb(); + }; + } + + StackPage { + name: "open"; + child: Box { + spacing: 8; + margin-top: 6; + margin-bottom: 6; + margin-start: 6; + margin-end: 6; + + 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..8adc1d04 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,50 @@ * 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; + GtkCustomSorter *sorter; }; G_DEFINE_FINAL_TYPE (BzAddonsDialog, bz_addons_dialog, ADW_TYPE_DIALOG) @@ -46,360 +70,627 @@ 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 int sort_func (BzEntryGroup *a, BzEntryGroup *b, BzAddonsDialog *self); 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; + + 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); + + 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); - g_object_get (entry, - "title", &title, - "description", &description, - NULL); + 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); - adw_preferences_row_set_title (ADW_PREFERENCES_ROW (action_row), title); - adw_action_row_set_subtitle (action_row, description); + 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); - update_button_for_entry (action_button, entry); + 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_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); + 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, 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); - 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); + g_signal_connect_swapped (self, "map", G_CALLBACK (animate_to_size), self); - adw_action_row_set_subtitle (action_row, description); + gtk_custom_sorter_set_sort_func (self->sorter, (GCompareDataFunc) sort_func, 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); +AdwDialog * +bz_addons_dialog_new (BzEntryGroup *group) +{ + GListModel *ids = NULL; + GListModel *groups = NULL; + BzApplicationMapFactory *factory = NULL; + BzAddonsDialog *self = NULL; - update_button_for_entry (action_button, entry); + 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); + } - g_object_set_data (G_OBJECT (action_row), "button", action_button); + self = g_object_new ( + BZ_TYPE_ADDONS_DIALOG, + "addon-groups", groups, + NULL); - adw_action_row_add_suffix (action_row, GTK_WIDGET (action_button)); - adw_action_row_set_activatable_widget (action_row, GTK_WIDGET (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; - 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); + set_selected_group (self, single); + full_view_page = adw_navigation_view_find_page (self->navigation_view, "full-view"); + adw_navigation_view_replace (self->navigation_view, &full_view_page, 1); + } - 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); + full_view = adw_navigation_view_find_page (self->navigation_view, "full-view"); + adw_navigation_view_replace (self->navigation_view, &full_view, 1); - return strcasecmp (bz_entry_get_title (a_entry), - bz_entry_get_title (b_entry)); + 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; + + if (self->selected_group == NULL) + return; - return dex_future_new_true (); + 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; + + ui_entry = bz_result_get_object (self->selected_ui_entry); + if (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); + 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); - - switch (prop_id) + const char *tag = NULL; + int target_width = 0; + int target_height = 0; + 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); + + 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 = 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); + } + else if (g_strcmp0 (tag, "full-view") == 0) + { + cur_width = gtk_widget_get_width (GTK_WIDGET (self)); + target_width = 500; + 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); + } + 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, 300, 700); + } + else if (g_strcmp0 (tag, "stats") == 0) + { + target_width = 1250; + target_height = 750; } + else + return; + + 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), 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); } 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 = 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)); + 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); + + 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 ADW_DIALOG (addons_dialog); + return result; } 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..3646b45a 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; @@ -220,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); @@ -388,6 +398,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); @@ -1904,6 +1916,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 +1964,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 +1974,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 +1988,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 +1999,20 @@ 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; + BzEntryGroup *group = NULL; + GHashTable *ref_to_addon_group_ids = NULL; + GPtrArray *pending = 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,29 +2020,17 @@ 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)); + group = ensure_group_and_add (self, id, entry, eol_runtime, ignore_eol, installed); - if (installed) - g_list_store_insert_sorted ( - self->installed_apps, new_group, - (GCompareDataFunc) cmp_group, NULL); + 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); } } @@ -2029,23 +2061,51 @@ 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); + 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)); + } + } + } + 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)); @@ -2934,6 +2994,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, @@ -3112,14 +3176,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',