""" Main application window for the EBoek.info scraper GUI. """ import sys from pathlib import Path from PyQt5.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLabel, QSpinBox, QDoubleSpinBox, QTextEdit, QGroupBox, QCheckBox, QProgressBar, QMessageBox, QFileDialog, QMenuBar, QMenu, QAction, QComboBox, QFrame, QScrollArea, QSizePolicy ) from PyQt5.QtCore import Qt, QTimer, pyqtSignal from PyQt5.QtGui import QFont, QIcon # Import our custom modules import os import sys # Add the project root directory to Python path so we can import our modules project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) from core.credentials import CredentialManager from core.scraper_thread import ScraperThread from utils.validators import validate_page_range, format_error_message from gui.login_dialog import LoginDialog from gui.progress_dialog import ProgressDialog class MainWindow(QMainWindow): """ Main application window for the EBoek.info scraper. This window provides the primary interface for: - Managing credentials - Setting scraping parameters - Starting/stopping scraping operations - Monitoring progress and logs """ # Custom signals scraping_requested = pyqtSignal(str, str, int, int, bool) # username, password, start_page, end_page, headless def __init__(self): super().__init__() self.credential_manager = CredentialManager() self.scraper_thread = None self.progress_dialog = None # Load application settings self.app_settings = self.credential_manager.load_app_settings() if not self.app_settings: self.app_settings = self.credential_manager.get_default_settings() self.init_ui() self.apply_light_theme() self.update_credential_status() def init_ui(self): """Initialize the user interface.""" self.setWindowTitle("EBoek.info Scraper") self.setMinimumSize(900, 650) self.resize(1200, 750) # Create menu bar self.create_menu_bar() # Create central widget with scroll area for better responsiveness scroll_area = QScrollArea() scroll_widget = QWidget() scroll_area.setWidget(scroll_widget) scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setCentralWidget(scroll_area) # Main layout with better spacing layout = QVBoxLayout(scroll_widget) layout.setSpacing(20) layout.setContentsMargins(20, 20, 20, 20) # Create sections in a more organized way self.create_credential_section(layout) self.create_scraping_section(layout) self.create_timing_section(layout) self.create_status_section(layout) self.create_control_section(layout) # Add stretch to push content to top layout.addStretch() # Status bar self.statusBar().showMessage("Ready") def apply_light_theme(self): """Apply minimal light theme styling with completely default system controls.""" # Very minimal stylesheet - only style the main containers, no form controls light_stylesheet = """ QMainWindow { background-color: #ffffff; } """ self.setStyleSheet(light_stylesheet) def create_menu_bar(self): """Create the menu bar.""" menubar = self.menuBar() # File menu file_menu = menubar.addMenu('File') export_action = QAction('Export Settings', self) export_action.triggered.connect(self.export_settings) file_menu.addAction(export_action) import_action = QAction('Import Settings', self) import_action.triggered.connect(self.import_settings) file_menu.addAction(import_action) file_menu.addSeparator() exit_action = QAction('Exit', self) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Settings menu settings_menu = menubar.addMenu('Settings') clear_creds_action = QAction('Clear Saved Credentials', self) clear_creds_action.triggered.connect(self.clear_credentials) settings_menu.addAction(clear_creds_action) # Help menu help_menu = menubar.addMenu('Help') about_action = QAction('About', self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def create_credential_section(self, parent_layout): """Create the credential management section.""" group = QGroupBox("Account Credentials") layout = QHBoxLayout(group) # Status with icon status_layout = QHBoxLayout() self.credential_status_label = QLabel("No credentials configured") self.credential_status_label.setStyleSheet("font-size: 13px;") status_layout.addWidget(self.credential_status_label) layout.addLayout(status_layout) layout.addStretch() # Button with default system styling self.change_credentials_btn = QPushButton("Change Credentials") self.change_credentials_btn.clicked.connect(self.show_login_dialog) layout.addWidget(self.change_credentials_btn) parent_layout.addWidget(group) def create_scraping_section(self, parent_layout): """Create the scraping configuration section.""" group = QGroupBox("Scraping Configuration") main_layout = QVBoxLayout(group) # Create horizontal layout for better space usage top_section = QHBoxLayout() # Left side - Mode selection mode_group = QGroupBox("Scraping Mode") mode_layout = QVBoxLayout(mode_group) self.mode_combo = QComboBox() self.mode_combo.addItems([ "All Comics (Complete Archive)", "Latest Comics (Recent Additions)" ]) self.mode_combo.setCurrentIndex(self.app_settings.get('scraping_mode', 0)) self.mode_combo.currentIndexChanged.connect(self.on_mode_changed) mode_layout.addWidget(self.mode_combo) # Mode description label self.mode_description_label = QLabel("") self.mode_description_label.setWordWrap(True) mode_layout.addWidget(self.mode_description_label) mode_group.setMaximumWidth(400) # Right side - Page range and options config_group = QGroupBox("Configuration") config_layout = QGridLayout(config_group) # Page range config_layout.addWidget(QLabel("Start Page:"), 0, 0) self.start_page_spin = QSpinBox() self.start_page_spin.setMinimum(1) self.start_page_spin.setMaximum(9999) self.start_page_spin.setValue(self.app_settings.get('default_start_page', 1)) config_layout.addWidget(self.start_page_spin, 0, 1) config_layout.addWidget(QLabel("End Page:"), 0, 2) self.end_page_spin = QSpinBox() self.end_page_spin.setMinimum(1) self.end_page_spin.setMaximum(9999) self.end_page_spin.setValue(self.app_settings.get('default_end_page', 1)) config_layout.addWidget(self.end_page_spin, 0, 3) # Options self.headless_checkbox = QCheckBox("Headless Mode") self.headless_checkbox.setChecked(self.app_settings.get('headless_mode', True)) config_layout.addWidget(self.headless_checkbox, 1, 0, 1, 2) self.verbose_checkbox = QCheckBox("Verbose Logging") self.verbose_checkbox.setChecked(self.app_settings.get('verbose_logging', False)) config_layout.addWidget(self.verbose_checkbox, 1, 2, 1, 2) # Help texts page_help = QLabel("πŸ’‘ Start with 1-3 pages to test") config_layout.addWidget(page_help, 2, 0, 1, 4) browser_help = QLabel("πŸ–₯️ Headless = background mode (recommended) β€’ Verbose = detailed logs") config_layout.addWidget(browser_help, 3, 0, 1, 4) # Add to horizontal layout top_section.addWidget(mode_group) top_section.addWidget(config_group) main_layout.addLayout(top_section) # Update mode description self.update_mode_description() parent_layout.addWidget(group) def on_mode_changed(self): """Handle scraping mode selection change.""" self.update_mode_description() self.save_current_settings() def update_mode_description(self): """Update the mode description text based on current selection.""" mode_index = self.mode_combo.currentIndex() if mode_index == 0: # All Comics description = ("πŸ—ƒοΈ All Comics (Complete Archive)
" "Scrapes from 'stripverhalen-alle' page containing the full comic archive. " "Best for systematic collection of all available comics. " "Large page counts available (1000+ pages).") elif mode_index == 1: # Latest Comics description = ("πŸ†• Latest Comics (Recent Additions)
" "Scrapes from 'laatste' page with most recently added comics. " "Perfect for staying up-to-date with new releases. " "Smaller page counts but fresh content.") else: description = "" self.mode_description_label.setText(description) def create_timing_section(self, parent_layout): """Create the timing configuration section.""" group = QGroupBox("Timing Configuration") main_layout = QVBoxLayout(group) # Create horizontal layout for better space usage top_layout = QHBoxLayout() # Left side - Quick presets preset_group = QGroupBox("Quick Presets") preset_layout = QVBoxLayout(preset_group) preset_info = QLabel("Choose a predefined timing profile:") preset_layout.addWidget(preset_info) preset_buttons_layout = QVBoxLayout() fast_btn = QPushButton("Fast Mode") fast_btn.clicked.connect(lambda: self.apply_timing_preset("fast")) preset_buttons_layout.addWidget(fast_btn) fast_desc = QLabel("Minimal delays β€’ Faster scraping β€’ Higher detection risk") fast_desc.setWordWrap(True) preset_buttons_layout.addWidget(fast_desc) balanced_btn = QPushButton("Balanced Mode (Recommended)") balanced_btn.clicked.connect(lambda: self.apply_timing_preset("balanced")) preset_buttons_layout.addWidget(balanced_btn) balanced_desc = QLabel("Default settings β€’ Good balance β€’ Recommended for most users") balanced_desc.setWordWrap(True) preset_buttons_layout.addWidget(balanced_desc) stealth_btn = QPushButton("Stealth Mode") stealth_btn.clicked.connect(lambda: self.apply_timing_preset("stealth")) preset_buttons_layout.addWidget(stealth_btn) stealth_desc = QLabel("Maximum delays β€’ Very human-like β€’ Slower but undetectable") stealth_desc.setWordWrap(True) preset_buttons_layout.addWidget(stealth_desc) preset_layout.addLayout(preset_buttons_layout) preset_layout.addStretch() preset_group.setMinimumWidth(350) # Changed from setMaximumWidth to setMinimumWidth # Right side - Manual controls in a more compact layout manual_group = QGroupBox("Manual Configuration") manual_layout = QGridLayout(manual_group) # Action delays manual_layout.addWidget(QLabel("Action Delays (sec):"), 0, 0) self.action_delay_min_spin = self.create_double_spinbox(0.1, 10.0, 0.1, self.app_settings.get('action_delay_min', 0.5)) manual_layout.addWidget(self.action_delay_min_spin, 0, 1) manual_layout.addWidget(QLabel("to"), 0, 2) self.action_delay_max_spin = self.create_double_spinbox(0.1, 10.0, 0.1, self.app_settings.get('action_delay_max', 2.0)) manual_layout.addWidget(self.action_delay_max_spin, 0, 3) # Page breaks manual_layout.addWidget(QLabel("Page Break Chance:"), 1, 0) self.page_break_chance_spin = QSpinBox() self.page_break_chance_spin.setRange(0, 100) self.page_break_chance_spin.setValue(self.app_settings.get('page_break_chance', 70)) self.page_break_chance_spin.setSuffix("%") manual_layout.addWidget(self.page_break_chance_spin, 1, 1) manual_layout.addWidget(QLabel("Duration:"), 1, 2) page_duration_layout = QHBoxLayout() self.page_break_min_spin = QSpinBox() self.page_break_min_spin.setRange(5, 300) self.page_break_min_spin.setValue(self.app_settings.get('page_break_min', 15)) page_duration_layout.addWidget(self.page_break_min_spin) page_duration_layout.addWidget(QLabel("-")) self.page_break_max_spin = QSpinBox() self.page_break_max_spin.setRange(5, 300) self.page_break_max_spin.setValue(self.app_settings.get('page_break_max', 45)) self.page_break_max_spin.setSuffix("s") page_duration_layout.addWidget(self.page_break_max_spin) manual_layout.addLayout(page_duration_layout, 1, 3) # Batch breaks manual_layout.addWidget(QLabel("Batch Break Every:"), 2, 0) self.batch_break_interval_spin = QSpinBox() self.batch_break_interval_spin.setRange(1, 50) self.batch_break_interval_spin.setValue(self.app_settings.get('batch_break_interval', 5)) self.batch_break_interval_spin.setSuffix(" comics") manual_layout.addWidget(self.batch_break_interval_spin, 2, 1) manual_layout.addWidget(QLabel("Duration:"), 2, 2) batch_duration_layout = QHBoxLayout() self.batch_break_min_spin = QSpinBox() self.batch_break_min_spin.setRange(1, 60) self.batch_break_min_spin.setValue(self.app_settings.get('batch_break_min', 3)) batch_duration_layout.addWidget(self.batch_break_min_spin) batch_duration_layout.addWidget(QLabel("-")) self.batch_break_max_spin = QSpinBox() self.batch_break_max_spin.setRange(1, 60) self.batch_break_max_spin.setValue(self.app_settings.get('batch_break_max', 7)) self.batch_break_max_spin.setSuffix("s") batch_duration_layout.addWidget(self.batch_break_max_spin) manual_layout.addLayout(batch_duration_layout, 2, 3) # Typing speed manual_layout.addWidget(QLabel("Typing Speed:"), 3, 0) self.typing_delay_spin = self.create_double_spinbox(0.01, 1.0, 0.01, self.app_settings.get('typing_delay', 0.1)) self.typing_delay_spin.setSuffix(" sec/char") manual_layout.addWidget(self.typing_delay_spin, 3, 1) # Bottom row with reset and help buttons button_layout = QHBoxLayout() self.reset_timing_btn = QPushButton("Reset to Balanced") self.reset_timing_btn.clicked.connect(self.reset_timing_defaults) button_layout.addWidget(self.reset_timing_btn) help_btn = QPushButton("Help") help_btn.clicked.connect(self.show_timing_help_dialog) button_layout.addWidget(help_btn) button_layout.addStretch() manual_layout.addLayout(button_layout, 4, 0, 1, 4) # Add to horizontal layout with proper proportions top_layout.addWidget(preset_group, 1) # Give preset group more space top_layout.addWidget(manual_group, 1) # Equal space for manual group main_layout.addLayout(top_layout) # Set the timing section to expand properly group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) parent_layout.addWidget(group) def create_double_spinbox(self, min_val, max_val, step, value): """Create a double precision spinbox with specified parameters.""" spinbox = QDoubleSpinBox() spinbox.setRange(min_val, max_val) spinbox.setSingleStep(step) spinbox.setDecimals(1) spinbox.setValue(value) return spinbox def show_timing_help_dialog(self): """Show a compact help dialog for timing configuration.""" dialog = QMessageBox(self) dialog.setWindowTitle("Timing Configuration Help") dialog.setIcon(QMessageBox.Information) help_text = """ Timing Configuration Guide

🎯 Purpose: Simulate human-like browsing to avoid automated detection

⚑ Action Delays: Time between clicks and scrolls
β€’ Lower = Faster scraping, higher detection risk
β€’ Higher = Slower scraping, more realistic behavior

⏸️ Page Breaks: Random pauses between pages
β€’ Simulates reading time and human fatigue
β€’ 70% chance with 15-45s duration is realistic

πŸ“š Batch Breaks: Pauses after processing multiple comics
β€’ Every 5 comics with 3-7s breaks simulates attention shifts

⌨️ Typing Speed: Character delay when entering credentials
β€’ 0.1 sec = Average typing speed (40 WPM)

πŸ’‘ Recommendations:
β€’ Fast Mode: Testing or when speed matters more than stealth
β€’ Balanced Mode: Recommended for regular use
β€’ Stealth Mode: Maximum safety but slower operation """ dialog.setText(help_text) dialog.setStandardButtons(QMessageBox.Ok) dialog.exec_() def apply_timing_preset(self, preset_type): """Apply a timing preset configuration.""" if preset_type == "fast": # Fast & Aggressive - minimal delays self.action_delay_min_spin.setValue(0.1) self.action_delay_max_spin.setValue(0.5) self.page_break_chance_spin.setValue(20) self.page_break_min_spin.setValue(5) self.page_break_max_spin.setValue(10) self.batch_break_interval_spin.setValue(15) self.batch_break_min_spin.setValue(1) self.batch_break_max_spin.setValue(2) self.typing_delay_spin.setValue(0.05) elif preset_type == "balanced": # Balanced - default recommended settings self.action_delay_min_spin.setValue(0.5) self.action_delay_max_spin.setValue(2.0) self.page_break_chance_spin.setValue(70) self.page_break_min_spin.setValue(15) self.page_break_max_spin.setValue(45) self.batch_break_interval_spin.setValue(5) self.batch_break_min_spin.setValue(3) self.batch_break_max_spin.setValue(7) self.typing_delay_spin.setValue(0.1) elif preset_type == "stealth": # Stealth - maximum human-like behavior self.action_delay_min_spin.setValue(1.0) self.action_delay_max_spin.setValue(3.0) self.page_break_chance_spin.setValue(90) self.page_break_min_spin.setValue(30) self.page_break_max_spin.setValue(90) self.batch_break_interval_spin.setValue(3) self.batch_break_min_spin.setValue(5) self.batch_break_max_spin.setValue(15) self.typing_delay_spin.setValue(0.15) # Save the new settings self.save_current_settings() # Show feedback self.statusBar().showMessage(f"Applied {preset_type.title()} timing preset", 3000) def reset_timing_defaults(self): """Reset all timing settings to safe defaults.""" self.apply_timing_preset("balanced") def create_status_section(self, parent_layout): """Create the status display section.""" group = QGroupBox("Status & Controls") layout = QVBoxLayout(group) # Status display status_layout = QHBoxLayout() status_info_layout = QVBoxLayout() self.status_label = QLabel("Ready to start scraping...") self.status_label.setStyleSheet("font-weight: bold; color: #2E8B57; font-size: 13px;") status_info_layout.addWidget(self.status_label) # Progress bar self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) status_info_layout.addWidget(self.progress_bar) status_layout.addLayout(status_info_layout) status_layout.addStretch() # Control buttons button_layout = QVBoxLayout() self.start_btn = QPushButton("Start Scraping") self.start_btn.clicked.connect(self.start_scraping) button_layout.addWidget(self.start_btn) self.downloads_btn = QPushButton("Open Downloads Folder") self.downloads_btn.clicked.connect(self.open_downloads_folder) button_layout.addWidget(self.downloads_btn) status_layout.addLayout(button_layout) layout.addLayout(status_layout) parent_layout.addWidget(group) def create_control_section(self, parent_layout): """Create the control buttons section - now integrated into status section.""" pass # This is now handled by create_status_section def update_credential_status(self): """Update the credential status display.""" username = self.credential_manager.get_saved_username() if username: self.credential_status_label.setText(f"Logged in as: {username}") self.credential_status_label.setStyleSheet("color: #2E8B57; font-weight: bold;") else: self.credential_status_label.setText("No credentials configured") self.credential_status_label.setStyleSheet("color: #FF6B35; font-weight: bold;") def show_login_dialog(self): """Show the login dialog for credential input.""" dialog = LoginDialog(self, self.credential_manager) if dialog.exec_() == dialog.Accepted: self.update_credential_status() self.log_message("Credentials updated successfully.") def start_scraping(self): """Start the scraping process.""" # Validate credentials credentials = self.credential_manager.load_credentials() if not credentials: QMessageBox.warning(self, "No Credentials", "Please configure your EBoek.info credentials first.") self.show_login_dialog() return # Validate page range start_page = self.start_page_spin.value() end_page = self.end_page_spin.value() validation = validate_page_range(start_page, end_page) if not validation['valid']: QMessageBox.warning(self, "Invalid Page Range", format_error_message(validation['errors'])) return # Save current settings self.save_current_settings() # Get scraping mode mode_index = self.mode_combo.currentIndex() mode_names = ["All Comics", "Latest Comics"] mode_name = mode_names[mode_index] if mode_index < len(mode_names) else "Unknown" # Start scraping self.log_message(f"Starting scraping: {mode_name} mode, pages {start_page} to {end_page}") # Create and start scraper thread # Collect current timing configuration timing_config = { 'action_delay_min': self.action_delay_min_spin.value(), 'action_delay_max': self.action_delay_max_spin.value(), 'page_break_chance': self.page_break_chance_spin.value(), 'page_break_min': self.page_break_min_spin.value(), 'page_break_max': self.page_break_max_spin.value(), 'batch_break_interval': self.batch_break_interval_spin.value(), 'batch_break_min': self.batch_break_min_spin.value(), 'batch_break_max': self.batch_break_max_spin.value(), 'typing_delay': self.typing_delay_spin.value(), } self.scraper_thread = ScraperThread( username=credentials['username'], password=credentials['password'], start_page=start_page, end_page=end_page, scraping_mode=mode_index, headless=self.headless_checkbox.isChecked(), timing_config=timing_config ) # Connect signals self.connect_scraper_signals() # Show progress dialog self.progress_dialog = ProgressDialog(self, self.scraper_thread) self.progress_dialog.show() # Start the thread self.scraper_thread.start() # Update UI state self.start_btn.setEnabled(False) self.status_label.setText("Scraping in progress...") self.status_label.setStyleSheet("font-weight: bold; color: #FF8C00;") def connect_scraper_signals(self): """Connect signals from the scraper thread to UI updates.""" if not self.scraper_thread: return # Login signals self.scraper_thread.login_started.connect(self.on_login_started) self.scraper_thread.login_success.connect(self.on_login_success) self.scraper_thread.login_failed.connect(self.on_login_failed) # Scraping completion self.scraper_thread.scraping_completed.connect(self.on_scraping_completed) # Status updates self.scraper_thread.status_update.connect(self.log_message) self.scraper_thread.error_occurred.connect(self.on_error_occurred) # Page progress self.scraper_thread.page_started.connect(self.on_page_started) self.scraper_thread.page_completed.connect(self.on_page_completed) def on_login_started(self, username): """Handle login started event.""" self.log_message(f"Logging in as {username}...") def on_login_success(self, username): """Handle successful login.""" self.log_message(f"Login successful for {username}") def on_login_failed(self, username, error): """Handle failed login.""" self.log_message(f"Login failed for {username}: {error}") QMessageBox.critical(self, "Login Failed", f"Could not log in as {username}.\n\n{error}\n\nPlease check your credentials.") def on_page_started(self, page_number, page_index, total_pages, url): """Handle page started event.""" self.log_message(f"Processing page {page_number} ({page_index}/{total_pages})") def on_page_completed(self, page_number, comics_processed): """Handle page completed event.""" self.log_message(f"Page {page_number} completed - {comics_processed} comics processed") def on_scraping_completed(self, summary): """Handle scraping completion.""" self.start_btn.setEnabled(True) if summary.get('cancelled'): self.status_label.setText("Scraping cancelled") self.status_label.setStyleSheet("font-weight: bold; color: #FF6B35;") self.log_message("Scraping was cancelled by user") elif summary.get('success'): self.status_label.setText("Scraping completed") self.status_label.setStyleSheet("font-weight: bold; color: #2E8B57;") self.log_message(f"Scraping completed! Processed {summary.get('total_comics_processed', 0)} comics, " f"triggered {summary.get('total_downloads_triggered', 0)} downloads") else: self.status_label.setText("Scraping failed") self.status_label.setStyleSheet("font-weight: bold; color: #f44336;") self.log_message("Scraping failed - see errors above") # Show summary if summary.get('errors'): error_count = len(summary['errors']) QMessageBox.warning(self, "Scraping Completed with Errors", f"Scraping completed but {error_count} errors occurred.\n" f"Check the log for details.") def on_error_occurred(self, error_message): """Handle error events.""" self.log_message(f"ERROR: {error_message}") def log_message(self, message): """Log message - removed from main window, only shown in progress dialog.""" # Activity log now only appears in the scraping progress dialog pass def save_current_settings(self): """Save current UI settings to configuration.""" settings = { 'headless_mode': self.headless_checkbox.isChecked(), 'verbose_logging': self.verbose_checkbox.isChecked(), 'default_start_page': self.start_page_spin.value(), 'default_end_page': self.end_page_spin.value(), 'scraping_mode': self.mode_combo.currentIndex(), # Timing settings 'action_delay_min': self.action_delay_min_spin.value(), 'action_delay_max': self.action_delay_max_spin.value(), 'page_break_chance': self.page_break_chance_spin.value(), 'page_break_min': self.page_break_min_spin.value(), 'page_break_max': self.page_break_max_spin.value(), 'batch_break_interval': self.batch_break_interval_spin.value(), 'batch_break_min': self.batch_break_min_spin.value(), 'batch_break_max': self.batch_break_max_spin.value(), 'typing_delay': self.typing_delay_spin.value(), } settings.update(self.app_settings) # Keep other settings self.credential_manager.save_app_settings(settings) self.app_settings = settings def open_downloads_folder(self): """Open the downloads folder in the system file manager.""" downloads_path = Path.home() / "Downloads" import sys import subprocess try: if sys.platform == "win32": os.startfile(downloads_path) elif sys.platform == "darwin": subprocess.run(["open", str(downloads_path)]) else: # linux subprocess.run(["xdg-open", str(downloads_path)]) except Exception as e: QMessageBox.information(self, "Downloads Folder", f"Downloads are saved to:\n{downloads_path}\n\n" f"Could not open folder automatically: {e}") def clear_credentials(self): """Clear saved credentials.""" reply = QMessageBox.question(self, "Clear Credentials", "Are you sure you want to clear the saved credentials?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: if self.credential_manager.clear_credentials(): self.update_credential_status() self.log_message("Saved credentials cleared.") QMessageBox.information(self, "Credentials Cleared", "Saved credentials have been cleared.") else: QMessageBox.warning(self, "Error", "Could not clear credentials.") def export_settings(self): """Export application settings to a file.""" file_path, _ = QFileDialog.getSaveFileName( self, "Export Settings", "eboek_scraper_settings.json", "JSON files (*.json);;All files (*.*)" ) if file_path: if self.credential_manager.export_settings(file_path): QMessageBox.information(self, "Export Successful", f"Settings exported to:\n{file_path}") else: QMessageBox.warning(self, "Export Failed", "Could not export settings.") def import_settings(self): """Import application settings from a file.""" file_path, _ = QFileDialog.getOpenFileName( self, "Import Settings", "", "JSON files (*.json);;All files (*.*)" ) if file_path: if self.credential_manager.import_settings(file_path): self.app_settings = self.credential_manager.load_app_settings() # Update UI with imported settings self.headless_checkbox.setChecked(self.app_settings.get('headless_mode', True)) self.verbose_checkbox.setChecked(self.app_settings.get('verbose_logging', False)) self.start_page_spin.setValue(self.app_settings.get('default_start_page', 1)) self.end_page_spin.setValue(self.app_settings.get('default_end_page', 1)) QMessageBox.information(self, "Import Successful", f"Settings imported from:\n{file_path}") self.log_message("Settings imported successfully.") else: QMessageBox.warning(self, "Import Failed", "Could not import settings.") def show_about(self): """Show the about dialog.""" QMessageBox.about(self, "About EBoek.info Scraper", "EBoek.info Scraper\n\n" "A GUI application for downloading comic strips from eboek.info.\n\n" "Features:\n" "β€’ Automated login and scraping\n" "β€’ Real-time progress monitoring\n" "β€’ Human-like behavior simulation\n" "β€’ Secure credential storage\n\n" "Built with Python and PyQt5.") def closeEvent(self, event): """Handle application close event.""" if self.scraper_thread and self.scraper_thread.isRunning(): reply = QMessageBox.question(self, "Scraping in Progress", "Scraping is currently in progress. " "Do you want to stop and exit?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.scraper_thread.request_stop() # Give it a moment to stop gracefully self.scraper_thread.wait(3000) # Wait up to 3 seconds event.accept() else: event.ignore() else: # Save settings before closing self.save_current_settings() event.accept()