""" Login dialog for EBoek.info credential input. """ from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLabel, QLineEdit, QCheckBox, QMessageBox, QProgressBar ) from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal from PyQt5.QtGui import QFont from pathlib import Path import sys # Add the project root directory to Python path project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) from utils.validators import validate_username, validate_password, format_error_message class LoginTestThread(QThread): """Thread for testing login credentials without blocking the UI.""" login_result = pyqtSignal(bool, str) # success, message def __init__(self, username, password): super().__init__() self.username = username self.password = password def run(self): """Test the login credentials.""" try: # Import here to avoid circular imports and ensure GUI responsiveness from core.scraper import Scraper # Create a scraper instance for testing scraper = Scraper(headless=True) # Attempt login success = scraper.login(self.username, self.password) # Clean up scraper.close() if success: self.login_result.emit(True, "Login successful!") else: self.login_result.emit(False, "Login failed. Please check your credentials.") except Exception as e: self.login_result.emit(False, f"Error testing login: {str(e)}") class LoginDialog(QDialog): """ Dialog for entering EBoek.info login credentials. Provides fields for username and password input, with options to save credentials and test them before saving. """ def __init__(self, parent=None, credential_manager=None): super().__init__(parent) self.credential_manager = credential_manager self.test_thread = None self.init_ui() self.load_existing_credentials() def init_ui(self): """Initialize the user interface.""" self.setWindowTitle("EBoek.info Login") self.setModal(True) self.setFixedSize(400, 300) layout = QVBoxLayout(self) # Title title_label = QLabel("EBoek.info Credentials") title_font = QFont() title_font.setPointSize(14) title_font.setBold(True) title_label.setFont(title_font) title_label.setAlignment(Qt.AlignCenter) layout.addWidget(title_label) layout.addSpacing(10) # Credentials form form_layout = QGridLayout() form_layout.addWidget(QLabel("Username:"), 0, 0) self.username_input = QLineEdit() self.username_input.setPlaceholderText("Enter your EBoek.info username") form_layout.addWidget(self.username_input, 0, 1) form_layout.addWidget(QLabel("Password:"), 1, 0) self.password_input = QLineEdit() self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setPlaceholderText("Enter your password") form_layout.addWidget(self.password_input, 1, 1) layout.addLayout(form_layout) layout.addSpacing(10) # Options self.remember_checkbox = QCheckBox("Save credentials for future use") self.remember_checkbox.setChecked(True) layout.addWidget(self.remember_checkbox) layout.addSpacing(5) # Info text info_label = QLabel( "Note: Credentials are stored securely on your computer " "for convenience. You can clear them anytime from the Settings menu." ) info_label.setWordWrap(True) info_label.setStyleSheet("color: #666; font-size: 10px;") layout.addWidget(info_label) layout.addSpacing(15) # Test progress (hidden initially) self.test_progress = QProgressBar() self.test_progress.setVisible(False) layout.addWidget(self.test_progress) self.test_status_label = QLabel("") self.test_status_label.setVisible(False) layout.addWidget(self.test_status_label) # Buttons button_layout = QHBoxLayout() self.test_btn = QPushButton("Test Login") self.test_btn.clicked.connect(self.test_login) button_layout.addWidget(self.test_btn) button_layout.addStretch() self.ok_btn = QPushButton("OK") self.ok_btn.clicked.connect(self.accept_credentials) self.ok_btn.setDefault(True) button_layout.addWidget(self.ok_btn) self.cancel_btn = QPushButton("Cancel") self.cancel_btn.clicked.connect(self.reject) button_layout.addWidget(self.cancel_btn) layout.addLayout(button_layout) # Connect Enter key to OK button self.username_input.returnPressed.connect(self.password_input.setFocus) self.password_input.returnPressed.connect(self.accept_credentials) def load_existing_credentials(self): """Load existing credentials if available.""" if self.credential_manager: username = self.credential_manager.get_saved_username() if username: self.username_input.setText(username) # Focus password field if username is pre-filled self.password_input.setFocus() else: self.username_input.setFocus() def validate_input(self): """ Validate the entered credentials. Returns: tuple: (is_valid, errors_list) """ username = self.username_input.text().strip() password = self.password_input.text() username_validation = validate_username(username) password_validation = validate_password(password) all_errors = [] all_errors.extend(username_validation.get('errors', [])) all_errors.extend(password_validation.get('errors', [])) return len(all_errors) == 0, all_errors def test_login(self): """Test the login credentials.""" # First validate input is_valid, errors = self.validate_input() if not is_valid: QMessageBox.warning(self, "Invalid Input", format_error_message(errors)) return # Disable UI elements during test self.test_btn.setEnabled(False) self.ok_btn.setEnabled(False) self.username_input.setEnabled(False) self.password_input.setEnabled(False) # Show progress self.test_progress.setVisible(True) self.test_progress.setRange(0, 0) # Indeterminate progress self.test_status_label.setText("Testing login credentials...") self.test_status_label.setVisible(True) # Start test thread username = self.username_input.text().strip() password = self.password_input.text() self.test_thread = LoginTestThread(username, password) self.test_thread.login_result.connect(self.on_test_completed) self.test_thread.start() def on_test_completed(self, success, message): """Handle test completion.""" # Re-enable UI elements self.test_btn.setEnabled(True) self.ok_btn.setEnabled(True) self.username_input.setEnabled(True) self.password_input.setEnabled(True) # Hide progress self.test_progress.setVisible(False) # Show result if success: self.test_status_label.setText("✓ " + message) self.test_status_label.setStyleSheet("color: #2E8B57; font-weight: bold;") else: self.test_status_label.setText("✗ " + message) self.test_status_label.setStyleSheet("color: #f44336; font-weight: bold;") # Auto-hide status after 5 seconds QTimer.singleShot(5000, lambda: self.test_status_label.setVisible(False)) # Clean up thread self.test_thread = None def accept_credentials(self): """Accept and save the credentials.""" # Validate input is_valid, errors = self.validate_input() if not is_valid: QMessageBox.warning(self, "Invalid Input", format_error_message(errors)) return username = self.username_input.text().strip() password = self.password_input.text() remember = self.remember_checkbox.isChecked() # Save credentials if manager is available if self.credential_manager: if remember: success = self.credential_manager.save_credentials(username, password, remember=True) if not success: QMessageBox.warning( self, "Save Error", "Could not save credentials. They will be used for this session only." ) else: # Clear any existing saved credentials if user unchecked remember self.credential_manager.clear_credentials() # Accept the dialog self.accept() def get_credentials(self): """ Get the entered credentials. Returns: dict: Dictionary with 'username', 'password', and 'remember' keys """ return { 'username': self.username_input.text().strip(), 'password': self.password_input.text(), 'remember': self.remember_checkbox.isChecked() } def closeEvent(self, event): """Handle dialog close event.""" # Make sure test thread is stopped if self.test_thread and self.test_thread.isRunning(): self.test_thread.quit() self.test_thread.wait(1000) # Wait up to 1 second event.accept() def reject(self): """Handle dialog rejection (Cancel button).""" # Stop test thread if running if self.test_thread and self.test_thread.isRunning(): self.test_thread.quit() self.test_thread.wait(1000) super().reject() def show_login_dialog(parent=None, credential_manager=None): """ Convenience function to show login dialog and get credentials. Args: parent: Parent widget credential_manager: CredentialManager instance Returns: dict or None: Credentials if dialog accepted, None if cancelled """ dialog = LoginDialog(parent, credential_manager) if dialog.exec_() == QDialog.Accepted: return dialog.get_credentials() return None