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




commit 1653d5cd3054d448e477c904e9a67573602e8530
Author: Milan Crha <mcrha redhat com>
Date:   Wed Jul 14 10:05:14 2021 +0200

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

 plugins/flatpak/gs-flatpak-utils.c        |  22 +-
 plugins/flatpak/gs-flatpak-utils.h        |   4 +-
 plugins/flatpak/gs-flatpak.c              |   2 +-
 plugins/fwupd/gs-plugin-fwupd.c           |   3 +
 plugins/packagekit/gs-plugin-packagekit.c |   4 +
 plugins/rpm-ostree/gs-plugin-rpm-ostree.c |   4 +
 po/POTFILES.in                            |   3 +
 src/gs-repo-row.c                         | 449 +++++++++++++++--------
 src/gs-repo-row.h                         |  19 +-
 src/gs-repo-row.ui                        | 188 +++++-----
 src/gs-repos-dialog.c                     | 570 +++++++++++-------------------
 src/gs-repos-dialog.ui                    |  49 +--
 src/gs-repos-section.c                    | 226 ++++++++++++
 src/gs-repos-section.h                    |  31 ++
 src/gtk-style.css                         |  28 ++
 src/meson.build                           |   1 +
 16 files changed, 915 insertions(+), 688 deletions(-)
---
diff --git a/plugins/flatpak/gs-flatpak-utils.c b/plugins/flatpak/gs-flatpak-utils.c
index 3882583c8..05ee126cd 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,9 @@ gs_flatpak_error_convert (GError **perror)
 }
 
 GsApp *
-gs_flatpak_app_new_from_remote (FlatpakRemote *xremote)
+gs_flatpak_app_new_from_remote (GsPlugin *plugin,
+                               FlatpakRemote *xremote,
+                               gboolean is_user)
 {
        g_autofree gchar *title = NULL;
        g_autofree gchar *url = NULL;
@@ -77,13 +81,23 @@ 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_management_plugin (app, gs_plugin_get_name (plugin));
+       gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_FLATPAK);
+       gs_app_set_scope (app, is_user ? AS_COMPONENT_SCOPE_USER : AS_COMPONENT_SCOPE_SYSTEM);
+
+       gs_app_set_metadata (app, "GnomeSoftware::SortKey", "100");
+       gs_app_set_metadata (app, "GnomeSoftware::InstallationKind",
+               is_user ? _("User Installation") : _("System Installation"));
+       if (!is_user)
+               gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE);
 
        /* 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 the repo dialogue section name,
+        * 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..1f1fa3689 100644
--- a/plugins/flatpak/gs-flatpak-utils.h
+++ b/plugins/flatpak/gs-flatpak-utils.h
@@ -13,7 +13,9 @@ 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         (GsPlugin       *plugin,
+                                                        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 0e4e2eff5..f3e77f041 100644
--- a/plugins/flatpak/gs-flatpak.c
+++ b/plugins/flatpak/gs-flatpak.c
@@ -379,7 +379,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 (self->plugin, xremote, flatpak_installation_get_is_user 
(self->installation));
        gs_flatpak_claim_app (self, app);
 
        /* we already have one, returned the ref'd cached copy */
diff --git a/plugins/fwupd/gs-plugin-fwupd.c b/plugins/fwupd/gs-plugin-fwupd.c
index af3897329..497c9dc2c 100644
--- a/plugins/fwupd/gs-plugin-fwupd.c
+++ b/plugins/fwupd/gs-plugin-fwupd.c
@@ -1175,6 +1175,9 @@ 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_metadata (app, "GnomeSoftware::SortKey", "800");
+               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 068bb306c..785d03066 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>
@@ -294,6 +295,9 @@ 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_metadata (app, "GnomeSoftware::SortKey", "300");
+               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 ac26448b7..84eadc9ca 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>
@@ -2146,6 +2147,9 @@ 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_metadata (app, "GnomeSoftware::SortKey", "200");
+               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 c8b5b2a1f..e037f4abb 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -92,9 +92,12 @@ 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
 plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in
+plugins/packagekit/gs-plugin-packagekit.c
+plugins/rpm-ostree/gs-plugin-rpm-ostree.c
 plugins/snap/gs-plugin-snap.c
 plugins/snap/org.gnome.Software.Plugin.Snap.metainfo.xml.in
diff --git a/src/gs-repo-row.c b/src/gs-repo-row.c
index e9529e9e3..e7dc023b0 100644
--- a/src/gs-repo-row.c
+++ b/src/gs-repo-row.c
@@ -8,153 +8,94 @@
 
 #include "config.h"
 
-#include "gs-repo-row.h"
-
 #include <glib/gi18n.h>
+#include <libsoup/soup.h>
+
+#include "gs-repo-row.h"
 
 typedef struct
 {
+       GsPluginLoader  *plugin_loader; /* owned */
        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       *remove_button;
+       GtkWidget       *disable_switch;
+       gulong           switch_handler_id;
        guint            refresh_idle_id;
+       guint            busy_counter;
+       gboolean         supports_remove;
+       gboolean         supports_enable_disable;
 } GsRepoRowPrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE (GsRepoRow, gs_repo_row, GTK_TYPE_LIST_BOX_ROW)
 
 enum {
-       SIGNAL_BUTTON_CLICKED,
+       SIGNAL_REMOVE_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 state_sensitive = FALSE;
+       gboolean busy = priv->busy_counter> 0;
+       gboolean is_system_repo;
 
        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;
+               state_sensitive = TRUE;
                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;
+               busy = TRUE;
                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;
+               busy = TRUE;
                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:
+               state_sensitive = TRUE;
                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;
-       }
+       is_system_repo = gs_app_has_quirk (priv->repo, GS_APP_QUIRK_PROVENANCE);
+
+       /* Disable for the system repos, if installed */
+       gtk_widget_set_sensitive (priv->disable_switch, priv->supports_enable_disable && (state_sensitive || 
!is_system_repo));
+       gtk_widget_set_visible (priv->remove_button, priv->supports_remove && !is_system_repo);
+
+       /* Set only the 'state' to visually indicate the state is not saved yet */
+       if (busy)
+               gtk_switch_set_state (GTK_SWITCH (priv->disable_switch), active);
+       else
+               gtk_switch_set_active (GTK_SWITCH (priv->disable_switch), active);
+
+       g_signal_handler_unblock (priv->disable_switch, priv->switch_handler_id);
 }
 
 static gboolean
@@ -180,66 +121,163 @@ repo_state_changed_cb (GsApp *repo, GParamSpec *pspec, GsRepoRow *row)
        priv->refresh_idle_id = g_idle_add (refresh_idle, g_object_ref (row));
 }
 
-void
-gs_repo_row_set_repo (GsRepoRow *row, GsApp *repo)
+static gchar *
+get_repo_installed_text (GsApp *repo)
 {
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+       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);
+}
+
+static void
+gs_repo_row_set_repo (GsRepoRow *self, GsApp *repo)
+{
+       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self);
+       GsPlugin *plugin;
+       g_autofree gchar *comment = NULL;
+       const gchar *tmp;
 
        g_assert (priv->repo == NULL);
 
        priv->repo = g_object_ref (repo);
        g_signal_connect_object (priv->repo, "notify::state",
                                 G_CALLBACK (repo_state_changed_cb),
-                                row, 0);
-       refresh_ui (row);
+                                self, 0);
+
+       plugin = gs_plugin_loader_find_plugin (priv->plugin_loader, gs_app_get_management_plugin (repo));
+       priv->supports_remove = plugin != NULL && gs_plugin_get_action_supported (plugin, 
GS_PLUGIN_ACTION_REMOVE_REPO);
+       priv->supports_enable_disable = plugin != NULL &&
+               gs_plugin_get_action_supported (plugin, GS_PLUGIN_ACTION_ENABLE_REPO) &&
+               gs_plugin_get_action_supported (plugin, GS_PLUGIN_ACTION_DISABLE_REPO);
+
+       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(SoupURI) uri = NULL;
+
+               uri = soup_uri_new (tmp);
+               if (uri && soup_uri_get_host (uri) != NULL && *soup_uri_get_host (uri) != '\0') {
+                       gtk_label_set_label (GTK_LABEL (priv->hostname_label), soup_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 (self);
 }
 
 GsApp *
-gs_repo_row_get_repo (GsRepoRow *row)
+gs_repo_row_get_repo (GsRepoRow *self)
 {
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self);
+       g_return_val_if_fail (GS_IS_REPO_ROW (self), NULL);
        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);
-
-       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));
+       gs_repo_row_emit_switch_clicked (row);
 }
 
-void
-gs_repo_row_hide_details (GsRepoRow *row)
+static void
+gs_repo_row_remove_button_clicked_cb (GtkWidget *button,
+                                     GsRepoRow *row)
 {
        GsRepoRowPrivate *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);
-}
+       g_return_if_fail (GS_IS_REPO_ROW (row));
 
-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 == NULL || priv->busy_counter)
+               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_REMOVE_CLICKED], 0);
 }
 
 static void
 gs_repo_row_destroy (GtkWidget *object)
 {
-       GsRepoRow *row = GS_REPO_ROW (object);
-       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+       GsRepoRow *self = GS_REPO_ROW (object);
+       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self);
 
        if (priv->repo != NULL) {
-               g_signal_handlers_disconnect_by_func (priv->repo, repo_state_changed_cb, row);
+               g_signal_handlers_disconnect_by_func (priv->repo, repo_state_changed_cb, self);
                g_clear_object (&priv->repo);
        }
 
@@ -248,17 +286,24 @@ gs_repo_row_destroy (GtkWidget *object)
                priv->refresh_idle_id = 0;
        }
 
+       g_clear_object (&priv->plugin_loader);
+
        GTK_WIDGET_CLASS (gs_repo_row_parent_class)->destroy (object);
 }
 
 static void
-gs_repo_row_init (GsRepoRow *row)
+gs_repo_row_init (GsRepoRow *self)
 {
-       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);
+       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self);
+       GtkWidget *image;
+
+       gtk_widget_init_template (GTK_WIDGET (self));
+       priv->switch_handler_id = g_signal_connect (priv->disable_switch, "notify::active",
+                                                   G_CALLBACK (disable_switch_clicked_cb), self);
+       image = gtk_image_new_from_icon_name ("user-trash-symbolic", GTK_ICON_SIZE_BUTTON);
+       gtk_button_set_image (GTK_BUTTON (priv->remove_button), image);
+       g_signal_connect (priv->remove_button, "clicked",
+               G_CALLBACK (gs_repo_row_remove_button_clicked_cb), self);
 }
 
 static void
@@ -269,26 +314,130 @@ 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_REMOVE_CLICKED] =
+               g_signal_new ("remove-clicked",
+                             G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+                             G_STRUCT_OFFSET (GsRepoRowClass, remove_clicked),
+                             NULL, NULL, g_cclosure_marshal_VOID__VOID,
+                             G_TYPE_NONE, 0, G_TYPE_NONE);
+
+       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_css_name (widget_class, "reporow");
 
        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, remove_button);
+       gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, disable_switch);
 }
 
 GtkWidget *
-gs_repo_row_new (void)
+gs_repo_row_new (GsPluginLoader        *plugin_loader,
+                GsApp *repo)
+{
+       GsRepoRow *row = g_object_new (GS_TYPE_REPO_ROW, NULL);
+       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+       priv->plugin_loader = g_object_ref (plugin_loader);
+       gs_repo_row_set_repo (row, repo);
+       return GTK_WIDGET (row);
+}
+
+static void
+gs_repo_row_change_busy (GsRepoRow *self,
+                        gboolean value)
+{
+       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self);
+
+       g_return_if_fail (GS_IS_REPO_ROW (self));
+
+       if (value)
+               g_return_if_fail (priv->busy_counter + 1 > priv->busy_counter);
+       else
+               g_return_if_fail (priv->busy_counter > 0);
+
+       priv->busy_counter += (value ? 1 : -1);
+
+       if (value && priv->busy_counter == 1)
+               gtk_widget_set_sensitive (priv->disable_switch, FALSE);
+       else if (!value && !priv->busy_counter)
+               refresh_ui (self);
+}
+
+/**
+ * gs_repo_row_mark_busy:
+ * @row: a #GsRepoRow
+ *
+ * Mark the @row as busy, that is the @row has pending operation(s).
+ * Unmark the @row as busy with gs_repo_row_unmark_busy() once
+ * the operation is done. This can be called mutliple times, only call
+ * the gs_repo_row_unmark_busy() as many times as this function had
+ * been called.
+ *
+ * Since: 41
+ **/
+void
+gs_repo_row_mark_busy (GsRepoRow *row)
+{
+       gs_repo_row_change_busy (row, TRUE);
+}
+
+/**
+ * gs_repo_row_unmark_busy:
+ * @row: a #GsRepoRow
+ *
+ * A pair function for gs_repo_row_mark_busy().
+ *
+ * Since: 41
+ **/
+void
+gs_repo_row_unmark_busy (GsRepoRow *row)
+{
+       gs_repo_row_change_busy (row, FALSE);
+}
+
+/**
+ * gs_repo_row_get_is_busy:
+ * @row: a #GsRepoRow
+ *
+ * Returns: %TRUE, when there is any pending operation for the @row
+ *
+ * Since: 41
+ **/
+gboolean
+gs_repo_row_get_is_busy (GsRepoRow *row)
+{
+       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row);
+
+       g_return_val_if_fail (GS_IS_REPO_ROW (row), FALSE);
+
+       return priv->busy_counter > 0;
+}
+
+/**
+ * gs_repo_row_emit_switch_clicked:
+ * @self: a #GsRepoRow
+ *
+ * Emits the GsRepoRow:switch-clicked signal, if applicable.
+ *
+ * Since: 41
+ **/
+void
+gs_repo_row_emit_switch_clicked (GsRepoRow *self)
 {
-       return g_object_new (GS_TYPE_REPO_ROW, NULL);
+       GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self);
+
+       g_return_if_fail (GS_IS_REPO_ROW (self));
+
+       if (priv->repo == NULL || priv->busy_counter > 0 || !gtk_widget_get_visible (priv->disable_switch))
+               return;
+
+       g_signal_emit (self, signals[SIGNAL_SWITCH_CLICKED], 0);
 }
diff --git a/src/gs-repo-row.h b/src/gs-repo-row.h
index ff2564ea5..e6f24bc80 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                    (*remove_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,
+GtkWidget      *gs_repo_row_new                        (GsPluginLoader *plugin_loader,
                                                         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_mark_busy                  (GsRepoRow      *row);
+void            gs_repo_row_unmark_busy                (GsRepoRow      *row);
+gboolean        gs_repo_row_get_is_busy                (GsRepoRow      *row);
+void            gs_repo_row_emit_switch_clicked        (GsRepoRow      *self);
 
 G_END_DECLS
diff --git a/src/gs-repo-row.ui b/src/gs-repo-row.ui
index 2cbe02532..86deee91c 100644
--- a/src/gs-repo-row.ui
+++ b/src/gs-repo-row.ui
@@ -2,118 +2,94 @@
 <interface>
   <!-- interface-requires gtk+ 3.10 -->
   <template class="GsRepoRow" parent="GtkListBoxRow">
+    <property name="visible">True</property>
     <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">16</property>
+        <property name="row-spacing">6</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>
+            <property name="ellipsize">end</property>
           </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>
+            <property name="ellipsize">end</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>
+            <property name="ellipsize">end</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="GtkButton" id="remove_button">
+            <property name="visible">False</property>
+            <property name="can_focus">True</property>
+            <property name="halign">end</property>
+            <property name="valign">center</property>
+            <property name="hexpand">False</property>
+            <property name="always-show-image">True</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>
+        <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">2</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 d5a4547ee..2a74bfb29 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;
 };
@@ -43,6 +42,7 @@ G_DEFINE_TYPE (GsReposDialog, gs_repos_dialog, GTK_TYPE_DIALOG)
 typedef struct {
        GsReposDialog   *dialog;
        GsApp           *repo;
+       GWeakRef         row_weakref;
        GsPluginAction   action;
 } InstallRemoveData;
 
@@ -51,97 +51,12 @@ install_remove_data_free (InstallRemoveData *data)
 {
        g_clear_object (&data->dialog);
        g_clear_object (&data->repo);
+       g_weak_ref_clear (&data->row_weakref);
        g_slice_free (InstallRemoveData, data);
 }
 
 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,
@@ -150,9 +65,13 @@ repo_enabled_cb (GObject *source,
        GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
        g_autoptr(InstallRemoveData) install_remove_data = (InstallRemoveData *) user_data;
        g_autoptr(GError) error = NULL;
+       g_autoptr(GsRepoRow) row = NULL;
        const gchar *action_str;
 
        action_str = gs_plugin_action_to_string (install_remove_data->action);
+       row = g_weak_ref_get (&install_remove_data->row_weakref);
+       if (row)
+               gs_repo_row_unmark_busy (row);
 
        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,30 @@ 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) {
+               g_autoptr(GsRepoRow) row = g_weak_ref_get (&install_data->row_weakref);
+               if (row)
+                       gs_repo_row_unmark_busy (row);
                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_ENABLE_REPO;
-       install_data->repo = g_object_ref (repo);
        install_data->dialog = g_object_ref (dialog);
+       install_data->repo = g_object_ref (repo);
+       g_weak_ref_init (&install_data->row_weakref, row);
+
+       gs_repo_row_mark_busy (row);
 
        /* user needs to confirm acceptance of an agreement */
        if (gs_app_get_agreement (repo) != NULL) {
@@ -266,8 +194,12 @@ 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) {
+               g_autoptr(GsRepoRow) row = g_weak_ref_get (&remove_data->row_weakref);
+               if (row)
+                       gs_repo_row_unmark_busy (row);
                return;
+       }
 
        g_debug ("removing repo %s", gs_app_get_id (remove_data->repo));
        plugin_job = gs_plugin_job_newv (remove_data->action,
@@ -281,54 +213,44 @@ 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,
+                    GsPluginAction action)
 {
        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->repo = g_object_ref (repo);
+       remove_data->action = action;
        remove_data->dialog = g_object_ref (dialog);
+       remove_data->repo = g_object_ref (repo);
+       g_weak_ref_init (&remove_data->row_weakref, row);
 
-       if (repo_supports_removal (repo)) {
-               remove_data->action = GS_PLUGIN_ACTION_REMOVE_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 {
-               remove_data->action = GS_PLUGIN_ACTION_DISABLE_REPO;
-               /* 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",
+                                                action == GS_PLUGIN_ACTION_DISABLE_REPO ? _("Disable 
Repository?") : _("Remove 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);
+       if (action == GS_PLUGIN_ACTION_DISABLE_REPO) {
+               /* TRANSLATORS: this is button text to disable a repo */
+               button = gtk_dialog_add_button (GTK_DIALOG (confirm_dialog), _("_Disable"), 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 remove a repo */
+               button = gtk_dialog_add_button (GTK_DIALOG (confirm_dialog), _("_Remove"), GTK_RESPONSE_OK);
        }
-
        context = gtk_widget_get_style_context (button);
        gtk_style_context_add_class (context, "destructive-action");
 
@@ -338,11 +260,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_mark_busy (row);
 }
 
 static void
-repo_button_clicked_cb (GsRepoRow *row,
-                        GsReposDialog *dialog)
+repo_section_switch_clicked_cb (GsReposSection *section,
+                               GsRepoRow *row,
+                               GsReposDialog *dialog)
 {
        GsApp *repo;
 
@@ -351,10 +276,10 @@ 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, GS_PLUGIN_ACTION_DISABLE_REPO);
                break;
        default:
                g_warning ("repo %s button clicked in unexpected state %s",
@@ -364,8 +289,18 @@ repo_button_clicked_cb (GsRepoRow *row,
        }
 }
 
-static GtkListBox *
-get_list_box_for_repo (GsReposDialog *dialog, GsApp *repo)
+static void
+repo_section_remove_clicked_cb (GsReposSection *section,
+                               GsRepoRow *row,
+                               GsReposDialog *dialog)
+{
+       GsApp *repo = gs_repo_row_get_repo (row);
+       remove_confirm_repo (dialog, row, repo, GS_PLUGIN_ACTION_REMOVE_REPO);
+}
+
+static gboolean
+is_third_party_repo (GsReposDialog *dialog,
+                    GsApp *repo)
 {
        if (dialog->third_party_repo != NULL) {
                const gchar *source_repo;
@@ -375,19 +310,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;
+       g_autofree gchar *origin_ui = NULL;
 
        state = gs_app_get_state (repo);
        if (!(state == GS_APP_STATE_AVAILABLE ||
@@ -401,122 +337,57 @@ 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);
-}
-
-static void
-third_party_repo_installed_cb (GObject *source,
-                               GAsyncResult *res,
-                               gpointer user_data)
-{
-       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
-       g_autoptr(InstallRemoveData) install_data = (InstallRemoveData *) user_data;
-       g_autoptr(GError) error = NULL;
-
-       if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) {
-               const gchar *action_str = gs_plugin_action_to_string (install_data->action);
-
-               if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
-                       g_debug ("third party repo %s cancelled", action_str);
-                       return;
-               }
-
-               g_warning ("failed to %s third party repo: %s", action_str, error->message);
+       if (third_party_repos && is_third_party_repo (dialog, repo)) {
+               *third_party_repos = g_slist_prepend (*third_party_repos, repo);
                return;
        }
 
-       reload_sources (install_data->dialog);
-}
-
-static void
-install_third_party_repo (GsReposDialog *dialog, gboolean install)
-{
-       InstallRemoveData *install_data;
-       g_autoptr(GsPluginJob) plugin_job = NULL;
-
-       install_data = g_slice_new0 (InstallRemoveData);
-       install_data->dialog = g_object_ref (dialog);
-       install_data->action = install ? GS_PLUGIN_ACTION_INSTALL : GS_PLUGIN_ACTION_REMOVE;
-
-       plugin_job = gs_plugin_job_newv (install_data->action,
-                                        "interactive", TRUE,
-                                        "app", dialog->third_party_repo,
-                                        NULL);
-       gs_plugin_loader_job_process_async (dialog->plugin_loader,
-                                           plugin_job,
-                                           dialog->cancellable,
-                                           third_party_repo_installed_cb,
-                                           install_data);
-}
-
-static void
-third_party_repo_button_clicked_cb (GsThirdPartyRepoRow *row,
-                                    gpointer user_data)
-{
-       GsReposDialog *dialog = (GsReposDialog *) user_data;
-       GsApp *app;
-
-       app = gs_third_party_repo_row_get_app (row);
-
-       switch (gs_app_get_state (app)) {
-       case GS_APP_STATE_UNAVAILABLE:
-       case GS_APP_STATE_AVAILABLE:
-       case GS_APP_STATE_AVAILABLE_LOCAL:
-               install_third_party_repo (dialog, TRUE);
-               break;
-       case GS_APP_STATE_UPDATABLE_LIVE:
-       case GS_APP_STATE_UPDATABLE:
-       case GS_APP_STATE_INSTALLED:
-               install_third_party_repo (dialog, FALSE);
-               break;
-       default:
-               g_warning ("third party repo %s button clicked in unexpected state %s",
-                          gs_app_get_id (app),
-                          gs_app_state_to_string (gs_app_get_state (app)));
-               break;
+       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 == NULL) {
+               section = gs_repos_section_new (dialog->plugin_loader, origin_ui);
+               g_object_set (G_OBJECT (section),
+                             "halign", GTK_ALIGN_FILL,
+                             "hexpand", TRUE,
+                             NULL);
+               g_signal_connect_object (section, "remove-clicked",
+                                        G_CALLBACK (repo_section_remove_clicked_cb), dialog, 0);
+               g_signal_connect_object (section, "switch-clicked",
+                                        G_CALLBACK (repo_section_switch_clicked_cb), dialog, 0);
+               g_hash_table_insert (dialog->sections, g_steal_pointer (&origin_ui), section);
+               gtk_widget_show (section);
        }
 
-       g_settings_set_boolean (dialog->settings, "show-nonfree-prompt", FALSE);
+       gs_repos_section_add_repo (GS_REPOS_SECTION (section), repo);
 }
 
-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;
-       }
+       GsReposSection *section_a = (GsReposSection *) aa;
+       GsReposSection *section_b = (GsReposSection *) bb;
+       const gchar *section_sort_key_a;
+       const gchar *section_sort_key_b;
+       g_autofree gchar *title_sort_key_a = NULL;
+       g_autofree gchar *title_sort_key_b = NULL;
+       gint res;
 
-       gtk_widget_show (dialog->listbox_third_party);
-}
+       section_sort_key_a = gs_repos_section_get_sort_key (section_a);
+       section_sort_key_b = gs_repos_section_get_sort_key (section_b);
 
-static void
-remove_all_repo_rows_cb (GtkWidget *widget, gpointer user_data)
-{
-       GtkContainer *container = GTK_CONTAINER (user_data);
+       res = g_strcmp0 (section_sort_key_a, section_sort_key_b);
+       if (res != 0)
+               return res;
 
-       if (GS_IS_REPO_ROW (widget))
-               gtk_container_remove (container, widget);
-}
+       title_sort_key_a = gs_utils_sort_key (gs_repos_section_get_title (section_a));
+       title_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 (title_sort_key_a, title_sort_key_b);
 }
 
 static void
@@ -527,6 +398,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);
@@ -546,8 +419,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));
@@ -567,11 +440,88 @@ 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);
        }
 
-       gtk_widget_set_visible (dialog->listbox,
-               gtk_list_box_get_row_at_index (GTK_LIST_BOX (dialog->listbox), 0) != NULL);
+       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->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 (dialog->plugin_loader, _("Fedora Third 
Party Repositories")));
+               g_object_set (G_OBJECT (section),
+                             "halign", GTK_ALIGN_FILL,
+                             "hexpand", TRUE,
+                             NULL);
+               gs_repos_section_set_sort_key (section, "900");
+               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 |
+                                                        GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE,
+                                        "dedupe-flags", GS_APP_LIST_FILTER_FLAG_NONE,
+                                        NULL);
+       gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job,
+                                           dialog->cancellable,
+                                           (GAsyncReadyCallback) get_sources_cb,
+                                           dialog);
 }
 
 static void
@@ -593,6 +543,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;
                }
        }
@@ -604,27 +555,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,
-                                        "dedupe-flags", GS_APP_LIST_FILTER_FLAG_NONE,
-                                        NULL);
-       gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job,
-                                           dialog->cancellable,
-                                           (GAsyncReadyCallback) get_sources_cb,
-                                           dialog);
+       reload_sources (dialog);
 }
 
 static gboolean
@@ -651,13 +582,16 @@ 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,
                                         "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
-                                                        GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES,
+                                                        GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES |
+                                                        GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE,
                                         NULL);
        gs_plugin_loader_job_process_async (dialog->plugin_loader, plugin_job,
                                            dialog->cancellable,
@@ -665,64 +599,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)
 {
@@ -744,7 +620,6 @@ get_os_name (void)
 static void
 reload_cb (GsPluginLoader *plugin_loader, GsReposDialog *dialog)
 {
-       reload_sources (dialog);
        reload_third_party_repo (dialog);
 }
 
@@ -767,6 +642,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);
@@ -777,7 +653,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);
@@ -786,47 +661,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."),
@@ -844,12 +682,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);
 }
@@ -867,7 +702,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..b3b34efde 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_top">32</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..5b163add8
--- /dev/null
+++ b/src/gs-repos-section.c
@@ -0,0 +1,226 @@
+/* -*- 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;
+       GsPluginLoader  *plugin_loader;
+       gchar           *sort_key;
+};
+
+G_DEFINE_TYPE (GsReposSection, gs_repos_section, GTK_TYPE_BOX)
+
+enum {
+       SIGNAL_REMOVE_CLICKED,
+       SIGNAL_SWITCH_CLICKED,
+       SIGNAL_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+static void
+repo_remove_clicked_cb (GsRepoRow *row,
+                       GsReposSection *section)
+{
+       g_signal_emit (section, signals[SIGNAL_REMOVE_CLICKED], 0, row);
+}
+
+static void
+repo_switch_clicked_cb (GsRepoRow *row,
+                       GsReposSection *section)
+{
+       g_signal_emit (section, signals[SIGNAL_SWITCH_CLICKED], 0, row);
+}
+
+static void
+gs_repos_section_row_activated_cb (GtkListBox *box,
+                                  GtkListBoxRow *row,
+                                  gpointer user_data)
+{
+       GsReposSection *section = user_data;
+       g_return_if_fail (GS_IS_REPOS_SECTION (section));
+       gs_repo_row_emit_switch_clicked (GS_REPO_ROW (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_clear_object (&self->plugin_loader);
+       g_free (self->sort_key);
+
+       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_REMOVE_CLICKED] =
+               g_signal_new ("remove-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);
+
+       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;
+
+       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);
+
+       g_signal_connect (self->list, "row-activated",
+                         G_CALLBACK (gs_repos_section_row_activated_cb), self);
+}
+
+GtkWidget *
+gs_repos_section_new (GsPluginLoader *plugin_loader,
+                     const gchar *title)
+{
+       GsReposSection *self;
+
+       g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+
+       self = g_object_new (GS_TYPE_REPOS_SECTION,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            "spacing", 8,
+                            NULL);
+
+       gtk_label_set_text (GTK_LABEL (self->title), title);
+       self->plugin_loader = g_object_ref (plugin_loader);
+
+       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));
+
+       /* Derive the sort key from the repository. All repositories of the same kind
+          should have set the same sort key. It's because there's no other way to provide
+          the section sort key by the plugin without breaking the abstraction. */
+       if (!self->sort_key)
+               self->sort_key = g_strdup (gs_app_get_metadata_item (repo, "GnomeSoftware::SortKey"));
+
+       row = gs_repo_row_new (self->plugin_loader, repo);
+
+       g_signal_connect (row, "remove-clicked",
+                         G_CALLBACK (repo_remove_clicked_cb), self);
+       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_sort_key (GsReposSection *self)
+{
+       g_return_val_if_fail (GS_IS_REPOS_SECTION (self), NULL);
+
+       return self->sort_key;
+}
+
+void
+gs_repos_section_set_sort_key (GsReposSection *self,
+                              const gchar *sort_key)
+{
+       g_return_if_fail (GS_IS_REPOS_SECTION (self));
+
+       if (g_strcmp0 (sort_key, self->sort_key) != 0) {
+               g_free (self->sort_key);
+               self->sort_key = g_strdup (sort_key);
+       }
+}
diff --git a/src/gs-repos-section.h b/src/gs-repos-section.h
new file mode 100644
index 000000000..6a8c43004
--- /dev/null
+++ b/src/gs-repos-section.h
@@ -0,0 +1,31 @@
+/* -*- 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 "gnome-software-private.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                   (GsPluginLoader         *plugin_loader,
+                                                        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_sort_key          (GsReposSection         *self);
+void            gs_repos_section_set_sort_key          (GsReposSection         *self,
+                                                        const gchar            *sort_key);
+
+G_END_DECLS
diff --git a/src/gtk-style.css b/src/gtk-style.css
index b073f05ff..4106fe0a8 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -43,6 +43,33 @@
        font-size: x-small;
 }
 
+repos-section list {
+       border-radius: 8px;
+       border: 1px solid darker(@theme_bg_color);
+       padding-bottom: 0px;
+       padding-top: 0px;
+}
+
+repos-section list reporow:not(:last-child) {
+       border-bottom: 1px solid darker(@theme_bg_color);
+}
+
+repos-section list reporow {
+       border-radius: 0;
+}
+
+repos-section list reporow:first-child {
+       border-radius: 8px 8px 0 0;
+}
+
+repos-section list reporow:last-child {
+       border-radius: 0 0 8px 8px;
+}
+
+repos-section list reporow:only-child {
+       border-radius: 8px;
+}
+
 /* Adapted from Adwaita’s .needs-attention class for stacksidebar */
 sidebar row.needs-attention > box > label {
        animation: needs_attention 150ms ease-in;
@@ -340,6 +367,7 @@ review-bar {
 .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 950bff434..d40c85f3d 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -52,6 +52,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]