Add a search bar and filters (by status, type, year, season) right above the table.

This commit is contained in:
Bernd 2025-07-20 17:02:59 +05:00
parent 307cd2fd43
commit 6fc4f716b7
2 changed files with 306 additions and 205 deletions

View File

@ -30,10 +30,28 @@ class AnimeBackend:
except Exception as e: except Exception as e:
logging.error(f"Error creating table: {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: try:
cursor = self.db.cursor() 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() return cursor.fetchall()
except Exception as e: except Exception as e:
logging.error(f"Error getting pre-2010 entries: {e}") logging.error(f"Error getting pre-2010 entries: {e}")
@ -48,10 +66,25 @@ class AnimeBackend:
logging.error(f"Error getting years: {e}") logging.error(f"Error getting years: {e}")
return [] 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: try:
cursor = self.db.cursor() 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() return cursor.fetchall()
except Exception as e: except Exception as e:
logging.error(f"Error getting entries for season {season} in year {year}: {e}") logging.error(f"Error getting entries for season {season} in year {year}: {e}")

View File

@ -9,7 +9,7 @@ from PyQt5.QtWidgets import (
QMainWindow, QTabWidget, QScrollArea, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QMainWindow, QTabWidget, QScrollArea, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
QLabel, QToolButton, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QSpinBox, QLabel, QToolButton, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QSpinBox,
QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox, 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.QtCore import Qt, QSettings
from PyQt5.QtGui import QColor, QIcon from PyQt5.QtGui import QColor, QIcon
@ -119,8 +119,6 @@ class CustomTableWidget(QTableWidget):
if selected_rows: if selected_rows:
row = selected_rows[0].row() row = selected_rows[0].row()
anime_id = int(self.item(row, 0).text()) 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: if key == Qt.Key_Delete:
self.parent_window.delete_anime(anime_id) self.parent_window.delete_anime(anime_id)
return return
@ -128,11 +126,15 @@ class CustomTableWidget(QTableWidget):
self.parent_window.edit_anime(anime_id) self.parent_window.edit_anime(anime_id)
return return
elif key == Qt.Key_W: 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) self.parent_window.change_status(anime_id, new_status)
return return
elif key == Qt.Key_C: 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) self.parent_window.change_status(anime_id, new_status)
return return
super().keyPressEvent(event) super().keyPressEvent(event)
@ -145,7 +147,48 @@ class AnimeTracker(QMainWindow):
self.resize(800, 600) self.resize(800, 600)
self.backend = AnimeBackend() self.backend = AnimeBackend()
self.tab_widget = QTabWidget() 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.create_menu()
self.load_tabs() self.load_tabs()
self.restoreGeometry(self.settings.value("geometry", self.saveGeometry())) self.restoreGeometry(self.settings.value("geometry", self.saveGeometry()))
@ -153,6 +196,19 @@ class AnimeTracker(QMainWindow):
if last_tab is not None: if last_tab is not None:
self.set_current_tab_by_identifier(last_tab) 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): def closeEvent(self, event):
self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("geometry", self.saveGeometry())
self.settings.setValue("lastTab", self.get_current_tab_identifier()) self.settings.setValue("lastTab", self.get_current_tab_identifier())
@ -220,114 +276,121 @@ class AnimeTracker(QMainWindow):
def load_tabs(self): def load_tabs(self):
self.tab_widget.clear() self.tab_widget.clear()
# Pre-2010 tab show_pre = True
pre_entries = self.backend.get_pre_2010_entries() if self.season_filter is not None and self.season_filter != '':
pre_tab = QScrollArea() show_pre = False
pre_tab.setWidgetResizable(True) if self.year_filter is not None and self.year_filter >= 2010:
pre_content = QWidget() show_pre = False
pre_layout = QVBoxLayout(pre_content) if show_pre:
if pre_entries: 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)
table = CustomTableWidget(self, is_pre=True) pre_tab = QScrollArea()
table.setRowCount(len(pre_entries)) pre_tab.setWidgetResizable(True)
table.setColumnCount(7) pre_content = QWidget()
headers = ['ID', 'Year', 'Name', 'Type', 'Status', 'Comment', 'Actions'] pre_layout = QVBoxLayout(pre_content)
table.setHorizontalHeaderLabels(headers) if pre_entries:
table.setColumnHidden(0, True) table = CustomTableWidget(self, is_pre=True)
table.setAlternatingRowColors(True) table.setRowCount(len(pre_entries))
table.setShowGrid(True) table.setColumnCount(7)
header = table.horizontalHeader() headers = ['ID', 'Year', 'Name', 'Type', 'Status', 'Comment', 'Actions']
header.setStretchLastSection(False) table.setHorizontalHeaderLabels(headers)
table.setSelectionBehavior(QAbstractItemView.SelectRows) table.setColumnHidden(0, True)
table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) table.setAlternatingRowColors(True)
table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) table.setShowGrid(True)
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID hidden header = table.horizontalHeader()
header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Year header.setStretchLastSection(False)
header.setSectionResizeMode(2, QHeaderView.Stretch) # Name table.setSelectionBehavior(QAbstractItemView.SelectRows)
header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Type table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Status table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
header.setSectionResizeMode(5, QHeaderView.Stretch) # Comment header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID hidden
header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # Actions header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Year
for row, entry in enumerate(pre_entries): header.setSectionResizeMode(2, QHeaderView.Stretch) # Name
col = 0 header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Type
# ID header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Status
id_item = QTableWidgetItem(str(entry[0])) header.setSectionResizeMode(5, QHeaderView.Stretch) # Comment
table.setItem(row, col, id_item) header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # Actions
col += 1 for row, entry in enumerate(pre_entries):
# Year col = 0
year_item = QTableWidgetItem(str(entry[2])) # ID
table.setItem(row, col, year_item) id_item = QTableWidgetItem(str(entry[0]))
col += 1 table.setItem(row, col, id_item)
# Name col += 1
name = entry[1] # Year
url = entry[7] year_item = QTableWidgetItem(str(entry[2]))
name_label = QLabel() table.setItem(row, col, year_item)
if url: col += 1
name_escaped = html.escape(name) # Name
name_label.setText(f'<a href="{url}">{name_escaped}</a>') name = entry[1]
name_label.setOpenExternalLinks(True) url = entry[7]
else: name_label = QLabel()
name_label.setText(html.escape(name)) if url:
table.setCellWidget(row, col, name_label) name_escaped = html.escape(name)
col += 1 name_label.setText(f'<a href="{url}">{name_escaped}</a>')
# Type name_label.setOpenExternalLinks(True)
type_item = QTableWidgetItem(entry[5] or '') else:
table.setItem(row, col, type_item) name_label.setText(html.escape(name))
col += 1 table.setCellWidget(row, col, name_label)
# Status col += 1
status_item = QTableWidgetItem(entry[4]) # Type
table.setItem(row, col, status_item) type_item = QTableWidgetItem(entry[5] or '')
col += 1 table.setItem(row, col, type_item)
# Comment col += 1
comment_item = QTableWidgetItem(entry[6] or '') # Status
table.setItem(row, col, comment_item) status_item = QTableWidgetItem(entry[4])
col += 1 table.setItem(row, col, status_item)
# Actions col += 1
actions_widget = self.create_actions_widget(entry[0], entry[4]) # Comment
table.setCellWidget(row, col, actions_widget) comment_item = QTableWidgetItem(entry[6] or '')
table.resizeColumnsToContents() table.setItem(row, col, comment_item)
table.resizeRowsToContents() col += 1
# Calculate fixed height # Actions
height = table.horizontalHeader().height() + 2 # small margin actions_widget = self.create_actions_widget(entry[0], entry[4])
for i in range(table.rowCount()): table.setCellWidget(row, col, actions_widget)
height += table.rowHeight(i) table.resizeColumnsToContents()
table.setFixedHeight(height) table.resizeRowsToContents()
# Apply status colors # Calculate fixed height
status_col = 4 height = table.horizontalHeader().height() + 2 # small margin
for r in range(table.rowCount()): for i in range(table.rowCount()):
status = table.item(r, status_col).text() height += table.rowHeight(i)
if status == 'unwatched': table.setFixedHeight(height)
color = QColor(255, 255, 255) # Apply status colors
elif status == 'watching': status_col = 4
color = QColor(255, 255, 0) for r in range(table.rowCount()):
elif status == 'completed': status = table.item(r, status_col).text()
color = QColor(0, 255, 0) if status == 'unwatched':
else: color = QColor(255, 255, 255)
color = QColor(255, 255, 255) elif status == 'watching':
for c in range(table.columnCount()): color = QColor(255, 255, 0)
if c == 0: elif status == 'completed':
continue # skip hidden id color = QColor(0, 255, 0)
item = table.item(r, c) else:
if item: color = QColor(255, 255, 255)
item.setBackground(color) for c in range(table.columnCount()):
widget = table.cellWidget(r, c) if c == 0:
if widget: continue # skip hidden id
widget.setStyleSheet(f"background-color: {color.name()};") item = table.item(r, c)
pre_layout.addWidget(table) if item:
pre_tab.setWidget(pre_content) item.setBackground(color)
tab_text = "Pre-2010" widget = table.cellWidget(r, c)
completed = False if widget:
total = len(pre_entries) widget.setStyleSheet(f"background-color: {color.name()};")
if total > 0: pre_layout.addWidget(table)
comp = sum(1 for e in pre_entries if e[4] == 'completed') pre_tab.setWidget(pre_content)
perc = (comp / total * 100) tab_text = "Pre-2010"
tab_text += f" ({perc:.0f}%)" completed = False
if comp == total: total = len(pre_entries)
completed = True if total > 0:
index = self.tab_widget.addTab(pre_tab, tab_text) comp = sum(1 for e in pre_entries if e[4] == 'completed')
if completed: perc = (comp / total * 100)
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray')) 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 >= 2010
years = self.backend.get_years() 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: for year in years:
year_tab = QScrollArea() year_tab = QScrollArea()
year_tab.setWidgetResizable(True) year_tab.setWidgetResizable(True)
@ -336,96 +399,101 @@ class AnimeTracker(QMainWindow):
total_entries = 0 total_entries = 0
comp_entries = 0 comp_entries = 0
for season in ['winter', 'spring', 'summer', 'fall', '']: for season in ['winter', 'spring', 'summer', 'fall', '']:
entries = self.backend.get_entries_for_season(year, season) show_section = True
if entries: if self.season_filter is not None:
s_name = season.capitalize() if season else 'Other' if season != self.season_filter:
label = QLabel(s_name) show_section = False
layout.addWidget(label) if show_section:
table = CustomTableWidget(self, is_pre=False) entries = self.backend.get_entries_for_season(year, season, self.status_filter, self.type_filter, self.search_text)
table.setRowCount(len(entries)) if entries:
table.setColumnCount(6) s_name = season.capitalize() if season else 'Other'
headers = ['ID', 'Name', 'Type', 'Status', 'Comment', 'Actions'] label = QLabel(s_name)
table.setHorizontalHeaderLabels(headers) layout.addWidget(label)
table.setColumnHidden(0, True) table = CustomTableWidget(self, is_pre=False)
table.setAlternatingRowColors(True) table.setRowCount(len(entries))
table.setShowGrid(True) table.setColumnCount(6)
header = table.horizontalHeader() headers = ['ID', 'Name', 'Type', 'Status', 'Comment', 'Actions']
header.setStretchLastSection(False) table.setHorizontalHeaderLabels(headers)
table.setSelectionBehavior(QAbstractItemView.SelectRows) table.setColumnHidden(0, True)
table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) table.setAlternatingRowColors(True)
table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) table.setShowGrid(True)
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID hidden header = table.horizontalHeader()
header.setSectionResizeMode(1, QHeaderView.Stretch) # Name header.setStretchLastSection(False)
header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Type table.setSelectionBehavior(QAbstractItemView.SelectRows)
header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Status table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
header.setSectionResizeMode(4, QHeaderView.Stretch) # Comment table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Actions header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID hidden
for row, entry in enumerate(entries): header.setSectionResizeMode(1, QHeaderView.Stretch) # Name
col = 0 header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Type
# ID header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Status
id_item = QTableWidgetItem(str(entry[0])) header.setSectionResizeMode(4, QHeaderView.Stretch) # Comment
table.setItem(row, col, id_item) header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Actions
col += 1 for row, entry in enumerate(entries):
# Name col = 0
name = entry[1] # ID
url = entry[7] id_item = QTableWidgetItem(str(entry[0]))
name_label = QLabel() table.setItem(row, col, id_item)
if url: col += 1
name_escaped = html.escape(name) # Name
name_label.setText(f'<a href="{url}">{name_escaped}</a>') name = entry[1]
name_label.setOpenExternalLinks(True) url = entry[7]
else: name_label = QLabel()
name_label.setText(html.escape(name)) if url:
table.setCellWidget(row, col, name_label) name_escaped = html.escape(name)
col += 1 name_label.setText(f'<a href="{url}">{name_escaped}</a>')
# Type name_label.setOpenExternalLinks(True)
type_item = QTableWidgetItem(entry[5] or '') else:
table.setItem(row, col, type_item) name_label.setText(html.escape(name))
col += 1 table.setCellWidget(row, col, name_label)
# Status col += 1
status_item = QTableWidgetItem(entry[4]) # Type
table.setItem(row, col, status_item) type_item = QTableWidgetItem(entry[5] or '')
col += 1 table.setItem(row, col, type_item)
# Comment col += 1
comment_item = QTableWidgetItem(entry[6] or '') # Status
table.setItem(row, col, comment_item) status_item = QTableWidgetItem(entry[4])
col += 1 table.setItem(row, col, status_item)
# Actions col += 1
actions_widget = self.create_actions_widget(entry[0], entry[4]) # Comment
table.setCellWidget(row, col, actions_widget) comment_item = QTableWidgetItem(entry[6] or '')
table.resizeColumnsToContents() table.setItem(row, col, comment_item)
table.resizeRowsToContents() col += 1
# Calculate fixed height # Actions
height = table.horizontalHeader().height() + 2 # small margin actions_widget = self.create_actions_widget(entry[0], entry[4])
for i in range(table.rowCount()): table.setCellWidget(row, col, actions_widget)
height += table.rowHeight(i) table.resizeColumnsToContents()
table.setFixedHeight(height) table.resizeRowsToContents()
# Apply status colors # Calculate fixed height
status_col = 3 height = table.horizontalHeader().height() + 2 # small margin
for r in range(table.rowCount()): for i in range(table.rowCount()):
status = table.item(r, status_col).text() height += table.rowHeight(i)
if status == 'unwatched': table.setFixedHeight(height)
color = QColor(255, 255, 255) # Apply status colors
elif status == 'watching': status_col = 3
color = QColor(255, 255, 0) for r in range(table.rowCount()):
elif status == 'completed': status = table.item(r, status_col).text()
color = QColor(0, 255, 0) if status == 'unwatched':
else: color = QColor(255, 255, 255)
color = QColor(255, 255, 255) elif status == 'watching':
for c in range(table.columnCount()): color = QColor(255, 255, 0)
if c == 0: elif status == 'completed':
continue # skip hidden id color = QColor(0, 255, 0)
item = table.item(r, c) else:
if item: color = QColor(255, 255, 255)
item.setBackground(color) for c in range(table.columnCount()):
widget = table.cellWidget(r, c) if c == 0:
if widget: continue # skip hidden id
widget.setStyleSheet(f"background-color: {color.name()};") item = table.item(r, c)
layout.addWidget(table) if item:
total_entries += len(entries) item.setBackground(color)
comp_entries += sum(1 for e in entries if e[4] == 'completed') 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) 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 perc = (comp_entries / total_entries * 100) if total_entries > 0 else 0
tab_text = f"{year} ({perc:.0f}%)" tab_text = f"{year} ({perc:.0f}%)"
completed = (comp_entries == total_entries) completed = (comp_entries == total_entries)