diff --git a/emmental/emmental.css b/emmental/emmental.css
index ee9beb0..df2baa1 100644
--- a/emmental/emmental.css
+++ b/emmental/emmental.css
@@ -60,3 +60,7 @@ image.emmental-sidebar-arrow:checked {
button.emmental-delete>image {
color: @destructive_color;
}
+
+button.emmental-stop>image {
+ color: @red_3;
+}
diff --git a/emmental/sidebar/row.py b/emmental/sidebar/row.py
index e1574f1..640edd6 100644
--- a/emmental/sidebar/row.py
+++ b/emmental/sidebar/row.py
@@ -93,3 +93,65 @@ class PlaylistRow(BaseRow):
def __on_delete(self, button: Gtk.Button) -> None:
if self.playlist is not None:
self.playlist.delete()
+
+
+class LibraryRow(BaseRow):
+ """An advaced playlist row with extra actions for library management."""
+
+ enabled = GObject.Property(type=bool, default=True)
+ online = GObject.Property(type=bool, default=True)
+ scanning = GObject.Property(type=bool, default=False)
+ progress = GObject.Property(type=float)
+
+ def __init__(self, **kwargs):
+ """Initialize a LibraryRow."""
+ super().__init__(**kwargs)
+ self._box = Gtk.Box()
+ self._overlay = Gtk.Overlay(child=self._box)
+ self._switch = Gtk.Switch(active=self.enabled, valign=Gtk.Align.CENTER)
+ self._title = PlaylistTitle(margin_start=12, margin_end=12)
+ self._scan = Gtk.Button(icon_name="update", has_frame=False,
+ valign=Gtk.Align.CENTER)
+ self._stop = Gtk.Button(icon_name="stop-sign-large", has_frame=False,
+ valign=Gtk.Align.CENTER, visible=False)
+ self._delete = Gtk.Button(icon_name="big-x-symbolic",
+ valign=Gtk.Align.CENTER, has_frame=False)
+ self._progress = Gtk.ProgressBar(valign=Gtk.Align.END, visible=False)
+
+ self.bind_property("enabled", self._switch, "active",
+ GObject.BindingFlags.BIDIRECTIONAL)
+ self.bind_property("online", self._switch, "sensitive")
+
+ self.bind_property("name", self._title, "title")
+ self.bind_property("count", self._title, "count")
+ self.bind_property("online", self._title, "sensitive")
+ self.bind_property("enabled", self._title, "sensitive")
+
+ self.bind_property("scanning", self._stop, "visible")
+ self.bind_property("scanning", self._scan, "visible",
+ GObject.BindingFlags.INVERT_BOOLEAN)
+ self.bind_property("scanning", self._progress, "visible")
+ self.bind_property("progress", self._progress, "fraction")
+
+ self._delete.connect("clicked", self.__on_button_press, "delete")
+ self._scan.connect("clicked", self.__on_button_press, "scan")
+ self._stop.connect("clicked", self.__on_button_press, "stop")
+
+ self._delete.add_css_class("emmental-delete")
+ self._stop.add_css_class("emmental-stop")
+ self._progress.add_css_class("osd")
+
+ self._box.append(self._switch)
+ self._box.append(self._title)
+ self._box.append(self._scan)
+ self._box.append(self._stop)
+ self._box.append(self._delete)
+ self._overlay.add_overlay(self._progress)
+ self.append(self._overlay)
+
+ def __on_button_press(self, button: Gtk.Button, action: str) -> None:
+ if self.playlist is not None:
+ match action:
+ case "delete": self.playlist.delete()
+ case "scan": self.playlist.scan()
+ case "stop": self.playlist.stop()
diff --git a/icons/scalable/actions/stop-sign-large-symbolic.svg b/icons/scalable/actions/stop-sign-large-symbolic.svg
new file mode 100644
index 0000000..55a00a2
--- /dev/null
+++ b/icons/scalable/actions/stop-sign-large-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/icons/scalable/actions/update-symbolic.svg b/icons/scalable/actions/update-symbolic.svg
new file mode 100644
index 0000000..8366791
--- /dev/null
+++ b/icons/scalable/actions/update-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/tests/sidebar/test_row.py b/tests/sidebar/test_row.py
index 1eecfec..b846ffd 100644
--- a/tests/sidebar/test_row.py
+++ b/tests/sidebar/test_row.py
@@ -177,3 +177,136 @@ class TestPlaylistRow(unittest.TestCase):
self.row.playlist = self.playlist
self.row._title.emit("request-rename", "New Name")
self.playlist.rename.assert_called_with("New Name")
+
+
+class TestLibraryRow(unittest.TestCase):
+ """Tests our library row with extra widgets."""
+
+ def setUp(self):
+ """Set up common variables."""
+ super().setUp()
+ self.row = emmental.sidebar.row.LibraryRow()
+ self.playlist = emmental.db.playlist.Playlist(Gio.ListStore(),
+ propertyid=12345,
+ name="Test Playlist")
+
+ def test_init(self):
+ """Test that the library row is configured properly."""
+ self.assertIsInstance(self.row, emmental.sidebar.row.BaseRow)
+ self.assertIsInstance(self.row._box, Gtk.Box)
+ self.assertIsInstance(self.row._overlay, Gtk.Overlay)
+
+ self.assertEqual(self.row.get_first_child(), self.row._overlay)
+ self.assertEqual(self.row._overlay.get_child(), self.row._box)
+
+ def test_switch(self):
+ """Test the switch widget and enabled property."""
+ self.assertIsInstance(self.row._switch, Gtk.Switch)
+ self.assertEqual(self.row._switch.get_valign(), Gtk.Align.CENTER)
+ self.assertEqual(self.row._box.get_first_child(), self.row._switch)
+
+ self.assertTrue(self.row.enabled)
+ self.assertTrue(self.row._switch.get_active())
+ self.assertTrue(self.row._title.get_sensitive())
+
+ self.row.enabled = False
+ self.assertFalse(self.row._switch.get_active())
+ self.assertFalse(self.row._title.get_sensitive())
+
+ self.row._switch.set_active(True)
+ self.assertTrue(self.row.enabled)
+ self.assertTrue(self.row._title.get_sensitive())
+
+ def test_progress(self):
+ """Test the progress bar widget and property."""
+ self.assertIsInstance(self.row._progress, Gtk.ProgressBar)
+ self.assertEqual(self.row._progress.get_valign(), Gtk.Align.END)
+ self.assertTrue(self.row._progress.has_css_class("osd"))
+ self.assertIn(self.row._progress, self.row._overlay)
+
+ self.assertEqual(self.row.progress, 0.0)
+
+ self.row.progress = 0.42
+ self.assertEqual(self.row._progress.get_fraction(), 0.42)
+
+ def test_title(self):
+ """Test the title widget and properties."""
+ self.assertIsInstance(self.row._title,
+ emmental.sidebar.title.PlaylistTitle)
+ self.assertEqual(self.row._title.get_margin_start(), 12)
+ self.assertEqual(self.row._title.get_margin_end(), 12)
+ self.assertEqual(self.row._switch.get_next_sibling(), self.row._title)
+
+ self.row.name = "/a/b/c"
+ self.row.count = 42
+ self.assertEqual(self.row._title.title, "/a/b/c")
+ self.assertEqual(self.row._title.count, 42)
+
+ def test_scan(self):
+ """Test the scan button."""
+ self.assertIsInstance(self.row._scan, Gtk.Button)
+ self.assertEqual(self.row._scan.get_icon_name(), "update")
+ self.assertEqual(self.row._scan.get_valign(), Gtk.Align.CENTER)
+ self.assertFalse(self.row._scan.get_has_frame())
+ self.assertEqual(self.row._title.get_next_sibling(), self.row._scan)
+
+ self.row._scan.emit("clicked")
+
+ self.playlist.scan = unittest.mock.Mock()
+ self.row.playlist = self.playlist
+ self.row._scan.emit("clicked")
+ self.playlist.scan.assert_called()
+
+ def test_stop(self):
+ """Test the stop button."""
+ self.assertIsInstance(self.row._stop, Gtk.Button)
+ self.assertEqual(self.row._stop.get_icon_name(), "stop-sign-large")
+ self.assertEqual(self.row._stop.get_valign(), Gtk.Align.CENTER)
+ self.assertFalse(self.row._stop.get_has_frame())
+ self.assertTrue(self.row._stop.has_css_class("emmental-stop"))
+ self.assertEqual(self.row._scan.get_next_sibling(), self.row._stop)
+
+ self.row._stop.emit("clicked")
+
+ self.playlist.stop = unittest.mock.Mock()
+ self.row.playlist = self.playlist
+ self.row._stop.emit("clicked")
+ self.playlist.stop.assert_called()
+
+ def test_delete(self):
+ """Test the delete button."""
+ self.assertIsInstance(self.row._delete, Gtk.Button)
+ self.assertEqual(self.row._delete.get_icon_name(), "big-x-symbolic")
+ self.assertEqual(self.row._delete.get_valign(), Gtk.Align.CENTER)
+ self.assertFalse(self.row._delete.get_has_frame())
+ self.assertTrue(self.row._delete.has_css_class("emmental-delete"))
+ self.assertEqual(self.row._stop.get_next_sibling(), self.row._delete)
+
+ self.row._delete.emit("clicked")
+
+ self.playlist.delete = unittest.mock.Mock()
+ self.row.playlist = self.playlist
+ self.row._delete.emit("clicked")
+ self.playlist.delete.assert_called()
+
+ def test_online(self):
+ """Test the online property."""
+ self.assertTrue(self.row.online)
+ self.assertTrue(self.row._switch.get_sensitive())
+ self.assertTrue(self.row._title.get_sensitive())
+
+ self.row.online = False
+ self.assertFalse(self.row._switch.get_sensitive())
+ self.assertFalse(self.row._title.get_sensitive())
+
+ def test_scaning(self):
+ """Test the scanning property."""
+ self.assertFalse(self.row.scanning)
+ self.assertFalse(self.row._progress.get_visible())
+ self.assertFalse(self.row._stop.get_visible())
+ self.assertTrue(self.row._scan.get_visible())
+
+ self.row.scanning = True
+ self.assertTrue(self.row._progress.get_visible())
+ self.assertTrue(self.row._stop.get_visible())
+ self.assertFalse(self.row._scan.get_visible())