From a00d9de546a89ef0d4ef16fcb8167ec5d3b46549 Mon Sep 17 00:00:00 2001
From: Yuvi9587 <114073886+Yuvi9587@users.noreply.github.com>
Date: Thu, 3 Jul 2025 12:54:05 +0530
Subject: [PATCH] Commit
---
src/core/workers.py | 13 +-
src/i18n/translator.py | 2 +-
src/ui/dialogs/DownloadHistoryDialog.py | 352 +++++++++++------------
src/ui/main_window.py | 364 ++++++++++++++++--------
4 files changed, 437 insertions(+), 294 deletions(-)
diff --git a/src/core/workers.py b/src/core/workers.py
index c5d054c..9d50471 100644
--- a/src/core/workers.py
+++ b/src/core/workers.py
@@ -987,6 +987,17 @@ class PostProcessorWorker:
cleaned_post_title_for_sub =clean_folder_name (post_title )
post_id_for_fallback =self .post .get ('id','unknown_id')
+ if getattr(self, 'use_post_date_prefix', False):
+ post_date = self.post.get('published') or self.post.get('date') or self.post.get('created_at')
+ if post_date:
+ # Format date as YYYY-MM-DD (or as you prefer)
+ try:
+ date_obj = datetime.datetime.fromisoformat(post_date.replace('Z', '+00:00'))
+ date_str = date_obj.strftime('%Y-%m-%d')
+ except Exception:
+ date_str = str(post_date)[:10]
+ cleaned_post_title_for_sub = f"{date_str} {cleaned_post_title_for_sub}"
+ cleaned_post_title_for_sub = clean_folder_name(cleaned_post_title_for_sub)
if not cleaned_post_title_for_sub or cleaned_post_title_for_sub =="untitled_folder":
self .logger (f" ⚠️ Post title '{post_title }' resulted in a generic subfolder name. Using 'post_{post_id_for_fallback }' as base.")
@@ -1673,4 +1684,4 @@ class DownloadThread (QThread ):
class InterruptedError(Exception):
"""Custom exception for handling cancellations gracefully."""
- pass
+ pass
\ No newline at end of file
diff --git a/src/i18n/translator.py b/src/i18n/translator.py
index 777f8d2..48fa3d5 100644
--- a/src/i18n/translator.py
+++ b/src/i18n/translator.py
@@ -2900,7 +2900,7 @@ translations ["en"]={
"tour_dialog_step6_title": "⑤ Organization & Performance",
"tour_dialog_step6_content": "Organize your downloads and manage performance:\n
\n- ⚙️ Separate folders by Name/Title: Creates subfolders based on the 'Filter by Character(s)' input or post titles (can use the Known.txt list as a fallback for folder names).
\n- Subfolder per post: If 'Separate Folders' is on, this creates an additional subfolder for each individual post inside the main character/title folder.
\n- 🚀 Use multithreading (Threads): Enables faster operations. The number in the 'Threads' input means:\n
- For Creator Feeds: Number of posts to process simultaneously. Files from each post are downloaded sequentially by its worker (unless 'Date Based' manga naming is on, which forces 1 post worker).
\n- For Single Post URLs: Number of files to download simultaneously from that single post.
\nIf unchecked, 1 thread is used. High thread counts (e.g., >40) may show a warning.
\n- Multipart Download Toggle (top-right of log area):
\nThe 'Multi-part: [ON/OFF]' button enables/disables multi-segment downloads for individual large files. \n- ON: Can speed up large file downloads (e.g., videos) but may increase UI stutter or log spam with many small files. A warning will appear on activation. If a multipart download fails, it retries as a single stream.
\n- OFF (Default): Files are downloaded in a single stream.
\nThis is disabled if 'Links Only' or 'Archives Only' mode is active.
\n- 📖 Manga/Comic Mode (Creator URLs only): Designed for sequential content.\n
\n- Downloads posts from oldest to newest.
\n- The 'Page Range' input is disabled as all posts are fetched.
\n- A filename style toggle button (e.g., 'Name: Post Title') appears at the top-right of the log area when this mode is active for a creator feed. Click it to cycle between naming styles:\n
\n- Name: Post Title (Default): The first file in a post is named after the cleaned post title (e.g., 'My Chapter 1.jpg'). Subsequent files in the *same post* will attempt to keep their original filenames (e.g., 'page_02.png', 'bonus_art.jpg'). If the post has only one file, it's named after the post title. This is generally recommended for most manga/comics.
\n- Name: Original File: All files attempt to keep their original filenames. An optional prefix (e.g., 'MySeries_') can be entered in the input field that appears next to the style button. Example: 'MySeries_OriginalFile.jpg'.
\n- Name: Title+G.Num (Post Title + Global Numbering): All files across all posts in the current download session are named sequentially using the cleaned post title as a prefix, followed by a global counter. E.g.: Post 'Chapter 1' (2 files) -> 'Chapter 1_001.jpg', 'Chapter 1_002.png'. The next post, 'Chapter 2' (1 file), would continue the numbering -> 'Chapter 2_003.jpg'. Multithreading for post processing is automatically disabled for this style to ensure correct global numbering.
\n- Name: Date Based: Files are named sequentially (001.ext, 002.ext, ...) based on the publish order of the posts. An optional prefix (e.g., 'MySeries_') can be entered in the input field that appears next to the style button. Example: 'MySeries_001.jpg'. Multithreading for post processing is automatically disabled for this style.
\n
\n
\n- For best results with the 'Name: Post Title', 'Name: Title+G.Num', or 'Name: Date Based' styles, use the 'Filter by Character(s)' field with the manga/series title for folder organization.
\n
\n- 🎭 Known.txt for Smart Folder Organization:
\nKnown.txt (in the app directory) allows fine-grained control over automatic folder organization when 'Separate folders by Name/Title' is on.\n\n- How it works: Each line in
Known.txt is an entry. \n- A simple line like
My Awesome Series means matching content will go into a folder named \"My Awesome Series\".
\n- A grouped line like
(Character A, Char A, Alt Name A) means 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 cleanup). All terms in the parentheses become aliases for that folder.
\n- Smart Fallback: When 'Separate folders by Name/Title' is on, and if a post doesn't match any specific 'Filter by Character(s)' entries, the downloader consults
Known.txt to find a matching master name for folder creation.
\n- User-Friendly Management: Add simple (non-grouped) names via the UI list below. For advanced editing (like creating/modifying grouped aliases), click 'Open Known.txt' to edit the file in your text editor. The app reloads it on next use or next startup.
\n
\n \n
",
"tour_dialog_step7_title": "⑥ Common Errors & Troubleshooting",
- "tour_dialog_step7_content": "Sometimes downloads can run into issues. Here are some of the most common ones:\n\n- Character Input Tooltip:
\nEnter character names, comma-separated (e.g., Tifa, Aerith).
\nGroup aliases for a combined folder name: (alias1, alias2, alias3) becomes folder 'alias1 alias2 alias3'.
\nAll names in the group are used as aliases for content matching.
\nThe 'Filter: [Type]' button next to this input changes how this filter applies:
\n- Filter: Files: Checks individual filenames. Only matching files are downloaded.
\n- Filter: Title: Checks post titles. All files from a matching post are downloaded.
\n- Filter: Both: Checks post title first. If no match, then checks filenames.
\n- Filter: Comments (Beta): Checks filenames first. If no match, then checks post comments.
\nThis filter also influences folder naming if 'Separate folders by Name/Title' is enabled.
\n- 502 Bad Gateway / 503 Service Unavailable / 504 Gateway Timeout:
\nThese usually indicate temporary server-side problems with Kemono/Coomer. The site might be overloaded, down for maintenance, or having issues.
\nSolution: Wait a while (e.g., 30 minutes to a few hours) and try again later. Check the site directly in your browser.
\n- Connection Lost / Connection Refused / Timeout (during file download):
\nThis can happen due to your internet connection, server instability, or if the server drops the connection for a large file.
\nSolution: Check your internet. Try reducing the 'Threads' count if it's high. The app may offer to retry some failed files at the end of a session.
\n- IncompleteRead Error:
\nThe server sent less data than expected. Often a temporary network hiccup or server issue.
\nSolution: The app will often mark these files for a retry at the end of the download session.
\n- 403 Forbidden / 401 Unauthorized (less common for public posts):
\nYou may not have permission to access the content. For some paywalled or private content, using the 'Use cookie' option with valid cookies from your browser session might help. Ensure your cookies are up to date.
\n- 404 Not Found:
\nThe post or file URL is incorrect, or the content has been deleted from the site. Double-check the URL.
\n- 'No posts found' / 'Target post not found':
\nEnsure the URL is correct and the creator/post exists. If using page ranges, make sure they are valid for the creator. For very new posts, there might be a slight delay before they appear in the API.
\n- General Slowness / App '(Not Responding)':
\nAs mentioned in Step 1, if the app appears to freeze after starting, especially with large creator feeds or many threads, please give it time. It is likely processing data in the background. Reducing the thread count can sometimes improve responsiveness if this is frequent. \n
",
+ "tour_dialog_step7_content": "Sometimes downloads can run into issues. Here are some of the most common ones:\n\n- 502 Bad Gateway / 503 Service Unavailable / 504 Gateway Timeout:
\nThese usually indicate temporary server-side problems with Kemono/Coomer. The site might be overloaded, down for maintenance, or having issues.
\nSolution: Wait a while (e.g., 30 minutes to a few hours) and try again later. Check the site directly in your browser.
\n- Connection Lost / Connection Refused / Timeout (during file download):
\nThis can happen due to your internet connection, server instability, or if the server drops the connection for a large file.
\nSolution: Check your internet. Try reducing the 'Threads' count if it's high. The app may offer to retry some failed files at the end of a session.
\n- IncompleteRead Error:
\nThe server sent less data than expected. Often a temporary network hiccup or server issue.
\nSolution: The app will often mark these files for a retry at the end of the download session.
\n- 403 Forbidden / 401 Unauthorized (less common for public posts):
\nYou may not have permission to access the content. For some paywalled or private content, using the 'Use cookie' option with valid cookies from your browser session might help. Ensure your cookies are up to date.
\n- 404 Not Found:
\nThe post or file URL is incorrect, or the content has been deleted from the site. Double-check the URL.
\n- 'No posts found' / 'Target post not found':
\nEnsure the URL is correct and the creator/post exists. If using page ranges, make sure they are valid for the creator. For very new posts, there might be a slight delay before they appear in the API.
\n- General Slowness / App '(Not Responding)':
\nAs mentioned in Step 1, if the app appears to freeze after starting, especially with large creator feeds or many threads, please give it time. It is likely processing data in the background. Reducing the thread count can sometimes improve responsiveness if this is frequent. \n
",
"tour_dialog_step8_title": "⑦ Logs & Final Controls",
"tour_dialog_step8_content": "Monitoring and Controls:\n\n- 📜 Progress Log / Extracted Links Log: Shows detailed download messages. If '🔗 Only Links' mode is active, this area displays the extracted links.
\n- Show external links in log: If checked, a secondary log panel appears below the main log to display external links found in post descriptions. (This is disabled if '🔗 Only Links' or '📦 Only Archives' mode is active).
\n- Log Display Toggle (👁️ / 🙈 Button):
\nThis button (top-right of the log area) changes the main log view:\n- 👁️ Progress Log (Default): Shows all download activity, errors, and summaries.
\n- 🙈 Missed Character Log: Displays a list of key terms from post titles that were skipped due to your 'Filter by Character(s)' settings. Useful for identifying content you might be unintentionally missing.
\n- 🔄 Reset: Clears all input fields, logs, and resets temporary settings to their defaults. Can only be used when no download is active.
\n- ⬇️ Start Download / 🔗 Extract Links / ⏸️ Pause / ❌ Cancel: These buttons control the process. 'Cancel & Reset UI' stops the current operation and performs a soft reset of the UI, keeping your URL and directory inputs. 'Pause/Resume' allows for temporary halting and continuing.
\n- If some files fail with recoverable errors (like 'IncompleteRead'), you may be prompted to retry them at the end of a session.
\n
\n
You're all set! Click 'Finish' to close the tour and start using the downloader.",
"help_guide_dialog_title": "Kemono Downloader - Feature Guide",
diff --git a/src/ui/dialogs/DownloadHistoryDialog.py b/src/ui/dialogs/DownloadHistoryDialog.py
index b64598b..f25922d 100644
--- a/src/ui/dialogs/DownloadHistoryDialog.py
+++ b/src/ui/dialogs/DownloadHistoryDialog.py
@@ -1,7 +1,7 @@
# --- Standard Library Imports ---
import os
import time
-
+import json
# --- PyQt5 Imports ---
from PyQt5.QtCore import Qt, QStandardPaths, QTimer
from PyQt5.QtWidgets import (
@@ -15,205 +15,207 @@ from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
-class DownloadHistoryDialog(QDialog):
- """
- Dialog to display download history, showing the last few downloaded files
- and the first posts processed in the current session. It also allows
- exporting this history to a text file.
- """
+class DownloadHistoryDialog (QDialog ):
+ """Dialog to display download history."""
+ def __init__ (self ,last_3_downloaded_entries ,first_processed_entries ,parent_app ,parent =None ):
+ super ().__init__ (parent )
+ self .parent_app =parent_app
+ self .last_3_downloaded_entries =last_3_downloaded_entries
+ self .first_processed_entries =first_processed_entries
+ self .setModal (True )
- def __init__(self, last_downloaded_entries, first_processed_entries, parent_app, parent=None):
- """
- Initializes the dialog.
+ # Patch missing creator_display_name and creator_name using parent_app.creator_name_cache if available
+ creator_name_cache = getattr(parent_app, 'creator_name_cache', None)
+ if creator_name_cache:
+ # Patch left pane (files)
+ for entry in self.last_3_downloaded_entries:
+ if not entry.get('creator_display_name'):
+ service = entry.get('service', '').lower()
+ user_id = str(entry.get('user_id', ''))
+ key = (service, user_id)
+ entry['creator_display_name'] = creator_name_cache.get(key, entry.get('folder_context_name', 'Unknown Creator/Series'))
+ # Patch right pane (posts)
+ for entry in self.first_processed_entries:
+ if not entry.get('creator_name'):
+ service = entry.get('service', '').lower()
+ user_id = str(entry.get('user_id', ''))
+ key = (service, user_id)
+ entry['creator_name'] = creator_name_cache.get(key, entry.get('user_id', 'Unknown'))
- Args:
- last_downloaded_entries (list): A list of dicts for the last few files.
- first_processed_entries (list): A list of dicts for the first few posts.
- parent_app (DownloaderApp): A reference to the main application window.
- parent (QWidget, optional): The parent widget. Defaults to None.
- """
- super().__init__(parent)
- self.parent_app = parent_app
- self.last_3_downloaded_entries = last_downloaded_entries
- self.first_processed_entries = first_processed_entries
- self.setModal(True)
+ app_icon =get_app_icon_object ()
+ if not app_icon .isNull ():
+ self .setWindowIcon (app_icon )
- # --- Basic Window Setup ---
- app_icon = get_app_icon_object()
- if app_icon and not app_icon.isNull():
- self.setWindowIcon(app_icon)
+ screen_height =QApplication .primaryScreen ().availableGeometry ().height ()if QApplication .primaryScreen ()else 768
+ scale_factor =screen_height /768.0
+ base_min_w ,base_min_h =600 ,450
- # Set window size dynamically
- screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
- scale_factor = screen_height / 1080.0
- base_min_w, base_min_h = 600, 450
- scaled_min_w = int(base_min_w * 1.5 * scale_factor)
- scaled_min_h = int(base_min_h * scale_factor)
- self.setMinimumSize(scaled_min_w, scaled_min_h)
- self.resize(scaled_min_w, scaled_min_h + 100) # Give it a bit more height
+ scaled_min_w =int (base_min_w *1.5 *scale_factor )
+ scaled_min_h =int (base_min_h *scale_factor )
+ self .setMinimumSize (scaled_min_w ,scaled_min_h )
- # --- Initialize UI and Apply Theming ---
- self._init_ui()
- self._retranslate_ui()
- self._apply_theme()
+ self .setWindowTitle (self ._tr ("download_history_dialog_title_combined","Download History"))
- def _init_ui(self):
- """Initializes all UI components and layouts for the dialog."""
- dialog_layout = QVBoxLayout(self)
- self.setLayout(dialog_layout)
- self.main_splitter = QSplitter(Qt.Horizontal)
- dialog_layout.addWidget(self.main_splitter)
+ dialog_layout =QVBoxLayout (self )
+ self .setLayout (dialog_layout )
- # --- Left Pane (Last Downloaded Files) ---
- left_pane_widget = self._create_history_pane(
- self.last_3_downloaded_entries,
- "history_last_downloaded_header", "Last 3 Files Downloaded:",
- self._format_last_downloaded_entry
- )
- self.main_splitter.addWidget(left_pane_widget)
- # --- Right Pane (First Processed Posts) ---
- right_pane_widget = self._create_history_pane(
- self.first_processed_entries,
- "first_files_processed_header", "First {count} Posts Processed This Session:",
- self._format_first_processed_entry,
- count=len(self.first_processed_entries)
- )
- self.main_splitter.addWidget(right_pane_widget)
+ self .main_splitter =QSplitter (Qt .Horizontal )
+ dialog_layout .addWidget (self .main_splitter )
- # --- Bottom Buttons ---
- bottom_button_layout = QHBoxLayout()
- self.save_history_button = QPushButton()
- self.save_history_button.clicked.connect(self._save_history_to_txt)
- bottom_button_layout.addStretch(1)
- bottom_button_layout.addWidget(self.save_history_button)
- dialog_layout.addLayout(bottom_button_layout)
- # Set splitter sizes after the dialog is shown to ensure correct proportions
- QTimer.singleShot(0, lambda: self.main_splitter.setSizes([self.width() // 2, self.width() // 2]))
+ left_pane_widget =QWidget ()
+ left_layout =QVBoxLayout (left_pane_widget )
+ left_header_label =QLabel (self ._tr ("history_last_downloaded_header","Last 3 Files Downloaded:"))
+ left_header_label .setAlignment (Qt .AlignCenter )
+ left_layout .addWidget (left_header_label )
- def _create_history_pane(self, entries, header_key, header_default, formatter_func, **kwargs):
- """Creates a generic pane for displaying a list of history entries."""
- pane_widget = QWidget()
- layout = QVBoxLayout(pane_widget)
- header_text = self._tr(header_key, header_default).format(**kwargs)
- header_label = QLabel(header_text)
- header_label.setAlignment(Qt.AlignCenter)
- layout.addWidget(header_label)
+ left_scroll_area =QScrollArea ()
+ left_scroll_area .setWidgetResizable (True )
+ left_scroll_content_widget =QWidget ()
+ left_scroll_layout =QVBoxLayout (left_scroll_content_widget )
- scroll_area = QScrollArea()
- scroll_area.setWidgetResizable(True)
- scroll_content_widget = QWidget()
- scroll_layout = QVBoxLayout(scroll_content_widget)
+ if not self .last_3_downloaded_entries :
+ no_left_history_label =QLabel (self ._tr ("no_download_history_header","No Downloads Yet"))
+ no_left_history_label .setAlignment (Qt .AlignCenter )
+ left_scroll_layout .addWidget (no_left_history_label )
+ else :
+ for entry in self .last_3_downloaded_entries :
+ group_box =QGroupBox (f"{self ._tr ('history_file_label','File:')} {entry .get ('disk_filename','N/A')}")
+ group_layout =QVBoxLayout (group_box )
+ details_text =(
+ f"{self ._tr ('history_from_post_label','From Post:')} {entry .get ('post_title','N/A')} (ID: {entry .get ('post_id','N/A')})
"
+ f"{self ._tr ('history_creator_series_label','Creator/Series:')} {entry .get ('creator_display_name','N/A')}
"
+ f"{self ._tr ('history_post_uploaded_label','Post Uploaded:')} {entry .get ('upload_date_str','N/A')}
"
+ f"{self ._tr ('history_file_downloaded_label','File Downloaded:')} {time .strftime ('%Y-%m-%d %H:%M:%S',time .localtime (entry .get ('download_timestamp',0 )))}
"
+ f"{self ._tr ('history_saved_in_folder_label','Saved In Folder:')} {entry .get ('download_path','N/A')}"
+ )
+ details_label =QLabel (details_text )
+ details_label .setWordWrap (True )
+ details_label .setTextFormat (Qt .RichText )
+ group_layout .addWidget (details_label )
+ left_scroll_layout .addWidget (group_box )
+ left_scroll_area .setWidget (left_scroll_content_widget )
+ left_layout .addWidget (left_scroll_area )
+ self .main_splitter .addWidget (left_pane_widget )
- if not entries:
- no_history_label = QLabel(self._tr("no_download_history_header", "No History Yet"))
- no_history_label.setAlignment(Qt.AlignCenter)
- scroll_layout.addWidget(no_history_label)
- else:
- for entry in entries:
- group_box, details_label = formatter_func(entry)
- group_layout = QVBoxLayout(group_box)
- group_layout.addWidget(details_label)
- scroll_layout.addWidget(group_box)
- scroll_area.setWidget(scroll_content_widget)
- layout.addWidget(scroll_area)
- return pane_widget
+ right_pane_widget =QWidget ()
+ right_layout =QVBoxLayout (right_pane_widget )
+ right_header_label =QLabel (self ._tr ("first_files_processed_header","First {count} Posts Processed This Session:").format (count =len (self .first_processed_entries )))
+ right_header_label .setAlignment (Qt .AlignCenter )
+ right_layout .addWidget (right_header_label )
- def _format_last_downloaded_entry(self, entry):
- """Formats a single entry for the 'Last Downloaded Files' pane."""
- group_box = QGroupBox(f"{self._tr('history_file_label', 'File:')} {entry.get('disk_filename', 'N/A')}")
- details_text = (
- f"{self._tr('history_from_post_label', 'From Post:')} {entry.get('post_title', 'N/A')} (ID: {entry.get('post_id', 'N/A')})
"
- f"{self._tr('history_creator_series_label', 'Creator/Series:')} {entry.get('creator_display_name', 'N/A')}
"
- f"{self._tr('history_post_uploaded_label', 'Post Uploaded:')} {entry.get('upload_date_str', 'N/A')}
"
- f"{self._tr('history_file_downloaded_label', 'File Downloaded:')} {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(entry.get('download_timestamp', 0)))}
"
- f"{self._tr('history_saved_in_folder_label', 'Saved In Folder:')} {entry.get('download_path', 'N/A')}"
- )
- details_label = QLabel(details_text)
- details_label.setWordWrap(True)
- details_label.setTextFormat(Qt.RichText)
- return group_box, details_label
+ right_scroll_area =QScrollArea ()
+ right_scroll_area .setWidgetResizable (True )
+ right_scroll_content_widget =QWidget ()
+ right_scroll_layout =QVBoxLayout (right_scroll_content_widget )
- def _format_first_processed_entry(self, entry):
- """Formats a single entry for the 'First Processed Posts' pane."""
- group_box = QGroupBox(f"{self._tr('history_post_label', 'Post:')} {entry.get('post_title', 'N/A')} (ID: {entry.get('post_id', 'N/A')})")
- details_text = (
- f"{self._tr('history_creator_label', 'Creator:')} {entry.get('creator_name', 'N/A')}
"
- f"{self._tr('history_top_file_label', 'Top File:')} {entry.get('top_file_name', 'N/A')}
"
- f"{self._tr('history_num_files_label', 'Num Files in Post:')} {entry.get('num_files', 0)}
"
- f"{self._tr('history_post_uploaded_label', 'Post Uploaded:')} {entry.get('upload_date_str', 'N/A')}
"
- f"{self._tr('history_processed_on_label', 'Processed On:')} {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(entry.get('download_date_timestamp', 0)))}
"
- f"{self._tr('history_saved_to_folder_label', 'Saved To Folder:')} {entry.get('download_location', 'N/A')}"
- )
- details_label = QLabel(details_text)
- details_label.setWordWrap(True)
- details_label.setTextFormat(Qt.RichText)
- return group_box, details_label
+ if not self .first_processed_entries :
+ no_right_history_label =QLabel (self ._tr ("no_processed_history_header","No Posts Processed Yet"))
+ no_right_history_label .setAlignment (Qt .AlignCenter )
+ right_scroll_layout .addWidget (no_right_history_label )
+ else :
+ for entry in self .first_processed_entries :
- def _tr(self, key, default_text=""):
- """Helper to get translation based on the main application's current language."""
- if callable(get_translation) and self.parent_app:
- return get_translation(self.parent_app.current_selected_language, key, default_text)
- return default_text
+ group_box =QGroupBox (f"{self ._tr ('history_post_label','Post:')} {entry .get ('post_title','N/A')} (ID: {entry .get ('post_id','N/A')})")
+ group_layout =QVBoxLayout (group_box )
+ details_text =(
+ f"{self ._tr ('history_creator_label','Creator:')} {entry .get ('creator_name','N/A')}
"
+ f"{self ._tr ('history_top_file_label','Top File:')} {entry .get ('top_file_name','N/A')}
"
+ f"{self ._tr ('history_num_files_label','Num Files in Post:')} {entry .get ('num_files',0 )}
"
+ f"{self ._tr ('history_post_uploaded_label','Post Uploaded:')} {entry .get ('upload_date_str','N/A')}
"
+ f"{self ._tr ('history_processed_on_label','Processed On:')} {time .strftime ('%Y-%m-%d %H:%M:%S',time .localtime (entry .get ('download_date_timestamp',0 )))}
"
+ f"{self ._tr ('history_saved_to_folder_label','Saved To Folder:')} {entry .get ('download_location','N/A')}"
+ )
+ details_label =QLabel (details_text )
+ details_label .setWordWrap (True )
+ details_label .setTextFormat (Qt .RichText )
+ group_layout .addWidget (details_label )
+ right_scroll_layout .addWidget (group_box )
+ right_scroll_area .setWidget (right_scroll_content_widget )
+ right_layout .addWidget (right_scroll_area )
+ self .main_splitter .addWidget (right_pane_widget )
- def _retranslate_ui(self):
- """Sets the text for all translatable UI elements."""
- self.setWindowTitle(self._tr("download_history_dialog_title_combined", "Download History"))
- self.save_history_button.setText(self._tr("history_save_button_text", "Save History to .txt"))
- def _apply_theme(self):
- """Applies the current theme from the parent application."""
- if self.parent_app and hasattr(self.parent_app, 'get_dark_theme') and self.parent_app.current_theme == "dark":
- self.setStyleSheet(self.parent_app.get_dark_theme())
+ QTimer .singleShot (0 ,lambda :self .main_splitter .setSizes ([self .width ()//2 ,self .width ()//2 ]))
- def _save_history_to_txt(self):
- """Saves the displayed history content to a user-selected text file."""
- if not self.last_3_downloaded_entries and not self.first_processed_entries:
- QMessageBox.information(
- self,
- self._tr("no_download_history_header", "No History Yet"),
- self._tr("history_nothing_to_save_message", "There is no history to save.")
- )
- return
- # Suggest saving in the main download directory or Documents as a fallback
- main_download_dir = self.parent_app.dir_input.text().strip()
- default_save_dir = ""
- if main_download_dir and os.path.isdir(main_download_dir):
- default_save_dir = main_download_dir
- else:
- default_save_dir = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) or self.parent_app.app_base_dir
+ bottom_button_layout =QHBoxLayout ()
+ self .save_history_button =QPushButton (self ._tr ("history_save_button_text","Save History to .txt"))
+ self .save_history_button .clicked .connect (self ._save_history_to_txt )
+ bottom_button_layout .addStretch (1 )
+ bottom_button_layout .addWidget (self .save_history_button )
- default_filepath = os.path.join(default_save_dir, "download_history.txt")
+ dialog_layout .addLayout (bottom_button_layout )
- filepath, _ = QFileDialog.getSaveFileName(
- self,
- self._tr("history_save_dialog_title", "Save Download History"),
- default_filepath,
- "Text Files (*.txt);;All Files (*)"
+ if self .parent_app and hasattr (self .parent_app ,'get_dark_theme')and self .parent_app .current_theme =="dark":
+ self .setStyleSheet (self .parent_app .get_dark_theme ())
+
+ def _tr (self ,key ,default_text =""):
+ if callable (get_translation )and self .parent_app :
+ return get_translation (self .parent_app .current_selected_language ,key ,default_text )
+ return default_text
+
+ def _save_history_to_txt (self ):
+ if not self .last_3_downloaded_entries and not self .first_processed_entries :
+ QMessageBox .information (self ,self ._tr ("no_download_history_header","No Downloads Yet"),
+ self ._tr ("history_nothing_to_save_message","There is no history to save."))
+ return
+
+ main_download_dir =self .parent_app .dir_input .text ().strip ()
+ default_save_dir =""
+ if main_download_dir and os .path .isdir (main_download_dir ):
+ default_save_dir =main_download_dir
+ else :
+ fallback_dir =QStandardPaths .writableLocation (QStandardPaths .DocumentsLocation )
+ if fallback_dir and os .path .isdir (fallback_dir ):
+ default_save_dir =fallback_dir
+ else :
+ default_save_dir =self .parent_app .app_base_dir
+
+ default_filepath =os .path .join (default_save_dir ,"download_history.txt")
+
+ filepath ,_ =QFileDialog .getSaveFileName (
+ self ,self ._tr ("history_save_dialog_title","Save Download History"),
+ default_filepath ,"Text Files (*.txt);;All Files (*)"
)
- if not filepath:
- return
+ if not filepath :
+ return
- # Build the text content
- history_content = []
- # ... logic for formatting the text content would go here ...
+ history_content =[]
+ history_content .append (f"{self ._tr ('history_last_downloaded_header','Last 3 Files Downloaded:')}\n")
+ if self .last_3_downloaded_entries :
+ for entry in self .last_3_downloaded_entries :
+ history_content .append (f" {self ._tr ('history_file_label','File:')} {entry .get ('disk_filename','N/A')}")
+ history_content .append (f" {self ._tr ('history_from_post_label','From Post:')} {entry .get ('post_title','N/A')} (ID: {entry .get ('post_id','N/A')})")
+ history_content .append (f" {self ._tr ('history_creator_series_label','Creator/Series:')} {entry .get ('creator_display_name','N/A')}")
+ history_content .append (f" {self ._tr ('history_post_uploaded_label','Post Uploaded:')} {entry .get ('upload_date_str','N/A')}")
+ history_content .append (f" {self ._tr ('history_file_downloaded_label','File Downloaded:')} {time .strftime ('%Y-%m-%d %H:%M:%S',time .localtime (entry .get ('download_timestamp',0 )))}")
+ history_content .append (f" {self ._tr ('history_saved_in_folder_label','Saved In Folder:')} {entry .get ('download_path','N/A')}\n")
+ else :
+ history_content .append (f" ({self ._tr ('no_download_history_header','No Downloads Yet')})\n")
- try:
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write("\n".join(history_content))
- QMessageBox.information(
- self,
- self._tr("history_export_success_title", "History Export Successful"),
- self._tr("history_export_success_message", "Successfully exported to:\n{filepath}").format(filepath=filepath)
- )
- except Exception as e:
- QMessageBox.critical(
- self,
- self._tr("history_export_error_title", "History Export Error"),
- self._tr("history_export_error_message", "Could not export: {error}").format(error=str(e))
- )
+ history_content .append (f"\n{self ._tr ('first_files_processed_header','First {count} Posts Processed This Session:').format (count =len (self .first_processed_entries ))}\n")
+ if self .first_processed_entries :
+ for entry in self .first_processed_entries :
+ history_content .append (f" {self ._tr ('history_post_label','Post:')} {entry .get ('post_title','N/A')} (ID: {entry .get ('post_id','N/A')})")
+ history_content .append (f" {self ._tr ('history_creator_label','Creator:')} {entry .get ('creator_name','N/A')}")
+ history_content .append (f" {self ._tr ('history_top_file_label','Top File:')} {entry .get ('top_file_name','N/A')}")
+ history_content .append (f" {self ._tr ('history_num_files_label','Num Files in Post:')} {entry .get ('num_files',0 )}")
+ history_content .append (f" {self ._tr ('history_post_uploaded_label','Post Uploaded:')} {entry .get ('upload_date_str','N/A')}")
+ history_content .append (f" {self ._tr ('history_processed_on_label','Processed On:')} {time .strftime ('%Y-%m-%d %H:%M:%S',time .localtime (entry .get ('download_date_timestamp',0 )))}")
+ history_content .append (f" {self ._tr ('history_saved_to_folder_label','Saved To Folder:')} {entry .get ('download_location','N/A')}\n")
+ else :
+ history_content .append (f" ({self ._tr ('no_processed_history_header','No Posts Processed Yet')})\n")
+
+ try :
+ with open (filepath ,'w',encoding ='utf-8')as f :
+ f .write ("\n".join (history_content ))
+ QMessageBox .information (self ,self ._tr ("history_export_success_title","History Export Successful"),
+ self ._tr ("history_export_success_message","Successfully exported download history to:\n{filepath}").format (filepath =filepath ))
+ except Exception as e :
+ QMessageBox .critical (self ,self ._tr ("history_export_error_title","History Export Error"),
+ self ._tr ("history_export_error_message","Could not export download history: {error}").format (error =str (e )))
\ No newline at end of file
diff --git a/src/ui/main_window.py b/src/ui/main_window.py
index e26dc66..f6b06b0 100644
--- a/src/ui/main_window.py
+++ b/src/ui/main_window.py
@@ -4825,124 +4825,254 @@ class DownloaderApp (QWidget ):
self .log_verbosity_toggle_button .setToolTip ("Current View: Progress Log. Click to switch to Missed Character Log.")
if self .progress_log_label :self .progress_log_label .setText (self ._tr ("progress_log_label_text","📜 Progress Log:"))
- def reset_application_state (self ):
- if self ._is_download_active ():QMessageBox .warning (self ,"Reset Error","Cannot reset while a download is in progress. Please cancel first.");return
- self .log_signal .emit ("🔄 Resetting application state to defaults...");self ._reset_ui_to_defaults ()
- self .main_log_output .clear ();self .external_log_output .clear ()
- if self .missed_character_log_output :self .missed_character_log_output .clear ()
-
- self .current_log_view ='progress'
- if self .log_view_stack :self .log_view_stack .setCurrentIndex (0 )
- if self .progress_log_label :self .progress_log_label .setText (self ._tr ("progress_log_label_text","📜 Progress Log:"))
- if self .log_verbosity_toggle_button :
- self .log_verbosity_toggle_button .setText (self .EYE_ICON )
- self .log_verbosity_toggle_button .setToolTip ("Current View: Progress Log. Click to switch to Missed Character Log.")
-
- if self .show_external_links and not (self .radio_only_links and self .radio_only_links .isChecked ()):self .external_log_output .append ("🔗 External Links Found:")
- self .external_link_queue .clear ();self .extracted_links_cache =[];self ._is_processing_external_link_queue =False ;self ._current_link_post_title =None
- self .progress_label .setText (self ._tr ("progress_idle_text","Progress: Idle"));self .file_progress_label .setText ("")
- with self .downloaded_files_lock :count =len (self .downloaded_files );self .downloaded_files .clear ();
- self .missed_title_key_terms_count .clear ()
- self .missed_title_key_terms_examples .clear ()
- self .logged_summary_for_key_term .clear ()
- self .already_logged_bold_key_terms .clear ()
- self .missed_key_terms_buffer .clear ()
- self .favorite_download_queue .clear ()
- self .only_links_log_display_mode =LOG_DISPLAY_LINKS
- self .mega_download_log_preserved_once =False
- self .permanently_failed_files_for_dialog .clear ()
- self .favorite_download_scope =FAVORITE_SCOPE_SELECTED_LOCATION
- self ._update_favorite_scope_button_text ()
- self .retryable_failed_files_info .clear ()
- self .cancellation_message_logged_this_session =False
- self .is_processing_favorites_queue =False
-
- if count >0 :self .log_signal .emit (f" Cleared {count } downloaded filename(s) from session memory.")
- with self .downloaded_file_hashes_lock :count =len (self .downloaded_file_hashes );self .downloaded_file_hashes .clear ();
- if count >0 :self .log_signal .emit (f" Cleared {count } downloaded file hash(es) from session memory.")
-
- self .total_posts_to_process =0 ;self .processed_posts_count =0 ;self .download_counter =0 ;self .skip_counter =0
- self .all_kept_original_filenames =[]
- self .cancellation_event .clear ()
- if self .pause_event :self .pause_event .clear ()
- self .is_paused =False
- self .manga_filename_style =STYLE_POST_TITLE
- self .settings .setValue (MANGA_FILENAME_STYLE_KEY ,self .manga_filename_style )
-
- self .skip_words_scope =SKIP_SCOPE_POSTS
- self .settings .setValue (SKIP_WORDS_SCOPE_KEY ,self .skip_words_scope )
- self ._update_skip_scope_button_text ()
-
- self .char_filter_scope =CHAR_SCOPE_TITLE
- self ._update_char_filter_scope_button_text ()
-
- self .settings .sync ()
- self ._update_manga_filename_style_button_text ()
- self .update_ui_for_manga_mode (self .manga_mode_checkbox .isChecked ()if self .manga_mode_checkbox else False )
-
- def _reset_ui_to_defaults (self ):
- self .link_input .clear ();self .dir_input .clear ();self .custom_folder_input .clear ();self .character_input .clear ();
- self .skip_words_input .clear ();self .start_page_input .clear ();self .end_page_input .clear ();self .new_char_input .clear ();
- if hasattr (self ,'remove_from_filename_input'):self .remove_from_filename_input .clear ()
- self .character_search_input .clear ();self .thread_count_input .setText ("4");self .radio_all .setChecked (True );
- self .skip_zip_checkbox .setChecked (True );self .skip_rar_checkbox .setChecked (True );self .download_thumbnails_checkbox .setChecked (False );
- self .compress_images_checkbox .setChecked (False );self .use_subfolders_checkbox .setChecked (True );
- self .use_subfolder_per_post_checkbox .setChecked (False );self .use_multithreading_checkbox .setChecked (True );
- if self .favorite_mode_checkbox :self .favorite_mode_checkbox .setChecked (False )
- self .external_links_checkbox .setChecked (False )
- if self .manga_mode_checkbox :self .manga_mode_checkbox .setChecked (False )
- if hasattr (self ,'use_cookie_checkbox'):self .use_cookie_checkbox .setChecked (False )
- self .selected_cookie_filepath =None
-
- if hasattr (self ,'cookie_text_input'):self .cookie_text_input .clear ()
- self .missed_title_key_terms_count .clear ()
- self .missed_title_key_terms_examples .clear ()
- self .logged_summary_for_key_term .clear ()
- self .already_logged_bold_key_terms .clear ()
- if hasattr (self ,'manga_date_prefix_input'):self .manga_date_prefix_input .clear ()
- if self .pause_event :self .pause_event .clear ()
- self .is_paused =False
- self .missed_key_terms_buffer .clear ()
- if self .download_extracted_links_button :
- self .only_links_log_display_mode =LOG_DISPLAY_LINKS
- self .cancellation_message_logged_this_session =False
- self .mega_download_log_preserved_once =False
- self .download_extracted_links_button .setEnabled (False )
-
- if self .missed_character_log_output :self .missed_character_log_output .clear ()
-
- self .permanently_failed_files_for_dialog .clear ()
- self .allow_multipart_download_setting =False
- self ._update_multipart_toggle_button_text ()
-
- self .skip_words_scope =SKIP_SCOPE_POSTS
- self ._update_skip_scope_button_text ()
- self .char_filter_scope =CHAR_SCOPE_TITLE
- self ._update_char_filter_scope_button_text ()
-
- self .current_log_view ='progress'
- self ._update_cookie_input_visibility (False );self ._update_cookie_input_placeholders_and_tooltips ()
- if self .log_view_stack :self .log_view_stack .setCurrentIndex (0 )
- if self .progress_log_label :self .progress_log_label .setText ("📜 Progress Log:")
- if self .progress_log_label :self .progress_log_label .setText (self ._tr ("progress_log_label_text","📜 Progress Log:"))
- self ._handle_filter_mode_change (self .radio_all ,True )
- self ._handle_multithreading_toggle (self .use_multithreading_checkbox .isChecked ())
- self .filter_character_list ("")
-
- self .download_btn .setEnabled (True );self .cancel_btn .setEnabled (False )
- if self .reset_button :self .reset_button .setEnabled (True );self .reset_button .setText (self ._tr ("reset_button_text","🔄 Reset"));self .reset_button .setToolTip (self ._tr ("reset_button_tooltip","Reset all inputs and logs to default state (only when idle)."))
- if self .log_verbosity_toggle_button :
- self .log_verbosity_toggle_button .setText (self .EYE_ICON )
- self .log_verbosity_toggle_button .setToolTip ("Current View: Progress Log. Click to switch to Missed Character Log.")
- self ._update_manga_filename_style_button_text ()
- self .update_ui_for_manga_mode (False )
- if hasattr (self ,'favorite_mode_checkbox'):
- self ._handle_favorite_mode_toggle (False )
- if hasattr (self ,'scan_content_images_checkbox'):
- self .scan_content_images_checkbox .setChecked (False )
- if hasattr (self ,'download_thumbnails_checkbox'):
- self ._handle_thumbnail_mode_change (self .download_thumbnails_checkbox .isChecked ())
+ def reset_application_state(self):
+ # --- Stop all background tasks and threads ---
+ if self._is_download_active():
+ # Try to cancel download thread
+ if self.download_thread and self.download_thread.isRunning():
+ self.log_signal.emit("⚠️ Cancelling active download thread for reset...")
+ self.cancellation_event.set()
+ self.download_thread.requestInterruption()
+ self.download_thread.wait(3000)
+ if self.download_thread.isRunning():
+ self.log_signal.emit(" ⚠️ Download thread did not terminate gracefully.")
+ self.download_thread.deleteLater()
+ self.download_thread = None
+
+ # Try to cancel thread pool
+ if self.thread_pool:
+ self.log_signal.emit(" Shutting down thread pool for reset...")
+ self.thread_pool.shutdown(wait=True, cancel_futures=True)
+ self.thread_pool = None
+ self.active_futures = []
+
+ # Try to cancel external link download thread
+ if self.external_link_download_thread and self.external_link_download_thread.isRunning():
+ self.log_signal.emit(" Cancelling external link download thread for reset...")
+ self.external_link_download_thread.cancel()
+ self.external_link_download_thread.wait(3000)
+ self.external_link_download_thread.deleteLater()
+ self.external_link_download_thread = None
+
+ # Try to cancel retry thread pool
+ if hasattr(self, 'retry_thread_pool') and self.retry_thread_pool:
+ self.log_signal.emit(" Shutting down retry thread pool for reset...")
+ self.retry_thread_pool.shutdown(wait=True)
+ self.retry_thread_pool = None
+ if hasattr(self, 'active_retry_futures'):
+ self.active_retry_futures.clear()
+ if hasattr(self, 'active_retry_futures_map'):
+ self.active_retry_futures_map.clear()
+
+ self.cancellation_event.clear()
+ if self.pause_event:
+ self.pause_event.clear()
+ self.is_paused = False
+
+ # --- Reset UI and all state ---
+ self.log_signal.emit("🔄 Resetting application state to defaults...")
+ self._reset_ui_to_defaults()
+ self.main_log_output.clear()
+ self.external_log_output.clear()
+ if self.missed_character_log_output:
+ self.missed_character_log_output.clear()
+
+ self.current_log_view = 'progress'
+ if self.log_view_stack:
+ self.log_view_stack.setCurrentIndex(0)
+ if self.progress_log_label:
+ self.progress_log_label.setText(self._tr("progress_log_label_text", "📜 Progress Log:"))
+ if self.log_verbosity_toggle_button:
+ self.log_verbosity_toggle_button.setText(self.EYE_ICON)
+ self.log_verbosity_toggle_button.setToolTip("Current View: Progress Log. Click to switch to Missed Character Log.")
+
+ # Clear all download-related state
+ self.external_link_queue.clear()
+ self.extracted_links_cache = []
+ self._is_processing_external_link_queue = False
+ self._current_link_post_title = None
+ self.progress_label.setText(self._tr("progress_idle_text", "Progress: Idle"))
+ self.file_progress_label.setText("")
+ with self.downloaded_files_lock:
+ self.downloaded_files.clear()
+ with self.downloaded_file_hashes_lock:
+ self.downloaded_file_hashes.clear()
+ self.missed_title_key_terms_count.clear()
+ self.missed_title_key_terms_examples.clear()
+ self.logged_summary_for_key_term.clear()
+ self.already_logged_bold_key_terms.clear()
+ self.missed_key_terms_buffer.clear()
+ self.favorite_download_queue.clear()
+ self.only_links_log_display_mode = LOG_DISPLAY_LINKS
+ self.mega_download_log_preserved_once = False
+ self.permanently_failed_files_for_dialog.clear()
+ self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION
+ self._update_favorite_scope_button_text()
+ self.retryable_failed_files_info.clear()
+ self.cancellation_message_logged_this_session = False
+ self.is_processing_favorites_queue = False
+ self.total_posts_to_process = 0
+ self.processed_posts_count = 0
+ self.download_counter = 0
+ self.skip_counter = 0
+ self.all_kept_original_filenames = []
+ self.is_paused = False
+ self.is_fetcher_thread_running = False
+ self.interrupted_session_data = None
+ self.is_restore_pending = False
+
+ self.settings.setValue(MANGA_FILENAME_STYLE_KEY, self.manga_filename_style)
+ self.settings.setValue(SKIP_WORDS_SCOPE_KEY, self.skip_words_scope)
+ self.settings.sync()
+ self._update_manga_filename_style_button_text()
+ self.update_ui_for_manga_mode(self.manga_mode_checkbox.isChecked() if self.manga_mode_checkbox else False)
+
+ self.set_ui_enabled(True)
+ self.log_signal.emit("✅ Application fully reset. Ready for new download.")
+ self.is_processing_favorites_queue = False
+ self.current_processing_favorite_item_info = None
+ self.favorite_download_queue.clear()
+ self.interrupted_session_data = None
+ self.is_restore_pending = False
+ self.last_link_input_text_for_queue_sync = ""
+ # Replace your current reset_application_state with the above.
+ def _reset_ui_to_defaults(self):
+ """Resets all UI elements and relevant state to their default values."""
+ # Clear all text fields
+ self.link_input.clear()
+ self.dir_input.clear()
+ self.custom_folder_input.clear()
+ self.character_input.clear()
+ self.skip_words_input.clear()
+ self.start_page_input.clear()
+ self.end_page_input.clear()
+ self.new_char_input.clear()
+ if hasattr(self, 'remove_from_filename_input'):
+ self.remove_from_filename_input.clear()
+ self.character_search_input.clear()
+ self.thread_count_input.setText("4")
+ if hasattr(self, 'manga_date_prefix_input'):
+ self.manga_date_prefix_input.clear()
+
+ # Set radio buttons and checkboxes to defaults
+ self.radio_all.setChecked(True)
+ self.skip_zip_checkbox.setChecked(True)
+ self.skip_rar_checkbox.setChecked(True)
+ self.download_thumbnails_checkbox.setChecked(False)
+ self.compress_images_checkbox.setChecked(False)
+ self.use_subfolders_checkbox.setChecked(True)
+ self.use_subfolder_per_post_checkbox.setChecked(False)
+ self.use_multithreading_checkbox.setChecked(True)
+ if self.favorite_mode_checkbox:
+ self.favorite_mode_checkbox.setChecked(False)
+ self.external_links_checkbox.setChecked(False)
+ if self.manga_mode_checkbox:
+ self.manga_mode_checkbox.setChecked(False)
+ if hasattr(self, 'use_cookie_checkbox'):
+ self.use_cookie_checkbox.setChecked(False)
+ self.selected_cookie_filepath = None
+ if hasattr(self, 'cookie_text_input'):
+ self.cookie_text_input.clear()
+
+ # Reset log and progress displays
+ if self.main_log_output:
+ self.main_log_output.clear()
+ if self.external_log_output:
+ self.external_log_output.clear()
+ if self.missed_character_log_output:
+ self.missed_character_log_output.clear()
+ self.progress_label.setText(self._tr("progress_idle_text", "Progress: Idle"))
+ self.file_progress_label.setText("")
+
+ # Reset internal state
+ self.missed_title_key_terms_count.clear()
+ self.missed_title_key_terms_examples.clear()
+ self.logged_summary_for_key_term.clear()
+ self.already_logged_bold_key_terms.clear()
+ self.missed_key_terms_buffer.clear()
+ self.permanently_failed_files_for_dialog.clear()
+ self.only_links_log_display_mode = LOG_DISPLAY_LINKS
+ self.cancellation_message_logged_this_session = False
+ self.mega_download_log_preserved_once = False
+ self.allow_multipart_download_setting = False
+ self.skip_words_scope = SKIP_SCOPE_POSTS
+ self.char_filter_scope = CHAR_SCOPE_TITLE
+ self.manga_filename_style = STYLE_POST_TITLE
+ self.favorite_download_scope = FAVORITE_SCOPE_SELECTED_LOCATION
+ self._update_skip_scope_button_text()
+ self._update_char_filter_scope_button_text()
+ self._update_manga_filename_style_button_text()
+ self._update_multipart_toggle_button_text()
+ self._update_favorite_scope_button_text()
+ self.current_log_view = 'progress'
+ self.is_paused = False
+ if self.pause_event:
+ self.pause_event.clear()
+
+ # Reset extracted/external links state
+ self.external_link_queue.clear()
+ self.extracted_links_cache = []
+ self._is_processing_external_link_queue = False
+ self._current_link_post_title = None
+ if self.download_extracted_links_button:
+ self.download_extracted_links_button.setEnabled(False)
+
+ # Reset favorite/queue/session state
+ self.favorite_download_queue.clear()
+ self.is_processing_favorites_queue = False
+ self.current_processing_favorite_item_info = None
+ self.interrupted_session_data = None
+ self.is_restore_pending = False
+ self.last_link_input_text_for_queue_sync = ""
+ self._update_button_states_and_connections()
+ # Reset counters and progress
+ self.total_posts_to_process = 0
+ self.processed_posts_count = 0
+ self.download_counter = 0
+ self.skip_counter = 0
+ self.all_kept_original_filenames = []
+
+ # Reset log view and UI state
+ if self.log_view_stack:
+ self.log_view_stack.setCurrentIndex(0)
+ if self.progress_log_label:
+ self.progress_log_label.setText(self._tr("progress_log_label_text", "📜 Progress Log:"))
+ if self.log_verbosity_toggle_button:
+ self.log_verbosity_toggle_button.setText(self.EYE_ICON)
+ self.log_verbosity_toggle_button.setToolTip("Current View: Progress Log. Click to switch to Missed Character Log.")
+
+ # Reset character list filter
+ self.filter_character_list("")
+
+ # Update UI for manga mode and multithreading
+ self._handle_multithreading_toggle(self.use_multithreading_checkbox.isChecked())
+ self.update_ui_for_manga_mode(False)
+ self.update_custom_folder_visibility(self.link_input.text())
+ self.update_page_range_enabled_state()
+ self._update_cookie_input_visibility(False)
+ self._update_cookie_input_placeholders_and_tooltips()
+
+ # Reset button states
+ self.download_btn.setEnabled(True)
+ self.cancel_btn.setEnabled(False)
+ if self.reset_button:
+ self.reset_button.setEnabled(True)
+ self.reset_button.setText(self._tr("reset_button_text", "🔄 Reset"))
+ self.reset_button.setToolTip(self._tr("reset_button_tooltip", "Reset all inputs and logs to default state (only when idle)."))
+
+ # Reset favorite mode UI
+ if hasattr(self, 'favorite_mode_checkbox'):
+ self._handle_favorite_mode_toggle(False)
+ if hasattr(self, 'scan_content_images_checkbox'):
+ self.scan_content_images_checkbox.setChecked(False)
+ if hasattr(self, 'download_thumbnails_checkbox'):
+ self._handle_thumbnail_mode_change(self.download_thumbnails_checkbox.isChecked())
+
+ self.set_ui_enabled(True)
+ self.log_signal.emit("✅ UI reset to defaults. Ready for new operation.")
+ self._update_button_states_and_connections()
+
def _show_feature_guide (self ):
steps_content_keys =[
("help_guide_step1_title","help_guide_step1_content"),