diff --git a/emmental/sidebar/library.py b/emmental/sidebar/library.py new file mode 100644 index 0000000..8c503d0 --- /dev/null +++ b/emmental/sidebar/library.py @@ -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}" diff --git a/tests/sidebar/test_library.py b/tests/sidebar/test_library.py new file mode 100644 index 0000000..d40e923 --- /dev/null +++ b/tests/sidebar/test_library.py @@ -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))