[gnome-software/1166-repository-dialog-design-updates] gs-repos-dialog: Implement design updates



commit 3f487bc14dd8d1d242129a9e2730ca35c7cb5d3b
Author: Milan Crha <mcrha redhat com>
Date:   Mon Jun 28 12:54:17 2021 +0200

    gs-repos-dialog: Implement design updates
    
    Closes https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1166

 plugins/flatpak/gs-flatpak-utils.c        |  15 +-
 plugins/flatpak/gs-flatpak-utils.h        |   3 +-
 plugins/flatpak/gs-flatpak.c              |  17 +-
 plugins/flatpak/gs-flatpak.h              |   2 +-
 plugins/flatpak/gs-plugin-flatpak.c       |   2 +-
 plugins/fwupd/gs-plugin-fwupd.c           |   2 +
 plugins/packagekit/gs-plugin-packagekit.c |   3 +
 plugins/rpm-ostree/gs-plugin-rpm-ostree.c |   3 +
 po/POTFILES.in                            |   1 +
 src/gs-repo-row.c                         | 319 +++++++++++---------
 src/gs-repo-row.h                         |  15 +-
 src/gs-repo-row.ui                        | 172 +++++------
 src/gs-repos-dialog.c                     | 471 +++++++++++-------------------
 src/gs-repos-dialog.ui                    |  47 +--
 src/gs-repos-section.c                    | 176 +++++++++++
 src/gs-repos-section.h                    |  27 ++
 src/gtk-style.css                         |  12 +
 src/meson.build                           |   1 +
 18 files changed, 684 insertions(+), 604 deletions(-)
---
diff --git a/plugins/flatpak/gs-flatpak-utils.c b/plugins/flatpak/gs-flatpak-utils.c
index af2af9141..d57982dc5 100644
--- a/plugins/flatpak/gs-flatpak-utils.c
+++ b/plugins/flatpak/gs-flatpak-utils.c
@@ -9,6 +9,8 @@
 #include <config.h>
 #include <ostree.h>
 
+#include <glib/gi18n.h>
+
 #include "gs-flatpak-app.h"
 #include "gs-flatpak.h"
 #include "gs-flatpak-utils.h"
@@ -62,7 +64,8 @@ gs_flatpak_error_convert (GError **perror)
 }
 
 GsApp *
-gs_flatpak_app_new_from_remote (FlatpakRemote *xremote)
+gs_flatpak_app_new_from_remote (FlatpakRemote *xremote,
+                               gboolean is_user)
 {
        g_autofree gchar *title = NULL;
        g_autofree gchar *url = NULL;
@@ -77,12 +80,16 @@ gs_flatpak_app_new_from_remote (FlatpakRemote *xremote)
                         flatpak_remote_get_name (xremote));
        gs_app_set_size_download (app, GS_APP_SIZE_UNKNOWABLE);
 
+       gs_app_set_metadata (app, "GnomeSoftware::InstallationKind",
+               is_user ? _("User Installation") : _("System Installation"));
+
        /* title */
        title = flatpak_remote_get_title (xremote);
-       if (title != NULL) {
+       if (title != NULL)
                gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, title);
-               gs_app_set_origin_ui (app, title);
-       }
+
+       /* origin_ui on a remote is not the remote title */
+       gs_app_set_origin_ui (app, _("Applications"));
 
        /* url */
        url = flatpak_remote_get_url (xremote);
diff --git a/plugins/flatpak/gs-flatpak-utils.h b/plugins/flatpak/gs-flatpak-utils.h
index 61cd62d37..dcbf2840e 100644
--- a/plugins/flatpak/gs-flatpak-utils.h
+++ b/plugins/flatpak/gs-flatpak-utils.h
@@ -13,7 +13,8 @@ G_BEGIN_DECLS
 #include <gnome-software.h>
 
 void            gs_flatpak_error_convert               (GError         **perror);
-GsApp          *gs_flatpak_app_new_from_remote         (FlatpakRemote  *xremote);
+GsApp          *gs_flatpak_app_new_from_remote         (FlatpakRemote  *xremote,
+                                                        gboolean        is_user);
 GsApp          *gs_flatpak_app_new_from_repo_file      (GFile          *file,
                                                         GCancellable   *cancellable,
                                                         GError         **error);
diff --git a/plugins/flatpak/gs-flatpak.c b/plugins/flatpak/gs-flatpak.c
index ce6a6615d..4dae82e60 100644
--- a/plugins/flatpak/gs-flatpak.c
+++ b/plugins/flatpak/gs-flatpak.c
@@ -377,7 +377,7 @@ gs_flatpak_create_source (GsFlatpak *self, FlatpakRemote *xremote)
        g_autoptr(GsApp) app = NULL;
 
        /* create a temp GsApp */
-       app = gs_flatpak_app_new_from_remote (xremote);
+       app = gs_flatpak_app_new_from_remote (xremote, flatpak_installation_get_is_user (self->installation));
        gs_flatpak_claim_app (self, app);
 
        /* we already have one, returned the ref'd cached copy */
@@ -3162,10 +3162,10 @@ gs_flatpak_launch (GsFlatpak *self,
 }
 
 gboolean
-gs_flatpak_app_remove_source (GsFlatpak *self,
-                             GsApp *app,
-                             GCancellable *cancellable,
-                             GError **error)
+gs_flatpak_app_disable_source (GsFlatpak *self,
+                              GsApp *app,
+                              GCancellable *cancellable,
+                              GError **error)
 {
        g_autoptr(FlatpakRemote) xremote = NULL;
 
@@ -3183,8 +3183,9 @@ gs_flatpak_app_remove_source (GsFlatpak *self,
 
        /* remove */
        gs_app_set_state (app, GS_APP_STATE_REMOVING);
-       if (!flatpak_installation_remove_remote (self->installation,
-                                                gs_app_get_id (app),
+       flatpak_remote_set_disabled (xremote, TRUE);
+       if (!flatpak_installation_modify_remote (self->installation,
+                                                xremote,
                                                 cancellable,
                                                 error)) {
                gs_flatpak_error_convert (error);
@@ -3198,7 +3199,7 @@ gs_flatpak_app_remove_source (GsFlatpak *self,
                xb_silo_invalidate (self->silo);
        g_rw_lock_reader_unlock (&self->silo_lock);
 
-       gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE);
+       gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
 
        gs_plugin_repository_changed (self->plugin, app);
 
diff --git a/plugins/flatpak/gs-flatpak.h b/plugins/flatpak/gs-flatpak.h
index 5eb7bb452..40b93d0e1 100644
--- a/plugins/flatpak/gs-flatpak.h
+++ b/plugins/flatpak/gs-flatpak.h
@@ -76,7 +76,7 @@ gboolean      gs_flatpak_launch               (GsFlatpak              *self,
                                                 GsApp                  *app,
                                                 GCancellable           *cancellable,
                                                 GError                 **error);
-gboolean       gs_flatpak_app_remove_source    (GsFlatpak              *self,
+gboolean       gs_flatpak_app_disable_source   (GsFlatpak              *self,
                                                 GsApp                  *app,
                                                 GCancellable           *cancellable,
                                                 GError                 **error);
diff --git a/plugins/flatpak/gs-plugin-flatpak.c b/plugins/flatpak/gs-plugin-flatpak.c
index 364bc27d1..694007359 100644
--- a/plugins/flatpak/gs-plugin-flatpak.c
+++ b/plugins/flatpak/gs-plugin-flatpak.c
@@ -867,7 +867,7 @@ gs_plugin_app_remove (GsPlugin *plugin,
 
        /* is a source */
        if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY)
-               return gs_flatpak_app_remove_source (flatpak, app, cancellable, error);
+               return gs_flatpak_app_disable_source (flatpak, app, cancellable, error);
 
        /* build and run transaction */
        transaction = _build_transaction (plugin, flatpak, cancellable, error);
diff --git a/plugins/fwupd/gs-plugin-fwupd.c b/plugins/fwupd/gs-plugin-fwupd.c
index 3ce397cac..1e90c592c 100644
--- a/plugins/fwupd/gs-plugin-fwupd.c
+++ b/plugins/fwupd/gs-plugin-fwupd.c
@@ -1190,6 +1190,8 @@ gs_plugin_add_sources (GsPlugin *plugin,
                gs_app_set_metadata (app, "fwupd::remote-id",
                                     fwupd_remote_get_id (remote));
                gs_app_set_management_plugin (app, "fwupd");
+               gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "fwupd");
+               gs_app_set_origin_ui (app, _("Firmware"));
                gs_app_list_add (list, app);
        }
        return TRUE;
diff --git a/plugins/packagekit/gs-plugin-packagekit.c b/plugins/packagekit/gs-plugin-packagekit.c
index 8988db2f9..e476cb919 100644
--- a/plugins/packagekit/gs-plugin-packagekit.c
+++ b/plugins/packagekit/gs-plugin-packagekit.c
@@ -11,6 +11,7 @@
 
 #include <config.h>
 
+#include <glib/gi18n-lib.h>
 #include <gnome-software.h>
 #include <gsettings-desktop-schemas/gdesktop-enums.h>
 #include <packagekit-glib2/packagekit.h>
@@ -295,6 +296,8 @@ gs_plugin_add_sources (GsPlugin *plugin,
                gs_app_set_summary (app,
                                    GS_APP_QUALITY_LOWEST,
                                    pk_repo_detail_get_description (rd));
+               gs_plugin_packagekit_set_packaging_format (plugin, app);
+               gs_app_set_origin_ui (app, _("Packages"));
                gs_app_list_add (list, app);
                g_hash_table_insert (hash,
                                     g_strdup (id),
diff --git a/plugins/rpm-ostree/gs-plugin-rpm-ostree.c b/plugins/rpm-ostree/gs-plugin-rpm-ostree.c
index 6b1a13519..869615183 100644
--- a/plugins/rpm-ostree/gs-plugin-rpm-ostree.c
+++ b/plugins/rpm-ostree/gs-plugin-rpm-ostree.c
@@ -14,6 +14,7 @@
 #include <gio/gio.h>
 #include <gio/gunixfdlist.h>
 #include <glib/gstdio.h>
+#include <glib/gi18n-lib.h>
 #include <libdnf/libdnf.h>
 #include <ostree.h>
 #include <rpm/rpmdb.h>
@@ -2147,6 +2148,8 @@ gs_plugin_add_sources (GsPlugin *plugin,
                gs_app_set_name (app, GS_APP_QUALITY_LOWEST, description);
                gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, description);
 
+               gs_app_set_origin_ui (app, _("Operating System (RPM-OStree)"));
+
                gs_app_list_add (list, app);
        }
 
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8c620503f..9b3b3d873 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -90,6 +90,7 @@ plugins/eos-updater/gs-plugin-eos-updater.c
 plugins/fedora-pkgdb-collections/gs-plugin-fedora-pkgdb-collections.c
 plugins/flatpak/org.gnome.Software.Plugin.Flatpak.metainfo.xml.in
 plugins/flatpak/gs-flatpak.c
+plugins/flatpak/gs-flatpak-utils.c
 plugins/flatpak/gs-plugin-flatpak.c
 plugins/fwupd/gs-fwupd-app.c
 plugins/fwupd/gs-plugin-fwupd.c
diff --git a/src/gs-repo-row.c b/src/gs-repo-row.c
index e9529e9e3..ce27f52f3 100644
--- a/src/gs-repo-row.c
+++ b/src/gs-repo-row.c
@@ -15,146 +15,73 @@
 typedef struct
 {
        GsApp           *repo;
-       GtkWidget       *button;
        GtkWidget       *name_label;
+       GtkWidget       *hostname_label;
        GtkWidget       *comment_label;
-       GtkWidget       *details_revealer;
-       GtkWidget       *status_label;
-       GtkWidget       *url_box;
-       GtkWidget       *url_label;
+       GtkWidget       *disable_switch;
+       gulong           switch_handler_id;
        guint            refresh_idle_id;
+       guint            busy;
 } GsRepoRowPrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE (GsRepoRow, gs_repo_row, GTK_TYPE_LIST_BOX_ROW)
 
 enum {
-       SIGNAL_BUTTON_CLICKED,
+       SIGNAL_SWITCH_CLICKED,
        SIGNAL_LAST
 };
 
 static guint signals [SIGNAL_LAST] = { 0 };
 
-void
-gs_repo_row_set_name (GsRepoRow *row, const gchar *name)
-{
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
-
-       gtk_label_set_text (GTK_LABEL (priv->name_label), name);
-       gtk_widget_set_visible (priv->name_label, name != NULL);
-}
-
-void
-gs_repo_row_set_comment (GsRepoRow *row, const gchar *comment)
-{
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
-
-       gtk_label_set_markup (GTK_LABEL (priv->comment_label), comment);
-       gtk_widget_set_visible (priv->comment_label, comment != NULL);
-}
-
-void
-gs_repo_row_set_url (GsRepoRow *row, const gchar *url)
-{
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
-
-       gtk_label_set_text (GTK_LABEL (priv->url_label), url);
-       gtk_widget_set_visible (priv->url_box, url != NULL);
-}
-
-static gboolean
-repo_supports_removal (GsApp *repo)
-{
-       const gchar *management_plugin = gs_app_get_management_plugin (repo);
-
-       /* can't remove a repo, only enable/disable existing ones */
-       if (g_strcmp0 (management_plugin, "fwupd") == 0 ||
-           g_strcmp0 (management_plugin, "packagekit") == 0 ||
-           g_strcmp0 (management_plugin, "rpm-ostree") == 0)
-               return FALSE;
-
-       return TRUE;
-}
-
 static void
 refresh_ui (GsRepoRow *row)
 {
        GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+       gboolean active = FALSE;
+       gboolean sensitive = TRUE;
 
        if (priv->repo == NULL) {
-               gtk_widget_set_visible (priv->button, FALSE);
+               gtk_widget_set_sensitive (priv->disable_switch, FALSE);
+               gtk_switch_set_active (GTK_SWITCH (priv->disable_switch), FALSE);
                return;
        }
 
-       gtk_widget_set_visible (priv->button, TRUE);
+       g_signal_handler_block (priv->disable_switch, priv->switch_handler_id);
+       gtk_widget_set_sensitive (priv->disable_switch, TRUE);
 
-       /* set button text */
        switch (gs_app_get_state (priv->repo)) {
        case GS_APP_STATE_AVAILABLE:
        case GS_APP_STATE_AVAILABLE_LOCAL:
-               /* TRANSLATORS: this is a button in the software repositories
-                  dialog for enabling a repo */
-               gtk_button_set_label (GTK_BUTTON (priv->button), _("_Enable"));
-               /* enable button */
-               gtk_widget_set_sensitive (priv->button, TRUE);
+               active = FALSE;
                break;
        case GS_APP_STATE_INSTALLED:
-               if (repo_supports_removal (priv->repo)) {
-                       /* TRANSLATORS: this is a button in the software repositories dialog
-                          for removing a repo. The ellipsis indicates that further
-                          steps are required */
-                       gtk_button_set_label (GTK_BUTTON (priv->button), _("_Remove…"));
-               } else {
-                       /* TRANSLATORS: this is a button in the software repositories dialog
-                          for disabling a repo. The ellipsis indicates that further
-                          steps are required */
-                       gtk_button_set_label (GTK_BUTTON (priv->button), _("_Disable…"));
-               }
-               /* enable button */
-               gtk_widget_set_sensitive (priv->button, TRUE);
+               active = TRUE;
                break;
        case GS_APP_STATE_INSTALLING:
-               /* TRANSLATORS: this is a button in the software repositories dialog
-                  that shows the status of a repo being enabled */
-               gtk_button_set_label (GTK_BUTTON (priv->button), _("Enabling"));
-               /* disable button */
-               gtk_widget_set_sensitive (priv->button, FALSE);
+               active = TRUE;
+               sensitive = FALSE;
                break;
        case GS_APP_STATE_REMOVING:
-               if (repo_supports_removal (priv->repo)) {
-                       /* TRANSLATORS: this is a button in the software repositories dialog
-                          that shows the status of a repo being removed */
-                       gtk_button_set_label (GTK_BUTTON (priv->button), _("Removing"));
-               } else {
-                       /* TRANSLATORS: this is a button in the software repositories dialog
-                          that shows the status of a repo being disabled */
-                       gtk_button_set_label (GTK_BUTTON (priv->button), _("Disabling"));
-               }
-               /* disable button */
-               gtk_widget_set_sensitive (priv->button, FALSE);
+               active = FALSE;
+               sensitive = FALSE;
                break;
        case GS_APP_STATE_UNAVAILABLE:
+               g_signal_handler_unblock (priv->disable_switch, priv->switch_handler_id);
                gtk_widget_destroy (GTK_WIDGET (row));
                return;
        default:
                break;
        }
 
-       /* set enabled/disabled label */
-       switch (gs_app_get_state (priv->repo)) {
-       case GS_APP_STATE_INSTALLED:
-               /* TRANSLATORS: this is a label in the software repositories
-                  dialog that indicates that a repo is enabled. */
-               gtk_label_set_text (GTK_LABEL (priv->status_label), _("Enabled"));
-               break;
-       case GS_APP_STATE_AVAILABLE:
-       case GS_APP_STATE_AVAILABLE_LOCAL:
-               /* TRANSLATORS: this is a label in the software repositories
-                  dialog that indicates that a repo is disabled. */
-               gtk_label_set_text (GTK_LABEL (priv->status_label), _("Disabled"));
-               break;
-       default:
-               break;
-       }
+       /* disable main repo */
+       sensitive = sensitive &&
+                   !priv->busy &&
+                   !gs_app_has_quirk (priv->repo, GS_APP_QUIRK_PROVENANCE);
+
+       gtk_switch_set_active (GTK_SWITCH (priv->disable_switch), active);
+       gtk_widget_set_sensitive (priv->disable_switch, sensitive);
+
+       g_signal_handler_unblock (priv->disable_switch, priv->switch_handler_id);
 }
 
 static gboolean
@@ -180,10 +107,77 @@ repo_state_changed_cb (GsApp *repo, GParamSpec *pspec, GsRepoRow *row)
        priv->refresh_idle_id = g_idle_add (refresh_idle, g_object_ref (row));
 }
 
+static gchar *
+get_repo_installed_text (GsApp *repo)
+{
+       GsAppList *related;
+       guint cnt_addon = 0;
+       guint cnt_apps = 0;
+       g_autofree gchar *addons_text = NULL;
+       g_autofree gchar *apps_text = NULL;
+
+       related = gs_app_get_related (repo);
+       for (guint i = 0; i < gs_app_list_length (related); i++) {
+               GsApp *app_tmp = gs_app_list_index (related, i);
+               switch (gs_app_get_kind (app_tmp)) {
+               case AS_COMPONENT_KIND_WEB_APP:
+               case AS_COMPONENT_KIND_DESKTOP_APP:
+                       cnt_apps++;
+                       break;
+               case AS_COMPONENT_KIND_FONT:
+               case AS_COMPONENT_KIND_CODEC:
+               case AS_COMPONENT_KIND_INPUT_METHOD:
+               case AS_COMPONENT_KIND_ADDON:
+                       cnt_addon++;
+                       break;
+               default:
+                       break;
+               }
+       }
+
+       if (cnt_addon == 0) {
+               /* TRANSLATORS: This string is used to construct the 'X applications
+                  installed' sentence, describing a software repository. */
+               return g_strdup_printf (ngettext ("%u application installed",
+                                                 "%u applications installed",
+                                                 cnt_apps), cnt_apps);
+       }
+       if (cnt_apps == 0) {
+               /* TRANSLATORS: This string is used to construct the 'X add-ons
+                  installed' sentence, describing a software repository. */
+               return g_strdup_printf (ngettext ("%u add-on installed",
+                                                 "%u add-ons installed",
+                                                 cnt_addon), cnt_addon);
+       }
+
+       /* TRANSLATORS: This string is used to construct the 'X applications
+          and y add-ons installed' sentence, describing a software repository.
+          The correct form here depends on the number of applications. */
+       apps_text = g_strdup_printf (ngettext ("%u application",
+                                              "%u applications",
+                                              cnt_apps), cnt_apps);
+       /* TRANSLATORS: This string is used to construct the 'X applications
+          and y add-ons installed' sentence, describing a software repository.
+          The correct form here depends on the number of add-ons. */
+       addons_text = g_strdup_printf (ngettext ("%u add-on",
+                                                "%u add-ons",
+                                                cnt_addon), cnt_addon);
+       /* TRANSLATORS: This string is used to construct the 'X applications
+          and y add-ons installed' sentence, describing a software repository.
+          The correct form here depends on the total number of
+          applications and add-ons. */
+       return g_strdup_printf (ngettext ("%s and %s installed",
+                                         "%s and %s installed",
+                                         cnt_apps + cnt_addon),
+                                         apps_text, addons_text);
+}
+
 void
 gs_repo_row_set_repo (GsRepoRow *row, GsApp *repo)
 {
        GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+       g_autofree gchar *comment = NULL;
+       const gchar *tmp;
 
        g_assert (priv->repo == NULL);
 
@@ -191,6 +185,36 @@ gs_repo_row_set_repo (GsRepoRow *row, GsApp *repo)
        g_signal_connect_object (priv->repo, "notify::state",
                                 G_CALLBACK (repo_state_changed_cb),
                                 row, 0);
+
+       gtk_label_set_label (GTK_LABEL (priv->name_label), gs_app_get_name (repo));
+
+       gtk_widget_set_visible (priv->hostname_label, FALSE);
+
+       tmp = gs_app_get_url (repo, AS_URL_KIND_HOMEPAGE);
+       if (tmp != NULL && *tmp != '\0') {
+               g_autoptr(GUri) uri = NULL;
+
+               uri = g_uri_parse (tmp, G_URI_FLAGS_PARSE_RELAXED, NULL);
+               if (uri && g_uri_get_host (uri) != NULL && *g_uri_get_host (uri) != '\0') {
+                       gtk_label_set_label (GTK_LABEL (priv->hostname_label), g_uri_get_host (uri));
+                       gtk_widget_set_visible (priv->hostname_label, TRUE);
+               }
+       }
+
+       comment = get_repo_installed_text (repo);
+       tmp = gs_app_get_metadata_item (priv->repo, "GnomeSoftware::InstallationKind");
+       if (tmp != NULL && *tmp != '\0') {
+               gchar *cnt;
+
+               /* Translators: The first '%s' is replaced with a text like '10 applications installed',
+                     the second '%s' is replaced with installation kind, like in case of Flatpak 'User 
Installation'. */
+               cnt = g_strdup_printf (C_("repo-row", "%s • %s"), comment, tmp);
+               g_clear_pointer (&comment, g_free);
+               comment = cnt;
+       }
+
+       gtk_label_set_label (GTK_LABEL (priv->comment_label), comment);
+
        refresh_ui (row);
 }
 
@@ -201,35 +225,21 @@ gs_repo_row_get_repo (GsRepoRow *row)
        return priv->repo;
 }
 
-void
-gs_repo_row_show_details (GsRepoRow *row)
+static void
+disable_switch_clicked_cb (GtkWidget *widget,
+                          GParamSpec *param,
+                          GsRepoRow *row)
 {
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+       GsRepoRowPrivate *priv;
 
-       gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE);
-       gtk_revealer_set_reveal_child (GTK_REVEALER (priv->details_revealer), TRUE);
-}
+       g_return_if_fail (GS_IS_REPO_ROW (row));
 
-void
-gs_repo_row_hide_details (GsRepoRow *row)
-{
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+       priv = gs_repo_row_get_instance_private (row);
 
-       gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE);
-       gtk_revealer_set_reveal_child (GTK_REVEALER (priv->details_revealer), FALSE);
-}
-
-void
-gs_repo_row_show_status (GsRepoRow *row)
-{
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
-       gtk_widget_set_visible (priv->status_label, TRUE);
-}
+       if (!priv->repo || priv->busy)
+               return;
 
-static void
-button_clicked_cb (GtkWidget *widget, GsRepoRow *row)
-{
-       g_signal_emit (row, signals[SIGNAL_BUTTON_CLICKED], 0);
+       g_signal_emit (row, signals[SIGNAL_SWITCH_CLICKED], 0);
 }
 
 static void
@@ -257,8 +267,8 @@ gs_repo_row_init (GsRepoRow *row)
        GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
 
        gtk_widget_init_template (GTK_WIDGET (row));
-       g_signal_connect (priv->button, "clicked",
-                         G_CALLBACK (button_clicked_cb), row);
+       priv->switch_handler_id = g_signal_connect (priv->disable_switch, "notify::active",
+                         G_CALLBACK (disable_switch_clicked_cb), row);
 }
 
 static void
@@ -269,22 +279,19 @@ gs_repo_row_class_init (GsRepoRowClass *klass)
 
        widget_class->destroy = gs_repo_row_destroy;
 
-       signals [SIGNAL_BUTTON_CLICKED] =
-               g_signal_new ("button-clicked",
+       signals [SIGNAL_SWITCH_CLICKED] =
+               g_signal_new ("switch-clicked",
                              G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
-                             G_STRUCT_OFFSET (GsRepoRowClass, button_clicked),
+                             G_STRUCT_OFFSET (GsRepoRowClass, switch_clicked),
                              NULL, NULL, g_cclosure_marshal_VOID__VOID,
-                             G_TYPE_NONE, 0);
+                             G_TYPE_NONE, 0, G_TYPE_NONE);
 
        gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-repo-row.ui");
 
-       gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, button);
        gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, name_label);
+       gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, hostname_label);
        gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, comment_label);
-       gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, details_revealer);
-       gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, status_label);
-       gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, url_box);
-       gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, url_label);
+       gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, disable_switch);
 }
 
 GtkWidget *
@@ -292,3 +299,45 @@ gs_repo_row_new (void)
 {
        return g_object_new (GS_TYPE_REPO_ROW, NULL);
 }
+
+void
+gs_repo_row_set_busy (GsRepoRow *row,
+                     gboolean value)
+{
+       GsRepoRowPrivate *priv;
+
+       g_return_if_fail (GS_IS_REPO_ROW (row));
+
+       priv = gs_repo_row_get_instance_private (row);
+
+       if (value)
+               g_return_if_fail (priv->busy + 1 > priv->busy);
+       else
+               g_return_if_fail (priv->busy > 0);
+
+       priv->busy += (value ? 1 : -1);
+
+       if (value && priv->busy == 1)
+               gtk_widget_set_sensitive (priv->disable_switch, FALSE);
+       else if (!value && !priv->busy)
+               refresh_ui (row);
+}
+
+gboolean
+gs_repo_row_get_busy (GsRepoRow *row)
+{
+       GsRepoRowPrivate *priv;
+
+       g_return_val_if_fail (GS_IS_REPO_ROW (row), FALSE);
+
+       priv = gs_repo_row_get_instance_private (row);
+       return priv->busy > 0;
+}
+
+void
+gs_repo_row_refresh (GsRepoRow *row)
+{
+       g_return_if_fail (GS_IS_REPO_ROW (row));
+
+       refresh_ui (row);
+}
diff --git a/src/gs-repo-row.h b/src/gs-repo-row.h
index ff2564ea5..813e4c841 100644
--- a/src/gs-repo-row.h
+++ b/src/gs-repo-row.h
@@ -20,21 +20,16 @@ G_DECLARE_DERIVABLE_TYPE (GsRepoRow, gs_repo_row, GS, REPO_ROW, GtkListBoxRow)
 struct _GsRepoRowClass
 {
        GtkListBoxRowClass        parent_class;
-       void                    (*button_clicked)       (GsRepoRow      *row);
+       void                    (*switch_clicked)       (GsRepoRow      *row);
 };
 
 GtkWidget      *gs_repo_row_new                        (void);
-void            gs_repo_row_set_name                   (GsRepoRow      *row,
-                                                        const gchar    *name);
-void            gs_repo_row_set_comment                (GsRepoRow      *row,
-                                                        const gchar    *comment);
-void            gs_repo_row_set_url                    (GsRepoRow      *row,
-                                                        const gchar    *url);
 void            gs_repo_row_set_repo                   (GsRepoRow      *row,
                                                         GsApp          *repo);
 GsApp          *gs_repo_row_get_repo                   (GsRepoRow      *row);
-void            gs_repo_row_show_details               (GsRepoRow      *row);
-void            gs_repo_row_hide_details               (GsRepoRow      *row);
-void            gs_repo_row_show_status                (GsRepoRow      *row);
+void            gs_repo_row_set_busy                   (GsRepoRow      *row,
+                                                        gboolean        value);
+gboolean        gs_repo_row_get_busy                   (GsRepoRow      *row);
+void            gs_repo_row_refresh                    (GsRepoRow      *row);
 
 G_END_DECLS
diff --git a/src/gs-repo-row.ui b/src/gs-repo-row.ui
index 2cbe02532..926dfa6b7 100644
--- a/src/gs-repo-row.ui
+++ b/src/gs-repo-row.ui
@@ -2,118 +2,78 @@
 <interface>
   <!-- interface-requires gtk+ 3.10 -->
   <template class="GsRepoRow" parent="GtkListBoxRow">
+    <property name="visible">True</property>
+    <style>
+      <class name="reporow"/>
+    </style>
     <child>
-      <object class="GtkBox">
+      <object class="GtkGrid">
         <property name="visible">True</property>
-        <property name="margin-top">18</property>
-        <property name="margin-bottom">18</property>
-        <property name="margin-start">18</property>
-        <property name="margin-end">18</property>
-        <property name="orientation">horizontal</property>
-        <property name="spacing">16</property>
+        <property name="margin">8</property>
+        <property name="row-spacing">4</property>
+        <property name="column-spacing">4</property>
         <child>
-          <object class="GtkBox" id="vbox">
+          <object class="GtkLabel" id="name_label">
             <property name="visible">True</property>
-            <property name="orientation">vertical</property>
-            <property name="spacing">6</property>
+            <property name="halign">start</property>
             <property name="hexpand">True</property>
-            <child>
-              <object class="GtkBox" id="hbox">
-                <property name="visible">True</property>
-                <property name="orientation">horizontal</property>
-                <property name="spacing">4</property>
-                <child>
-                  <object class="GtkLabel" id="name_label">
-                    <property name="visible">True</property>
-                    <property name="halign">start</property>
-                    <property name="ellipsize">end</property>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkLabel" id="status_label">
-                    <property name="visible">False</property>
-                    <style>
-                      <class name="dim-label"/>
-                    </style>
-                  </object>
-                  <packing>
-                    <property name="pack-type">end</property>
-                  </packing>
-                </child>
-              </object>
-            </child>
-            <child>
-              <object class="GtkLabel" id="comment_label">
-                <property name="visible">False</property>
-                <property name="halign">start</property>
-                <property name="xalign">0</property>
-                <property name="wrap">True</property>
-                <style>
-                  <class name="dim-label"/>
-                </style>
-              </object>
-            </child>
-            <child>
-              <object class="GtkRevealer" id="details_revealer">
-                <property name="visible">True</property>
-                <property name="transition-type">slide-down</property>
-                <child>
-                  <object class="GtkBox">
-                    <property name="visible">True</property>
-                    <property name="orientation">vertical</property>
-                    <property name="spacing">12</property>
-                    <child>
-                      <object class="GtkBox" id="url_box">
-                        <property name="visible">True</property>
-                        <property name="orientation">horizontal</property>
-                        <property name="spacing">8</property>
-                        <child>
-                          <object class="GtkLabel">
-                            <property name="visible">True</property>
-                            <property name="halign">start</property>
-                            <property name="valign">start</property>
-                            <property name="label" translatable="yes">URL</property>
-                            <style>
-                              <class name="dim-label"/>
-                            </style>
-                          </object>
-                        </child>
-                        <child>
-                          <object class="GtkLabel" id="url_label">
-                            <property name="visible">True</property>
-                            <property name="halign">start</property>
-                            <property name="valign">start</property>
-                            <property name="ellipsize">end</property>
-                            <style>
-                              <class name="dim-label"/>
-                            </style>
-                          </object>
-                        </child>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkBox">
-                        <property name="visible">True</property>
-                        <property name="orientation">horizontal</property>
-                        <property name="spacing">8</property>
-                        <child>
-                          <object class="GtkButton" id="button">
-                            <property name="visible">True</property>
-                            <property name="use_underline">True</property>
-                            <property name="width_request">105</property>
-                            <property name="can_focus">True</property>
-                            <property name="receives_default">True</property>
-                            <property name="halign">start</property>
-                            <property name="valign">start</property>
-                          </object>
-                        </child>
-                      </object>
-                    </child>
-                  </object>
-                </child>
-              </object>
-            </child>
           </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="hostname_label">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="hexpand">True</property>
+            <attributes>
+              <attribute name="scale" value="0.8"/>
+            </attributes>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="comment_label">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="hexpand">True</property>
+            <attributes>
+              <attribute name="scale" value="0.8"/>
+            </attributes>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">2</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSwitch" id="disable_switch">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="halign">end</property>
+            <property name="valign">center</property>
+            <property name="hexpand">False</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">0</property>
+            <property name="width">1</property>
+            <property name="height">3</property>
+          </packing>
         </child>
       </object>
     </child>
diff --git a/src/gs-repos-dialog.c b/src/gs-repos-dialog.c
index 3d00b2510..eb44eb3a0 100644
--- a/src/gs-repos-dialog.c
+++ b/src/gs-repos-dialog.c
@@ -16,6 +16,7 @@
 #include "gs-common.h"
 #include "gs-os-release.h"
 #include "gs-repo-row.h"
+#include "gs-repos-section.h"
 #include "gs-third-party-repo-row.h"
 #include "gs-utils.h"
 #include <glib/gi18n.h>
@@ -25,15 +26,13 @@ struct _GsReposDialog
        GtkDialog        parent_instance;
        GSettings       *settings;
        GsApp           *third_party_repo;
+       GHashTable      *sections; /* gchar * ~> GsReposSection * */
 
        GCancellable    *cancellable;
        GsPluginLoader  *plugin_loader;
-       GtkWidget       *label_description;
        GtkWidget       *label_empty;
        GtkWidget       *label_header;
-       GtkWidget       *listbox;
-       GtkWidget       *listbox_third_party;
-       GtkWidget       *row_third_party;
+       GtkWidget       *content_box;
        GtkWidget       *spinner;
        GtkWidget       *stack;
 };
@@ -42,6 +41,7 @@ G_DEFINE_TYPE (GsReposDialog, gs_repos_dialog, GTK_TYPE_DIALOG)
 
 typedef struct {
        GsReposDialog   *dialog;
+       GsRepoRow       *row;
        GsApp           *repo;
        GsPluginAction   action;
 } InstallRemoveData;
@@ -50,6 +50,7 @@ static void
 install_remove_data_free (InstallRemoveData *data)
 {
        g_clear_object (&data->dialog);
+       g_clear_object (&data->row);
        g_clear_object (&data->repo);
        g_slice_free (InstallRemoveData, data);
 }
@@ -59,89 +60,6 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(InstallRemoveData, install_remove_data_free);
 static void reload_sources (GsReposDialog *dialog);
 static void reload_third_party_repo (GsReposDialog *dialog);
 
-static gchar *
-get_repo_installed_text (GsApp *repo)
-{
-       GsAppList *related;
-       guint cnt_addon = 0;
-       guint cnt_apps = 0;
-       g_autofree gchar *addons_text = NULL;
-       g_autofree gchar *apps_text = NULL;
-
-       related = gs_app_get_related (repo);
-       for (guint i = 0; i < gs_app_list_length (related); i++) {
-               GsApp *app_tmp = gs_app_list_index (related, i);
-               switch (gs_app_get_kind (app_tmp)) {
-               case AS_COMPONENT_KIND_WEB_APP:
-               case AS_COMPONENT_KIND_DESKTOP_APP:
-                       cnt_apps++;
-                       break;
-               case AS_COMPONENT_KIND_FONT:
-               case AS_COMPONENT_KIND_CODEC:
-               case AS_COMPONENT_KIND_INPUT_METHOD:
-               case AS_COMPONENT_KIND_ADDON:
-                       cnt_addon++;
-                       break;
-               default:
-                       break;
-               }
-       }
-
-       if (cnt_apps == 0 && cnt_addon == 0) {
-               /* nothing! */
-               return NULL;
-       }
-       if (cnt_addon == 0) {
-               /* TRANSLATORS: This string is used to construct the 'X applications
-                  installed' sentence, describing a software repository. */
-               return g_strdup_printf (ngettext ("%u application installed",
-                                                 "%u applications installed",
-                                                 cnt_apps), cnt_apps);
-       }
-       if (cnt_apps == 0) {
-               /* TRANSLATORS: This string is used to construct the 'X add-ons
-                  installed' sentence, describing a software repository. */
-               return g_strdup_printf (ngettext ("%u add-on installed",
-                                                 "%u add-ons installed",
-                                                 cnt_addon), cnt_addon);
-       }
-
-       /* TRANSLATORS: This string is used to construct the 'X applications
-          and y add-ons installed' sentence, describing a software repository.
-          The correct form here depends on the number of applications. */
-       apps_text = g_strdup_printf (ngettext ("%u application",
-                                              "%u applications",
-                                              cnt_apps), cnt_apps);
-       /* TRANSLATORS: This string is used to construct the 'X applications
-          and y add-ons installed' sentence, describing a software repository.
-          The correct form here depends on the number of add-ons. */
-       addons_text = g_strdup_printf (ngettext ("%u add-on",
-                                                "%u add-ons",
-                                                cnt_addon), cnt_addon);
-       /* TRANSLATORS: This string is used to construct the 'X applications
-          and y add-ons installed' sentence, describing a software repository.
-          The correct form here depends on the total number of
-          applications and add-ons. */
-       return g_strdup_printf (ngettext ("%s and %s installed",
-                                         "%s and %s installed",
-                                         cnt_apps + cnt_addon),
-                                         apps_text, addons_text);
-}
-
-static gboolean
-repo_supports_removal (GsApp *repo)
-{
-       const gchar *management_plugin = gs_app_get_management_plugin (repo);
-
-       /* can't remove a repo, only enable/disable existing ones */
-       if (g_strcmp0 (management_plugin, "fwupd") == 0 ||
-           g_strcmp0 (management_plugin, "packagekit") == 0 ||
-           g_strcmp0 (management_plugin, "rpm-ostree") == 0)
-               return FALSE;
-
-       return TRUE;
-}
-
 static void
 repo_enabled_cb (GObject *source,
                  GAsyncResult *res,
@@ -153,6 +71,7 @@ repo_enabled_cb (GObject *source,
        const gchar *action_str;
 
        action_str = gs_plugin_action_to_string (install_remove_data->action);
+       gs_repo_row_set_busy (install_remove_data->row, FALSE);
 
        if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) {
                if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
@@ -194,21 +113,28 @@ enable_repo_response_cb (GtkDialog *confirm_dialog,
        gtk_widget_destroy (GTK_WIDGET (confirm_dialog));
 
        /* not agreed */
-       if (response != GTK_RESPONSE_OK)
+       if (response != GTK_RESPONSE_OK) {
+               gs_repo_row_set_busy (install_data->row, FALSE);
                return;
+       }
 
        _enable_repo (g_steal_pointer (&install_data));
 }
 
 static void
-enable_repo (GsReposDialog *dialog, GsApp *repo)
+enable_repo (GsReposDialog *dialog,
+            GsRepoRow *row,
+            GsApp *repo)
 {
        g_autoptr(InstallRemoveData) install_data = NULL;
 
        install_data = g_slice_new0 (InstallRemoveData);
        install_data->action = GS_PLUGIN_ACTION_INSTALL;
-       install_data->repo = g_object_ref (repo);
        install_data->dialog = g_object_ref (dialog);
+       install_data->row = g_object_ref (row);
+       install_data->repo = g_object_ref (repo);
+
+       gs_repo_row_set_busy (row, TRUE);
 
        /* user needs to confirm acceptance of an agreement */
        if (gs_app_get_agreement (repo) != NULL) {
@@ -266,8 +192,10 @@ remove_repo_response_cb (GtkDialog *confirm_dialog,
        gtk_widget_destroy (GTK_WIDGET (confirm_dialog));
 
        /* not agreed */
-       if (response != GTK_RESPONSE_OK)
+       if (response != GTK_RESPONSE_OK) {
+               gs_repo_row_set_busy (remove_data->row, FALSE);
                return;
+       }
 
        g_debug ("removing repo %s", gs_app_get_id (remove_data->repo));
        plugin_job = gs_plugin_job_newv (remove_data->action,
@@ -281,52 +209,37 @@ remove_repo_response_cb (GtkDialog *confirm_dialog,
 }
 
 static void
-remove_confirm_repo (GsReposDialog *dialog, GsApp *repo)
+remove_confirm_repo (GsReposDialog *dialog,
+                    GsRepoRow *row,
+                    GsApp *repo)
 {
        InstallRemoveData *remove_data;
        GtkWidget *confirm_dialog;
        g_autofree gchar *message = NULL;
-       g_autofree gchar *title = NULL;
        GtkWidget *button;
        GtkStyleContext *context;
 
        remove_data = g_slice_new0 (InstallRemoveData);
        remove_data->action = GS_PLUGIN_ACTION_REMOVE;
-       remove_data->repo = g_object_ref (repo);
        remove_data->dialog = g_object_ref (dialog);
+       remove_data->row = g_object_ref (row);
+       remove_data->repo = g_object_ref (repo);
 
-       if (repo_supports_removal (repo)) {
-               /* TRANSLATORS: this is a prompt message, and '%s' is a
-                * repository name, e.g. 'GNOME Nightly' */
-               title = g_strdup_printf (_("Remove “%s”?"),
-                                        gs_app_get_name (repo));
-       } else {
-               /* TRANSLATORS: this is a prompt message, and '%s' is a
-                * repository name, e.g. 'GNOME Nightly' */
-               title = g_strdup_printf (_("Disable “%s”?"),
-                                        gs_app_get_name (repo));
-       }
-       /* TRANSLATORS: longer dialog text */
-       message = g_strdup (_("Software that has been installed from this "
-                             "repository will no longer receive updates, "
-                             "including security fixes."));
+       /* TRANSLATORS: The '%s' is replaced with a repository name, like "Fedora Modular - x86_64" */
+       message = g_strdup_printf (_("Software that has been installed from %s will cease receive updates."),
+                       gs_app_get_name (repo));
 
        /* ask for confirmation */
        confirm_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog),
                                                 GTK_DIALOG_MODAL,
                                                 GTK_MESSAGE_QUESTION,
                                                 GTK_BUTTONS_CANCEL,
-                                                "%s", title);
+                                                "%s", _("Disable Repository?"));
        gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (confirm_dialog),
                                                  "%s", message);
 
-       if (repo_supports_removal (repo)) {
-               /* TRANSLATORS: this is button text to remove the repo */
-               button = gtk_dialog_add_button (GTK_DIALOG (confirm_dialog), _("Remove"), GTK_RESPONSE_OK);
-       } else {
-               /* TRANSLATORS: this is button text to remove the repo */
-               button = gtk_dialog_add_button (GTK_DIALOG (confirm_dialog), _("Disable"), GTK_RESPONSE_OK);
-       }
+       /* TRANSLATORS: this is button text to disable a repo */
+       button = gtk_dialog_add_button (GTK_DIALOG (confirm_dialog), _("Disable"), GTK_RESPONSE_OK);
 
        context = gtk_widget_get_style_context (button);
        gtk_style_context_add_class (context, "destructive-action");
@@ -337,11 +250,14 @@ remove_confirm_repo (GsReposDialog *dialog, GsApp *repo)
 
        gtk_window_set_modal (GTK_WINDOW (confirm_dialog), TRUE);
        gtk_window_present (GTK_WINDOW (confirm_dialog));
+
+       gs_repo_row_set_busy (row, TRUE);
 }
 
 static void
-repo_button_clicked_cb (GsRepoRow *row,
-                        GsReposDialog *dialog)
+repo_section_switch_clicked_cb (GsReposSection *section,
+                               GsRepoRow *row,
+                               GsReposDialog *dialog)
 {
        GsApp *repo;
 
@@ -350,12 +266,13 @@ repo_button_clicked_cb (GsRepoRow *row,
        switch (gs_app_get_state (repo)) {
        case GS_APP_STATE_AVAILABLE:
        case GS_APP_STATE_AVAILABLE_LOCAL:
-               enable_repo (dialog, repo);
+               enable_repo (dialog, row, repo);
                break;
        case GS_APP_STATE_INSTALLED:
-               remove_confirm_repo (dialog, repo);
+               remove_confirm_repo (dialog, row, repo);
                break;
        default:
+               gs_repo_row_refresh (row);
                g_warning ("repo %s button clicked in unexpected state %s",
                           gs_app_get_id (repo),
                           gs_app_state_to_string (gs_app_get_state (repo)));
@@ -363,8 +280,9 @@ repo_button_clicked_cb (GsRepoRow *row,
        }
 }
 
-static GtkListBox *
-get_list_box_for_repo (GsReposDialog *dialog, GsApp *repo)
+static gboolean
+is_third_party_repo (GsReposDialog *dialog,
+                    GsApp *repo)
 {
        if (dialog->third_party_repo != NULL) {
                const gchar *source_repo;
@@ -374,19 +292,20 @@ get_list_box_for_repo (GsReposDialog *dialog, GsApp *repo)
                source_third_party_package = gs_app_get_source_id_default (dialog->third_party_repo);
 
                /* group repos from the same repo-release package together */
-               if (g_strcmp0 (source_repo, source_third_party_package) == 0)
-                       return GTK_LIST_BOX (dialog->listbox_third_party);
+               return g_strcmp0 (source_repo, source_third_party_package) == 0;
        }
 
-       return GTK_LIST_BOX (dialog->listbox);
+       return FALSE;
 }
 
 static void
-add_repo (GsReposDialog *dialog, GsApp *repo)
+add_repo (GsReposDialog *dialog,
+         GsApp *repo,
+         GSList **third_party_repos)
 {
-       GtkWidget *row;
-       g_autofree gchar *text = NULL;
        GsAppState state;
+       GtkWidget *section;
+       gchar *origin_ui;
 
        state = gs_app_get_state (repo);
        if (!(state == GS_APP_STATE_AVAILABLE ||
@@ -400,21 +319,32 @@ add_repo (GsReposDialog *dialog, GsApp *repo)
                return;
        }
 
-       row = gs_repo_row_new ();
-       gs_repo_row_set_name (GS_REPO_ROW (row),
-                             gs_app_get_name (repo));
-       text = get_repo_installed_text (repo);
-       gs_repo_row_set_comment (GS_REPO_ROW (row), text);
-       gs_repo_row_set_url (GS_REPO_ROW (row),
-                            gs_app_get_url (repo, AS_URL_KIND_HOMEPAGE));
-       gs_repo_row_show_status (GS_REPO_ROW (row));
-       gs_repo_row_set_repo (GS_REPO_ROW (row), repo);
-
-       g_signal_connect (row, "button-clicked",
-                         G_CALLBACK (repo_button_clicked_cb), dialog);
-
-       gtk_list_box_prepend (get_list_box_for_repo (dialog, repo), row);
-       gtk_widget_show (row);
+       if (third_party_repos && is_third_party_repo (dialog, repo)) {
+               *third_party_repos = g_slist_prepend (*third_party_repos, repo);
+               return;
+       }
+
+       origin_ui = gs_app_get_origin_ui (repo);
+       if (!origin_ui)
+               origin_ui = gs_app_get_packaging_format (repo);
+       if (!origin_ui)
+               origin_ui = g_strdup (gs_app_get_management_plugin (repo));
+       section = g_hash_table_lookup (dialog->sections, origin_ui);
+       if (section) {
+               g_free (origin_ui);
+       } else {
+               section = gs_repos_section_new (origin_ui);
+               g_object_set (G_OBJECT (section),
+                       "halign", GTK_ALIGN_FILL,
+                       "hexpand", TRUE,
+                       NULL);
+               g_signal_connect_object (section, "switch-clicked",
+                                        G_CALLBACK (repo_section_switch_clicked_cb), dialog, 0);
+               g_hash_table_insert (dialog->sections, origin_ui, section);
+               gtk_widget_show (section);
+       }
+
+       gs_repos_section_add_repo (GS_REPOS_SECTION (section), repo);
 }
 
 static void
@@ -492,30 +422,25 @@ third_party_repo_button_clicked_cb (GsThirdPartyRepoRow *row,
        g_settings_set_boolean (dialog->settings, "show-nonfree-prompt", FALSE);
 }
 
-static void
-refresh_third_party_repo (GsReposDialog *dialog)
+static gint
+repos_dialog_compare_sections_cb (gconstpointer aa,
+                                 gconstpointer bb)
 {
-       if (dialog->third_party_repo == NULL) {
-               gtk_widget_hide (dialog->listbox_third_party);
-               return;
-       }
-
-       gtk_widget_show (dialog->listbox_third_party);
-}
+       GsReposSection *section_a = (GsReposSection *) aa;
+       GsReposSection *section_b = (GsReposSection *) bb;
+       g_autofree gchar *sort_key_a = NULL;
+       g_autofree gchar *sort_key_b = NULL;
 
-static void
-remove_all_repo_rows_cb (GtkWidget *widget, gpointer user_data)
-{
-       GtkContainer *container = GTK_CONTAINER (user_data);
+       /* Place firmware updates at the bottom */
+       if (g_strcmp0 (gs_repos_section_get_packaging_format (section_a), "fwupd") == 0)
+               return 1;
+       if (g_strcmp0 (gs_repos_section_get_packaging_format (section_b), "fwupd") == 0)
+               return -1;
 
-       if (GS_IS_REPO_ROW (widget))
-               gtk_container_remove (container, widget);
-}
+       sort_key_a = gs_utils_sort_key (gs_repos_section_get_title (section_a));
+       sort_key_b = gs_utils_sort_key (gs_repos_section_get_title (section_b));
 
-static void
-container_remove_all_repo_rows (GtkContainer *container)
-{
-       gtk_container_foreach (container, remove_all_repo_rows_cb, container);
+       return g_strcmp0 (sort_key_a, sort_key_b);
 }
 
 static void
@@ -526,6 +451,8 @@ get_sources_cb (GsPluginLoader *plugin_loader,
        GsApp *app;
        g_autoptr(GError) error = NULL;
        g_autoptr(GsAppList) list = NULL;
+       g_autoptr(GSList) other_repos = NULL;
+       g_autoptr(GList) sections = NULL;
 
        /* get the results */
        list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
@@ -545,8 +472,8 @@ get_sources_cb (GsPluginLoader *plugin_loader,
        }
 
        /* remove previous */
-       gs_container_remove_all (GTK_CONTAINER (dialog->listbox));
-       container_remove_all_repo_rows (GTK_CONTAINER (dialog->listbox_third_party));
+       g_hash_table_remove_all (dialog->sections);
+       gs_container_remove_all (GTK_CONTAINER (dialog->content_box));
 
        /* stop the spinner */
        gs_stop_spinner (GTK_SPINNER (dialog->spinner));
@@ -566,11 +493,85 @@ get_sources_cb (GsPluginLoader *plugin_loader,
        gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "sources");
        for (guint i = 0; i < gs_app_list_length (list); i++) {
                app = gs_app_list_index (list, i);
-               add_repo (dialog, app);
+               add_repo (dialog, app, &other_repos);
+       }
+
+       sections = g_hash_table_get_values (dialog->sections);
+       sections = g_list_sort (sections, repos_dialog_compare_sections_cb);
+       for (GList *link = sections; link; link = g_list_next (link)) {
+               GtkWidget *section = link->data;
+               gtk_container_add (GTK_CONTAINER (dialog->content_box), section);
        }
 
-       gtk_widget_set_visible (dialog->listbox,
-               gtk_list_box_get_row_at_index (GTK_LIST_BOX (dialog->listbox), 0) != NULL);
+       gtk_widget_set_visible (dialog->content_box, sections != NULL);
+
+       if (other_repos) {
+               GsReposSection *section;
+               GtkWidget *label;
+               GtkStyleContext *style;
+               g_autofree gchar *anchor = NULL;
+               g_autofree gchar *hint = NULL;
+
+               section = GS_REPOS_SECTION (gs_repos_section_new (_("Fedora Third Party Repositories")));
+               g_object_set (G_OBJECT (section),
+                       "halign", GTK_ALIGN_FILL,
+                       "hexpand", TRUE,
+                       NULL);
+               g_signal_connect_object (section, "switch-clicked",
+                                        G_CALLBACK (repo_section_switch_clicked_cb), dialog, 0);
+               gtk_widget_show (GTK_WIDGET (section));
+
+               anchor = g_strdup_printf ("<a href=\"%s\">%s</a>",
+                               
"https://fedoraproject.org/wiki/Workstation/Third_Party_Software_Repositories";,
+                               /* TRANSLATORS: this is the clickable
+                                * link on the third party repositories info bar */
+                               _("more information"));
+               hint = g_strdup_printf (
+                        /* TRANSLATORS: this is the third party repositories info bar. The '%s' is replaced
+                           with a link consisting a text "more information", which constructs a sentence:
+                           "Additional repositories from selected third parties - more information."*/
+                        _("Additional repositories from selected third parties - %s."),
+                        anchor);
+
+               label = gtk_label_new ("");
+               g_object_set (G_OBJECT (label),
+                       "halign", GTK_ALIGN_START,
+                       "hexpand", TRUE,
+                       "label", hint,
+                       "use-markup", TRUE,
+                       "visible", TRUE,
+                       "wrap", TRUE,
+                       "xalign", 0.0,
+                       NULL);
+               style = gtk_widget_get_style_context (label);
+               gtk_style_context_add_class (style, "dim-label");
+
+               gtk_box_pack_start (GTK_BOX (section), label, FALSE, TRUE, 0);
+               gtk_box_reorder_child (GTK_BOX (section), label, 1);
+
+               for (GSList *link = other_repos; link; link = g_slist_next (link)) {
+                       GsApp *repo = link->data;
+                       gs_repos_section_add_repo (section, repo);
+               }
+
+               gtk_container_add (GTK_CONTAINER (dialog->content_box), GTK_WIDGET (section));
+       }
+}
+
+static void
+reload_sources (GsReposDialog *dialog)
+{
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+
+       /* get the list of non-core software repositories */
+       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES,
+                                        "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED |
+                                                        GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME,
+                                        NULL);
+       gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job,
+                                           dialog->cancellable,
+                                           (GAsyncReadyCallback) get_sources_cb,
+                                           dialog);
 }
 
 static void
@@ -592,6 +593,7 @@ resolve_third_party_repo_cb (GsPluginLoader *plugin_loader,
                        return;
                } else {
                        g_warning ("failed to resolve third party repo: %s", error->message);
+                       reload_sources (dialog);
                        return;
                }
        }
@@ -603,26 +605,7 @@ resolve_third_party_repo_cb (GsPluginLoader *plugin_loader,
                app = NULL;
 
        g_set_object (&dialog->third_party_repo, app);
-       gs_third_party_repo_row_set_app (GS_THIRD_PARTY_REPO_ROW (dialog->row_third_party), app);
-
-       /* refresh widget */
-       refresh_third_party_repo (dialog);
-}
-
-static void
-reload_sources (GsReposDialog *dialog)
-{
-       g_autoptr(GsPluginJob) plugin_job = NULL;
-
-       /* get the list of non-core software repositories */
-       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES,
-                                        "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED |
-                                                        GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME,
-                                        NULL);
-       gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job,
-                                           dialog->cancellable,
-                                           (GAsyncReadyCallback) get_sources_cb,
-                                           dialog);
+       reload_sources (dialog);
 }
 
 static gboolean
@@ -649,8 +632,10 @@ reload_third_party_repo (GsReposDialog *dialog)
        g_autoptr(GsPluginJob) plugin_job = NULL;
 
        /* Fedora-specific functionality */
-       if (!is_fedora ())
+       if (!is_fedora ()) {
+               reload_sources (dialog);
                return;
+       }
 
        plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH_PROVIDES,
                                         "search", third_party_repo_package,
@@ -663,64 +648,6 @@ reload_third_party_repo (GsReposDialog *dialog)
                                            dialog);
 }
 
-static gchar *
-get_row_sort_key (GtkListBoxRow *row)
-{
-       GsApp *app;
-       guint sort_order;
-       g_autofree gchar *sort_key = NULL;
-
-       /* sort third party repo rows first */
-       if (GS_IS_THIRD_PARTY_REPO_ROW (row)) {
-               sort_order = 1;
-               app = gs_third_party_repo_row_get_app (GS_THIRD_PARTY_REPO_ROW (row));
-       } else {
-               sort_order = 2;
-               app = gs_repo_row_get_repo (GS_REPO_ROW (row));
-       }
-
-       if (gs_app_get_name (app) != NULL) {
-               sort_key = gs_utils_sort_key (gs_app_get_name (app));
-               return g_strdup_printf ("%u:%s", sort_order, sort_key);
-       } else {
-               return g_strdup_printf ("%u:", sort_order);
-       }
-}
-
-static gint
-list_sort_func (GtkListBoxRow *a,
-               GtkListBoxRow *b,
-               gpointer user_data)
-{
-       g_autofree gchar *key1 = get_row_sort_key (a);
-       g_autofree gchar *key2 = get_row_sort_key (b);
-
-       /* compare the keys according to the algorithm above */
-       return g_strcmp0 (key1, key2);
-}
-
-static void
-list_row_activated_cb (GtkListBox *list_box,
-                      GtkListBoxRow *row,
-                      GsReposDialog *dialog)
-{
-       GtkListBoxRow *other_row;
-
-       if (!GS_IS_REPO_ROW (row))
-               return;
-
-       gs_repo_row_show_details (GS_REPO_ROW (row));
-
-       for (guint i = 0; (other_row = gtk_list_box_get_row_at_index (list_box, i)) != NULL; i++) {
-               if (!GS_IS_REPO_ROW (other_row))
-                       continue;
-               if (other_row == row)
-                       continue;
-
-               gs_repo_row_hide_details (GS_REPO_ROW (other_row));
-       }
-}
-
 static gchar *
 get_os_name (void)
 {
@@ -742,7 +669,6 @@ get_os_name (void)
 static void
 reload_cb (GsPluginLoader *plugin_loader, GsReposDialog *dialog)
 {
-       reload_sources (dialog);
        reload_third_party_repo (dialog);
 }
 
@@ -765,6 +691,7 @@ gs_repos_dialog_dispose (GObject *object)
        }
 
        g_cancellable_cancel (dialog->cancellable);
+       g_clear_pointer (&dialog->sections, g_hash_table_unref);
        g_clear_object (&dialog->cancellable);
        g_clear_object (&dialog->settings);
        g_clear_object (&dialog->third_party_repo);
@@ -775,7 +702,6 @@ gs_repos_dialog_dispose (GObject *object)
 static void
 gs_repos_dialog_init (GsReposDialog *dialog)
 {
-       g_autofree gchar *label_description_text = NULL;
        g_autofree gchar *label_empty_text = NULL;
        g_autofree gchar *os_name = NULL;
        g_autoptr(GString) str = g_string_new (NULL);
@@ -784,47 +710,10 @@ gs_repos_dialog_init (GsReposDialog *dialog)
 
        dialog->cancellable = g_cancellable_new ();
        dialog->settings = g_settings_new ("org.gnome.software");
+       dialog->sections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
 
        os_name = get_os_name ();
 
-       gtk_list_box_set_sort_func (GTK_LIST_BOX (dialog->listbox),
-                                   list_sort_func,
-                                   dialog, NULL);
-       g_signal_connect (dialog->listbox, "row-activated",
-                         G_CALLBACK (list_row_activated_cb), dialog);
-
-       /* TRANSLATORS: This is the description text displayed in the Software Repositories dialog.
-          %s gets replaced by the name of the actual distro, e.g. Fedora. */
-       label_description_text = g_strdup_printf (_("These repositories supplement the default software 
provided by %s."),
-                                                 os_name);
-       gtk_label_set_text (GTK_LABEL (dialog->label_description), label_description_text);
-
-       /* set up third party repository row */
-       gtk_list_box_set_sort_func (GTK_LIST_BOX (dialog->listbox_third_party),
-                                   list_sort_func,
-                                   dialog, NULL);
-       g_signal_connect (dialog->listbox_third_party, "row-activated",
-                         G_CALLBACK (list_row_activated_cb), dialog);
-       g_signal_connect (dialog->row_third_party, "button-clicked",
-                         G_CALLBACK (third_party_repo_button_clicked_cb), dialog);
-       gs_third_party_repo_row_set_name (GS_THIRD_PARTY_REPO_ROW (dialog->row_third_party),
-                                         /* TRANSLATORS: info bar title in the software repositories dialog 
*/
-                                         _("Third Party Repositories"));
-       g_string_append (str,
-                        /* TRANSLATORS: this is the third party repositories info bar. */
-                        _("Access additional software from selected third party sources."));
-       g_string_append (str, " ");
-       g_string_append (str,
-                        /* TRANSLATORS: this is the third party repositories info bar. */
-                        _("Some of this software is proprietary and therefore has restrictions on use, 
sharing, and access to source code."));
-       g_string_append_printf (str, " <a href=\"%s\">%s</a>",
-                               
"https://fedoraproject.org/wiki/Workstation/Third_Party_Software_Repositories";,
-                               /* TRANSLATORS: this is the clickable
-                                * link on the third party repositories info bar */
-                               _("Find out more…"));
-       gs_third_party_repo_row_set_comment (GS_THIRD_PARTY_REPO_ROW (dialog->row_third_party), str->str);
-       refresh_third_party_repo (dialog);
-
        /* TRANSLATORS: This is the description text displayed in the Software Repositories dialog.
           %s gets replaced by the name of the actual distro, e.g. Fedora. */
        label_empty_text = g_strdup_printf (_("These repositories supplement the default software provided by 
%s."),
@@ -842,12 +731,9 @@ gs_repos_dialog_class_init (GsReposDialogClass *klass)
 
        gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-repos-dialog.ui");
 
-       gtk_widget_class_bind_template_child (widget_class, GsReposDialog, label_description);
        gtk_widget_class_bind_template_child (widget_class, GsReposDialog, label_empty);
        gtk_widget_class_bind_template_child (widget_class, GsReposDialog, label_header);
-       gtk_widget_class_bind_template_child (widget_class, GsReposDialog, listbox);
-       gtk_widget_class_bind_template_child (widget_class, GsReposDialog, listbox_third_party);
-       gtk_widget_class_bind_template_child (widget_class, GsReposDialog, row_third_party);
+       gtk_widget_class_bind_template_child (widget_class, GsReposDialog, content_box);
        gtk_widget_class_bind_template_child (widget_class, GsReposDialog, spinner);
        gtk_widget_class_bind_template_child (widget_class, GsReposDialog, stack);
 }
@@ -865,7 +751,6 @@ gs_repos_dialog_new (GtkWindow *parent, GsPluginLoader *plugin_loader)
        set_plugin_loader (dialog, plugin_loader);
        gtk_stack_set_visible_child_name (GTK_STACK (dialog->stack), "waiting");
        gs_start_spinner (GTK_SPINNER (dialog->spinner));
-       reload_sources (dialog);
        reload_third_party_repo (dialog);
 
        return GTK_WIDGET (dialog);
diff --git a/src/gs-repos-dialog.ui b/src/gs-repos-dialog.ui
index 246fa477c..0f0e1a37f 100644
--- a/src/gs-repos-dialog.ui
+++ b/src/gs-repos-dialog.ui
@@ -99,57 +99,14 @@
                 <property name="vscrollbar_policy">automatic</property>
                 <property name="shadow_type">none</property>
                 <child>
-                  <object class="GtkBox" id="box1">
+                  <object class="GtkBox" id="content_box">
                     <property name="visible">True</property>
                     <property name="margin_start">52</property>
                     <property name="margin_end">52</property>
                     <property name="margin_top">24</property>
                     <property name="margin_bottom">40</property>
                     <property name="orientation">vertical</property>
-                    <property name="spacing">4</property>
-                    <child>
-                      <object class="GtkLabel" id="label_description">
-                        <property name="visible">True</property>
-                        <property name="xalign">0</property>
-                        <property name="wrap">True</property>
-                        <property name="max_width_chars">30</property>
-                        <property name="margin_bottom">16</property>
-                        <style>
-                          <class name="dim-label"/>
-                        </style>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkListBox" id="listbox_third_party">
-                        <property name="visible">True</property>
-                        <property name="selection_mode">none</property>
-                        <property name="halign">fill</property>
-                        <property name="valign">start</property>
-                        <property name="margin_bottom">16</property>
-                        <style>
-                          <class name="content" />
-                        </style>
-                        <child>
-                          <object class="GsThirdPartyRepoRow" id="row_third_party">
-                            <property name="visible">True</property>
-                            <property name="activatable">False</property>
-                          </object>
-                        </child>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkListBox" id="listbox">
-                        <property name="halign">fill</property>
-                        <property name="valign">start</property>
-                        <property name="vexpand">True</property>
-                        <property name="margin_top">9</property>
-                        <property name="visible">True</property>
-                        <property name="selection_mode">none</property>
-                        <style>
-                          <class name="content" />
-                        </style>
-                      </object>
-                    </child>
+                    <property name="spacing">20</property>
                   </object>
                 </child>
               </object>
diff --git a/src/gs-repos-section.c b/src/gs-repos-section.c
new file mode 100644
index 000000000..685a7dc4a
--- /dev/null
+++ b/src/gs-repos-section.c
@@ -0,0 +1,176 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Red Hat <www.redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+
+#include "gs-repo-row.h"
+#include "gs-repos-section.h"
+
+struct _GsReposSection
+{
+       GtkBox           parent_instance;
+       GtkWidget       *title;
+       GtkListBox      *list;
+       gchar           *packaging_format;
+};
+
+G_DEFINE_TYPE (GsReposSection, gs_repos_section, GTK_TYPE_BOX)
+
+enum {
+       SIGNAL_SWITCH_CLICKED,
+       SIGNAL_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+static void
+repo_switch_clicked_cb (GsRepoRow *row,
+                       GsReposSection *section)
+{
+       g_signal_emit (section, signals[SIGNAL_SWITCH_CLICKED], 0, row);
+}
+
+static gchar *
+_get_app_sort_key (GsApp *app)
+{
+       if (gs_app_get_name (app) != NULL)
+               return gs_utils_sort_key (gs_app_get_name (app));
+
+       return NULL;
+}
+
+static gint
+_list_sort_func (GtkListBoxRow *a, GtkListBoxRow *b, gpointer user_data)
+{
+       GsApp *a1 = gs_repo_row_get_repo (GS_REPO_ROW (a));
+       GsApp *a2 = gs_repo_row_get_repo (GS_REPO_ROW (b));
+       g_autofree gchar *key1 = _get_app_sort_key (a1);
+       g_autofree gchar *key2 = _get_app_sort_key (a2);
+
+       return g_strcmp0 (key1, key2);
+}
+
+static void
+gs_repos_section_finalize (GObject *object)
+{
+       GsReposSection *self = GS_REPOS_SECTION (object);
+
+       g_free (self->packaging_format);
+
+       G_OBJECT_CLASS (gs_repos_section_parent_class)->finalize (object);
+}
+
+static void
+gs_repos_section_class_init (GsReposSectionClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+       object_class->finalize = gs_repos_section_finalize;
+
+       signals [SIGNAL_SWITCH_CLICKED] =
+               g_signal_new ("switch-clicked",
+                             G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+                             0,
+                             NULL, NULL, g_cclosure_marshal_VOID__OBJECT,
+                             G_TYPE_NONE, 1, GS_TYPE_REPO_ROW);
+
+       gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (klass), "repos-section");
+}
+
+static void
+gs_repos_section_init (GsReposSection *self)
+{
+       PangoAttrList *attrs = NULL;
+
+       attrs = pango_attr_list_new ();
+       pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+
+       self->title = gtk_label_new ("");
+       g_object_set (G_OBJECT (self->title),
+               "visible", TRUE,
+               "halign", GTK_ALIGN_START,
+               "valign", GTK_ALIGN_CENTER,
+               "xalign", 0.0,
+               "yalign", 0.5,
+               "attributes", attrs,
+               NULL);
+
+       pango_attr_list_unref (attrs);
+
+       gtk_box_pack_start (GTK_BOX (self), self->title, FALSE, FALSE, 0);
+
+       self->list = GTK_LIST_BOX (gtk_list_box_new ());
+       g_object_set (G_OBJECT (self->list),
+               "visible", TRUE,
+               "halign", GTK_ALIGN_FILL,
+               "hexpand", TRUE,
+               "valign", GTK_ALIGN_CENTER,
+               "vexpand", TRUE,
+               "margin-top", 4,
+               "margin-bottom", 4,
+               "selection-mode", GTK_SELECTION_NONE,
+               NULL);
+       gtk_list_box_set_sort_func (self->list, _list_sort_func, self, NULL);
+
+       gtk_box_pack_start (GTK_BOX (self), GTK_WIDGET (self->list), TRUE, TRUE, 0);
+}
+
+GtkWidget *
+gs_repos_section_new (const gchar *title)
+{
+       GsReposSection *self;
+
+       self = g_object_new (GS_TYPE_REPOS_SECTION,
+               "orientation", GTK_ORIENTATION_VERTICAL,
+               "spacing", 4,
+               NULL);
+
+       gtk_label_set_text (GTK_LABEL (self->title), title);
+
+       return GTK_WIDGET (self);
+}
+
+void
+gs_repos_section_add_repo (GsReposSection *self,
+                          GsApp *repo)
+{
+       GtkWidget *row;
+
+       g_return_if_fail (GS_IS_REPOS_SECTION (self));
+       g_return_if_fail (GS_IS_APP (repo));
+
+       if (!self->packaging_format)
+               self->packaging_format = gs_app_get_packaging_format (repo);
+
+       row = gs_repo_row_new ();
+       gs_repo_row_set_repo (GS_REPO_ROW (row), repo);
+
+       g_signal_connect (row, "switch-clicked",
+                         G_CALLBACK (repo_switch_clicked_cb), self);
+
+       gtk_list_box_prepend (self->list, row);
+       gtk_widget_show (row);
+}
+
+const gchar *
+gs_repos_section_get_title (GsReposSection *self)
+{
+       g_return_val_if_fail (GS_IS_REPOS_SECTION (self), NULL);
+
+       return gtk_label_get_text (GTK_LABEL (self->title));
+}
+
+const gchar *
+gs_repos_section_get_packaging_format (GsReposSection *self)
+{
+       g_return_val_if_fail (GS_IS_REPOS_SECTION (self), NULL);
+
+       return self->packaging_format;
+}
diff --git a/src/gs-repos-section.h b/src/gs-repos-section.h
new file mode 100644
index 000000000..2679017cd
--- /dev/null
+++ b/src/gs-repos-section.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Red Hat <www.redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "gs-app.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_REPOS_SECTION (gs_repos_section_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsReposSection, gs_repos_section, GS, REPOS_SECTION, GtkBox)
+
+GtkWidget      *gs_repos_section_new                   (const gchar            *title);
+void            gs_repos_section_add_repo              (GsReposSection         *self,
+                                                        GsApp                  *repo);
+const gchar    *gs_repos_section_get_title             (GsReposSection         *self);
+const gchar    *gs_repos_section_get_packaging_format  (GsReposSection         *self);
+
+G_END_DECLS
diff --git a/src/gtk-style.css b/src/gtk-style.css
index 641bac64b..16f25d0c1 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -41,6 +41,17 @@
        font-size: x-small;
 }
 
+repos-section list {
+       border-radius: 6px;
+       border: 1px solid darker(@theme_bg_color);
+       padding-bottom: 4px;
+       padding-top: 4px;
+}
+
+repos-section list .reporow:not(:last-child) {
+       border-bottom: 1px solid @theme_unfocused_bg_color;
+}
+
 /* Adapted from Adwaita’s .needs-attention class for stacksidebar */
 sidebar row.needs-attention > box > label {
        animation: needs_attention 150ms ease-in;
@@ -446,6 +457,7 @@ summary-tile {
 .app-updates-section {
        border-radius: 4px;
        border: 1px solid darker(@theme_bg_color);
+       -gtk-outline-radius: 50%;
 }
 
 .app-listbox-header-title {
diff --git a/src/meson.build b/src/meson.build
index 9fcacf268..060925f70 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -50,6 +50,7 @@ gnome_software_sources = [
   'gs-progress-button.c',
   'gs-removal-dialog.c',
   'gs-repos-dialog.c',
+  'gs-repos-section.c',
   'gs-repo-row.c',
   'gs-review-bar.c',
   'gs-review-dialog.c',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]