sidebar: Create a Section for Libraries

This section shows a list of library path playlists. I also add an extra
widget that opens a Gtk.FileDialogso users can add music to their
collections.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-09-01 16:27:55 -04:00
parent 0de8089d59
commit 0dcdcd4a68
2 changed files with 216 additions and 0 deletions

View File

@ -0,0 +1,77 @@
# Copyright 2022 (c) Anna Schumaker.
"""Displays library path playlists."""
import pathlib
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import Adw
from .. import db
from . import row
from . import section
DIRECTORY_FILTERS = Gio.ListStore()
DIRECTORY_FILTERS.append(Gtk.FileFilter(name="Directories",
mime_types=["inode/directory"]))
class LibraryRow(row.TreeRow):
"""A TreeRow for displaying Library playlists."""
def __init__(self, *args, **kwargs):
"""Initialize a LibraryRow."""
super().__init__(*args, indented=False, **kwargs)
self.child = row.LibraryRow()
def do_bind(self) -> None:
"""Bind Library properties to the LibraryRow widget."""
super().do_bind()
self.bind_and_set_property("enabled", "enabled", bidirectional=True)
self.bind_and_set_property("online", "online", bidirectional=True)
self.bind_and_set_property("deleting", "sensitive",
invert_boolean=True)
self.bind_and_set(self.item.queue, "running", self.child, "scanning")
self.bind_and_set(self.item.queue, "progress", self.child, "progress")
class Section(section.Section):
"""A sidebar Section for library path playlists."""
def __init__(self, table=db.libraries.Table):
"""Initialize our library path section."""
super().__init__(table, LibraryRow, icon_name="library-music",
title="Library Paths", subtitle="0 library paths")
self.extra_widget = Gtk.Button(icon_name="folder-new", has_frame=False)
self._dialog = Gtk.FileDialog(title="Pick a Directory",
filters=DIRECTORY_FILTERS)
self._toast = None
self.extra_widget.connect("clicked", self.__show_dialog)
self.table.connect("library-online", self.__library_online)
def __async_ready(self, dialog: Gtk.FileDialog, task: Gio.Task) -> None:
try:
dir = dialog.select_folder_finish(task)
self.table.create(pathlib.Path(dir.get_path()))
except GLib.Error:
pass
def __library_online(self, table: db.libraries.Table,
library: db.libraries.Library) -> None:
if win := self.get_ancestor(Adw.Window):
if self._toast:
self._toast.dismiss()
state = "online" if library.online else "offline"
self._toast = win.post_toast(f"{library.path} is {state}")
self._toast.set_priority(Adw.ToastPriority.HIGH)
def __show_dialog(self, button: Gtk.Button) -> None:
music = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_MUSIC)
self._dialog.set_initial_file(Gio.File.new_for_path(music))
self._dialog.select_folder(self.get_ancestor(Gtk.Window), None,
self.__async_ready)
def do_get_subtitle(self, n_libraries: int) -> str:
"""Return a subtitle for this section."""
s_libraries = "s" if n_libraries != 1 else ""
return f"{n_libraries} library path{s_libraries}"

View File

@ -0,0 +1,139 @@
# Copyright 2022 (c) Anna Schumaker.
"""Tests our library path section."""
import pathlib
import emmental.sidebar.library
import tests.util
import unittest.mock
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import Adw
class TestLibraries(tests.util.TestCase):
"""Tests our Library Paths section."""
def setUp(self):
"""Set up common variables."""
super().setUp()
self.libraries = emmental.sidebar.library.Section(self.sql.libraries)
def test_init(self):
"""Test that the libraries section is set up correctly."""
self.assertIsInstance(self.libraries, emmental.sidebar.section.Section)
self.assertEqual(self.libraries._factory.row_type,
emmental.sidebar.library.LibraryRow)
self.assertEqual(self.libraries.table, self.sql.libraries)
self.assertEqual(self.libraries.icon_name, "library-music")
self.assertEqual(self.libraries.title, "Library Paths")
def test_extra_widget(self):
"""Test the library section extra widget."""
self.assertIsInstance(self.libraries.extra_widget, Gtk.Button)
self.assertEqual(self.libraries.extra_widget.get_icon_name(),
"folder-new")
self.assertFalse(self.libraries.extra_widget.get_has_frame())
mock_set_initial_file = unittest.mock.Mock()
self.libraries._dialog.set_initial_file = mock_set_initial_file
music = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_MUSIC)
with unittest.mock.patch.object(self.libraries._dialog,
"select_folder") as mock_func:
self.libraries.extra_widget.emit("clicked")
self.assertEqual(mock_set_initial_file.call_args[0][0].get_path(),
str(music))
mock_func.assert_called_with(None, None,
self.libraries._Section__async_ready)
def test_subtitle(self):
"""Test that the subtitle property is set properly."""
self.assertEqual(self.libraries.subtitle, "0 library paths")
self.sql.libraries.create("/a/b/c")
self.assertEqual(self.libraries.subtitle, "1 library path")
self.sql.libraries.create("/a/b/d")
self.assertEqual(self.libraries.subtitle, "2 library paths")
def test_library_row(self):
"""Test the library playlist row."""
library = self.sql.libraries.create(pathlib.Path("/a/b/c"))
treeitem = Gtk.TreeListRow()
treeitem.get_item = unittest.mock.Mock(return_value=library)
listitem = Gtk.ListItem()
listitem.get_item = unittest.mock.Mock(return_value=treeitem)
row = emmental.sidebar.library.LibraryRow(listitem)
self.assertIsInstance(row.child, emmental.sidebar.row.LibraryRow)
self.assertFalse(row.indented)
row.bind()
self.assertTrue(row.child.enabled)
self.assertTrue(row.child.scanning)
self.assertFalse(row.child.online)
row.child.enabled = False
library.online = True
library.deleting = True
library.queue.running = False
library.queue.progress = 0.42
self.assertFalse(library.enabled)
self.assertFalse(row.child.get_sensitive())
self.assertFalse(row.child.scanning)
self.assertTrue(row.child.online)
self.assertEqual(row.child.progress, 0.42)
row.unbind()
def test_toast(self):
"""Test sending a toast notification for library online status."""
self.assertIsNone(self.libraries._toast)
toast = unittest.mock.Mock()
window = unittest.mock.Mock()
window.post_toast = unittest.mock.Mock(return_value=toast)
self.libraries.get_ancestor = unittest.mock.Mock(return_value=window)
self.sql.libraries.loaded = True
library = self.sql.libraries.create("/a/b/c")
library.online = False
window.post_toast.assert_called_with("/a/b/c is offline")
toast.set_priority.assert_called_with(Adw.ToastPriority.HIGH)
toast.dismiss.assert_not_called()
self.assertEqual(self.libraries._toast, toast)
library.online = True
window.post_toast.assert_called_with("/a/b/c is online")
toast.dismiss.assert_called()
def test_directory_filter(self):
"""Test that the directory filters list is set up properly."""
self.assertIsInstance(emmental.sidebar.library.DIRECTORY_FILTERS,
Gio.ListStore)
filter = emmental.sidebar.library.DIRECTORY_FILTERS[0]
self.assertIsInstance(filter, Gtk.FileFilter)
self.assertEqual(filter.get_name(), "Directories")
(name, mime_types) = filter.to_gvariant()
self.assertEqual(name, "Directories")
self.assertListEqual(mime_types, [(1, "inode/directory")])
self.assertEqual(len(mime_types), 1)
def test_dialog(self):
"""Test that the FileDialog works as expected."""
self.assertIsInstance(self.libraries._dialog, Gtk.FileDialog)
self.assertEqual(self.libraries._dialog.get_title(),
"Pick a Directory")
self.assertEqual(self.libraries._dialog.get_filters(),
emmental.sidebar.library.DIRECTORY_FILTERS)
with unittest.mock.patch.object(self.libraries._dialog,
"select_folder_finish") as mock_finish:
task = Gio.Task()
path = pathlib.Path("/a/b/c")
mock_finish.return_value = Gio.File.new_for_path(str(path))
self.libraries._Section__async_ready(self.libraries._dialog, task)
mock_finish.assert_called_with(task)
self.assertIsNotNone(self.sql.libraries.lookup(path))