diff --git a/frontend.py b/frontend.py index 201a97d..8b8cb8c 100644 --- a/frontend.py +++ b/frontend.py @@ -12,7 +12,7 @@ from PyQt5.QtWidgets import ( QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView ) from PyQt5.QtCore import Qt, QUrl, QEvent -from PyQt5.QtGui import QColor, QIcon +from PyQt5.QtGui import QColor, QIcon, QKeySequence from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from backend import AnimeBackend @@ -77,6 +77,32 @@ class AnimeDialog(QDialog): 'url': self.url_edit.text() } +class ShortcutsDialog(QDialog): + def __init__(self, parent): + super().__init__(parent) + self.setWindowTitle("Keyboard Shortcuts") + layout = QVBoxLayout(self) + shortcuts_text = QTextEdit() + shortcuts_text.setReadOnly(True) + shortcuts_text.setPlainText(""" +Global Shortcuts: +- A: Add an anime +- Page Down: Next tab +- Page Up: Previous tab +- Q: Quit the program +- R: Pick random anime + +Table-specific Shortcuts (apply to selected entry): +- Delete: Delete an anime +- E: Edit +- W: Set to watching +- C: Set to completed +""") + layout.addWidget(shortcuts_text) + buttons = QDialogButtonBox(QDialogButtonBox.Ok) + buttons.accepted.connect(self.accept) + layout.addWidget(buttons) + class HoverLabel(QLabel): def __init__(self, main_window, name, url=None): super().__init__() @@ -101,6 +127,33 @@ class HoverLabel(QLabel): self.fetched = True super().enterEvent(event) +class CustomTableWidget(QTableWidget): + def __init__(self, parent, is_pre): + super().__init__() + self.parent_window = parent + self.is_pre = is_pre + + def keyPressEvent(self, event): + key = event.key() + if event.modifiers() == Qt.NoModifier: + selected_rows = self.selectionModel().selectedRows() + if selected_rows: + row = selected_rows[0].row() + anime_id = int(self.item(row, 0).text()) + if key == Qt.Key_Delete: + self.parent_window.delete_anime(anime_id) + return + elif key == Qt.Key_E: + self.parent_window.edit_anime(anime_id) + return + elif key == Qt.Key_W: + self.parent_window.change_status(anime_id, 'watching') + return + elif key == Qt.Key_C: + self.parent_window.change_status(anime_id, 'completed') + return + super().keyPressEvent(event) + class AnimeTracker(QMainWindow): def __init__(self): super().__init__() @@ -138,6 +191,14 @@ class AnimeTracker(QMainWindow): random_act = QAction('Random Pick', self) random_act.triggered.connect(self.random_pick) tools_menu.addAction(random_act) + help_menu = menubar.addMenu('Help') + shortcuts_act = QAction('Shortcuts', self) + shortcuts_act.triggered.connect(self.show_shortcuts) + help_menu.addAction(shortcuts_act) + + def show_shortcuts(self): + dialog = ShortcutsDialog(self) + dialog.exec_() def get_current_tab_identifier(self): index = self.tab_widget.currentIndex() @@ -173,7 +234,85 @@ class AnimeTracker(QMainWindow): pre_content = QWidget() pre_layout = QVBoxLayout(pre_content) if pre_entries: - table = self.create_table_widget(pre_entries, is_pre=True) + table = CustomTableWidget(self, is_pre=True) + table.setRowCount(len(pre_entries)) + table.setColumnCount(7) + headers = ['ID', 'Year', 'Name', 'Type', 'Status', 'Comment', 'Actions'] + table.setHorizontalHeaderLabels(headers) + table.setColumnHidden(0, True) + table.setAlternatingRowColors(True) + table.setShowGrid(True) + header = table.horizontalHeader() + header.setStretchLastSection(False) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID hidden + header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Year + header.setSectionResizeMode(2, QHeaderView.Stretch) # Name + header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Type + header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Status + header.setSectionResizeMode(5, QHeaderView.Stretch) # Comment + header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # Actions + for row, entry in enumerate(pre_entries): + col = 0 + # ID + id_item = QTableWidgetItem(str(entry[0])) + table.setItem(row, col, id_item) + col += 1 + # Year + year_item = QTableWidgetItem(str(entry[2])) + table.setItem(row, col, year_item) + col += 1 + # Name + name = entry[1] + url = entry[7] + name_label = HoverLabel(self, name, url) + table.setCellWidget(row, col, name_label) + col += 1 + # Type + type_item = QTableWidgetItem(entry[5] or '') + table.setItem(row, col, type_item) + col += 1 + # Status + status_item = QTableWidgetItem(entry[4]) + table.setItem(row, col, status_item) + col += 1 + # Comment + comment_item = QTableWidgetItem(entry[6] or '') + table.setItem(row, col, comment_item) + col += 1 + # Actions + actions_widget = self.create_actions_widget(entry[0], entry[4]) + table.setCellWidget(row, col, actions_widget) + table.resizeColumnsToContents() + table.resizeRowsToContents() + # Calculate fixed height + height = table.horizontalHeader().height() + 2 # small margin + for i in range(table.rowCount()): + height += table.rowHeight(i) + table.setFixedHeight(height) + # Apply status colors + status_col = 4 + for r in range(table.rowCount()): + status = table.item(r, status_col).text() + if status == 'unwatched': + color = QColor(255, 255, 255) + elif status == 'watching': + color = QColor(255, 255, 0) + elif status == 'completed': + color = QColor(0, 255, 0) + else: + color = QColor(255, 255, 255) + for c in range(table.columnCount()): + if c == 0: + continue # skip hidden id + item = table.item(r, c) + if item: + item.setBackground(color) + widget = table.cellWidget(r, c) + if widget: + widget.setStyleSheet(f"background-color: {color.name()};") pre_layout.addWidget(table) pre_tab.setWidget(pre_content) tab_text = "Pre-2010" @@ -203,7 +342,80 @@ class AnimeTracker(QMainWindow): s_name = season.capitalize() if season else 'Other' label = QLabel(s_name) layout.addWidget(label) - table = self.create_table_widget(entries, is_pre=False) + table = CustomTableWidget(self, is_pre=False) + table.setRowCount(len(entries)) + table.setColumnCount(6) + headers = ['ID', 'Name', 'Type', 'Status', 'Comment', 'Actions'] + table.setHorizontalHeaderLabels(headers) + table.setColumnHidden(0, True) + table.setAlternatingRowColors(True) + table.setShowGrid(True) + header = table.horizontalHeader() + header.setStretchLastSection(False) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID hidden + header.setSectionResizeMode(1, QHeaderView.Stretch) # Name + header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Type + header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Status + header.setSectionResizeMode(4, QHeaderView.Stretch) # Comment + header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Actions + for row, entry in enumerate(entries): + col = 0 + # ID + id_item = QTableWidgetItem(str(entry[0])) + table.setItem(row, col, id_item) + col += 1 + # Name + name = entry[1] + url = entry[7] + name_label = HoverLabel(self, name, url) + table.setCellWidget(row, col, name_label) + col += 1 + # Type + type_item = QTableWidgetItem(entry[5] or '') + table.setItem(row, col, type_item) + col += 1 + # Status + status_item = QTableWidgetItem(entry[4]) + table.setItem(row, col, status_item) + col += 1 + # Comment + comment_item = QTableWidgetItem(entry[6] or '') + table.setItem(row, col, comment_item) + col += 1 + # Actions + actions_widget = self.create_actions_widget(entry[0], entry[4]) + table.setCellWidget(row, col, actions_widget) + table.resizeColumnsToContents() + table.resizeRowsToContents() + # Calculate fixed height + height = table.horizontalHeader().height() + 2 # small margin + for i in range(table.rowCount()): + height += table.rowHeight(i) + table.setFixedHeight(height) + # Apply status colors + status_col = 3 + for r in range(table.rowCount()): + status = table.item(r, status_col).text() + if status == 'unwatched': + color = QColor(255, 255, 255) + elif status == 'watching': + color = QColor(255, 255, 0) + elif status == 'completed': + color = QColor(0, 255, 0) + else: + color = QColor(255, 255, 255) + for c in range(table.columnCount()): + if c == 0: + continue # skip hidden id + item = table.item(r, c) + if item: + item.setBackground(color) + widget = table.cellWidget(r, c) + if widget: + widget.setStyleSheet(f"background-color: {color.name()};") layout.addWidget(table) total_entries += len(entries) comp_entries += sum(1 for e in entries if e[4] == 'completed') @@ -216,86 +428,6 @@ class AnimeTracker(QMainWindow): if completed: self.tab_widget.tabBar().setTabTextColor(index, QColor('gray')) - def create_table_widget(self, entries, is_pre): - col_count = 6 if is_pre else 5 - table = QTableWidget(len(entries), col_count) - headers = ['Year', 'Name', 'Type', 'Status', 'Comment', 'Actions'] if is_pre else ['Name', 'Type', 'Status', 'Comment', 'Actions'] - table.setHorizontalHeaderLabels(headers) - table.setAlternatingRowColors(True) - table.setShowGrid(True) - header = table.horizontalHeader() - header.setStretchLastSection(False) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - if is_pre: - header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # Year - header.setSectionResizeMode(1, QHeaderView.Stretch) # Name - header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Type - header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Status - header.setSectionResizeMode(4, QHeaderView.Stretch) # Comment - header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Actions - else: - header.setSectionResizeMode(0, QHeaderView.Stretch) # Name - header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Type - header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Status - header.setSectionResizeMode(3, QHeaderView.Stretch) # Comment - header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Actions - for row, entry in enumerate(entries): - col = 0 - if is_pre: - year_item = QTableWidgetItem(str(entry[2])) - table.setItem(row, col, year_item) - col += 1 - # Name - name = entry[1] - url = entry[7] - name_label = HoverLabel(self, name, url) - table.setCellWidget(row, col, name_label) - col += 1 - # Type - type_item = QTableWidgetItem(entry[5] or '') - table.setItem(row, col, type_item) - col += 1 - # Status - status_item = QTableWidgetItem(entry[4]) - table.setItem(row, col, status_item) - col += 1 - # Comment - comment_item = QTableWidgetItem(entry[6] or '') - table.setItem(row, col, comment_item) - col += 1 - # Actions - actions_widget = self.create_actions_widget(entry[0], entry[4]) - table.setCellWidget(row, col, actions_widget) - table.resizeColumnsToContents() - table.resizeRowsToContents() - # Calculate fixed height - height = table.horizontalHeader().height() + 2 # small margin - for i in range(table.rowCount()): - height += table.rowHeight(i) - table.setFixedHeight(height) - # Apply status colors - status_col = 3 if is_pre else 2 - for r in range(table.rowCount()): - status = table.item(r, status_col).text() - if status == 'unwatched': - color = QColor(255, 255, 255) - elif status == 'watching': - color = QColor(255, 255, 0) - elif status == 'completed': - color = QColor(0, 255, 0) - else: - color = QColor(255, 255, 255) - for c in range(table.columnCount()): - item = table.item(r, c) - if item: - item.setBackground(color) - widget = table.cellWidget(r, c) - if widget: - widget.setStyleSheet(f"background-color: {color.name()};") - return table - def create_actions_widget(self, anime_id, status): widget = QWidget() layout = QHBoxLayout(widget) @@ -428,9 +560,9 @@ class AnimeTracker(QMainWindow): tab_text = self.tab_widget.tabText(self.tab_widget.currentIndex()) is_pre = 'Pre-2010' in tab_text current_widget = self.tab_widget.currentWidget().widget() if isinstance(self.tab_widget.currentWidget(), QScrollArea) else self.tab_widget.currentWidget() - tables = current_widget.findChildren(QTableWidget) - name_col = 1 if is_pre else 0 - status_col = 3 if is_pre else 2 + tables = current_widget.findChildren(CustomTableWidget) + name_col = 2 if is_pre else 1 + status_col = 4 if is_pre else 3 unwatched = [] for table in tables: for row in range(table.rowCount()): @@ -458,6 +590,26 @@ class AnimeTracker(QMainWindow): if file_name: self.backend.export_to_csv(file_name) + def keyPressEvent(self, event): + key = event.key() + modifiers = event.modifiers() + if modifiers == Qt.NoModifier: + if key == Qt.Key_A: + self.add_anime() + elif key == Qt.Key_PageDown: + current = self.tab_widget.currentIndex() + if current < self.tab_widget.count() - 1: + self.tab_widget.setCurrentIndex(current + 1) + elif key == Qt.Key_PageUp: + current = self.tab_widget.currentIndex() + if current > 0: + self.tab_widget.setCurrentIndex(current - 1) + elif key == Qt.Key_Q: + self.close() + elif key == Qt.Key_R: + self.random_pick() + super().keyPressEvent(event) + if __name__ == '__main__': app = QApplication(sys.argv) window = AnimeTracker()