Files
eboek.info-scraper/gui/login_dialog.py
Louis Mylle ea4cab15c3 feat: Add installation scripts for Windows and Unix-based systems
- Created `install_and_run.bat` for Windows installation and setup.
- Created `install_and_run.sh` for Unix-based systems installation and setup.
- Removed `main.py` as it is no longer needed.
- Updated `requirements.txt` to specify package versions and added PyQt5.
- Deleted `start.bat` as it is redundant.
- Added unit tests for core functionality and scraping modes.
- Implemented input validation utilities in `utils/validators.py`.
- Added support for dual scraping modes in the scraper.
2026-01-10 14:45:00 +01:00

317 lines
10 KiB
Python

"""
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