Compare commits

...

7 Commits

Author SHA1 Message Date
Keith Edmunds
639f006a10 Add favourite to playlists 2025-02-22 20:23:07 +00:00
Keith Edmunds
9e27418f80 Remove queries table definition
It mistakenly was introduced to the wrong branch. It persists on the
query_tabs branch.
2025-02-22 20:13:44 +00:00
Keith Edmunds
c1448dfdd5 WIP: manage templates: template rows have different background 2025-02-22 19:42:48 +00:00
Keith Edmunds
5f396a0993 WIP: template management: new, rename, delete working 2025-02-22 19:16:42 +00:00
Keith Edmunds
e10c2adafe WIP: template management: edit and delete working 2025-02-22 11:34:36 +00:00
Keith Edmunds
b0f6e4e819 Framework for dynamic submenus 2025-02-21 15:18:45 +00:00
Keith Edmunds
afd3be608c Move menu definitions to YAML file 2025-02-21 14:16:34 +00:00
8 changed files with 579 additions and 207 deletions

View File

@ -31,6 +31,7 @@ class Config(object):
COLOUR_NORMAL_TAB = "#000000"
COLOUR_NOTES_PLAYLIST = "#b8daff"
COLOUR_ODD_PLAYLIST = "#f2f2f2"
COLOUR_TEMPLATE_ROW = "#FFAF68"
COLOUR_UNREADABLE = "#dc3545"
COLOUR_WARNING_TIMER = "#ffc107"
DBFS_SILENCE = -50

View File

@ -80,8 +80,8 @@ class PlaylistsTable(Model):
cascade="all, delete-orphan",
order_by="PlaylistRowsTable.row_number",
)
query: Mapped["QueriesTable"] = relationship(
back_populates="playlist", cascade="all, delete-orphan"
favourite: Mapped[bool] = mapped_column(
Boolean, nullable=False, index=False, default=False
)
def __repr__(self) -> str:
@ -121,20 +121,6 @@ class PlaylistRowsTable(Model):
)
class QueriesTable(Model):
__tablename__ = "queries"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
query: Mapped[str] = mapped_column(
String(2048), index=False, default="", nullable=False
)
playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"), index=True)
playlist: Mapped[PlaylistsTable] = relationship(back_populates="query")
def __repr__(self) -> str:
return f"<Queries(id={self.id}, playlist={self.playlist}, query={self.query}>"
class SettingsTable(Model):
"""Manage settings"""

View File

@ -203,12 +203,12 @@ class Playlists(dbtables.PlaylistsTable):
@classmethod
def create_playlist_from_template(
cls, session: Session, template: "Playlists", playlist_name: str
cls, session: Session, template_id: int, playlist_name: str
) -> Optional["Playlists"]:
"""Create a new playlist from template"""
# Sanity check
if not template.id:
if not template_id:
return None
playlist = cls(session, playlist_name)
@ -217,7 +217,7 @@ class Playlists(dbtables.PlaylistsTable):
if not playlist or not playlist.id:
return None
PlaylistRows.copy_playlist(session, template.id, playlist.id)
PlaylistRows.copy_playlist(session, template_id, playlist.id)
return playlist

View File

@ -1,15 +1,18 @@
#!/usr/bin/env python3
# Standard library imports
from __future__ import annotations
from slugify import slugify # type: ignore
from typing import Callable, Optional
import argparse
from dataclasses import dataclass
import datetime as dt
import os
import subprocess
import sys
import urllib.parse
import webbrowser
import yaml
# PyQt imports
from PyQt6.QtCore import (
@ -22,6 +25,7 @@ from PyQt6.QtGui import (
QAction,
QCloseEvent,
QColor,
QFont,
QIcon,
QKeySequence,
QPalette,
@ -29,6 +33,7 @@ from PyQt6.QtGui import (
)
from PyQt6.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QDialog,
QFileDialog,
@ -38,8 +43,11 @@ from PyQt6.QtWidgets import (
QLineEdit,
QListWidgetItem,
QMainWindow,
QMenu,
QMessageBox,
QPushButton,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)
@ -75,18 +83,6 @@ from utilities import check_db, update_bitrates
import helpers
class DownloadCSV(QDialog):
def __init__(self, parent=None):
super().__init__()
self.ui = Ui_DateSelect()
self.ui.setupUi(self)
self.ui.dateTimeEdit.setDate(QDate.currentDate())
self.ui.dateTimeEdit.setTime(QTime(19, 59, 0))
self.ui.buttonBox.accepted.connect(self.accept)
self.ui.buttonBox.rejected.connect(self.reject)
class Current:
base_model: PlaylistModel
proxy_model: PlaylistProxyModel
@ -100,6 +96,18 @@ class Current:
)
class DownloadCSV(QDialog):
def __init__(self, parent=None):
super().__init__()
self.ui = Ui_DateSelect()
self.ui.setupUi(self)
self.ui.dateTimeEdit.setDate(QDate.currentDate())
self.ui.dateTimeEdit.setTime(QTime(19, 59, 0))
self.ui.buttonBox.accepted.connect(self.accept)
self.ui.buttonBox.rejected.connect(self.reject)
class EditDeleteDialog(QDialog):
def __init__(self, templates: list[tuple[str, int]]) -> None:
super().__init__()
@ -158,6 +166,134 @@ class EditDeleteDialog(QDialog):
self.reject()
@dataclass
class ItemlistItem:
id: int
name: str
favourite: bool = False
class ItemlistManager(QDialog):
def __init__(
self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks
) -> None:
super().__init__()
self.setWindowTitle("Item Manager")
self.setMinimumSize(600, 400)
self.items = items
self.callbacks = callbacks
layout = QVBoxLayout(self)
self.table = QTableWidget(len(items), 2, self)
self.table.setHorizontalHeaderLabels(["Item", "Actions"])
hh = self.table.horizontalHeader()
if not hh:
raise ApplicationError("ItemlistManager failed to create horizontalHeader")
hh.setStretchLastSection(True)
self.table.setColumnWidth(0, 200)
self.table.setColumnWidth(1, 300)
self.populate_table()
layout.addWidget(self.table)
button_layout = QHBoxLayout()
self.new_button = QPushButton("New")
self.new_button.clicked.connect(self.new_item)
button_layout.addWidget(self.new_button)
self.close_button = QPushButton("Close")
self.close_button.clicked.connect(self.close)
button_layout.addWidget(self.close_button)
layout.addLayout(button_layout)
def populate_table(self) -> None:
"""Populates the table with items and action buttons."""
self.table.setRowCount(len(self.items))
for row, item in enumerate(self.items):
item_text = QTableWidgetItem(item.name)
if item.favourite:
item_text.setFont(QFont("Arial", weight=QFont.Weight.Bold))
self.table.setItem(row, 0, item_text)
# Action Buttons and Checkbox in a widget
widget = QWidget()
h_layout = QHBoxLayout(widget)
h_layout.setContentsMargins(0, 0, 0, 0)
h_layout.setSpacing(5)
rename_button = QPushButton("Rename")
rename_button.clicked.connect(lambda _, i=item.id: self.rename_item(i))
h_layout.addWidget(rename_button)
edit_button = QPushButton("Edit")
edit_button.clicked.connect(lambda _, i=item.id: self.edit_item(i))
h_layout.addWidget(edit_button)
delete_button = QPushButton("Delete")
delete_button.clicked.connect(lambda _, i=item.id: self.delete_item(i))
h_layout.addWidget(delete_button)
fav_checkbox = QCheckBox()
fav_checkbox.setChecked(item.favourite)
fav_checkbox.stateChanged.connect(
lambda state, cb=fav_checkbox, i=item.id: self.toggle_favourite(
i, cb.isChecked()
)
)
h_layout.addWidget(fav_checkbox)
self.table.setCellWidget(row, 1, widget)
def delete_item(self, item_id: int) -> None:
self.callbacks.delete(item_id)
def edit_item(self, item_id: int) -> None:
self.callbacks.edit(item_id)
def rename_item(self, item_id: int) -> None:
new_name = self.callbacks.rename(item_id)
if not new_name:
return
# Rename item in list
for row in range(self.table.rowCount()):
item = self.table.item(row, 0)
if item and self.items[row].id == item_id:
item.setText(new_name)
self.items[row].name = new_name
break
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)
for row in range(self.table.rowCount()):
item = self.table.item(row, 0)
if item and self.items[row].id == item_id:
font = QFont(
"Arial",
weight=QFont.Weight.Bold if checked else QFont.Weight.Normal,
)
item.setFont(font)
self.items[row].favourite = checked
break
def new_item(self) -> None:
self.callbacks.new_item()
@dataclass
class ItemlistManagerCallbacks:
delete: Callable[[int], None]
edit: Callable[[int], None]
favourite: Callable[[int, bool], None]
new_item: Callable[[], None]
rename: Callable[[int], Optional[str]]
class PreviewManager:
"""
Manage track preview player
@ -320,16 +456,21 @@ class TemplateSelectorDialog(QDialog):
Class to manage user selection of template
"""
def __init__(self, templates: list[tuple[str, int]]) -> None:
def __init__(
self, templates: list[tuple[str, int]], template_prompt: Optional[str]
) -> None:
super().__init__()
self.templates = templates
self.template_prompt = template_prompt
self.selected_id = None
self.init_ui()
def init_ui(self):
# Create label
label = QLabel("Select template:")
if not self.template_prompt:
self.template_prompt = "Select template:"
label = QLabel(self.template_prompt)
# Create combo box
self.combo_box = QComboBox()
@ -475,104 +616,85 @@ class Window(QMainWindow):
return action
def create_menu_bar(self):
"""Dynamically creates the menu bar from a YAML file."""
menu_bar = self.menuBar()
# File Menu
file_menu = menu_bar.addMenu("&File")
file_menu.addAction(
self.create_action("Open Playlist", self.open_playlist, "Ctrl+O")
)
file_menu.addAction(self.create_action("New Playlist", self.new_playlist))
file_menu.addAction(
self.create_action("Close Playlist", self.close_playlist_tab)
)
file_menu.addAction(self.create_action("Rename Playlist", self.rename_playlist))
file_menu.addAction(self.create_action("Delete Playlist", self.delete_playlist))
file_menu.addSeparator()
file_menu.addAction(
self.create_action("Save as Template", self.save_as_template)
)
file_menu.addAction(
self.create_action("Manage Templates", self.manage_templates)
)
file_menu.addSeparator()
file_menu.addAction(
self.create_action(
"Import Files", self.import_files_wrapper, "Ctrl+Shift+I"
)
)
file_menu.addSeparator()
file_menu.addAction(self.create_action("Exit", self.close))
# Load menu structure from YAML file
with open("menu.yaml", "r") as file:
menu_data = yaml.safe_load(file)
# Playlist Menu
playlist_menu = menu_bar.addMenu("&Playlist")
playlist_menu.addSeparator()
playlist_menu.addAction(
self.create_action("Insert Track", self.insert_track, "Ctrl+T")
)
playlist_menu.addAction(
self.create_action("Insert Section Header", self.insert_header, "Ctrl+H")
)
playlist_menu.addSeparator()
playlist_menu.addAction(
self.create_action("Mark for Moving", self.mark_rows_for_moving, "Ctrl+C")
)
playlist_menu.addAction(self.create_action("Paste", self.paste_rows, "Ctrl+V"))
playlist_menu.addSeparator()
playlist_menu.addAction(
self.create_action("Export Playlist", self.export_playlist_tab)
)
playlist_menu.addAction(
self.create_action(
"Download CSV of Played Tracks", self.download_played_tracks
)
)
playlist_menu.addSeparator()
playlist_menu.addAction(
self.create_action(
"Select Duplicate Rows",
lambda: self.active_tab().select_duplicate_rows(),
)
)
playlist_menu.addAction(self.create_action("Move Selected", self.move_selected))
playlist_menu.addAction(self.create_action("Move Unplayed", self.move_unplayed))
self.menu_actions = {} # Store reference for enabling/disabling actions
self.dynamic_submenus = {} # Store submenus for dynamic population
# Clear Selection with Escape key. Save in module so we can
# enable/disable it later
self.action_Clear_selection = self.create_action(
"Clear Selection", self.clear_selection, "Esc"
)
playlist_menu.addAction(self.action_Clear_selection)
for menu_item in menu_data["menus"]:
menu = menu_bar.addMenu(menu_item["title"])
# Music Menu
music_menu = menu_bar.addMenu("&Music")
music_menu.addAction(
self.create_action("Set Next", self.set_selected_track_next, "Ctrl+N")
)
music_menu.addAction(self.create_action("Play Next", self.play_next, "Return"))
music_menu.addAction(self.create_action("Fade", self.fade, "Ctrl+Z"))
music_menu.addAction(self.create_action("Stop", self.stop, "Ctrl+Alt+S"))
music_menu.addAction(self.create_action("Resume", self.resume, "Ctrl+R"))
music_menu.addAction(
self.create_action("Skip to Next", self.play_next, "Ctrl+Alt+Return")
)
music_menu.addSeparator()
music_menu.addAction(self.create_action("Search", self.search_playlist, "/"))
music_menu.addAction(
self.create_action(
"Search Title in Wikipedia", self.lookup_row_in_wikipedia, "Ctrl+W"
)
)
music_menu.addAction(
self.create_action(
"Search Title in Songfacts", self.lookup_row_in_songfacts, "Ctrl+S"
)
)
for action_item in menu_item["actions"]:
if "separator" in action_item and action_item["separator"]:
menu.addSeparator()
continue
# Help Menu
help_menu = menu_bar.addMenu("Help")
help_menu.addAction(self.create_action("About", self.about))
help_menu.addAction(self.create_action("Debug", self.debug))
# Check whether this is a submenu first
if action_item.get("submenu"):
submenu = QMenu(action_item["text"], self)
menu.addMenu(submenu)
# Store submenu reference for dynamic population
self.dynamic_submenus[action_item["handler"]] = submenu
submenu.aboutToShow.connect(self.populate_dynamic_submenu)
continue # Skip the rest of the loop (no handler needed)
# Now check for a normal menu action
handler = getattr(self, action_item["handler"], None)
if handler is None:
print(f"Warning: No handler found for {action_item['text']}")
continue
action = self.create_action(
action_item["text"], handler, action_item.get("shortcut")
)
# Store reference to "Clear Selection" so we can enable/disable it
if action_item.get("store_reference"):
self.menu_actions[action_item["handler"]] = action
menu.addAction(action)
def populate_dynamic_submenu(self):
"""Dynamically populates submenus when they are selected."""
submenu = self.sender() # Get the submenu that triggered the event
# Find which submenu it is
for key, stored_submenu in self.dynamic_submenus.items():
if submenu == stored_submenu:
submenu.clear()
# Dynamically call the correct function
items = getattr(self, f"get_{key}_items")()
for item in items:
action = QAction(item["text"], self)
action.triggered.connect(
lambda _, i=item["handler"]: getattr(self, i)()
)
submenu.addAction(action)
break
def get_new_playlist_dynamic_submenu_items(self):
"""Returns dynamically generated menu items for Submenu 1."""
return [
{"text": "Option A", "handler": "option_a_handler"},
{"text": "Option B", "handler": "option_b_handler"},
]
def get_query_dynamic_submenu_items(self):
"""Returns dynamically generated menu items for Submenu 2."""
return [
{"text": "Action X", "handler": "action_x_handler"},
{"text": "Action Y", "handler": "action_y_handler"},
]
def select_duplicate_rows(self) -> None:
"""Call playlist to select duplicate rows"""
self.active_tab().select_duplicate_rows()
def about(self) -> None:
"""Get git tag and database name"""
@ -749,13 +871,23 @@ class Window(QMainWindow):
self.signals.search_wikipedia_signal.connect(self.open_wikipedia_browser)
def create_playlist(
self, session: Session, playlist_name: str
self, session: Session, template_id: int
) -> Optional[Playlists]:
"""Create new playlist"""
log.debug(f"create_playlist({playlist_name=}")
# Get a name for this new playlist
playlist_name = self.solicit_playlist_name(session)
if not playlist_name:
return None
playlist = Playlists(session, playlist_name)
# 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
@ -764,7 +896,7 @@ class Window(QMainWindow):
return None
def create_playlist_tab(self, playlist: Playlists) -> int:
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.
@ -773,7 +905,7 @@ class Window(QMainWindow):
log.debug(f"create_playlist_tab({playlist=})")
# Create model and proxy model
base_model = PlaylistModel(playlist.id)
base_model = PlaylistModel(playlist.id, is_template)
proxy_model = PlaylistProxyModel()
proxy_model.setSourceModel(base_model)
@ -863,7 +995,8 @@ class Window(QMainWindow):
log.debug(f"enable_escape({enabled=})")
self.action_Clear_selection.setEnabled(enabled)
if "clear_selection" in self.menu_actions:
self.menu_actions["clear_selection"].setEnabled(enabled)
def end_of_track_actions(self) -> None:
"""
@ -951,6 +1084,18 @@ class Window(QMainWindow):
if track_sequence.current:
track_sequence.current.fade()
def get_tab_index_for_playlist(self, playlist_id: int) -> Optional[int]:
"""
Return the tab index for the passed playlist_id if it is displayed,
else return None.
"""
for idx in range(self.playlist_section.tabPlaylist.count()):
if self.playlist_section.tabPlaylist.widget(idx).playlist_id == playlist_id:
return idx
return None
def hide_played(self):
"""Toggle hide played tracks"""
@ -1057,42 +1202,105 @@ class Window(QMainWindow):
Delete / edit templates
"""
# Build a list of (template-name, playlist-id) tuples
template_list: list[tuple[str, int]] = []
# Define callbacks to handle management options
def delete(template_id: int) -> None:
"""delete template"""
template = session.get(Playlists, template_id)
if not template:
raise ApplicationError(
f"manage_templeate.delete({template_id=}) can't load template"
)
if helpers.ask_yes_no(
"Delete template",
f"Delete template '{template.name}': " "Are you sure?",
):
# If template is currently open, re-check
open_idx = self.get_tab_index_for_playlist(template_id)
if open_idx:
if not helpers.ask_yes_no(
"Delete open template",
f"Template '{template.name}' is currently open. Really delete?",
):
return
else:
self.playlist_section.tabPlaylist.removeTab(open_idx)
log.info(f"manage_templates: delete {template=}")
template.delete(session)
session.commit()
def edit(template_id: int) -> None:
"""Edit template"""
template = session.get(Playlists, template_id)
if not template:
raise ApplicationError(
f"manage_templeate.edit({template_id=}) can't load template"
)
# Simply load the template as a playlist. Any changes
# made will persist
idx = self.create_playlist_tab(template, is_template=True)
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
def favourite(template_id: int, favourite: bool) -> None:
"""Mark template as (not) favourite"""
print(f"manage_templates.favourite({template_id=}")
print(f"{session=}")
def new_item() -> None:
"""Create new template"""
# Get base template
template_id = self.solicit_template_to_use(
session, template_prmompt="New template based upon:"
)
if template_id is None:
return
new_template = self.create_playlist(session, template_id)
if new_template:
self.open_playlist(session, new_template, is_template=True)
def rename(template_id: int) -> Optional[str]:
"""rename template"""
template = session.get(Playlists, template_id)
if not template:
raise ApplicationError(
f"manage_templeate.delete({template_id=}) can't load template"
)
new_name = self.solicit_playlist_name(session, template.name)
if new_name:
template.rename(session, new_name)
idx = self.tabBar.currentIndex()
self.tabBar.setTabText(idx, new_name)
session.commit()
return new_name
return None
# Call listitem management dialog to manage templates
callbacks = ItemlistManagerCallbacks(
delete=delete,
edit=edit,
favourite=favourite,
new_item=new_item,
rename=rename,
)
# Build a list of templates
template_list: list[ItemlistItem] = []
with db.Session() as session:
for template in Playlists.get_all_templates(session):
template_list.append((template.name, template.id))
# Get user's selection
dlg = EditDeleteDialog(template_list)
if not dlg.exec():
return # User cancelled
action, template_id = dlg.selection
playlist = session.get(Playlists, template_id)
if not playlist:
log.error(f"Error opening {template_id=}")
if action == "Edit":
# Simply load the template as a playlist. Any changes
# made will persist
idx = self.create_playlist_tab(playlist)
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
elif action == "Delete":
if helpers.ask_yes_no(
"Delete template",
f"Delete template '{playlist.name}': " "Are you sure?",
):
if self.close_playlist_tab():
playlist.delete(session)
session.commit()
else:
raise ApplicationError(
f"Unrecognised action from EditDeleteDialog: {action=}"
)
# TODO: need to add in favourites
template_list.append(ItemlistItem(name=template.name, id=template.id))
# We need to retain a reference to the dialog box to stop it
# going out of scope and being garbage-collected.
self.dlg = ItemlistManager(template_list, callbacks)
self.dlg.show()
def mark_rows_for_moving(self) -> None:
"""
@ -1177,37 +1385,17 @@ class Window(QMainWindow):
self.move_playlist_rows(unplayed_rows)
self.disable_selection_timing = False
def new_playlist(self) -> None:
def new_playlist(self) -> Optional[Playlists]:
"""
Create new playlist, optionally from template
"""
# Build a list of (template-name, playlist-id) tuples starting
# with the "no template" entry
template_list: list[tuple[str, int]] = []
template_list.append((Config.NO_TEMPLATE_NAME, 0))
with db.Session() as session:
for template in Playlists.get_all_templates(session):
template_list.append((template.name, template.id))
template_id = self.solicit_template_to_use(session)
if not template_id:
return None # User cancelled
dlg = TemplateSelectorDialog(template_list)
if not dlg.exec():
return # User cancelled
template_id = dlg.selected_id
# Get a name for this new playlist
playlist_name = self.solicit_playlist_name(session)
if not playlist_name:
return
# If template_id == 0, user doesn't want a template
if template_id == 0:
playlist = self.create_playlist(session, playlist_name)
else:
playlist = Playlists.create_playlist_from_template(
session, template, playlist_name
)
playlist = self.create_playlist(session, template_id)
if playlist:
playlist.mark_open()
@ -1216,10 +1404,13 @@ class Window(QMainWindow):
session.commit()
idx = self.create_playlist_tab(playlist)
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
return playlist
else:
log.error("Playlist failed to create")
ApplicationError("new_playlist: Playlist failed to create")
def open_playlist(self) -> None:
return None
def open_existing_playlist(self) -> None:
"""Open existing playlist"""
with db.Session() as session:
@ -1228,11 +1419,18 @@ class Window(QMainWindow):
dlg.exec()
playlist = dlg.playlist
if playlist:
idx = self.create_playlist_tab(playlist)
playlist.mark_open()
session.commit()
self.open_playlist(session, playlist)
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
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:
"""Search Songfacts for title"""
@ -1699,24 +1897,45 @@ class Window(QMainWindow):
# Switch to correct tab
if playlist_id != self.current.playlist_id:
for idx in range(self.playlist_section.tabPlaylist.count()):
if (
self.playlist_section.tabPlaylist.widget(idx).playlist_id
== playlist_id
):
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
break
open_idx = self.get_tab_index_for_playlist(playlist_id)
if open_idx:
self.playlist_section.tabPlaylist.setCurrentIndex(open_idx)
else:
raise ApplicationError(
f"show_track() can't find current playlist tab {playlist_id=}"
)
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 = ""
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("Playlist name:")
dlg.setLabelText(prompt)
while True:
if default:
dlg.setTextValue(default)

View File

@ -26,6 +26,7 @@ from PyQt6.QtGui import (
)
# Third party imports
from sqlalchemy.orm.session import Session
import obswebsocket # type: ignore
# import snoop # type: ignore
@ -74,12 +75,14 @@ class PlaylistModel(QAbstractTableModel):
def __init__(
self,
playlist_id: int,
is_template: bool,
*args: Optional[QObject],
**kwargs: Optional[QObject],
) -> None:
log.debug("PlaylistModel.__init__()")
self.playlist_id = playlist_id
self.is_template = is_template
super().__init__(*args, **kwargs)
self.playlist_rows: dict[int, RowAndTrack] = {}
@ -223,6 +226,9 @@ class PlaylistModel(QAbstractTableModel):
if note_background:
return QBrush(QColor(note_background))
if self.is_template:
return QBrush(QColor(Config.COLOUR_TEMPLATE_ROW))
return QBrush()
def begin_reset_model(self, playlist_id: int) -> None:
@ -772,7 +778,7 @@ class PlaylistModel(QAbstractTableModel):
return None
def load_data(self, session: db.session) -> None:
def load_data(self, session: Session) -> None:
"""
Same as refresh data, but only used when creating playslit.
Distinguishes profile time between initial load and other
@ -1061,7 +1067,7 @@ class PlaylistModel(QAbstractTableModel):
# Update display
self.invalidate_row(track_sequence.previous.row_number)
def refresh_data(self, session: db.session) -> None:
def refresh_data(self, session: Session) -> None:
"""
Populate self.playlist_rows with playlist data

View File

@ -364,7 +364,7 @@ class PlaylistTab(QTableView):
Override closeEditor to enable play controls and update display.
"""
self.musicmuster.action_Clear_selection.setEnabled(True)
self.musicmuster.enable_escape(True)
super(PlaylistTab, self).closeEditor(editor, hint)

102
menu.yaml Normal file
View File

@ -0,0 +1,102 @@
menus:
- title: "&File"
actions:
- text: "Save as Template"
handler: "save_as_template"
- text: "Manage Templates"
handler: "manage_templates"
- separator: true
- separator: true
- text: "Exit"
handler: "close"
- title: "&Playlist"
actions:
- text: "Open Playlist"
handler: "open_existing_playlist"
shortcut: "Ctrl+O"
- text: "New Playlist"
handler: "new_playlist_dynamic_submenu"
submenu: true
- text: "Close Playlist"
handler: "close_playlist_tab"
- text: "Rename Playlist"
handler: "rename_playlist"
- text: "Delete Playlist"
handler: "delete_playlist"
- separator: true
- text: "Insert Track"
handler: "insert_track"
shortcut: "Ctrl+T"
- text: "Select Track from Query"
handler: "query_dynamic_submenu"
submenu: true
- text: "Insert Section Header"
handler: "insert_header"
shortcut: "Ctrl+H"
- text: "Import Files"
handler: "import_files_wrapper"
shortcut: "Ctrl+Shift+I"
- separator: true
- text: "Mark for Moving"
handler: "mark_rows_for_moving"
shortcut: "Ctrl+C"
- text: "Paste"
handler: "paste_rows"
shortcut: "Ctrl+V"
- separator: true
- text: "Export Playlist"
handler: "export_playlist_tab"
- text: "Download CSV of Played Tracks"
handler: "download_played_tracks"
- separator: true
- text: "Select Duplicate Rows"
handler: "select_duplicate_rows"
- text: "Move Selected"
handler: "move_selected"
- text: "Move Unplayed"
handler: "move_unplayed"
- separator: true
- text: "Clear Selection"
handler: "clear_selection"
shortcut: "Esc"
store_reference: true # So we can enable/disable later
- title: "&Music"
actions:
- text: "Set Next"
handler: "set_selected_track_next"
shortcut: "Ctrl+N"
- text: "Play Next"
handler: "play_next"
shortcut: "Return"
- text: "Fade"
handler: "fade"
shortcut: "Ctrl+Z"
- text: "Stop"
handler: "stop"
shortcut: "Ctrl+Alt+S"
- text: "Resume"
handler: "resume"
shortcut: "Ctrl+R"
- text: "Skip to Next"
handler: "play_next"
shortcut: "Ctrl+Alt+Return"
- separator: true
- text: "Search"
handler: "search_playlist"
shortcut: "/"
- text: "Search Title in Wikipedia"
handler: "lookup_row_in_wikipedia"
shortcut: "Ctrl+W"
- text: "Search Title in Songfacts"
handler: "lookup_row_in_songfacts"
shortcut: "Ctrl+S"
- title: "Help"
actions:
- text: "About"
handler: "about"
- text: "Debug"
handler: "debug"

View File

@ -0,0 +1,58 @@
"""add favouirit to playlists
Revision ID: 04df697e40cd
Revises: 33c04e3c12c8
Create Date: 2025-02-22 20:20:45.030024
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '04df697e40cd'
down_revision = '33c04e3c12c8'
branch_labels = None
depends_on = None
def upgrade(engine_name: str) -> None:
globals()["upgrade_%s" % engine_name]()
def downgrade(engine_name: str) -> None:
globals()["downgrade_%s" % engine_name]()
def upgrade_() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('notecolours', schema=None) as batch_op:
batch_op.add_column(sa.Column('strip_substring', sa.Boolean(), nullable=False))
batch_op.create_index(batch_op.f('ix_notecolours_substring'), ['substring'], unique=False)
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
batch_op.drop_constraint('playlist_rows_ibfk_1', type_='foreignkey')
with op.batch_alter_table('playlists', schema=None) as batch_op:
batch_op.add_column(sa.Column('favourite', sa.Boolean(), nullable=False))
# ### end Alembic commands ###
def downgrade_() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('playlists', schema=None) as batch_op:
batch_op.drop_column('favourite')
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
batch_op.create_foreign_key('playlist_rows_ibfk_1', 'tracks', ['track_id'], ['id'])
with op.batch_alter_table('notecolours', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_notecolours_substring'))
batch_op.drop_column('strip_substring')
# ### end Alembic commands ###