diff --git a/app/musicmuster.py b/app/musicmuster.py index a502b79..e7ffd83 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -1,9 +1,11 @@ #!/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 @@ -23,6 +25,7 @@ from PyQt6.QtGui import ( QAction, QCloseEvent, QColor, + QFont, QIcon, QKeySequence, QPalette, @@ -30,6 +33,7 @@ from PyQt6.QtGui import ( ) from PyQt6.QtWidgets import ( QApplication, + QCheckBox, QComboBox, QDialog, QFileDialog, @@ -42,6 +46,8 @@ from PyQt6.QtWidgets import ( QMenu, QMessageBox, QPushButton, + QTableWidget, + QTableWidgetItem, QVBoxLayout, QWidget, ) @@ -102,6 +108,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__() @@ -160,6 +178,125 @@ 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: + print(f"Rename item {item_id}") + + 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], None] + + class PreviewManager: """ Manage track preview player @@ -1041,42 +1178,81 @@ class Window(QMainWindow): Delete / edit templates """ - # Build a list of (template-name, playlist-id) tuples - template_list: list[tuple[str, int]] = [] + 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 + for idx in range(self.playlist_section.tabPlaylist.count()): + if ( + self.playlist_section.tabPlaylist.widget(idx).playlist_id + == template_id + ): + 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(idx) + break + + 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) + self.playlist_section.tabPlaylist.setCurrentIndex(idx) + + def favourite(template_id: int, favourite: bool) -> None: + """favourite template""" + + print(f"manage_templates.favourite({template_id=}") + print(f"{session=}") + + def new_item() -> None: + """new item""" + + print("manage_templates.new()") + print(f"{session=}") + + def rename(template_id: int) -> None: + """rename template""" + + print(f"manage_templates.rename({template_id=}") + print(f"{session=}") + + 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)) + x = ItemlistManager(template_list, callbacks) + x.show() def mark_rows_for_moving(self) -> None: """