[rhythmbox] sync: split sync state and settings UI code up into three new classes



commit 073efe208fce7690a8c053a8238d244558135061
Author: Jonathan Matthew <jonathan d14n org>
Date:   Fri Jun 4 15:55:49 2010 +1000

    sync: split sync state and settings UI code up into three new classes
    
    The purpose of this is to allow us to use the sync settings and state
    widgets in other places.  It's still tied directly to the media player
    source class.

 configure.ac                                       |    1 +
 data/ui/Makefile.am                                |    1 +
 data/ui/media-player-properties.ui                 |  117 +---
 data/ui/sync-state.ui                              |  110 +++
 plugins/generic-player/Makefile.am                 |    1 +
 plugins/generic-player/rb-generic-player-source.c  |    2 +-
 plugins/ipod/Makefile.am                           |    3 +-
 plugins/ipod/rb-ipod-source.c                      |    2 +-
 plugins/mtpdevice/Makefile.am                      |    3 +-
 plugins/mtpdevice/rb-mtp-source.c                  |    2 +-
 po/POTFILES.in                                     |    3 +
 shell/Makefile.am                                  |    1 +
 sources/Makefile.am                                |    9 +-
 sources/rb-media-player-source.c                   |  880 ++------------------
 sources/rb-media-player-source.h                   |    6 +
 sources/rb-media-player-sync-settings.h            |   99 ---
 sources/sync/Makefile.am                           |   33 +
 sources/sync/rb-sync-settings-ui.c                 |  415 +++++++++
 sources/sync/rb-sync-settings-ui.h                 |   67 ++
 .../rb-sync-settings.c}                            |  109 ++-
 sources/sync/rb-sync-settings.h                    |   99 +++
 sources/sync/rb-sync-state-ui.c                    |  332 ++++++++
 sources/sync/rb-sync-state-ui.h                    |   83 ++
 sources/sync/rb-sync-state.c                       |  552 ++++++++++++
 sources/sync/rb-sync-state.h                       |   91 ++
 25 files changed, 1940 insertions(+), 1081 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index f807236..0a3dda0 100644
--- a/configure.ac
+++ b/configure.ac
@@ -850,6 +850,7 @@ data/icons/hicolor/scalable/places/Makefile
 data/icons/hicolor/scalable/status/Makefile
 data/icons/src/Makefile
 sources/Makefile
+sources/sync/Makefile
 plugins/Makefile
 plugins/sample/Makefile
 plugins/audiocd/Makefile
diff --git a/data/ui/Makefile.am b/data/ui/Makefile.am
index 63f95cb..ff94ff3 100644
--- a/data/ui/Makefile.am
+++ b/data/ui/Makefile.am
@@ -15,6 +15,7 @@ GTK_BUILDER_FILES =					\
 	podcast-properties.ui				\
 	song-info.ui					\
 	song-info-multiple.ui				\
+	sync-state.ui					\
 	uri-new.ui
 
 uidir = $(pkgdatadir)
diff --git a/data/ui/media-player-properties.ui b/data/ui/media-player-properties.ui
index da9093f..e9f8327 100644
--- a/data/ui/media-player-properties.ui
+++ b/data/ui/media-player-properties.ui
@@ -2,7 +2,6 @@
 <interface>
   <requires lib="gtk+" version="2.16"/>
   <!-- interface-naming-policy toplevel-contextual -->
-  <object class="GtkTreeStore" id="treestore1"/>
   <object class="GtkDialog" id="media-player-properties">
     <property name="visible">True</property>
     <property name="title" translatable="yes">Media Player Properties</property>
@@ -77,8 +76,7 @@
                     <child type="label">
                       <object class="GtkLabel" id="label-frame-usage">
                         <property name="visible">True</property>
-                        <property name="label" translatable="yes"
-                         comments="Translators: This refers to the usage of media space">&lt;b&gt;Volume usage&lt;/b&gt;</property>
+                        <property name="label" translatable="yes" comments="Translators: This refers to the usage of media space">&lt;b&gt;Volume usage&lt;/b&gt;</property>
                         <property name="use_markup">True</property>
                       </object>
                     </child>
@@ -117,18 +115,14 @@
                         <property name="border_width">12</property>
                         <property name="orientation">vertical</property>
                         <child>
-                          <object class="GtkScrolledWindow" id="scrolledwindow1">
+                          <object class="GtkScrolledWindow" id="sync-settings-ui-container">
                             <property name="visible">True</property>
                             <property name="can_focus">True</property>
                             <property name="hscrollbar_policy">automatic</property>
                             <property name="vscrollbar_policy">automatic</property>
                             <property name="shadow_type">in</property>
                             <child>
-                              <object class="GtkTreeView" id="treeview-sync">
-                                <property name="visible">True</property>
-                                <property name="can_focus">True</property>
-                                <property name="headers_visible">False</property>
-                              </object>
+                              <placeholder/>
                             </child>
                           </object>
                           <packing>
@@ -159,109 +153,10 @@
                         <property name="visible">True</property>
                         <property name="left_padding">12</property>
                         <child>
-                          <object class="GtkVBox" id="vbox5">
+                          <object class="GtkHBox" id="sync-state-ui-container">
                             <property name="visible">True</property>
-                            <property name="border_width">6</property>
-                            <property name="orientation">vertical</property>
-                            <property name="spacing">12</property>
-                            <child>
-                              <object class="GtkLabel" id="sync-before-label">
-                                <property name="visible">True</property>
-                                <property name="xalign">0</property>
-                                <property name="label" translatable="yes">Current contents</property>
-                              </object>
-                              <packing>
-                                <property name="position">0</property>
-                              </packing>
-                            </child>
-                            <child>
-                              <object class="GtkVBox" id="sync-before-container">
-                                <property name="visible">True</property>
-                                <property name="orientation">vertical</property>
-                                <child>
-                                  <placeholder/>
-                                </child>
-                              </object>
-                              <packing>
-                                <property name="padding">6</property>
-                                <property name="position">1</property>
-                              </packing>
-                            </child>
                             <child>
-                              <object class="GtkLabel" id="sync-after-label">
-                                <property name="visible">True</property>
-                                <property name="xalign">0</property>
-                                <property name="label" translatable="yes">Contents after sync</property>
-                              </object>
-                              <packing>
-                                <property name="position">2</property>
-                              </packing>
-                            </child>
-                            <child>
-                              <object class="GtkVBox" id="sync-after-container">
-                                <property name="visible">True</property>
-                                <property name="orientation">vertical</property>
-                                <child>
-                                  <placeholder/>
-                                </child>
-                              </object>
-                              <packing>
-                                <property name="padding">6</property>
-                                <property name="position">3</property>
-                              </packing>
-                            </child>
-                            <child>
-                              <object class="GtkTable" id="table1">
-                                <property name="visible">True</property>
-                                <property name="n_columns">4</property>
-                                <property name="column_spacing">6</property>
-                                <property name="row_spacing">12</property>
-                                <child>
-                                  <object class="GtkLabel" id="label2">
-                                    <property name="visible">True</property>
-                                    <property name="xalign">0</property>
-                                    <property name="label" translatable="yes">Added files:</property>
-                                  </object>
-                                  <packing>
-                                    <property name="x_options">GTK_FILL</property>
-                                  </packing>
-                                </child>
-                                <child>
-                                  <object class="GtkLabel" id="added-tracks">
-                                    <property name="visible">True</property>
-                                    <property name="xalign">0</property>
-                                  </object>
-                                  <packing>
-                                    <property name="left_attach">1</property>
-                                    <property name="right_attach">2</property>
-                                  </packing>
-                                </child>
-                                <child>
-                                  <object class="GtkLabel" id="label3">
-                                    <property name="visible">True</property>
-                                    <property name="xalign">0</property>
-                                    <property name="label" translatable="yes">Removed files:</property>
-                                  </object>
-                                  <packing>
-                                    <property name="left_attach">2</property>
-                                    <property name="right_attach">3</property>
-                                    <property name="x_options">GTK_FILL</property>
-                                  </packing>
-                                </child>
-                                <child>
-                                  <object class="GtkLabel" id="removed-tracks">
-                                    <property name="visible">True</property>
-                                    <property name="xalign">0</property>
-                                  </object>
-                                  <packing>
-                                    <property name="left_attach">3</property>
-                                    <property name="right_attach">4</property>
-                                  </packing>
-                                </child>
-                              </object>
-                              <packing>
-                                <property name="position">4</property>
-                              </packing>
+                              <placeholder/>
                             </child>
                           </object>
                         </child>
@@ -332,6 +227,4 @@
       <action-widget response="-7">closebutton1</action-widget>
     </action-widgets>
   </object>
-  <object class="GtkTreeSelection" id="treeselection1"/>
-  <object class="GtkTreeSelection" id="treeselection2"/>
 </interface>
diff --git a/data/ui/sync-state.ui b/data/ui/sync-state.ui
new file mode 100644
index 0000000..68001a2
--- /dev/null
+++ b/data/ui/sync-state.ui
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<interface>
+  <requires lib="gtk+" version="2.16"/>
+  <!-- interface-naming-policy project-wide -->
+  <object class="GtkVBox" id="sync-state-ui">
+    <property name="visible">True</property>
+    <property name="border_width">6</property>
+    <property name="orientation">vertical</property>
+    <property name="spacing">12</property>
+    <child>
+      <object class="GtkLabel" id="sync-before-label">
+        <property name="visible">True</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Current contents</property>
+      </object>
+      <packing>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkVBox" id="sync-before-container">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="padding">6</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="sync-after-label">
+        <property name="visible">True</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Contents after sync</property>
+      </object>
+      <packing>
+        <property name="position">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkVBox" id="sync-after-container">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="padding">6</property>
+        <property name="position">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkTable" id="table1">
+        <property name="visible">True</property>
+        <property name="n_columns">4</property>
+        <property name="column_spacing">6</property>
+        <property name="row_spacing">12</property>
+        <child>
+          <object class="GtkLabel" id="label2">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Added files:</property>
+          </object>
+          <packing>
+            <property name="x_options">GTK_FILL</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="added-tracks">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="right_attach">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label3">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Removed files:</property>
+          </object>
+          <packing>
+            <property name="left_attach">2</property>
+            <property name="right_attach">3</property>
+            <property name="x_options">GTK_FILL</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="removed-tracks">
+            <property name="visible">True</property>
+            <property name="xalign">0</property>
+          </object>
+          <packing>
+            <property name="left_attach">3</property>
+            <property name="right_attach">4</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="position">4</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/plugins/generic-player/Makefile.am b/plugins/generic-player/Makefile.am
index 04a7f31..1098e24 100644
--- a/plugins/generic-player/Makefile.am
+++ b/plugins/generic-player/Makefile.am
@@ -29,6 +29,7 @@ INCLUDES = 						\
 	-I$(top_srcdir)/rhythmdb                       	\
 	-I$(top_srcdir)/widgets                    	\
 	-I$(top_srcdir)/sources                    	\
+	-I$(top_srcdir)/sources/sync                   	\
 	-I$(top_srcdir)/iradio                    	\
 	-I$(top_srcdir)/podcast                    	\
 	-I$(top_srcdir)/remote				\
diff --git a/plugins/generic-player/rb-generic-player-source.c b/plugins/generic-player/rb-generic-player-source.c
index 1211322..7de077f 100644
--- a/plugins/generic-player/rb-generic-player-source.c
+++ b/plugins/generic-player/rb-generic-player-source.c
@@ -52,7 +52,7 @@
 #include "rhythmdb-import-job.h"
 #include "rb-import-errors-source.h"
 #include "rb-builder-helpers.h"
-#include "rb-media-player-sync-settings.h"
+#include "rb-sync-settings.h"
 
 static void impl_constructed (GObject *object);
 static void impl_dispose (GObject *object);
diff --git a/plugins/ipod/Makefile.am b/plugins/ipod/Makefile.am
index 1f29644..dd5f72c 100644
--- a/plugins/ipod/Makefile.am
+++ b/plugins/ipod/Makefile.am
@@ -25,12 +25,11 @@ INCLUDES = 						\
 	-I$(top_srcdir)/lib                             \
 	-I$(top_srcdir)/lib/libmediaplayerid            \
 	-I$(top_srcdir)/metadata                       	\
-	-I$(top_srcdir)/player                       	\
 	-I$(top_srcdir)/rhythmdb                       	\
 	-I$(top_srcdir)/widgets                    	\
 	-I$(top_srcdir)/sources                    	\
+	-I$(top_srcdir)/sources/sync                   	\
 	-I$(top_srcdir)/podcast                    	\
-	-I$(top_srcdir)/plugins				\
 	-I$(top_srcdir)/shell				\
 	-DPIXMAP_DIR=\""$(datadir)/pixmaps"\"		\
 	-DSHARE_DIR=\"$(pkgdatadir)\"                   \
diff --git a/plugins/ipod/rb-ipod-source.c b/plugins/ipod/rb-ipod-source.c
index dbaf6e4..e41b696 100644
--- a/plugins/ipod/rb-ipod-source.c
+++ b/plugins/ipod/rb-ipod-source.c
@@ -49,7 +49,7 @@
 #include "rhythmdb.h"
 #include "rb-cut-and-paste-code.h"
 #include "rb-media-player-source.h"
-#include "rb-media-player-sync-settings.h"
+#include "rb-sync-settings.h"
 #include "rb-playlist-source.h"
 #include "rb-playlist-manager.h"
 #include "rb-podcast-manager.h"
diff --git a/plugins/mtpdevice/Makefile.am b/plugins/mtpdevice/Makefile.am
index f1fbab7..51fcfae 100644
--- a/plugins/mtpdevice/Makefile.am
+++ b/plugins/mtpdevice/Makefile.am
@@ -28,12 +28,11 @@ INCLUDES = 						\
 	-I$(top_srcdir)/lib                        	\
 	-I$(top_srcdir)/lib/libmediaplayerid          	\
 	-I$(top_srcdir)/metadata                       	\
-	-I$(top_srcdir)/player                       	\
 	-I$(top_srcdir)/rhythmdb                       	\
 	-I$(top_srcdir)/widgets                    	\
 	-I$(top_srcdir)/sources                    	\
+	-I$(top_srcdir)/sources/sync                   	\
 	-I$(top_srcdir)/podcast                    	\
-	-I$(top_srcdir)/plugins				\
 	-I$(top_srcdir)/shell				\
 	-DPIXMAP_DIR=\""$(datadir)/pixmaps"\"		\
 	-DSHARE_DIR=\"$(pkgdatadir)\"                   \
diff --git a/plugins/mtpdevice/rb-mtp-source.c b/plugins/mtpdevice/rb-mtp-source.c
index ec96d3f..375298a 100644
--- a/plugins/mtpdevice/rb-mtp-source.c
+++ b/plugins/mtpdevice/rb-mtp-source.c
@@ -52,7 +52,7 @@
 #include "rb-shell-player.h"
 #include "rb-player.h"
 #include "rb-encoder.h"
-#include "rb-media-player-sync-settings.h"
+#include "rb-sync-settings.h"
 
 #include "rb-mtp-source.h"
 #include "rb-mtp-thread.h"
diff --git a/po/POTFILES.in b/po/POTFILES.in
index c884d7e..bb41ce7 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -15,6 +15,7 @@ backends/rb-player.c
 [type: gettext/glade]data/ui/podcast-properties.ui
 [type: gettext/glade]data/ui/song-info-multiple.ui
 [type: gettext/glade]data/ui/song-info.ui
+[type: gettext/glade]data/ui/sync-state.ui
 [type: gettext/glade]data/ui/uri-new.ui
 data/playlists.xml.in
 data/rhythmbox.desktop.in.in
@@ -188,6 +189,8 @@ sources/rb-source-group.c
 sources/rb-sourcelist.c
 sources/rb-static-playlist-source.c
 sources/rb-streaming-source.c
+sources/sync/rb-sync-settings-ui.c
+sources/sync/rb-sync-state-ui.c
 widgets/rb-alert-dialog.c
 widgets/rb-cell-renderer-pixbuf.c
 widgets/rb-dialog.c
diff --git a/shell/Makefile.am b/shell/Makefile.am
index ff8f1db..ac879be 100644
--- a/shell/Makefile.am
+++ b/shell/Makefile.am
@@ -111,6 +111,7 @@ librhythmbox_core_la_SOURCES =				\
 
 librhythmbox_core_la_LIBADD =				\
 	$(top_builddir)/sources/libsources.la	        \
+	$(top_builddir)/sources/sync/libsourcesync.la	\
 	$(top_builddir)/podcast/librbpodcast.la	        \
 	$(top_builddir)/metadata/librbmetadata.la	\
 	$(top_builddir)/widgets/librbwidgets.la         \
diff --git a/sources/Makefile.am b/sources/Makefile.am
index a89540f..9e284c1 100644
--- a/sources/Makefile.am
+++ b/sources/Makefile.am
@@ -1,5 +1,7 @@
 NULL =
 
+SUBDIRS = sync
+
 noinst_LTLIBRARIES = libsources.la
 
 sourceincludedir = $(includedir)/rhythmbox/sources
@@ -34,8 +36,6 @@ libsources_la_SOURCES = 		\
 	rb-podcast-source.h		\
 	rb-removable-media-source.c	\
 	rb-media-player-source.c	\
-	rb-media-player-sync-settings.c	\
-	rb-media-player-sync-settings.h	\
 	rb-playlist-source.c            \
 	rb-auto-playlist-source.c	\
 	rb-static-playlist-source.c	\
@@ -58,13 +58,10 @@ INCLUDES =						\
 	-I$(top_srcdir)/rhythmdb			\
 	-I$(top_srcdir)/metadata 			\
 	-I$(top_srcdir)/widgets 			\
-	-I$(top_srcdir)/library 			\
-	-I$(top_srcdir)/player	 			\
-	-I$(top_srcdir)/iradio				\
 	-I$(top_srcdir)/podcast				\
 	-I$(top_srcdir)/shell				\
+	-I$(top_srcdir)/sources/sync			\
 	-I$(top_srcdir)/backends			\
-	-I$(top_srcdir)/plugins				\
 	-DPIXMAP_DIR=\""$(datadir)/pixmaps"\"		\
 	-DSHARE_DIR=\"$(pkgdatadir)\"                   \
 	-DDATADIR=\""$(datadir)"\"			\
diff --git a/sources/rb-media-player-source.c b/sources/rb-media-player-source.c
index 4ed9248..31bed7e 100644
--- a/sources/rb-media-player-source.c
+++ b/sources/rb-media-player-source.c
@@ -1,5 +1,6 @@
 /*
  *  Copyright (C) 2009 Paul Bellamy  <paul a bellamy gmail com>
+ *  Copyright (C) 2010 Jonathan Matthew  <jonathan d14n org>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -34,7 +35,10 @@
 
 #include "rb-shell.h"
 #include "rb-media-player-source.h"
-#include "rb-media-player-sync-settings.h"
+#include "rb-sync-settings.h"
+#include "rb-sync-settings-ui.h"
+#include "rb-sync-state.h"
+#include "rb-sync-state-ui.h"
 #include "rb-dialog.h"
 #include "rb-debug.h"
 #include "rb-file-helpers.h"
@@ -45,24 +49,14 @@
 #include "rb-segmented-bar.h"
 
 typedef struct {
-	GtkWidget *widget;
-	guint music_segment;
-	guint podcast_segment;
-	guint other_segment;
-	guint free_segment;
-} SegmentedBarData;
-
-typedef struct {
-	RBMediaPlayerSyncSettings *sync_settings;
+	RBSyncSettings *sync_settings;
 
 	GtkActionGroup *action_group;
 	GtkAction *sync_action;
 
 	/* properties dialog bits */
 	GtkDialog *properties_dialog;
-	GtkTreeStore *sync_tree_store;
-	GtkWidget *added_files_count;
-	GtkWidget *removed_files_count;
+	RBSyncBarData volume_usage;
 
 	/* segmented bars used in various places */
 	SegmentedBarData volume_usage;
@@ -76,9 +70,8 @@ typedef struct {
 	guint64 sync_podcast_size;
 
 	/* sync state */
-	guint64 sync_space_needed;
-	GList *sync_to_add;
-	GList *sync_to_remove;
+	RBSyncState *sync_state;
+	GList *sync_add_remaining;
 
 } RBMediaPlayerSourcePrivate;
 
@@ -110,11 +103,11 @@ static gboolean rb_media_player_source_track_add_error (RBRemovableMediaSource *
 							const char *uri,
 							GError *error);
 
+static gboolean sync_idle_cb_update_sync (RBMediaPlayerSource *source);
+
 static void track_add_done (RBMediaPlayerSource *source, RhythmDBEntry *entry);
-static void update_sync (RBMediaPlayerSource *source);
 static void sync_cmd (GtkAction *action, RBSource *source);
-static char *make_track_uuid  (RhythmDBEntry *entry);
-static GHashTable *build_device_state (RBMediaPlayerSource *source);
+static gboolean sync_idle_delete_entries (RBMediaPlayerSource *source);
 
 static GtkActionEntry rb_media_player_source_actions[] = {
 	{ "MediaPlayerSourceSync", GTK_STOCK_REFRESH, N_("Sync"), NULL,
@@ -195,6 +188,11 @@ rb_media_player_source_dispose (GObject *object)
 		priv->sync_settings = NULL;
 	}
 
+	if (priv->sync_state) {
+		g_object_unref (priv->sync_state);
+		priv->sync_state = NULL;
+	}
+
 	G_OBJECT_CLASS (rb_media_player_source_parent_class)->dispose (object);
 }
 
@@ -295,20 +293,20 @@ rb_media_player_source_load		(RBMediaPlayerSource *source)
 	g_free (device_id);
 	g_free (sync_dir);
 
-	priv->sync_settings = rb_media_player_sync_settings_new (sync_path);
+	priv->sync_settings = rb_sync_settings_new (sync_path);
 	g_free (sync_path);
 }
 
-static guint64
-get_capacity (RBMediaPlayerSource *source)
+guint64
+rb_media_player_source_get_capacity (RBMediaPlayerSource *source)
 {
 	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
 
 	return klass->impl_get_capacity (source);
 }
 
-static guint64
-get_free_space (RBMediaPlayerSource *source)
+guint64
+rb_media_player_source_get_free_space (RBMediaPlayerSource *source)
 {
 	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
 
@@ -316,6 +314,15 @@ get_free_space (RBMediaPlayerSource *source)
 }
 
 void
+rb_media_player_source_get_entries (RBMediaPlayerSource *source,
+				    const char *category,
+				    GHashTable *map)
+{
+	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
+	return klass->impl_get_entries (source, category, map);
+}
+
+void
 rb_media_player_source_delete_entries	(RBMediaPlayerSource *source,
 					 GList *entries,
 					 RBMediaPlayerSourceDeleteCallback callback,
@@ -328,266 +335,26 @@ rb_media_player_source_delete_entries	(RBMediaPlayerSource *source,
 }
 
 static void
-properties_dialog_response_cb (GtkDialog *dialog,
-			       int response_id,
-			       RBMediaPlayerSource *source)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	rb_debug ("media player properties dialog closed");
-	gtk_widget_destroy (GTK_WIDGET (dialog));
-	g_object_unref (priv->properties_dialog);
-	priv->properties_dialog = NULL;
-}
-
-static void
-update_volume_usage_bar (RBMediaPlayerSource *source, SegmentedBarData *bar)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	double fraction;
-	guint64 capacity;
-	guint64 total_other;
-	guint64 free_space;
-
-	capacity = get_capacity (source);
-	free_space = get_free_space (source);
-	total_other = capacity - (free_space + priv->total_music_size + priv->total_podcast_size);
-
-	fraction = (double)priv->total_music_size/(double)capacity;
-	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
-					 bar->music_segment,
-					 fraction);
-	fraction = (double)priv->total_podcast_size/(double)capacity;
-	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
-					 bar->podcast_segment,
-					 fraction);
-	fraction = (double)total_other/(double)capacity;
-	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
-					 bar->other_segment,
-					 fraction);
-	fraction = (double)free_space/(double)capacity;
-	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
-					 bar->free_segment,
-					 fraction);
-}
-
-static void
-update_sync_preview_bars (RBMediaPlayerSource *source)
+update_sync (RBMediaPlayerSource *source)
 {
 	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	char *text;
-	double music_fraction;
-	double podcast_fraction;
-	double other_fraction;
-	double free_fraction;
-	guint64 total_other_size;
-	guint64 device_capacity;
-
-	update_sync (source);
-
-	device_capacity = get_capacity (source);
-	total_other_size = device_capacity - (get_free_space (source) + priv->total_music_size + priv->total_podcast_size);
-
-	/* sync before state */
-	update_volume_usage_bar (source, &priv->sync_before);
-
-	/* sync after state */
-
-	if (rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, SYNC_CATEGORY_MUSIC) ||
-	    rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_MUSIC)) {
-		music_fraction = (double)priv->sync_music_size / (double)device_capacity;
-	} else {
-		music_fraction = (double)priv->total_music_size / (double)device_capacity;
-	}
-	if (rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, SYNC_CATEGORY_PODCAST) ||
-	    rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
-		podcast_fraction = (double)priv->sync_podcast_size / (double)device_capacity;
+	if (priv->sync_state == NULL) {
+		priv->sync_state = rb_sync_state_new (source, priv->sync_settings);
 	} else {
-		podcast_fraction = (double)priv->total_podcast_size / (double)device_capacity;
+		rb_sync_state_update (priv->sync_state);
 	}
-	other_fraction = (double)total_other_size / (double)device_capacity;
-
-	free_fraction = 1.0 - (music_fraction + podcast_fraction + other_fraction);
-	if (free_fraction < 0.0) {
-		free_fraction = 0.0;
-	}
-
-	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (priv->sync_after.widget),
-					 priv->sync_after.music_segment,
-					 music_fraction);
-	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (priv->sync_after.widget),
-					 priv->sync_after.podcast_segment,
-					 podcast_fraction);
-	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (priv->sync_after.widget),
-					 priv->sync_after.other_segment,
-					 other_fraction);
-	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (priv->sync_after.widget),
-					 priv->sync_after.free_segment,
-					 free_fraction);
-
-	/* other stuff */
-	text = g_strdup_printf ("%d", g_list_length (priv->sync_to_add));
-	gtk_label_set_text (GTK_LABEL (priv->added_files_count), text);
-	g_free (text);
-
-	text = g_strdup_printf ("%d", g_list_length (priv->sync_to_remove));
-	gtk_label_set_text (GTK_LABEL (priv->removed_files_count), text);
-	g_free (text);
 }
 
 static void
-set_treeview_children (RBMediaPlayerSource *source,
-		       GtkTreeIter *parent,
-		       const char *category,
-		       gboolean value,
-		       gboolean apply_to_settings)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	GtkTreeIter iter;
-	char *group;
-	gboolean valid;
-
-	valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->sync_tree_store), &iter, parent);
-
-	while (valid) {
-		gtk_tree_model_get (GTK_TREE_MODEL (priv->sync_tree_store), &iter,
-				    2, &group,
-				    -1);
-
-		if (apply_to_settings) {
-			rb_media_player_sync_settings_set_group (priv->sync_settings, category, group, value);
-		}
-		gtk_tree_store_set (priv->sync_tree_store, &iter,
-		/* Active */	    0, value,
-				    -1);
-
-		g_free (group);
-		valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->sync_tree_store), &iter);
-	}
-}
-
-static void
-sync_entries_changed_cb (GtkCellRendererToggle *cell_renderer,
-			 gchar *path,
-			 RBMediaPlayerSource *source)
+properties_dialog_response_cb (GtkDialog *dialog,
+			       int response_id,
+			       RBMediaPlayerSource *source)
 {
 	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	GtkTreeIter iter;
-	char *group;
-	char *category_name;
-	gboolean is_category;
-	gboolean value;
-
-	if (gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (priv->sync_tree_store), &iter, path) == FALSE) {
-		return;
-	}
-
-	gtk_tree_model_get (GTK_TREE_MODEL (priv->sync_tree_store),
-			    &iter,
-			    2, &group,
-			    4, &is_category,
-			    5, &category_name,
-			    -1);
-
-	value = !gtk_cell_renderer_toggle_get_active (cell_renderer);
-
-	if (is_category) {
-		rb_debug ("state for category %s changed to %d", category_name, value);
-		rb_media_player_sync_settings_set_category (priv->sync_settings, category_name, value);
-		rb_media_player_sync_settings_clear_groups (priv->sync_settings, category_name);
-
-		gtk_tree_store_set (priv->sync_tree_store,
-				    &iter,
-				    0, value,
-				    1, FALSE,		/* category is no longer inconsistent */
-				    -1);
-		set_treeview_children (source, &iter, category_name, value, FALSE);
-	} else {
-		gboolean parent_value;
-		gboolean parent_inconsistent;
-		GtkTreeIter parent;
-		rb_debug ("state for group %s in category %s changed to %d", group, category_name, value);
-
-		/* update parent state.  if the parent was previously enabled or disabled, then we
-		 * set all the other groups to that state before setting the row that was just changed,
-		 * and set the parent to inconsistent state.
-		 */
-		gtk_tree_model_iter_parent (GTK_TREE_MODEL (priv->sync_tree_store), &parent, &iter);
-		gtk_tree_model_get (GTK_TREE_MODEL (priv->sync_tree_store), &parent,
-				    0, &parent_value,
-				    1, &parent_inconsistent,
-				    -1);
-		if (parent_inconsistent == FALSE) {
-			/* category is now inconsistent */
-			rb_debug ("setting category %s to disabled, inconsistent", category_name);
-			rb_media_player_sync_settings_set_category (priv->sync_settings, category_name, FALSE);
-			gtk_tree_store_set (priv->sync_tree_store,
-					    &parent,
-					    0, FALSE,
-					    1, TRUE,
-					    -1);
-
-			/* set all groups in the category to the parent's state */
-			set_treeview_children (source, &parent, category_name, parent_value, TRUE);
-		}
-
-		rb_media_player_sync_settings_set_group (priv->sync_settings, category_name, group, value);
-		gtk_tree_store_set (priv->sync_tree_store, &iter,
-				    0, value,
-				    -1);
-
-		/* if no groups are enabled, the category is no longer inconsistent */
-		if (value == FALSE && rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, category_name) == FALSE) {
-			rb_debug ("no enabled groups left in category %s", category_name);
-			gtk_tree_store_set (priv->sync_tree_store, &parent,
-					    1, FALSE,
-					    -1);
-		} else if (value == FALSE) {
-			rb_debug ("category %s still has some groups", category_name);
-		}
-	}
-
-	g_free (category_name);
-	g_free (group);
-	update_sync_preview_bars (source);
-}
-
-
-static char *
-value_formatter (gdouble percent, gpointer data)
-{
-	gsize total_size = GPOINTER_TO_SIZE (data);
-	return g_format_size_for_display (percent * total_size);
-}
-
-static void
-create_segmented_bar (RBMediaPlayerSource *source, GtkWidget *label, SegmentedBarData *bar)
-{
-	guint64 capacity;		/* XXX only really need to get this once on construction.. */
-
-	bar->widget = rb_segmented_bar_new ();
-	g_object_set (bar->widget, "show-labels", TRUE, NULL);
-
-	capacity = get_capacity (source);
-	rb_segmented_bar_set_value_formatter (RB_SEGMENTED_BAR (bar->widget),
-					      value_formatter,
-					      GSIZE_TO_POINTER (capacity));
-
-	bar->music_segment = rb_segmented_bar_add_segment (RB_SEGMENTED_BAR (bar->widget),_("Music"), 0.0, 0.2, 0.4, 0.65, 1.0);
-	bar->podcast_segment = rb_segmented_bar_add_segment (RB_SEGMENTED_BAR (bar->widget), _("Podcasts"), 0.0, 0.96, 0.47, 0.0, 1.0);
-	bar->other_segment = rb_segmented_bar_add_segment (RB_SEGMENTED_BAR (bar->widget), _("Other"), 0.0, 0.45, 0.82, 0.08, 1.0);
-	bar->free_segment = rb_segmented_bar_add_segment_default_color (RB_SEGMENTED_BAR (bar->widget), _("Available"), 1.0);
-
-	/* set up label relationship */
-	if (label != NULL) {
-		AtkObject *lobj;
-		AtkObject *robj;
-
-		lobj = gtk_widget_get_accessible (label);
-		robj = gtk_widget_get_accessible (bar->widget);
-
-		atk_object_add_relationship (lobj, ATK_RELATION_LABEL_FOR, robj);
-		atk_object_add_relationship (robj, ATK_RELATION_LABELLED_BY, lobj);
-	}
+	rb_debug ("media player properties dialog closed");
+	gtk_widget_destroy (GTK_WIDGET (dialog));
+	g_object_unref (priv->properties_dialog);
+	priv->properties_dialog = NULL;
 }
 
 void
@@ -596,23 +363,10 @@ rb_media_player_source_show_properties (RBMediaPlayerSource *source)
 	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
 	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
 	GtkBuilder *builder;
-	GtkTreeIter tree_iter;
-	GtkTreeIter parent_iter;
-	GtkTreeModel *query_model;
-	GtkCellRenderer *renderer;
-	GtkTreeViewColumn *col;
-	GtkWidget *tree_view;
 	GtkContainer *container;
 	const char *ui_file;
 	char *name;
 	char *text;
-	GList *l;
-	GList *playlists;
-	gboolean valid;
-	RBShell *shell;
-	RhythmDB *db;
-	RBPlaylistManager *playlist_manager;
-	GHashTable *device;
 
 	if (priv->properties_dialog != NULL) {
 		gtk_window_present (GTK_WINDOW (priv->properties_dialog));
@@ -632,9 +386,6 @@ rb_media_player_source_show_properties (RBMediaPlayerSource *source)
 		return;
 	}
 
-	g_object_get (source, "shell", &shell, NULL);
-	g_object_get (shell, "db", &db, "playlist-manager", &playlist_manager, NULL);
-
 	priv->properties_dialog = GTK_DIALOG (gtk_builder_get_object (builder, "media-player-properties"));
 	g_object_ref (priv->properties_dialog);
 	g_signal_connect_object (priv->properties_dialog,
@@ -648,18 +399,15 @@ rb_media_player_source_show_properties (RBMediaPlayerSource *source)
 	g_free (text);
 	g_free (name);
 
-	/* constructing the device state also updates our idea of the total music
-	 * and podcast sizes on the device
-	 */
-	device = build_device_state (source);
-	g_hash_table_destroy (device);
+	/* ensure device usage information is available and up to date */
+	update_sync (source);
 
 	/*
 	 * fill in some common details:
 	 * - volume usage (need to hook up signals etc. to update this live)
 	 */
-	create_segmented_bar (source, NULL, &priv->volume_usage);
-	update_volume_usage_bar (source, &priv->volume_usage);
+	rb_sync_state_ui_create_bar (&priv->volume_usage, rb_media_player_source_get_capacity (source), NULL);
+	rb_sync_state_ui_update_volume_usage (&priv->volume_usage, priv->sync_state);
 
 	gtk_widget_show_all (priv->volume_usage.widget);
 	container = GTK_CONTAINER (gtk_builder_get_object (builder, "device-usage-container"));
@@ -676,480 +424,20 @@ rb_media_player_source_show_properties (RBMediaPlayerSource *source)
 					     GTK_WIDGET (gtk_builder_get_object (builder, "media-player-notebook")));
 	}
 
-	/* set up sync widgetry */
-	priv->added_files_count = GTK_WIDGET (gtk_builder_get_object (builder, "added-tracks"));
-	priv->removed_files_count = GTK_WIDGET (gtk_builder_get_object (builder, "removed-tracks"));
-
-	create_segmented_bar (source,
-			      GTK_WIDGET (gtk_builder_get_object (builder, "sync-before-label")),
-			      &priv->sync_before);
-	gtk_widget_show_all (priv->sync_before.widget);
-	container = GTK_CONTAINER (gtk_builder_get_object (builder, "sync-before-container"));
-	gtk_container_add (container, priv->sync_before.widget);
-
-	create_segmented_bar (source,
-			      GTK_WIDGET (gtk_builder_get_object (builder, "sync-after-label")),
-			      &priv->sync_after);
-	gtk_widget_show_all (priv->sync_after.widget);
-	container = GTK_CONTAINER (gtk_builder_get_object (builder, "sync-after-container"));
-	gtk_container_add (container, priv->sync_after.widget);
-
-	/* tree_store columns are: active, inconsistent, name, display-name, is-category, category name */
-	priv->sync_tree_store = gtk_tree_store_new (6,
-						    G_TYPE_BOOLEAN,
-						    G_TYPE_BOOLEAN,
-						    G_TYPE_STRING,
-						    G_TYPE_STRING,
-						    G_TYPE_BOOLEAN,
-						    G_TYPE_STRING);
-
-	/* music library parent */
-	gtk_tree_store_append (priv->sync_tree_store, &parent_iter, NULL);
-	gtk_tree_store_set (priv->sync_tree_store, &parent_iter,
-			    0, rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_MUSIC),
-			    1, rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, SYNC_CATEGORY_MUSIC),
-			    2, _("Music"),
-			    3, _("Music"),
-			    4, TRUE,
-			    5, SYNC_CATEGORY_MUSIC,
-			    -1);
-
-	/* 'all music' entry */
-	gtk_tree_store_append (priv->sync_tree_store, &tree_iter, &parent_iter);
-	gtk_tree_store_set (priv->sync_tree_store, &tree_iter,
-			    0, rb_media_player_sync_settings_sync_group (priv->sync_settings, SYNC_CATEGORY_MUSIC, SYNC_GROUP_ALL_MUSIC),
-			    1, FALSE,
-			    2, SYNC_GROUP_ALL_MUSIC,
-			    3, _("All Music"),
-			    4, FALSE,
-			    5, SYNC_CATEGORY_MUSIC,
-			    -1);
-
-	/* playlist entries */
-	playlists = rb_playlist_manager_get_playlists (playlist_manager);
-	for (l = playlists; l != NULL; l = l->next) {
-		char *name;
-
-		gtk_tree_store_append (priv->sync_tree_store, &tree_iter, &parent_iter);
-		/* set playlists data here */
-		g_object_get (l->data, "name", &name, NULL);
-
-		/* set this row's data */
-		gtk_tree_store_set (priv->sync_tree_store, &tree_iter,
-				    0, rb_media_player_sync_settings_sync_group (priv->sync_settings, SYNC_CATEGORY_MUSIC, name),
-				    1, FALSE,
-				    2, name,
-				    3, name,
-				    4, FALSE,
-				    5, SYNC_CATEGORY_MUSIC,
-				    -1);
-
-		g_free (name);
-	}
-
-	/* Append the Podcasts parent */
-	gtk_tree_store_append (priv->sync_tree_store,
-			       &parent_iter,
-			       NULL);
-	gtk_tree_store_set (priv->sync_tree_store, &parent_iter,
-			    0, rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PODCAST),
-			    1, rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, SYNC_CATEGORY_PODCAST),
-			    2, _("Podcasts"),
-			    3, _("Podcasts"),
-			    4, TRUE,
-			    5, SYNC_CATEGORY_PODCAST,
-			    -1);
-
-	/* this really needs to use a live query model */
-	query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
-	rhythmdb_query_model_set_sort_order (RHYTHMDB_QUERY_MODEL (query_model),
-					     (GCompareDataFunc) rhythmdb_query_model_title_sort_func,
-					     NULL, NULL, FALSE);
-	rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
-				RHYTHMDB_QUERY_PROP_EQUALS,
-				RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_FEED,
-				RHYTHMDB_QUERY_END);
-	valid = gtk_tree_model_get_iter_first (query_model, &tree_iter);
-	while (valid) {
-		RhythmDBEntry *entry;
-		GtkTreeIter tree_iter2;
-		const char *name;
-		const char *feed;
-
-		entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (query_model), &tree_iter);
-		gtk_tree_store_append (priv->sync_tree_store, &tree_iter2, &parent_iter);
-
-		/* set up this row */
-		name = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE);
-		feed = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
-		rb_debug ("adding feed %s (name %s)", feed, name);
-		gtk_tree_store_set (priv->sync_tree_store, &tree_iter2,
-				    0, rb_media_player_sync_settings_sync_group (priv->sync_settings, SYNC_CATEGORY_PODCAST, name),
-				    1, FALSE,
-				    2, feed,
-				    3, name,
-				    4, FALSE,
-				    5, SYNC_CATEGORY_PODCAST,
-				    -1);
-
-		valid = gtk_tree_model_iter_next (query_model, &tree_iter);
-	}
+	/* create sync UI */
+	container = GTK_CONTAINER (gtk_builder_get_object (builder, "sync-settings-ui-container"));
+	gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (container), rb_sync_settings_ui_new (source, priv->sync_settings));
 
-	/* Set up the treeview */
-	tree_view = GTK_WIDGET (gtk_builder_get_object (builder, "treeview-sync"));
-
-	/* First column */
-	renderer = gtk_cell_renderer_toggle_new ();
-	col = gtk_tree_view_column_new_with_attributes (NULL,
-							renderer,
-							"active", 0,
-							"inconsistent", 1,
-							NULL);
-	g_signal_connect (G_OBJECT (renderer),
-			  "toggled", G_CALLBACK (sync_entries_changed_cb),
-			  source);
-	gtk_tree_view_append_column(GTK_TREE_VIEW (tree_view), col);
-
-	/* Second column */
-	renderer = gtk_cell_renderer_text_new ();
-	col = gtk_tree_view_column_new_with_attributes (NULL,
-							renderer,
-							"text", 3,
-							NULL);
-	gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), col);
-	gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view),
-				 GTK_TREE_MODEL (priv->sync_tree_store));
-	gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)),
-				    GTK_SELECTION_NONE);
-
-	update_sync_preview_bars (source);
+	container = GTK_CONTAINER (gtk_builder_get_object (builder, "sync-state-ui-container"));
+	gtk_box_pack_start (GTK_BOX (container), rb_sync_state_ui_new (priv->sync_state), TRUE, TRUE, 0);
+	gtk_widget_show_all (GTK_WIDGET (container));
 
 	gtk_widget_show (GTK_WIDGET (priv->properties_dialog));
 
 	g_object_unref (builder);
-	g_object_unref (playlist_manager);
-	g_object_unref (shell);
-	g_object_unref (db);
-}
-
-typedef struct {
-	GHashTable *target;
-	GList *result;
-} BuildSyncListData;
-
-static void
-build_sync_list_cb (char *uuid, RhythmDBEntry *entry, BuildSyncListData *data)
-{
-	if (!g_hash_table_lookup (data->target, uuid)) {
-		rb_debug ("adding %s (%" G_GINT64_FORMAT " bytes); id %s to sync list",
-			  rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
-			  rhythmdb_entry_get_uint64 (entry, RHYTHMDB_PROP_FILE_SIZE),
-			  uuid);
-		data->result = g_list_prepend (data->result, rhythmdb_entry_ref (entry));
-	}
 }
 
 
-static gboolean
-entry_is_undownloaded_podcast (RhythmDBEntry *entry)
-{
-	if (rhythmdb_entry_get_entry_type (entry) == RHYTHMDB_ENTRY_TYPE_PODCAST_POST) {
-		return (!rb_podcast_manager_entry_downloaded (entry));
-	}
-
-	return FALSE;
-}
-
-
-static void
-update_sync_space_needed (RBMediaPlayerSource *source)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	GList *list_iter;
-	gint64 add_size = 0;
-	gint64 remove_size = 0;
-
-	for (list_iter = priv->sync_to_add; list_iter; list_iter = list_iter->next) {
-		add_size += rhythmdb_entry_get_uint64 (list_iter->data, RHYTHMDB_PROP_FILE_SIZE);
-	}
-
-	for (list_iter = priv->sync_to_remove; list_iter; list_iter = list_iter->next) {
-		remove_size += rhythmdb_entry_get_uint64 (list_iter->data, RHYTHMDB_PROP_FILE_SIZE);
-	}
-
-	priv->sync_space_needed = get_capacity (source) - get_free_space (source);
-	rb_debug ("current space used: %" G_GINT64_FORMAT " bytes; adding %" G_GINT64_FORMAT ", removing %" G_GINT64_FORMAT,
-		  priv->sync_space_needed,
-		  add_size,
-		  remove_size);
-	priv->sync_space_needed = priv->sync_space_needed + add_size - remove_size;
-	rb_debug ("space used after sync: %" G_GINT64_FORMAT " bytes", priv->sync_space_needed);
-}
-
-static gboolean
-hash_table_insert_from_tree_model_cb (GtkTreeModel *query_model,
-				      GtkTreePath  *path,
-				      GtkTreeIter  *iter,
-				      GHashTable   *target)
-{
-	RhythmDBEntry *entry;
-
-	entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (query_model), iter);
-	if (!entry_is_undownloaded_podcast (entry)) {
-		g_hash_table_insert (target,
-				     make_track_uuid (entry),
-				     rhythmdb_entry_ref (entry));
-	}
-
-	return FALSE;
-}
-
-static void
-itinerary_insert_all_of_type (RhythmDB *db,
-			      RhythmDBEntryType entry_type,
-			      GHashTable *target)
-{
-	GtkTreeModel *query_model;
-
-	query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
-	rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
-				RHYTHMDB_QUERY_PROP_EQUALS,
-				RHYTHMDB_PROP_TYPE, entry_type,
-				RHYTHMDB_QUERY_END);
-
-	gtk_tree_model_foreach (query_model,
-				(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
-				target);
-}
-
-static void
-itinerary_insert_some_playlists (RBMediaPlayerSource *source,
-				 GHashTable *target)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	GList *list_iter;
-	GList *playlists;
-	RBShell *shell;
-
-	g_object_get (source, "shell", &shell, NULL);
-	playlists = rb_playlist_manager_get_playlists ((RBPlaylistManager *) rb_shell_get_playlist_manager (shell));
-	g_object_unref (shell);
-
-	for (list_iter = playlists; list_iter; list_iter = list_iter->next) {
-		gchar *name;
-
-		g_object_get (G_OBJECT (list_iter->data), "name", &name, NULL);
-
-		/* See if we should sync it */
-		if (rb_media_player_sync_settings_sync_group (priv->sync_settings, SYNC_CATEGORY_MUSIC, name)) {
-			GtkTreeModel *query_model;
-
-			rb_debug ("adding entries from playlist %s to itinerary", name);
-			g_object_get (RB_SOURCE (list_iter->data), "base-query-model", &query_model, NULL);
-			gtk_tree_model_foreach (query_model,
-						(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
-						target);
-			g_object_unref (query_model);
-		} else {
-			rb_debug ("not adding playlist %s to itinerary", name);
-		}
-
-		g_free (name);
-	}
-
-	g_list_free (playlists);
-}
-
-static void
-itinerary_insert_some_podcasts (RBMediaPlayerSource *source,
-				RhythmDB *db,
-				GHashTable *target)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	GList *podcasts;
-	GList *i;
-
-	podcasts = rb_media_player_sync_settings_get_enabled_groups (priv->sync_settings, SYNC_CATEGORY_PODCAST);
-	for (i = podcasts; i != NULL; i = i->next) {
-		GtkTreeModel *query_model;
-		rb_debug ("adding entries from podcast %s to itinerary", (char *)i->data);
-		query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
-		rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
-					RHYTHMDB_QUERY_PROP_EQUALS,
-					RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
-					RHYTHMDB_QUERY_PROP_EQUALS,
-					RHYTHMDB_PROP_SUBTITLE, i->data,
-					RHYTHMDB_QUERY_END);
-
-		/* TODO: exclude undownloaded episodes, sort by post date, set limit, optionally exclude things with play count > 0
-		 * RHYTHMDB_QUERY_PROP_NOT_EQUAL, RHYTHMDB_PROP_MOUNTPOINT, NULL,	(will this work?)
-		 * RHYTHMDB_QUERY_PROP_NOT_EQUAL, RHYTHMDB_PROP_STATUS, RHYTHMDB_PODCAST_STATUS_ERROR,
-		 *
-		 * RHYTHMDB_QUERY_PROP_EQUALS, RHYTHMDB_PROP_PLAYCOUNT, 0
-		 */
-
-		gtk_tree_model_foreach (query_model,
-					(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
-					target);
-		g_object_unref (query_model);
-	}
-}
-
-static guint64
-_sum_entry_size (GHashTable *entries)
-{
-	GHashTableIter iter;
-	gpointer key, value;
-	guint64 sum = 0;
-
-	g_hash_table_iter_init (&iter, entries);
-	while (g_hash_table_iter_next (&iter, &key, &value)) {
-		RhythmDBEntry *entry = (RhythmDBEntry *)value;
-		sum += rhythmdb_entry_get_uint64 (entry, RHYTHMDB_PROP_FILE_SIZE);
-	}
-
-	return sum;
-}
-
-
-static GHashTable *
-build_sync_itinerary (RBMediaPlayerSource *source)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	RBShell *shell;
-	RhythmDB *db;
-	GHashTable *itinerary;
-
-	rb_debug ("building itinerary hash");
-
-	g_object_get (source, "shell", &shell, NULL);
-	g_object_get (shell, "db", &db, NULL);
-
-	itinerary = g_hash_table_new_full (g_str_hash,
-					   g_str_equal,
-					   g_free,
-					   (GDestroyNotify)rhythmdb_entry_unref);
-
-	if (rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_MUSIC) ||
-	    rb_media_player_sync_settings_sync_group (priv->sync_settings, SYNC_CATEGORY_MUSIC, SYNC_GROUP_ALL_MUSIC)) {
-		rb_debug ("adding all music to the itinerary");
-		itinerary_insert_all_of_type (db, RHYTHMDB_ENTRY_TYPE_SONG, itinerary);
-	} else if (rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, SYNC_CATEGORY_MUSIC)) {
-		rb_debug ("adding selected playlists to the itinerary");
-		itinerary_insert_some_playlists (source, itinerary);
-	}
-
-	priv->sync_music_size = _sum_entry_size (itinerary);
-
-	if (rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
-		rb_debug ("adding all podcasts to the itinerary");
-		/* TODO: when we get #episodes/not-if-played settings, use
-		 * equivalent of insert_some_podcasts, iterating through all feeds
-		 * (use a query for all entries of type PODCAST_FEED to find them)
-		 */
-		itinerary_insert_all_of_type (db, RHYTHMDB_ENTRY_TYPE_PODCAST_POST, itinerary);
-	} else if (rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
-		rb_debug ("adding selected podcasts to the itinerary");
-		itinerary_insert_some_podcasts (source, db, itinerary);
-	}
-
-	priv->sync_podcast_size = _sum_entry_size (itinerary) - priv->sync_music_size;
-
-	g_object_unref (shell);
-	g_object_unref (db);
-
-	rb_debug ("finished building itinerary hash; has %d entries", g_hash_table_size (itinerary));
-	return itinerary;
-}
-
-static void
-_g_hash_table_transfer_all (GHashTable *target, GHashTable *source)
-{
-	GHashTableIter iter;
-	gpointer key, value;
-
-	g_hash_table_iter_init (&iter, source);
-	while (g_hash_table_iter_next (&iter, &key, &value)) {
-		g_hash_table_insert (target, key, value);
-		g_hash_table_iter_steal (&iter);
-	}
-}
-
-static GHashTable *
-build_device_state (RBMediaPlayerSource *source)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
-	GHashTable *device;
-	GHashTable *entries;
-
-	rb_debug ("building device contents hash");
-
-	device = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)rhythmdb_entry_unref);
-
-	rb_debug ("getting music entries from device");
-	entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) rhythmdb_entry_unref);
-	klass->impl_get_entries (source, SYNC_CATEGORY_MUSIC, entries);
-	priv->total_music_size = _sum_entry_size (entries);
-	if (rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, SYNC_CATEGORY_MUSIC)) {
-		_g_hash_table_transfer_all (device, entries);
-	}
-	g_hash_table_destroy (entries);
-	rb_debug ("done getting music entries from device");
-
-	rb_debug ("getting podcast entries from device");
-	entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) rhythmdb_entry_unref);
-	klass->impl_get_entries (source, SYNC_CATEGORY_PODCAST, entries);
-	priv->total_podcast_size = _sum_entry_size (entries);
-	if (rb_media_player_sync_settings_has_enabled_groups (priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
-		_g_hash_table_transfer_all (device, entries);
-	}
-	g_hash_table_destroy (entries);
-	rb_debug ("done getting podcast entries from device");
-
-	rb_debug ("done building device contents hash; has %d entries", g_hash_table_size (device));
-	return device;
-}
-
-static void
-update_sync (RBMediaPlayerSource *source)
-{
-	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	GHashTable *device;
-	GHashTable *itinerary;
-	BuildSyncListData data;
-
-	/* destroy existing sync lists */
-	rb_list_destroy_free (priv->sync_to_add, (GDestroyNotify) rhythmdb_entry_unref);
-	rb_list_destroy_free (priv->sync_to_remove, (GDestroyNotify) rhythmdb_entry_unref);
-	priv->sync_to_add = NULL;
-	priv->sync_to_remove = NULL;
-
-	/* figure out what we want on the device and what's already there */
-	itinerary = build_sync_itinerary (source);
-	device = build_device_state (source);
-
-	/* figure out what to add to the device */
-	rb_debug ("building list of files to transfer to device");
-	data.target = device;
-	data.result = NULL;
-	g_hash_table_foreach (itinerary, (GHFunc)build_sync_list_cb, &data);
-	priv->sync_to_add = data.result;
-	rb_debug ("decided to transfer %d files to the device", g_list_length (priv->sync_to_add));
-
-	/* and what to remove */
-	rb_debug ("building list of files to remove from device");
-	data.target = itinerary;
-	data.result = NULL;
-	g_hash_table_foreach (device, (GHFunc)build_sync_list_cb, &data);
-	priv->sync_to_remove = data.result;
-	rb_debug ("decided to remove %d files from the device", g_list_length (priv->sync_to_remove));
-
-	g_hash_table_destroy (device);
-	g_hash_table_destroy (itinerary);
-
-	update_sync_space_needed (source);
-}
 
 static void
 sync_playlists (RBMediaPlayerSource *source)
@@ -1170,7 +458,8 @@ sync_playlists (RBMediaPlayerSource *source)
 	/* build an updated device contents map, so we can find the device entries
 	 * corresponding to the entries in the local playlists.
 	 */
-	device = build_device_state (source);
+	device = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)rhythmdb_entry_unref);
+	rb_media_player_source_get_entries (source, SYNC_CATEGORY_MUSIC, device);
 
 	/* remove all playlists from the device, then add the synced playlists. */
 	klass->impl_remove_playlists (source);
@@ -1191,7 +480,7 @@ sync_playlists (RBMediaPlayerSource *source)
 
 		/* is this playlist selected for syncing? */
 		g_object_get (playlist_source, "name", &name, NULL);
-		if (rb_media_player_sync_settings_group_enabled (priv->sync_settings, SYNC_CATEGORY_MUSIC, name) == FALSE) {
+		if (rb_sync_settings_group_enabled (priv->sync_settings, SYNC_CATEGORY_MUSIC, name) == FALSE) {
 			rb_debug ("not syncing playlist %s", name);
 			g_free (name);
 			continue;
@@ -1212,7 +501,7 @@ sync_playlists (RBMediaPlayerSource *source)
 			RhythmDBEntry *device_entry;
 
 			entry = rhythmdb_query_model_iter_to_entry (model, &iter);
-			trackid = make_track_uuid (entry);
+			trackid = rb_sync_state_make_track_uuid (entry);
 
 			device_entry = g_hash_table_lookup (device, trackid);
 			if (device_entry != NULL) {
@@ -1271,10 +560,10 @@ track_add_done (RBMediaPlayerSource *source, RhythmDBEntry *entry)
 	 * if the set is now empty, trigger the next sync stage.
 	 */
 
-	l = g_list_find (priv->sync_to_add, entry);
+	l = g_list_find (priv->sync_add_remaining, entry);
 	if (l != NULL) {
-		priv->sync_to_add = g_list_remove_link (priv->sync_to_add, l);
-		if (priv->sync_to_add == NULL) {
+		priv->sync_add_remaining = g_list_remove_link (priv->sync_add_remaining, l);
+		if (priv->sync_add_remaining == NULL) {
 			rb_debug ("finished transferring files to the device");
 			g_idle_add ((GSourceFunc) sync_idle_cb_playlists, source);
 		}
@@ -1286,14 +575,15 @@ static void
 sync_delete_done_cb (RBMediaPlayerSource *source, gpointer dontcare)
 {
 	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	rb_debug ("finished deleting %d files from media player", g_list_length (priv->sync_to_remove));
-	rb_list_destroy_free (priv->sync_to_remove, (GDestroyNotify) rhythmdb_entry_unref);
-	priv->sync_to_remove = NULL;
+	rb_debug ("finished deleting %d files from media player", priv->sync_state->sync_remove_count);
 
 	/* Transfer needed tracks and podcasts from itinerary to device */
-	if (priv->sync_to_add != NULL) {
-		rb_debug ("transferring %d files from media player", g_list_length (priv->sync_to_add));
-		rb_source_paste (RB_SOURCE (source), priv->sync_to_add);
+	if (priv->sync_state->sync_add_count != 0) {
+
+		rb_debug ("transferring %d files to media player", priv->sync_state->sync_add_count);
+		priv->sync_add_remaining = g_list_copy (priv->sync_state->sync_to_add);
+		g_list_foreach (priv->sync_add_remaining, (GFunc) rhythmdb_entry_ref, NULL);
+		rb_source_paste (RB_SOURCE (source), priv->sync_add_remaining);
 	} else {
 		rb_debug ("no files to transfer to the device");
 		g_idle_add ((GSourceFunc) sync_idle_cb_playlists, source);
@@ -1304,9 +594,9 @@ static gboolean
 sync_idle_cb_delete_entries (RBMediaPlayerSource *source)
 {
 	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
-	rb_debug ("deleting %d files from media player", g_list_length (priv->sync_to_remove));
+	rb_debug ("deleting %d files from media player", priv->sync_state->sync_remove_count);
 	rb_media_player_source_delete_entries (source,
-					       priv->sync_to_remove,
+					       priv->sync_state->sync_to_remove,
 					       (RBMediaPlayerSourceDeleteCallback) sync_delete_done_cb,
 					       NULL,
 					       NULL);
@@ -1346,43 +636,11 @@ rb_media_player_source_sync (RBMediaPlayerSource *source)
 	g_idle_add ((GSourceFunc)sync_idle_cb_update_sync, source);
 }
 
-static char *
-make_track_uuid  (RhythmDBEntry *entry)
-{
-	/* This function is for hashing the two databases for syncing. */
-	GString *str = g_string_new ("");
-	char *result;
-
-	/*
-	 * possible improvements here:
-	 * - use musicbrainz track ID if known (maybe not a great idea?)
-	 * - fuzz the duration a bit (round to nearest 5 seconds?) to catch slightly
-	 *   different encodings of the same track
-	 * - maybe don't include genre, since there's no canonical genre for anything
-	 */
-
-	g_string_printf (str, "%s%s%s%s%lu%lu%lu",
-			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE),
-			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST),
-			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_GENRE),
-			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM),
-			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_DURATION),
-			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_TRACK_NUMBER),
-			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_DISC_NUMBER));
-
-	/* not sure why we md5 this.  how does it help? */
-	result = g_compute_checksum_for_string (G_CHECKSUM_MD5, str->str, str->len);
-
-	g_string_free (str, TRUE);
-
-	return result;
-}
-
 void
 _rb_media_player_source_add_to_map (GHashTable *map, RhythmDBEntry *entry)
 {
 	g_hash_table_insert (map,
-			     make_track_uuid (entry),
+			     rb_sync_state_make_track_uuid (entry),
 			     rhythmdb_entry_ref (entry));
 }
 
diff --git a/sources/rb-media-player-source.h b/sources/rb-media-player-source.h
index a35f2cd..4c802d0 100644
--- a/sources/rb-media-player-source.h
+++ b/sources/rb-media-player-source.h
@@ -77,6 +77,12 @@ void	rb_media_player_source_init_actions	(RBShell *shell);
 
 void	rb_media_player_source_load		(RBMediaPlayerSource *source);
 
+guint64 rb_media_player_source_get_capacity	(RBMediaPlayerSource *source);
+guint64 rb_media_player_source_get_free_space	(RBMediaPlayerSource *source);
+void	rb_media_player_source_get_entries	(RBMediaPlayerSource *source,
+						 const char *category,	/* defined in rb-sync-settings.h */
+						 GHashTable *entries);
+
 void	rb_media_player_source_delete_entries	(RBMediaPlayerSource *source,
 						 GList *entries,
 						 RBMediaPlayerSourceDeleteCallback callback,
diff --git a/sources/sync/Makefile.am b/sources/sync/Makefile.am
new file mode 100644
index 0000000..bbfe562
--- /dev/null
+++ b/sources/sync/Makefile.am
@@ -0,0 +1,33 @@
+NULL =
+
+noinst_LTLIBRARIES = libsourcesync.la
+
+libsourcesync_la_SOURCES =		\
+	rb-sync-settings.c		\
+	rb-sync-settings.h		\
+	rb-sync-settings-ui.c		\
+	rb-sync-settings-ui.h		\
+	rb-sync-state.c			\
+	rb-sync-state.h			\
+	rb-sync-state-ui.c		\
+	rb-sync-state-ui.h		\
+	$(NULL)
+
+INCLUDES =						\
+        -DGNOMELOCALEDIR=\""$(datadir)/locale"\"        \
+	-DG_LOG_DOMAIN=\"Rhythmbox\"		 	\
+	-I$(top_srcdir) 				\
+	-I$(top_srcdir)/lib 				\
+	-I$(top_builddir)/lib 				\
+	-I$(top_srcdir)/rhythmdb			\
+	-I$(top_srcdir)/podcast				\
+	-I$(top_srcdir)/shell				\
+	-I$(top_srcdir)/sources				\
+	-I$(top_srcdir)/widgets				\
+	-DPIXMAP_DIR=\""$(datadir)/pixmaps"\"		\
+	-DSHARE_DIR=\"$(pkgdatadir)\"                   \
+	-DDATADIR=\""$(datadir)"\"			\
+	$(RHYTHMBOX_CFLAGS)				\
+	$(NO_STRICT_ALIASING_CFLAGS)
+
+libsourcesync_la_LDFLAGS = -export-dynamic
diff --git a/sources/sync/rb-sync-settings-ui.c b/sources/sync/rb-sync-settings-ui.c
new file mode 100644
index 0000000..e7406d6
--- /dev/null
+++ b/sources/sync/rb-sync-settings-ui.c
@@ -0,0 +1,415 @@
+/*
+ *  Copyright (C) 2009 Paul Bellamy  <paul a bellamy gmail com>
+ *  Copyright (C) 2010 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "rb-sync-settings-ui.h"
+#include "rb-debug.h"
+#include "rb-util.h"
+#include "rb-playlist-manager.h"
+
+struct _RBSyncSettingsUIPrivate
+{
+	RBMediaPlayerSource *source;
+	RBSyncSettings *sync_settings;
+
+	GtkTreeStore *sync_tree_store;
+};
+
+enum {
+	PROP_0,
+	PROP_SOURCE,
+	PROP_SYNC_SETTINGS
+};
+
+G_DEFINE_TYPE (RBSyncSettingsUI, rb_sync_settings_ui, GTK_TYPE_VBOX)
+
+
+static void
+set_treeview_children (RBSyncSettingsUI *ui,
+		       GtkTreeIter *parent,
+		       const char *category,
+		       gboolean value,
+		       gboolean apply_to_settings)
+{
+	GtkTreeIter iter;
+	char *group;
+	gboolean valid;
+
+	valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (ui->priv->sync_tree_store), &iter, parent);
+
+	while (valid) {
+		gtk_tree_model_get (GTK_TREE_MODEL (ui->priv->sync_tree_store), &iter,
+				    2, &group,
+				    -1);
+
+		if (apply_to_settings) {
+			rb_sync_settings_set_group (ui->priv->sync_settings, category, group, value);
+		}
+		gtk_tree_store_set (ui->priv->sync_tree_store, &iter,
+		/* Active */	    0, value,
+				    -1);
+
+		g_free (group);
+		valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (ui->priv->sync_tree_store), &iter);
+	}
+}
+
+static void
+sync_entries_changed_cb (GtkCellRendererToggle *cell_renderer,
+			 gchar *path,
+			 RBSyncSettingsUI *ui)
+{
+	GtkTreeIter iter;
+	char *group;
+	char *category_name;
+	gboolean is_category;
+	gboolean value;
+
+	if (gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (ui->priv->sync_tree_store), &iter, path) == FALSE) {
+		return;
+	}
+
+	gtk_tree_model_get (GTK_TREE_MODEL (ui->priv->sync_tree_store),
+			    &iter,
+			    2, &group,
+			    4, &is_category,
+			    5, &category_name,
+			    -1);
+
+	value = !gtk_cell_renderer_toggle_get_active (cell_renderer);
+
+	if (is_category) {
+		rb_debug ("state for category %s changed to %d", category_name, value);
+		rb_sync_settings_set_category (ui->priv->sync_settings, category_name, value);
+		rb_sync_settings_clear_groups (ui->priv->sync_settings, category_name);
+
+		gtk_tree_store_set (ui->priv->sync_tree_store,
+				    &iter,
+				    0, value,
+				    1, FALSE,		/* category is no longer inconsistent */
+				    -1);
+		set_treeview_children (ui, &iter, category_name, value, FALSE);
+	} else {
+		gboolean parent_value;
+		gboolean parent_inconsistent;
+		GtkTreeIter parent;
+		rb_debug ("state for group %s in category %s changed to %d", group, category_name, value);
+
+		/* update parent state.  if the parent was previously enabled or disabled, then we
+		 * set all the other groups to that state before setting the row that was just changed,
+		 * and set the parent to inconsistent state.
+		 */
+		gtk_tree_model_iter_parent (GTK_TREE_MODEL (ui->priv->sync_tree_store), &parent, &iter);
+		gtk_tree_model_get (GTK_TREE_MODEL (ui->priv->sync_tree_store), &parent,
+				    0, &parent_value,
+				    1, &parent_inconsistent,
+				    -1);
+		if (parent_inconsistent == FALSE) {
+			/* category is now inconsistent */
+			rb_debug ("setting category %s to disabled, inconsistent", category_name);
+			rb_sync_settings_set_category (ui->priv->sync_settings, category_name, FALSE);
+			gtk_tree_store_set (ui->priv->sync_tree_store,
+					    &parent,
+					    0, FALSE,
+					    1, TRUE,
+					    -1);
+
+			/* set all groups in the category to the parent's state */
+			set_treeview_children (ui, &parent, category_name, parent_value, TRUE);
+		}
+
+		rb_sync_settings_set_group (ui->priv->sync_settings, category_name, group, value);
+		gtk_tree_store_set (ui->priv->sync_tree_store, &iter,
+				    0, value,
+				    -1);
+
+		/* if no groups are enabled, the category is no longer inconsistent */
+		if (value == FALSE && rb_sync_settings_has_enabled_groups (ui->priv->sync_settings, category_name) == FALSE) {
+			rb_debug ("no enabled groups left in category %s", category_name);
+			gtk_tree_store_set (ui->priv->sync_tree_store, &parent,
+					    1, FALSE,
+					    -1);
+		} else if (value == FALSE) {
+			rb_debug ("category %s still has some groups", category_name);
+		}
+	}
+
+	g_free (category_name);
+	g_free (group);
+}
+
+
+GtkWidget *
+rb_sync_settings_ui_new (RBMediaPlayerSource *source, RBSyncSettings *settings)
+{
+	GObject *ui;
+	ui = g_object_new (RB_TYPE_SYNC_SETTINGS_UI,
+			   "source", source,
+			   "sync-settings", settings,
+			   NULL);
+	return GTK_WIDGET (ui);
+}
+
+static void
+rb_sync_settings_ui_init (RBSyncSettingsUI *ui)
+{
+	ui->priv = G_TYPE_INSTANCE_GET_PRIVATE (ui, RB_TYPE_SYNC_SETTINGS_UI, RBSyncSettingsUIPrivate);
+}
+
+static void
+impl_constructed (GObject *object)
+{
+	RBSyncSettingsUI *ui = RB_SYNC_SETTINGS_UI (object);
+	GtkTreeIter tree_iter;
+	GtkTreeIter parent_iter;
+	GtkTreeModel *query_model;
+	GtkCellRenderer *renderer;
+	GtkTreeViewColumn *col;
+	GtkWidget *tree_view;
+	GList *l;
+	GList *playlists;
+	RBShell *shell;
+	RhythmDB *db;
+	RBPlaylistManager *playlist_manager;
+	gboolean valid;
+
+	g_object_get (ui->priv->source, "shell", &shell, NULL);
+	g_object_get (shell, "db", &db, "playlist-manager", &playlist_manager, NULL);
+
+	/* tree_store columns are: active, inconsistent, name, display-name, is-category, category name */
+	ui->priv->sync_tree_store = gtk_tree_store_new (6, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING);
+
+	/* music library parent */
+	gtk_tree_store_append (ui->priv->sync_tree_store, &parent_iter, NULL);
+	gtk_tree_store_set (ui->priv->sync_tree_store, &parent_iter,
+			    0, rb_sync_settings_sync_category (ui->priv->sync_settings, SYNC_CATEGORY_MUSIC),
+			    1, rb_sync_settings_has_enabled_groups (ui->priv->sync_settings, SYNC_CATEGORY_MUSIC),
+			    2, _("Music"),
+			    3, _("Music"),
+			    4, TRUE,
+			    5, SYNC_CATEGORY_MUSIC,
+			    -1);
+
+	/* 'all music' entry */
+	gtk_tree_store_append (ui->priv->sync_tree_store, &tree_iter, &parent_iter);
+	gtk_tree_store_set (ui->priv->sync_tree_store, &tree_iter,
+			    0, rb_sync_settings_sync_group (ui->priv->sync_settings, SYNC_CATEGORY_MUSIC, SYNC_GROUP_ALL_MUSIC),
+			    1, FALSE,
+			    2, SYNC_GROUP_ALL_MUSIC,
+			    3, _("All Music"),
+			    4, FALSE,
+			    5, SYNC_CATEGORY_MUSIC,
+			    -1);
+
+	/* playlist entries */
+	playlists = rb_playlist_manager_get_playlists (playlist_manager);
+	for (l = playlists; l != NULL; l = l->next) {
+		char *name;
+
+		gtk_tree_store_append (ui->priv->sync_tree_store, &tree_iter, &parent_iter);
+		/* set playlists data here */
+		g_object_get (l->data, "name", &name, NULL);
+
+		/* set this row's data */
+		gtk_tree_store_set (ui->priv->sync_tree_store, &tree_iter,
+				    0, rb_sync_settings_sync_group (ui->priv->sync_settings, SYNC_CATEGORY_MUSIC, name),
+				    1, FALSE,
+				    2, name,
+				    3, name,
+				    4, FALSE,
+				    5, SYNC_CATEGORY_MUSIC,
+				    -1);
+
+		g_free (name);
+	}
+
+	/* Append the Podcasts parent */
+	gtk_tree_store_append (ui->priv->sync_tree_store,
+			       &parent_iter,
+			       NULL);
+	gtk_tree_store_set (ui->priv->sync_tree_store, &parent_iter,
+			    0, rb_sync_settings_sync_category (ui->priv->sync_settings, SYNC_CATEGORY_PODCAST),
+			    1, rb_sync_settings_has_enabled_groups (ui->priv->sync_settings, SYNC_CATEGORY_PODCAST),
+			    2, _("Podcasts"),
+			    3, _("Podcasts"),
+			    4, TRUE,
+			    5, SYNC_CATEGORY_PODCAST,
+			    -1);
+
+	/* this really needs to use a live query model */
+	query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
+	rhythmdb_query_model_set_sort_order (RHYTHMDB_QUERY_MODEL (query_model),
+					     (GCompareDataFunc) rhythmdb_query_model_title_sort_func,
+					     NULL, NULL, FALSE);
+	rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
+				RHYTHMDB_QUERY_PROP_EQUALS,
+				RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_FEED,
+				RHYTHMDB_QUERY_END);
+	valid = gtk_tree_model_get_iter_first (query_model, &tree_iter);
+	while (valid) {
+		RhythmDBEntry *entry;
+		GtkTreeIter tree_iter2;
+		const char *name;
+		const char *feed;
+
+		entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (query_model), &tree_iter);
+		gtk_tree_store_append (ui->priv->sync_tree_store, &tree_iter2, &parent_iter);
+
+		/* set up this row */
+		name = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE);
+		feed = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
+		rb_debug ("adding feed %s (name %s)", feed, name);
+		gtk_tree_store_set (ui->priv->sync_tree_store, &tree_iter2,
+				    0, rb_sync_settings_sync_group (ui->priv->sync_settings, SYNC_CATEGORY_PODCAST, feed),
+				    1, FALSE,
+				    2, feed,
+				    3, name,
+				    4, FALSE,
+				    5, SYNC_CATEGORY_PODCAST,
+				    -1);
+
+		valid = gtk_tree_model_iter_next (query_model, &tree_iter);
+	}
+
+	/* Set up the treeview */
+	tree_view = gtk_tree_view_new ();
+	gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree_view), FALSE);
+	gtk_box_pack_start (GTK_BOX (ui), tree_view, TRUE, TRUE, 0);
+
+	/* First column */
+	renderer = gtk_cell_renderer_toggle_new ();
+	col = gtk_tree_view_column_new_with_attributes (NULL,
+							renderer,
+							"active", 0,
+							"inconsistent", 1,
+							NULL);
+	g_signal_connect (G_OBJECT (renderer),
+			  "toggled", G_CALLBACK (sync_entries_changed_cb),
+			  ui);
+	gtk_tree_view_append_column(GTK_TREE_VIEW (tree_view), col);
+
+	/* Second column */
+	renderer = gtk_cell_renderer_text_new ();
+	col = gtk_tree_view_column_new_with_attributes (NULL,
+							renderer,
+							"text", 3,
+							NULL);
+	gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), col);
+	gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view),
+				 GTK_TREE_MODEL (ui->priv->sync_tree_store));
+	gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)),
+				    GTK_SELECTION_NONE);
+
+	g_object_unref (playlist_manager);
+	g_object_unref (shell);
+	g_object_unref (db);
+
+	gtk_widget_show_all (GTK_WIDGET (ui));
+
+	RB_CHAIN_GOBJECT_METHOD(rb_sync_settings_ui_parent_class, constructed, object);
+}
+
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBSyncSettingsUI *ui = RB_SYNC_SETTINGS_UI (object);
+	switch (prop_id) {
+	case PROP_SOURCE:
+		ui->priv->source = g_value_get_object (value);
+		break;
+	case PROP_SYNC_SETTINGS:
+		ui->priv->sync_settings = g_value_get_object (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBSyncSettingsUI *ui = RB_SYNC_SETTINGS_UI (object);
+	switch (prop_id) {
+	case PROP_SOURCE:
+		g_value_set_object (value, ui->priv->source);
+		break;
+	case PROP_SYNC_SETTINGS:
+		g_value_set_object (value, ui->priv->sync_settings);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_dispose (GObject *object)
+{
+	RBSyncSettingsUI *ui = RB_SYNC_SETTINGS_UI (object);
+
+	if (ui->priv->sync_tree_store) {
+		g_object_unref (ui->priv->sync_tree_store);
+		ui->priv->sync_tree_store = NULL;
+	}
+
+	G_OBJECT_CLASS (rb_sync_settings_ui_parent_class)->dispose (object);
+}
+
+static void
+rb_sync_settings_ui_class_init (RBSyncSettingsUIClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->dispose = impl_dispose;
+	object_class->constructed = impl_constructed;
+
+	object_class->set_property = impl_set_property;
+	object_class->get_property = impl_get_property;
+
+	g_object_class_install_property (object_class,
+					 PROP_SOURCE,
+					 g_param_spec_object ("source",
+							      "source",
+							      "RBMediaPlayerSource instance",
+							      RB_TYPE_MEDIA_PLAYER_SOURCE,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+	g_object_class_install_property (object_class,
+					 PROP_SYNC_SETTINGS,
+					 g_param_spec_object ("sync-settings",
+							      "sync settings",
+							      "RBSyncSettings instance",
+							      RB_TYPE_SYNC_SETTINGS,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_type_class_add_private (object_class, sizeof (RBSyncSettingsUIPrivate));
+}
diff --git a/sources/sync/rb-sync-settings-ui.h b/sources/sync/rb-sync-settings-ui.h
new file mode 100644
index 0000000..144140c
--- /dev/null
+++ b/sources/sync/rb-sync-settings-ui.h
@@ -0,0 +1,67 @@
+/*
+ *  Copyright (C) 2010 Jonathan Matthew  <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef __RB_SYNC_SETTINGS_UI_H
+#define __RB_SYNC_SETTINGS_UI_H
+
+#include <gtk/gtk.h>
+
+#include "rb-media-player-source.h"
+#include "rb-sync-settings.h"
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_SYNC_SETTINGS_UI         (rb_sync_settings_ui_get_type ())
+#define RB_SYNC_SETTINGS_UI(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_SYNC_SETTINGS_UI, RBSyncSettingsUI))
+#define RB_SYNC_SETTINGS_UI_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_SYNC_SETTINGS_UI, RBSyncSettingsUIClass))
+#define RB_IS_SYNC_SETTINGS_UI(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_SYNC_SETTINGS_UI))
+#define RB_IS_SYNC_SETTINGS_UI_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_SYNC_SETTINGS_UI))
+#define RB_SYNC_SETTINGS_UI_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_SYNC_SETTINGS_UI, RBSyncSettingsUIClass))
+
+typedef struct _RBSyncSettingsUI RBSyncSettingsUI;
+typedef struct _RBSyncSettingsUIClass RBSyncSettingsUIClass;
+typedef struct _RBSyncSettingsUIPrivate RBSyncSettingsUIPrivate;
+
+struct _RBSyncSettingsUI
+{
+	GtkVBox parent;
+
+	RBSyncSettingsUIPrivate *priv;
+};
+
+struct _RBSyncSettingsUIClass
+{
+	GtkVBoxClass parent_class;
+};
+
+GType			rb_sync_settings_ui_get_type (void);
+
+GtkWidget *		rb_sync_settings_ui_new (RBMediaPlayerSource *source, RBSyncSettings *settings);
+
+G_END_DECLS
+
+#endif /* __RB_SYNC_SETTINGS_UI_H */
diff --git a/sources/rb-media-player-sync-settings.c b/sources/sync/rb-sync-settings.c
similarity index 73%
rename from sources/rb-media-player-sync-settings.c
rename to sources/sync/rb-sync-settings.c
index 9e6636f..86d6633 100644
--- a/sources/rb-media-player-sync-settings.c
+++ b/sources/sync/rb-sync-settings.c
@@ -51,7 +51,7 @@
 #include <gio/gio.h>
 #include <string.h>
 
-#include "rb-media-player-sync-settings.h"
+#include "rb-sync-settings.h"
 #include "rb-debug.h"
 #include "rb-util.h"
 
@@ -64,31 +64,38 @@ typedef struct {
 	char *key_file_path;
 
 	guint save_key_file_id;
-} RBMediaPlayerSyncSettingsPrivate;
+} RBSyncSettingsPrivate;
 
 enum {
 	PROP_0,
 	PROP_KEYFILE_PATH
 };
 
-G_DEFINE_TYPE (RBMediaPlayerSyncSettings, rb_media_player_sync_settings, G_TYPE_OBJECT)
+enum {
+	UPDATED,
+	LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
 
-#define GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS, RBMediaPlayerSyncSettingsPrivate))
+G_DEFINE_TYPE (RBSyncSettings, rb_sync_settings, G_TYPE_OBJECT)
 
-RBMediaPlayerSyncSettings *
-rb_media_player_sync_settings_new (const char *keyfile)
+#define GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_SYNC_SETTINGS, RBSyncSettingsPrivate))
+
+RBSyncSettings *
+rb_sync_settings_new (const char *keyfile)
 {
 	GObject *settings;
-	settings = g_object_new (RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS,
+	settings = g_object_new (RB_TYPE_SYNC_SETTINGS,
 				 "keyfile-path", keyfile,
 				 NULL);
-	return RB_MEDIA_PLAYER_SYNC_SETTINGS (settings);
+	return RB_SYNC_SETTINGS (settings);
 }
 
 gboolean
-rb_media_player_sync_settings_save (RBMediaPlayerSyncSettings *settings)
+rb_sync_settings_save (RBSyncSettings *settings)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	char *data;
 	gsize length;
 	GError *error = NULL;
@@ -113,18 +120,20 @@ rb_media_player_sync_settings_save (RBMediaPlayerSyncSettings *settings)
 }
 
 static gboolean
-_save_idle_cb (RBMediaPlayerSyncSettings *settings)
+_save_idle_cb (RBSyncSettings *settings)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	priv->save_key_file_id = 0;
-	rb_media_player_sync_settings_save (settings);
+	rb_sync_settings_save (settings);
+
+	g_signal_emit (settings, signals[UPDATED], 0);
 	return FALSE;
 }
 
 static void
-_save_idle (RBMediaPlayerSyncSettings *settings)
+_save_idle (RBSyncSettings *settings)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	if (priv->save_key_file_id == 0) {
 		priv->save_key_file_id = g_idle_add ((GSourceFunc) _save_idle_cb, settings);
 	}
@@ -144,27 +153,27 @@ _get_boolean_with_default (GKeyFile *keyfile, const char *group, const char *key
 }
 
 void
-rb_media_player_sync_settings_set_category (RBMediaPlayerSyncSettings *settings,
+rb_sync_settings_set_category (RBSyncSettings *settings,
 					    const char *category,
 					    gboolean enabled)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	g_key_file_set_boolean (priv->key_file, category, CATEGORY_ENABLED_KEY, enabled);
 	_save_idle (settings);
 }
 
 gboolean
-rb_media_player_sync_settings_sync_category (RBMediaPlayerSyncSettings *settings,
+rb_sync_settings_sync_category (RBSyncSettings *settings,
 					     const char *category)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	return _get_boolean_with_default (priv->key_file, category, CATEGORY_ENABLED_KEY, FALSE);
 }
 
 GList *
-rb_media_player_sync_settings_get_enabled_categories (RBMediaPlayerSyncSettings *settings)
+rb_sync_settings_get_enabled_categories (RBSyncSettings *settings)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	char **groups;
 	GList *categories;
 	int i;
@@ -184,12 +193,12 @@ rb_media_player_sync_settings_get_enabled_categories (RBMediaPlayerSyncSettings
 }
 
 void
-rb_media_player_sync_settings_set_group (RBMediaPlayerSyncSettings *settings,
+rb_sync_settings_set_group (RBSyncSettings *settings,
 					 const char *category,
 					 const char *group,
 					 gboolean enabled)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	char **groups;
 	int ngroups;
 
@@ -236,11 +245,11 @@ rb_media_player_sync_settings_set_group (RBMediaPlayerSyncSettings *settings,
 }
 
 gboolean
-rb_media_player_sync_settings_group_enabled (RBMediaPlayerSyncSettings *settings,
+rb_sync_settings_group_enabled (RBSyncSettings *settings,
 					     const char *category,
 					     const char *group)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	char **groups;
 	int i;
 	gboolean found = FALSE;
@@ -262,21 +271,21 @@ rb_media_player_sync_settings_group_enabled (RBMediaPlayerSyncSettings *settings
 }
 
 gboolean
-rb_media_player_sync_settings_sync_group (RBMediaPlayerSyncSettings *settings,
+rb_sync_settings_sync_group (RBSyncSettings *settings,
 					  const char *category,
 					  const char *group)
 {
-	if (rb_media_player_sync_settings_sync_category (settings, category) == TRUE) {
+	if (rb_sync_settings_sync_category (settings, category) == TRUE) {
 		return TRUE;
 	}
-	return rb_media_player_sync_settings_group_enabled (settings, category, group);
+	return rb_sync_settings_group_enabled (settings, category, group);
 }
 
 gboolean
-rb_media_player_sync_settings_has_enabled_groups (RBMediaPlayerSyncSettings *settings,
+rb_sync_settings_has_enabled_groups (RBSyncSettings *settings,
 						  const char *category)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	char **groups;
 
 	groups = g_key_file_get_string_list (priv->key_file, category, CATEGORY_GROUPS_KEY, NULL, NULL);
@@ -289,10 +298,10 @@ rb_media_player_sync_settings_has_enabled_groups (RBMediaPlayerSyncSettings *set
 }
 
 GList *
-rb_media_player_sync_settings_get_enabled_groups (RBMediaPlayerSyncSettings *settings,
+rb_sync_settings_get_enabled_groups (RBSyncSettings *settings,
 						  const char *category)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	char **groups;
 	GList *glist = NULL;
 	int i;
@@ -311,9 +320,9 @@ rb_media_player_sync_settings_get_enabled_groups (RBMediaPlayerSyncSettings *set
 }
 
 void
-rb_media_player_sync_settings_clear_groups (RBMediaPlayerSyncSettings *settings, const char *category)
+rb_sync_settings_clear_groups (RBSyncSettings *settings, const char *category)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (settings);
 	g_key_file_remove_key (priv->key_file, category, CATEGORY_GROUPS_KEY, NULL);
 	_save_idle (settings);
 }
@@ -321,7 +330,7 @@ rb_media_player_sync_settings_clear_groups (RBMediaPlayerSyncSettings *settings,
 
 
 static void
-rb_media_player_sync_settings_init (RBMediaPlayerSyncSettings *settings)
+rb_sync_settings_init (RBSyncSettings *settings)
 {
 	/* nothing */
 }
@@ -329,7 +338,7 @@ rb_media_player_sync_settings_init (RBMediaPlayerSyncSettings *settings)
 static void
 impl_constructed (GObject *object)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (object);
 	GError *error = NULL;
 
 	priv->key_file = g_key_file_new ();
@@ -346,13 +355,13 @@ impl_constructed (GObject *object)
 		 */
 	}
 
-	RB_CHAIN_GOBJECT_METHOD(rb_media_player_sync_settings_parent_class, constructed, object);
+	RB_CHAIN_GOBJECT_METHOD(rb_sync_settings_parent_class, constructed, object);
 }
 
 static void
 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (object);
 	switch (prop_id) {
 	case PROP_KEYFILE_PATH:
 		priv->key_file_path = g_value_dup_string (value);
@@ -366,7 +375,7 @@ impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSp
 static void
 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (object);
 	switch (prop_id) {
 	case PROP_KEYFILE_PATH:
 		g_value_set_string (value, priv->key_file_path);
@@ -380,31 +389,31 @@ impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *ps
 static void
 impl_dispose (GObject *object)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (object);
 
 	/* if a save is pending, do it now */
 	if (priv->save_key_file_id != 0) {
 		g_source_remove (priv->save_key_file_id);
 		priv->save_key_file_id = 0;
-		rb_media_player_sync_settings_save (RB_MEDIA_PLAYER_SYNC_SETTINGS (object));
+		rb_sync_settings_save (RB_SYNC_SETTINGS (object));
 	}
 
-	G_OBJECT_CLASS (rb_media_player_sync_settings_parent_class)->dispose (object);
+	G_OBJECT_CLASS (rb_sync_settings_parent_class)->dispose (object);
 }
 
 static void
 impl_finalize (GObject *object)
 {
-	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+	RBSyncSettingsPrivate *priv = GET_PRIVATE (object);
 
 	g_key_file_free (priv->key_file);
 	g_free (priv->key_file_path);
 
-	G_OBJECT_CLASS (rb_media_player_sync_settings_parent_class)->finalize (object);
+	G_OBJECT_CLASS (rb_sync_settings_parent_class)->finalize (object);
 }
 
 static void
-rb_media_player_sync_settings_class_init (RBMediaPlayerSyncSettingsClass *klass)
+rb_sync_settings_class_init (RBSyncSettingsClass *klass)
 {
 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
@@ -422,5 +431,13 @@ rb_media_player_sync_settings_class_init (RBMediaPlayerSyncSettingsClass *klass)
 							      "path to the key file storing the sync settings",
 							      NULL,
 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
-	g_type_class_add_private (object_class, sizeof (RBMediaPlayerSyncSettingsPrivate));
+	signals[UPDATED] = g_signal_new ("updated",
+					 RB_TYPE_SYNC_SETTINGS,
+					 G_SIGNAL_RUN_LAST,
+					 G_STRUCT_OFFSET (RBSyncSettingsClass, updated),
+					 NULL, NULL,
+					 g_cclosure_marshal_VOID__VOID,
+					 G_TYPE_NONE,
+					 0);
+	g_type_class_add_private (object_class, sizeof (RBSyncSettingsPrivate));
 }
diff --git a/sources/sync/rb-sync-settings.h b/sources/sync/rb-sync-settings.h
new file mode 100644
index 0000000..1d70235
--- /dev/null
+++ b/sources/sync/rb-sync-settings.h
@@ -0,0 +1,99 @@
+/*
+ *  Copyright (C) 2009 Paul Bellamy <paul a bellamy gmail com>
+ *  Copyright (C) 2009 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef RB_SYNC_SETTINGS__H
+#define RB_SYNC_SETTINGS__H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_SYNC_SETTINGS         (rb_sync_settings_get_type ())
+#define RB_SYNC_SETTINGS(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_SYNC_SETTINGS, RBSyncSettings))
+#define RB_SYNC_SETTINGS_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_SYNC_SETTINGS, RBSyncSettingsClass))
+#define RB_IS_SYNC_SETTINGS(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_SYNC_SETTINGS))
+#define RB_IS_SYNC_SETTINGS_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_SYNC_SETTINGS))
+#define RB_SYNC_SETTINGS_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_SYNC_SETTINGS, RBSyncSettingsClass))
+
+/* defined sync categories */
+#define		SYNC_CATEGORY_MUSIC		"music"
+#define 	SYNC_CATEGORY_PODCAST		"podcast"
+
+/* defined sync groups */
+#define		SYNC_GROUP_ALL_MUSIC		"x-rb-all-music"
+
+typedef struct
+{
+	GObject parent;
+} RBSyncSettings;
+
+typedef struct
+{
+	GObjectClass parent;
+
+	/* signals */
+	void	(*updated) (RBSyncSettings *settings);
+} RBSyncSettingsClass;
+
+GType			rb_sync_settings_get_type (void);
+
+RBSyncSettings *	rb_sync_settings_new (const char *keyfile);
+
+gboolean		rb_sync_settings_save (RBSyncSettings *settings);
+
+/* sync categories */
+
+void			rb_sync_settings_set_category (RBSyncSettings *settings,
+						       const char *category,
+						       gboolean enabled);
+gboolean		rb_sync_settings_sync_category (RBSyncSettings *settings,
+							const char *category);
+GList *			rb_sync_settings_get_enabled_categories (RBSyncSettings *settings);
+
+/* sync category groups */
+
+void			rb_sync_settings_set_group (RBSyncSettings *settings,
+						    const char *category,
+						    const char *group,
+						    gboolean enabled);
+gboolean		rb_sync_settings_group_enabled (RBSyncSettings *settings,
+							const char *category,
+							const char *group);
+gboolean		rb_sync_settings_sync_group (RBSyncSettings *settings,
+						     const char *category,
+						     const char *group);
+gboolean		rb_sync_settings_has_enabled_groups (RBSyncSettings *settings,
+							     const char *category);
+GList *			rb_sync_settings_get_enabled_groups (RBSyncSettings *settings,
+							     const char *category);
+void			rb_sync_settings_clear_groups (RBSyncSettings *settings,
+						       const char *category);
+
+G_END_DECLS
+
+#endif /* RB_SYNC_SETTINGS__H */
diff --git a/sources/sync/rb-sync-state-ui.c b/sources/sync/rb-sync-state-ui.c
new file mode 100644
index 0000000..75214a6
--- /dev/null
+++ b/sources/sync/rb-sync-state-ui.c
@@ -0,0 +1,332 @@
+/*
+ *  Copyright (C) 2010 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "rb-sync-state-ui.h"
+#include "rb-debug.h"
+#include "rb-util.h"
+#include "rb-file-helpers.h"
+#include "rb-builder-helpers.h"
+
+#include "rb-segmented-bar.h"
+
+struct _RBSyncStateUIPrivate
+{
+	/* we don't own a reference on this (maybe we should?) */
+	RBSyncState *state;
+
+	/* or any of these */
+	GtkWidget *add_count;
+	GtkWidget *remove_count;
+	RBSyncBarData sync_before;
+	RBSyncBarData sync_after;
+};
+
+enum {
+	PROP_0,
+	PROP_SYNC_STATE
+};
+
+G_DEFINE_TYPE (RBSyncStateUI, rb_sync_state_ui, GTK_TYPE_VBOX)
+
+
+static char *
+value_formatter (gdouble percent, gpointer data)
+{
+	gsize total_size = GPOINTER_TO_SIZE (data);
+	return g_format_size_for_display (percent * total_size);
+}
+
+void
+rb_sync_state_ui_create_bar (RBSyncBarData *bar, guint64 capacity, GtkWidget *label)
+{
+	bar->widget = rb_segmented_bar_new ();
+	g_object_set (bar->widget, "show-labels", TRUE, NULL);
+
+	rb_segmented_bar_set_value_formatter (RB_SEGMENTED_BAR (bar->widget),
+					      value_formatter,
+					      GSIZE_TO_POINTER (capacity));
+
+	bar->music_segment = rb_segmented_bar_add_segment (RB_SEGMENTED_BAR (bar->widget),_("Music"), 0.0, 0.2, 0.4, 0.65, 1.0);
+	bar->podcast_segment = rb_segmented_bar_add_segment (RB_SEGMENTED_BAR (bar->widget), _("Podcasts"), 0.0, 0.96, 0.47, 0.0, 1.0);
+	bar->other_segment = rb_segmented_bar_add_segment (RB_SEGMENTED_BAR (bar->widget), _("Other"), 0.0, 0.45, 0.82, 0.08, 1.0);
+	bar->free_segment = rb_segmented_bar_add_segment_default_color (RB_SEGMENTED_BAR (bar->widget), _("Available"), 1.0);
+
+	/* set up label relationship */
+	if (label != NULL) {
+		AtkObject *lobj;
+		AtkObject *robj;
+
+		lobj = gtk_widget_get_accessible (label);
+		robj = gtk_widget_get_accessible (bar->widget);
+
+		atk_object_add_relationship (lobj, ATK_RELATION_LABEL_FOR, robj);
+		atk_object_add_relationship (robj, ATK_RELATION_LABELLED_BY, lobj);
+	}
+}
+
+
+void
+rb_sync_state_ui_update_volume_usage (RBSyncBarData *bar, RBSyncState *state)
+{
+	RBMediaPlayerSource *source;
+	double fraction;
+	guint64 capacity;
+	guint64 total_other;
+	guint64 free_space;
+
+	g_object_get (state, "source", &source, NULL);
+	capacity = rb_media_player_source_get_capacity (source);
+	free_space = rb_media_player_source_get_free_space (source);
+	g_object_unref (source);
+
+	total_other = capacity - (free_space + state->total_music_size + state->total_podcast_size);
+
+	fraction = (double)state->total_music_size/(double)capacity;
+	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
+					 bar->music_segment,
+					 fraction);
+	fraction = (double)state->total_podcast_size/(double)capacity;
+	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
+					 bar->podcast_segment,
+					 fraction);
+	fraction = (double)total_other/(double)capacity;
+	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
+					 bar->other_segment,
+					 fraction);
+	fraction = (double)free_space/(double)capacity;
+	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
+					 bar->free_segment,
+					 fraction);
+}
+
+static void
+update_sync_after_bar (RBSyncBarData *bar, RBSyncState *state)
+{
+	RBMediaPlayerSource *source;
+	RBSyncSettings *settings;
+	double music_fraction;
+	double podcast_fraction;
+	double other_fraction;
+	double free_fraction;
+	guint64 total_other_size;
+	guint64 device_capacity;
+
+	g_object_get (state,
+		      "source", &source,
+		      "sync-settings", &settings,
+		      NULL);
+	device_capacity = rb_media_player_source_get_capacity (source);
+
+	if (rb_sync_settings_has_enabled_groups (settings, SYNC_CATEGORY_MUSIC) ||
+	    rb_sync_settings_sync_category (settings, SYNC_CATEGORY_MUSIC)) {
+		music_fraction = (double)state->sync_music_size / (double)device_capacity;
+	} else {
+		music_fraction = (double)state->total_music_size / (double)device_capacity;
+	}
+	if (rb_sync_settings_has_enabled_groups (settings, SYNC_CATEGORY_PODCAST) ||
+	    rb_sync_settings_sync_category (settings, SYNC_CATEGORY_PODCAST)) {
+		podcast_fraction = (double)state->sync_podcast_size / (double)device_capacity;
+	} else {
+		podcast_fraction = (double)state->total_podcast_size / (double)device_capacity;
+	}
+
+	total_other_size = device_capacity - (rb_media_player_source_get_free_space (source) + state->total_music_size + state->total_podcast_size);
+	other_fraction = (double)total_other_size / (double)device_capacity;
+
+	free_fraction = 1.0 - (music_fraction + podcast_fraction + other_fraction);
+	if (free_fraction < 0.0) {
+		free_fraction = 0.0;
+	}
+
+	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
+					 bar->music_segment,
+					 music_fraction);
+	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
+					 bar->podcast_segment,
+					 podcast_fraction);
+	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
+					 bar->other_segment,
+					 other_fraction);
+	rb_segmented_bar_update_segment (RB_SEGMENTED_BAR (bar->widget),
+					 bar->free_segment,
+					 free_fraction);
+
+	g_object_unref (source);
+	g_object_unref (settings);
+}
+
+static void
+sync_state_updated (RBSyncState *state, RBSyncStateUI *ui)
+{
+	char *text;
+	rb_debug ("sync state updated");
+
+	/* sync before state */
+	rb_sync_state_ui_update_volume_usage (&ui->priv->sync_before, state);
+	update_sync_after_bar (&ui->priv->sync_after, state);
+
+	/* other stuff */
+	text = g_strdup_printf ("%d", state->sync_add_count);
+	gtk_label_set_text (GTK_LABEL (ui->priv->add_count), text);
+	g_free (text);
+
+	text = g_strdup_printf ("%d", state->sync_remove_count);
+	gtk_label_set_text (GTK_LABEL (ui->priv->remove_count), text);
+	g_free (text);
+}
+
+
+GtkWidget *
+rb_sync_state_ui_new (RBSyncState *state)
+{
+	GObject *ui;
+	ui = g_object_new (RB_TYPE_SYNC_STATE_UI,
+			   "sync-state", state,
+			   NULL);
+	return GTK_WIDGET (ui);
+}
+
+static void
+rb_sync_state_ui_init (RBSyncStateUI *ui)
+{
+	ui->priv = G_TYPE_INSTANCE_GET_PRIVATE (ui, RB_TYPE_SYNC_STATE_UI, RBSyncStateUIPrivate);
+}
+
+static void
+build_ui (RBSyncStateUI *ui)
+{
+	RBMediaPlayerSource *source;
+	GtkWidget *widget;
+	GtkWidget *container;
+	guint64 capacity;
+	GtkBuilder *builder;
+	const char *ui_file;
+
+	g_object_get (ui->priv->state, "source", &source, NULL);
+	capacity = rb_media_player_source_get_capacity (source);
+	g_object_unref (source);
+
+	ui_file = rb_file ("sync-state.ui");
+	if (ui_file == NULL) {
+		g_warning ("Couldn't find sync-state.ui");
+		return;
+	}
+
+	builder = rb_builder_load (ui_file, NULL);
+	if (builder == NULL) {
+		g_warning ("Couldn't load sync-state.ui");
+		return;
+	}
+
+	container = GTK_WIDGET (gtk_builder_get_object (builder, "sync-state-ui"));
+	gtk_box_pack_start (GTK_BOX (ui), container, TRUE, TRUE, 0);
+
+	ui->priv->add_count = GTK_WIDGET (gtk_builder_get_object (builder, "added-tracks"));
+	ui->priv->remove_count = GTK_WIDGET (gtk_builder_get_object (builder, "removed-tracks"));
+
+	widget = GTK_WIDGET (gtk_builder_get_object (builder, "sync-before-label"));
+	rb_sync_state_ui_create_bar (&ui->priv->sync_before, capacity, widget);
+	container = GTK_WIDGET (gtk_builder_get_object (builder, "sync-before-container"));
+	gtk_container_add (GTK_CONTAINER (container), ui->priv->sync_before.widget);
+
+	widget = GTK_WIDGET (gtk_builder_get_object (builder, "sync-after-label"));
+	rb_sync_state_ui_create_bar (&ui->priv->sync_after, capacity, widget);
+	container = GTK_WIDGET (gtk_builder_get_object (builder, "sync-after-container"));
+	gtk_container_add (GTK_CONTAINER (container), ui->priv->sync_after.widget);
+
+	g_object_unref (builder);
+}
+
+static void
+impl_constructed (GObject *object)
+{
+	RBSyncStateUI *ui = RB_SYNC_STATE_UI (object);
+
+	build_ui (ui);
+	sync_state_updated (ui->priv->state, ui);
+
+	g_signal_connect_object (ui->priv->state,
+				 "updated",
+				 G_CALLBACK (sync_state_updated),
+				 ui, 0);
+
+	RB_CHAIN_GOBJECT_METHOD(rb_sync_state_ui_parent_class, constructed, object);
+}
+
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBSyncStateUI *ui = RB_SYNC_STATE_UI (object);
+	switch (prop_id) {
+	case PROP_SYNC_STATE:
+		ui->priv->state = g_value_get_object (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBSyncStateUI *ui = RB_SYNC_STATE_UI (object);
+	switch (prop_id) {
+	case PROP_SYNC_STATE:
+		g_value_set_object (value, ui->priv->state);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+
+static void
+rb_sync_state_ui_class_init (RBSyncStateUIClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->constructed = impl_constructed;
+	object_class->set_property = impl_set_property;
+	object_class->get_property = impl_get_property;
+
+	g_object_class_install_property (object_class,
+					 PROP_SYNC_STATE,
+					 g_param_spec_object ("sync-state",
+							      "sync-state",
+							      "RBSyncState instance",
+							      RB_TYPE_SYNC_STATE,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_type_class_add_private (object_class, sizeof (RBSyncStateUIPrivate));
+}
diff --git a/sources/sync/rb-sync-state-ui.h b/sources/sync/rb-sync-state-ui.h
new file mode 100644
index 0000000..c2d4f93
--- /dev/null
+++ b/sources/sync/rb-sync-state-ui.h
@@ -0,0 +1,83 @@
+/*
+ *  Copyright (C) 2010 Jonathan Matthew  <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef __RB_SYNC_STATE_UI_H
+#define __RB_SYNC_STATE_UI_H
+
+#include <gtk/gtk.h>
+
+#include "rb-sync-state.h"
+
+G_BEGIN_DECLS
+
+/* Segmented bar for device usage and sync before/after views */
+
+typedef struct _RBSyncBarData RBSyncBarData;
+
+struct _RBSyncBarData
+{
+	GtkWidget *widget;
+	guint music_segment;
+	guint podcast_segment;
+	guint other_segment;
+	guint free_segment;
+};
+
+void	rb_sync_state_ui_create_bar (RBSyncBarData *bar, guint64 capacity, GtkWidget *label);
+
+void	rb_sync_state_ui_update_volume_usage (RBSyncBarData *bar, RBSyncState *state);	/* hm */
+
+/* Sync state UI container */
+
+#define RB_TYPE_SYNC_STATE_UI         (rb_sync_state_ui_get_type ())
+#define RB_SYNC_STATE_UI(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_SYNC_STATE_UI, RBSyncStateUI))
+#define RB_SYNC_STATE_UI_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_SYNC_STATE_UI, RBSyncStateUIClass))
+#define RB_IS_SYNC_STATE_UI(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_SYNC_STATE_UI))
+#define RB_IS_SYNC_STATE_UI_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_SYNC_STATE_UI))
+#define RB_SYNC_STATE_UI_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_SYNC_STATE_UI, RBSyncStateUIClass))
+
+typedef struct _RBSyncStateUI RBSyncStateUI;
+typedef struct _RBSyncStateUIClass RBSyncStateUIClass;
+typedef struct _RBSyncStateUIPrivate RBSyncStateUIPrivate;
+
+struct _RBSyncStateUI {
+	GtkVBox parent;
+
+	RBSyncStateUIPrivate *priv;
+};
+
+struct _RBSyncStateUIClass {
+	GtkVBoxClass parent_class;
+};
+
+GType		rb_sync_state_ui_get_type (void);
+
+GtkWidget *	rb_sync_state_ui_new (RBSyncState *state);
+
+G_END_DECLS
+
+#endif /* __RB_SYNC_STATE_UI_H */
diff --git a/sources/sync/rb-sync-state.c b/sources/sync/rb-sync-state.c
new file mode 100644
index 0000000..6009d9f
--- /dev/null
+++ b/sources/sync/rb-sync-state.c
@@ -0,0 +1,552 @@
+/*
+ *  Copyright (C) 2010 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include "rb-sync-state.h"
+#include "rb-util.h"
+#include "rb-debug.h"
+
+#include "rhythmdb-query-model.h"
+#include "rb-podcast-manager.h"
+#include "rb-playlist-manager.h"
+#include "rb-shell.h"
+
+struct _RBSyncStatePrivate
+{
+	/* we don't own a reference on these */
+	RBMediaPlayerSource *source;
+	RBSyncSettings *sync_settings;
+};
+
+enum {
+	PROP_0,
+	PROP_SOURCE,
+	PROP_SYNC_SETTINGS
+};
+
+enum {
+	UPDATED,
+	LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (RBSyncState, rb_sync_state, G_TYPE_OBJECT)
+
+static gboolean
+entry_is_undownloaded_podcast (RhythmDBEntry *entry)
+{
+	if (rhythmdb_entry_get_entry_type (entry) == RHYTHMDB_ENTRY_TYPE_PODCAST_POST) {
+		return (!rb_podcast_manager_entry_downloaded (entry));
+	}
+
+	return FALSE;
+}
+
+static guint64
+_sum_entry_size (GHashTable *entries)
+{
+	GHashTableIter iter;
+	gpointer key, value;
+	guint64 sum = 0;
+
+	g_hash_table_iter_init (&iter, entries);
+	while (g_hash_table_iter_next (&iter, &key, &value)) {
+		RhythmDBEntry *entry = (RhythmDBEntry *)value;
+		sum += rhythmdb_entry_get_uint64 (entry, RHYTHMDB_PROP_FILE_SIZE);
+	}
+
+	return sum;
+}
+
+static void
+_g_hash_table_transfer_all (GHashTable *target, GHashTable *source)
+{
+	GHashTableIter iter;
+	gpointer key, value;
+
+	g_hash_table_iter_init (&iter, source);
+	while (g_hash_table_iter_next (&iter, &key, &value)) {
+		g_hash_table_insert (target, key, value);
+		g_hash_table_iter_steal (&iter);
+	}
+}
+
+char *
+rb_sync_state_make_track_uuid  (RhythmDBEntry *entry)
+{
+	/* This function is for hashing the two databases for syncing. */
+	GString *str = g_string_new ("");
+	char *result;
+
+	/*
+	 * possible improvements here:
+	 * - use musicbrainz track ID if known (maybe not a great idea?)
+	 * - fuzz the duration a bit (round to nearest 5 seconds?) to catch slightly
+	 *   different encodings of the same track
+	 * - maybe don't include genre, since there's no canonical genre for anything
+	 */
+
+	g_string_printf (str, "%s%s%s%s%lu%lu%lu",
+			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE),
+			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST),
+			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_GENRE),
+			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM),
+			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_DURATION),
+			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_TRACK_NUMBER),
+			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_DISC_NUMBER));
+
+	/* not sure why we md5 this.  how does it help? */
+	result = g_compute_checksum_for_string (G_CHECKSUM_MD5, str->str, str->len);
+
+	g_string_free (str, TRUE);
+
+	return result;
+}
+
+static void
+free_sync_lists (RBSyncState *state)
+{
+	rb_list_destroy_free (state->sync_to_add, (GDestroyNotify) rhythmdb_entry_unref);
+	rb_list_destroy_free (state->sync_to_remove, (GDestroyNotify) rhythmdb_entry_unref);
+	state->sync_to_add = NULL;
+	state->sync_to_remove = NULL;
+}
+
+typedef struct {
+	GHashTable *target;
+	GList *result;
+	guint64 bytes;
+	guint64 duration;
+} BuildSyncListData;
+
+static void
+build_sync_list_cb (char *uuid, RhythmDBEntry *entry, BuildSyncListData *data)
+{
+	guint64 bytes;
+	gulong duration;
+
+	if (g_hash_table_lookup (data->target, uuid) != NULL) {
+		/* already present */
+		return;
+	}
+
+	bytes = rhythmdb_entry_get_uint64 (entry, RHYTHMDB_PROP_FILE_SIZE);
+	duration = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DURATION);
+	rb_debug ("adding %s (%" G_GINT64_FORMAT " bytes); id %s to sync list",
+		  rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
+		  bytes,
+		  uuid);
+	data->bytes += bytes;
+	data->duration += duration;
+	data->result = g_list_prepend (data->result, rhythmdb_entry_ref (entry));
+}
+
+
+static gboolean
+hash_table_insert_from_tree_model_cb (GtkTreeModel *query_model,
+				      GtkTreePath  *path,
+				      GtkTreeIter  *iter,
+				      GHashTable   *target)
+{
+	RhythmDBEntry *entry;
+
+	entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (query_model), iter);
+	if (!entry_is_undownloaded_podcast (entry)) {
+		g_hash_table_insert (target,
+				     rb_sync_state_make_track_uuid (entry),
+				     rhythmdb_entry_ref (entry));
+	}
+
+	return FALSE;
+}
+
+static void
+itinerary_insert_all_of_type (RhythmDB *db,
+			      RhythmDBEntryType entry_type,
+			      GHashTable *target)
+{
+	GtkTreeModel *query_model;
+
+	query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
+	rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
+				RHYTHMDB_QUERY_PROP_EQUALS,
+				RHYTHMDB_PROP_TYPE, entry_type,
+				RHYTHMDB_QUERY_END);
+
+	gtk_tree_model_foreach (query_model,
+				(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
+				target);
+}
+
+static void
+itinerary_insert_some_playlists (RBSyncState *state,
+				 GHashTable *target)
+{
+	GList *list_iter;
+	GList *playlists;
+	RBShell *shell;
+
+	g_object_get (state->priv->source, "shell", &shell, NULL);
+	playlists = rb_playlist_manager_get_playlists ((RBPlaylistManager *) rb_shell_get_playlist_manager (shell));
+	g_object_unref (shell);
+
+	for (list_iter = playlists; list_iter; list_iter = list_iter->next) {
+		gchar *name;
+
+		g_object_get (list_iter->data, "name", &name, NULL);
+
+		/* See if we should sync it */
+		if (rb_sync_settings_sync_group (state->priv->sync_settings, SYNC_CATEGORY_MUSIC, name)) {
+			GtkTreeModel *query_model;
+
+			rb_debug ("adding entries from playlist %s to itinerary", name);
+			g_object_get (RB_SOURCE (list_iter->data), "base-query-model", &query_model, NULL);
+			gtk_tree_model_foreach (query_model,
+						(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
+						target);
+			g_object_unref (query_model);
+		} else {
+			rb_debug ("not adding playlist %s to itinerary", name);
+		}
+
+		g_free (name);
+	}
+
+	g_list_free (playlists);
+}
+
+static void
+itinerary_insert_some_podcasts (RBSyncState *state,
+				RhythmDB *db,
+				GHashTable *target)
+{
+	GList *podcasts;
+	GList *i;
+
+	podcasts = rb_sync_settings_get_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_PODCAST);
+	for (i = podcasts; i != NULL; i = i->next) {
+		GtkTreeModel *query_model;
+		rb_debug ("adding entries from podcast %s to itinerary", (char *)i->data);
+		query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
+		rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
+					RHYTHMDB_QUERY_PROP_EQUALS,
+					RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
+					RHYTHMDB_QUERY_PROP_EQUALS,
+					RHYTHMDB_PROP_SUBTITLE, i->data,
+					RHYTHMDB_QUERY_END);
+
+		/* TODO: exclude undownloaded episodes, sort by post date, set limit, optionally exclude things with play count > 0
+		 * RHYTHMDB_QUERY_PROP_NOT_EQUAL, RHYTHMDB_PROP_MOUNTPOINT, NULL,	(will this work?)
+		 * RHYTHMDB_QUERY_PROP_NOT_EQUAL, RHYTHMDB_PROP_STATUS, RHYTHMDB_PODCAST_STATUS_ERROR,
+		 *
+		 * RHYTHMDB_QUERY_PROP_EQUALS, RHYTHMDB_PROP_PLAYCOUNT, 0
+		 */
+
+		gtk_tree_model_foreach (query_model,
+					(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
+					target);
+		g_object_unref (query_model);
+	}
+}
+
+static GHashTable *
+build_sync_itinerary (RBSyncState *state)
+{
+	RBShell *shell;
+	RhythmDB *db;
+	GHashTable *itinerary;
+
+	rb_debug ("building itinerary hash");
+
+	g_object_get (state->priv->source, "shell", &shell, NULL);
+	g_object_get (shell, "db", &db, NULL);
+
+	itinerary = g_hash_table_new_full (g_str_hash,
+					   g_str_equal,
+					   g_free,
+					   (GDestroyNotify)rhythmdb_entry_unref);
+
+	if (rb_sync_settings_sync_category (state->priv->sync_settings, SYNC_CATEGORY_MUSIC) ||
+	    rb_sync_settings_sync_group (state->priv->sync_settings, SYNC_CATEGORY_MUSIC, SYNC_GROUP_ALL_MUSIC)) {
+		rb_debug ("adding all music to the itinerary");
+		itinerary_insert_all_of_type (db, RHYTHMDB_ENTRY_TYPE_SONG, itinerary);
+	} else if (rb_sync_settings_has_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_MUSIC)) {
+		rb_debug ("adding selected playlists to the itinerary");
+		itinerary_insert_some_playlists (state, itinerary);
+	}
+
+	state->sync_music_size = _sum_entry_size (itinerary);
+
+	if (rb_sync_settings_sync_category (state->priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
+		rb_debug ("adding all podcasts to the itinerary");
+		/* TODO: when we get #episodes/not-if-played settings, use
+		 * equivalent of insert_some_podcasts, iterating through all feeds
+		 * (use a query for all entries of type PODCAST_FEED to find them)
+		 */
+		itinerary_insert_all_of_type (db, RHYTHMDB_ENTRY_TYPE_PODCAST_POST, itinerary);
+	} else if (rb_sync_settings_has_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
+		rb_debug ("adding selected podcasts to the itinerary");
+		itinerary_insert_some_podcasts (state, db, itinerary);
+	}
+
+	state->sync_podcast_size = _sum_entry_size (itinerary) - state->sync_music_size;
+
+	g_object_unref (shell);
+	g_object_unref (db);
+
+	rb_debug ("finished building itinerary hash; has %d entries", g_hash_table_size (itinerary));
+	return itinerary;
+}
+
+static GHashTable *
+build_device_state (RBSyncState *state)
+{
+	GHashTable *device;
+	GHashTable *entries;
+
+	rb_debug ("building device contents hash");
+
+	device = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)rhythmdb_entry_unref);
+
+	rb_debug ("getting music entries from device");
+	entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) rhythmdb_entry_unref);
+
+	rb_media_player_source_get_entries (state->priv->source, SYNC_CATEGORY_MUSIC, entries);
+	/*klass->impl_get_entries (source, SYNC_CATEGORY_MUSIC, entries);*/
+	state->total_music_size = _sum_entry_size (entries);
+	if (rb_sync_settings_has_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_MUSIC)) {
+		_g_hash_table_transfer_all (device, entries);
+	}
+	g_hash_table_destroy (entries);
+	rb_debug ("done getting music entries from device");
+
+	rb_debug ("getting podcast entries from device");
+	entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) rhythmdb_entry_unref);
+	rb_media_player_source_get_entries (state->priv->source, SYNC_CATEGORY_PODCAST, entries);
+	/*klass->impl_get_entries (source, SYNC_CATEGORY_PODCAST, entries);*/
+	state->total_podcast_size = _sum_entry_size (entries);
+	if (rb_sync_settings_has_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
+		_g_hash_table_transfer_all (device, entries);
+	}
+	g_hash_table_destroy (entries);
+	rb_debug ("done getting podcast entries from device");
+
+	rb_debug ("done building device contents hash; has %d entries", g_hash_table_size (device));
+	return device;
+}
+
+void
+rb_sync_state_update (RBSyncState *state)
+{
+	GHashTable *device;
+	GHashTable *itinerary;
+	BuildSyncListData data;
+	GList *list_iter;
+	gint64 add_size = 0;
+	gint64 remove_size = 0;
+
+	/* clear existing state */
+	free_sync_lists (state);
+
+	/* figure out what we want on the device and what's already there */
+	itinerary = build_sync_itinerary (state);
+	device = build_device_state (state);
+
+	/* figure out what to add to the device */
+	rb_debug ("building list of files to transfer to device");
+	data.target = device;
+	data.result = NULL;
+	g_hash_table_foreach (itinerary, (GHFunc)build_sync_list_cb, &data);
+	state->sync_to_add = data.result;
+	state->sync_add_size = data.bytes;
+	state->sync_add_count = g_list_length (state->sync_to_add);
+	rb_debug ("decided to transfer %d files (%" G_GINT64_FORMAT" bytes) to the device",
+		  state->sync_add_count,
+		  state->sync_add_size);
+
+	/* and what to remove */
+	rb_debug ("building list of files to remove from device");
+	data.target = itinerary;
+	data.result = NULL;
+	g_hash_table_foreach (device, (GHFunc)build_sync_list_cb, &data);
+	state->sync_to_remove = data.result;
+	state->sync_remove_size = data.bytes;
+	state->sync_remove_count = g_list_length (state->sync_to_remove);
+	rb_debug ("decided to remove %d files (%" G_GINT64_FORMAT" bytes) from the device",
+		  state->sync_remove_count,
+		  state->sync_remove_size);
+
+	state->sync_keep_count = g_hash_table_size (device) - g_list_length (state->sync_to_remove);
+	rb_debug ("keeping %d files on the device", state->sync_keep_count);
+
+	g_hash_table_destroy (device);
+	g_hash_table_destroy (itinerary);
+
+	/* calculate space requirements */
+	for (list_iter = state->sync_to_add; list_iter; list_iter = list_iter->next) {
+		add_size += rhythmdb_entry_get_uint64 (list_iter->data, RHYTHMDB_PROP_FILE_SIZE);
+	}
+
+	for (list_iter = state->sync_to_remove; list_iter; list_iter = list_iter->next) {
+		remove_size += rhythmdb_entry_get_uint64 (list_iter->data, RHYTHMDB_PROP_FILE_SIZE);
+	}
+
+	state->sync_space_needed = rb_media_player_source_get_capacity (state->priv->source) -
+				   rb_media_player_source_get_free_space (state->priv->source);
+	rb_debug ("current space used: %" G_GINT64_FORMAT " bytes; adding %" G_GINT64_FORMAT ", removing %" G_GINT64_FORMAT,
+		  state->sync_space_needed,
+		  add_size,
+		  remove_size);
+	state->sync_space_needed = state->sync_space_needed + add_size - remove_size;
+	rb_debug ("space used after sync: %" G_GINT64_FORMAT " bytes", state->sync_space_needed);
+
+	g_signal_emit (state, signals[UPDATED], 0);
+}
+
+static void
+sync_settings_updated (RBSyncSettings *settings, RBSyncState *state)
+{
+	rb_debug ("sync settings updated, updating state");
+	rb_sync_state_update (state);
+}
+
+
+RBSyncState *
+rb_sync_state_new (RBMediaPlayerSource *source, RBSyncSettings *settings)
+{
+	GObject *state;
+	state = g_object_new (RB_TYPE_SYNC_STATE,
+			      "source", source,
+			      "sync-settings", settings,
+			      NULL);
+	return RB_SYNC_STATE (state);
+}
+
+
+static void
+rb_sync_state_init (RBSyncState *state)
+{
+	state->priv = G_TYPE_INSTANCE_GET_PRIVATE (state, RB_TYPE_SYNC_STATE, RBSyncStatePrivate);
+}
+
+static void
+impl_constructed (GObject *object)
+{
+	RBSyncState *state = RB_SYNC_STATE (object);
+
+	rb_sync_state_update (state);
+
+	g_signal_connect_object (state->priv->sync_settings,
+				 "updated",
+				 G_CALLBACK (sync_settings_updated),
+				 state, 0);
+
+	RB_CHAIN_GOBJECT_METHOD(rb_sync_state_parent_class, constructed, object);
+}
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBSyncState *state = RB_SYNC_STATE (object);
+	switch (prop_id) {
+	case PROP_SOURCE:
+		state->priv->source = g_value_get_object (value);
+		break;
+	case PROP_SYNC_SETTINGS:
+		state->priv->sync_settings = g_value_get_object (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBSyncState *state = RB_SYNC_STATE (object);
+	switch (prop_id) {
+	case PROP_SOURCE:
+		g_value_set_object (value, state->priv->source);
+		break;
+	case PROP_SYNC_SETTINGS:
+		g_value_set_object (value, state->priv->sync_settings);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_finalize (GObject *object)
+{
+	RBSyncState *state = RB_SYNC_STATE (object);
+
+	free_sync_lists (state);
+
+	G_OBJECT_CLASS (rb_sync_state_parent_class)->finalize (object);
+}
+
+static void
+rb_sync_state_class_init (RBSyncStateClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->finalize = impl_finalize;
+	object_class->constructed = impl_constructed;
+	object_class->set_property = impl_set_property;
+	object_class->get_property = impl_get_property;
+
+	g_object_class_install_property (object_class,
+					 PROP_SOURCE,
+					 g_param_spec_object ("source",
+							      "source",
+							      "RBMediaPlayerSource instance",
+							      RB_TYPE_MEDIA_PLAYER_SOURCE,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+	g_object_class_install_property (object_class,
+					 PROP_SYNC_SETTINGS,
+					 g_param_spec_object ("sync-settings",
+							      "sync-settings",
+							      "RBSyncSettings instance",
+							      RB_TYPE_SYNC_SETTINGS,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	signals[UPDATED] = g_signal_new ("updated",
+					 RB_TYPE_SYNC_STATE,
+					 G_SIGNAL_RUN_LAST,
+					 G_STRUCT_OFFSET (RBSyncStateClass, updated),
+					 NULL, NULL,
+					 g_cclosure_marshal_VOID__VOID,
+					 G_TYPE_NONE,
+					 0);
+
+	g_type_class_add_private (object_class, sizeof (RBSyncStatePrivate));
+}
diff --git a/sources/sync/rb-sync-state.h b/sources/sync/rb-sync-state.h
new file mode 100644
index 0000000..0676232
--- /dev/null
+++ b/sources/sync/rb-sync-state.h
@@ -0,0 +1,91 @@
+/*
+ *  Copyright (C) 2010 Jonathan Matthew  <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef __RB_SYNC_STATE_H
+#define __RB_SYNC_STATE_H
+
+#include <glib-object.h>
+
+#include "rb-media-player-source.h"
+#include "rb-sync-settings.h"
+
+#include "rhythmdb.h"
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_SYNC_STATE         (rb_sync_state_get_type ())
+#define RB_SYNC_STATE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_SYNC_STATE, RBSyncState))
+#define RB_SYNC_STATE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_SYNC_STATE, RBSyncStateClass))
+#define RB_IS_SYNC_STATE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_SYNC_STATE))
+#define RB_IS_SYNC_STATE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_SYNC_STATE))
+#define RB_SYNC_STATE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_SYNC_STATE, RBSyncStateClass))
+
+typedef struct _RBSyncState RBSyncState;
+typedef struct _RBSyncStateClass RBSyncStateClass;
+typedef struct _RBSyncStatePrivate RBSyncStatePrivate;
+
+struct _RBSyncState
+{
+	GObject parent;
+
+	guint64 total_music_size;
+	guint64 total_podcast_size;
+	guint64 sync_music_size;
+	guint64 sync_podcast_size;
+
+	guint64 sync_space_needed;
+	guint64 sync_add_size;
+	guint64 sync_remove_size;
+	int sync_add_count;
+	int sync_remove_count;
+	int sync_keep_count;
+
+	GList *sync_to_add;
+	GList *sync_to_remove;
+
+	RBSyncStatePrivate *priv;
+};
+
+struct _RBSyncStateClass
+{
+	GObjectClass parent_class;
+
+	/* signals */
+	void (*updated) (RBSyncState *state);
+};
+
+char *		rb_sync_state_make_track_uuid (RhythmDBEntry *entry);
+
+GType		rb_sync_state_get_type (void);
+
+RBSyncState *	rb_sync_state_new (RBMediaPlayerSource *source, RBSyncSettings *settings);
+
+void		rb_sync_state_update (RBSyncState *state);
+
+G_END_DECLS
+
+#endif /* __RB_SYNC_STATE_H */



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