- 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.
317 lines
10 KiB
Python
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 |