improving Search functionality

This commit is contained in:
Bernd 2025-07-23 20:23:29 +05:00
parent b5314497da
commit bee297bf19
3 changed files with 264 additions and 334 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ __pycache__/
anime_tracker.log anime_tracker.log
todo.txt todo.txt
*.csv *.csv
anime_backlog.db.bk

View File

@ -30,28 +30,10 @@ 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, status_filter=None, type_filter=None, search=None, year_filter=None): def get_pre_2010_entries(self):
try: try:
cursor = self.db.cursor() cursor = self.db.cursor()
sql = "SELECT * FROM anime WHERE year < 2010" cursor.execute("SELECT * FROM anime WHERE year < 2010 ORDER BY year DESC, name")
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}")
@ -66,25 +48,10 @@ 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, status_filter=None, type_filter=None, search=None): def get_entries_for_season(self, year, season):
try: try:
cursor = self.db.cursor() cursor = self.db.cursor()
sql = "SELECT * FROM anime WHERE year = ? AND season = ?" cursor.execute("SELECT * FROM anime WHERE year = ? AND season = ? ORDER BY name", (year, 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

@ -153,45 +153,18 @@ class AnimeTracker(QMainWindow):
central_layout = QVBoxLayout(self.central_widget) central_layout = QVBoxLayout(self.central_widget)
self.filter_bar = QWidget() self.filter_bar = QWidget()
filter_layout = QHBoxLayout(self.filter_bar) 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:") label_search = QLabel("Search:")
self.search_edit = QLineEdit() self.search_edit = QLineEdit()
self.search_edit.setPlaceholderText("Search name or comment") self.search_edit.setPlaceholderText("Search name")
self.search_edit.textChanged.connect(self.filter_tables)
filter_layout.addWidget(label_search) filter_layout.addWidget(label_search)
filter_layout.addWidget(self.search_edit) 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.filter_bar)
central_layout.addWidget(self.tab_widget) central_layout.addWidget(self.tab_widget)
self.setCentralWidget(self.central_widget) self.setCentralWidget(self.central_widget)
self.year_filter = None self.search_text = ''
self.season_filter = None
self.status_filter = None
self.type_filter = None
self.search_text = None
self.table_scale = self.settings.value("tableScale", 1.0, type=float) self.table_scale = self.settings.value("tableScale", 1.0, type=float)
self.tables = []
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()))
@ -200,18 +173,14 @@ class AnimeTracker(QMainWindow):
self.set_current_tab_by_identifier(last_tab) self.set_current_tab_by_identifier(last_tab)
self.tab_widget.setFocus() self.tab_widget.setFocus()
def apply_filters(self): def filter_tables(self, text):
year_val = self.year_spin.value() self.search_text = text.strip().lower()
self.year_filter = year_val if year_val > 0 else None for table in self.tables:
season_val = self.season_combo.currentText() for row in range(table.rowCount()):
self.season_filter = None if season_val == 'All' else ('' if season_val == 'Other' else season_val) name_col = 2 if table.is_pre else 1
status_val = self.status_combo.currentText() name_text = table.cellWidget(row, name_col).text()
self.status_filter = None if status_val == 'All' else status_val clean_name = re.sub(r'<[^>]+>', '', name_text).lower()
type_val = self.type_combo.currentText() table.setRowHidden(row, self.search_text not in clean_name if self.search_text else False)
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())
@ -281,143 +250,139 @@ class AnimeTracker(QMainWindow):
def load_tabs(self): def load_tabs(self):
self.tab_widget.clear() self.tab_widget.clear()
show_pre = True self.tables = []
if self.season_filter is not None and self.season_filter != '': # Pre-2010 tab
show_pre = False pre_entries = self.backend.get_pre_2010_entries()
if self.year_filter is not None and self.year_filter >= 2010: pre_tab = QScrollArea()
show_pre = False pre_tab.setWidgetResizable(True)
if show_pre: pre_content = QWidget()
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_layout = QVBoxLayout(pre_content)
pre_tab = QScrollArea() if pre_entries:
pre_tab.setWidgetResizable(True) table = CustomTableWidget(self, is_pre=True)
pre_content = QWidget() table.is_pre = True
pre_layout = QVBoxLayout(pre_content) self.tables.append(table)
if pre_entries: table.setRowCount(len(pre_entries))
table = CustomTableWidget(self, is_pre=True) table.setColumnCount(7)
table.setRowCount(len(pre_entries)) headers = ['ID', 'Year', 'Name', 'Type', 'Status', 'Comment', 'Actions']
table.setColumnCount(7) table.setHorizontalHeaderLabels(headers)
headers = ['ID', 'Year', 'Name', 'Type', 'Status', 'Comment', 'Actions'] table.setColumnHidden(0, True)
table.setHorizontalHeaderLabels(headers) table.setAlternatingRowColors(True)
table.setColumnHidden(0, True) table.setShowGrid(True)
table.setAlternatingRowColors(True) header = table.horizontalHeader()
table.setShowGrid(True) header.setStretchLastSection(False)
header = table.horizontalHeader() table.setSelectionBehavior(QAbstractItemView.SelectRows)
header.setStretchLastSection(False) table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
table.setSelectionBehavior(QAbstractItemView.SelectRows) table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) header.setSectionResizeMode(0, QHeaderView.Fixed) # ID hidden
table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Year
header.setSectionResizeMode(0, QHeaderView.Fixed) # ID hidden header.setSectionResizeMode(2, QHeaderView.Stretch) # Name
header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Year header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Type
header.setSectionResizeMode(2, QHeaderView.Stretch) # Name header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Status
header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Type header.setSectionResizeMode(5, QHeaderView.Stretch) # Comment
header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Status header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # Actions
header.setSectionResizeMode(5, QHeaderView.Stretch) # Comment table.setColumnWidth(0, 0) # Set ID column width to 0 to ensure it's hidden
header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # Actions font = QFont()
table.setColumnWidth(0, 0) # Set ID column width to 0 to ensure it's hidden font.setPointSize(int(10 * self.table_scale))
font = QFont() header_font = QFont()
font.setPointSize(int(10 * self.table_scale)) header_font.setPointSize(int(10 * self.table_scale))
header_font = QFont() header_font.setBold(True)
header_font.setPointSize(int(10 * self.table_scale)) table.setFont(font)
header_font.setBold(True) table.horizontalHeader().setFont(header_font)
table.setFont(font) table.verticalHeader().setFont(font)
table.horizontalHeader().setFont(header_font) for row, entry in enumerate(pre_entries):
table.verticalHeader().setFont(font) col = 0
for row, entry in enumerate(pre_entries): # ID
col = 0 id_item = QTableWidgetItem(str(entry[0]))
# ID table.setItem(row, col, id_item)
id_item = QTableWidgetItem(str(entry[0])) col += 1
table.setItem(row, col, id_item) # Year
col += 1 year_item = QTableWidgetItem(str(entry[2]))
# Year year_item.setFont(font)
year_item = QTableWidgetItem(str(entry[2])) table.setItem(row, col, year_item)
year_item.setFont(font) col += 1
table.setItem(row, col, year_item) # Name
col += 1 name = entry[1]
# Name url = entry[7]
name = entry[1] name_label = QLabel()
url = entry[7] name_font = QFont(font)
name_label = QLabel() if entry[4] == 'watching':
name_font = QFont(font) name_font.setItalic(True)
if entry[4] == 'watching': elif entry[4] == 'completed':
name_font.setItalic(True) name_font.setStrikeOut(True)
elif entry[4] == 'completed': name_label.setFont(name_font)
name_font.setStrikeOut(True) if url:
name_label.setFont(name_font) name_escaped = html.escape(name)
if url: name_label.setText(f'<a href="{url}" style="color: #0000FF; text-decoration: none;">{name_escaped}</a>')
name_escaped = html.escape(name) name_label.setOpenExternalLinks(True)
name_label.setText(f'<a href="{url}" style="color: #0000FF; text-decoration: none;">{name_escaped}</a>') else:
name_label.setOpenExternalLinks(True) name_label.setText(html.escape(name))
else: name_label.setStyleSheet("padding-left: 10px;")
name_label.setText(html.escape(name)) table.setCellWidget(row, col, name_label)
name_label.setStyleSheet("padding-left: 10px;") col += 1
table.setCellWidget(row, col, name_label) # Type
col += 1 type_item = QTableWidgetItem(entry[5] or '')
# Type type_item.setFont(font)
type_item = QTableWidgetItem(entry[5] or '') table.setItem(row, col, type_item)
type_item.setFont(font) col += 1
table.setItem(row, col, type_item) # Status
col += 1 status_item = QTableWidgetItem(entry[4])
# Status status_item.setFont(font)
status_item = QTableWidgetItem(entry[4]) table.setItem(row, col, status_item)
status_item.setFont(font) col += 1
table.setItem(row, col, status_item) # Comment
col += 1 comment_item = QTableWidgetItem(entry[6] or '')
# Comment comment_item.setFont(font)
comment_item = QTableWidgetItem(entry[6] or '') table.setItem(row, col, comment_item)
comment_item.setFont(font) col += 1
table.setItem(row, col, comment_item) # Actions
col += 1 actions_widget = self.create_actions_widget(entry[0], entry[4])
# Actions for child in actions_widget.findChildren(QToolButton):
actions_widget = self.create_actions_widget(entry[0], entry[4]) child.setFont(font)
for child in actions_widget.findChildren(QToolButton): table.setCellWidget(row, col, actions_widget)
child.setFont(font) table.resizeColumnsToContents()
table.setCellWidget(row, col, actions_widget) table.resizeRowsToContents()
table.resizeColumnsToContents() # Calculate fixed height
table.resizeRowsToContents() height = table.horizontalHeader().height() + 2 # small margin
# Calculate fixed height for i in range(table.rowCount()):
height = table.horizontalHeader().height() + 2 # small margin height += table.rowHeight(i)
for i in range(table.rowCount()): table.setFixedHeight(height)
height += table.rowHeight(i) # Apply status colors
table.setFixedHeight(height) status_col = 4
# Apply status colors for r in range(table.rowCount()):
status_col = 4 status = table.item(r, status_col).text()
for r in range(table.rowCount()): if status == 'unwatched':
status = table.item(r, status_col).text() color = QColor(255, 255, 255)
if status == 'unwatched': elif status == 'watching':
color = QColor(255, 255, 255) color = QColor(255, 255, 0)
elif status == 'watching': elif status == 'completed':
color = QColor(255, 255, 0) color = QColor(0, 255, 0)
elif status == 'completed': else:
color = QColor(0, 255, 0) color = QColor(255, 255, 255)
else: for c in range(table.columnCount()):
color = QColor(255, 255, 255) if c == 0:
for c in range(table.columnCount()): continue # skip hidden id
if c == 0: item = table.item(r, c)
continue # skip hidden id if item:
item = table.item(r, c) item.setBackground(color)
if item: widget = table.cellWidget(r, c)
item.setBackground(color) if widget:
widget = table.cellWidget(r, c) widget.setStyleSheet(f"background-color: {color.name()};")
if widget: pre_layout.addWidget(table)
widget.setStyleSheet(f"background-color: {color.name()};") pre_tab.setWidget(pre_content)
pre_layout.addWidget(table) tab_text = "Pre-2010"
pre_tab.setWidget(pre_content) completed = False
tab_text = "Pre-2010" total = len(pre_entries)
completed = False if total > 0:
total = len(pre_entries) comp = sum(1 for e in pre_entries if e[4] == 'completed')
if total > 0: perc = (comp / total * 100)
comp = sum(1 for e in pre_entries if e[4] == 'completed') tab_text += f" ({perc:.0f}%)"
perc = (comp / total * 100) if comp == total:
tab_text += f" ({perc:.0f}%)" completed = True
if comp == total: index = self.tab_widget.addTab(pre_tab, tab_text)
completed = True if completed:
index = self.tab_widget.addTab(pre_tab, tab_text) self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
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)
@ -426,126 +391,123 @@ 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', '']:
show_section = True entries = self.backend.get_entries_for_season(year, season)
if self.season_filter is not None: if entries:
if season != self.season_filter: s_name = season.capitalize() if season else 'Other'
show_section = False label = QLabel(s_name)
if show_section: season_font = QFont()
entries = self.backend.get_entries_for_season(year, season, self.status_filter, self.type_filter, self.search_text) season_font.setPointSize(int(12 * self.table_scale))
if entries: season_font.setBold(True)
s_name = season.capitalize() if season else 'Other' label.setFont(season_font)
label = QLabel(s_name) layout.addWidget(label)
season_font = QFont() table = CustomTableWidget(self, is_pre=False)
season_font.setPointSize(int(12 * self.table_scale)) table.is_pre = False
season_font.setBold(True) self.tables.append(table)
label.setFont(season_font) table.setRowCount(len(entries))
layout.addWidget(label) table.setColumnCount(6)
table = CustomTableWidget(self, is_pre=False) headers = ['ID', 'Name', 'Type', 'Status', 'Comment', 'Actions']
table.setRowCount(len(entries)) table.setHorizontalHeaderLabels(headers)
table.setColumnCount(6) table.setColumnHidden(0, True)
headers = ['ID', 'Name', 'Type', 'Status', 'Comment', 'Actions'] table.setAlternatingRowColors(True)
table.setHorizontalHeaderLabels(headers) table.setShowGrid(True)
table.setColumnHidden(0, True) header = table.horizontalHeader()
table.setAlternatingRowColors(True) header.setStretchLastSection(False)
table.setShowGrid(True) table.setSelectionBehavior(QAbstractItemView.SelectRows)
header = table.horizontalHeader() table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
header.setStretchLastSection(False) table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
table.setSelectionBehavior(QAbstractItemView.SelectRows) header.setSectionResizeMode(0, QHeaderView.Fixed) # ID hidden
table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) header.setSectionResizeMode(1, QHeaderView.Stretch) # Name
table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Type
header.setSectionResizeMode(0, QHeaderView.Fixed) # ID hidden header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Status
header.setSectionResizeMode(1, QHeaderView.Stretch) # Name header.setSectionResizeMode(4, QHeaderView.Stretch) # Comment
header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Type header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Actions
header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Status table.setColumnWidth(0, 0) # Set ID column width to 0 to ensure it's hidden
header.setSectionResizeMode(4, QHeaderView.Stretch) # Comment font = QFont()
header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Actions font.setPointSize(int(10 * self.table_scale))
table.setColumnWidth(0, 0) # Set ID column width to 0 to ensure it's hidden header_font = QFont()
font = QFont() header_font.setPointSize(int(10 * self.table_scale))
font.setPointSize(int(10 * self.table_scale)) header_font.setBold(True)
header_font = QFont() table.setFont(font)
header_font.setPointSize(int(10 * self.table_scale)) table.horizontalHeader().setFont(header_font)
header_font.setBold(True) table.verticalHeader().setFont(font)
table.setFont(font) for row, entry in enumerate(entries):
table.horizontalHeader().setFont(header_font) col = 0
table.verticalHeader().setFont(font) # ID
for row, entry in enumerate(entries): id_item = QTableWidgetItem(str(entry[0]))
col = 0 table.setItem(row, col, id_item)
# ID col += 1
id_item = QTableWidgetItem(str(entry[0])) # Name
table.setItem(row, col, id_item) name = entry[1]
col += 1 url = entry[7]
# Name name_label = QLabel()
name = entry[1] name_font = QFont(font)
url = entry[7] if entry[4] == 'watching':
name_label = QLabel() name_font.setItalic(True)
name_font = QFont(font) elif entry[4] == 'completed':
if entry[4] == 'watching': name_font.setStrikeOut(True)
name_font.setItalic(True) name_label.setFont(name_font)
elif entry[4] == 'completed': if url:
name_font.setStrikeOut(True) name_escaped = html.escape(name)
name_label.setFont(name_font) name_label.setText(f'<a href="{url}" style="color: #0000FF; text-decoration: none;">{name_escaped}</a>')
if url: name_label.setOpenExternalLinks(True)
name_escaped = html.escape(name) else:
name_label.setText(f'<a href="{url}" style="color: #0000FF; text-decoration: none;">{name_escaped}</a>') name_label.setText(html.escape(name))
name_label.setOpenExternalLinks(True) name_label.setStyleSheet("padding-left: 10px;")
else: table.setCellWidget(row, col, name_label)
name_label.setText(html.escape(name)) col += 1
name_label.setStyleSheet("padding-left: 10px;") # Type
table.setCellWidget(row, col, name_label) type_item = QTableWidgetItem(entry[5] or '')
col += 1 type_item.setFont(font)
# Type table.setItem(row, col, type_item)
type_item = QTableWidgetItem(entry[5] or '') col += 1
type_item.setFont(font) # Status
table.setItem(row, col, type_item) status_item = QTableWidgetItem(entry[4])
col += 1 status_item.setFont(font)
# Status table.setItem(row, col, status_item)
status_item = QTableWidgetItem(entry[4]) col += 1
status_item.setFont(font) # Comment
table.setItem(row, col, status_item) comment_item = QTableWidgetItem(entry[6] or '')
col += 1 comment_item.setFont(font)
# Comment table.setItem(row, col, comment_item)
comment_item = QTableWidgetItem(entry[6] or '') col += 1
comment_item.setFont(font) # Actions
table.setItem(row, col, comment_item) actions_widget = self.create_actions_widget(entry[0], entry[4])
col += 1 for child in actions_widget.findChildren(QToolButton):
# Actions child.setFont(font)
actions_widget = self.create_actions_widget(entry[0], entry[4]) table.setCellWidget(row, col, actions_widget)
for child in actions_widget.findChildren(QToolButton): table.resizeColumnsToContents()
child.setFont(font) table.resizeRowsToContents()
table.setCellWidget(row, col, actions_widget) # Calculate fixed height
table.resizeColumnsToContents() height = table.horizontalHeader().height() + 2 # small margin
table.resizeRowsToContents() for i in range(table.rowCount()):
# Calculate fixed height height += table.rowHeight(i)
height = table.horizontalHeader().height() + 2 # small margin table.setFixedHeight(height)
for i in range(table.rowCount()): # Apply status colors
height += table.rowHeight(i) status_col = 3
table.setFixedHeight(height) for r in range(table.rowCount()):
# Apply status colors status = table.item(r, status_col).text()
status_col = 3 if status == 'unwatched':
for r in range(table.rowCount()): color = QColor(255, 255, 255)
status = table.item(r, status_col).text() elif status == 'watching':
if status == 'unwatched': color = QColor(255, 255, 0)
color = QColor(255, 255, 255) elif status == 'completed':
elif status == 'watching': color = QColor(0, 255, 0)
color = QColor(255, 255, 0) else:
elif status == 'completed': color = QColor(255, 255, 255)
color = QColor(0, 255, 0) for c in range(table.columnCount()):
else: if c == 0:
color = QColor(255, 255, 255) continue # skip hidden id
for c in range(table.columnCount()): item = table.item(r, c)
if c == 0: if item:
continue # skip hidden id item.setBackground(color)
item = table.item(r, c) widget = table.cellWidget(r, c)
if item: if widget:
item.setBackground(color) widget.setStyleSheet(f"background-color: {color.name()};")
widget = table.cellWidget(r, c) layout.addWidget(table)
if widget: total_entries += len(entries)
widget.setStyleSheet(f"background-color: {color.name()};") comp_entries += sum(1 for e in entries if e[4] == 'completed')
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 or (self.year_filter == year): if total_entries > 0:
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)