header: Add a PasswordEntry for inputting the ListenBrainz token
The user can fill this out to connect to their listenbrainz account and submit listens. I add a listenbrainz logo icon based on their icon from the website. I also create a symbolic version that I end up using in the popover menu. Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
c49a23b046
commit
efe2611422
|
@ -9,6 +9,7 @@ from ..action import ActionEntry
|
||||||
from .. import db
|
from .. import db
|
||||||
from .. import buttons
|
from .. import buttons
|
||||||
from .. import gsetup
|
from .. import gsetup
|
||||||
|
from . import listenbrainz
|
||||||
from . import open
|
from . import open
|
||||||
from . import replaygain
|
from . import replaygain
|
||||||
from . import volume
|
from . import volume
|
||||||
|
@ -34,6 +35,7 @@ class Header(Gtk.HeaderBar):
|
||||||
sql = GObject.Property(type=db.Connection)
|
sql = GObject.Property(type=db.Connection)
|
||||||
title = GObject.Property(type=str)
|
title = GObject.Property(type=str)
|
||||||
subtitle = GObject.Property(type=str)
|
subtitle = GObject.Property(type=str)
|
||||||
|
listenbrainz_token = GObject.Property(type=str)
|
||||||
show_sidebar = GObject.Property(type=bool, default=False)
|
show_sidebar = GObject.Property(type=bool, default=False)
|
||||||
bg_enabled = GObject.Property(type=bool, default=False)
|
bg_enabled = GObject.Property(type=bool, default=False)
|
||||||
bg_volume = GObject.Property(type=float, default=0.5)
|
bg_volume = GObject.Property(type=float, default=0.5)
|
||||||
|
@ -50,10 +52,12 @@ class Header(Gtk.HeaderBar):
|
||||||
icon = "sidebar-show-symbolic"
|
icon = "sidebar-show-symbolic"
|
||||||
self._show_sidebar = Gtk.ToggleButton(icon_name=icon, has_frame=False)
|
self._show_sidebar = Gtk.ToggleButton(icon_name=icon, has_frame=False)
|
||||||
self._open = open.OpenRow()
|
self._open = open.OpenRow()
|
||||||
|
self._listenbrainz = listenbrainz.ListenBrainzRow()
|
||||||
|
|
||||||
self._menu_box = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
|
self._menu_box = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
|
||||||
self._menu_box.add_css_class("boxed-list")
|
self._menu_box.add_css_class("boxed-list")
|
||||||
self._menu_box.append(self._open)
|
self._menu_box.append(self._open)
|
||||||
|
self._menu_box.append(self._listenbrainz)
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
self._settings = settings.Row(sql)
|
self._settings = settings.Row(sql)
|
||||||
|
@ -85,6 +89,7 @@ class Header(Gtk.HeaderBar):
|
||||||
|
|
||||||
self.bind_property("title", self._title, "title")
|
self.bind_property("title", self._title, "title")
|
||||||
self.bind_property("subtitle", self._title, "subtitle")
|
self.bind_property("subtitle", self._title, "subtitle")
|
||||||
|
self.bind_property("listenbrainz-token", self._listenbrainz, "text")
|
||||||
self.bind_property("show-sidebar", self._show_sidebar, "active",
|
self.bind_property("show-sidebar", self._show_sidebar, "active",
|
||||||
GObject.BindingFlags.BIDIRECTIONAL)
|
GObject.BindingFlags.BIDIRECTIONAL)
|
||||||
self.bind_property("bg-enabled", self._background, "enabled",
|
self.bind_property("bg-enabled", self._background, "enabled",
|
||||||
|
@ -104,7 +109,9 @@ class Header(Gtk.HeaderBar):
|
||||||
self.pack_end(self._vol_button)
|
self.pack_end(self._vol_button)
|
||||||
self.set_title_widget(self._title)
|
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._open.connect("track-requested", self.__track_requested)
|
||||||
|
self._listenbrainz.connect("apply", self.__listenbrainz_apply)
|
||||||
self.connect("notify", self.__notify)
|
self.connect("notify", self.__notify)
|
||||||
|
|
||||||
def __run_settings(self, button: Gtk.Button) -> None:
|
def __run_settings(self, button: Gtk.Button) -> None:
|
||||||
|
@ -129,10 +136,33 @@ class Header(Gtk.HeaderBar):
|
||||||
f"normalizing: {rg_status}")
|
f"normalizing: {rg_status}")
|
||||||
self._vol_button.set_tooltip_text(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,
|
def __track_requested(self, button: open.OpenRow,
|
||||||
path: pathlib.Path) -> None:
|
path: pathlib.Path) -> None:
|
||||||
self.emit("track-requested", path)
|
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
|
@property
|
||||||
def accelerators(self) -> list[ActionEntry]:
|
def accelerators(self) -> list[ActionEntry]:
|
||||||
"""Get a list of accelerators for the Header."""
|
"""Get a list of accelerators for the Header."""
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
id="a"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
version="1.1"
|
||||||
|
sodipodi:docname="listenbrainz-logo-symbolic.svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview2"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="52.917468"
|
||||||
|
inkscape:cx="4.2330068"
|
||||||
|
inkscape:cy="7.9652337"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1016"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="a" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<style
|
||||||
|
id="style1">.b{fill:#353070;}.c{fill:#eb743b;}</style>
|
||||||
|
</defs>
|
||||||
|
<polygon
|
||||||
|
class="b"
|
||||||
|
points="13,29 13,1 1,8 1,22 "
|
||||||
|
id="polygon1"
|
||||||
|
transform="matrix(0.5,0,0,0.5,1,0.5)"
|
||||||
|
style="fill:#222222;fill-opacity:1" />
|
||||||
|
<polygon
|
||||||
|
class="c"
|
||||||
|
points="14,29 14,1 26,8 26,22 "
|
||||||
|
id="polygon2"
|
||||||
|
transform="matrix(0.399792,0,0,0.42127119,3.5057644,1.6847072)"
|
||||||
|
style="fill:none;stroke:#222222;stroke-width:3.01583;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
id="a"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
version="1.1"
|
||||||
|
sodipodi:docname="listenbrainz-logo.svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview2"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="45.119402"
|
||||||
|
inkscape:cx="3.9007609"
|
||||||
|
inkscape:cy="8.3445255"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1016"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="a" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<style
|
||||||
|
id="style1">.b{fill:#353070;}.c{fill:#eb743b;}</style>
|
||||||
|
</defs>
|
||||||
|
<polygon
|
||||||
|
class="b"
|
||||||
|
points="1,22 13,29 13,1 1,8 "
|
||||||
|
id="polygon1"
|
||||||
|
transform="matrix(0.5,0,0,0.5,1.25,0.5)" />
|
||||||
|
<polygon
|
||||||
|
class="c"
|
||||||
|
points="26,8 26,22 14,29 14,1 "
|
||||||
|
id="polygon2"
|
||||||
|
transform="matrix(0.5,0,0,0.5,1.25,0.5)" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -62,11 +62,55 @@ class TestHeader(tests.util.TestCase):
|
||||||
self.header._open.emit("track-requested", pathlib.Path("/a/b/c/1.ogg"))
|
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"))
|
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):
|
def test_settings(self):
|
||||||
"""Check that the SettingsRow is set up correctly."""
|
"""Check that the SettingsRow is set up correctly."""
|
||||||
self.assertIsInstance(self.header._settings,
|
self.assertIsInstance(self.header._settings,
|
||||||
emmental.header.settings.Row)
|
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)
|
self.header._settings)
|
||||||
|
|
||||||
def test_menu_button(self):
|
def test_menu_button(self):
|
||||||
|
|
|
@ -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")
|
Loading…
Reference in New Issue