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", "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", + "tour_dialog_step7_content": "Sometimes downloads can run into issues. Here are some of the most common ones:\n", "tour_dialog_step8_title": "⑦ Logs & Final Controls", "tour_dialog_step8_content": "Monitoring and Controls:\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"),