diff --git a/main.py b/main.py index 3403e06..50ba067 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,6 @@ import queue import hashlib import http.client import traceback -import html # For FavoriteArtistsDialog import subprocess # Added for opening files cross-platform import random from collections import deque # Ensure deque is imported @@ -23,7 +22,7 @@ from PyQt5.QtGui import ( from PyQt5.QtWidgets import ( QApplication, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog, QMessageBox, QListWidget, QRadioButton, QButtonGroup, QCheckBox, QSplitter, - QDialog, QStackedWidget, QScrollArea, QListWidgetItem, QSizePolicy, + QDialog, QStackedWidget, QScrollArea, QListWidgetItem, QAbstractItemView, # Added for QListWidget.NoSelection QFrame, QAbstractButton @@ -36,7 +35,7 @@ try: except ImportError: Image = None -from io import BytesIO # Keep this if used elsewhere, though not directly in this diff +from io import BytesIO try: print("Attempting to import from downloader_utils...") @@ -46,20 +45,19 @@ try: extract_post_info, download_from_api, PostProcessorSignals, - prepare_cookies_for_request, # Added for FavoriteArtistsDialog PostProcessorWorker, DownloadThread as BackendDownloadThread, SKIP_SCOPE_FILES, SKIP_SCOPE_POSTS, SKIP_SCOPE_BOTH, - CHAR_SCOPE_TITLE, # Added for completeness if used directly - CHAR_SCOPE_FILES, # Ensure this is imported - CHAR_SCOPE_BOTH, + CHAR_SCOPE_TITLE, + CHAR_SCOPE_FILES, + CHAR_SCOPE_BOTH, CHAR_SCOPE_COMMENTS, FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER, - STYLE_DATE_BASED, # Import new manga style - STYLE_POST_TITLE_GLOBAL_NUMBERING # Import new manga style - # IMAGE_EXTENSIONS will be used from downloader_utils directly + STYLE_DATE_BASED, + STYLE_POST_TITLE_GLOBAL_NUMBERING + ) print("Successfully imported names from downloader_utils.") except ImportError as e: @@ -68,18 +66,16 @@ except ImportError as e: print(f"--- Check downloader_utils.py for syntax errors or missing dependencies. ---") KNOWN_NAMES = [] PostProcessorWorker = object - # Create a mock PostProcessorSignals class with the expected signals class _MockPostProcessorSignals(QObject): progress_signal = pyqtSignal(str) file_download_status_signal = pyqtSignal(bool) external_link_signal = pyqtSignal(str, str, str, str) file_progress_signal = pyqtSignal(str, object) missed_character_post_signal = pyqtSignal(str, str) - # Add any other signals that might be expected if the real class is extended def __init__(self, parent=None): super().__init__(parent) print("WARNING: Using MOCK PostProcessorSignals due to import error from downloader_utils.py. Some functionalities might be impaired.") - PostProcessorSignals = _MockPostProcessorSignals # Use the mock class + PostProcessorSignals = _MockPostProcessorSignals BackendDownloadThread = QThread def clean_folder_name(n): return str(n) def extract_post_info(u): return None, None, None @@ -93,7 +89,7 @@ except ImportError as e: CHAR_SCOPE_COMMENTS = "comments" FILE_DOWNLOAD_STATUS_FAILED_RETRYABLE_LATER = "failed_retry_later" STYLE_DATE_BASED = "date_based" - STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" # Mock for safety + STYLE_POST_TITLE_GLOBAL_NUMBERING = "post_title_global_numbering" except Exception as e: print(f"--- UNEXPECTED IMPORT ERROR ---") @@ -108,31 +104,28 @@ RECOMMENDED_MAX_THREADS = 50 MAX_FILE_THREADS_PER_POST_OR_WORKER = 10 POST_WORKER_BATCH_THRESHOLD = 30 POST_WORKER_NUM_BATCHES = 4 -SOFT_WARNING_THREAD_THRESHOLD = 40 # New constant for soft warning -POST_WORKER_BATCH_DELAY_SECONDS = 2.5 # Seconds -MAX_POST_WORKERS_WHEN_COMMENT_FILTERING = 3 # New constant +SOFT_WARNING_THREAD_THRESHOLD = 40 +POST_WORKER_BATCH_DELAY_SECONDS = 2.5 +MAX_POST_WORKERS_WHEN_COMMENT_FILTERING = 3 HTML_PREFIX = "" CONFIG_ORGANIZATION_NAME = "KemonoDownloader" CONFIG_APP_NAME_MAIN = "ApplicationSettings" MANGA_FILENAME_STYLE_KEY = "mangaFilenameStyleV1" -STYLE_POST_TITLE = "post_title" # Already defined, but ensure it's STYLE_POST_TITLE +STYLE_POST_TITLE = "post_title" STYLE_ORIGINAL_NAME = "original_name" -STYLE_DATE_BASED = "date_based" # New style for date-based naming -STYLE_POST_TITLE_GLOBAL_NUMBERING = STYLE_POST_TITLE_GLOBAL_NUMBERING # Use imported or mocked +STYLE_DATE_BASED = "date_based" +STYLE_POST_TITLE_GLOBAL_NUMBERING = STYLE_POST_TITLE_GLOBAL_NUMBERING SKIP_WORDS_SCOPE_KEY = "skipWordsScopeV1" ALLOW_MULTIPART_DOWNLOAD_KEY = "allowMultipartDownloadV1" -USE_COOKIE_KEY = "useCookieV1" # New setting key -COOKIE_TEXT_KEY = "cookieTextV1" # New setting key for cookie text +USE_COOKIE_KEY = "useCookieV1" +COOKIE_TEXT_KEY = "cookieTextV1" CHAR_FILTER_SCOPE_KEY = "charFilterScopeV1" -SCAN_CONTENT_IMAGES_KEY = "scanContentForImagesV1" # New setting key +SCAN_CONTENT_IMAGES_KEY = "scanContentForImagesV1" CONFIRM_ADD_ALL_ACCEPTED = 1 -# Favorite Download Scopes -FAVORITE_SCOPE_SELECTED_LOCATION = "selected_location" -FAVORITE_SCOPE_ARTIST_FOLDERS = "artist_folders" CONFIRM_ADD_ALL_SKIP_ADDING = 2 CONFIRM_ADD_ALL_CANCEL_DOWNLOAD = 3 @@ -143,7 +136,7 @@ class ConfirmAddAllDialog(QDialog): self.setWindowTitle("Confirm Adding New Names") self.setModal(True) self.new_filter_objects_list = new_filter_objects_list - self.user_choice = CONFIRM_ADD_ALL_CANCEL_DOWNLOAD # Default to cancel if closed + self.user_choice = CONFIRM_ADD_ALL_CANCEL_DOWNLOAD main_layout = QVBoxLayout(self) @@ -158,14 +151,10 @@ class ConfirmAddAllDialog(QDialog): self.names_list_widget = QListWidget() for filter_obj in self.new_filter_objects_list: item_text = filter_obj["name"] - # Optionally, make group display more informative - # if filter_obj["is_group"]: - # item_text += f" (Group with aliases: {', '.join(filter_obj['aliases'])})" - list_item = QListWidgetItem(item_text) list_item.setFlags(list_item.flags() | Qt.ItemIsUserCheckable) - list_item.setCheckState(Qt.Checked) # Default to checked - list_item.setData(Qt.UserRole, filter_obj) # Store the full filter object + list_item.setCheckState(Qt.Checked) + list_item.setData(Qt.UserRole, filter_obj) self.names_list_widget.addItem(list_item) main_layout.addWidget(self.names_list_widget) @@ -219,26 +208,23 @@ class ConfirmAddAllDialog(QDialog): item = self.names_list_widget.item(i) if item.checkState() == Qt.Checked: filter_obj = item.data(Qt.UserRole) - if filter_obj: # Should always be true if populated correctly + if filter_obj: selected_objects.append(filter_obj) - # self.user_choice will be the list of selected filter_obj, or empty list if none selected - self.user_choice = selected_objects + self.user_choice = selected_objects self.accept() def _reject_skip_adding(self): self.user_choice = CONFIRM_ADD_ALL_SKIP_ADDING - self.reject() # QDialog.reject() is fine, we check user_choice + self.reject() def _reject_cancel_download(self): self.user_choice = CONFIRM_ADD_ALL_CANCEL_DOWNLOAD - self.reject() # QDialog.reject() is fine, we check user_choice + self.reject() def exec_(self): super().exec_() - # If user accepted but selected nothing, treat it as skipping addition if isinstance(self.user_choice, list) and not self.user_choice: - # QMessageBox.information(self, "No Selection", "No names were selected to be added. Skipping addition.") return CONFIRM_ADD_ALL_SKIP_ADDING return self.user_choice @@ -248,8 +234,6 @@ class KnownNamesFilterDialog(QDialog): super().__init__(parent) self.setWindowTitle("Add Known Names to Filter") self.setModal(True) - # Store the full list of known name objects. Each object is a dict. - # Sort them by the 'name' field for consistent display. self.all_known_name_entries = sorted(known_names_list, key=lambda x: x['name'].lower()) self.selected_entries_to_return = [] @@ -261,20 +245,19 @@ class KnownNamesFilterDialog(QDialog): main_layout.addWidget(self.search_input) self.names_list_widget = QListWidget() - self._populate_list_widget() # Populate with all entries initially + self._populate_list_widget() main_layout.addWidget(self.names_list_widget) - # Buttons layout: Select All, Deselect All, Add, Cancel buttons_layout = QHBoxLayout() self.select_all_button = QPushButton("Select All") self.select_all_button.clicked.connect(self._select_all_items) - buttons_layout.addWidget(self.select_all_button) # Add to main buttons_layout + buttons_layout.addWidget(self.select_all_button) self.deselect_all_button = QPushButton("Deselect All") self.deselect_all_button.clicked.connect(self._deselect_all_items) - buttons_layout.addWidget(self.deselect_all_button) # Add to main buttons_layout - buttons_layout.addStretch(1) # Stretch between Deselect All and Add Selected + buttons_layout.addWidget(self.deselect_all_button) + buttons_layout.addStretch(1) self.add_button = QPushButton("Add Selected") self.add_button.clicked.connect(self._accept_selection_action) @@ -295,10 +278,10 @@ class KnownNamesFilterDialog(QDialog): self.names_list_widget.clear() current_entries_source = names_to_display if names_to_display is not None else self.all_known_name_entries for entry_obj in current_entries_source: - item = QListWidgetItem(entry_obj['name']) # Display the 'name' (folder name) + item = QListWidgetItem(entry_obj['name']) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Unchecked) - item.setData(Qt.UserRole, entry_obj) # Store the full entry object + item.setData(Qt.UserRole, entry_obj) self.names_list_widget.addItem(item) def _filter_list_display(self): @@ -306,7 +289,6 @@ class KnownNamesFilterDialog(QDialog): if not search_text: self._populate_list_widget() return - # Filter based on the 'name' field of each entry object filtered_entries = [ entry_obj for entry_obj in self.all_known_name_entries if search_text in entry_obj['name'].lower() ] @@ -317,7 +299,7 @@ class KnownNamesFilterDialog(QDialog): for i in range(self.names_list_widget.count()): item = self.names_list_widget.item(i) if item.checkState() == Qt.Checked: - self.selected_entries_to_return.append(item.data(Qt.UserRole)) # Get the stored entry object + self.selected_entries_to_return.append(item.data(Qt.UserRole)) self.accept() def _select_all_items(self): @@ -330,201 +312,20 @@ class KnownNamesFilterDialog(QDialog): for i in range(self.names_list_widget.count()): self.names_list_widget.item(i).setCheckState(Qt.Unchecked) - def get_selected_entries(self): # Renamed method + def get_selected_entries(self): return self.selected_entries_to_return -class FavoriteArtistsDialog(QDialog): - """Dialog to display and select favorite artists.""" - def __init__(self, parent_app, cookies_config): - super().__init__(parent_app) - self.parent_app = parent_app # To access things like get_dark_theme and log_signal - self.cookies_config = cookies_config # dict: {'use_cookie', 'cookie_text', 'selected_cookie_file', 'app_base_dir'} - self.all_fetched_artists = [] # List of {'name': str, 'url': str} - self.selected_artist_urls = [] - - self.setWindowTitle("Favorite Artists") - self.setModal(True) - self.setMinimumSize(500, 600) - if hasattr(self.parent_app, 'get_dark_theme'): - self.setStyleSheet(self.parent_app.get_dark_theme()) - - self._init_ui() - self._fetch_favorite_artists() - - def _init_ui(self): - main_layout = QVBoxLayout(self) - - self.status_label = QLabel("Loading favorite artists...") - self.status_label.setAlignment(Qt.AlignCenter) - main_layout.addWidget(self.status_label) - - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Search artists...") - self.search_input.textChanged.connect(self._filter_artist_list_display) - main_layout.addWidget(self.search_input) - - self.artist_list_widget = QListWidget() - # Add a stylesheet to create a separation line between items - # The color #5A5A5A is chosen to match the border colors from your dark theme. - self.artist_list_widget.setStyleSheet(""" - QListWidget::item { - border-bottom: 1px solid #4A4A4A; /* Slightly softer line */ - padding-top: 4px; - padding-bottom: 4px; - }""") - main_layout.addWidget(self.artist_list_widget) - - # Combined buttons layout - combined_buttons_layout = QHBoxLayout() - - self.select_all_button = QPushButton("Select All") - self.select_all_button.clicked.connect(self._select_all_items) - combined_buttons_layout.addWidget(self.select_all_button) - - self.deselect_all_button = QPushButton("Deselect All") - self.deselect_all_button.clicked.connect(self._deselect_all_items) - combined_buttons_layout.addWidget(self.deselect_all_button) - - # combined_buttons_layout.addStretch(1) # Removed stretch from between button groups - - self.download_button = QPushButton("Download Selected") - self.download_button.clicked.connect(self._accept_selection_action) - self.download_button.setEnabled(False) # Disabled until artists are loaded - self.download_button.setDefault(True) - combined_buttons_layout.addWidget(self.download_button) - - self.cancel_button = QPushButton("Cancel") - self.cancel_button.clicked.connect(self.reject) - combined_buttons_layout.addWidget(self.cancel_button) - - combined_buttons_layout.addStretch(1) # Add stretch at the end to push all buttons to the left - - main_layout.addLayout(combined_buttons_layout) - - def _logger(self, message): - """Helper to log messages, either to parent app or console.""" - if hasattr(self.parent_app, 'log_signal') and self.parent_app.log_signal: - self.parent_app.log_signal.emit(f"[FavArtistsDialog] {message}") - else: - print(f"[FavArtistsDialog] {message}") - - def _fetch_favorite_artists(self): - fav_url = "https://kemono.su/api/v1/account/favorites?type=artist" # API endpoint for listing favorite artists - self._logger(f"Attempting to fetch favorite artists from: {fav_url}") - - cookies_dict = prepare_cookies_for_request( - self.cookies_config['use_cookie'], - self.cookies_config['cookie_text'], - self.cookies_config['selected_cookie_file'], - self.cookies_config['app_base_dir'], - self._logger - ) - - if self.cookies_config['use_cookie'] and not cookies_dict: - self.status_label.setText("Error: Cookies enabled but could not be loaded. Cannot fetch favorites.") - self._logger("Error: Cookies enabled but could not be loaded.") - QMessageBox.warning(self, "Cookie Error", "Cookies are enabled, but no valid cookies could be loaded. Please check your cookie settings or file.") - return - - try: - headers = {'User-Agent': 'Mozilla/5.0'} - response = requests.get(fav_url, headers=headers, cookies=cookies_dict, timeout=20) - response.raise_for_status() - - # Parse JSON response instead of HTML - artists_data = response.json() # This should be a list of artist objects - - if not isinstance(artists_data, list): - self.status_label.setText("Error: API did not return a list of artists.") - self._logger(f"Error: Expected a list from API, got {type(artists_data)}") - QMessageBox.critical(self, "API Error", "The favorite artists API did not return the expected data format (list).") - return - - self.all_fetched_artists = [] # Clear previous results before populating - for artist_entry in artists_data: - artist_id = artist_entry.get("id") - artist_name = html.unescape(artist_entry.get("name", "Unknown Artist").strip()) - artist_service = artist_entry.get("service") - - if artist_id and artist_name and artist_service: - # Construct the URL, assuming kemono.su base for now. - full_url = f"https://kemono.su/{artist_service}/user/{artist_id}" - self.all_fetched_artists.append({'name': artist_name, 'url': full_url, 'service': artist_service}) # Store service - else: - self._logger(f"Warning: Skipping favorite artist entry due to missing data: {artist_entry}") - - self.all_fetched_artists.sort(key=lambda x: x['name'].lower()) - self._populate_artist_list_widget() - self.status_label.setText(f"{len(self.all_fetched_artists)} favorite artist(s) found.") - self.download_button.setEnabled(len(self.all_fetched_artists) > 0) - - except requests.exceptions.RequestException as e: - self.status_label.setText(f"Error fetching favorites: {e}") - self._logger(f"Error fetching favorites: {e}") - QMessageBox.critical(self, "Fetch Error", f"Could not fetch favorite artists: {e}") - except Exception as e: - self.status_label.setText(f"An unexpected error occurred: {e}") - self._logger(f"Unexpected error: {e}") - QMessageBox.critical(self, "Error", f"An unexpected error occurred: {e}") - - def _populate_artist_list_widget(self, artists_to_display=None): - self.artist_list_widget.clear() - source_list = artists_to_display if artists_to_display is not None else self.all_fetched_artists - for artist_data in source_list: - # Display name and service - item = QListWidgetItem(f"{artist_data['name']} ({artist_data.get('service', 'N/A').capitalize()})") # type: ignore - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) - item.setCheckState(Qt.Unchecked) - item.setData(Qt.UserRole, artist_data) # Store the entire artist_data dictionary - self.artist_list_widget.addItem(item) - - def _filter_artist_list_display(self): - search_text = self.search_input.text().lower().strip() - if not search_text: - self._populate_artist_list_widget() - return - - filtered_artists = [ - artist for artist in self.all_fetched_artists - if search_text in artist['name'].lower() or search_text in artist['url'].lower() - ] - self._populate_artist_list_widget(filtered_artists) - - def _select_all_items(self): - for i in range(self.artist_list_widget.count()): - self.artist_list_widget.item(i).setCheckState(Qt.Checked) - - def _deselect_all_items(self): - for i in range(self.artist_list_widget.count()): - self.artist_list_widget.item(i).setCheckState(Qt.Unchecked) - - def _accept_selection_action(self): - self.selected_artists_data = [] # Renamed - for i in range(self.artist_list_widget.count()): - item = self.artist_list_widget.item(i) - if item.checkState() == Qt.Checked: - self.selected_artists_data.append(item.data(Qt.UserRole)) # Append the whole dict - - if not self.selected_artists_data: - QMessageBox.information(self, "No Selection", "Please select at least one artist to download.") - return - self.accept() - - def get_selected_artists(self): # Renamed - return self.selected_artists_data - class HelpGuideDialog(QDialog): """A multi-page dialog for displaying the feature guide.""" def __init__(self, steps_data, parent=None): super().__init__(parent) self.current_step = 0 - self.steps_data = steps_data # List of (title, content_html) tuples + self.steps_data = steps_data self.setWindowTitle("Kemono Downloader - Feature Guide") self.setModal(True) - self.setFixedSize(650, 600) # Adjusted size for guide content + self.setFixedSize(650, 600) - # Apply similar styling to TourDialog, or a distinct one if preferred self.setStyleSheet(parent.get_dark_theme() if hasattr(parent, 'get_dark_theme') else """ QDialog { background-color: #2E2E2E; border: 1px solid #5A5A5A; } QLabel { color: #E0E0E0; } @@ -533,7 +334,7 @@ class HelpGuideDialog(QDialog): QPushButton:pressed { background-color: #4A4A4A; } """) self._init_ui() - if parent: # Attempt to center on parent + if parent: self.move(parent.geometry().center() - self.rect().center()) def _init_ui(self): @@ -544,9 +345,9 @@ class HelpGuideDialog(QDialog): self.stacked_widget = QStackedWidget() main_layout.addWidget(self.stacked_widget, 1) - self.tour_steps_widgets = [] # To hold TourStepWidget instances + self.tour_steps_widgets = [] for title, content in self.steps_data: - step_widget = TourStepWidget(title, content) # Reuse TourStepWidget + step_widget = TourStepWidget(title, content) self.tour_steps_widgets.append(step_widget) self.stacked_widget.addWidget(step_widget) @@ -558,16 +359,9 @@ class HelpGuideDialog(QDialog): self.back_button.clicked.connect(self._previous_step) self.back_button.setEnabled(False) - # Determine base directory for assets - # This logic assumes 'assest' folder is at the same level as main.py or the executable if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): - # For PyInstaller, assets are in _MEIPASS or a relative path from executable - # If 'assest' is bundled at the root of _MEIPASS: assets_base_dir = sys._MEIPASS - # If 'assest' is bundled relative to the executable directory: - # assets_base_dir = os.path.dirname(sys.executable) else: - # For development, assets are relative to the script assets_base_dir = os.path.dirname(os.path.abspath(__file__)) github_icon_path = os.path.join(assets_base_dir, "assets", "github.png") @@ -578,8 +372,7 @@ class HelpGuideDialog(QDialog): self.instagram_button = QPushButton(QIcon(instagram_icon_path), "") self.Discord_button = QPushButton(QIcon(discord_icon_path), "") - # Optional: Set a fixed icon size for consistency - icon_size = QSize(24, 24) # Adjust as needed + icon_size = QSize(24, 24) self.github_button.setIconSize(icon_size) self.instagram_button.setIconSize(icon_size) self.Discord_button.setIconSize(icon_size) @@ -595,35 +388,31 @@ class HelpGuideDialog(QDialog): self.Discord_button.setToolTip("Visit our Discord community (Opens in browser)") - # Social media buttons layout social_layout = QHBoxLayout() social_layout.setSpacing(10) social_layout.addWidget(self.github_button) social_layout.addWidget(self.instagram_button) social_layout.addWidget(self.Discord_button) - # social_layout.addStretch(1) # Pushes social buttons to the left if uncommented and placed before nav buttons - # Add social buttons to the main buttons_layout, before the stretch, to keep them left - # Clear buttons_layout and rebuild to ensure order while buttons_layout.count(): - item = buttons_layout.takeAt(0) # Removes the item from the layout - if item.widget(): # Check if the item is a widget - item.widget().setParent(None) # Detach the widget from this layout - elif item.layout(): # If it's a sub-layout - pass # Sub-layouts are handled by Qt's ownership or need explicit deletion if complex - buttons_layout.addLayout(social_layout) # Add social buttons on the left - buttons_layout.addStretch(1) # Stretch between social and nav buttons - buttons_layout.addWidget(self.back_button) # Back and Next on the right + item = buttons_layout.takeAt(0) + if item.widget(): + item.widget().setParent(None) + elif item.layout(): + pass + buttons_layout.addLayout(social_layout) + buttons_layout.addStretch(1) + buttons_layout.addWidget(self.back_button) buttons_layout.addWidget(self.next_button) main_layout.addLayout(buttons_layout) - self._update_button_states() # Set initial button states + self._update_button_states() def _next_step_action(self): if self.current_step < len(self.tour_steps_widgets) - 1: self.current_step += 1 self.stacked_widget.setCurrentIndex(self.current_step) - else: # Last page - self.accept() # Close dialog + else: + self.accept() self._update_button_states() def _previous_step(self): @@ -640,15 +429,12 @@ class HelpGuideDialog(QDialog): self.back_button.setEnabled(self.current_step > 0) def _open_github_link(self): - # Replace with your actual GitHub project URL QDesktopServices.openUrl(QUrl("https://github.com/Yuvi9587")) def _open_instagram_link(self): - # Replace with your actual Instagram URL QDesktopServices.openUrl(QUrl("https://www.instagram.com/uvi.arts/")) def _open_Discord_link(self): - # Replace with your actual Discord URL QDesktopServices.openUrl(QUrl("https://discord.gg/BqP64XTdJN")) class TourStepWidget(QWidget): @@ -657,18 +443,18 @@ class TourStepWidget(QWidget): super().__init__(parent) layout = QVBoxLayout(self) layout.setContentsMargins(20, 20, 20, 20) - layout.setSpacing(10) # Adjusted spacing between title and content for bullet points + layout.setSpacing(10) title_label = QLabel(title_text) title_label.setAlignment(Qt.AlignCenter) title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;") layout.addWidget(title_label) scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) # Important for the content_label to resize correctly - scroll_area.setFrameShape(QFrame.NoFrame) # Make it look seamless with the dialog - scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Content is word-wrapped - scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Show scrollbar only when needed - scroll_area.setStyleSheet("background-color: transparent;") # Match dialog background + scroll_area.setWidgetResizable(True) + scroll_area.setFrameShape(QFrame.NoFrame) + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + scroll_area.setStyleSheet("background-color: transparent;") content_label = QLabel(content_text) content_label.setWordWrap(True) @@ -676,7 +462,7 @@ class TourStepWidget(QWidget): content_label.setTextFormat(Qt.RichText) content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;") scroll_area.setWidget(content_label) - layout.addWidget(scroll_area, 1) # The '1' is a stretch factor + layout.addWidget(scroll_area, 1) class TourDialog(QDialog): @@ -688,9 +474,9 @@ class TourDialog(QDialog): tour_finished_normally = pyqtSignal() tour_skipped = pyqtSignal() - CONFIG_ORGANIZATION_NAME = "KemonoDownloader" # Shared with main app for consistency if needed, but can be distinct - CONFIG_APP_NAME_TOUR = "ApplicationTour" # Specific QSettings group for tour - TOUR_SHOWN_KEY = "neverShowTourAgainV5" # Updated key to re-show tour + CONFIG_ORGANIZATION_NAME = "KemonoDownloader" + CONFIG_APP_NAME_TOUR = "ApplicationTour" + TOUR_SHOWN_KEY = "neverShowTourAgainV6" def __init__(self, parent=None): super().__init__(parent) @@ -699,7 +485,7 @@ class TourDialog(QDialog): self.setWindowTitle("Welcome to Kemono Downloader!") self.setModal(True) - self.setFixedSize(600, 620) # Slightly adjusted for potentially more text + self.setFixedSize(600, 620) self.setStyleSheet(""" QDialog { background-color: #2E2E2E; @@ -742,7 +528,7 @@ class TourDialog(QDialog): primary_screen = QApplication.primaryScreen() if not primary_screen: screens = QApplication.screens() - if not screens: return # Cannot center + if not screens: return primary_screen = screens[0] available_geo = primary_screen.availableGeometry() @@ -767,8 +553,8 @@ class TourDialog(QDialog): "" - ) # Original step5_content - self.step5_organization = TourStepWidget("β‘€ Organization & Performance", step5_content) + ) + self.step5 = TourStepWidget("β‘£ Organization & Performance", step5_content) step6_errors_content = ( "Sometimes, downloads might encounter issues. Here are a few common ones:" "" ) - self.step6_errors = TourStepWidget("β‘₯ Common Errors & Troubleshooting", step6_errors_content) # Title remains same, number changes - + self.step6_errors = TourStepWidget("β‘₯ Common Errors & Troubleshooting", step6_errors_content) + step7_final_controls_content = ( "Monitoring and Controls:" "