import sys import os import random import re import html import logging from datetime import datetime from PyQt5.QtWidgets import ( QMainWindow, QTabWidget, QScrollArea, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QLabel, QToolButton, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox, QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView ) from PyQt5.QtCore import Qt, QSettings from PyQt5.QtGui import QColor, QIcon, QFont from backend import AnimeBackend # Set up logging logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s') class AnimeDialog(QDialog): def __init__(self, parent, entry=None, default_year=None, default_season=None): super().__init__(parent) self.setWindowTitle("Add Anime" if entry is None else "Edit Anime") layout = QFormLayout(self) self.name_edit = QLineEdit() self.name_edit.setMaxLength(255) # Prevent overly long inputs layout.addRow("Name", self.name_edit) self.year_spin = QSpinBox() self.year_spin.setRange(1960, 2073) layout.addRow("Year", self.year_spin) self.season_combo = QComboBox() self.season_combo.addItems(['Winter', 'Spring', 'Summer', 'Fall', 'Other']) # Map display names to database values self.season_map = { 'Winter': 'winter', 'Spring': 'spring', 'Summer': 'summer', 'Fall': 'fall', 'Other': '' } # Reverse map for setting current selection self.reverse_season_map = {v: k for k, v in self.season_map.items()} layout.addRow("Season", self.season_combo) self.type_combo = QComboBox() self.type_combo.addItems(['TV', 'Movie', 'OVA', 'Special', 'Short TV', 'Other']) layout.addRow("Type", self.type_combo) self.status_combo = QComboBox() self.status_combo.addItems(['unwatched', 'watching', 'completed']) layout.addRow("Status", self.status_combo) self.comment_edit = QTextEdit() self.comment_edit.setAcceptRichText(False) # Prevent HTML injection layout.addRow("Comment", self.comment_edit) self.url_edit = QLineEdit() self.url_edit.setMaxLength(2048) # Reasonable limit for URLs layout.addRow("MAL URL", self.url_edit) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addRow(buttons) if entry: # Unescape for display in input fields self.name_edit.setText(html.unescape(entry[1])) self.year_spin.setValue(entry[2]) self.season_combo.setCurrentText((entry[3] or "Other").capitalize()) self.status_combo.setCurrentText(entry[4]) self.type_combo.setCurrentText(entry[5] or '') self.comment_edit.setPlainText(html.unescape(entry[6] or '')) self.url_edit.setText(entry[7] or '') else: if default_year is not None: self.year_spin.setValue(default_year) if default_season is not None: # Map database season value to display name for default display_season = self.reverse_season_map.get(default_season, 'Other') self.year_spin.valueChanged.connect(self.update_season) self.update_season(self.year_spin.value()) def update_season(self, year): if year < 2010: self.season_combo.setEnabled(False) self.season_combo.setCurrentText('Other') else: self.season_combo.setEnabled(True) def get_data(self): # Convert display season name back to database value display_season = self.season_combo.currentText().strip() db_season = self.season_map.get(display_season, '') # Sanitize inputs by escaping special characters return { 'name': html.escape(self.name_edit.text().strip()), 'year': self.year_spin.value(), 'season': html.escape(db_season), # Use mapped database value 'status': html.escape(self.status_combo.currentText().strip()), 'type': html.escape(self.type_combo.currentText().strip()), 'comment': html.escape(self.comment_edit.toPlainText().strip()), 'url': html.escape(self.url_edit.text().strip()) } 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 - Ctrl + Page Down: Next tab - Ctrl + Page Up: Previous tab - Ctrl + Q: Quit the program - R: Pick random anime - Ctrl + + : Increase table scale - Ctrl + - : Decrease table scale Table-specific Shortcuts (apply to selected entry): - Delete: Delete an anime - E: Edit - W: Set to watching / Set to unwatched (toggle if watching) - C: Set to completed / Set to unwatched (toggle if completed) """) layout.addWidget(shortcuts_text) buttons = QDialogButtonBox(QDialogButtonBox.Ok) buttons.accepted.connect(self.accept) layout.addWidget(buttons) 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: status_col = 4 if self.is_pre else 3 status = self.item(row, status_col).text() new_status = 'watching' if status != 'watching' else 'unwatched' self.parent_window.change_status(anime_id, new_status) return elif key == Qt.Key_C: status_col = 4 if self.is_pre else 3 status = self.item(row, status_col).text() new_status = 'completed' if status != 'completed' else 'unwatched' self.parent_window.change_status(anime_id, new_status) return super().keyPressEvent(event) class AnimeTracker(QMainWindow): def __init__(self): super().__init__() self.settings = QSettings("xAI", "AnimeBacklogTracker") self.last_used_season = self.settings.value("lastUsedSeason", "winter") self.setWindowTitle("Anime Backlog Tracker") # Add application icon icon_path = os.path.join(os.path.dirname(__file__), './icons/anime-app-icon.png') if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) self.resize(800, 600) self.backend = AnimeBackend() self.tab_widget = QTabWidget() self.central_widget = QWidget() central_layout = QVBoxLayout(self.central_widget) self.filter_bar = QWidget() filter_layout = QHBoxLayout(self.filter_bar) label_search = QLabel("Search:") self.search_edit = QLineEdit() self.search_edit.setPlaceholderText("Search name") self.search_edit.textChanged.connect(self.filter_tables) filter_layout.addWidget(label_search) filter_layout.addWidget(self.search_edit) central_layout.addWidget(self.filter_bar) central_layout.addWidget(self.tab_widget) self.setCentralWidget(self.central_widget) self.search_text = '' self.table_scale = self.settings.value("tableScale", 1.0, type=float) self.tables = [] self.create_menu() self.load_tabs() self.restoreGeometry(self.settings.value("geometry", self.saveGeometry())) last_tab = self.settings.value("lastTab", None) if last_tab is not None: self.set_current_tab_by_identifier(last_tab) self.tab_widget.setFocus() def get_current_scroll_pos(self): widget = self.tab_widget.currentWidget() if widget: return widget.verticalScrollBar().value() return 0 def set_current_scroll_pos(self, pos): widget = self.tab_widget.currentWidget() if widget: widget.verticalScrollBar().setValue(pos) def filter_tables(self, text): self.search_text = text.strip().lower() for table in self.tables: for row in range(table.rowCount()): name_col = 2 if table.is_pre else 1 name_text = table.cellWidget(row, name_col).text() clean_name = re.sub(r'<[^>]+>', '', name_text).lower() table.setRowHidden(row, self.search_text not in clean_name if self.search_text else False) def closeEvent(self, event): self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("lastTab", self.get_current_tab_identifier()) self.settings.setValue("tableScale", self.table_scale) super().closeEvent(event) def create_menu(self): menubar = self.menuBar() file_menu = menubar.addMenu('File') import_act = QAction('Import CSV', self) import_act.triggered.connect(self.import_csv) file_menu.addAction(import_act) export_act = QAction('Export CSV', self) export_act.triggered.connect(self.export_csv) file_menu.addAction(export_act) edit_menu = menubar.addMenu('Edit') add_act = QAction('Add Anime', self) add_act.triggered.connect(self.add_anime) edit_menu.addAction(add_act) add_year_act = QAction('Add New Year', self) add_year_act.triggered.connect(self.add_new_year) edit_menu.addAction(add_year_act) del_year_act = QAction('Delete Year', self) del_year_act.triggered.connect(self.delete_year) edit_menu.addAction(del_year_act) tools_menu = menubar.addMenu('Tools') random_act = QAction('Random Pick', self) random_act.triggered.connect(self.random_pick) tools_menu.addAction(random_act) statistics_act = QAction('Statistics', self) statistics_act.triggered.connect(self.show_statistics) tools_menu.addAction(statistics_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() if index == -1: return None tab_text = self.tab_widget.tabText(index) if "Pre-2010" in tab_text: return "pre" else: return int(tab_text.split(" ")[0]) def set_current_tab_by_identifier(self, identifier): if identifier is None: return if identifier == "pre": for i in range(self.tab_widget.count()): if "Pre-2010" in self.tab_widget.tabText(i): self.tab_widget.setCurrentIndex(i) return else: for i in range(self.tab_widget.count()): t = self.tab_widget.tabText(i) if t.startswith(str(identifier) + " ("): self.tab_widget.setCurrentIndex(i) return # If the year no longer exists, default to first tab if self.tab_widget.count() > 0: self.tab_widget.setCurrentIndex(0) def load_tabs(self): self.tab_widget.clear() self.tables = [] # Pre-2010 tab pre_entries = self.backend.get_pre_2010_entries() pre_tab = QScrollArea() pre_tab.setWidgetResizable(True) pre_content = QWidget() pre_layout = QVBoxLayout(pre_content) if pre_entries: table = CustomTableWidget(self, is_pre=True) table.is_pre = True self.tables.append(table) table.setRowCount(len(pre_entries)) table.setColumnCount(7) headers = ['ID', 'Year', 'Name', 'Type', 'Status', 'Comment', 'Actions'] table.setHorizontalHeaderLabels(headers) table.setColumnHidden(4, True) # Hide Status column 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.Fixed) # 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 table.setColumnWidth(0, 0) # Set ID column width to 0 to ensure it's hidden font = QFont() font.setPointSize(int(10 * self.table_scale)) header_font = QFont() header_font.setPointSize(int(10 * self.table_scale)) header_font.setBold(True) table.setFont(font) table.horizontalHeader().setFont(header_font) table.verticalHeader().setFont(font) # Add vertical header styling for index numbers vheader = table.verticalHeader() vheader.setDefaultAlignment(Qt.AlignCenter) vheader.setStyleSheet(""" QHeaderView::section { padding-left: 5px; padding-right: 5px; } """) 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])) year_item.setFont(font) table.setItem(row, col, year_item) col += 1 # Name name = entry[1] url = entry[7] name_label = QLabel() name_font = QFont(font) if entry[4] == 'watching': name_font.setItalic(True) elif entry[4] == 'completed': name_font.setStrikeOut(True) name_label.setFont(name_font) if url: name_escaped = html.escape(name) name_label.setText(f' {name_escaped}') name_label.setOpenExternalLinks(True) else: name_label.setText(html.escape(name)) table.setCellWidget(row, col, name_label) col += 1 # Type type_item = QTableWidgetItem(entry[5] or '') type_item.setFont(font) table.setItem(row, col, type_item) col += 1 # Status status_item = QTableWidgetItem(entry[4]) status_item.setFont(font) table.setItem(row, col, status_item) col += 1 # Comment comment_item = QTableWidgetItem(entry[6] or '') comment_item.setFont(font) table.setItem(row, col, comment_item) col += 1 # Actions actions_widget = self.create_actions_widget(entry[0], entry[4]) for child in actions_widget.findChildren(QToolButton): child.setFont(font) 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"padding-left: 2px;background-color: {color.name()};") pre_layout.addWidget(table) pre_tab.setWidget(pre_content) tab_text = "Pre-2010" completed = False total = len(pre_entries) if total > 0: comp = sum(1 for e in pre_entries if e[4] == 'completed') perc = (comp / total * 100) tab_text += f" ({perc:.0f}%)" if comp == total: completed = True index = self.tab_widget.addTab(pre_tab, tab_text) if completed: self.tab_widget.tabBar().setTabTextColor(index, QColor('gray')) # Years >= 2010 years = self.backend.get_years() for year in years: year_tab = QScrollArea() year_tab.setWidgetResizable(True) content = QWidget() layout = QVBoxLayout(content) total_entries = 0 comp_entries = 0 for season in ['winter', 'spring', 'summer', 'fall', '']: entries = self.backend.get_entries_for_season(year, season) # Database schema order: # 0: id # 1: name # 2: year # 3: season # 4: status # 5: type # 6: comment # 7: url if entries: # Season title s_name = season.capitalize() if season else 'Other' label = QLabel() season_font = QFont() season_font.setPointSize(int(12 * self.table_scale)) season_font.setBold(True) label.setFont(season_font) if season: # Only create links for actual seasons, not 'Other' mal_url = f"https://myanimelist.net/anime/season/{year}/{season}" label.setText(f'{s_name}') label.setTextFormat(Qt.RichText) label.setOpenExternalLinks(True) # This makes the link clickable and open in browser else: label.setText(s_name) # No link for 'Other' section layout.addWidget(label) # Season's stats total_anime = len(entries) completed_anime = sum(1 for entry in entries if entry[4] == 'completed') completion_percentage = (completed_anime / total_anime * 100) if total_anime > 0 else 0 s_stat = QLabel(f"Completed: {completed_anime}/{total_anime} ({completion_percentage:.0f}%)") s_stat_font = QFont() s_stat_font.setPointSize(int(10 * self.table_scale)) # Slightly smaller than season label s_stat.setFont(s_stat_font) layout.addWidget(s_stat) table = CustomTableWidget(self, is_pre=False) table.is_pre = False self.tables.append(table) table.setRowCount(len(entries)) table.setColumnCount(6) headers = ['ID', 'Name', 'Type', 'Status', 'Comment', 'Actions'] table.setHorizontalHeaderLabels(headers) table.setColumnHidden(3, True) # Hide Status column 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.Fixed) # 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 table.setColumnWidth(0, 0) # Set ID column width to 0 to ensure it's hidden font = QFont() font.setPointSize(int(10 * self.table_scale)) header_font = QFont() header_font.setPointSize(int(10 * self.table_scale)) header_font.setBold(True) table.setFont(font) table.horizontalHeader().setFont(header_font) table.verticalHeader().setFont(font) # Add vertical header styling for index numbers vheader = table.verticalHeader() vheader.setDefaultAlignment(Qt.AlignCenter) vheader.setStyleSheet(""" QHeaderView::section { padding-left: 5px; padding-right: 5px; } """) 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 = QLabel() name_font = QFont(font) if entry[4] == 'watching': name_font.setItalic(True) elif entry[4] == 'completed': name_font.setStrikeOut(True) name_label.setFont(name_font) if url: name_escaped = html.escape(name) name_label.setText(f' {name_escaped}') name_label.setOpenExternalLinks(True) else: name_label.setText(html.escape(name)) table.setCellWidget(row, col, name_label) col += 1 # Type type_item = QTableWidgetItem(entry[5] or '') type_item.setFont(font) table.setItem(row, col, type_item) col += 1 # Status status_item = QTableWidgetItem(entry[4]) status_item.setFont(font) table.setItem(row, col, status_item) col += 1 # Comment comment_item = QTableWidgetItem(entry[6] or '') comment_item.setFont(font) table.setItem(row, col, comment_item) col += 1 # Actions actions_widget = self.create_actions_widget(entry[0], entry[4]) for child in actions_widget.findChildren(QToolButton): child.setFont(font) 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"padding-left: 2px; background-color: {color.name()};") layout.addWidget(table) total_entries += len(entries) comp_entries += sum(1 for e in entries if e[4] == 'completed') year_tab.setWidget(content) if total_entries > 0: perc = (comp_entries / total_entries * 100) if total_entries > 0 else 0 tab_text = f"{year} ({perc:.0f}%)" completed = (comp_entries == total_entries) index = self.tab_widget.addTab(year_tab, tab_text) if completed: self.tab_widget.tabBar().setTabTextColor(index, QColor('gray')) self.filter_tables(self.search_text) # Apply search filter after loading tabs def create_actions_widget(self, anime_id, status): widget = QWidget() layout = QHBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(2) # Edit edit_btn = QToolButton() edit_btn.setText("E") edit_btn.setToolTip("Edit") edit_btn.clicked.connect(lambda: self.edit_anime(anime_id)) layout.addWidget(edit_btn) # Delete del_btn = QToolButton() del_btn.setText("D") del_btn.setToolTip("Delete") del_btn.clicked.connect(lambda: self.delete_anime(anime_id)) layout.addWidget(del_btn) # Status buttons if status == 'unwatched': # Add W and C btn_w = QToolButton() btn_w.setText("W") btn_w.setToolTip("Set to watching") btn_w.clicked.connect(lambda: self.change_status(anime_id, 'watching')) layout.addWidget(btn_w) btn_c = QToolButton() btn_c.setText("C") btn_c.setToolTip("Set to completed") btn_c.clicked.connect(lambda: self.change_status(anime_id, 'completed')) layout.addWidget(btn_c) else: # Add U btn_u = QToolButton() btn_u.setText("U") btn_u.setToolTip("Set to unwatched") btn_u.clicked.connect(lambda: self.change_status(anime_id, 'unwatched')) layout.addWidget(btn_u) return widget def add_anime(self): tab_index = self.tab_widget.currentIndex() if tab_index == -1: default_year = 2010 default_season = '' else: tab_text = self.tab_widget.tabText(tab_index) if 'Pre-2010' in tab_text: default_year = 2009 default_season = '' else: parts = tab_text.split(' (') default_year = int(parts[0]) default_season = '' current_id = self.get_current_tab_identifier() current_scroll = self.get_current_scroll_pos() dialog = AnimeDialog(self, None, default_year, self.last_used_season) if dialog.exec_() == QDialog.Accepted: data = dialog.get_data() if data['season']: self.last_used_season = data['season'] self.settings.setValue("lastUsedSeason", self.last_used_season) if not data['name']: QMessageBox.warning(self, "Error", "Anime name cannot be empty.") return self.backend.add_anime(data) new_year = data['year'] new_id = "pre" if new_year < 2010 else new_year self.load_tabs() self.set_current_tab_by_identifier(new_id) if self.get_current_tab_identifier() == current_id: self.set_current_scroll_pos(current_scroll) def edit_anime(self, anime_id): entry = self.backend.get_anime_by_id(anime_id) if entry: current_id = self.get_current_tab_identifier() current_scroll = self.get_current_scroll_pos() dialog = AnimeDialog(self, entry) if dialog.exec_() == QDialog.Accepted: data = dialog.get_data() if not data['name']: QMessageBox.warning(self, "Error", "Anime name cannot be empty.") return self.backend.edit_anime(anime_id, data) new_year = data['year'] new_id = "pre" if new_year < 2010 else new_year self.load_tabs() self.set_current_tab_by_identifier(new_id) if self.get_current_tab_identifier() == current_id: self.set_current_scroll_pos(current_scroll) def delete_anime(self, anime_id): if QMessageBox.question(self, "Confirm Delete", "Are you sure you want to delete this entry?") == QMessageBox.Yes: current_id = self.get_current_tab_identifier() current_scroll = self.get_current_scroll_pos() self.backend.delete_anime(anime_id) self.load_tabs() self.set_current_tab_by_identifier(current_id) if self.get_current_tab_identifier() == current_id: self.set_current_scroll_pos(current_scroll) def change_status(self, anime_id, new_status): current_id = self.get_current_tab_identifier() current_scroll = self.get_current_scroll_pos() self.backend.change_status(anime_id, new_status) self.load_tabs() self.set_current_tab_by_identifier(current_id) if self.get_current_tab_identifier() == current_id: self.set_current_scroll_pos(current_scroll) def add_new_year(self): current_year = datetime.now().year year, ok = QInputDialog.getInt(self, "Add New Year", "Enter the year to add:", current_year + 1, 2010, 2100) if ok: self.backend.add_placeholders_for_year(year) self.load_tabs() self.set_current_tab_by_identifier(year) def delete_year(self): current_year = datetime.now().year current_id = self.get_current_tab_identifier() default_year = 2010 if current_id == 'pre' else current_id year, ok = QInputDialog.getInt(self, "Delete Year", "Enter the year to delete:", default_year, 2010, current_year + 10) if ok: years = self.backend.get_years() if year not in years: QMessageBox.warning(self, "Error", f"Year {year} does not exist.") return if QMessageBox.question(self, "Confirm Delete", f"Are you sure you want to delete all entries for {year}?") == QMessageBox.Yes: current_id = self.get_current_tab_identifier() current_scroll = self.get_current_scroll_pos() self.backend.delete_year(year) self.load_tabs() self.set_current_tab_by_identifier(current_id) if self.get_current_tab_identifier() == current_id: self.set_current_scroll_pos(current_scroll) def random_pick(self): if self.tab_widget.currentIndex() == -1: QMessageBox.information(self, "Random Pick", "No tab selected.") return 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(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()): status = table.item(row, status_col).text() if status == 'unwatched': name_text = table.cellWidget(row, name_col).text() clean_name = re.sub(r'<[^>]+>', '', name_text) unwatched.append(clean_name) if unwatched: random_name = random.choice(unwatched) QMessageBox.information(self, "Random Pick", f"Watch: {random_name}") else: QMessageBox.information(self, "Random Pick", "No unwatched anime in this tab.") def show_statistics(self): dialog = QDialog(self) dialog.setWindowTitle("Anime Statistics") layout = QVBoxLayout(dialog) total = self.backend.get_total_entries() completed = self.backend.get_completed_entries() percentage = (completed / total * 100) if total > 0 else 0 layout.addWidget(QLabel(f"Total anime entries: {total}")) layout.addWidget(QLabel(f"Total completed entries: {completed}")) layout.addWidget(QLabel(f"Percentage of completed entries: {percentage:.2f}%")) layout.addWidget(QLabel("Number of anime entries by type:")) types_data = self.backend.get_entries_by_type() if types_data: for typ, count in types_data: display_type = typ if typ else "None" # Handle empty type as 'None' layout.addWidget(QLabel(f" {display_type}: {count}")) else: layout.addWidget(QLabel(" No data available")) db_path = 'anime_backlog.db' if os.path.exists(db_path): db_size_kb = os.path.getsize(db_path) / 1024 layout.addWidget(QLabel(f"Size of the database: {db_size_kb:.2f} Kb")) else: layout.addWidget(QLabel("Size of the database: N/A (database file not found)")) buttons = QDialogButtonBox(QDialogButtonBox.Ok) buttons.accepted.connect(dialog.accept) layout.addWidget(buttons) dialog.exec_() def import_csv(self): file_name, _ = QFileDialog.getOpenFileName(self, "Import CSV", "", "CSV Files (*.csv)") if file_name: current_id = self.get_current_tab_identifier() current_scroll = self.get_current_scroll_pos() self.backend.import_from_csv(file_name) self.load_tabs() self.set_current_tab_by_identifier(current_id) if self.get_current_tab_identifier() == current_id: self.set_current_scroll_pos(current_scroll) def export_csv(self): file_name, _ = QFileDialog.getSaveFileName(self, "Export CSV", "anime_backlog.csv", "CSV Files (*.csv)") 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_R: self.random_pick() elif key == Qt.Key_Q: self.close() elif modifiers == Qt.ControlModifier: if key == Qt.Key_Plus or key == Qt.Key_Equal: current_id = self.get_current_tab_identifier() self.table_scale = min(self.table_scale + 0.1, 2.0) self.load_tabs() self.set_current_tab_by_identifier(current_id) elif key == Qt.Key_Minus: current_id = self.get_current_tab_identifier() self.table_scale = max(self.table_scale - 0.1, 0.5) self.load_tabs() self.set_current_tab_by_identifier(current_id) elif key == Qt.Key_Q: self.close() 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) super().keyPressEvent(event) if __name__ == '__main__': app = QApplication(sys.argv) window = AnimeTracker() window.show() sys.exit(app.exec_())