diff --git a/backend.py b/backend.py index a981f05..8682e75 100644 --- a/backend.py +++ b/backend.py @@ -30,10 +30,28 @@ class AnimeBackend: except Exception as e: logging.error(f"Error creating table: {e}") - def get_pre_2010_entries(self): + def get_pre_2010_entries(self, status_filter=None, type_filter=None, search=None, year_filter=None): try: cursor = self.db.cursor() - cursor.execute("SELECT * FROM anime WHERE year < 2010 ORDER BY year DESC, name") + sql = "SELECT * FROM anime WHERE year < 2010" + add_where = [] + params = [] + if year_filter is not None: + add_where.append("year = ?") + params.append(year_filter) + if status_filter is not None: + add_where.append("status = ?") + params.append(status_filter) + if type_filter is not None: + add_where.append("type = ?") + params.append(type_filter) + if search is not None: + add_where.append("(name LIKE ? OR comment LIKE ?)") + params.append(f"%{search}%") + params.append(f"%{search}%") + where_clause = " AND " + " AND ".join(add_where) if add_where else "" + sql += where_clause + " ORDER BY year DESC, name" + cursor.execute(sql, params) return cursor.fetchall() except Exception as e: logging.error(f"Error getting pre-2010 entries: {e}") @@ -48,10 +66,25 @@ class AnimeBackend: logging.error(f"Error getting years: {e}") return [] - def get_entries_for_season(self, year, season): + def get_entries_for_season(self, year, season, status_filter=None, type_filter=None, search=None): try: cursor = self.db.cursor() - cursor.execute("SELECT * FROM anime WHERE year = ? AND season = ? ORDER BY name", (year, season)) + sql = "SELECT * FROM anime WHERE year = ? AND season = ?" + params = [year, season] + add_where = [] + if status_filter is not None: + add_where.append("status = ?") + params.append(status_filter) + if type_filter is not None: + add_where.append("type = ?") + params.append(type_filter) + if search is not None: + add_where.append("(name LIKE ? OR comment LIKE ?)") + params.append(f"%{search}%") + params.append(f"%{search}%") + where_clause = " AND " + " AND ".join(add_where) if add_where else "" + sql += where_clause + " ORDER BY name" + cursor.execute(sql, params) return cursor.fetchall() except Exception as e: logging.error(f"Error getting entries for season {season} in year {year}: {e}") diff --git a/frontend.py b/frontend.py index 606a253..5f3a15e 100644 --- a/frontend.py +++ b/frontend.py @@ -9,7 +9,7 @@ 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 + QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView, QPushButton ) from PyQt5.QtCore import Qt, QSettings from PyQt5.QtGui import QColor, QIcon @@ -119,8 +119,6 @@ class CustomTableWidget(QTableWidget): if selected_rows: row = selected_rows[0].row() anime_id = int(self.item(row, 0).text()) - status_col = 4 if self.is_pre else 3 - status = self.item(row, status_col).text() if key == Qt.Key_Delete: self.parent_window.delete_anime(anime_id) return @@ -128,11 +126,15 @@ class CustomTableWidget(QTableWidget): self.parent_window.edit_anime(anime_id) return elif key == Qt.Key_W: - new_status = 'watching' if status == 'unwatched' else 'unwatched' + 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: - new_status = 'completed' if status == 'unwatched' else 'unwatched' + 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) @@ -145,7 +147,48 @@ class AnimeTracker(QMainWindow): self.resize(800, 600) self.backend = AnimeBackend() self.tab_widget = QTabWidget() - self.setCentralWidget(self.tab_widget) + self.central_widget = QWidget() + central_layout = QVBoxLayout(self.central_widget) + self.filter_bar = QWidget() + filter_layout = QHBoxLayout(self.filter_bar) + label_year = QLabel("Year:") + self.year_spin = QSpinBox() + self.year_spin.setRange(0, 2100) + self.year_spin.setValue(0) + self.year_spin.setSpecialValueText("All") + filter_layout.addWidget(label_year) + filter_layout.addWidget(self.year_spin) + label_season = QLabel("Season:") + self.season_combo = QComboBox() + self.season_combo.addItems(['All', 'winter', 'spring', 'summer', 'fall', 'Other']) + filter_layout.addWidget(label_season) + filter_layout.addWidget(self.season_combo) + label_status = QLabel("Status:") + self.status_combo = QComboBox() + self.status_combo.addItems(['All', 'unwatched', 'watching', 'completed']) + filter_layout.addWidget(label_status) + filter_layout.addWidget(self.status_combo) + label_type = QLabel("Type:") + self.type_combo = QComboBox() + self.type_combo.addItems(['All', 'TV', 'Movie', 'OVA', 'Special', 'Short TV', 'Other']) + filter_layout.addWidget(label_type) + filter_layout.addWidget(self.type_combo) + label_search = QLabel("Search:") + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("Search name or comment") + filter_layout.addWidget(label_search) + filter_layout.addWidget(self.search_edit) + apply_btn = QPushButton("Apply") + apply_btn.clicked.connect(self.apply_filters) + filter_layout.addWidget(apply_btn) + central_layout.addWidget(self.filter_bar) + central_layout.addWidget(self.tab_widget) + self.setCentralWidget(self.central_widget) + self.year_filter = None + self.season_filter = None + self.status_filter = None + self.type_filter = None + self.search_text = None self.create_menu() self.load_tabs() self.restoreGeometry(self.settings.value("geometry", self.saveGeometry())) @@ -153,6 +196,19 @@ class AnimeTracker(QMainWindow): if last_tab is not None: self.set_current_tab_by_identifier(last_tab) + def apply_filters(self): + year_val = self.year_spin.value() + self.year_filter = year_val if year_val > 0 else None + season_val = self.season_combo.currentText() + self.season_filter = None if season_val == 'All' else ('' if season_val == 'Other' else season_val) + status_val = self.status_combo.currentText() + self.status_filter = None if status_val == 'All' else status_val + type_val = self.type_combo.currentText() + self.type_filter = None if type_val == 'All' else type_val + search_val = self.search_edit.text().strip() + self.search_text = search_val if search_val else None + self.load_tabs() + def closeEvent(self, event): self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("lastTab", self.get_current_tab_identifier()) @@ -220,114 +276,121 @@ class AnimeTracker(QMainWindow): def load_tabs(self): self.tab_widget.clear() - # 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.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 = QLabel() - 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 '') - 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" - 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')) + show_pre = True + if self.season_filter is not None and self.season_filter != '': + show_pre = False + if self.year_filter is not None and self.year_filter >= 2010: + show_pre = False + if show_pre: + pre_entries = self.backend.get_pre_2010_entries(self.status_filter, self.type_filter, self.search_text, self.year_filter if self.year_filter and self.year_filter < 2010 else None) + 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.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 = QLabel() + 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 '') + 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" + 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() + if self.year_filter is not None: + years = [self.year_filter] if self.year_filter in years else [] for year in years: year_tab = QScrollArea() year_tab.setWidgetResizable(True) @@ -336,96 +399,101 @@ class AnimeTracker(QMainWindow): total_entries = 0 comp_entries = 0 for season in ['winter', 'spring', 'summer', 'fall', '']: - entries = self.backend.get_entries_for_season(year, season) - if entries: - s_name = season.capitalize() if season else 'Other' - label = QLabel(s_name) - layout.addWidget(label) - 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 = QLabel() - 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 '') - 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') + show_section = True + if self.season_filter is not None: + if season != self.season_filter: + show_section = False + if show_section: + entries = self.backend.get_entries_for_season(year, season, self.status_filter, self.type_filter, self.search_text) + if entries: + s_name = season.capitalize() if season else 'Other' + label = QLabel(s_name) + layout.addWidget(label) + 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 = QLabel() + 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 '') + 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') year_tab.setWidget(content) - if total_entries > 0: + if total_entries > 0 or (self.year_filter == year): perc = (comp_entries / total_entries * 100) if total_entries > 0 else 0 tab_text = f"{year} ({perc:.0f}%)" completed = (comp_entries == total_entries)