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:
parent
0de8089d59
commit
0dcdcd4a68
|
@ -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}"
|
|
@ -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))
|
Loading…
Reference in New Issue