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.
This commit is contained in:
317
gui/login_dialog.py
Normal file
317
gui/login_dialog.py
Normal file
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user