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." + "" + ) + self.step1 = TourStepWidget("👋 Welcome!", step1_content) + + step2_content = ( + "Let's start with the basics for downloading:" + "" + ) + self.step2 = TourStepWidget("① Getting Started", step2_content) + + step3_content = ( + "Refine what you download with these filters:" + "" + ) + self.step3 = TourStepWidget("② Filtering Downloads", step3_content) + + step4_content = ( + "More options to customize your downloads:" + "" + ) + self.step4 = TourStepWidget("â‘ĸ Fine-Tuning Downloads", step4_content) + + step5_content = ( + "Organize your downloads and manage performance:" + "" + ) + self.step5 = TourStepWidget("â‘Ŗ Organization & Performance", step5_content) + + step6_content = ( + "Monitoring and Controls:" + "" + "
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." - "" - ) - self.step1 = TourStepWidget("👋 Welcome!", step1_content) - - step2_content = ( - "Let's start with the basics for downloading:" - "" - ) - self.step2 = TourStepWidget("① Getting Started", step2_content) - - step3_content = ( - "Refine what you download with these filters:" - "" - ) - self.step3 = TourStepWidget("② Filtering Downloads", step3_content) - - step4_content = ( - "More options to customize your downloads:" - "" - ) - self.step4 = TourStepWidget("â‘ĸ Fine-Tuning Downloads", step4_content) - - step5_content = ( - "Organize your downloads and manage performance:" - "" - ) - self.step5 = TourStepWidget("â‘Ŗ Organization & Performance", step5_content) - - step6_content = ( - "Monitoring and Controls:" - "" - "
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()