# --- Standard Library Imports --- import json import os import sys import threading import time import unicodedata from collections import defaultdict from urllib.parse import urlparse # --- PyQt5 Imports --- from PyQt5.QtCore import pyqtSignal, QCoreApplication, QSize, QThread, QTimer, Qt from PyQt5.QtWidgets import ( QApplication, QDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout, QAbstractItemView, QSplitter, QProgressBar, QWidget, QFileDialog ) # --- Local Application Imports --- from ...i18n.translator import get_translation from ..main_window import get_app_icon_object from ...core.api_client import download_from_api from ...utils.network_utils import extract_post_info, prepare_cookies_for_request from ...utils.resolution import get_dark_theme class PostsFetcherThread (QThread ): status_update =pyqtSignal (str ) posts_fetched_signal =pyqtSignal (object ,list ) fetch_error_signal =pyqtSignal (object ,str ) finished_signal =pyqtSignal () def __init__ (self ,creators_to_fetch ,parent_dialog_ref ): super ().__init__ () self .creators_to_fetch =creators_to_fetch self .parent_dialog =parent_dialog_ref self .cancellation_flag =threading .Event () def cancel (self ): self .cancellation_flag .set () self .status_update .emit (self .parent_dialog ._tr ("post_fetch_cancelled_status","Post fetching cancellation requested...")) def run (self ): if not self .creators_to_fetch : self .status_update .emit (self .parent_dialog ._tr ("no_creators_to_fetch_status","No creators selected to fetch posts for.")) self .finished_signal .emit () return for creator_data in self .creators_to_fetch : if self .cancellation_flag .is_set (): break creator_name =creator_data .get ('name','Unknown Creator') service =creator_data .get ('service') user_id =creator_data .get ('id') print(f"[DEBUG] Fetching posts for: name={creator_name}, service={service}, user_id={user_id}") if not service or not user_id : self .fetch_error_signal .emit (creator_data ,f"Missing service or ID for {creator_name }") continue self .status_update .emit (self .parent_dialog ._tr ("fetching_posts_for_creator_status_all_pages","Fetching all posts for {creator_name} ({service})... This may take a while.").format (creator_name =creator_name ,service =service )) domain =self .parent_dialog ._get_domain_for_service (service ) api_url_base =f"https://{domain }/api/v1/{service }/user/{user_id }" print(f"[DEBUG] API URL: {api_url_base}") use_cookie_param =False cookie_text_param ="" selected_cookie_file_param =None app_base_dir_param =None if self .parent_dialog .parent_app : app =self .parent_dialog .parent_app use_cookie_param =app .use_cookie_checkbox .isChecked () cookie_text_param =app .cookie_text_input .text ().strip () selected_cookie_file_param =app .selected_cookie_filepath app_base_dir_param =app .app_base_dir all_posts_for_this_creator =[] try : post_generator =download_from_api ( api_url_base , logger =lambda msg :self .status_update .emit (f"[API Fetch - {creator_name }] {msg }"), use_cookie =use_cookie_param , cookie_text =cookie_text_param , selected_cookie_file =selected_cookie_file_param , app_base_dir =app_base_dir_param , manga_filename_style_for_sort_check =None , cancellation_event =self .cancellation_flag ) for posts_batch in post_generator : if self .cancellation_flag .is_set (): self .status_update .emit (f"Post fetching for {creator_name } cancelled during pagination.") break all_posts_for_this_creator .extend (posts_batch ) print(f"[DEBUG] Fetched batch: {len(posts_batch)} posts, total so far: {len(all_posts_for_this_creator)}") self .status_update .emit (f"Fetched {len (all_posts_for_this_creator )} posts so far for {creator_name }...") print(f"[DEBUG] Finished fetching for {creator_name}: {len(all_posts_for_this_creator)} posts total.") if not self .cancellation_flag .is_set (): self .posts_fetched_signal .emit (creator_data ,all_posts_for_this_creator ) self .status_update .emit (f"Finished fetching {len (all_posts_for_this_creator )} posts for {creator_name }.") else : self .posts_fetched_signal .emit (creator_data ,all_posts_for_this_creator ) self .status_update .emit (f"Fetching for {creator_name } cancelled. {len (all_posts_for_this_creator )} posts collected.") except RuntimeError as e : print(f"[DEBUG] RuntimeError for {creator_name}: {e}") if "cancelled by user"in str (e ).lower ()or self .cancellation_flag .is_set (): self .status_update .emit (f"Post fetching for {creator_name } cancelled: {e }") self .posts_fetched_signal .emit (creator_data ,all_posts_for_this_creator ) else : self .fetch_error_signal .emit (creator_data ,f"Runtime error fetching posts for {creator_name }: {e }") except Exception as e : print(f"[DEBUG] Exception for {creator_name}: {e}") self .fetch_error_signal .emit (creator_data ,f"Error fetching posts for {creator_name }: {e }") if self .cancellation_flag .is_set (): break QThread .msleep (200 ) if self .cancellation_flag .is_set (): self .status_update .emit (self .parent_dialog ._tr ("post_fetch_cancelled_status_done","Post fetching cancelled.")) else : self .status_update .emit (self .parent_dialog ._tr ("post_fetch_finished_status","Finished fetching posts for selected creators.")) self .finished_signal .emit () class EmptyPopupDialog (QDialog ): """A simple empty popup dialog.""" SCOPE_CHARACTERS ="Characters" INITIAL_LOAD_LIMIT =200 SCOPE_CREATORS ="Creators" def __init__ (self ,app_base_dir ,parent_app_ref ,parent =None ): super ().__init__ (parent ) self.parent_app = parent_app_ref scale_factor = getattr(self.parent_app, 'scale_factor', 1.0) self.setMinimumSize(int(400 * scale_factor), int(300 * scale_factor)) self.current_scope_mode = self.SCOPE_CREATORS self .app_base_dir =app_base_dir app_icon =get_app_icon_object () if app_icon and not app_icon .isNull (): self .setWindowIcon (app_icon ) self.update_profile_data = None self.update_creator_name = None self .selected_creators_for_queue =[] self .globally_selected_creators ={} self .fetched_posts_data ={} self .post_fetch_thread =None self .TITLE_COLUMN_WIDTH_FOR_POSTS =70 self .globally_selected_post_ids =set () self ._is_scrolling_titles =False self ._is_scrolling_dates =False dialog_layout =QHBoxLayout (self ) self .setLayout (dialog_layout ) self .left_pane_widget =QWidget () left_pane_layout =QVBoxLayout (self .left_pane_widget ) search_fetch_layout =QHBoxLayout () self .search_input =QLineEdit () self .search_input .textChanged .connect (self ._filter_list ) search_fetch_layout .addWidget (self .search_input ,1 ) self .fetch_posts_button =QPushButton () self .fetch_posts_button .setEnabled (False ) self .fetch_posts_button .clicked .connect (self ._handle_fetch_posts_click ) search_fetch_layout .addWidget (self .fetch_posts_button ) left_pane_layout .addLayout (search_fetch_layout ) self .progress_bar =QProgressBar () self .progress_bar .setRange (0 ,0 ) self .progress_bar .setTextVisible (False ) self .progress_bar .setVisible (False ) left_pane_layout .addWidget (self .progress_bar ) self .list_widget =QListWidget () self .list_widget .itemChanged .connect (self ._handle_item_check_changed ) left_pane_layout .addWidget (self .list_widget ) left_bottom_buttons_layout =QHBoxLayout () self .add_selected_button =QPushButton () self .add_selected_button .setToolTip ( "Add Selected Creators to URL Input\n\n" "Adds the names of all checked creators to the main URL input field,\n" "comma-separated, and closes this dialog." ) self .add_selected_button .clicked .connect (self ._handle_add_selected ) self .add_selected_button .setDefault (True ) left_bottom_buttons_layout .addWidget (self .add_selected_button ) self .scope_button =QPushButton () self .scope_button .clicked .connect (self ._toggle_scope_mode ) left_bottom_buttons_layout .addWidget (self .scope_button ) left_pane_layout .addLayout (left_bottom_buttons_layout ) self.update_button = QPushButton() self.update_button.clicked.connect(self._handle_update_check) left_bottom_buttons_layout.addWidget(self.update_button) self .right_pane_widget =QWidget () right_pane_layout =QVBoxLayout (self .right_pane_widget ) self .posts_area_title_label =QLabel ("Fetched Posts") self .posts_area_title_label .setAlignment (Qt .AlignCenter ) right_pane_layout .addWidget (self .posts_area_title_label ) self .posts_search_input =QLineEdit () self .posts_search_input .setVisible (False ) self .posts_search_input .textChanged .connect (self ._filter_fetched_posts_list ) right_pane_layout .addWidget (self .posts_search_input ) posts_headers_layout =QHBoxLayout () self .posts_title_header_label =QLabel () self .posts_title_header_label .setStyleSheet ("font-weight: bold; padding-left: 20px;") posts_headers_layout .addWidget (self .posts_title_header_label ,7 ) self .posts_date_header_label =QLabel () self .posts_date_header_label .setStyleSheet ("font-weight: bold;") posts_headers_layout .addWidget (self .posts_date_header_label ,3 ) right_pane_layout .addLayout (posts_headers_layout ) self .posts_content_splitter =QSplitter (Qt .Horizontal ) self .posts_title_list_widget =QListWidget () self .posts_title_list_widget .itemChanged .connect (self ._handle_post_item_check_changed ) self .posts_title_list_widget .setAlternatingRowColors (True ) self .posts_content_splitter .addWidget (self .posts_title_list_widget ) self .posts_date_list_widget =QListWidget () self .posts_date_list_widget .setSelectionMode (QAbstractItemView .NoSelection ) self .posts_date_list_widget .setAlternatingRowColors (True ) self .posts_date_list_widget .setHorizontalScrollBarPolicy (Qt .ScrollBarAlwaysOff ) self .posts_content_splitter .addWidget (self .posts_date_list_widget ) right_pane_layout .addWidget (self .posts_content_splitter ,1 ) posts_buttons_top_layout =QHBoxLayout () self .posts_select_all_button =QPushButton () self .posts_select_all_button .clicked .connect (self ._handle_posts_select_all ) posts_buttons_top_layout .addWidget (self .posts_select_all_button ) self .posts_deselect_all_button =QPushButton () self .posts_deselect_all_button .clicked .connect (self ._handle_posts_deselect_all ) posts_buttons_top_layout .addWidget (self .posts_deselect_all_button ) right_pane_layout .addLayout (posts_buttons_top_layout ) posts_buttons_bottom_layout =QHBoxLayout () self .posts_add_selected_button =QPushButton () self .posts_add_selected_button .clicked .connect (self ._handle_posts_add_selected_to_queue ) posts_buttons_bottom_layout .addWidget (self .posts_add_selected_button ) self .posts_close_button =QPushButton () self .posts_close_button .clicked .connect (self ._handle_posts_close_view ) posts_buttons_bottom_layout .addWidget (self .posts_close_button ) right_pane_layout .addLayout (posts_buttons_bottom_layout ) self .right_pane_widget .hide () self .main_splitter =QSplitter (Qt .Horizontal ) self .main_splitter .addWidget (self .left_pane_widget ) self .main_splitter .addWidget (self .right_pane_widget ) self .main_splitter .setCollapsible (0 ,False ) self .main_splitter .setCollapsible (1 ,True ) self .posts_title_list_widget .verticalScrollBar ().valueChanged .connect (self ._sync_scroll_dates ) self .posts_date_list_widget .verticalScrollBar ().valueChanged .connect (self ._sync_scroll_titles ) dialog_layout .addWidget (self .main_splitter ) self .original_size =self .sizeHint () self .main_splitter .setSizes ([int (self .width ()*scale_factor ),0 ]) self ._retranslate_ui () if self.parent_app and self.parent_app.current_theme == "dark": # Get the scale factor from the parent app scale = getattr(self.parent_app, 'scale_factor', 1) # Call the imported function with the correct scale self.setStyleSheet(get_dark_theme(scale)) else: # Explicitly set a blank stylesheet for light mode self.setStyleSheet("") self .resize (int ((self .original_size .width ()+50 )*scale_factor ),int ((self .original_size .height ()+100 )*scale_factor )) QTimer .singleShot (0 ,self ._perform_initial_load ) def _center_on_screen (self ): """Centers the dialog on the parent's screen or the primary screen.""" if self .parent_app : parent_rect =self .parent_app .frameGeometry () self .move (parent_rect .center ()-self .rect ().center ()) else : try : screen_geo =QApplication .primaryScreen ().availableGeometry () self .move (screen_geo .center ()-self .rect ().center ()) except AttributeError : pass def _handle_update_check(self): """Opens a dialog to select a creator profile and loads it for an update session.""" appdata_dir = os.path.join(self.app_base_dir, "appdata") profiles_dir = os.path.join(appdata_dir, "creator_profiles") if not os.path.isdir(profiles_dir): QMessageBox.warning(self, "Directory Not Found", f"The creator profiles directory does not exist yet.\n\nPath: {profiles_dir}") return filepath, _ = QFileDialog.getOpenFileName(self, "Select Creator Profile for Update", profiles_dir, "JSON Files (*.json)") if filepath: try: with open(filepath, 'r', encoding='utf-8') as f: data = json.load(f) if 'creator_url' not in data or 'processed_post_ids' not in data: raise ValueError("Invalid profile format.") self.update_profile_data = data self.update_creator_name = os.path.basename(filepath).replace('.json', '') self.accept() # Close the dialog and signal success except Exception as e: QMessageBox.critical(self, "Error Loading Profile", f"Could not load or parse the selected profile file:\n\n{e}") def _handle_fetch_posts_click (self ): selected_creators =list (self .globally_selected_creators .values ()) print(f"[DEBUG] Selected creators for fetch: {selected_creators}") if not selected_creators : QMessageBox .information (self ,self ._tr ("no_selection_title","No Selection"), "Please select at least one creator to fetch posts for.") return if self .parent_app : parent_geometry =self .parent_app .geometry () new_width =int (parent_geometry .width ()*0.75 ) new_height =int (parent_geometry .height ()*0.80 ) self .resize (new_width ,new_height ) self ._center_on_screen () self .right_pane_widget .show () QTimer .singleShot (10 ,lambda :self .main_splitter .setSizes ([int (self .width ()*0.3 ),int (self .width ()*0.7 )])) QTimer .singleShot (20 ,lambda :self .posts_content_splitter .setSizes ([int (self .posts_content_splitter .width ()*0.7 ),int (self .posts_content_splitter .width ()*0.3 )])) self .add_selected_button .setEnabled (False ) self .globally_selected_post_ids .clear () self .posts_search_input .setVisible (True ) self .setWindowTitle (self ._tr ("creator_popup_title_fetching","Creator Posts")) self .fetch_posts_button .setEnabled (False ) self .posts_title_list_widget .clear () self .posts_date_list_widget .clear () self .fetched_posts_data .clear () self .posts_area_title_label .setText (self ._tr ("fav_posts_loading_status","Loading favorite posts...")) self .posts_title_list_widget .itemChanged .connect (self ._handle_post_item_check_changed ) self .progress_bar .setVisible (True ) if self .post_fetch_thread and self .post_fetch_thread .isRunning (): self .post_fetch_thread .cancel () self .post_fetch_thread .wait () print(f"[DEBUG] Starting PostsFetcherThread with creators: {selected_creators}") self .post_fetch_thread =PostsFetcherThread (selected_creators ,self ) self .post_fetch_thread .status_update .connect (self ._handle_fetch_status_update ) self .post_fetch_thread .posts_fetched_signal .connect (self ._handle_posts_fetched ) self .post_fetch_thread .fetch_error_signal .connect (self ._handle_fetch_error ) self .post_fetch_thread .finished_signal .connect (self ._handle_fetch_finished ) self .post_fetch_thread .start () def _tr (self ,key ,default_text =""): """Helper to get translation based on current app language.""" if callable (get_translation )and self .parent_app : return get_translation (self .parent_app .current_selected_language ,key ,default_text ) return default_text def _retranslate_ui (self ): self .setWindowTitle (self ._tr ("creator_popup_title","Creator Selection")) self .search_input .setPlaceholderText (self ._tr ("creator_popup_search_placeholder","Search by name, service, or paste creator URL...")) self .add_selected_button .setText (self ._tr ("creator_popup_add_selected_button","Add Selected")) self .fetch_posts_button .setText (self ._tr ("fetch_posts_button_text","Fetch Posts")) self ._update_scope_button_text_and_tooltip () self.update_button.setText(self._tr("check_for_updates_button", "Check for Updates")) self .posts_search_input .setPlaceholderText (self ._tr ("creator_popup_posts_search_placeholder","Search fetched posts by title...")) self .posts_title_header_label .setText (self ._tr ("column_header_post_title","Post Title")) self .posts_date_header_label .setText (self ._tr ("column_header_date_uploaded","Date Uploaded")) self .posts_area_title_label .setText (self ._tr ("creator_popup_posts_area_title","Fetched Posts")) self .posts_select_all_button .setText (self ._tr ("select_all_button_text","Select All")) self .posts_deselect_all_button .setText (self ._tr ("deselect_all_button_text","Deselect All")) self .posts_add_selected_button .setText (self ._tr ("creator_popup_add_posts_to_queue_button","Add Selected Posts to Queue")) self .posts_close_button .setText (self ._tr ("fav_posts_cancel_button","Cancel")) def _sync_scroll_dates (self ,value ): if not self ._is_scrolling_titles : self ._is_scrolling_dates =True self .posts_date_list_widget .verticalScrollBar ().setValue (value ) self ._is_scrolling_dates =False def _sync_scroll_titles (self ,value ): if not self ._is_scrolling_dates : self ._is_scrolling_titles =True self .posts_title_list_widget .verticalScrollBar ().setValue (value ) self ._is_scrolling_titles =False def _perform_initial_load (self ): """Called by QTimer to load data after dialog is shown.""" self ._load_creators_from_json () def _load_creators_from_json (self ): """Loads creators from creators.json and populates the list widget.""" self .list_widget .clear () self .progress_bar .setVisible (True ) QCoreApplication .processEvents () if not self .isVisible ():return # Always resolve project root relative to this file project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")) creators_file_path = os.path.join(project_root, "data", "creators.json") if not os .path .exists (creators_file_path ): self .list_widget .addItem (f"Error: creators.json not found at {creators_file_path }") self .all_creators_data =[] self .progress_bar .setVisible (False ) QCoreApplication .processEvents () return try : with open (creators_file_path ,'r',encoding ='utf-8')as f : data =json .load (f ) QCoreApplication .processEvents () if not self .isVisible ():return raw_data_list =[] if isinstance (data ,list )and len (data )>0 and isinstance (data [0 ],list ): raw_data_list =data [0 ] elif isinstance (data ,list )and all (isinstance (item ,dict )for item in data ): raw_data_list =data else : self .list_widget .addItem ("Error: Invalid format in creators.json.") self .all_creators_data =[] self .progress_bar .setVisible (False );QCoreApplication .processEvents ();return unique_creators_map ={} duplicates_skipped_count =0 for creator_entry in raw_data_list : service =creator_entry .get ('service') creator_id =creator_entry .get ('id') name_for_log =creator_entry .get ('name','Unknown') if service and creator_id : key =(str (service ).lower ().strip (),str (creator_id ).strip ()) if key not in unique_creators_map : unique_creators_map [key ]=creator_entry else : duplicates_skipped_count +=1 else : print (f"Warning: Creator entry in creators.json missing service or ID: {name_for_log }") self .all_creators_data =list (unique_creators_map .values ()) if duplicates_skipped_count >0 : print (f"INFO (Creator Popup): Skipped {duplicates_skipped_count } duplicate creator entries from creators.json based on (service, id).") self .all_creators_data .sort (key =lambda c :(-c .get ('favorited',0 ),c .get ('name','').lower ())) QCoreApplication .processEvents () if not self .isVisible ():return except json .JSONDecodeError : self .list_widget .addItem ("Error: Could not parse creators.json.") self .all_creators_data =[] self .progress_bar .setVisible (False );QCoreApplication .processEvents ();return except Exception as e : self .list_widget .addItem (f"Error loading creators: {e }") self .all_creators_data =[] self .progress_bar .setVisible (False );QCoreApplication .processEvents ();return self ._filter_list () def _populate_list_widget (self ,creators_to_display ): """Clears and populates the list widget with the given creator data.""" self .list_widget .blockSignals (True ) self .list_widget .clear () if not creators_to_display : self .list_widget .blockSignals (False ) return CHUNK_SIZE_POPULATE =100 for i in range (0 ,len (creators_to_display ),CHUNK_SIZE_POPULATE ): if not self .isVisible (): self .list_widget .blockSignals (False ) return chunk =creators_to_display [i :i +CHUNK_SIZE_POPULATE ] for creator in chunk : creator_name_raw =creator .get ('name') display_creator_name =creator_name_raw .strip ()if isinstance (creator_name_raw ,str )and creator_name_raw .strip ()else "Unknown Creator" service_display_name =creator .get ('service','N/A').capitalize () display_text =f"{display_creator_name } ({service_display_name })" item =QListWidgetItem (display_text ) item .setFlags (item .flags ()|Qt .ItemIsUserCheckable ) item .setData (Qt .UserRole ,creator ) service =creator .get ('service') creator_id =creator .get ('id') if service is not None and creator_id is not None : unique_key =(str (service ),str (creator_id )) if unique_key in self .globally_selected_creators : item .setCheckState (Qt .Checked ) else : item .setCheckState (Qt .Unchecked ) else : item .setCheckState (Qt .Unchecked ) self .list_widget .addItem (item ) QCoreApplication .processEvents () self .list_widget .blockSignals (False ) def _filter_list (self ): """Filters the list widget based on the search input.""" raw_search_input =self .search_input .text () check_search_text_for_empty =raw_search_input .lower ().strip () QCoreApplication .processEvents () if not self .isVisible ():return if not check_search_text_for_empty : self .progress_bar .setVisible (False ) creators_to_show =self .all_creators_data [:self .INITIAL_LOAD_LIMIT ] self ._populate_list_widget (creators_to_show ) self .search_input .setToolTip ("Search by name, service, or paste creator URL...") QCoreApplication .processEvents () else : self .progress_bar .setVisible (True ) QCoreApplication .processEvents () if not self .isVisible ():return norm_search_casefolded =unicodedata .normalize ('NFKC',raw_search_input ).casefold ().strip () scored_matches =[] parsed_service_from_url ,parsed_user_id_from_url ,_ =extract_post_info (raw_search_input ) if parsed_service_from_url and parsed_user_id_from_url : self .search_input .setToolTip (f"Searching for URL: {raw_search_input [:50 ]}...") for creator_data in self .all_creators_data : creator_service_lower =creator_data .get ('service','').lower () creator_id_str_lower =str (creator_data .get ('id','')).lower () if creator_service_lower ==parsed_service_from_url .lower ()and creator_id_str_lower ==parsed_user_id_from_url .lower (): scored_matches .append ((5 ,creator_data )) break else : self .search_input .setToolTip ("Searching by name or service...") norm_search_casefolded =unicodedata .normalize ('NFKC',raw_search_input ).casefold ().strip () CHUNK_SIZE_FILTER =500 for i in range (0 ,len (self .all_creators_data ),CHUNK_SIZE_FILTER ): if not self .isVisible ():break chunk =self .all_creators_data [i :i +CHUNK_SIZE_FILTER ] for creator_data in chunk : creator_name_raw =creator_data .get ('name','') creator_service_raw =creator_data .get ('service','') norm_creator_name_casefolded =unicodedata .normalize ('NFKC',creator_name_raw ).casefold () norm_service_casefolded =unicodedata .normalize ('NFKC',creator_service_raw ).casefold () current_score =0 if norm_search_casefolded ==norm_creator_name_casefolded : current_score =4 elif norm_creator_name_casefolded .startswith (norm_search_casefolded ): current_score =3 elif norm_search_casefolded in norm_creator_name_casefolded : current_score =2 elif norm_search_casefolded in norm_service_casefolded : current_score =1 if current_score >0 : scored_matches .append ((current_score ,creator_data )) QCoreApplication .processEvents () scored_matches .sort (key =lambda x :(-x [0 ],unicodedata .normalize ('NFKC',x [1 ].get ('name','')).casefold ())) final_creators_to_display =[creator_data for score ,creator_data in scored_matches [:20 ]] self ._populate_list_widget (final_creators_to_display ) self .progress_bar .setVisible (False ) if parsed_service_from_url and parsed_user_id_from_url : if final_creators_to_display : self .search_input .setToolTip (f"Found creator by URL: {final_creators_to_display [0 ].get ('name')}") else : self .search_input .setToolTip (f"URL parsed, but no matching creator found in your creators.json.") else : if final_creators_to_display : self .search_input .setToolTip (f"Showing top {len (final_creators_to_display )} match(es) for '{raw_search_input [:30 ]}...'") else : self .search_input .setToolTip (f"No matches found for '{raw_search_input [:30 ]}...'") def _toggle_scope_mode (self ): """Toggles the scope mode and updates the button text.""" if self .current_scope_mode ==self .SCOPE_CHARACTERS : self .current_scope_mode =self .SCOPE_CREATORS else : self .current_scope_mode =self .SCOPE_CHARACTERS self ._update_scope_button_text_and_tooltip () def _update_scope_button_text_and_tooltip (self ): if self .current_scope_mode ==self .SCOPE_CHARACTERS : self .scope_button .setText (self ._tr ("creator_popup_scope_characters_button","Scope: Characters")) else : self .scope_button .setText (self ._tr ("creator_popup_scope_creators_button","Scope: Creators")) self .scope_button .setToolTip ( f"Current Download Scope: {self .current_scope_mode }\n\n" f"Click to toggle between '{self .SCOPE_CHARACTERS }' and '{self .SCOPE_CREATORS }' scopes.\n" f"'{self .SCOPE_CHARACTERS }': Downloads into character-named folders directly in the main Download Location (artists mixed).\n" f"'{self .SCOPE_CREATORS }': Downloads into artist-named subfolders within the main Download Location, then character folders inside those.") def _handle_fetch_status_update (self ,message ): if self .parent_app : self .parent_app .log_signal .emit (f"[CreatorPopup Fetch] {message }") self .posts_area_title_label .setText (message ) def _handle_posts_fetched (self ,creator_info ,posts_list ): creator_key =(creator_info .get ('service'),str (creator_info .get ('id'))) self .fetched_posts_data [creator_key ]=(creator_info ,posts_list ) self ._filter_fetched_posts_list () def _filter_fetched_posts_list (self ): search_text =self .posts_search_input .text ().lower ().strip () data_for_rebuild ={} if not self .fetched_posts_data : self .posts_area_title_label .setText (self ._tr ("no_posts_fetched_yet_status","No posts fetched yet.")) elif not search_text : data_for_rebuild =self .fetched_posts_data total_posts_in_view =sum (len (posts_tuple [1 ])for posts_tuple in data_for_rebuild .values ()) if total_posts_in_view >0 : self .posts_area_title_label .setText (self ._tr ("fetched_posts_count_label","Fetched {count} post(s). Select to add to queue.").format (count =total_posts_in_view )) else : self .posts_area_title_label .setText (self ._tr ("no_posts_found_for_selection","No posts found for selected creator(s).")) else : for creator_key ,(creator_data_tuple_part ,posts_list_tuple_part )in self .fetched_posts_data .items (): matching_posts_for_creator =[ post for post in posts_list_tuple_part if search_text in post .get ('title','').lower () ] if matching_posts_for_creator : data_for_rebuild [creator_key ]=(creator_data_tuple_part ,matching_posts_for_creator ) total_matching_posts =sum (len (posts_tuple [1 ])for posts_tuple in data_for_rebuild .values ()) if total_matching_posts >0 : self .posts_area_title_label .setText (self ._tr ("fetched_posts_count_label_filtered","Displaying {count} post(s) matching filter.").format (count =total_matching_posts )) else : self .posts_area_title_label .setText (self ._tr ("no_posts_match_search_filter","No posts match your search filter.")) self ._rebuild_posts_list_widget (filtered_data_map =data_for_rebuild ) def _rebuild_posts_list_widget (self ,filtered_data_map ): self .posts_title_list_widget .blockSignals (True ) self .posts_date_list_widget .blockSignals (True ) self .posts_title_list_widget .clear () self .posts_date_list_widget .clear () data_to_display =filtered_data_map if not data_to_display : self .posts_title_list_widget .blockSignals (False ) self .posts_date_list_widget .blockSignals (False ) return sorted_creator_keys =sorted ( data_to_display .keys (), key =lambda k :data_to_display [k ][0 ].get ('name','').lower () ) total_posts_shown =0 for creator_key in sorted_creator_keys : creator_info_original ,posts_for_this_creator =data_to_display .get (creator_key ,(None ,[])) if not creator_info_original or not posts_for_this_creator : continue creator_header_item =QListWidgetItem (f"--- {self ._tr ('posts_for_creator_header','Posts for')} {creator_info_original ['name']} ({creator_info_original ['service']}) ---") font =creator_header_item .font () font .setBold (True ) creator_header_item .setFont (font ) creator_header_item .setFlags (Qt .NoItemFlags ) self .posts_title_list_widget .addItem (creator_header_item ) self .posts_date_list_widget .addItem (QListWidgetItem ("")) for post in posts_for_this_creator : post_title =post .get ('title',self ._tr ('untitled_post_placeholder','Untitled Post')) date_prefix_str ="[No Date]" published_date_str =post .get ('published') added_date_str =post .get ('added') date_to_use_str =None if published_date_str : date_to_use_str =published_date_str elif added_date_str : date_to_use_str =added_date_str if date_to_use_str : try : formatted_date =date_to_use_str .split ('T')[0 ] date_prefix_str =f"[{formatted_date }]" except Exception : pass date_display_str ="[No Date]" published_date_str =post .get ('published') added_date_str =post .get ('added') date_to_use_str =None if published_date_str : date_to_use_str =published_date_str elif added_date_str : date_to_use_str =added_date_str if date_to_use_str : try : formatted_date =date_to_use_str .split ('T')[0 ] date_display_str =f"[{formatted_date }]" except Exception : pass title_item_text =f" {post_title }" item =QListWidgetItem (title_item_text ) item .setFlags (item .flags ()|Qt .ItemIsUserCheckable ) item .setCheckState (Qt .Unchecked ) item_data ={ 'title':post_title , 'id':post .get ('id'), 'service':creator_info_original ['service'], 'user_id':creator_info_original ['id'], 'creator_name':creator_info_original ['name'], 'full_post_data':post , 'date_display_str':date_display_str , 'published_date_for_sort':date_to_use_str } item .setData (Qt .UserRole ,item_data ) post_unique_key =( item_data ['service'], str (item_data ['user_id']), str (item_data ['id']) ) if post_unique_key in self .globally_selected_post_ids : item .setCheckState (Qt .Checked ) else : item .setCheckState (Qt .Unchecked ) self .posts_title_list_widget .addItem (item ) total_posts_shown +=1 date_item =QListWidgetItem (f" {date_display_str }") date_item .setFlags (Qt .NoItemFlags ) self .posts_date_list_widget .addItem (date_item ) self .posts_title_list_widget .blockSignals (False ) self .posts_date_list_widget .blockSignals (False ) def _handle_fetch_error (self ,creator_info ,error_message ): creator_name =creator_info .get ('name','Unknown Creator') if self .parent_app : self .parent_app .log_signal .emit (f"[CreatorPopup Fetch ERROR] For {creator_name }: {error_message }") self .posts_area_title_label .setText (self ._tr ("fetch_error_for_creator_label","Error fetching for {creator_name}").format (creator_name =creator_name )) def _handle_fetch_finished (self ): self .fetch_posts_button .setEnabled (True ) self .progress_bar .setVisible (False ) if not self .fetched_posts_data : if self .post_fetch_thread and self .post_fetch_thread .cancellation_flag .is_set (): self .posts_area_title_label .setText (self ._tr ("post_fetch_cancelled_status_done","Post fetching cancelled.")) else : self .posts_area_title_label .setText (self ._tr ("failed_to_fetch_or_no_posts_label","Failed to fetch posts or no posts found.")) self .posts_search_input .setVisible (False ) elif not self .posts_title_list_widget .count ()and not self .posts_search_input .text ().strip (): self .posts_area_title_label .setText (self ._tr ("no_posts_found_for_selection","No posts found for selected creator(s).")) self .posts_search_input .setVisible (True ) else : QTimer .singleShot (10 ,lambda :self .posts_content_splitter .setSizes ([int (self .posts_content_splitter .width ()*0.7 ),int (self .posts_content_splitter .width ()*0.3 )])) self .posts_search_input .setVisible (True ) def _handle_posts_select_all (self ): self .posts_title_list_widget .blockSignals (True ) for i in range (self .posts_title_list_widget .count ()): item =self .posts_title_list_widget .item (i ) if item .flags ()&Qt .ItemIsUserCheckable : item .setCheckState (Qt .Checked ) item_data =item .data (Qt .UserRole ) if item_data : post_unique_key =( item_data ['service'], str (item_data ['user_id']), str (item_data ['id']) ) self .globally_selected_post_ids .add (post_unique_key ) self .posts_title_list_widget .blockSignals (False ) def _handle_posts_deselect_all (self ): self .posts_title_list_widget .blockSignals (True ) for i in range (self .posts_title_list_widget .count ()): item =self .posts_title_list_widget .item (i ) if item .flags ()&Qt .ItemIsUserCheckable : item .setCheckState (Qt .Unchecked ) self .globally_selected_post_ids .clear () self .posts_title_list_widget .blockSignals (False ) def _handle_post_item_check_changed (self ,item ): if not item or not item .data (Qt .UserRole ): return item_data =item .data (Qt .UserRole ) post_unique_key =( item_data ['service'], str (item_data ['user_id']), str (item_data ['id']) ) if item .checkState ()==Qt .Checked : self .globally_selected_post_ids .add (post_unique_key ) else : self .globally_selected_post_ids .discard (post_unique_key ) def _handle_posts_add_selected_to_queue (self ): selected_posts_for_queue =[] if not self .globally_selected_post_ids : QMessageBox .information (self ,self ._tr ("no_selection_title","No Selection"), self ._tr ("select_posts_to_queue_message","Please select at least one post to add to the queue.")) return for post_key in self .globally_selected_post_ids : service ,user_id_str ,post_id_str =post_key post_data_found =None creator_key_for_fetched_data =(service ,user_id_str ) if creator_key_for_fetched_data in self .fetched_posts_data : _unused_creator_info ,posts_in_list_for_creator =self .fetched_posts_data [creator_key_for_fetched_data ] for post_in_list in posts_in_list_for_creator : if str (post_in_list .get ('id'))==post_id_str : post_data_found =post_in_list break if post_data_found : creator_info_original ,_unused_posts =self .fetched_posts_data .get (creator_key_for_fetched_data ,({},[])) creator_name =creator_info_original .get ('name','Unknown Creator')if creator_info_original else 'Unknown Creator' domain =self ._get_domain_for_service (service ) post_url =f"https://{domain }/{service }/user/{user_id_str }/post/{post_id_str }" queue_item ={ 'type':'single_post_from_popup', 'url':post_url , 'name':post_data_found .get ('title',self ._tr ('untitled_post_placeholder','Untitled Post')), 'name_for_folder':creator_name , 'service':service , 'user_id':user_id_str , 'post_id':post_id_str } selected_posts_for_queue .append (queue_item ) else : if self .parent_app and hasattr (self .parent_app ,'log_signal'): self .parent_app .log_signal .emit (f"⚠️ Could not find full post data for selected key: {post_key } when adding to queue.") else : domain =self ._get_domain_for_service (service ) post_url =f"https://{domain }/{service }/user/{user_id_str }/post/{post_id_str }" queue_item ={ 'type':'single_post_from_popup', 'url':post_url , 'name':f"post id {post_id_str }", 'name_for_folder':user_id_str , 'service':service , 'user_id':user_id_str , 'post_id':post_id_str } selected_posts_for_queue .append (queue_item ) if selected_posts_for_queue : if self .parent_app and hasattr (self .parent_app ,'favorite_download_queue'): for qi in selected_posts_for_queue : self .parent_app .favorite_download_queue .append (qi ) num_just_added_posts =len (selected_posts_for_queue ) total_in_queue =len (self .parent_app .favorite_download_queue ) self .parent_app .log_signal .emit (f"ℹ️ Added {num_just_added_posts } selected posts to the download queue. Total in queue: {total_in_queue }.") # --- START: MODIFIED LOGIC --- # Removed the blockSignals(True/False) calls to allow the main window's UI to update correctly. if self .parent_app .link_input : self .parent_app .link_input .setText ( self .parent_app ._tr ("popup_posts_selected_text","Posts - {count} selected").format (count =num_just_added_posts ) ) self .parent_app .link_input .setPlaceholderText ( self .parent_app ._tr ("items_in_queue_placeholder","{count} items in queue from popup.").format (count =total_in_queue ) ) # --- END: MODIFIED LOGIC --- self.selected_creators_for_queue.clear() self .accept () else : QMessageBox .information (self ,self ._tr ("no_selection_title","No Selection"), self ._tr ("select_posts_to_queue_message","Please select at least one post to add to the queue.")) def _handle_posts_close_view (self ): self .right_pane_widget .hide () self .main_splitter .setSizes ([self .width (),0 ]) self .posts_list_widget .itemChanged .disconnect (self ._handle_post_item_check_changed ) if hasattr (self ,'_handle_post_item_check_changed'): self .posts_title_list_widget .itemChanged .disconnect (self ._handle_post_item_check_changed ) self .posts_search_input .setVisible (False ) self .posts_search_input .clear () self .globally_selected_post_ids .clear () self .add_selected_button .setEnabled (True ) self .setWindowTitle (self ._tr ("creator_popup_title","Creator Selection")) def _get_domain_for_service (self ,service_name ): """Determines the base domain for a given service.""" service_lower =service_name .lower () if service_lower in ['onlyfans','fansly']: return "coomer.su" return "kemono.su" def _handle_add_selected (self ): """Gathers globally selected creators and processes them.""" selected_display_names =[] self .selected_creators_for_queue .clear () for creator_data in self .globally_selected_creators .values (): creator_name =creator_data .get ('name') self .selected_creators_for_queue .append (creator_data ) if creator_name : selected_display_names .append (creator_name ) if selected_display_names : main_app_window =self .parent () if hasattr (main_app_window ,'link_input'): main_app_window .link_input .setText (", ".join (selected_display_names )) self .accept () else : QMessageBox .information (self ,"No Selection","No creators selected to add.") def _handle_item_check_changed (self ,item ): """Updates the globally_selected_creators dict when an item's check state changes.""" creator_data =item .data (Qt .UserRole ) if not isinstance (creator_data ,dict ): return service =creator_data .get ('service') creator_id =creator_data .get ('id') if service is None or creator_id is None : print (f"Warning: Creator data in list item missing service or id: {creator_data .get ('name')}") return unique_key =(str (service ),str (creator_id )) if item .checkState ()==Qt .Checked : self .globally_selected_creators [unique_key ]=creator_data else : if unique_key in self .globally_selected_creators : del self .globally_selected_creators [unique_key ] self .fetch_posts_button .setEnabled (bool (self .globally_selected_creators ))