Compare commits
7 Commits
aef8cb5cb5
...
639f006a10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
639f006a10 | ||
|
|
9e27418f80 | ||
|
|
c1448dfdd5 | ||
|
|
5f396a0993 | ||
|
|
e10c2adafe | ||
|
|
b0f6e4e819 | ||
|
|
afd3be608c |
@ -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
|
||||
|
||||
@ -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"""
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
102
menu.yaml
Normal 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"
|
||||
|
||||
@ -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 ###
|
||||
|
||||
Loading…
Reference in New Issue
Block a user