Compare commits
6 Commits
639f006a10
...
589a664971
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
589a664971 | ||
|
|
67bf926ed8 | ||
|
|
040020e7ed | ||
|
|
911859ef49 | ||
|
|
68bdff53cf | ||
|
|
632937101a |
@ -31,7 +31,6 @@ class Config(object):
|
|||||||
COLOUR_NORMAL_TAB = "#000000"
|
COLOUR_NORMAL_TAB = "#000000"
|
||||||
COLOUR_NOTES_PLAYLIST = "#b8daff"
|
COLOUR_NOTES_PLAYLIST = "#b8daff"
|
||||||
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
||||||
COLOUR_TEMPLATE_ROW = "#FFAF68"
|
|
||||||
COLOUR_UNREADABLE = "#dc3545"
|
COLOUR_UNREADABLE = "#dc3545"
|
||||||
COLOUR_WARNING_TIMER = "#ffc107"
|
COLOUR_WARNING_TIMER = "#ffc107"
|
||||||
DBFS_SILENCE = -50
|
DBFS_SILENCE = -50
|
||||||
@ -96,6 +95,7 @@ class Config(object):
|
|||||||
PLAY_SETTLE = 500000
|
PLAY_SETTLE = 500000
|
||||||
PLAYLIST_ICON_CURRENT = ":/icons/green-circle.png"
|
PLAYLIST_ICON_CURRENT = ":/icons/green-circle.png"
|
||||||
PLAYLIST_ICON_NEXT = ":/icons/yellow-circle.png"
|
PLAYLIST_ICON_NEXT = ":/icons/yellow-circle.png"
|
||||||
|
PLAYLIST_ICON_TEMPLATE = ":/icons/redstar.png"
|
||||||
PREVIEW_ADVANCE_MS = 5000
|
PREVIEW_ADVANCE_MS = 5000
|
||||||
PREVIEW_BACK_MS = 5000
|
PREVIEW_BACK_MS = 5000
|
||||||
PREVIEW_END_BUFFER_MS = 1000
|
PREVIEW_END_BUFFER_MS = 1000
|
||||||
|
|||||||
@ -179,12 +179,18 @@ class Playdates(dbtables.PlaydatesTable):
|
|||||||
|
|
||||||
|
|
||||||
class Playlists(dbtables.PlaylistsTable):
|
class Playlists(dbtables.PlaylistsTable):
|
||||||
def __init__(self, session: Session, name: str):
|
def __init__(self, session: Session, name: str, template_id: int) -> None:
|
||||||
|
"""Create playlist with passed name"""
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.last_used = dt.datetime.now()
|
self.last_used = dt.datetime.now()
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
# If a template is specified, copy from it
|
||||||
|
if template_id:
|
||||||
|
PlaylistRows.copy_playlist(session, template_id, self.id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clear_tabs(session: Session, playlist_ids: list[int]) -> None:
|
def clear_tabs(session: Session, playlist_ids: list[int]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -201,26 +207,6 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
self.open = False
|
self.open = False
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_playlist_from_template(
|
|
||||||
cls, session: Session, template_id: int, playlist_name: str
|
|
||||||
) -> Optional["Playlists"]:
|
|
||||||
"""Create a new playlist from template"""
|
|
||||||
|
|
||||||
# Sanity check
|
|
||||||
if not template_id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
playlist = cls(session, playlist_name)
|
|
||||||
|
|
||||||
# Sanity / mypy checks
|
|
||||||
if not playlist or not playlist.id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
PlaylistRows.copy_playlist(session, template_id, playlist.id)
|
|
||||||
|
|
||||||
return playlist
|
|
||||||
|
|
||||||
def delete(self, session: Session) -> None:
|
def delete(self, session: Session) -> None:
|
||||||
"""
|
"""
|
||||||
Delete playlist
|
Delete playlist
|
||||||
@ -247,6 +233,19 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
|
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_favourite_templates(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
|
"""Returns a list of favourite templates ordered by name"""
|
||||||
|
|
||||||
|
return session.scalars(
|
||||||
|
select(cls)
|
||||||
|
.where(
|
||||||
|
cls.is_template.is_(True),
|
||||||
|
cls.favourite.is_(True)
|
||||||
|
)
|
||||||
|
.order_by(cls.name)
|
||||||
|
).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
"""Returns a list of all closed playlists ordered by last use"""
|
"""Returns a list of all closed playlists ordered by last use"""
|
||||||
@ -301,7 +300,7 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Save passed playlist as new template"""
|
"""Save passed playlist as new template"""
|
||||||
|
|
||||||
template = Playlists(session, template_name)
|
template = Playlists(session, template_name, template_id=0)
|
||||||
if not template or not template.id:
|
if not template or not template.id:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -596,7 +595,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
|
|||||||
|
|
||||||
|
|
||||||
class Settings(dbtables.SettingsTable):
|
class Settings(dbtables.SettingsTable):
|
||||||
def __init__(self, session: Session, name: str):
|
def __init__(self, session: Session, name: str) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -624,7 +623,7 @@ class Tracks(dbtables.TracksTable):
|
|||||||
fade_at: int,
|
fade_at: int,
|
||||||
silence_at: int,
|
silence_at: int,
|
||||||
bitrate: int,
|
bitrate: int,
|
||||||
):
|
) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.title = title
|
self.title = title
|
||||||
self.artist = artist
|
self.artist = artist
|
||||||
|
|||||||
@ -191,7 +191,7 @@ class ItemlistManager(QDialog):
|
|||||||
if not hh:
|
if not hh:
|
||||||
raise ApplicationError("ItemlistManager failed to create horizontalHeader")
|
raise ApplicationError("ItemlistManager failed to create horizontalHeader")
|
||||||
hh.setStretchLastSection(True)
|
hh.setStretchLastSection(True)
|
||||||
self.table.setColumnWidth(0, 200)
|
self.table.setColumnWidth(0, 288)
|
||||||
self.table.setColumnWidth(1, 300)
|
self.table.setColumnWidth(1, 300)
|
||||||
|
|
||||||
self.populate_table()
|
self.populate_table()
|
||||||
@ -267,7 +267,6 @@ class ItemlistManager(QDialog):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def toggle_favourite(self, item_id: int, checked: bool) -> None:
|
def toggle_favourite(self, item_id: int, checked: bool) -> None:
|
||||||
print(f"Toggle favourite for item {item_id}: {checked}")
|
|
||||||
self.callbacks.favourite(item_id, checked)
|
self.callbacks.favourite(item_id, checked)
|
||||||
|
|
||||||
for row in range(self.table.rowCount()):
|
for row in range(self.table.rowCount()):
|
||||||
@ -602,6 +601,61 @@ class Window(QMainWindow):
|
|||||||
self.load_last_playlists()
|
self.load_last_playlists()
|
||||||
self.stop_autoplay = False
|
self.stop_autoplay = False
|
||||||
|
|
||||||
|
# # # # # # # # # # Overrides # # # # # # # # # #
|
||||||
|
|
||||||
|
def closeEvent(self, event: Optional[QCloseEvent]) -> None:
|
||||||
|
"""Handle attempt to close main window"""
|
||||||
|
|
||||||
|
if not event:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Don't allow window to close when a track is playing
|
||||||
|
if track_sequence.current and track_sequence.current.is_playing():
|
||||||
|
event.ignore()
|
||||||
|
helpers.show_warning(
|
||||||
|
self, "Track playing", "Can't close application while track is playing"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
with db.Session() as session:
|
||||||
|
# Save tab number of open playlists
|
||||||
|
open_playlist_ids: dict[int, int] = {}
|
||||||
|
for idx in range(self.playlist_section.tabPlaylist.count()):
|
||||||
|
open_playlist_ids[
|
||||||
|
self.playlist_section.tabPlaylist.widget(idx).playlist_id
|
||||||
|
] = idx
|
||||||
|
Playlists.clear_tabs(session, list(open_playlist_ids.keys()))
|
||||||
|
for playlist_id, idx in open_playlist_ids.items():
|
||||||
|
playlist = session.get(Playlists, playlist_id)
|
||||||
|
if playlist:
|
||||||
|
log.debug(f"Set {playlist=} tab to {idx=}")
|
||||||
|
playlist.tab = idx
|
||||||
|
|
||||||
|
# Save window attributes
|
||||||
|
attributes_to_save = dict(
|
||||||
|
mainwindow_height=self.height(),
|
||||||
|
mainwindow_width=self.width(),
|
||||||
|
mainwindow_x=self.x(),
|
||||||
|
mainwindow_y=self.y(),
|
||||||
|
active_tab=self.playlist_section.tabPlaylist.currentIndex(),
|
||||||
|
)
|
||||||
|
for name, value in attributes_to_save.items():
|
||||||
|
record = Settings.get_setting(session, name)
|
||||||
|
record.f_int = value
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
# # # # # # # # # # Internal utility functions # # # # # # # # # #
|
||||||
|
|
||||||
|
def active_base_model(self) -> PlaylistModel:
|
||||||
|
return self.current.base_model
|
||||||
|
|
||||||
|
def active_tab(self) -> PlaylistTab:
|
||||||
|
return self.playlist_section.tabPlaylist.currentWidget()
|
||||||
|
|
||||||
|
# # # # # # # # # # Menu functions # # # # # # # # # #
|
||||||
|
|
||||||
def create_action(
|
def create_action(
|
||||||
self, text: str, handler: Callable, shortcut: Optional[str] = None
|
self, text: str, handler: Callable, shortcut: Optional[str] = None
|
||||||
) -> QAction:
|
) -> QAction:
|
||||||
@ -620,7 +674,7 @@ class Window(QMainWindow):
|
|||||||
menu_bar = self.menuBar()
|
menu_bar = self.menuBar()
|
||||||
|
|
||||||
# Load menu structure from YAML file
|
# Load menu structure from YAML file
|
||||||
with open("menu.yaml", "r") as file:
|
with open("app/menu.yaml", "r") as file:
|
||||||
menu_data = yaml.safe_load(file)
|
menu_data = yaml.safe_load(file)
|
||||||
|
|
||||||
self.menu_actions = {} # Store reference for enabling/disabling actions
|
self.menu_actions = {} # Store reference for enabling/disabling actions
|
||||||
@ -671,26 +725,231 @@ class Window(QMainWindow):
|
|||||||
items = getattr(self, f"get_{key}_items")()
|
items = getattr(self, f"get_{key}_items")()
|
||||||
for item in items:
|
for item in items:
|
||||||
action = QAction(item["text"], self)
|
action = QAction(item["text"], self)
|
||||||
action.triggered.connect(
|
|
||||||
lambda _, i=item["handler"]: getattr(self, i)()
|
# Extract handler and arguments
|
||||||
)
|
handler = getattr(self, item["handler"], None)
|
||||||
|
args = item.get("args", ())
|
||||||
|
|
||||||
|
if handler:
|
||||||
|
# Use a lambda to pass arguments to the function
|
||||||
|
action.triggered.connect(lambda _, h=handler, a=args: h(*a))
|
||||||
|
|
||||||
submenu.addAction(action)
|
submenu.addAction(action)
|
||||||
break
|
break
|
||||||
|
|
||||||
def get_new_playlist_dynamic_submenu_items(self):
|
def get_new_playlist_dynamic_submenu_items(
|
||||||
"""Returns dynamically generated menu items for Submenu 1."""
|
self,
|
||||||
return [
|
) -> list[dict[str, str | tuple[Session, int]]]:
|
||||||
{"text": "Option A", "handler": "option_a_handler"},
|
"""
|
||||||
{"text": "Option B", "handler": "option_b_handler"},
|
Return dynamically generated menu items, in this case
|
||||||
|
templates marked as favourite from which to generate a
|
||||||
|
new playlist.
|
||||||
|
|
||||||
|
The handler is to call create_playlist with a session
|
||||||
|
and template_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
submenu_items: list[dict[str, str | tuple[Session, int]]] = [
|
||||||
|
{"text": "Show all",
|
||||||
|
"handler": "create_playlist_from_template",
|
||||||
|
"args": (session, 0)
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
templates = Playlists.get_favourite_templates(session)
|
||||||
|
for template in templates:
|
||||||
|
submenu_items.append(
|
||||||
|
{
|
||||||
|
"text": template.name,
|
||||||
|
"handler": "create_playlist_from_template",
|
||||||
|
"args": (
|
||||||
|
session,
|
||||||
|
template.id,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return submenu_items
|
||||||
|
|
||||||
def get_query_dynamic_submenu_items(self):
|
def get_query_dynamic_submenu_items(self):
|
||||||
"""Returns dynamically generated menu items for Submenu 2."""
|
"""Returns dynamically generated menu items for Submenu 2."""
|
||||||
return [
|
return [
|
||||||
{"text": "Action X", "handler": "action_x_handler"},
|
{"text": "Action Xargs", "handler": "kae", "args": (21,)},
|
||||||
{"text": "Action Y", "handler": "action_y_handler"},
|
{"text": "Action Y", "handler": "action_y_handler"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# # # # # # # # # # Playlist management functions # # # # # # # # # #
|
||||||
|
|
||||||
|
def _create_playlist(
|
||||||
|
self, session: Session, name: str, template_id: int
|
||||||
|
) -> Playlists:
|
||||||
|
"""
|
||||||
|
Create a playlist in the database, populate it from the template
|
||||||
|
if template_id > 0, and return the Playlists object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.debug(f" _create_playlist({name=}, {template_id=})")
|
||||||
|
|
||||||
|
return Playlists(session, name, template_id)
|
||||||
|
|
||||||
|
def _open_playlist(self, playlist: Playlists, is_template: bool = False) -> int:
|
||||||
|
"""
|
||||||
|
With passed playlist:
|
||||||
|
- create models
|
||||||
|
- create tab
|
||||||
|
- switch to tab
|
||||||
|
- mark playist as open
|
||||||
|
return: tab index
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.debug(f" _open_playlist({playlist=}, {is_template=})")
|
||||||
|
|
||||||
|
# Create base model and proxy model
|
||||||
|
base_model = PlaylistModel(playlist.id, is_template)
|
||||||
|
proxy_model = PlaylistProxyModel()
|
||||||
|
proxy_model.setSourceModel(base_model)
|
||||||
|
|
||||||
|
# Create tab
|
||||||
|
playlist_tab = PlaylistTab(musicmuster=self, model=proxy_model)
|
||||||
|
idx = self.playlist_section.tabPlaylist.addTab(playlist_tab, playlist.name)
|
||||||
|
|
||||||
|
# Mark playlist as open
|
||||||
|
playlist.mark_open()
|
||||||
|
|
||||||
|
# Switch to new tab
|
||||||
|
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||||
|
self.update_playlist_icons()
|
||||||
|
|
||||||
|
return idx
|
||||||
|
|
||||||
|
def create_playlist_from_template(self, session: Session, template_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Prompt for new playlist name and create from passed template_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
if template_id == 0:
|
||||||
|
# Show all templates
|
||||||
|
selected_template_id = self.solicit_template_to_use(session)
|
||||||
|
if selected_template_id is None:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
template_id = selected_template_id
|
||||||
|
|
||||||
|
playlist_name = self.solicit_playlist_name(session)
|
||||||
|
if not playlist_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
playlist = self._create_playlist(session, playlist_name, template_id)
|
||||||
|
self._open_playlist(playlist)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
def delete_playlist(self) -> None:
|
||||||
|
"""
|
||||||
|
Delete current playlist
|
||||||
|
"""
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist_id = self.current.playlist_id
|
||||||
|
playlist = session.get(Playlists, playlist_id)
|
||||||
|
if playlist:
|
||||||
|
if helpers.ask_yes_no(
|
||||||
|
"Delete playlist",
|
||||||
|
f"Delete playlist '{playlist.name}': " "Are you sure?",
|
||||||
|
):
|
||||||
|
if self.close_playlist_tab():
|
||||||
|
playlist.delete(session)
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
log.error("Failed to retrieve playlist")
|
||||||
|
|
||||||
|
def open_existing_playlist(self) -> None:
|
||||||
|
"""Open existing playlist"""
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
playlists = Playlists.get_closed(session)
|
||||||
|
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
||||||
|
dlg.exec()
|
||||||
|
playlist = dlg.playlist
|
||||||
|
if playlist:
|
||||||
|
self._open_playlist(playlist)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
def save_as_template(self) -> None:
|
||||||
|
"""Save current playlist as template"""
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
template_names = [a.name for a in Playlists.get_all_templates(session)]
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Get name for new template
|
||||||
|
dlg = QInputDialog(self)
|
||||||
|
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||||
|
dlg.setLabelText("Template name:")
|
||||||
|
dlg.resize(500, 100)
|
||||||
|
ok = dlg.exec()
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
template_name = dlg.textValue()
|
||||||
|
if template_name not in template_names:
|
||||||
|
break
|
||||||
|
helpers.show_warning(
|
||||||
|
self, "Duplicate template", "Template name already in use"
|
||||||
|
)
|
||||||
|
Playlists.save_as_template(session, self.current.playlist_id, template_name)
|
||||||
|
session.commit()
|
||||||
|
helpers.show_OK("Template", "Template saved", self)
|
||||||
|
|
||||||
|
def solicit_playlist_name(
|
||||||
|
self, session: Session, default: str = "", prompt: str = "Playlist name:"
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Get name of new playlist from user"""
|
||||||
|
|
||||||
|
dlg = QInputDialog(self)
|
||||||
|
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||||
|
dlg.setLabelText(prompt)
|
||||||
|
while True:
|
||||||
|
if default:
|
||||||
|
dlg.setTextValue(default)
|
||||||
|
dlg.resize(500, 100)
|
||||||
|
ok = dlg.exec()
|
||||||
|
if ok:
|
||||||
|
proposed_name = dlg.textValue()
|
||||||
|
if Playlists.name_is_available(session, proposed_name):
|
||||||
|
return proposed_name
|
||||||
|
else:
|
||||||
|
helpers.show_warning(
|
||||||
|
self,
|
||||||
|
"Name in use",
|
||||||
|
f"There's already a playlist called '{proposed_name}'",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def solicit_template_to_use(
|
||||||
|
self, session: Session, template_prompt: Optional[str] = None
|
||||||
|
) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Have user select a template. Return the template.id, or None if they cancel.
|
||||||
|
template_id of zero means don't use a template.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name_id_list: list[tuple[str, int]] = []
|
||||||
|
template_name_id_list.append((Config.NO_TEMPLATE_NAME, 0))
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
for template in Playlists.get_all_templates(session):
|
||||||
|
template_name_id_list.append((template.name, template.id))
|
||||||
|
|
||||||
|
dlg = TemplateSelectorDialog(template_name_id_list, template_prompt)
|
||||||
|
if not dlg.exec() or dlg.selected_id is None:
|
||||||
|
return None # User cancelled
|
||||||
|
|
||||||
|
return dlg.selected_id
|
||||||
|
|
||||||
|
# # # # # # # # # # Miscellaneous functions # # # # # # # # # #
|
||||||
|
|
||||||
def select_duplicate_rows(self) -> None:
|
def select_duplicate_rows(self) -> None:
|
||||||
"""Call playlist to select duplicate rows"""
|
"""Call playlist to select duplicate rows"""
|
||||||
|
|
||||||
@ -717,12 +976,6 @@ class Window(QMainWindow):
|
|||||||
QMessageBox.StandardButton.Ok,
|
QMessageBox.StandardButton.Ok,
|
||||||
)
|
)
|
||||||
|
|
||||||
def active_base_model(self) -> PlaylistModel:
|
|
||||||
return self.current.base_model
|
|
||||||
|
|
||||||
def active_tab(self) -> PlaylistTab:
|
|
||||||
return self.playlist_section.tabPlaylist.currentWidget()
|
|
||||||
|
|
||||||
def clear_next(self) -> None:
|
def clear_next(self) -> None:
|
||||||
"""
|
"""
|
||||||
Clear next track
|
Clear next track
|
||||||
@ -741,49 +994,6 @@ class Window(QMainWindow):
|
|||||||
# Clear the search bar
|
# Clear the search bar
|
||||||
self.search_playlist_clear()
|
self.search_playlist_clear()
|
||||||
|
|
||||||
def closeEvent(self, event: Optional[QCloseEvent]) -> None:
|
|
||||||
"""Handle attempt to close main window"""
|
|
||||||
|
|
||||||
if not event:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Don't allow window to close when a track is playing
|
|
||||||
if track_sequence.current and track_sequence.current.is_playing():
|
|
||||||
event.ignore()
|
|
||||||
helpers.show_warning(
|
|
||||||
self, "Track playing", "Can't close application while track is playing"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
with db.Session() as session:
|
|
||||||
# Save tab number of open playlists
|
|
||||||
open_playlist_ids: dict[int, int] = {}
|
|
||||||
for idx in range(self.playlist_section.tabPlaylist.count()):
|
|
||||||
open_playlist_ids[
|
|
||||||
self.playlist_section.tabPlaylist.widget(idx).playlist_id
|
|
||||||
] = idx
|
|
||||||
Playlists.clear_tabs(session, list(open_playlist_ids.keys()))
|
|
||||||
for playlist_id, idx in open_playlist_ids.items():
|
|
||||||
playlist = session.get(Playlists, playlist_id)
|
|
||||||
if playlist:
|
|
||||||
log.debug(f"Set {playlist=} tab to {idx=}")
|
|
||||||
playlist.tab = idx
|
|
||||||
|
|
||||||
# Save window attributes
|
|
||||||
attributes_to_save = dict(
|
|
||||||
mainwindow_height=self.height(),
|
|
||||||
mainwindow_width=self.width(),
|
|
||||||
mainwindow_x=self.x(),
|
|
||||||
mainwindow_y=self.y(),
|
|
||||||
active_tab=self.playlist_section.tabPlaylist.currentIndex(),
|
|
||||||
)
|
|
||||||
for name, value in attributes_to_save.items():
|
|
||||||
record = Settings.get_setting(session, name)
|
|
||||||
record.f_int = value
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
def close_playlist_tab(self) -> bool:
|
def close_playlist_tab(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Close active playlist tab, called by menu item
|
Close active playlist tab, called by menu item
|
||||||
@ -870,53 +1080,6 @@ class Window(QMainWindow):
|
|||||||
self.signals.search_songfacts_signal.connect(self.open_songfacts_browser)
|
self.signals.search_songfacts_signal.connect(self.open_songfacts_browser)
|
||||||
self.signals.search_wikipedia_signal.connect(self.open_wikipedia_browser)
|
self.signals.search_wikipedia_signal.connect(self.open_wikipedia_browser)
|
||||||
|
|
||||||
def create_playlist(
|
|
||||||
self, session: Session, template_id: int
|
|
||||||
) -> Optional[Playlists]:
|
|
||||||
"""Create new playlist"""
|
|
||||||
|
|
||||||
# Get a name for this new playlist
|
|
||||||
playlist_name = self.solicit_playlist_name(session)
|
|
||||||
if not playlist_name:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# If template.id == 0, user doesn't want a template
|
|
||||||
playlist: Optional[Playlists]
|
|
||||||
if template_id == 0:
|
|
||||||
playlist = Playlists(session, playlist_name)
|
|
||||||
else:
|
|
||||||
playlist = Playlists.create_playlist_from_template(
|
|
||||||
session, template_id, playlist_name
|
|
||||||
)
|
|
||||||
|
|
||||||
if playlist:
|
|
||||||
return playlist
|
|
||||||
else:
|
|
||||||
log.error(f"Failed to create playlist, {playlist_name=}")
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def create_playlist_tab(self, playlist: Playlists, is_template: bool = False) -> int:
|
|
||||||
"""
|
|
||||||
Take the passed playlist, create a playlist tab and
|
|
||||||
add tab to display. Return index number of tab.
|
|
||||||
"""
|
|
||||||
|
|
||||||
log.debug(f"create_playlist_tab({playlist=})")
|
|
||||||
|
|
||||||
# Create model and proxy model
|
|
||||||
base_model = PlaylistModel(playlist.id, is_template)
|
|
||||||
proxy_model = PlaylistProxyModel()
|
|
||||||
proxy_model.setSourceModel(base_model)
|
|
||||||
|
|
||||||
# Create tab
|
|
||||||
playlist_tab = PlaylistTab(musicmuster=self, model=proxy_model)
|
|
||||||
idx = self.playlist_section.tabPlaylist.addTab(playlist_tab, playlist.name)
|
|
||||||
|
|
||||||
log.debug(f"create_playlist_tab() returned: {idx=}")
|
|
||||||
|
|
||||||
return idx
|
|
||||||
|
|
||||||
def current_row_or_end(self) -> int:
|
def current_row_or_end(self) -> int:
|
||||||
"""
|
"""
|
||||||
If a row or rows are selected, return the row number of the first
|
If a row or rows are selected, return the row number of the first
|
||||||
@ -935,25 +1098,6 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
ipdb.set_trace()
|
ipdb.set_trace()
|
||||||
|
|
||||||
def delete_playlist(self) -> None:
|
|
||||||
"""
|
|
||||||
Delete current playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
with db.Session() as session:
|
|
||||||
playlist_id = self.current.playlist_id
|
|
||||||
playlist = session.get(Playlists, playlist_id)
|
|
||||||
if playlist:
|
|
||||||
if helpers.ask_yes_no(
|
|
||||||
"Delete playlist",
|
|
||||||
f"Delete playlist '{playlist.name}': " "Are you sure?",
|
|
||||||
):
|
|
||||||
if self.close_playlist_tab():
|
|
||||||
playlist.delete(session)
|
|
||||||
session.commit()
|
|
||||||
else:
|
|
||||||
log.error("Failed to retrieve playlist")
|
|
||||||
|
|
||||||
def download_played_tracks(self) -> None:
|
def download_played_tracks(self) -> None:
|
||||||
"""Download a CSV of played tracks"""
|
"""Download a CSV of played tracks"""
|
||||||
|
|
||||||
@ -1161,7 +1305,7 @@ class Window(QMainWindow):
|
|||||||
if playlist:
|
if playlist:
|
||||||
log.debug(f"load_last_playlists() loaded {playlist=}")
|
log.debug(f"load_last_playlists() loaded {playlist=}")
|
||||||
# Create tab
|
# Create tab
|
||||||
playlist_ids.append(self.create_playlist_tab(playlist))
|
playlist_ids.append(self._open_playlist(playlist))
|
||||||
|
|
||||||
# Set active tab
|
# Set active tab
|
||||||
record = Settings.get_setting(session, "active_tab")
|
record = Settings.get_setting(session, "active_tab")
|
||||||
@ -1240,28 +1384,39 @@ class Window(QMainWindow):
|
|||||||
)
|
)
|
||||||
# Simply load the template as a playlist. Any changes
|
# Simply load the template as a playlist. Any changes
|
||||||
# made will persist
|
# made will persist
|
||||||
idx = self.create_playlist_tab(template, is_template=True)
|
self._open_playlist(template, is_template=True)
|
||||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
|
||||||
|
|
||||||
def favourite(template_id: int, favourite: bool) -> None:
|
def favourite(template_id: int, favourite: bool) -> None:
|
||||||
"""Mark template as (not) favourite"""
|
"""Mark template as (not) favourite"""
|
||||||
|
|
||||||
print(f"manage_templates.favourite({template_id=}")
|
template = session.get(Playlists, template_id)
|
||||||
print(f"{session=}")
|
template.favourite = favourite
|
||||||
|
session.commit()
|
||||||
|
|
||||||
def new_item() -> None:
|
def new_item() -> None:
|
||||||
"""Create new template"""
|
"""Create new template"""
|
||||||
|
|
||||||
# Get base template
|
# Get base template
|
||||||
template_id = self.solicit_template_to_use(
|
template_id = self.solicit_template_to_use(
|
||||||
session, template_prmompt="New template based upon:"
|
session, template_prompt="New template based upon:"
|
||||||
)
|
)
|
||||||
if template_id is None:
|
if template_id is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
new_template = self.create_playlist(session, template_id)
|
# Get new template name
|
||||||
if new_template:
|
name = self.solicit_playlist_name(
|
||||||
self.open_playlist(session, new_template, is_template=True)
|
session, default="", prompt="New template name:"
|
||||||
|
)
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create playlist for template and mark is as a template
|
||||||
|
template = self._create_playlist(session, name, template_id)
|
||||||
|
template.is_template = True
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Open it for editing
|
||||||
|
self._open_playlist(template, is_template=True)
|
||||||
|
|
||||||
def rename(template_id: int) -> Optional[str]:
|
def rename(template_id: int) -> Optional[str]:
|
||||||
"""rename template"""
|
"""rename template"""
|
||||||
@ -1295,8 +1450,11 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
for template in Playlists.get_all_templates(session):
|
for template in Playlists.get_all_templates(session):
|
||||||
# TODO: need to add in favourites
|
template_list.append(
|
||||||
template_list.append(ItemlistItem(name=template.name, id=template.id))
|
ItemlistItem(
|
||||||
|
name=template.name, id=template.id, favourite=template.favourite
|
||||||
|
)
|
||||||
|
)
|
||||||
# We need to retain a reference to the dialog box to stop it
|
# We need to retain a reference to the dialog box to stop it
|
||||||
# going out of scope and being garbage-collected.
|
# going out of scope and being garbage-collected.
|
||||||
self.dlg = ItemlistManager(template_list, callbacks)
|
self.dlg = ItemlistManager(template_list, callbacks)
|
||||||
@ -1385,53 +1543,6 @@ class Window(QMainWindow):
|
|||||||
self.move_playlist_rows(unplayed_rows)
|
self.move_playlist_rows(unplayed_rows)
|
||||||
self.disable_selection_timing = False
|
self.disable_selection_timing = False
|
||||||
|
|
||||||
def new_playlist(self) -> Optional[Playlists]:
|
|
||||||
"""
|
|
||||||
Create new playlist, optionally from template
|
|
||||||
"""
|
|
||||||
|
|
||||||
with db.Session() as session:
|
|
||||||
template_id = self.solicit_template_to_use(session)
|
|
||||||
if not template_id:
|
|
||||||
return None # User cancelled
|
|
||||||
|
|
||||||
playlist = self.create_playlist(session, template_id)
|
|
||||||
|
|
||||||
if playlist:
|
|
||||||
playlist.mark_open()
|
|
||||||
# Need to ensure that the new playlist is committed to
|
|
||||||
# the database before it is opened by the model.
|
|
||||||
session.commit()
|
|
||||||
idx = self.create_playlist_tab(playlist)
|
|
||||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
|
||||||
return playlist
|
|
||||||
else:
|
|
||||||
ApplicationError("new_playlist: Playlist failed to create")
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def open_existing_playlist(self) -> None:
|
|
||||||
"""Open existing playlist"""
|
|
||||||
|
|
||||||
with db.Session() as session:
|
|
||||||
playlists = Playlists.get_closed(session)
|
|
||||||
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
|
||||||
dlg.exec()
|
|
||||||
playlist = dlg.playlist
|
|
||||||
if playlist:
|
|
||||||
self.open_playlist(session, playlist)
|
|
||||||
|
|
||||||
def open_playlist(
|
|
||||||
self, session: Session, playlist: Playlists, is_template: bool = False
|
|
||||||
) -> None:
|
|
||||||
"""Open passed playlist"""
|
|
||||||
|
|
||||||
idx = self.create_playlist_tab(playlist, is_template)
|
|
||||||
playlist.mark_open()
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
|
||||||
|
|
||||||
def open_songfacts_browser(self, title: str) -> None:
|
def open_songfacts_browser(self, title: str) -> None:
|
||||||
"""Search Songfacts for title"""
|
"""Search Songfacts for title"""
|
||||||
|
|
||||||
@ -1755,32 +1866,6 @@ class Window(QMainWindow):
|
|||||||
)
|
)
|
||||||
track_sequence.current.start_time -= dt.timedelta(milliseconds=elapsed_ms)
|
track_sequence.current.start_time -= dt.timedelta(milliseconds=elapsed_ms)
|
||||||
|
|
||||||
def save_as_template(self) -> None:
|
|
||||||
"""Save current playlist as template"""
|
|
||||||
|
|
||||||
with db.Session() as session:
|
|
||||||
template_names = [a.name for a in Playlists.get_all_templates(session)]
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# Get name for new template
|
|
||||||
dlg = QInputDialog(self)
|
|
||||||
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
|
||||||
dlg.setLabelText("Template name:")
|
|
||||||
dlg.resize(500, 100)
|
|
||||||
ok = dlg.exec()
|
|
||||||
if not ok:
|
|
||||||
return
|
|
||||||
|
|
||||||
template_name = dlg.textValue()
|
|
||||||
if template_name not in template_names:
|
|
||||||
break
|
|
||||||
helpers.show_warning(
|
|
||||||
self, "Duplicate template", "Template name already in use"
|
|
||||||
)
|
|
||||||
Playlists.save_as_template(session, self.current.playlist_id, template_name)
|
|
||||||
session.commit()
|
|
||||||
helpers.show_OK("Template", "Template saved", self)
|
|
||||||
|
|
||||||
def search_playlist(self) -> None:
|
def search_playlist(self) -> None:
|
||||||
"""Show text box to search playlist"""
|
"""Show text box to search playlist"""
|
||||||
|
|
||||||
@ -1907,54 +1992,6 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
self.active_tab().scroll_to_top(playlist_track.row_number)
|
self.active_tab().scroll_to_top(playlist_track.row_number)
|
||||||
|
|
||||||
def solicit_template_to_use(
|
|
||||||
self, session: Session, template_prmompt: Optional[str] = None
|
|
||||||
) -> Optional[int]:
|
|
||||||
"""
|
|
||||||
Have user select a template. Return the template.id, or None if they cancel.
|
|
||||||
template_id of zero means don't use a template.
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name_id_list: list[tuple[str, int]] = []
|
|
||||||
template_name_id_list.append((Config.NO_TEMPLATE_NAME, 0))
|
|
||||||
|
|
||||||
with db.Session() as session:
|
|
||||||
for template in Playlists.get_all_templates(session):
|
|
||||||
template_name_id_list.append((template.name, template.id))
|
|
||||||
|
|
||||||
dlg = TemplateSelectorDialog(template_name_id_list, template_prmompt)
|
|
||||||
if not dlg.exec() or dlg.selected_id is None:
|
|
||||||
return None # User cancelled
|
|
||||||
|
|
||||||
return dlg.selected_id
|
|
||||||
|
|
||||||
def solicit_playlist_name(
|
|
||||||
self, session: Session, default: str = "", prompt: str = "Playlist name:"
|
|
||||||
) -> Optional[str]:
|
|
||||||
"""Get name of new playlist from user"""
|
|
||||||
|
|
||||||
dlg = QInputDialog(self)
|
|
||||||
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
|
||||||
dlg.setLabelText(prompt)
|
|
||||||
while True:
|
|
||||||
if default:
|
|
||||||
dlg.setTextValue(default)
|
|
||||||
dlg.resize(500, 100)
|
|
||||||
ok = dlg.exec()
|
|
||||||
if ok:
|
|
||||||
proposed_name = dlg.textValue()
|
|
||||||
if Playlists.name_is_available(session, proposed_name):
|
|
||||||
return proposed_name
|
|
||||||
else:
|
|
||||||
helpers.show_warning(
|
|
||||||
self,
|
|
||||||
"Name in use",
|
|
||||||
f"There's already a playlist called '{proposed_name}'",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""Stop playing immediately"""
|
"""Stop playing immediately"""
|
||||||
|
|
||||||
@ -2162,6 +2199,10 @@ class Window(QMainWindow):
|
|||||||
self.playlist_section.tabPlaylist.setTabIcon(
|
self.playlist_section.tabPlaylist.setTabIcon(
|
||||||
idx, QIcon(Config.PLAYLIST_ICON_CURRENT)
|
idx, QIcon(Config.PLAYLIST_ICON_CURRENT)
|
||||||
)
|
)
|
||||||
|
elif self.playlist_section.tabPlaylist.widget(idx).model().sourceModel().is_template:
|
||||||
|
self.playlist_section.tabPlaylist.setTabIcon(
|
||||||
|
idx, QIcon(Config.PLAYLIST_ICON_TEMPLATE)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.playlist_section.tabPlaylist.setTabIcon(idx, QIcon())
|
self.playlist_section.tabPlaylist.setTabIcon(idx, QIcon())
|
||||||
|
|
||||||
|
|||||||
@ -226,9 +226,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if note_background:
|
if note_background:
|
||||||
return QBrush(QColor(note_background))
|
return QBrush(QColor(note_background))
|
||||||
|
|
||||||
if self.is_template:
|
|
||||||
return QBrush(QColor(Config.COLOUR_TEMPLATE_ROW))
|
|
||||||
|
|
||||||
return QBrush()
|
return QBrush()
|
||||||
|
|
||||||
def begin_reset_model(self, playlist_id: int) -> None:
|
def begin_reset_model(self, playlist_id: int) -> None:
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="icons">
|
<qresource prefix="icons">
|
||||||
<file>yellow-circle.png</file>
|
<file>yellow-circle.png</file>
|
||||||
|
<file>redstar.png</file>
|
||||||
<file>green-circle.png</file>
|
<file>green-circle.png</file>
|
||||||
<file>star.png</file>
|
<file>star.png</file>
|
||||||
<file>star_empty.png</file>
|
<file>star_empty.png</file>
|
||||||
|
|||||||
1399
app/ui/icons_rc.py
1399
app/ui/icons_rc.py
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>1249</width>
|
<width>1249</width>
|
||||||
<height>499</height>
|
<height>538</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
|
|||||||
BIN
app/ui/redstar.png
Normal file
BIN
app/ui/redstar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Loading…
Reference in New Issue
Block a user