610 lines
23 KiB
Python
610 lines
23 KiB
Python
"""
|
|
Violations View - Violation management and history
|
|
Displays violation records, details, and management tools.
|
|
"""
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
|
|
QTableWidget, QTableWidgetItem, QHeaderView, QDateEdit,
|
|
QComboBox, QSpinBox, QLineEdit, QTextEdit, QDialog,
|
|
QDialogButtonBox, QSplitter, QListWidget, QListWidgetItem
|
|
)
|
|
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QDate, QSize
|
|
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont, QIcon
|
|
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
import os
|
|
|
|
# Import finale components
|
|
from ..styles import FinaleStyles, MaterialColors
|
|
from ..icons import FinaleIcons
|
|
from qt_app_pyside.utils.helpers import save_configuration, create_export_csv, create_export_json
|
|
from qt_app_pyside.utils.annotation_utils import draw_detections
|
|
from qt_app_pyside.utils.enhanced_annotation_utils import enhanced_draw_detections
|
|
from qt_app_pyside.ui.export_tab import ExportTab
|
|
from qt_app_pyside.ui.violations_tab import ViolationsTab as OriginalViolationsTab
|
|
|
|
class ViolationDetailDialog(QDialog):
|
|
"""
|
|
Dialog for viewing detailed violation information.
|
|
"""
|
|
|
|
def __init__(self, violation_data, parent=None):
|
|
super().__init__(parent)
|
|
self.violation_data = violation_data
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""Setup the detail dialog UI"""
|
|
self.setWindowTitle("Violation Details")
|
|
self.setMinimumSize(600, 500)
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Header with violation type and timestamp
|
|
header_frame = QFrame()
|
|
header_frame.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {MaterialColors.primary};
|
|
color: {MaterialColors.text_on_primary};
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}}
|
|
""")
|
|
|
|
header_layout = QHBoxLayout(header_frame)
|
|
|
|
violation_type = self.violation_data.get('type', 'Unknown')
|
|
timestamp = self.violation_data.get('timestamp', 'Unknown')
|
|
|
|
type_label = QLabel(violation_type)
|
|
type_label.setFont(QFont("Segoe UI", 16, QFont.Bold))
|
|
|
|
time_label = QLabel(timestamp)
|
|
time_label.setFont(QFont("Segoe UI", 12))
|
|
|
|
header_layout.addWidget(type_label)
|
|
header_layout.addStretch()
|
|
header_layout.addWidget(time_label)
|
|
|
|
layout.addWidget(header_frame)
|
|
|
|
# Main content area
|
|
content_splitter = QSplitter(Qt.Horizontal)
|
|
|
|
# Left side - Image/Video
|
|
image_group = QGroupBox("Evidence")
|
|
image_layout = QVBoxLayout(image_group)
|
|
|
|
self.image_label = QLabel()
|
|
self.image_label.setMinimumSize(300, 200)
|
|
self.image_label.setStyleSheet("""
|
|
QLabel {
|
|
border: 2px solid #424242;
|
|
border-radius: 8px;
|
|
background-color: #1a1a1a;
|
|
}
|
|
""")
|
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
self.image_label.setText("No image available")
|
|
|
|
# Load image if available
|
|
image_path = self.violation_data.get('image_path')
|
|
if image_path and os.path.exists(image_path):
|
|
pixmap = QPixmap(image_path)
|
|
if not pixmap.isNull():
|
|
scaled_pixmap = pixmap.scaled(300, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
self.image_label.setPixmap(scaled_pixmap)
|
|
|
|
image_layout.addWidget(self.image_label)
|
|
|
|
# Image controls
|
|
image_controls = QHBoxLayout()
|
|
|
|
save_image_btn = QPushButton(FinaleIcons.get_icon("save"), "Save Image")
|
|
view_full_btn = QPushButton(FinaleIcons.get_icon("zoom_in"), "View Full")
|
|
|
|
image_controls.addWidget(save_image_btn)
|
|
image_controls.addWidget(view_full_btn)
|
|
image_controls.addStretch()
|
|
|
|
image_layout.addLayout(image_controls)
|
|
|
|
content_splitter.addWidget(image_group)
|
|
|
|
# Right side - Details
|
|
details_group = QGroupBox("Details")
|
|
details_layout = QGridLayout(details_group)
|
|
|
|
# Violation details
|
|
details = [
|
|
("Vehicle ID:", self.violation_data.get('vehicle_id', 'Unknown')),
|
|
("Location:", self.violation_data.get('location', 'Unknown')),
|
|
("Confidence:", f"{self.violation_data.get('confidence', 0.0):.2f}"),
|
|
("Speed:", f"{self.violation_data.get('speed', 0.0):.1f} km/h"),
|
|
("Lane:", self.violation_data.get('lane', 'Unknown')),
|
|
("Weather:", self.violation_data.get('weather', 'Unknown')),
|
|
("Officer ID:", self.violation_data.get('officer_id', 'N/A')),
|
|
("Status:", self.violation_data.get('status', 'Pending'))
|
|
]
|
|
|
|
for i, (label, value) in enumerate(details):
|
|
label_widget = QLabel(label)
|
|
label_widget.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
|
|
|
value_widget = QLabel(str(value))
|
|
value_widget.setStyleSheet(f"color: {MaterialColors.text_secondary};")
|
|
|
|
details_layout.addWidget(label_widget, i, 0)
|
|
details_layout.addWidget(value_widget, i, 1)
|
|
|
|
# Notes section
|
|
notes_label = QLabel("Notes:")
|
|
notes_label.setFont(QFont("Segoe UI", 9, QFont.Bold))
|
|
details_layout.addWidget(notes_label, len(details), 0, 1, 2)
|
|
|
|
self.notes_edit = QTextEdit()
|
|
self.notes_edit.setMaximumHeight(100)
|
|
self.notes_edit.setPlainText(self.violation_data.get('notes', ''))
|
|
details_layout.addWidget(self.notes_edit, len(details) + 1, 0, 1, 2)
|
|
|
|
content_splitter.addWidget(details_group)
|
|
layout.addWidget(content_splitter)
|
|
|
|
# Action buttons
|
|
button_layout = QHBoxLayout()
|
|
|
|
export_btn = QPushButton(FinaleIcons.get_icon("export"), "Export Report")
|
|
delete_btn = QPushButton(FinaleIcons.get_icon("delete"), "Delete")
|
|
delete_btn.setStyleSheet(f"background-color: {MaterialColors.error};")
|
|
|
|
button_layout.addWidget(export_btn)
|
|
button_layout.addWidget(delete_btn)
|
|
button_layout.addStretch()
|
|
|
|
# Standard dialog buttons
|
|
button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
|
|
button_box.accepted.connect(self.save_changes)
|
|
button_box.rejected.connect(self.reject)
|
|
|
|
button_layout.addWidget(button_box)
|
|
layout.addLayout(button_layout)
|
|
|
|
# Apply styling
|
|
self.setStyleSheet(FinaleStyles.get_dialog_style())
|
|
|
|
@Slot()
|
|
def save_changes(self):
|
|
"""Save changes to violation data"""
|
|
# Update notes
|
|
self.violation_data['notes'] = self.notes_edit.toPlainText()
|
|
# Here you would save to database/file
|
|
self.accept()
|
|
|
|
class ViolationFilterWidget(QGroupBox):
|
|
"""
|
|
Widget for filtering violations by various criteria.
|
|
"""
|
|
|
|
filter_changed = Signal(dict)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__("Filter Violations", parent)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""Setup filter UI"""
|
|
layout = QGridLayout(self)
|
|
|
|
# Date range
|
|
layout.addWidget(QLabel("Date From:"), 0, 0)
|
|
self.date_from = QDateEdit()
|
|
self.date_from.setDate(QDate.currentDate().addDays(-30))
|
|
self.date_from.setCalendarPopup(True)
|
|
layout.addWidget(self.date_from, 0, 1)
|
|
|
|
layout.addWidget(QLabel("Date To:"), 0, 2)
|
|
self.date_to = QDateEdit()
|
|
self.date_to.setDate(QDate.currentDate())
|
|
self.date_to.setCalendarPopup(True)
|
|
layout.addWidget(self.date_to, 0, 3)
|
|
|
|
# Violation type
|
|
layout.addWidget(QLabel("Type:"), 1, 0)
|
|
self.type_combo = QComboBox()
|
|
self.type_combo.addItems(["All Types", "Red Light", "Speed", "Wrong Lane", "No Helmet", "Other"])
|
|
layout.addWidget(self.type_combo, 1, 1)
|
|
|
|
# Status
|
|
layout.addWidget(QLabel("Status:"), 1, 2)
|
|
self.status_combo = QComboBox()
|
|
self.status_combo.addItems(["All Status", "Pending", "Reviewed", "Closed", "Disputed"])
|
|
layout.addWidget(self.status_combo, 1, 3)
|
|
|
|
# Location
|
|
layout.addWidget(QLabel("Location:"), 2, 0)
|
|
self.location_edit = QLineEdit()
|
|
self.location_edit.setPlaceholderText("Enter location...")
|
|
layout.addWidget(self.location_edit, 2, 1)
|
|
|
|
# Confidence threshold
|
|
layout.addWidget(QLabel("Min Confidence:"), 2, 2)
|
|
self.confidence_spin = QSpinBox()
|
|
self.confidence_spin.setRange(0, 100)
|
|
self.confidence_spin.setValue(50)
|
|
self.confidence_spin.setSuffix("%")
|
|
layout.addWidget(self.confidence_spin, 2, 3)
|
|
|
|
# Apply button
|
|
self.apply_btn = QPushButton(FinaleIcons.get_icon("filter"), "Apply Filter")
|
|
self.apply_btn.clicked.connect(self.apply_filter)
|
|
layout.addWidget(self.apply_btn, 3, 0, 1, 4)
|
|
|
|
# Connect signals for auto-update
|
|
self.date_from.dateChanged.connect(self.on_filter_changed)
|
|
self.date_to.dateChanged.connect(self.on_filter_changed)
|
|
self.type_combo.currentTextChanged.connect(self.on_filter_changed)
|
|
self.status_combo.currentTextChanged.connect(self.on_filter_changed)
|
|
|
|
# Apply styling
|
|
self.setStyleSheet(FinaleStyles.get_group_box_style())
|
|
|
|
@Slot()
|
|
def apply_filter(self):
|
|
"""Apply current filter settings"""
|
|
self.on_filter_changed()
|
|
|
|
def on_filter_changed(self):
|
|
"""Emit filter changed signal with current settings"""
|
|
filter_data = {
|
|
'date_from': self.date_from.date().toPython(),
|
|
'date_to': self.date_to.date().toPython(),
|
|
'type': self.type_combo.currentText(),
|
|
'status': self.status_combo.currentText(),
|
|
'location': self.location_edit.text(),
|
|
'min_confidence': self.confidence_spin.value() / 100.0
|
|
}
|
|
self.filter_changed.emit(filter_data)
|
|
|
|
class ViolationListWidget(QWidget):
|
|
"""
|
|
Widget displaying violation list with thumbnails and quick info.
|
|
"""
|
|
|
|
violation_selected = Signal(dict)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.violations = []
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""Setup violation list UI"""
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Header
|
|
header_layout = QHBoxLayout()
|
|
|
|
self.count_label = QLabel("0 violations")
|
|
self.count_label.setFont(QFont("Segoe UI", 12, QFont.Bold))
|
|
|
|
self.sort_combo = QComboBox()
|
|
self.sort_combo.addItems(["Sort by Time", "Sort by Type", "Sort by Confidence", "Sort by Status"])
|
|
self.sort_combo.currentTextChanged.connect(self.sort_violations)
|
|
|
|
header_layout.addWidget(self.count_label)
|
|
header_layout.addStretch()
|
|
header_layout.addWidget(QLabel("Sort:"))
|
|
header_layout.addWidget(self.sort_combo)
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
# Violations list
|
|
self.list_widget = QListWidget()
|
|
self.list_widget.itemClicked.connect(self.on_item_clicked)
|
|
self.list_widget.setStyleSheet(FinaleStyles.get_list_style())
|
|
|
|
layout.addWidget(self.list_widget)
|
|
|
|
def add_violation(self, violation_data):
|
|
"""Add a violation to the list"""
|
|
self.violations.append(violation_data)
|
|
self.update_list()
|
|
|
|
def set_violations(self, violations):
|
|
"""Set the complete list of violations"""
|
|
self.violations = violations
|
|
self.update_list()
|
|
|
|
def update_list(self):
|
|
"""Update the violation list display"""
|
|
self.list_widget.clear()
|
|
|
|
for violation in self.violations:
|
|
item = QListWidgetItem()
|
|
|
|
# Create custom widget for violation item
|
|
item_widget = self.create_violation_item_widget(violation)
|
|
|
|
item.setSizeHint(item_widget.sizeHint())
|
|
self.list_widget.addItem(item)
|
|
self.list_widget.setItemWidget(item, item_widget)
|
|
|
|
# Update count
|
|
self.count_label.setText(f"{len(self.violations)} violations")
|
|
|
|
def create_violation_item_widget(self, violation):
|
|
"""Create a custom widget for a violation list item"""
|
|
widget = QWidget()
|
|
layout = QHBoxLayout(widget)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
# Thumbnail (placeholder for now)
|
|
thumbnail = QLabel()
|
|
thumbnail.setFixedSize(80, 60)
|
|
thumbnail.setStyleSheet("""
|
|
QLabel {
|
|
border: 1px solid #424242;
|
|
border-radius: 4px;
|
|
background-color: #2d2d2d;
|
|
}
|
|
""")
|
|
thumbnail.setAlignment(Qt.AlignCenter)
|
|
thumbnail.setText("IMG")
|
|
layout.addWidget(thumbnail)
|
|
|
|
# Violation info
|
|
info_layout = QVBoxLayout()
|
|
|
|
# Title line
|
|
title_layout = QHBoxLayout()
|
|
|
|
type_label = QLabel(violation.get('type', 'Unknown'))
|
|
type_label.setFont(QFont("Segoe UI", 11, QFont.Bold))
|
|
|
|
time_label = QLabel(violation.get('timestamp', ''))
|
|
time_label.setStyleSheet(f"color: {MaterialColors.text_secondary}; font-size: 10px;")
|
|
|
|
title_layout.addWidget(type_label)
|
|
title_layout.addStretch()
|
|
title_layout.addWidget(time_label)
|
|
|
|
info_layout.addLayout(title_layout)
|
|
|
|
# Details line
|
|
details = f"Vehicle: {violation.get('vehicle_id', 'Unknown')} | Location: {violation.get('location', 'Unknown')}"
|
|
details_label = QLabel(details)
|
|
details_label.setStyleSheet(f"color: {MaterialColors.text_secondary}; font-size: 9px;")
|
|
info_layout.addWidget(details_label)
|
|
|
|
# Confidence and status
|
|
status_layout = QHBoxLayout()
|
|
|
|
confidence = violation.get('confidence', 0.0)
|
|
confidence_label = QLabel(f"Confidence: {confidence:.2f}")
|
|
confidence_label.setStyleSheet(f"color: {MaterialColors.primary}; font-size: 9px;")
|
|
|
|
status = violation.get('status', 'Pending')
|
|
status_label = QLabel(status)
|
|
status_color = {
|
|
'Pending': MaterialColors.warning,
|
|
'Reviewed': MaterialColors.primary,
|
|
'Closed': MaterialColors.success,
|
|
'Disputed': MaterialColors.error
|
|
}.get(status, MaterialColors.text_secondary)
|
|
status_label.setStyleSheet(f"color: {status_color}; font-size: 9px; font-weight: bold;")
|
|
|
|
status_layout.addWidget(confidence_label)
|
|
status_layout.addStretch()
|
|
status_layout.addWidget(status_label)
|
|
|
|
info_layout.addLayout(status_layout)
|
|
layout.addLayout(info_layout, 1)
|
|
|
|
# Store violation data in widget
|
|
widget.violation_data = violation
|
|
|
|
return widget
|
|
|
|
def sort_violations(self, sort_by):
|
|
"""Sort violations by the specified criteria"""
|
|
if sort_by == "Sort by Time":
|
|
self.violations.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
|
elif sort_by == "Sort by Type":
|
|
self.violations.sort(key=lambda x: x.get('type', ''))
|
|
elif sort_by == "Sort by Confidence":
|
|
self.violations.sort(key=lambda x: x.get('confidence', 0.0), reverse=True)
|
|
elif sort_by == "Sort by Status":
|
|
self.violations.sort(key=lambda x: x.get('status', ''))
|
|
|
|
self.update_list()
|
|
|
|
@Slot(QListWidgetItem)
|
|
def on_item_clicked(self, item):
|
|
"""Handle violation item click"""
|
|
item_widget = self.list_widget.itemWidget(item)
|
|
if hasattr(item_widget, 'violation_data'):
|
|
self.violation_selected.emit(item_widget.violation_data)
|
|
|
|
class ViolationsView(QWidget):
|
|
"""
|
|
Main violations view with filtering, list, and detail management.
|
|
"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setup_ui()
|
|
self.load_sample_data()
|
|
|
|
self.save_config = save_configuration
|
|
self.export_csv = create_export_csv
|
|
self.export_json = create_export_json
|
|
self.draw_detections = draw_detections
|
|
self.enhanced_draw_detections = enhanced_draw_detections
|
|
# Add export functionality from original export_tab
|
|
self.export_handler = ExportTab()
|
|
|
|
def setup_ui(self):
|
|
"""Setup the violations view UI"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(16)
|
|
|
|
# Filter widget
|
|
self.filter_widget = ViolationFilterWidget()
|
|
self.filter_widget.filter_changed.connect(self.apply_filter)
|
|
layout.addWidget(self.filter_widget)
|
|
|
|
# Main content area
|
|
content_splitter = QSplitter(Qt.Horizontal)
|
|
|
|
# Left side - Violation list
|
|
self.violation_list = ViolationListWidget()
|
|
self.violation_list.violation_selected.connect(self.show_violation_details)
|
|
content_splitter.addWidget(self.violation_list)
|
|
|
|
# Right side - Quick actions and summary
|
|
right_panel = QWidget()
|
|
right_layout = QVBoxLayout(right_panel)
|
|
|
|
# Quick actions
|
|
actions_group = QGroupBox("Quick Actions")
|
|
actions_layout = QVBoxLayout(actions_group)
|
|
|
|
export_all_btn = QPushButton(FinaleIcons.get_icon("export"), "Export All")
|
|
export_filtered_btn = QPushButton(FinaleIcons.get_icon("filter"), "Export Filtered")
|
|
delete_selected_btn = QPushButton(FinaleIcons.get_icon("delete"), "Delete Selected")
|
|
mark_reviewed_btn = QPushButton(FinaleIcons.get_icon("check"), "Mark as Reviewed")
|
|
|
|
actions_layout.addWidget(export_all_btn)
|
|
actions_layout.addWidget(export_filtered_btn)
|
|
actions_layout.addWidget(delete_selected_btn)
|
|
actions_layout.addWidget(mark_reviewed_btn)
|
|
|
|
actions_group.setStyleSheet(FinaleStyles.get_group_box_style())
|
|
right_layout.addWidget(actions_group)
|
|
|
|
# Summary statistics
|
|
summary_group = QGroupBox("Summary")
|
|
summary_layout = QGridLayout(summary_group)
|
|
|
|
self.total_label = QLabel("Total: 0")
|
|
self.pending_label = QLabel("Pending: 0")
|
|
self.reviewed_label = QLabel("Reviewed: 0")
|
|
self.closed_label = QLabel("Closed: 0")
|
|
|
|
summary_layout.addWidget(self.total_label, 0, 0)
|
|
summary_layout.addWidget(self.pending_label, 0, 1)
|
|
summary_layout.addWidget(self.reviewed_label, 1, 0)
|
|
summary_layout.addWidget(self.closed_label, 1, 1)
|
|
|
|
summary_group.setStyleSheet(FinaleStyles.get_group_box_style())
|
|
right_layout.addWidget(summary_group)
|
|
|
|
right_layout.addStretch()
|
|
content_splitter.addWidget(right_panel)
|
|
|
|
# Set splitter proportions
|
|
content_splitter.setSizes([700, 300])
|
|
|
|
layout.addWidget(content_splitter, 1)
|
|
|
|
# Apply theme
|
|
self.apply_theme(True)
|
|
|
|
def load_sample_data(self):
|
|
"""Load sample violation data for demonstration"""
|
|
sample_violations = [
|
|
{
|
|
'timestamp': '14:23:15',
|
|
'type': 'Red Light',
|
|
'vehicle_id': 'VH1234',
|
|
'location': 'Main St & 1st Ave',
|
|
'confidence': 0.92,
|
|
'status': 'Pending',
|
|
'speed': 45.2,
|
|
'lane': 'Left Turn',
|
|
'notes': 'Clear violation captured on camera.'
|
|
},
|
|
{
|
|
'timestamp': '13:45:32',
|
|
'type': 'Speed',
|
|
'vehicle_id': 'VH5678',
|
|
'location': 'Highway 101',
|
|
'confidence': 0.87,
|
|
'status': 'Reviewed',
|
|
'speed': 78.5,
|
|
'lane': 'Right',
|
|
'notes': 'Speed limit 60 km/h, vehicle traveling at 78.5 km/h.'
|
|
},
|
|
{
|
|
'timestamp': '12:15:48',
|
|
'type': 'Wrong Lane',
|
|
'vehicle_id': 'VH9012',
|
|
'location': 'Oak St Bridge',
|
|
'confidence': 0.76,
|
|
'status': 'Closed',
|
|
'speed': 32.1,
|
|
'lane': 'Bus Lane',
|
|
'notes': 'Vehicle in bus-only lane during restricted hours.'
|
|
}
|
|
]
|
|
|
|
self.violation_list.set_violations(sample_violations)
|
|
self.update_summary()
|
|
|
|
def add_violation(self, violation_data):
|
|
"""Add a new violation (called from main window)"""
|
|
self.violation_list.add_violation(violation_data)
|
|
self.update_summary()
|
|
|
|
@Slot(dict)
|
|
def apply_filter(self, filter_data):
|
|
"""Apply filter to violation list"""
|
|
print(f"Applying filter: {filter_data}")
|
|
# Here you would filter the violations based on criteria
|
|
# For now, just update summary
|
|
self.update_summary()
|
|
|
|
@Slot(dict)
|
|
def show_violation_details(self, violation_data):
|
|
"""Show detailed view of selected violation"""
|
|
dialog = ViolationDetailDialog(violation_data, self)
|
|
dialog.exec()
|
|
|
|
def update_summary(self):
|
|
"""Update summary statistics"""
|
|
violations = self.violation_list.violations
|
|
|
|
total = len(violations)
|
|
pending = len([v for v in violations if v.get('status') == 'Pending'])
|
|
reviewed = len([v for v in violations if v.get('status') == 'Reviewed'])
|
|
closed = len([v for v in violations if v.get('status') == 'Closed'])
|
|
|
|
self.total_label.setText(f"Total: {total}")
|
|
self.pending_label.setText(f"Pending: {pending}")
|
|
self.reviewed_label.setText(f"Reviewed: {reviewed}")
|
|
self.closed_label.setText(f"Closed: {closed}")
|
|
|
|
def apply_theme(self, dark_mode=True):
|
|
"""Apply theme to the view"""
|
|
if dark_mode:
|
|
self.setStyleSheet(f"""
|
|
QWidget {{
|
|
background-color: {MaterialColors.surface};
|
|
color: {MaterialColors.text_primary};
|
|
}}
|
|
QPushButton {{
|
|
background-color: {MaterialColors.primary};
|
|
color: {MaterialColors.text_on_primary};
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 16px;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: {MaterialColors.primary_variant};
|
|
}}
|
|
""")
|