From da507b2b3a0a2cf14a7f5a96fcddb923309cd09e Mon Sep 17 00:00:00 2001
From: Yuvi9587 <114073886+Yuvi9587@users.noreply.github.com>
Date: Mon, 12 May 2025 18:37:11 +0530
Subject: [PATCH] Commit
---
downloader_utils.py | 8 +-
main.py | 336 +++++++++++++++++++++++++++++++++++++++++---
tour.py | 328 ------------------------------------------
3 files changed, 317 insertions(+), 355 deletions(-)
delete mode 100644 tour.py
diff --git a/downloader_utils.py b/downloader_utils.py
index 29b3012..9a9d647 100644
--- a/downloader_utils.py
+++ b/downloader_utils.py
@@ -46,8 +46,8 @@ DUPLICATE_MODE_MOVE_TO_SUBFOLDER = "move"
fastapi_app = None
KNOWN_NAMES = []
-MIN_SIZE_FOR_MULTIPART_DOWNLOAD = 10 * 1024 * 1024 # 10 MB
-MAX_PARTS_FOR_MULTIPART_DOWNLOAD = 8 # Max concurrent connections for a single file
+MIN_SIZE_FOR_MULTIPART_DOWNLOAD = 10 * 1024 * 1024 # 10 MB - Stays the same
+MAX_PARTS_FOR_MULTIPART_DOWNLOAD = 15 # Max concurrent connections for a single file
IMAGE_EXTENSIONS = {
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
@@ -266,7 +266,7 @@ def download_from_api(api_url_input, logger=print, start_page=None, end_page=Non
logger("â
Reached end of posts (Manga Mode fetch all).")
break
all_posts_for_manga_mode.extend(posts_batch_manga)
- current_offset_manga += len(posts_batch_manga)
+ current_offset_manga += page_size # Increment by page_size for the next API call's 'o' parameter
time.sleep(0.6)
except RuntimeError as e:
if "cancelled by user" in str(e).lower():
@@ -353,7 +353,7 @@ def download_from_api(api_url_input, logger=print, start_page=None, end_page=Non
if processed_target_post_flag:
break
- current_offset += len(posts_batch)
+ current_offset += page_size # Increment by page_size for the next API call's 'o' parameter
current_page_num += 1
time.sleep(0.6)
diff --git a/main.py b/main.py
index 0fa5d7a..95c2e4f 100644
--- a/main.py
+++ b/main.py
@@ -20,7 +20,7 @@ from PyQt5.QtGui import (
from PyQt5.QtWidgets import (
QApplication, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QFileDialog, QMessageBox, QListWidget, QDesktopWidget,
- QRadioButton, QButtonGroup, QCheckBox, QSplitter, QSizePolicy, QDialog,
+ QRadioButton, QButtonGroup, QCheckBox, QSplitter, QSizePolicy, QDialog, QStackedWidget,
QFrame,
QAbstractButton
)
@@ -76,20 +76,6 @@ except Exception as e:
print(f"-----------------------------", file=sys.stderr)
sys.exit(1)
-try:
- from tour import TourDialog
- print("Successfully imported TourDialog from tour.py.")
-except ImportError as e:
- print(f"--- TOUR IMPORT ERROR ---")
- print(f"Failed to import TourDialog from 'tour.py': {e}")
- print("Tour functionality will be unavailable.")
- TourDialog = None
-except Exception as e:
- print(f"--- UNEXPECTED TOUR IMPORT ERROR ---")
- print(f"An unexpected error occurred during tour import: {e}")
- traceback.print_exc()
- TourDialog = None
-
MAX_THREADS = 200
RECOMMENDED_MAX_THREADS = 50
@@ -114,6 +100,305 @@ DUPLICATE_MODE_DELETE = "delete"
DUPLICATE_MODE_MOVE_TO_SUBFOLDER = "move" # New mode
+# --- Tour Classes (Moved from tour.py) ---
+class TourStepWidget(QWidget):
+ """A single step/page in the tour."""
+ def __init__(self, title_text, content_text, parent=None):
+ super().__init__(parent)
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(20, 20, 20, 20)
+ layout.setSpacing(10) # Adjusted spacing between title and content for bullet points
+
+ title_label = QLabel(title_text)
+ title_label.setAlignment(Qt.AlignCenter)
+ # Increased padding-bottom for more space below title
+ title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
+
+ content_label = QLabel(content_text)
+ content_label.setWordWrap(True)
+ content_label.setAlignment(Qt.AlignLeft)
+ content_label.setTextFormat(Qt.RichText)
+ # Adjusted line-height for bullet point readability
+ content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;")
+
+ layout.addWidget(title_label)
+ layout.addWidget(content_label)
+ layout.addStretch(1)
+
+class TourDialog(QDialog):
+ """
+ A dialog that shows a multi-page tour to the user.
+ Includes a "Never show again" checkbox.
+ Uses QSettings to remember this preference.
+ """
+ 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 = "neverShowTourAgainV3" # Updated key for new tour content
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.settings = QSettings(self.CONFIG_ORGANIZATION_NAME, self.CONFIG_APP_NAME_TOUR)
+ self.current_step = 0
+
+ self.setWindowTitle("Welcome to Kemono Downloader!")
+ self.setModal(True)
+ # Set fixed square size, smaller than main window
+ self.setFixedSize(600, 620) # Slightly adjusted for potentially more text
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #2E2E2E;
+ border: 1px solid #5A5A5A;
+ }
+ QLabel {
+ color: #E0E0E0;
+ }
+ QCheckBox {
+ color: #C0C0C0;
+ font-size: 10pt;
+ spacing: 5px;
+ }
+ QCheckBox::indicator {
+ width: 13px;
+ height: 13px;
+ }
+ QPushButton {
+ background-color: #555;
+ color: #F0F0F0;
+ border: 1px solid #6A6A6A;
+ padding: 8px 15px;
+ border-radius: 4px;
+ min-height: 25px;
+ font-size: 11pt;
+ }
+ QPushButton:hover {
+ background-color: #656565;
+ }
+ QPushButton:pressed {
+ background-color: #4A4A4A;
+ }
+ """)
+ self._init_ui()
+ self._center_on_screen()
+
+ def _center_on_screen(self):
+ """Centers the dialog on the screen."""
+ try:
+ screen_geometry = QDesktopWidget().screenGeometry()
+ dialog_geometry = self.frameGeometry()
+ center_point = screen_geometry.center()
+ dialog_geometry.moveCenter(center_point)
+ self.move(dialog_geometry.topLeft())
+ except Exception as e:
+ print(f"[Tour] Error centering dialog: {e}")
+
+
+ def _init_ui(self):
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ self.stacked_widget = QStackedWidget()
+ main_layout.addWidget(self.stacked_widget, 1)
+
+ # --- Define Tour Steps with Updated Content ---
+ step1_content = (
+ "Hello! This quick tour will walk you through the main features of the Kemono Downloader."
+ "
"
+ "- Our goal is to help you easily download content from Kemono and Coomer.
"
+ "- Use the Next and Back buttons to navigate.
"
+ "- Click Skip Tour to close this guide at any time.
"
+ "- Check 'Never show this tour again' if you don't want to see this on future startups.
"
+ "
"
+ )
+ self.step1 = TourStepWidget("đ Welcome!", step1_content)
+
+ step2_content = (
+ "Let's start with the basics for downloading:"
+ ""
+ "- đ Kemono Creator/Post URL:
"
+ " Paste the full web address (URL) of a creator's page (e.g., https://kemono.su/patreon/user/12345) "
+ "or a specific post (e.g., .../post/98765).
"
+ "- đ Download Location:
"
+ " Click 'Browse...' to choose a folder on your computer where all downloaded files will be saved. "
+ "This is required unless you are using 'Only Links' mode.
"
+ "- đ Page Range (Creator URLs only):
"
+ " If downloading from a creator's page, you can specify a range of pages (e.g., pages 2 to 5). "
+ "Leave blank for all pages. This is disabled for single post URLs or when Manga/Comic Mode is active. "
+ "
"
+ )
+ self.step2 = TourStepWidget("â Getting Started", step2_content)
+
+ step3_content = (
+ "Refine what you download with these filters:"
+ ""
+ "- đ¯ Filter by Character(s):
"
+ " Enter character names, comma-separated (e.g., Tifa, Aerith). "
+ " - In Normal Mode, this filters individual files by matching their filenames.
"
+ " - In Manga/Comic Mode, this filters entire posts by matching the post title. Useful for targeting specific series.
"
+ " - Also helps in folder naming if 'Separate Folders' is enabled.
"
+ "- đĢ Skip with Words:
"
+ " Enter words, comma-separated (e.g., WIP, sketch, preview). "
+ " The Scope button (next to this input) cycles how this filter applies:"
+ " - Scope: Files: Skips files if their names contain any of these words.
"
+ " - Scope: Posts: Skips entire posts if their titles contain any of these words.
"
+ " - Scope: Both: Applies both file and post title skipping.
"
+ "- Filter Files (Radio Buttons): Choose what to download:"
+ "
"
+ " - All: Downloads all file types found.
"
+ " - Images/GIFs: Only common image formats and GIFs.
"
+ " - Videos: Only common video formats.
"
+ " - đĻ Only Archives: Exclusively downloads .zip and .rar files. When selected, 'Skip .zip' and 'Skip .rar' checkboxes are automatically disabled and unchecked.
"
+ " - đ Only Links: Extracts and displays external links from post descriptions instead of downloading files.
"
+ "
"
+ "
"
+ )
+ self.step3 = TourStepWidget("⥠Filtering Downloads", step3_content)
+
+ step4_content = (
+ "More options to customize your downloads:"
+ ""
+ "- Skip .zip / Skip .rar: Check these to avoid downloading these archive file types. "
+ " (Note: These are disabled and ignored if 'đĻ Only Archives' mode is selected).
"
+ "- Download Thumbnails Only: Downloads small preview images instead of full-sized files (if available).
"
+ "- Compress Large Images: If the 'Pillow' library is installed, images larger than 1.5MB will be converted to WebP format if the WebP version is significantly smaller.
"
+ "- đī¸ Custom Folder Name (Single Post Only):
"
+ " If you are downloading a single specific post URL AND 'Separate Folders by Name/Title' is enabled, "
+ "you can enter a custom name here for that post's download folder. "
+ "
"
+ )
+ self.step4 = TourStepWidget("âĸ Fine-Tuning Downloads", step4_content)
+
+ step5_content = (
+ "Organize your downloads and manage performance:"
+ ""
+ "- âī¸ Separate Folders by Name/Title: Creates subfolders based on the 'Filter by Character(s)' input or post titles (can use the 'Known Shows/Characters' list as a fallback for folder names).
"
+ "- Subfolder per Post: If 'Separate Folders' is on, this creates an additional subfolder for each individual post inside the main character/title folder.
"
+ "- đ Use Multithreading (Threads): Enables faster downloads for creator pages by processing multiple posts or files concurrently. The number of threads can be adjusted. Single post URLs are processed using a single thread for post data but can use multiple threads for file downloads within that post.
"
+ "- đ Manga/Comic Mode (Creator URLs only): Tailored for sequential content."
+ "
"
+ " - Downloads posts from oldest to newest.
"
+ " - The 'Page Range' input is disabled as all posts are fetched.
"
+ " - A filename style toggle button (e.g., 'Name: Post Title' or 'Name: Original File') appears in the top-right of the log area when this mode is active for a creator feed. Click it to change naming:"
+ "
"
+ " - Name: Post Title (Default): The first file in a post is named after the post's title (e.g., MyMangaChapter1.jpg). Subsequent files in the same post (if any) will retain their original filenames.
"
+ " - Name: Original File: All files will attempt to keep their original filenames as provided by the site (e.g., 001.jpg, page_02.png). You'll see a recommendation to use 'Post Title' style if you choose this.
"
+ "
"
+ " "
+ " - For best results with 'Name: Post Title' style, use the 'Filter by Character(s)' field with the manga/series title.
"
+ "
"
+ "- đ Known Shows/Characters: Add names here (e.g., Game Title, Series Name, Character Full Name). These are used for automatic folder creation when 'Separate Folders' is on and no specific 'Filter by Character(s)' is provided for a post.
"
+ "
"
+ )
+ self.step5 = TourStepWidget("âŖ Organization & Performance", step5_content)
+
+ step6_content = (
+ "Monitoring and Controls:"
+ ""
+ "- đ Progress Log / Extracted Links Log: Shows detailed download messages. If 'đ Only Links' mode is active, this area displays the extracted links.
"
+ "- Show External Links in Log: If checked, a secondary log panel appears below the main log to display any external links found in post descriptions. (This is disabled if 'đ Only Links' or 'đĻ Only Archives' mode is active).
"
+ "- Log Verbosity (Show Basic/Full Log): Toggles the main log between showing all messages (Full) or only key summaries, errors, and warnings (Basic).
"
+ "- đ Reset: Clears all input fields, logs, and resets temporary settings to their defaults. Can only be used when no download is active.
"
+ "- âŦī¸ Start Download / â Cancel: These buttons initiate or stop the current download/extraction process.
"
+ "
"
+ "
You're all set! Click 'Finish' to close the tour and start using the downloader."
+ )
+ self.step6 = TourStepWidget("⤠Logs & Final Controls", step6_content)
+
+
+ self.tour_steps = [self.step1, self.step2, self.step3, self.step4, self.step5, self.step6]
+ for step_widget in self.tour_steps:
+ self.stacked_widget.addWidget(step_widget)
+
+ bottom_controls_layout = QVBoxLayout()
+ bottom_controls_layout.setContentsMargins(15, 10, 15, 15) # Adjusted margins
+ bottom_controls_layout.setSpacing(10)
+
+ self.never_show_again_checkbox = QCheckBox("Never show this tour again")
+ bottom_controls_layout.addWidget(self.never_show_again_checkbox, 0, Qt.AlignLeft)
+
+ buttons_layout = QHBoxLayout()
+ buttons_layout.setSpacing(10)
+
+ self.skip_button = QPushButton("Skip Tour")
+ self.skip_button.clicked.connect(self._skip_tour_action)
+
+ self.back_button = QPushButton("Back")
+ self.back_button.clicked.connect(self._previous_step)
+ self.back_button.setEnabled(False)
+
+ self.next_button = QPushButton("Next")
+ self.next_button.clicked.connect(self._next_step_action)
+ self.next_button.setDefault(True)
+
+ buttons_layout.addWidget(self.skip_button)
+ buttons_layout.addStretch(1)
+ buttons_layout.addWidget(self.back_button)
+ buttons_layout.addWidget(self.next_button)
+
+ bottom_controls_layout.addLayout(buttons_layout)
+ main_layout.addLayout(bottom_controls_layout)
+
+ self._update_button_states()
+
+ def _handle_exit_actions(self):
+ if self.never_show_again_checkbox.isChecked():
+ self.settings.setValue(self.TOUR_SHOWN_KEY, True)
+ self.settings.sync()
+ # else:
+ # print(f"[Tour] '{self.TOUR_SHOWN_KEY}' setting not set to True (checkbox was unchecked on exit).")
+
+
+ def _next_step_action(self):
+ if self.current_step < len(self.tour_steps) - 1:
+ self.current_step += 1
+ self.stacked_widget.setCurrentIndex(self.current_step)
+ else:
+ self._handle_exit_actions()
+ self.tour_finished_normally.emit()
+ self.accept()
+ self._update_button_states()
+
+ def _previous_step(self):
+ if self.current_step > 0:
+ self.current_step -= 1
+ self.stacked_widget.setCurrentIndex(self.current_step)
+ self._update_button_states()
+
+ def _skip_tour_action(self):
+ self._handle_exit_actions()
+ self.tour_skipped.emit()
+ self.reject()
+
+ def _update_button_states(self):
+ if self.current_step == len(self.tour_steps) - 1:
+ self.next_button.setText("Finish")
+ else:
+ self.next_button.setText("Next")
+ self.back_button.setEnabled(self.current_step > 0)
+
+ @staticmethod
+ def run_tour_if_needed(parent_app_window):
+ try:
+ settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
+ never_show_again_from_settings = settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)
+
+ if never_show_again_from_settings:
+ print(f"[Tour] Skipped: '{TourDialog.TOUR_SHOWN_KEY}' is True in settings.")
+ return QDialog.Rejected
+
+ tour_dialog = TourDialog(parent_app_window)
+ result = tour_dialog.exec_()
+
+ return result
+ except Exception as e:
+ print(f"[Tour] CRITICAL ERROR in run_tour_if_needed: {e}")
+ traceback.print_exc()
+ return QDialog.Rejected
+# --- End Tour Classes ---
+
class DownloaderApp(QWidget):
character_prompt_response_signal = pyqtSignal(bool)
@@ -203,7 +488,8 @@ class DownloaderApp(QWidget):
self.manga_filename_style = self.settings.value(MANGA_FILENAME_STYLE_KEY, STYLE_POST_TITLE, type=str)
self.skip_words_scope = self.settings.value(SKIP_WORDS_SCOPE_KEY, SKIP_SCOPE_POSTS, type=str)
self.char_filter_scope = self.settings.value(CHAR_FILTER_SCOPE_KEY, CHAR_SCOPE_TITLE, type=str)
- self.allow_multipart_download_setting = self.settings.value(ALLOW_MULTIPART_DOWNLOAD_KEY, False, type=bool) # Default to OFF
+ # Always default multi-part download to OFF on launch, ignoring any saved setting.
+ self.allow_multipart_download_setting = False
self.duplicate_file_mode = self.settings.value(DUPLICATE_FILE_MODE_KEY, DUPLICATE_MODE_DELETE, type=str) # Default to DELETE
print(f"âšī¸ Known.txt will be loaded/saved at: {self.config_file}")
@@ -223,7 +509,7 @@ class DownloaderApp(QWidget):
self.log_signal.emit(f"âšī¸ Manga filename style loaded: '{self.manga_filename_style}'")
self.log_signal.emit(f"âšī¸ Skip words scope loaded: '{self.skip_words_scope}'")
self.log_signal.emit(f"âšī¸ Character filter scope loaded: '{self.char_filter_scope}'")
- self.log_signal.emit(f"âšī¸ Multi-part download preference loaded: {'Enabled' if self.allow_multipart_download_setting else 'Disabled'}")
+ self.log_signal.emit(f"âšī¸ Multi-part download defaults to: {'Enabled' if self.allow_multipart_download_setting else 'Disabled'} on launch")
self.log_signal.emit(f"âšī¸ Duplicate file handling mode loaded: '{self.duplicate_file_mode.capitalize()}'")
@@ -1747,7 +2033,7 @@ class DownloaderApp(QWidget):
self.external_log_output.append("đ External Links Found:")
self.file_progress_label.setText(""); self.cancellation_event.clear(); self.active_futures = []
- self.total_posts_to_process = self.processed_posts_count = self.download_counter = self.skip_counter = 0
+ self.total_posts_to_process = 0; self.processed_posts_count = 0; self.download_counter = 0; self.skip_counter = 0
self.progress_label.setText("Progress: Initializing...")
effective_num_post_workers = 1
@@ -2416,14 +2702,18 @@ if __name__ == '__main__':
# Center the window on the screen after it's shown and sized
downloader_app_instance._center_on_screen()
- if TourDialog:
- tour_settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
- tour_settings.setValue(TourDialog.TOUR_SHOWN_KEY, False)
- tour_settings.sync()
- print("[Main] Forcing tour to be active for this session.")
+ # TourDialog is now defined in this file, so we can call it directly.
+ try:
+ # The following lines were forcing the tour to show on every launch.
+ # By commenting them out, the application will now respect the
+ # "Never show this tour again" setting saved by the TourDialog.
tour_result = TourDialog.run_tour_if_needed(downloader_app_instance)
if tour_result == QDialog.Accepted: print("Tour completed by user.")
elif tour_result == QDialog.Rejected: print("Tour skipped or was already shown.")
+ except NameError:
+ print("[Main] TourDialog class not found. Skipping tour.") # Should not happen if code is correct
+ except Exception as e_tour:
+ print(f"[Main] Error during tour execution: {e_tour}")
exit_code = qt_app.exec_()
print(f"Application finished with exit code: {exit_code}")
diff --git a/tour.py b/tour.py
deleted file mode 100644
index c376e79..0000000
--- a/tour.py
+++ /dev/null
@@ -1,328 +0,0 @@
-import sys
-import traceback # Added for enhanced error reporting
-from PyQt5.QtWidgets import (
- QApplication, QDialog, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout,
- QStackedWidget, QSpacerItem, QSizePolicy, QCheckBox, QDesktopWidget
-)
-from PyQt5.QtCore import Qt, QSettings, pyqtSignal
-
-class TourStepWidget(QWidget):
- """A single step/page in the tour."""
- def __init__(self, title_text, content_text, parent=None):
- super().__init__(parent)
- layout = QVBoxLayout(self)
- layout.setContentsMargins(20, 20, 20, 20)
- layout.setSpacing(10) # Adjusted spacing between title and content for bullet points
-
- title_label = QLabel(title_text)
- title_label.setAlignment(Qt.AlignCenter)
- # Increased padding-bottom for more space below title
- title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
-
- content_label = QLabel(content_text)
- content_label.setWordWrap(True)
- content_label.setAlignment(Qt.AlignLeft)
- content_label.setTextFormat(Qt.RichText)
- # Adjusted line-height for bullet point readability
- content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;")
-
- layout.addWidget(title_label)
- layout.addWidget(content_label)
- layout.addStretch(1)
-
-class TourDialog(QDialog):
- """
- A dialog that shows a multi-page tour to the user.
- Includes a "Never show again" checkbox.
- Uses QSettings to remember this preference.
- """
- tour_finished_normally = pyqtSignal()
- tour_skipped = pyqtSignal()
-
- CONFIG_ORGANIZATION_NAME = "KemonoDownloader"
- CONFIG_APP_NAME_TOUR = "ApplicationTour"
- TOUR_SHOWN_KEY = "neverShowTourAgainV3" # Updated key for new tour content
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self.settings = QSettings(self.CONFIG_ORGANIZATION_NAME, self.CONFIG_APP_NAME_TOUR)
- self.current_step = 0
-
- self.setWindowTitle("Welcome to Kemono Downloader!")
- self.setModal(True)
- # Set fixed square size, smaller than main window
- self.setFixedSize(600, 620) # Slightly adjusted for potentially more text
- self.setStyleSheet("""
- QDialog {
- background-color: #2E2E2E;
- border: 1px solid #5A5A5A;
- }
- QLabel {
- color: #E0E0E0;
- }
- QCheckBox {
- color: #C0C0C0;
- font-size: 10pt;
- spacing: 5px;
- }
- QCheckBox::indicator {
- width: 13px;
- height: 13px;
- }
- QPushButton {
- background-color: #555;
- color: #F0F0F0;
- border: 1px solid #6A6A6A;
- padding: 8px 15px;
- border-radius: 4px;
- min-height: 25px;
- font-size: 11pt;
- }
- QPushButton:hover {
- background-color: #656565;
- }
- QPushButton:pressed {
- background-color: #4A4A4A;
- }
- """)
- self._init_ui()
- self._center_on_screen()
-
- def _center_on_screen(self):
- """Centers the dialog on the screen."""
- try:
- screen_geometry = QDesktopWidget().screenGeometry()
- dialog_geometry = self.frameGeometry()
- center_point = screen_geometry.center()
- dialog_geometry.moveCenter(center_point)
- self.move(dialog_geometry.topLeft())
- except Exception as e:
- print(f"[Tour] Error centering dialog: {e}")
-
-
- def _init_ui(self):
- main_layout = QVBoxLayout(self)
- main_layout.setContentsMargins(0, 0, 0, 0)
- main_layout.setSpacing(0)
-
- self.stacked_widget = QStackedWidget()
- main_layout.addWidget(self.stacked_widget, 1)
-
- # --- Define Tour Steps with Updated Content ---
- step1_content = (
- "Hello! This quick tour will walk you through the main features of the Kemono Downloader."
- ""
- "- Our goal is to help you easily download content from Kemono and Coomer.
"
- "- Use the Next and Back buttons to navigate.
"
- "- Click Skip Tour to close this guide at any time.
"
- "- Check 'Never show this tour again' if you don't want to see this on future startups.
"
- "
"
- )
- self.step1 = TourStepWidget("đ Welcome!", step1_content)
-
- step2_content = (
- "Let's start with the basics for downloading:"
- ""
- "- đ Kemono Creator/Post URL:
"
- " Paste the full web address (URL) of a creator's page (e.g., https://kemono.su/patreon/user/12345) "
- "or a specific post (e.g., .../post/98765).
"
- "- đ Download Location:
"
- " Click 'Browse...' to choose a folder on your computer where all downloaded files will be saved. "
- "This is required unless you are using 'Only Links' mode.
"
- "- đ Page Range (Creator URLs only):
"
- " If downloading from a creator's page, you can specify a range of pages (e.g., pages 2 to 5). "
- "Leave blank for all pages. This is disabled for single post URLs or when Manga/Comic Mode is active. "
- "
"
- )
- self.step2 = TourStepWidget("â Getting Started", step2_content)
-
- step3_content = (
- "Refine what you download with these filters:"
- ""
- "- đ¯ Filter by Character(s):
"
- " Enter character names, comma-separated (e.g., Tifa, Aerith). "
- " - In Normal Mode, this filters individual files by matching their filenames.
"
- " - In Manga/Comic Mode, this filters entire posts by matching the post title. Useful for targeting specific series.
"
- " - Also helps in folder naming if 'Separate Folders' is enabled.
"
- "- đĢ Skip with Words:
"
- " Enter words, comma-separated (e.g., WIP, sketch, preview). "
- " The Scope button (next to this input) cycles how this filter applies:"
- " - Scope: Files: Skips files if their names contain any of these words.
"
- " - Scope: Posts: Skips entire posts if their titles contain any of these words.
"
- " - Scope: Both: Applies both file and post title skipping.
"
- "- Filter Files (Radio Buttons): Choose what to download:"
- "
"
- " - All: Downloads all file types found.
"
- " - Images/GIFs: Only common image formats and GIFs.
"
- " - Videos: Only common video formats.
"
- " - đĻ Only Archives: Exclusively downloads .zip and .rar files. When selected, 'Skip .zip' and 'Skip .rar' checkboxes are automatically disabled and unchecked.
"
- " - đ Only Links: Extracts and displays external links from post descriptions instead of downloading files.
"
- "
"
- "
"
- )
- self.step3 = TourStepWidget("⥠Filtering Downloads", step3_content)
-
- step4_content = (
- "More options to customize your downloads:"
- ""
- "- Skip .zip / Skip .rar: Check these to avoid downloading these archive file types. "
- " (Note: These are disabled and ignored if 'đĻ Only Archives' mode is selected).
"
- "- Download Thumbnails Only: Downloads small preview images instead of full-sized files (if available).
"
- "- Compress Large Images: If the 'Pillow' library is installed, images larger than 1.5MB will be converted to WebP format if the WebP version is significantly smaller.
"
- "- đī¸ Custom Folder Name (Single Post Only):
"
- " If you are downloading a single specific post URL AND 'Separate Folders by Name/Title' is enabled, "
- "you can enter a custom name here for that post's download folder. "
- "
"
- )
- self.step4 = TourStepWidget("âĸ Fine-Tuning Downloads", step4_content)
-
- step5_content = (
- "Organize your downloads and manage performance:"
- ""
- "- âī¸ Separate Folders by Name/Title: Creates subfolders based on the 'Filter by Character(s)' input or post titles (can use the 'Known Shows/Characters' list as a fallback for folder names).
"
- "- Subfolder per Post: If 'Separate Folders' is on, this creates an additional subfolder for each individual post inside the main character/title folder.
"
- "- đ Use Multithreading (Threads): Enables faster downloads for creator pages by processing multiple posts or files concurrently. The number of threads can be adjusted. Single post URLs are processed using a single thread for post data but can use multiple threads for file downloads within that post.
"
- "- đ Manga/Comic Mode (Creator URLs only): Tailored for sequential content."
- "
"
- " - Downloads posts from oldest to newest.
"
- " - The 'Page Range' input is disabled as all posts are fetched.
"
- " - A filename style toggle button (e.g., 'Name: Post Title' or 'Name: Original File') appears in the top-right of the log area when this mode is active for a creator feed. Click it to change naming:"
- "
"
- " - Name: Post Title (Default): The first file in a post is named after the post's title (e.g., MyMangaChapter1.jpg). Subsequent files in the same post (if any) will retain their original filenames.
"
- " - Name: Original File: All files will attempt to keep their original filenames as provided by the site (e.g., 001.jpg, page_02.png). You'll see a recommendation to use 'Post Title' style if you choose this.
"
- "
"
- " "
- " - For best results with 'Name: Post Title' style, use the 'Filter by Character(s)' field with the manga/series title.
"
- "
"
- "- đ Known Shows/Characters: Add names here (e.g., Game Title, Series Name, Character Full Name). These are used for automatic folder creation when 'Separate Folders' is on and no specific 'Filter by Character(s)' is provided for a post.
"
- "
"
- )
- self.step5 = TourStepWidget("âŖ Organization & Performance", step5_content)
-
- step6_content = (
- "Monitoring and Controls:"
- ""
- "- đ Progress Log / Extracted Links Log: Shows detailed download messages. If 'đ Only Links' mode is active, this area displays the extracted links.
"
- "- Show External Links in Log: If checked, a secondary log panel appears below the main log to display any external links found in post descriptions. (This is disabled if 'đ Only Links' or 'đĻ Only Archives' mode is active).
"
- "- Log Verbosity (Show Basic/Full Log): Toggles the main log between showing all messages (Full) or only key summaries, errors, and warnings (Basic).
"
- "- đ Reset: Clears all input fields, logs, and resets temporary settings to their defaults. Can only be used when no download is active.
"
- "- âŦī¸ Start Download / â Cancel: These buttons initiate or stop the current download/extraction process.
"
- "
"
- "
You're all set! Click 'Finish' to close the tour and start using the downloader."
- )
- self.step6 = TourStepWidget("⤠Logs & Final Controls", step6_content)
-
-
- self.tour_steps = [self.step1, self.step2, self.step3, self.step4, self.step5, self.step6]
- for step_widget in self.tour_steps:
- self.stacked_widget.addWidget(step_widget)
-
- bottom_controls_layout = QVBoxLayout()
- bottom_controls_layout.setContentsMargins(15, 10, 15, 15) # Adjusted margins
- bottom_controls_layout.setSpacing(10)
-
- self.never_show_again_checkbox = QCheckBox("Never show this tour again")
- bottom_controls_layout.addWidget(self.never_show_again_checkbox, 0, Qt.AlignLeft)
-
- buttons_layout = QHBoxLayout()
- buttons_layout.setSpacing(10)
-
- self.skip_button = QPushButton("Skip Tour")
- self.skip_button.clicked.connect(self._skip_tour_action)
-
- self.back_button = QPushButton("Back")
- self.back_button.clicked.connect(self._previous_step)
- self.back_button.setEnabled(False)
-
- self.next_button = QPushButton("Next")
- self.next_button.clicked.connect(self._next_step_action)
- self.next_button.setDefault(True)
-
- buttons_layout.addWidget(self.skip_button)
- buttons_layout.addStretch(1)
- buttons_layout.addWidget(self.back_button)
- buttons_layout.addWidget(self.next_button)
-
- bottom_controls_layout.addLayout(buttons_layout)
- main_layout.addLayout(bottom_controls_layout)
-
- self._update_button_states()
-
- def _handle_exit_actions(self):
- if self.never_show_again_checkbox.isChecked():
- self.settings.setValue(self.TOUR_SHOWN_KEY, True)
- self.settings.sync()
- # else:
- # print(f"[Tour] '{self.TOUR_SHOWN_KEY}' setting not set to True (checkbox was unchecked on exit).")
-
-
- def _next_step_action(self):
- if self.current_step < len(self.tour_steps) - 1:
- self.current_step += 1
- self.stacked_widget.setCurrentIndex(self.current_step)
- else:
- self._handle_exit_actions()
- self.tour_finished_normally.emit()
- self.accept()
- self._update_button_states()
-
- def _previous_step(self):
- if self.current_step > 0:
- self.current_step -= 1
- self.stacked_widget.setCurrentIndex(self.current_step)
- self._update_button_states()
-
- def _skip_tour_action(self):
- self._handle_exit_actions()
- self.tour_skipped.emit()
- self.reject()
-
- def _update_button_states(self):
- if self.current_step == len(self.tour_steps) - 1:
- self.next_button.setText("Finish")
- else:
- self.next_button.setText("Next")
- self.back_button.setEnabled(self.current_step > 0)
-
- @staticmethod
- def run_tour_if_needed(parent_app_window):
- try:
- settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
- never_show_again_from_settings = settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)
-
- if never_show_again_from_settings:
- print(f"[Tour] Skipped: '{TourDialog.TOUR_SHOWN_KEY}' is True in settings.")
- return QDialog.Rejected
-
- tour_dialog = TourDialog(parent_app_window)
- result = tour_dialog.exec_()
-
- return result
- except Exception as e:
- print(f"[Tour] CRITICAL ERROR in run_tour_if_needed: {e}")
- traceback.print_exc()
- return QDialog.Rejected
-
-if __name__ == '__main__':
- app = QApplication(sys.argv)
-
- # --- For testing: force the tour to show by resetting the flag ---
- # This block ensures that if tour.py is run directly, the "Never show again" flag in QSettings is reset.
- print("[Tour Direct Run] Resetting 'Never show again' flag in QSettings.")
- test_settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
- test_settings.setValue(TourDialog.TOUR_SHOWN_KEY, False) # Set to False to force tour
- test_settings.sync()
- # --- End testing block ---
-
- print("[Tour Test] Running tour standalone...")
- result = TourDialog.run_tour_if_needed(None)
-
- if result == QDialog.Accepted:
- print("[Tour Test] Tour dialog was accepted (Finished).")
- elif result == QDialog.Rejected:
- print("[Tour Test] Tour dialog was rejected (Skipped or previously set to 'Never show again').")
-
- final_settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
- print(f"[Tour Test] Final state of '{TourDialog.TOUR_SHOWN_KEY}' in settings: {final_settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)}")
-
- sys.exit()