diff --git a/emmental/header/__init__.py b/emmental/header/__init__.py
index 4457b39..a8f01fa 100644
--- a/emmental/header/__init__.py
+++ b/emmental/header/__init__.py
@@ -9,6 +9,7 @@ from ..action import ActionEntry
from .. import db
from .. import buttons
from .. import gsetup
+from . import listenbrainz
from . import open
from . import replaygain
from . import volume
@@ -34,6 +35,7 @@ class Header(Gtk.HeaderBar):
sql = GObject.Property(type=db.Connection)
title = GObject.Property(type=str)
subtitle = GObject.Property(type=str)
+ listenbrainz_token = GObject.Property(type=str)
show_sidebar = GObject.Property(type=bool, default=False)
bg_enabled = GObject.Property(type=bool, default=False)
bg_volume = GObject.Property(type=float, default=0.5)
@@ -50,10 +52,12 @@ class Header(Gtk.HeaderBar):
icon = "sidebar-show-symbolic"
self._show_sidebar = Gtk.ToggleButton(icon_name=icon, has_frame=False)
self._open = open.OpenRow()
+ self._listenbrainz = listenbrainz.ListenBrainzRow()
self._menu_box = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
self._menu_box.add_css_class("boxed-list")
self._menu_box.append(self._open)
+ self._menu_box.append(self._listenbrainz)
if __debug__:
self._settings = settings.Row(sql)
@@ -85,6 +89,7 @@ class Header(Gtk.HeaderBar):
self.bind_property("title", self._title, "title")
self.bind_property("subtitle", self._title, "subtitle")
+ self.bind_property("listenbrainz-token", self._listenbrainz, "text")
self.bind_property("show-sidebar", self._show_sidebar, "active",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("bg-enabled", self._background, "enabled",
@@ -104,7 +109,9 @@ class Header(Gtk.HeaderBar):
self.pack_end(self._vol_button)
self.set_title_widget(self._title)
+ self._menu_button.props.popover.connect("closed", self.__menu_closed)
self._open.connect("track-requested", self.__track_requested)
+ self._listenbrainz.connect("apply", self.__listenbrainz_apply)
self.connect("notify", self.__notify)
def __run_settings(self, button: Gtk.Button) -> None:
@@ -129,10 +136,33 @@ class Header(Gtk.HeaderBar):
f"normalizing: {rg_status}")
self._vol_button.set_tooltip_text(status)
+ def __listenbrainz_apply(self, entry: Adw.PasswordEntryRow) -> None:
+ self.listenbrainz_token = entry.get_text()
+ self._menu_button.popdown()
+
+ def __menu_closed(self, popover: Gtk.Popover) -> None:
+ self._listenbrainz.props.text = self.listenbrainz_token
+
def __track_requested(self, button: open.OpenRow,
path: pathlib.Path) -> None:
self.emit("track-requested", path)
+ @GObject.Property(type=bool, default=True)
+ def listenbrainz_token_valid(self) -> bool:
+ """Check if we think the listenbrainz token is valid."""
+ return not self._listenbrainz.has_css_class("warning")
+
+ @listenbrainz_token_valid.setter
+ def listenbrainz_token_valid(self, valid: bool) -> None:
+ if valid:
+ self._menu_button.remove_css_class("warning")
+ self._listenbrainz.remove_css_class("warning")
+ else:
+ win = self.get_ancestor(Gtk.Window)
+ win.post_toast("listenbrainz: user token is invalid")
+ self._menu_button.add_css_class("warning")
+ self._listenbrainz.add_css_class("warning")
+
@property
def accelerators(self) -> list[ActionEntry]:
"""Get a list of accelerators for the Header."""
diff --git a/emmental/header/listenbrainz.py b/emmental/header/listenbrainz.py
new file mode 100644
index 0000000..e7efbdd
--- /dev/null
+++ b/emmental/header/listenbrainz.py
@@ -0,0 +1,14 @@
+# Copyright 2024 (c) Anna Schumaker.
+"""A custom Adw.PasswordEntryRow to set the user token."""
+from gi.repository import Gtk
+from gi.repository import Adw
+
+
+def ListenBrainzRow() -> Adw.PasswordEntryRow:
+ """Create a new PasswordEntryRow for entering the user token."""
+ row = Adw.PasswordEntryRow(title="ListenBrainz User Token",
+ show_apply_button=True)
+ row.prefix = Gtk.Image(icon_name="listenbrainz-logo-symbolic")
+
+ row.add_prefix(row.prefix)
+ return row
diff --git a/icons/scalable/actions/listenbrainz-logo-symbolic.svg b/icons/scalable/actions/listenbrainz-logo-symbolic.svg
new file mode 100644
index 0000000..2c9cc60
--- /dev/null
+++ b/icons/scalable/actions/listenbrainz-logo-symbolic.svg
@@ -0,0 +1,49 @@
+
+
diff --git a/icons/scalable/actions/listenbrainz-logo.svg b/icons/scalable/actions/listenbrainz-logo.svg
new file mode 100644
index 0000000..b7dc970
--- /dev/null
+++ b/icons/scalable/actions/listenbrainz-logo.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/tests/header/test_header.py b/tests/header/test_header.py
index 23fe2f6..a5daf7f 100644
--- a/tests/header/test_header.py
+++ b/tests/header/test_header.py
@@ -62,11 +62,55 @@ class TestHeader(tests.util.TestCase):
self.header._open.emit("track-requested", pathlib.Path("/a/b/c/1.ogg"))
signal.assert_called_with(self.header, pathlib.Path("/a/b/c/1.ogg"))
+ def test_listenbrainz(self):
+ """Check that the ListenBrainzRow is set up correctly."""
+ self.assertIsInstance(self.header._listenbrainz, Adw.PasswordEntryRow)
+ self.assertEqual(self.header._menu_box.get_row_at_index(1),
+ self.header._listenbrainz)
+
+ self.assertEqual(self.header.listenbrainz_token, "")
+ self.assertEqual(self.header._listenbrainz.props.text, "")
+
+ self.header.listenbrainz_token = "abcde"
+ self.assertEqual(self.header._listenbrainz.props.text, "abcde")
+
+ with unittest.mock.patch.object(self.header._menu_button,
+ "popdown") as mock_popdown:
+ self.header._listenbrainz.props.text = "fghij"
+ self.header._listenbrainz.emit("apply")
+ self.assertEqual(self.header.listenbrainz_token, "fghij")
+ mock_popdown.assert_called()
+
+ self.header._listenbrainz.props.text = "abcde"
+ self.header._menu_button.get_popover().emit("closed")
+ self.assertEqual(self.header._listenbrainz.props.text, "fghij")
+
+ def test_listenbrainz_token_valid(self):
+ """Test the listenbrainz-token-valid property."""
+ win = Gtk.Window(titlebar=self.header)
+ win.post_toast = unittest.mock.Mock()
+
+ self.assertTrue(self.header.listenbrainz_token_valid)
+
+ self.header.listenbrainz_token_valid = False
+ self.assertTrue(self.header._menu_button.has_css_class("warning"))
+ self.assertTrue(self.header._listenbrainz.has_css_class("warning"))
+ self.assertFalse(self.header.listenbrainz_token_valid)
+ win.post_toast.assert_called_with(
+ "listenbrainz: user token is invalid")
+
+ win.post_toast.reset_mock()
+ self.header.listenbrainz_token_valid = True
+ self.assertFalse(self.header._menu_button.has_css_class("warning"))
+ self.assertFalse(self.header._listenbrainz.has_css_class("warning"))
+ self.assertTrue(self.header.listenbrainz_token_valid)
+ win.post_toast.assert_not_called()
+
def test_settings(self):
"""Check that the SettingsRow is set up correctly."""
self.assertIsInstance(self.header._settings,
emmental.header.settings.Row)
- self.assertEqual(self.header._menu_box.get_row_at_index(1),
+ self.assertEqual(self.header._menu_box.get_row_at_index(2),
self.header._settings)
def test_menu_button(self):
diff --git a/tests/header/test_listenbrainz.py b/tests/header/test_listenbrainz.py
new file mode 100644
index 0000000..62fad72
--- /dev/null
+++ b/tests/header/test_listenbrainz.py
@@ -0,0 +1,25 @@
+# Copyright 2024 (c) Anna Schumaker.
+"""Tests our Listenbrainz User Token entry."""
+import emmental.header.listenbrainz
+import unittest
+from gi.repository import Gtk
+from gi.repository import Adw
+
+
+class TestListenbrainzRow(unittest.TestCase):
+ """Test the ListenBrainzRow."""
+
+ def setUp(self):
+ """Set up common variables."""
+ self.row = emmental.header.listenbrainz.ListenBrainzRow()
+
+ def test_init(self):
+ """Test that the ListenBrainzRow was set up properly."""
+ self.assertIsInstance(self.row, Adw.PasswordEntryRow)
+ self.assertIsInstance(self.row.prefix, Gtk.Image)
+
+ self.assertEqual(self.row.props.title, "ListenBrainz User Token")
+ self.assertTrue(self.row.props.show_apply_button)
+
+ self.assertEqual(self.row.prefix.props.icon_name,
+ "listenbrainz-logo-symbolic")