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',