diff --git a/Kemono.png b/Kemono.png new file mode 100644 index 0000000..ee1e3b1 Binary files /dev/null and b/Kemono.png differ diff --git a/main.py b/main.py index 46bf0df..1e7b8f6 100644 --- a/main.py +++ b/main.py @@ -214,9 +214,86 @@ class ConfirmAddAllDialog(QDialog): 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.") + # QMessageBox.information(self, "No Selection", "No names were selected to be added. Skipping addition.") return CONFIRM_ADD_ALL_SKIP_ADDING return self.user_choice + +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.setWindowTitle("Kemono Downloader - Feature Guide") + self.setModal(True) + self.setFixedSize(650, 600) # Adjusted size for guide content + + # 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; } + 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() + if parent: # Attempt to center on parent + self.move(parent.geometry().center() - self.rect().center()) + + 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) + + self.tour_steps_widgets = [] # To hold TourStepWidget instances + for title, content in self.steps_data: + step_widget = TourStepWidget(title, content) # Reuse TourStepWidget + self.tour_steps_widgets.append(step_widget) + self.stacked_widget.addWidget(step_widget) + + buttons_layout = QHBoxLayout() + buttons_layout.setContentsMargins(15, 10, 15, 15) + buttons_layout.setSpacing(10) + + 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.addStretch(1) + buttons_layout.addWidget(self.back_button) + buttons_layout.addWidget(self.next_button) + main_layout.addLayout(buttons_layout) + 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 + 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 _update_button_states(self): + if self.current_step == len(self.tour_steps_widgets) - 1: + self.next_button.setText("Finish") + else: + self.next_button.setText("Next") + self.back_button.setEnabled(self.current_step > 0) + class TourStepWidget(QWidget): """A single step/page in the tour.""" def __init__(self, title_text, content_text, parent=None): @@ -1369,8 +1446,18 @@ class DownloaderApp(QWidget): self.new_char_input.returnPressed.connect(self.add_char_button.click) self.delete_char_button.clicked.connect(self.delete_selected_character) char_manage_layout.addWidget(self.new_char_input, 2) - char_manage_layout.addWidget(self.add_char_button, 1) - char_manage_layout.addWidget(self.delete_char_button, 1) + char_manage_layout.addWidget(self.add_char_button, 0) + + # Help button for Known Names list + self.known_names_help_button = QPushButton("?") # Restored question mark + self.known_names_help_button.setFixedWidth(35) # Small width for a square-like button + # self.known_names_help_button.setStyleSheet("font-weight: bold; padding-left: 8px; padding-right: 8px;") # Removed stylesheet + self.known_names_help_button.setToolTip("Open the application feature guide.") + self.known_names_help_button.clicked.connect(self._show_feature_guide) + + + char_manage_layout.addWidget(self.delete_char_button, 0) + char_manage_layout.addWidget(self.known_names_help_button, 0) # Moved to the end (rightmost) left_layout.addLayout(char_manage_layout) left_layout.addStretch(0) @@ -1378,7 +1465,7 @@ class DownloaderApp(QWidget): self.progress_log_label = QLabel("π Progress Log:") log_title_layout.addWidget(self.progress_log_label) log_title_layout.addStretch(1) - + self.link_search_input = QLineEdit() self.link_search_input.setToolTip("When in 'Only Links' mode, type here to filter the displayed links by text, URL, or platform.") self.link_search_input.setPlaceholderText("Search Links...") @@ -1897,12 +1984,19 @@ class DownloaderApp(QWidget): self.update_external_links_setting(self.external_links_checkbox.isChecked() if self.external_links_checkbox else False) self.log_signal.emit(f"="*20 + f" Mode changed to: {filter_mode_text} " + "="*20) - subfolders_on = self.use_subfolders_checkbox.isChecked() if self.use_subfolders_checkbox else False - + subfolders_on = self.use_subfolders_checkbox.isChecked() if self.use_subfolders_checkbox else False manga_on = self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False - - enable_character_filter_related_widgets = file_download_mode_active and (subfolders_on or manga_on) + # Determine if character filter section should be active (visible and enabled) + # It should be active if we are in a file downloading mode (not 'Only Links' or 'Only Archives') + character_filter_should_be_active = not is_only_links and not is_only_archives + + if self.character_filter_widget: + self.character_filter_widget.setVisible(character_filter_should_be_active) + + # Enable/disable character input and its scope button based on whether character filtering is active + enable_character_filter_related_widgets = character_filter_should_be_active + if self.character_input: self.character_input.setEnabled(enable_character_filter_related_widgets) if not enable_character_filter_related_widgets: @@ -1911,7 +2005,9 @@ class DownloaderApp(QWidget): if self.char_filter_scope_toggle_button: self.char_filter_scope_toggle_button.setEnabled(enable_character_filter_related_widgets) - self.update_ui_for_subfolders(subfolders_on) + # Call update_ui_for_subfolders to correctly set the "Subfolder per Post" checkbox state + # and "Custom Folder Name" visibility, which DO depend on the "Separate Folders" checkbox. + self.update_ui_for_subfolders(subfolders_on) # Pass the current state of the main subfolder checkbox self.update_custom_folder_visibility() self.update_ui_for_manga_mode(self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False) @@ -2307,23 +2403,13 @@ class DownloaderApp(QWidget): is_only_archives = self.radio_only_archives and self.radio_only_archives.isChecked() if self.use_subfolder_per_post_checkbox: - self.use_subfolder_per_post_checkbox.setEnabled(not is_only_links and not is_only_archives) + can_enable_subfolder_per_post = checked and not is_only_links and not is_only_archives + self.use_subfolder_per_post_checkbox.setEnabled(can_enable_subfolder_per_post) + if not can_enable_subfolder_per_post: # If it's disabled, also uncheck it + self.use_subfolder_per_post_checkbox.setChecked(False) - if hasattr(self, 'use_cookie_checkbox'): - self.use_cookie_checkbox.setEnabled(not is_only_links) # Cookies might be relevant for archives - - - enable_character_filter_related_widgets = checked and not is_only_links and not is_only_archives - - - if self.character_filter_widget: - self.character_filter_widget.setVisible(enable_character_filter_related_widgets) - if not self.character_filter_widget.isVisible(): - if self.character_input: self.character_input.clear() - if self.char_filter_scope_toggle_button: self.char_filter_scope_toggle_button.setEnabled(False) - else: - if self.char_filter_scope_toggle_button: self.char_filter_scope_toggle_button.setEnabled(True) - + # Visibility and enabled state of character filter widgets are now primarily handled + # by _handle_filter_mode_change to decouple from the subfolder checkbox. self.update_custom_folder_visibility() @@ -2468,14 +2554,16 @@ class DownloaderApp(QWidget): self.update_page_range_enabled_state() file_download_mode_active = not (self.radio_only_links and self.radio_only_links.isChecked()) - subfolders_on = self.use_subfolders_checkbox.isChecked() if self.use_subfolders_checkbox else False - enable_char_filter_widgets = file_download_mode_active and (subfolders_on or manga_mode_effectively_on) + # Character filter widgets should be enabled if it's a file download mode + enable_char_filter_widgets = file_download_mode_active and not (self.radio_only_archives and self.radio_only_archives.isChecked()) if self.character_input: self.character_input.setEnabled(enable_char_filter_widgets) if not enable_char_filter_widgets: self.character_input.clear() if self.char_filter_scope_toggle_button: self.char_filter_scope_toggle_button.setEnabled(enable_char_filter_widgets) + if self.character_filter_widget: # Also ensure the main widget visibility is correct + self.character_filter_widget.setVisible(enable_char_filter_widgets) self._update_multithreading_for_date_mode() # Update multithreading state based on manga mode @@ -3776,6 +3864,261 @@ class DownloaderApp(QWidget): self._update_manga_filename_style_button_text() self.update_ui_for_manga_mode(False) + def _show_feature_guide(self): + # Define content for each page + page1_title = "β Introduction & Main Inputs" + page1_content = """
+This guide provides an overview of the Kemono Downloader's features, fields, and buttons.
+ +Tifa, Aerith).(Vivi, Ulti, Uta).
+ Known.txt.(Yuffie, Sonon)~ (note the tilde ~).
+ Known.txt.Filter: Files: Checks individual filenames. A post is kept if any file matches; only matching files are downloaded. Folder naming uses the character from the matching filename.Filter: Title: Checks post titles. All files from a matching post are downloaded. Folder naming uses the character from the matching post title.Filter: Both: Checks post title first. If it matches, all files are downloaded. If not, it then checks filenames, and only matching files are downloaded. Folder naming prioritizes title match, then file match.Filter: Comments (Beta): Checks filenames first. If a file matches, all files from the post are downloaded. If no file match, it then checks post comments. If a comment matches, all files are downloaded. (Uses more API requests). Folder naming prioritizes file match, then comment match.WIP, sketch, preview) to skip certain content.Scope: Files: Skips individual 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 (post title first, then individual files).patreon, [HD]), to remove from downloaded filenames (case-insensitive).All: Downloads all file types found.Images/GIFs: Only common image formats (JPG, PNG, GIF, WEBP, etc.) and GIFs.Videos: Only common video formats (MP4, MKV, WEBM, MOV, etc.).π¦ Only Archives: Exclusively downloads .zip and .rar files. When selected, 'Skip .zip' and 'Skip .rar' checkboxes are automatically disabled and unchecked. 'Show External Links' is also disabled.π Only Links: Extracts and displays external links from post descriptions instead of downloading files. Download-related options and 'Show External Links' are disabled. The main download button changes to 'π Extract Links'.name1=value1; name2=value2).cookies.txt file (Netscape format). The path will appear in the text field.cookies.txt from the app's directory.Name: Post Title (Default): The first file in a post is named after the post's title. Subsequent files in the same post keep original names.Name: Original File: All files attempt to keep their original filenames.Name: Date Based: Files are named sequentially (001.ext, 002.ext, ...) based on post publication order. Multithreading for post processing is automatically disabled for this style.This section helps manage the Known.txt file, which is used for smart folder organization when 'Separate Folders by Name/Title' is enabled, especially as a fallback if a post doesn't match your active 'Filter by Character(s)' input.
Known.txt file (located in the app's directory) in your default text editor for advanced editing (like creating complex grouped aliases).Known.txt. Select entries here to delete them.My Awesome Series. Adds as a single entry.(Vivi, Ulti, Uta). Adds "Vivi", "Ulti", and "Uta" as three separate individual entries to Known.txt.~): e.g., (Character A, Char A)~. Adds one entry to Known.txt named "Character A Char A". "Character A" and "Char A" become aliases for this single folder/entry.Known.txt.Known.txt.Post Title, Original File, Date Based. (See Manga/Comic Mode section for details)..txt file.Known.txt:
+ .exe or main.py is).My Awesome Series. Content matching this will go into a folder named "My Awesome Series".(Character A, Char A, Alt Name A). Content matching "Character A", "Char A", OR "Alt Name A" will ALL go into a single folder named "Character A Char A Alt Name A" (after cleaning). All terms in the parentheses become aliases for that folder.cookies.txt (Optional):
+ cookies.txt in its directory.Many UI elements also have tooltips that appear when you hover your mouse over them, providing quick hints.
+ + """ + + steps = [ + (page1_title, page1_content), + (page2_title, page2_content), + (page3_title, page3_content), + (page4_title, page4_content), + (page5_title, page5_content), + (page6_title, page6_content), + (page7_title, page7_content), + (page8_title, page8_content), + ] + guide_dialog = HelpGuideDialog(steps, self) + guide_dialog.exec_() + def prompt_add_character(self, character_name): global KNOWN_NAMES reply = QMessageBox.question(self, "Add Filter Name to Known List?", f"The name '{character_name}' was encountered or used as a filter.\nIt's not in your known names list (used for folder suggestions).\nAdd it now?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)