Added ability to select multiple items. Added alt+a to act on all checked items.
This commit is contained in:
1 parent
bd6cfb83eb
commit
059e4e124b
5 files changed
+306
-16
No files matched your search
@@ -9,6 +9,7 @@ This project is vibe coded. If you have problems with fully AI generated code, m
|
||||
- Keyboard-driven playback controls with live announcements
|
||||
- Queue management (play now, next/previous, clear/remove)
|
||||
- Track-change announcements (toggle in View → Announce Track Changes)
|
||||
- Persistent accessible checkboxes for combining artists, albums, and songs across library views
|
||||
- Context menus on library tables and search results (right click or Shift+F10/Menu) for play, queue, favorite
|
||||
- Search dialog uses a results tree (artists/albums/songs/genres) with expand/collapse, Enter to play, and context menus
|
||||
- Discover section with recently added/played, frequently played, random albums, server “Now Playing”, and “Similar to current track”
|
||||
@@ -47,7 +48,7 @@ Logs are written to `~/.local/share/stormux/navipy/navipy.log`.
|
||||
- Navigation: `Z` previous, `B` next, `Ctrl+Left/Right` previous/next, `Alt+S` shuffle, `Alt+R` repeat
|
||||
- Announcements: `Alt+C` announce current track (live region), `Ctrl+T` announce track (dialog), `Ctrl+P` announce position
|
||||
- Volume: `Ctrl+Up/Down` or `0/9` to adjust
|
||||
- Library/Search: `Ctrl+O` connect, `F5` refresh, `Ctrl+F` search, `Alt+I` context menu for current track/selection, `Shift+F10` or Menu key for item context menu
|
||||
- Library/Search: `Ctrl+O` connect, `F5` refresh, `Ctrl+F` search, `Alt+I` context menu for current track/selection, `Alt+A` actions for checked library items, `Shift+F10` or Menu key for item context menu
|
||||
|
||||
## Browsing the Library
|
||||
|
||||
@@ -57,6 +58,7 @@ Logs are written to `~/.local/share/stormux/navipy/navipy.log`.
|
||||
- Use `Backspace`, `Alt+Left`, or the Back button to return to the previous table and selection.
|
||||
- Use Play selected to play an entire selected album, artist, playlist, or genre without leaving the table.
|
||||
- Use `Shift+F10`, the Menu key, or Actions for play, queue, playlist, and favorite actions.
|
||||
- In library tables, press `Space` to check or uncheck an artist, album, or song. Checks remain selected while you browse other tabs and collections. `Alt+A` opens actions for all checked items.
|
||||
- Collection actions applies to the collection currently being viewed, including all artists or favorites.
|
||||
- Albums and genre tracks have a Load more button when more results are available.
|
||||
- Each tab keeps its own browsing position. Refreshing the library resets the tables.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||
pkgname=navipy-git
|
||||
pkgver=r12.g33bba72
|
||||
pkgver=r13.gbd6cfb8
|
||||
pkgrel=1
|
||||
pkgdesc='Accessible Subsonic desktop client tested with Navidrome'
|
||||
arch=('any')
|
||||
|
||||
+87
-2
@@ -758,7 +758,15 @@ class MainWindow(QMainWindow):
|
||||
def playLibrarySelection(self, data: Dict):
|
||||
"""Play a table selection without changing the browser's location."""
|
||||
itemType = data.get("type")
|
||||
if itemType == "song":
|
||||
if itemType == "checked_items":
|
||||
self.statusBar.showMessage("Collecting checked songs...", 2000)
|
||||
self._run_background(
|
||||
"collect checked songs for playback",
|
||||
lambda: self.collectSongsForItem(data),
|
||||
on_success=lambda result: self.startPlaybackWithSongs(result[0], result[1]),
|
||||
on_error=lambda err: self._handleCollectionError(str(err))
|
||||
)
|
||||
elif itemType == "song":
|
||||
song = data["song"]
|
||||
self.playback.enqueue(song, playNow=True)
|
||||
self.statusBar.showMessage(f"Playing {song.title} by {song.artist}", 4000)
|
||||
@@ -783,6 +791,15 @@ class MainWindow(QMainWindow):
|
||||
menu = QMenu(self)
|
||||
menu.setAccessibleName("Library Context Menu")
|
||||
itemType = data.get("type")
|
||||
if itemType == "checked_items":
|
||||
menu.addAction("Play Checked Items", lambda: self.playLibrarySelection(data))
|
||||
menu.addAction("Add Checked Items to Queue", lambda: self.addItemToQueue(data))
|
||||
menu.addAction("Add Checked Items to Playlist", lambda: self.addItemToPlaylist(data))
|
||||
menu.addSeparator()
|
||||
menu.addAction("Favorite Checked Items", lambda: self._favoriteLibraryData(data, True))
|
||||
menu.addAction("Unfavorite Checked Items", lambda: self._favoriteLibraryData(data, False))
|
||||
menu.addSeparator()
|
||||
menu.addAction("Clear Checked Items", self.libraryBrowser.clearChecked)
|
||||
if itemType in ("album", "artist", "playlist", "genre", "album_list",
|
||||
"now_playing", "similar_current"):
|
||||
menu.addAction("Open", lambda: self.libraryBrowser.currentWidget().openData(data))
|
||||
@@ -802,6 +819,19 @@ class MainWindow(QMainWindow):
|
||||
"""Apply menu actions to their original target even if selection changes."""
|
||||
action = "favorite" if favorite else "unfavorite"
|
||||
label = data.get("label", "selection")
|
||||
if data.get("type") == "checked_items":
|
||||
items = data.get("items", [])
|
||||
if not items or not self._confirmCheckedItemsAction(action, len(items)):
|
||||
return
|
||||
self.statusBar.showMessage(f"Updating {label}...", 2000)
|
||||
self._run_background(
|
||||
f"{action} checked items",
|
||||
lambda: self._setCheckedItemsFavorite(items, favorite),
|
||||
on_success=lambda result: self._afterCheckedItemsFavorite(action, result),
|
||||
on_error=lambda err: AccessibleTextDialog.showError(
|
||||
"Favorite Error", str(err), parent=self)
|
||||
)
|
||||
return
|
||||
if data["type"] != "song" and not self._confirmBulkAction(action, label):
|
||||
return
|
||||
try:
|
||||
@@ -811,6 +841,48 @@ class MainWindow(QMainWindow):
|
||||
except Exception:
|
||||
AccessibleTextDialog.showError("Favorite Error", "Could not update favorites.", parent=self)
|
||||
|
||||
def _confirmCheckedItemsAction(self, action: str, count: int) -> bool:
|
||||
response = QMessageBox.question(
|
||||
self,
|
||||
"Confirm action",
|
||||
f"This will {action} {count} checked item{'s' if count != 1 else ''}. Continue?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
return response == QMessageBox.Yes
|
||||
|
||||
def _setCheckedItemsFavorite(self, items: List[Dict], favorite: bool) -> Tuple[int, List[str]]:
|
||||
"""Favorite mixed media items, retaining enough detail to report partial failure."""
|
||||
method = self.client.star if favorite else self.client.unstar
|
||||
updated = 0
|
||||
failed = []
|
||||
for data in items:
|
||||
itemType = data.get("type")
|
||||
itemId = data.get("id")
|
||||
if itemType not in ("song", "album", "artist") or not itemId:
|
||||
failed.append(data.get("label", "unknown item"))
|
||||
continue
|
||||
try:
|
||||
method(itemId, itemType)
|
||||
updated += 1
|
||||
except Exception: # Continue so one server error does not skip later checked items.
|
||||
failed.append(data.get("label", "unknown item"))
|
||||
return updated, failed
|
||||
|
||||
def _afterCheckedItemsFavorite(self, action: str, result: Tuple[int, List[str]]):
|
||||
updated, failed = result
|
||||
pastTense = "favorited" if action == "favorite" else "unfavorited"
|
||||
message = f"{updated} checked item{'s' if updated != 1 else ''} {pastTense}"
|
||||
if failed:
|
||||
message += f"; {len(failed)} failed"
|
||||
AccessibleTextDialog.showError(
|
||||
"Favorite Error",
|
||||
f"{message}. The failed items remain checked.",
|
||||
parent=self
|
||||
)
|
||||
self.statusBar.showMessage(message, 4000)
|
||||
self.announceLive(message)
|
||||
|
||||
def showCurrentMediaContextMenu(self):
|
||||
"""Open a context menu for the currently playing song (Alt+I)."""
|
||||
currentSong = self.playback.currentSong()
|
||||
@@ -997,6 +1069,17 @@ class MainWindow(QMainWindow):
|
||||
if not self.client:
|
||||
return [], label, truncated
|
||||
|
||||
if itemType == "checked_items":
|
||||
songs: List[Song] = []
|
||||
seen = set()
|
||||
for item in data.get("items", []):
|
||||
itemSongs, _itemLabel, itemTruncated = self.collectSongsForItem(item)
|
||||
truncated = truncated or itemTruncated
|
||||
for song in itemSongs:
|
||||
if song and song.id not in seen:
|
||||
seen.add(song.id)
|
||||
songs.append(song)
|
||||
return songs, label, truncated
|
||||
if itemType == "song":
|
||||
song: Optional[Song] = data.get("song")
|
||||
return ([song] if song else []), (self.formatSongLabel(song, includeAlbum=True) if song else label), truncated
|
||||
@@ -1074,7 +1157,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _handleCollectionError(self, message: str):
|
||||
"""Surface collection errors consistently."""
|
||||
AccessibleTextDialog.showError("Queue Error", message, parent=self)
|
||||
AccessibleTextDialog.showError("Collection Error", message, parent=self)
|
||||
|
||||
def _onSongsCollectedForPlaylist(self, result: Tuple[List[Song], str, bool]):
|
||||
"""Handle async collection results before showing playlist dialog."""
|
||||
@@ -1863,11 +1946,13 @@ class MainWindow(QMainWindow):
|
||||
("Alt+C", "Announce current track"),
|
||||
("Ctrl+P", "Announce playback position"),
|
||||
("Alt+I", "Open actions for the current item"),
|
||||
("Alt+A", "Open actions for checked items or the selected library row"),
|
||||
("Ctrl+Tab", "Next library tab"),
|
||||
("Ctrl+Shift+Tab", "Previous library tab"),
|
||||
("Enter", "Open selected collection or play selected song"),
|
||||
("Backspace or Alt+Left", "Go back in the library"),
|
||||
("Shift+F10 or Menu", "Open actions for selected library item"),
|
||||
("Space in library table", "Check or uncheck an artist, album, or song"),
|
||||
("Ctrl+C", "Copy selected library row"),
|
||||
)
|
||||
dialog = QDialog(self)
|
||||
|
||||
+135
-12
@@ -2,7 +2,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, Optional
|
||||
|
||||
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal
|
||||
from PySide6.QtCore import QAbstractTableModel, QEvent, QModelIndex, Qt, Signal
|
||||
from PySide6.QtGui import QKeySequence, QGuiApplication, QShortcut
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView, QHBoxLayout, QHeaderView, QLabel, QPushButton, QTableView,
|
||||
@@ -49,8 +49,9 @@ HEADERS = {
|
||||
|
||||
|
||||
class LibraryTableModel(QAbstractTableModel):
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, browser, parent=None):
|
||||
super().__init__(parent)
|
||||
self.browser = browser
|
||||
self.headers = ()
|
||||
self.rows = []
|
||||
|
||||
@@ -61,10 +62,36 @@ class LibraryTableModel(QAbstractTableModel):
|
||||
return 0 if parent.isValid() else len(self.headers)
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
if (index.isValid() and index.column() == 0
|
||||
and role == Qt.CheckStateRole
|
||||
and self.isCheckable(index)):
|
||||
return Qt.Checked if self.browser.isChecked(
|
||||
self.rows[index.row()].data) else Qt.Unchecked
|
||||
if index.isValid() and role in (Qt.DisplayRole, Qt.AccessibleTextRole, Qt.ToolTipRole):
|
||||
return str(self.rows[index.row()].values[index.column()])
|
||||
return None
|
||||
|
||||
def flags(self, index):
|
||||
flags = super().flags(index)
|
||||
if self.isCheckable(index):
|
||||
flags |= Qt.ItemIsUserCheckable
|
||||
return flags
|
||||
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
if role != Qt.CheckStateRole or not self.isCheckable(index):
|
||||
return False
|
||||
self.browser.setChecked(self.rows[index.row()].data, value == Qt.Checked)
|
||||
return True
|
||||
|
||||
def isCheckable(self, index):
|
||||
return (index.isValid() and index.column() == 0
|
||||
and self.rows[index.row()].data.get('type') in ('artist', 'album', 'song'))
|
||||
|
||||
def refreshChecks(self):
|
||||
if self.rows and self.headers:
|
||||
self.dataChanged.emit(self.index(0, 0), self.index(len(self.rows) - 1, 0),
|
||||
[Qt.CheckStateRole])
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if role == Qt.DisplayRole:
|
||||
return self.headers[section] if orientation == Qt.Horizontal else str(section + 1)
|
||||
@@ -79,7 +106,27 @@ class LibraryTableModel(QAbstractTableModel):
|
||||
class LibraryTable(QTableView):
|
||||
backRequested = Signal()
|
||||
|
||||
def event(self, event):
|
||||
# Keep Space available for native checkbox operation while the library
|
||||
# table has focus instead of letting the window's play/pause shortcut
|
||||
# consume it first.
|
||||
if event.type() == QEvent.ShortcutOverride and event.key() == Qt.Key_Space:
|
||||
index = self.currentIndex()
|
||||
if index.isValid() and self.model().isCheckable(self.model().index(index.row(), 0)):
|
||||
event.accept()
|
||||
return True
|
||||
return super().event(event)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Space:
|
||||
index = self.currentIndex()
|
||||
checkIndex = self.model().index(index.row(), 0) if index.isValid() else QModelIndex()
|
||||
if self.model().isCheckable(checkIndex):
|
||||
checked = self.model().data(checkIndex, Qt.CheckStateRole) == Qt.Checked
|
||||
self.model().setData(
|
||||
checkIndex, Qt.Unchecked if checked else Qt.Checked, Qt.CheckStateRole)
|
||||
event.accept()
|
||||
return
|
||||
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
||||
if self.currentIndex().isValid():
|
||||
self.activated.emit(self.currentIndex())
|
||||
@@ -140,20 +187,25 @@ class BrowserPage(QWidget):
|
||||
self.playButton.clicked.connect(self.playSelected)
|
||||
bar.addWidget(self.playButton)
|
||||
self.actionsButton = QPushButton('Actions')
|
||||
self.actionsButton.setAccessibleName('Actions for selected library row')
|
||||
self.actionsButton.setAccessibleName('Actions for current or checked library items')
|
||||
self.actionsButton.clicked.connect(self.selectedMenu)
|
||||
bar.addWidget(self.actionsButton)
|
||||
self.collectionButton = QPushButton('Collection actions')
|
||||
self.collectionButton.clicked.connect(self.collectionMenu)
|
||||
bar.addWidget(self.collectionButton)
|
||||
self.clearCheckedButton = QPushButton('Clear checked')
|
||||
self.clearCheckedButton.setAccessibleName('Clear all checked library items')
|
||||
self.clearCheckedButton.clicked.connect(self.browser.clearChecked)
|
||||
bar.addWidget(self.clearCheckedButton)
|
||||
bar.addStretch()
|
||||
layout.addLayout(bar)
|
||||
self.table = LibraryTable()
|
||||
self.table.setAccessibleName(f'{name} table')
|
||||
self.table.setAccessibleDescription(
|
||||
'Enter opens a collection or plays a song. Backspace returns to the previous table. '
|
||||
'Shift+F10 opens actions. Control+Tab changes library tabs.')
|
||||
self.model = LibraryTableModel(self.table)
|
||||
'Space checks or unchecks an artist, album, or song. Alt+A or Shift+F10 opens actions. '
|
||||
'Control+Tab changes library tabs.')
|
||||
self.model = LibraryTableModel(self.browser, self.table)
|
||||
self.table.setModel(self.model)
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
@@ -181,6 +233,9 @@ class BrowserPage(QWidget):
|
||||
self.backShortcut = QShortcut(QKeySequence('Alt+Left'), self)
|
||||
self.backShortcut.setContext(Qt.WidgetWithChildrenShortcut)
|
||||
self.backShortcut.activated.connect(self.goBack)
|
||||
self.actionsShortcut = QShortcut(QKeySequence('Alt+A'), self)
|
||||
self.actionsShortcut.setContext(Qt.WidgetWithChildrenShortcut)
|
||||
self.actionsShortcut.activated.connect(self.selectedMenu)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -198,9 +253,13 @@ class BrowserPage(QWidget):
|
||||
|
||||
def updateButtons(self, *args):
|
||||
data = self.selectedData()
|
||||
self.playButton.setEnabled(bool(data and data['type'] in (
|
||||
'song', 'album', 'artist', 'playlist', 'genre')))
|
||||
self.actionsButton.setEnabled(data is not None)
|
||||
checkedCount = self.browser.checkedCount()
|
||||
self.playButton.setText(f'Play checked ({checkedCount})' if checkedCount else 'Play selected')
|
||||
self.actionsButton.setText(f'Actions ({checkedCount} checked)' if checkedCount else 'Actions')
|
||||
self.playButton.setEnabled(bool(checkedCount or (data and data['type'] in (
|
||||
'song', 'album', 'artist', 'playlist', 'genre'))))
|
||||
self.actionsButton.setEnabled(bool(checkedCount or data is not None))
|
||||
self.clearCheckedButton.setEnabled(bool(checkedCount))
|
||||
|
||||
def render(self):
|
||||
state = self.state
|
||||
@@ -217,9 +276,23 @@ class BrowserPage(QWidget):
|
||||
self.moreButton.setEnabled(not self.loading)
|
||||
self.retryButton.hide()
|
||||
count = len(state.rows)
|
||||
self.status.setText(f'{count} row{"s" if count != 1 else ""}.' if count else 'No items found.')
|
||||
rowStatus = f'{count} row{"s" if count != 1 else ""}.' if count else 'No items found.'
|
||||
checkedCount = self.browser.checkedCount()
|
||||
checkedStatus = (f' {checkedCount} item{"s" if checkedCount != 1 else ""} checked.'
|
||||
if checkedCount else '')
|
||||
self.status.setText(rowStatus + checkedStatus)
|
||||
self.updateButtons()
|
||||
|
||||
def checkedStateChanged(self):
|
||||
self.model.refreshChecks()
|
||||
self.updateButtons()
|
||||
count = len(self.state.rows)
|
||||
rowStatus = f'{count} row{"s" if count != 1 else ""}.' if count else 'No items found.'
|
||||
checkedCount = self.browser.checkedCount()
|
||||
checkedStatus = (f' {checkedCount} item{"s" if checkedCount != 1 else ""} checked.'
|
||||
if checkedCount else '')
|
||||
self.status.setText(rowStatus + checkedStatus)
|
||||
|
||||
def remember(self):
|
||||
self.state.selected = max(0, self.table.currentIndex().row())
|
||||
self.state.scroll = self.table.verticalScrollBar().value()
|
||||
@@ -289,7 +362,7 @@ class BrowserPage(QWidget):
|
||||
on_success=apply, on_error=failed)
|
||||
|
||||
def playSelected(self):
|
||||
data = self.selectedData()
|
||||
data = self.browser.actionData(self.selectedData())
|
||||
if data:
|
||||
self.browser.playRequested.emit(data)
|
||||
|
||||
@@ -297,11 +370,11 @@ class BrowserPage(QWidget):
|
||||
index = self.table.indexAt(position)
|
||||
if index.isValid():
|
||||
self.table.setCurrentIndex(index)
|
||||
self.browser.menuRequested.emit(self.selectedData(),
|
||||
self.browser.menuRequested.emit(self.browser.actionData(self.selectedData()),
|
||||
self.table.viewport().mapToGlobal(position))
|
||||
|
||||
def selectedMenu(self):
|
||||
data = self.selectedData()
|
||||
data = self.browser.actionData(self.selectedData())
|
||||
if data:
|
||||
self.browser.menuRequested.emit(data, self.actionsButton.mapToGlobal(
|
||||
self.actionsButton.rect().bottomLeft()))
|
||||
@@ -320,6 +393,7 @@ class LibraryBrowser(QTabWidget):
|
||||
super().__init__(parent)
|
||||
self.runTask, self.currentSong = runTask, currentSong
|
||||
self.client = None
|
||||
self._checked = {}
|
||||
self.setAccessibleName('Library categories')
|
||||
self.pages = {}
|
||||
for name in ('Artists', 'Albums', 'Favorites', 'Playlists', 'Genres', 'Discover'):
|
||||
@@ -331,6 +405,7 @@ class LibraryBrowser(QTabWidget):
|
||||
|
||||
def clear(self):
|
||||
self.client = None
|
||||
self.clearChecked()
|
||||
for name, page in self.pages.items():
|
||||
page.reset(ViewState(name, (name,)))
|
||||
|
||||
@@ -363,6 +438,54 @@ class LibraryBrowser(QTabWidget):
|
||||
def selectedData(self):
|
||||
return self.currentWidget().selectedData()
|
||||
|
||||
@staticmethod
|
||||
def itemKey(data):
|
||||
itemType = data.get('type')
|
||||
itemId = data.get('id')
|
||||
return (itemType, itemId) if itemType in ('artist', 'album', 'song') and itemId else None
|
||||
|
||||
def isChecked(self, data):
|
||||
key = self.itemKey(data)
|
||||
return key in self._checked if key else False
|
||||
|
||||
def setChecked(self, data, checked):
|
||||
key = self.itemKey(data)
|
||||
if not key:
|
||||
return
|
||||
if checked:
|
||||
self._checked[key] = dict(data)
|
||||
else:
|
||||
self._checked.pop(key, None)
|
||||
self._refreshCheckedState()
|
||||
|
||||
def clearChecked(self):
|
||||
if not self._checked:
|
||||
return
|
||||
self._checked.clear()
|
||||
self._refreshCheckedState()
|
||||
|
||||
def checkedCount(self):
|
||||
return len(self._checked)
|
||||
|
||||
def checkedData(self):
|
||||
return [dict(data) for data in self._checked.values()]
|
||||
|
||||
def actionData(self, fallback=None):
|
||||
items = self.checkedData()
|
||||
if items:
|
||||
count = len(items)
|
||||
return {
|
||||
'type': 'checked_items',
|
||||
'items': items,
|
||||
'label': f'{count} checked item{"s" if count != 1 else ""}',
|
||||
}
|
||||
return fallback
|
||||
|
||||
def _refreshCheckedState(self):
|
||||
for page in self.pages.values():
|
||||
if page.history:
|
||||
page.checkedStateChanged()
|
||||
|
||||
def focusTable(self):
|
||||
self.currentWidget().table.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
|
||||
@@ -235,6 +235,86 @@ def test_collection_actions_and_queue_use_current_payload(window, monkeypatch):
|
||||
assert targets[-1]['type'] == 'artist'
|
||||
|
||||
|
||||
def test_checked_items_persist_across_tabs_and_alt_a_uses_the_group(window):
|
||||
w, pending = window
|
||||
browser = w.libraryBrowser
|
||||
artistsPage = browser.pages['Artists']
|
||||
|
||||
QTest.keyClick(artistsPage.table, Qt.Key_Space)
|
||||
assert artistsPage.model.data(artistsPage.model.index(0, 0), Qt.CheckStateRole) == Qt.Checked
|
||||
|
||||
browser.setCurrentWidget(browser.pages['Albums'])
|
||||
finish(pending)
|
||||
albumsPage = browser.currentWidget()
|
||||
QTest.keyClick(albumsPage.table, Qt.Key_Space)
|
||||
|
||||
browser.setCurrentWidget(browser.pages['Favorites'])
|
||||
favoritesPage = browser.currentWidget()
|
||||
QTest.keyClick(favoritesPage.table, Qt.Key_Space)
|
||||
|
||||
group = browser.actionData(favoritesPage.selectedData())
|
||||
assert group['type'] == 'checked_items'
|
||||
assert [item['type'] for item in group['items']] == ['artist', 'album', 'song']
|
||||
assert favoritesPage.actionsButton.text() == 'Actions (3 checked)'
|
||||
assert '3 items checked' in favoritesPage.status.text()
|
||||
|
||||
targets = []
|
||||
w.addItemToQueue = targets.append
|
||||
def choose_queue():
|
||||
menu = QApplication.activePopupWidget()
|
||||
action = next(action for action in menu.actions()
|
||||
if action.text() == 'Add Checked Items to Queue')
|
||||
menu.close()
|
||||
action.trigger()
|
||||
QTimer.singleShot(0, choose_queue)
|
||||
QTest.keyClick(favoritesPage.table, Qt.Key_A, Qt.AltModifier)
|
||||
assert targets[-1]['type'] == 'checked_items'
|
||||
assert len(targets[-1]['items']) == 3
|
||||
|
||||
favoritesPage.clearCheckedButton.click()
|
||||
assert browser.checkedCount() == 0
|
||||
assert artistsPage.model.data(artistsPage.model.index(0, 0), Qt.CheckStateRole) == Qt.Unchecked
|
||||
|
||||
|
||||
def test_checked_collections_expand_and_deduplicate_songs(window):
|
||||
w, _ = window
|
||||
selectedSong = w.client.getAlbum.return_value['songs'][0]
|
||||
result, label, truncated = w.collectSongsForItem({
|
||||
'type': 'checked_items',
|
||||
'label': '3 checked items',
|
||||
'items': [
|
||||
{'type': 'artist', 'id': 'artist', 'label': 'Artist'},
|
||||
{'type': 'album', 'id': 'album', 'label': 'Album'},
|
||||
{'type': 'song', 'id': selectedSong.id, 'label': selectedSong.title,
|
||||
'song': selectedSong},
|
||||
],
|
||||
})
|
||||
|
||||
assert result == [selectedSong]
|
||||
assert label == '3 checked items'
|
||||
assert not truncated
|
||||
|
||||
|
||||
def test_checked_favorite_action_attempts_every_item(window):
|
||||
w, _ = window
|
||||
w.client.star.side_effect = [None, RuntimeError('synthetic failure'), None]
|
||||
items = [
|
||||
{'type': 'artist', 'id': 'artist', 'label': 'Artist'},
|
||||
{'type': 'album', 'id': 'album', 'label': 'Album'},
|
||||
{'type': 'song', 'id': 'song', 'label': 'Song'},
|
||||
]
|
||||
|
||||
updated, failed = w._setCheckedItemsFavorite(items, True)
|
||||
|
||||
assert updated == 2
|
||||
assert failed == ['Album']
|
||||
assert [entry.args for entry in w.client.star.call_args_list] == [
|
||||
('artist', 'artist'),
|
||||
('album', 'album'),
|
||||
('song', 'song'),
|
||||
]
|
||||
|
||||
|
||||
def test_actual_background_request_does_not_block_back_navigation(window):
|
||||
import threading
|
||||
import time
|
||||
|
||||
Reference in new issue
Block a user