This commit is contained in:
Yuvi9587
2025-07-15 06:54:31 -07:00
parent 440cf60d90
commit 874902ad60
17 changed files with 2802 additions and 3009 deletions

View File

@@ -115,217 +115,248 @@ def fetch_post_comments(api_domain, service, user_id, post_id, headers, logger,
except ValueError as e:
raise RuntimeError(f"Error decoding JSON from comments API for post {post_id}: {e}")
def download_from_api (
api_url_input ,
logger =print ,
start_page =None ,
end_page =None ,
manga_mode =False ,
cancellation_event =None ,
pause_event =None ,
use_cookie =False ,
cookie_text ="",
selected_cookie_file =None ,
app_base_dir =None ,
manga_filename_style_for_sort_check =None
def download_from_api(
api_url_input,
logger=print,
start_page=None,
end_page=None,
manga_mode=False,
cancellation_event=None,
pause_event=None,
use_cookie=False,
cookie_text="",
selected_cookie_file=None,
app_base_dir=None,
manga_filename_style_for_sort_check=None,
processed_post_ids=None # --- ADD THIS ARGUMENT ---
):
headers ={
'User-Agent':'Mozilla/5.0',
'Accept':'application/json'
headers = {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
}
service ,user_id ,target_post_id =extract_post_info (api_url_input )
# --- ADD THIS BLOCK ---
# Ensure processed_post_ids is a set for fast lookups
if processed_post_ids is None:
processed_post_ids = set()
else:
processed_post_ids = set(processed_post_ids)
# --- END OF ADDITION ---
if cancellation_event and cancellation_event .is_set ():
logger (" Download_from_api cancelled at start.")
return
service, user_id, target_post_id = extract_post_info(api_url_input)
parsed_input_url_for_domain =urlparse (api_url_input )
api_domain =parsed_input_url_for_domain .netloc
if not any (d in api_domain .lower ()for d in ['kemono.su','kemono.party','coomer.su','coomer.party']):
logger (f"⚠️ Unrecognized domain '{api_domain }' from input URL. Defaulting to kemono.su for API calls.")
api_domain ="kemono.su"
cookies_for_api =None
if use_cookie and app_base_dir :
cookies_for_api =prepare_cookies_for_request (use_cookie ,cookie_text ,selected_cookie_file ,app_base_dir ,logger ,target_domain =api_domain )
if target_post_id :
direct_post_api_url =f"https://{api_domain }/api/v1/{service }/user/{user_id }/post/{target_post_id }"
logger (f" Attempting direct fetch for target post: {direct_post_api_url }")
try :
direct_response =requests .get (direct_post_api_url ,headers =headers ,timeout =(10 ,30 ),cookies =cookies_for_api )
direct_response .raise_for_status ()
direct_post_data =direct_response .json ()
if isinstance (direct_post_data ,list )and direct_post_data :
direct_post_data =direct_post_data [0 ]
if isinstance (direct_post_data ,dict )and 'post'in direct_post_data and isinstance (direct_post_data ['post'],dict ):
direct_post_data =direct_post_data ['post']
if isinstance (direct_post_data ,dict )and direct_post_data .get ('id')==target_post_id :
logger (f" ✅ Direct fetch successful for post {target_post_id }.")
yield [direct_post_data ]
return
else :
response_type =type (direct_post_data ).__name__
response_snippet =str (direct_post_data )[:200 ]
logger (f" ⚠️ Direct fetch for post {target_post_id } returned unexpected data (Type: {response_type }, Snippet: '{response_snippet }'). Falling back to pagination.")
except requests .exceptions .RequestException as e :
logger (f" ⚠️ Direct fetch failed for post {target_post_id }: {e }. Falling back to pagination.")
except Exception as e :
logger (f" ⚠️ Unexpected error during direct fetch for post {target_post_id }: {e }. Falling back to pagination.")
if not service or not user_id :
logger (f"❌ Invalid URL or could not extract service/user: {api_url_input }")
return
if target_post_id and (start_page or end_page ):
logger ("⚠️ Page range (start/end page) is ignored when a specific post URL is provided (searching all pages for the post).")
if cancellation_event and cancellation_event.is_set():
logger(" Download_from_api cancelled at start.")
return
is_manga_mode_fetch_all_and_sort_oldest_first =manga_mode and (manga_filename_style_for_sort_check !=STYLE_DATE_POST_TITLE )and not target_post_id
api_base_url =f"https://{api_domain }/api/v1/{service }/user/{user_id }"
page_size =50
if is_manga_mode_fetch_all_and_sort_oldest_first :
logger (f" Manga Mode (Style: {manga_filename_style_for_sort_check if manga_filename_style_for_sort_check else 'Default'} - Oldest First Sort Active): Fetching all posts to sort by date...")
all_posts_for_manga_mode =[]
current_offset_manga =0
if start_page and start_page >1 :
current_offset_manga =(start_page -1 )*page_size
logger (f" Manga Mode: Starting fetch from page {start_page } (offset {current_offset_manga }).")
elif start_page :
logger (f" Manga Mode: Starting fetch from page 1 (offset 0).")
if end_page :
logger (f" Manga Mode: Will fetch up to page {end_page }.")
while True :
if pause_event and pause_event .is_set ():
logger (" Manga mode post fetching paused...")
while pause_event .is_set ():
if cancellation_event and cancellation_event .is_set ():
logger (" Manga mode post fetching cancelled while paused.")
break
time .sleep (0.5 )
if not (cancellation_event and cancellation_event .is_set ()):logger (" Manga mode post fetching resumed.")
if cancellation_event and cancellation_event .is_set ():
logger (" Manga mode post fetching cancelled.")
break
current_page_num_manga =(current_offset_manga //page_size )+1
if end_page and current_page_num_manga >end_page :
logger (f" Manga Mode: Reached specified end page ({end_page }). Stopping post fetch.")
break
try :
posts_batch_manga =fetch_posts_paginated (api_base_url ,headers ,current_offset_manga ,logger ,cancellation_event ,pause_event ,cookies_dict =cookies_for_api )
if not isinstance (posts_batch_manga ,list ):
logger (f"❌ API Error (Manga Mode): Expected list of posts, got {type (posts_batch_manga )}.")
break
if not posts_batch_manga :
logger ("✅ Reached end of posts (Manga Mode fetch all).")
if start_page and not end_page and current_page_num_manga <start_page :
logger (f" Manga Mode: No posts found on or after specified start page {start_page }.")
elif end_page and current_page_num_manga <=end_page and not all_posts_for_manga_mode :
logger (f" Manga Mode: No posts found within the specified page range ({start_page or 1 }-{end_page }).")
break
all_posts_for_manga_mode .extend (posts_batch_manga )
current_offset_manga +=page_size
time .sleep (0.6 )
except RuntimeError as e :
if "cancelled by user"in str (e ).lower ():
logger (f" Manga mode pagination stopped due to cancellation: {e }")
else :
logger (f"{e }\n Aborting manga mode pagination.")
break
except Exception as e :
logger (f"❌ Unexpected error during manga mode fetch: {e }")
traceback .print_exc ()
break
if cancellation_event and cancellation_event .is_set ():return
if all_posts_for_manga_mode :
logger (f" Manga Mode: Fetched {len (all_posts_for_manga_mode )} total posts. Sorting by publication date (oldest first)...")
def sort_key_tuple (post ):
published_date_str =post .get ('published')
added_date_str =post .get ('added')
post_id_str =post .get ('id',"0")
primary_sort_val ="0000-00-00T00:00:00"
if published_date_str :
primary_sort_val =published_date_str
elif added_date_str :
logger (f" ⚠️ Post ID {post_id_str } missing 'published' date, using 'added' date '{added_date_str }' for primary sorting.")
primary_sort_val =added_date_str
else :
logger (f" ⚠️ Post ID {post_id_str } missing both 'published' and 'added' dates. Placing at start of sort (using default earliest date).")
secondary_sort_val =0
try :
secondary_sort_val =int (post_id_str )
except ValueError :
logger (f" ⚠️ Post ID '{post_id_str }' is not a valid integer for secondary sorting, using 0.")
return (primary_sort_val ,secondary_sort_val )
all_posts_for_manga_mode .sort (key =sort_key_tuple )
for i in range (0 ,len (all_posts_for_manga_mode ),page_size ):
if cancellation_event and cancellation_event .is_set ():
logger (" Manga mode post yielding cancelled.")
break
yield all_posts_for_manga_mode [i :i +page_size ]
return
parsed_input_url_for_domain = urlparse(api_url_input)
api_domain = parsed_input_url_for_domain.netloc
if not any(d in api_domain.lower() for d in ['kemono.su', 'kemono.party', 'coomer.su', 'coomer.party']):
logger(f"⚠️ Unrecognized domain '{api_domain}' from input URL. Defaulting to kemono.su for API calls.")
api_domain = "kemono.su"
cookies_for_api = None
if use_cookie and app_base_dir:
cookies_for_api = prepare_cookies_for_request(use_cookie, cookie_text, selected_cookie_file, app_base_dir, logger, target_domain=api_domain)
if target_post_id:
# --- ADD THIS CHECK FOR RESTORE ---
if target_post_id in processed_post_ids:
logger(f" Skipping already processed target post ID: {target_post_id}")
return
# --- END OF ADDITION ---
direct_post_api_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}/post/{target_post_id}"
logger(f" Attempting direct fetch for target post: {direct_post_api_url}")
try:
direct_response = requests.get(direct_post_api_url, headers=headers, timeout=(10, 30), cookies=cookies_for_api)
direct_response.raise_for_status()
direct_post_data = direct_response.json()
if isinstance(direct_post_data, list) and direct_post_data:
direct_post_data = direct_post_data[0]
if isinstance(direct_post_data, dict) and 'post' in direct_post_data and isinstance(direct_post_data['post'], dict):
direct_post_data = direct_post_data['post']
if isinstance(direct_post_data, dict) and direct_post_data.get('id') == target_post_id:
logger(f" ✅ Direct fetch successful for post {target_post_id}.")
yield [direct_post_data]
return
else:
response_type = type(direct_post_data).__name__
response_snippet = str(direct_post_data)[:200]
logger(f" ⚠️ Direct fetch for post {target_post_id} returned unexpected data (Type: {response_type}, Snippet: '{response_snippet}'). Falling back to pagination.")
except requests.exceptions.RequestException as e:
logger(f" ⚠️ Direct fetch failed for post {target_post_id}: {e}. Falling back to pagination.")
except Exception as e:
logger(f" ⚠️ Unexpected error during direct fetch for post {target_post_id}: {e}. Falling back to pagination.")
if not service or not user_id:
logger(f"❌ Invalid URL or could not extract service/user: {api_url_input}")
return
if target_post_id and (start_page or end_page):
logger("⚠️ Page range (start/end page) is ignored when a specific post URL is provided (searching all pages for the post).")
is_manga_mode_fetch_all_and_sort_oldest_first = manga_mode and (manga_filename_style_for_sort_check != STYLE_DATE_POST_TITLE) and not target_post_id
api_base_url = f"https://{api_domain}/api/v1/{service}/user/{user_id}"
page_size = 50
if is_manga_mode_fetch_all_and_sort_oldest_first:
logger(f" Manga Mode (Style: {manga_filename_style_for_sort_check if manga_filename_style_for_sort_check else 'Default'} - Oldest First Sort Active): Fetching all posts to sort by date...")
all_posts_for_manga_mode = []
current_offset_manga = 0
if start_page and start_page > 1:
current_offset_manga = (start_page - 1) * page_size
logger(f" Manga Mode: Starting fetch from page {start_page} (offset {current_offset_manga}).")
elif start_page:
logger(f" Manga Mode: Starting fetch from page 1 (offset 0).")
if end_page:
logger(f" Manga Mode: Will fetch up to page {end_page}.")
while True:
if pause_event and pause_event.is_set():
logger(" Manga mode post fetching paused...")
while pause_event.is_set():
if cancellation_event and cancellation_event.is_set():
logger(" Manga mode post fetching cancelled while paused.")
break
time.sleep(0.5)
if not (cancellation_event and cancellation_event.is_set()): logger(" Manga mode post fetching resumed.")
if cancellation_event and cancellation_event.is_set():
logger(" Manga mode post fetching cancelled.")
break
current_page_num_manga = (current_offset_manga // page_size) + 1
if end_page and current_page_num_manga > end_page:
logger(f" Manga Mode: Reached specified end page ({end_page}). Stopping post fetch.")
break
try:
posts_batch_manga = fetch_posts_paginated(api_base_url, headers, current_offset_manga, logger, cancellation_event, pause_event, cookies_dict=cookies_for_api)
if not isinstance(posts_batch_manga, list):
logger(f"❌ API Error (Manga Mode): Expected list of posts, got {type(posts_batch_manga)}.")
break
if not posts_batch_manga:
logger("✅ Reached end of posts (Manga Mode fetch all).")
if start_page and not end_page and current_page_num_manga < start_page:
logger(f" Manga Mode: No posts found on or after specified start page {start_page}.")
elif end_page and current_page_num_manga <= end_page and not all_posts_for_manga_mode:
logger(f" Manga Mode: No posts found within the specified page range ({start_page or 1}-{end_page}).")
break
all_posts_for_manga_mode.extend(posts_batch_manga)
current_offset_manga += page_size
time.sleep(0.6)
except RuntimeError as e:
if "cancelled by user" in str(e).lower():
logger(f" Manga mode pagination stopped due to cancellation: {e}")
else:
logger(f"{e}\n Aborting manga mode pagination.")
break
except Exception as e:
logger(f"❌ Unexpected error during manga mode fetch: {e}")
traceback.print_exc()
break
if cancellation_event and cancellation_event.is_set(): return
if all_posts_for_manga_mode:
# --- ADD THIS BLOCK TO FILTER POSTS IN MANGA MODE ---
if processed_post_ids:
original_count = len(all_posts_for_manga_mode)
all_posts_for_manga_mode = [post for post in all_posts_for_manga_mode if post.get('id') not in processed_post_ids]
skipped_count = original_count - len(all_posts_for_manga_mode)
if skipped_count > 0:
logger(f" Manga Mode: Skipped {skipped_count} already processed post(s) before sorting.")
# --- END OF ADDITION ---
logger(f" Manga Mode: Fetched {len(all_posts_for_manga_mode)} total posts. Sorting by publication date (oldest first)...")
def sort_key_tuple(post):
published_date_str = post.get('published')
added_date_str = post.get('added')
post_id_str = post.get('id', "0")
primary_sort_val = "0000-00-00T00:00:00"
if published_date_str:
primary_sort_val = published_date_str
elif added_date_str:
logger(f" ⚠️ Post ID {post_id_str} missing 'published' date, using 'added' date '{added_date_str}' for primary sorting.")
primary_sort_val = added_date_str
else:
logger(f" ⚠️ Post ID {post_id_str} missing both 'published' and 'added' dates. Placing at start of sort (using default earliest date).")
secondary_sort_val = 0
try:
secondary_sort_val = int(post_id_str)
except ValueError:
logger(f" ⚠️ Post ID '{post_id_str}' is not a valid integer for secondary sorting, using 0.")
return (primary_sort_val, secondary_sort_val)
all_posts_for_manga_mode.sort(key=sort_key_tuple)
for i in range(0, len(all_posts_for_manga_mode), page_size):
if cancellation_event and cancellation_event.is_set():
logger(" Manga mode post yielding cancelled.")
break
yield all_posts_for_manga_mode[i:i + page_size]
return
if manga_mode and not target_post_id and (manga_filename_style_for_sort_check ==STYLE_DATE_POST_TITLE ):
logger (f" Manga Mode (Style: {STYLE_DATE_POST_TITLE }): Processing posts in default API order (newest first).")
if manga_mode and not target_post_id and (manga_filename_style_for_sort_check == STYLE_DATE_POST_TITLE):
logger(f" Manga Mode (Style: {STYLE_DATE_POST_TITLE}): Processing posts in default API order (newest first).")
current_page_num =1
current_offset =0
processed_target_post_flag =False
if start_page and start_page >1 and not target_post_id :
current_offset =(start_page -1 )*page_size
current_page_num =start_page
logger (f" Starting from page {current_page_num } (calculated offset {current_offset }).")
while True :
if pause_event and pause_event .is_set ():
logger (" Post fetching loop paused...")
while pause_event .is_set ():
if cancellation_event and cancellation_event .is_set ():
logger (" Post fetching loop cancelled while paused.")
break
time .sleep (0.5 )
if not (cancellation_event and cancellation_event .is_set ()):logger (" Post fetching loop resumed.")
if cancellation_event and cancellation_event .is_set ():
logger (" Post fetching loop cancelled.")
break
if target_post_id and processed_target_post_flag :
break
if not target_post_id and end_page and current_page_num >end_page :
logger (f"✅ Reached specified end page ({end_page }) for creator feed. Stopping.")
break
try :
posts_batch =fetch_posts_paginated (api_base_url ,headers ,current_offset ,logger ,cancellation_event ,pause_event ,cookies_dict =cookies_for_api )
if not isinstance (posts_batch ,list ):
logger (f"❌ API Error: Expected list of posts, got {type (posts_batch )} at page {current_page_num } (offset {current_offset }).")
break
except RuntimeError as e :
if "cancelled by user"in str (e ).lower ():
logger (f" Pagination stopped due to cancellation: {e }")
else :
logger (f"{e }\n Aborting pagination at page {current_page_num } (offset {current_offset }).")
break
except Exception as e :
logger (f"❌ Unexpected error fetching page {current_page_num } (offset {current_offset }): {e }")
traceback .print_exc ()
break
if not posts_batch :
if target_post_id and not processed_target_post_flag :
logger (f"❌ Target post {target_post_id } not found after checking all available pages (API returned no more posts at offset {current_offset }).")
elif not target_post_id :
if current_page_num ==(start_page or 1 ):
logger (f"😕 No posts found on the first page checked (page {current_page_num }, offset {current_offset }).")
else :
logger (f"✅ Reached end of posts (no more content from API at offset {current_offset }).")
break
if target_post_id and not processed_target_post_flag :
matching_post =next ((p for p in posts_batch if str (p .get ('id'))==str (target_post_id )),None )
if matching_post :
logger (f"🎯 Found target post {target_post_id } on page {current_page_num } (offset {current_offset }).")
yield [matching_post ]
processed_target_post_flag =True
elif not target_post_id :
yield posts_batch
if processed_target_post_flag :
break
current_offset +=page_size
current_page_num +=1
time .sleep (0.6 )
if target_post_id and not processed_target_post_flag and not (cancellation_event and cancellation_event .is_set ()):
logger (f"❌ Target post {target_post_id } could not be found after checking all relevant pages (final check after loop).")
current_page_num = 1
current_offset = 0
processed_target_post_flag = False
if start_page and start_page > 1 and not target_post_id:
current_offset = (start_page - 1) * page_size
current_page_num = start_page
logger(f" Starting from page {current_page_num} (calculated offset {current_offset}).")
while True:
if pause_event and pause_event.is_set():
logger(" Post fetching loop paused...")
while pause_event.is_set():
if cancellation_event and cancellation_event.is_set():
logger(" Post fetching loop cancelled while paused.")
break
time.sleep(0.5)
if not (cancellation_event and cancellation_event.is_set()): logger(" Post fetching loop resumed.")
if cancellation_event and cancellation_event.is_set():
logger(" Post fetching loop cancelled.")
break
if target_post_id and processed_target_post_flag:
break
if not target_post_id and end_page and current_page_num > end_page:
logger(f"✅ Reached specified end page ({end_page}) for creator feed. Stopping.")
break
try:
posts_batch = fetch_posts_paginated(api_base_url, headers, current_offset, logger, cancellation_event, pause_event, cookies_dict=cookies_for_api)
if not isinstance(posts_batch, list):
logger(f"❌ API Error: Expected list of posts, got {type(posts_batch)} at page {current_page_num} (offset {current_offset}).")
break
except RuntimeError as e:
if "cancelled by user" in str(e).lower():
logger(f" Pagination stopped due to cancellation: {e}")
else:
logger(f"{e}\n Aborting pagination at page {current_page_num} (offset {current_offset}).")
break
except Exception as e:
logger(f"❌ Unexpected error fetching page {current_page_num} (offset {current_offset}): {e}")
traceback.print_exc()
break
# --- ADD THIS BLOCK TO FILTER POSTS IN STANDARD MODE ---
if processed_post_ids:
original_count = len(posts_batch)
posts_batch = [post for post in posts_batch if post.get('id') not in processed_post_ids]
skipped_count = original_count - len(posts_batch)
if skipped_count > 0:
logger(f" Skipped {skipped_count} already processed post(s) from page {current_page_num}.")
# --- END OF ADDITION ---
if not posts_batch:
if target_post_id and not processed_target_post_flag:
logger(f"❌ Target post {target_post_id} not found after checking all available pages (API returned no more posts at offset {current_offset}).")
elif not target_post_id:
if current_page_num == (start_page or 1):
logger(f"😕 No posts found on the first page checked (page {current_page_num}, offset {current_offset}).")
else:
logger(f"✅ Reached end of posts (no more content from API at offset {current_offset}).")
break
if target_post_id and not processed_target_post_flag:
matching_post = next((p for p in posts_batch if str(p.get('id')) == str(target_post_id)), None)
if matching_post:
logger(f"🎯 Found target post {target_post_id} on page {current_page_num} (offset {current_offset}).")
yield [matching_post]
processed_target_post_flag = True
elif not target_post_id:
yield posts_batch
if processed_target_post_flag:
break
current_offset += page_size
current_page_num += 1
time.sleep(0.6)
if target_post_id and not processed_target_post_flag and not (cancellation_event and cancellation_event.is_set()):
logger(f"❌ Target post {target_post_id} could not be found after checking all relevant pages (final check after loop).")

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ from PyQt5.QtWidgets import (
# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
class CookieHelpDialog(QDialog):
"""

View File

@@ -13,7 +13,7 @@ from PyQt5.QtWidgets import (
from ...i18n.translator import get_translation
# get_app_icon_object is defined in the main window module in this refactoring plan.
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
class DownloadExtractedLinksDialog(QDialog):
"""
@@ -141,19 +141,25 @@ class DownloadExtractedLinksDialog(QDialog):
self.deselect_all_button.setText(self._tr("deselect_all_button_text", "Deselect All"))
self.download_button.setText(self._tr("download_selected_button_text", "Download Selected"))
self.cancel_button.setText(self._tr("fav_posts_cancel_button", "Cancel"))
def _apply_theme(self):
"""Applies the current theme from the parent application."""
is_dark_theme = self.parent() and hasattr(self.parent_app, 'current_theme') and self.parent_app.current_theme == "dark"
is_dark_theme = self.parent_app and self.parent_app.current_theme == "dark"
if is_dark_theme:
# 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("")
if is_dark_theme and hasattr(self.parent_app, 'get_dark_theme'):
self.setStyleSheet(self.parent_app.get_dark_theme())
# Set header text color based on theme
header_color = Qt.cyan if is_dark_theme else Qt.blue
for i in range(self.links_list_widget.count()):
item = self.links_list_widget.item(i)
# Headers are not checkable
# Headers are not checkable (they have no checkable flag)
if not item.flags() & Qt.ItemIsUserCheckable:
item.setForeground(header_color)

View File

@@ -13,6 +13,7 @@ from PyQt5.QtWidgets import (
# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
class DownloadHistoryDialog (QDialog ):
@@ -23,7 +24,7 @@ class DownloadHistoryDialog (QDialog ):
self .last_3_downloaded_entries =last_3_downloaded_entries
self .first_processed_entries =first_processed_entries
self .setModal (True )
self._apply_theme()
# 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:
@@ -158,6 +159,14 @@ class DownloadHistoryDialog (QDialog ):
return get_translation (self .parent_app .current_selected_language ,key ,default_text )
return default_text
def _apply_theme(self):
"""Applies the current theme from the parent application."""
if self.parent_app and self.parent_app.current_theme == "dark":
scale = getattr(self.parent_app, 'scale_factor', 1)
self.setStyleSheet(get_dark_theme(scale))
else:
self.setStyleSheet("QDialog { background-color: #f0f0f0; }")
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"),

View File

@@ -21,6 +21,7 @@ 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 ):
@@ -129,6 +130,7 @@ class PostsFetcherThread (QThread ):
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"
@@ -289,9 +291,14 @@ class EmptyPopupDialog (QDialog ):
self ._retranslate_ui ()
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 ())
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 ))
@@ -997,4 +1004,4 @@ class EmptyPopupDialog (QDialog ):
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 ))
self .fetch_posts_button .setEnabled (bool (self .globally_selected_creators ))

View File

@@ -10,7 +10,7 @@ from ...i18n.translator import get_translation
from ..assets import get_app_icon_object
# Corrected Import: The filename uses PascalCase.
from .ExportOptionsDialog import ExportOptionsDialog
from ...utils.resolution import get_dark_theme
class ErrorFilesDialog(QDialog):
"""
@@ -132,9 +132,14 @@ class ErrorFilesDialog(QDialog):
def _apply_theme(self):
"""Applies the current theme from the parent application."""
if self.parent_app and hasattr(self.parent_app, 'current_theme') and self.parent_app.current_theme == "dark":
if hasattr(self.parent_app, 'get_dark_theme'):
self.setStyleSheet(self.parent_app.get_dark_theme())
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("")
def _select_all_items(self):
"""Checks all items in the list."""

View File

@@ -10,7 +10,7 @@ from PyQt5.QtWidgets import (
from ...i18n.translator import get_translation
# get_app_icon_object is defined in the main window module in this refactoring plan.
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
class ExportOptionsDialog(QDialog):
"""

View File

@@ -16,7 +16,7 @@ from ...i18n.translator import get_translation
from ..assets import get_app_icon_object
from ...utils.network_utils import prepare_cookies_for_request
from .CookieHelpDialog import CookieHelpDialog
from ...utils.resolution import get_dark_theme
class FavoriteArtistsDialog (QDialog ):
"""Dialog to display and select favorite artists."""

View File

@@ -25,7 +25,7 @@ from ...utils.network_utils import prepare_cookies_for_request
# Corrected Import: Import CookieHelpDialog directly from its own module
from .CookieHelpDialog import CookieHelpDialog
from ...core.api_client import download_from_api
from ...utils.resolution import get_dark_theme
class FavoritePostsFetcherThread (QThread ):
"""Worker thread to fetch favorite posts and creator names."""

View File

@@ -11,6 +11,7 @@ from PyQt5.QtWidgets import (
# --- Local Application Imports ---
# This assumes the new project structure is in place.
from ...i18n.translator import get_translation
from ...utils.resolution import get_dark_theme
from ..main_window import get_app_icon_object
from ...config.constants import (
THEME_KEY, LANGUAGE_KEY, DOWNLOAD_LOCATION_KEY
@@ -113,8 +114,9 @@ class FutureSettingsDialog(QDialog):
def _apply_theme(self):
"""Applies the current theme from the parent application."""
if self.parent_app.current_theme == "dark":
self.setStyleSheet(self.parent_app.get_dark_theme())
if self.parent_app and self.parent_app.current_theme == "dark":
scale = getattr(self.parent_app, 'scale_factor', 1)
self.setStyleSheet(get_dark_theme(scale))
else:
self.setStyleSheet("")

View File

@@ -13,7 +13,7 @@ from PyQt5.QtWidgets import (
# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
class TourStepWidget(QWidget):
"""

View File

@@ -8,7 +8,7 @@ from PyQt5.QtWidgets import (
# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
class KnownNamesFilterDialog(QDialog):
"""
@@ -102,8 +102,14 @@ class KnownNamesFilterDialog(QDialog):
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())
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("")
def _populate_list_widget(self):
"""Populates the list widget with the known names."""

View File

@@ -2,6 +2,7 @@ from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QRadioButton, QDialogButtonBox, QButtonGroup, QLabel, QComboBox, QHBoxLayout, QCheckBox
)
from PyQt5.QtCore import Qt
from ...utils.resolution import get_dark_theme
class MoreOptionsDialog(QDialog):
"""
@@ -12,6 +13,7 @@ class MoreOptionsDialog(QDialog):
def __init__(self, parent=None, current_scope=None, current_format=None, single_pdf_checked=False):
super().__init__(parent)
self.parent_app = parent
self.setWindowTitle("More Options")
self.setMinimumWidth(350)
@@ -62,7 +64,7 @@ class MoreOptionsDialog(QDialog):
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.setLayout(layout)
self._apply_theme()
def update_single_pdf_checkbox_state(self, text):
"""Enable the Single PDF checkbox only if the format is PDF."""
is_pdf = (text.upper() == "PDF")
@@ -80,4 +82,15 @@ class MoreOptionsDialog(QDialog):
def get_single_pdf_state(self):
"""Returns the state of the Single PDF checkbox."""
return self.single_pdf_checkbox.isChecked() and self.single_pdf_checkbox.isEnabled()
return self.single_pdf_checkbox.isChecked() and self.single_pdf_checkbox.isEnabled()
def _apply_theme(self):
"""Applies the current theme from the parent application."""
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("")

View File

@@ -12,6 +12,7 @@ from PyQt5.QtWidgets import (
# --- Local Application Imports ---
from ...i18n.translator import get_translation
from ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme
from ...config.constants import (
CONFIG_ORGANIZATION_NAME
)
@@ -150,8 +151,9 @@ class TourDialog(QDialog):
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())
if self.parent_app and self.parent_app.current_theme == "dark":
scale = getattr(self.parent_app, 'scale_factor', 1)
self.setStyleSheet(get_dark_theme(scale))
else:
self.setStyleSheet("QDialog { background-color: #f0f0f0; }")

File diff suppressed because it is too large Load Diff

545
src/utils/resolution.py Normal file
View File

@@ -0,0 +1,545 @@
# src/ui/utils/resolution.py
# --- Standard Library Imports ---
import os
# --- PyQt5 Imports ---
from PyQt5.QtWidgets import (
QSplitter, QScrollArea, QFrame, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QStackedWidget, QButtonGroup, QRadioButton, QCheckBox,
QListWidget, QTextEdit, QApplication
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIntValidator # <-- Import QIntValidator from here
# --- Local Application Imports ---
# Assuming execution from project root
from ..config.constants import *
def setup_ui(main_app):
"""
Initializes and scales the user interface for the DownloaderApp.
Args:
main_app: The instance of the main DownloaderApp.
"""
# --- START: New Scaling Logic ---
screen = QApplication.primaryScreen()
if screen:
resolution = screen.size()
if resolution.width() > 1920 and resolution.height() > 1200:
main_app.scale_factor = 2
else:
main_app.scale_factor = 1
else:
# Fallback if a primary screen isn't detected
main_app.scale_factor = 1
scale = main_app.scale_factor # Use a convenient local variable
# --- END: New Scaling Logic ---
main_app.main_splitter = QSplitter(Qt.Horizontal)
# --- Use a scroll area for the left panel for consistency ---
left_scroll_area = QScrollArea()
left_scroll_area.setWidgetResizable(True)
left_scroll_area.setFrameShape(QFrame.NoFrame)
left_panel_widget = QWidget()
left_layout = QVBoxLayout(left_panel_widget)
left_scroll_area.setWidget(left_panel_widget)
right_panel_widget = QWidget()
right_layout = QVBoxLayout(right_panel_widget)
left_layout.setContentsMargins(10, 10, 10, 10)
right_layout.setContentsMargins(10, 10, 10, 10)
apply_theme_to_app(main_app, main_app.current_theme, initial_load=True)
# --- URL and Page Range ---
main_app.url_input_widget = QWidget()
url_input_layout = QHBoxLayout(main_app.url_input_widget)
url_input_layout.setContentsMargins(0, 0, 0, 0)
main_app.url_label_widget = QLabel()
url_input_layout.addWidget(main_app.url_label_widget)
main_app.link_input = QLineEdit()
main_app.link_input.setPlaceholderText("e.g., https://kemono.su/patreon/user/12345 or .../post/98765")
main_app.link_input.textChanged.connect(main_app.update_custom_folder_visibility)
url_input_layout.addWidget(main_app.link_input, 1)
main_app.empty_popup_button = QPushButton("🎨")
main_app.empty_popup_button.setStyleSheet(f"padding: {4*scale}px {6*scale}px;")
main_app.empty_popup_button.clicked.connect(main_app._show_empty_popup)
url_input_layout.addWidget(main_app.empty_popup_button)
main_app.page_range_label = QLabel(main_app._tr("page_range_label_text", "Page Range:"))
main_app.page_range_label.setStyleSheet("font-weight: bold; padding-left: 10px;")
url_input_layout.addWidget(main_app.page_range_label)
main_app.start_page_input = QLineEdit()
main_app.start_page_input.setPlaceholderText(main_app._tr("start_page_input_placeholder", "Start"))
main_app.start_page_input.setFixedWidth(50 * scale)
main_app.start_page_input.setValidator(QIntValidator(1, 99999))
url_input_layout.addWidget(main_app.start_page_input)
main_app.to_label = QLabel(main_app._tr("page_range_to_label_text", "to"))
url_input_layout.addWidget(main_app.to_label)
main_app.end_page_input = QLineEdit()
main_app.end_page_input.setPlaceholderText(main_app._tr("end_page_input_placeholder", "End"))
main_app.end_page_input.setFixedWidth(50 * scale)
main_app.end_page_input.setToolTip(main_app._tr("end_page_input_tooltip", "For creator URLs: Specify the ending page number..."))
main_app.end_page_input.setValidator(QIntValidator(1, 99999))
url_input_layout.addWidget(main_app.end_page_input)
main_app.url_placeholder_widget = QWidget()
placeholder_layout = QHBoxLayout(main_app.url_placeholder_widget)
placeholder_layout.setContentsMargins(0, 0, 0, 0)
main_app.fav_mode_active_label = QLabel(main_app._tr("fav_mode_active_label_text", "⭐ Favorite Mode is active..."))
main_app.fav_mode_active_label.setAlignment(Qt.AlignCenter)
placeholder_layout.addWidget(main_app.fav_mode_active_label)
main_app.url_or_placeholder_stack = QStackedWidget()
main_app.url_or_placeholder_stack.addWidget(main_app.url_input_widget)
main_app.url_or_placeholder_stack.addWidget(main_app.url_placeholder_widget)
left_layout.addWidget(main_app.url_or_placeholder_stack)
# --- Download Location ---
main_app.download_location_label_widget = QLabel()
left_layout.addWidget(main_app.download_location_label_widget)
dir_layout = QHBoxLayout()
main_app.dir_input = QLineEdit()
main_app.dir_input.setPlaceholderText("Select folder where downloads will be saved")
main_app.dir_button = QPushButton("Browse...")
main_app.dir_button.clicked.connect(main_app.browse_directory)
dir_layout.addWidget(main_app.dir_input, 1)
dir_layout.addWidget(main_app.dir_button)
left_layout.addLayout(dir_layout)
# --- Filters and Custom Folder Container ---
main_app.filters_and_custom_folder_container_widget = QWidget()
filters_and_custom_folder_layout = QHBoxLayout(main_app.filters_and_custom_folder_container_widget)
filters_and_custom_folder_layout.setContentsMargins(0, 5, 0, 0)
filters_and_custom_folder_layout.setSpacing(10)
main_app.character_filter_widget = QWidget()
character_filter_v_layout = QVBoxLayout(main_app.character_filter_widget)
character_filter_v_layout.setContentsMargins(0, 0, 0, 0)
character_filter_v_layout.setSpacing(2)
main_app.character_label = QLabel("🎯 Filter by Character(s) (comma-separated):")
character_filter_v_layout.addWidget(main_app.character_label)
char_input_and_button_layout = QHBoxLayout()
char_input_and_button_layout.setContentsMargins(0, 0, 0, 0)
char_input_and_button_layout.setSpacing(10)
main_app.character_input = QLineEdit()
main_app.character_input.setPlaceholderText("e.g., Tifa, Aerith, (Cloud, Zack)")
char_input_and_button_layout.addWidget(main_app.character_input, 3)
main_app.char_filter_scope_toggle_button = QPushButton()
main_app._update_char_filter_scope_button_text()
char_input_and_button_layout.addWidget(main_app.char_filter_scope_toggle_button, 1)
character_filter_v_layout.addLayout(char_input_and_button_layout)
# --- Custom Folder Widget Definition ---
main_app.custom_folder_widget = QWidget()
custom_folder_v_layout = QVBoxLayout(main_app.custom_folder_widget)
custom_folder_v_layout.setContentsMargins(0, 0, 0, 0)
custom_folder_v_layout.setSpacing(2)
main_app.custom_folder_label = QLabel("🗄️ Custom Folder Name (Single Post Only):")
main_app.custom_folder_input = QLineEdit()
main_app.custom_folder_input.setPlaceholderText("Optional: Save this post to specific folder")
custom_folder_v_layout.addWidget(main_app.custom_folder_label)
custom_folder_v_layout.addWidget(main_app.custom_folder_input)
main_app.custom_folder_widget.setVisible(False)
filters_and_custom_folder_layout.addWidget(main_app.character_filter_widget, 1)
filters_and_custom_folder_layout.addWidget(main_app.custom_folder_widget, 1)
left_layout.addWidget(main_app.filters_and_custom_folder_container_widget)
# --- Word Manipulation Container ---
word_manipulation_container_widget = QWidget()
word_manipulation_outer_layout = QHBoxLayout(word_manipulation_container_widget)
word_manipulation_outer_layout.setContentsMargins(0, 0, 0, 0)
word_manipulation_outer_layout.setSpacing(15)
skip_words_widget = QWidget()
skip_words_vertical_layout = QVBoxLayout(skip_words_widget)
skip_words_vertical_layout.setContentsMargins(0, 0, 0, 0)
skip_words_vertical_layout.setSpacing(2)
main_app.skip_words_label_widget = QLabel()
skip_words_vertical_layout.addWidget(main_app.skip_words_label_widget)
skip_input_and_button_layout = QHBoxLayout()
skip_input_and_button_layout.setContentsMargins(0, 0, 0, 0)
skip_input_and_button_layout.setSpacing(10)
main_app.skip_words_input = QLineEdit()
main_app.skip_words_input.setPlaceholderText("e.g., WM, WIP, sketch, preview")
skip_input_and_button_layout.addWidget(main_app.skip_words_input, 1)
main_app.skip_scope_toggle_button = QPushButton()
main_app._update_skip_scope_button_text()
skip_input_and_button_layout.addWidget(main_app.skip_scope_toggle_button, 0)
skip_words_vertical_layout.addLayout(skip_input_and_button_layout)
word_manipulation_outer_layout.addWidget(skip_words_widget, 7)
remove_words_widget = QWidget()
remove_words_vertical_layout = QVBoxLayout(remove_words_widget)
remove_words_vertical_layout.setContentsMargins(0, 0, 0, 0)
remove_words_vertical_layout.setSpacing(2)
main_app.remove_from_filename_label_widget = QLabel()
remove_words_vertical_layout.addWidget(main_app.remove_from_filename_label_widget)
main_app.remove_from_filename_input = QLineEdit()
main_app.remove_from_filename_input.setPlaceholderText("e.g., patreon, HD")
remove_words_vertical_layout.addWidget(main_app.remove_from_filename_input)
word_manipulation_outer_layout.addWidget(remove_words_widget, 3)
left_layout.addWidget(word_manipulation_container_widget)
# --- File Filter Layout ---
file_filter_layout = QVBoxLayout()
file_filter_layout.setContentsMargins(0, 10, 0, 0)
file_filter_layout.addWidget(QLabel("Filter Files:"))
radio_button_layout = QHBoxLayout()
radio_button_layout.setSpacing(10)
main_app.radio_group = QButtonGroup(main_app)
main_app.radio_all = QRadioButton("All")
main_app.radio_images = QRadioButton("Images/GIFs")
main_app.radio_videos = QRadioButton("Videos")
main_app.radio_only_archives = QRadioButton("📦 Only Archives")
main_app.radio_only_audio = QRadioButton("🎧 Only Audio")
main_app.radio_only_links = QRadioButton("🔗 Only Links")
main_app.radio_more = QRadioButton("More")
main_app.radio_all.setChecked(True)
for btn in [main_app.radio_all, main_app.radio_images, main_app.radio_videos, main_app.radio_only_archives, main_app.radio_only_audio, main_app.radio_only_links, main_app.radio_more]:
main_app.radio_group.addButton(btn)
radio_button_layout.addWidget(btn)
main_app.favorite_mode_checkbox = QCheckBox()
main_app.favorite_mode_checkbox.setChecked(False)
radio_button_layout.addWidget(main_app.favorite_mode_checkbox)
radio_button_layout.addStretch(1)
file_filter_layout.addLayout(radio_button_layout)
left_layout.addLayout(file_filter_layout)
# --- Checkboxes Group ---
checkboxes_group_layout = QVBoxLayout()
checkboxes_group_layout.setSpacing(10)
row1_layout = QHBoxLayout()
row1_layout.setSpacing(10)
main_app.skip_zip_checkbox = QCheckBox("Skip .zip")
main_app.skip_zip_checkbox.setChecked(True)
row1_layout.addWidget(main_app.skip_zip_checkbox)
main_app.skip_rar_checkbox = QCheckBox("Skip .rar")
main_app.skip_rar_checkbox.setChecked(True)
row1_layout.addWidget(main_app.skip_rar_checkbox)
main_app.download_thumbnails_checkbox = QCheckBox("Download Thumbnails Only")
row1_layout.addWidget(main_app.download_thumbnails_checkbox)
main_app.scan_content_images_checkbox = QCheckBox("Scan Content for Images")
main_app.scan_content_images_checkbox.setChecked(main_app.scan_content_images_setting)
row1_layout.addWidget(main_app.scan_content_images_checkbox)
main_app.compress_images_checkbox = QCheckBox("Compress to WebP")
main_app.compress_images_checkbox.setToolTip("Compress images > 1.5MB to WebP format (requires Pillow).")
row1_layout.addWidget(main_app.compress_images_checkbox)
main_app.keep_duplicates_checkbox = QCheckBox("Keep Duplicates")
main_app.keep_duplicates_checkbox.setToolTip("If checked, downloads all files from a post even if they have the same name.")
row1_layout.addWidget(main_app.keep_duplicates_checkbox)
row1_layout.addStretch(1)
checkboxes_group_layout.addLayout(row1_layout)
# --- Advanced Settings ---
advanced_settings_label = QLabel("⚙️ Advanced Settings:")
checkboxes_group_layout.addWidget(advanced_settings_label)
advanced_row1_layout = QHBoxLayout()
advanced_row1_layout.setSpacing(10)
main_app.use_subfolders_checkbox = QCheckBox("Separate Folders by Name/Title")
main_app.use_subfolders_checkbox.setChecked(True)
main_app.use_subfolders_checkbox.toggled.connect(main_app.update_ui_for_subfolders)
advanced_row1_layout.addWidget(main_app.use_subfolders_checkbox)
main_app.use_subfolder_per_post_checkbox = QCheckBox("Subfolder per Post")
main_app.use_subfolder_per_post_checkbox.toggled.connect(main_app.update_ui_for_subfolders)
advanced_row1_layout.addWidget(main_app.use_subfolder_per_post_checkbox)
main_app.date_prefix_checkbox = QCheckBox("Date Prefix")
main_app.date_prefix_checkbox.setToolTip("When 'Subfolder per Post' is active, prefix the folder name with the post's upload date.")
advanced_row1_layout.addWidget(main_app.date_prefix_checkbox)
main_app.use_cookie_checkbox = QCheckBox("Use Cookie")
main_app.use_cookie_checkbox.setChecked(main_app.use_cookie_setting)
main_app.cookie_text_input = QLineEdit()
main_app.cookie_text_input.setPlaceholderText("if no Select cookies.txt)")
main_app.cookie_text_input.setText(main_app.cookie_text_setting)
advanced_row1_layout.addWidget(main_app.use_cookie_checkbox)
advanced_row1_layout.addWidget(main_app.cookie_text_input, 2)
main_app.cookie_browse_button = QPushButton("Browse...")
main_app.cookie_browse_button.setFixedWidth(80 * scale)
advanced_row1_layout.addWidget(main_app.cookie_browse_button)
advanced_row1_layout.addStretch(1)
checkboxes_group_layout.addLayout(advanced_row1_layout)
advanced_row2_layout = QHBoxLayout()
advanced_row2_layout.setSpacing(10)
multithreading_layout = QHBoxLayout()
multithreading_layout.setContentsMargins(0, 0, 0, 0)
main_app.use_multithreading_checkbox = QCheckBox("Use Multithreading")
main_app.use_multithreading_checkbox.setChecked(True)
multithreading_layout.addWidget(main_app.use_multithreading_checkbox)
main_app.thread_count_label = QLabel("Threads:")
multithreading_layout.addWidget(main_app.thread_count_label)
main_app.thread_count_input = QLineEdit("4")
main_app.thread_count_input.setFixedWidth(40 * scale)
main_app.thread_count_input.setValidator(QIntValidator(1, MAX_THREADS))
multithreading_layout.addWidget(main_app.thread_count_input)
advanced_row2_layout.addLayout(multithreading_layout)
main_app.external_links_checkbox = QCheckBox("Show External Links in Log")
advanced_row2_layout.addWidget(main_app.external_links_checkbox)
main_app.manga_mode_checkbox = QCheckBox("Manga/Comic Mode")
advanced_row2_layout.addWidget(main_app.manga_mode_checkbox)
advanced_row2_layout.addStretch(1)
checkboxes_group_layout.addLayout(advanced_row2_layout)
left_layout.addLayout(checkboxes_group_layout)
# --- Action Buttons ---
main_app.standard_action_buttons_widget = QWidget()
btn_layout = QHBoxLayout(main_app.standard_action_buttons_widget)
btn_layout.setContentsMargins(0, 10, 0, 0)
btn_layout.setSpacing(10)
main_app.download_btn = QPushButton("⬇️ Start Download")
main_app.download_btn.setStyleSheet("font-weight: bold;")
main_app.download_btn.clicked.connect(main_app.start_download)
main_app.pause_btn = QPushButton("⏸️ Pause Download")
main_app.pause_btn.setEnabled(False)
main_app.pause_btn.clicked.connect(main_app._handle_pause_resume_action)
main_app.cancel_btn = QPushButton("❌ Cancel & Reset UI")
main_app.cancel_btn.setEnabled(False)
main_app.cancel_btn.clicked.connect(main_app.cancel_download_button_action)
main_app.error_btn = QPushButton("Error")
main_app.error_btn.setToolTip("View files skipped due to errors and optionally retry them.")
main_app.error_btn.setEnabled(True)
btn_layout.addWidget(main_app.download_btn)
btn_layout.addWidget(main_app.pause_btn)
btn_layout.addWidget(main_app.cancel_btn)
btn_layout.addWidget(main_app.error_btn)
main_app.favorite_action_buttons_widget = QWidget()
favorite_buttons_layout = QHBoxLayout(main_app.favorite_action_buttons_widget)
main_app.favorite_mode_artists_button = QPushButton("🖼️ Favorite Artists")
main_app.favorite_mode_posts_button = QPushButton("📄 Favorite Posts")
main_app.favorite_scope_toggle_button = QPushButton()
favorite_buttons_layout.addWidget(main_app.favorite_mode_artists_button)
favorite_buttons_layout.addWidget(main_app.favorite_mode_posts_button)
favorite_buttons_layout.addWidget(main_app.favorite_scope_toggle_button)
main_app.bottom_action_buttons_stack = QStackedWidget()
main_app.bottom_action_buttons_stack.addWidget(main_app.standard_action_buttons_widget)
main_app.bottom_action_buttons_stack.addWidget(main_app.favorite_action_buttons_widget)
left_layout.addWidget(main_app.bottom_action_buttons_stack)
left_layout.addSpacing(10)
# --- Known Names Layout ---
known_chars_label_layout = QHBoxLayout()
known_chars_label_layout.setSpacing(10)
main_app.known_chars_label = QLabel("🎭 Known Shows/Characters (for Folder Names):")
known_chars_label_layout.addWidget(main_app.known_chars_label)
main_app.open_known_txt_button = QPushButton("Open Known.txt")
main_app.open_known_txt_button.setFixedWidth(120 * scale)
known_chars_label_layout.addWidget(main_app.open_known_txt_button)
main_app.character_search_input = QLineEdit()
main_app.character_search_input.setPlaceholderText("Search characters...")
known_chars_label_layout.addWidget(main_app.character_search_input, 1)
left_layout.addLayout(known_chars_label_layout)
main_app.character_list = QListWidget()
main_app.character_list.setSelectionMode(QListWidget.ExtendedSelection)
left_layout.addWidget(main_app.character_list, 1)
char_manage_layout = QHBoxLayout()
char_manage_layout.setSpacing(10)
main_app.new_char_input = QLineEdit()
main_app.new_char_input.setPlaceholderText("Add new show/character name")
main_app.add_char_button = QPushButton(" Add")
main_app.add_to_filter_button = QPushButton("⤵️ Add to Filter")
main_app.add_to_filter_button.setToolTip("Select names... to add to the 'Filter by Character(s)' field.")
main_app.delete_char_button = QPushButton("🗑️ Delete Selected")
main_app.delete_char_button.setToolTip("Delete the selected name(s)...")
main_app.add_char_button.clicked.connect(main_app._handle_ui_add_new_character)
main_app.new_char_input.returnPressed.connect(main_app.add_char_button.click)
main_app.delete_char_button.clicked.connect(main_app.delete_selected_character)
char_manage_layout.addWidget(main_app.new_char_input, 2)
char_manage_layout.addWidget(main_app.add_char_button, 0)
main_app.known_names_help_button = QPushButton("?")
main_app.known_names_help_button.setFixedWidth(45 * scale)
main_app.known_names_help_button.clicked.connect(main_app._show_feature_guide)
main_app.history_button = QPushButton("📜")
main_app.history_button.setFixedWidth(45 * scale)
main_app.history_button.setToolTip(main_app._tr("history_button_tooltip_text", "View download history"))
main_app.future_settings_button = QPushButton("⚙️")
main_app.future_settings_button.setFixedWidth(45 * scale)
main_app.future_settings_button.clicked.connect(main_app._show_future_settings_dialog)
char_manage_layout.addWidget(main_app.add_to_filter_button, 1)
char_manage_layout.addWidget(main_app.delete_char_button, 1)
char_manage_layout.addWidget(main_app.known_names_help_button, 0)
char_manage_layout.addWidget(main_app.history_button, 0)
char_manage_layout.addWidget(main_app.future_settings_button, 0)
left_layout.addLayout(char_manage_layout)
left_layout.addStretch(0)
# --- Right Panel (Logs) ---
right_panel_widget.setLayout(right_layout)
log_title_layout = QHBoxLayout()
main_app.progress_log_label = QLabel("📜 Progress Log:")
log_title_layout.addWidget(main_app.progress_log_label)
log_title_layout.addStretch(1)
main_app.link_search_input = QLineEdit()
main_app.link_search_input.setPlaceholderText("Search Links...")
main_app.link_search_input.setVisible(False)
log_title_layout.addWidget(main_app.link_search_input)
main_app.link_search_button = QPushButton("🔍")
main_app.link_search_button.setVisible(False)
main_app.link_search_button.setFixedWidth(30 * scale)
log_title_layout.addWidget(main_app.link_search_button)
main_app.manga_rename_toggle_button = QPushButton()
main_app.manga_rename_toggle_button.setVisible(False)
main_app.manga_rename_toggle_button.setFixedWidth(140 * scale)
main_app._update_manga_filename_style_button_text()
log_title_layout.addWidget(main_app.manga_rename_toggle_button)
main_app.manga_date_prefix_input = QLineEdit()
main_app.manga_date_prefix_input.setPlaceholderText("Prefix for Manga Filenames")
main_app.manga_date_prefix_input.setVisible(False)
log_title_layout.addWidget(main_app.manga_date_prefix_input)
main_app.multipart_toggle_button = QPushButton()
main_app.multipart_toggle_button.setToolTip("Toggle between Multi-part and Single-stream downloads for large files.")
main_app.multipart_toggle_button.setFixedWidth(130 * scale)
main_app._update_multipart_toggle_button_text()
log_title_layout.addWidget(main_app.multipart_toggle_button)
main_app.EYE_ICON = "\U0001F441"
main_app.CLOSED_EYE_ICON = "\U0001F648"
main_app.log_verbosity_toggle_button = QPushButton(main_app.EYE_ICON)
main_app.log_verbosity_toggle_button.setFixedWidth(45 * scale)
main_app.log_verbosity_toggle_button.setStyleSheet(f"font-size: {11 * scale}pt; padding: {4 * scale}px {2 * scale}px;")
log_title_layout.addWidget(main_app.log_verbosity_toggle_button)
main_app.reset_button = QPushButton("🔄 Reset")
main_app.reset_button.setFixedWidth(80 * scale)
log_title_layout.addWidget(main_app.reset_button)
right_layout.addLayout(log_title_layout)
main_app.log_splitter = QSplitter(Qt.Vertical)
main_app.log_view_stack = QStackedWidget()
main_app.main_log_output = QTextEdit()
main_app.main_log_output.setReadOnly(True)
main_app.main_log_output.setLineWrapMode(QTextEdit.NoWrap)
main_app.log_view_stack.addWidget(main_app.main_log_output)
main_app.missed_character_log_output = QTextEdit()
main_app.missed_character_log_output.setReadOnly(True)
main_app.missed_character_log_output.setLineWrapMode(QTextEdit.NoWrap)
main_app.log_view_stack.addWidget(main_app.missed_character_log_output)
main_app.external_log_output = QTextEdit()
main_app.external_log_output.setReadOnly(True)
main_app.external_log_output.setLineWrapMode(QTextEdit.NoWrap)
main_app.external_log_output.hide()
main_app.log_splitter.addWidget(main_app.log_view_stack)
main_app.log_splitter.addWidget(main_app.external_log_output)
main_app.log_splitter.setSizes([main_app.height(), 0])
right_layout.addWidget(main_app.log_splitter, 1)
export_button_layout = QHBoxLayout()
export_button_layout.addStretch(1)
main_app.export_links_button = QPushButton(main_app._tr("export_links_button_text", "Export Links"))
main_app.export_links_button.setFixedWidth(100 * scale)
main_app.export_links_button.setEnabled(False)
main_app.export_links_button.setVisible(False)
export_button_layout.addWidget(main_app.export_links_button)
main_app.download_extracted_links_button = QPushButton(main_app._tr("download_extracted_links_button_text", "Download"))
main_app.download_extracted_links_button.setFixedWidth(100 * scale)
main_app.download_extracted_links_button.setEnabled(False)
main_app.download_extracted_links_button.setVisible(False)
export_button_layout.addWidget(main_app.download_extracted_links_button)
main_app.log_display_mode_toggle_button = QPushButton()
main_app.log_display_mode_toggle_button.setFixedWidth(120 * scale)
main_app.log_display_mode_toggle_button.setVisible(False)
export_button_layout.addWidget(main_app.log_display_mode_toggle_button)
right_layout.addLayout(export_button_layout)
main_app.progress_label = QLabel("Progress: Idle")
main_app.progress_label.setStyleSheet("padding-top: 5px; font-style: italic;")
right_layout.addWidget(main_app.progress_label)
main_app.file_progress_label = QLabel("")
main_app.file_progress_label.setToolTip("Shows the progress of individual file downloads, including speed and size.")
main_app.file_progress_label.setWordWrap(True)
main_app.file_progress_label.setStyleSheet("padding-top: 2px; font-style: italic; color: #A0A0A0;")
right_layout.addWidget(main_app.file_progress_label)
# --- Final Assembly ---
main_app.main_splitter.addWidget(left_scroll_area)
main_app.main_splitter.addWidget(right_panel_widget)
main_app.main_splitter.setStretchFactor(0, 7)
main_app.main_splitter.setStretchFactor(1, 3)
top_level_layout = QHBoxLayout(main_app)
top_level_layout.setContentsMargins(0, 0, 0, 0)
top_level_layout.addWidget(main_app.main_splitter)
# --- Initial UI State Updates ---
main_app.update_ui_for_subfolders(main_app.use_subfolders_checkbox.isChecked())
main_app.update_external_links_setting(main_app.external_links_checkbox.isChecked())
main_app.update_multithreading_label(main_app.thread_count_input.text())
main_app.update_page_range_enabled_state()
if main_app.manga_mode_checkbox:
main_app.update_ui_for_manga_mode(main_app.manga_mode_checkbox.isChecked())
if hasattr(main_app, 'link_input'):
main_app.link_input.textChanged.connect(lambda: main_app.update_ui_for_manga_mode(main_app.manga_mode_checkbox.isChecked() if main_app.manga_mode_checkbox else False))
main_app._load_creator_name_cache_from_json()
main_app.load_known_names_from_util()
main_app._update_cookie_input_visibility(main_app.use_cookie_checkbox.isChecked() if hasattr(main_app, 'use_cookie_checkbox') else False)
main_app._handle_multithreading_toggle(main_app.use_multithreading_checkbox.isChecked())
if hasattr(main_app, 'radio_group') and main_app.radio_group.checkedButton():
main_app._handle_filter_mode_change(main_app.radio_group.checkedButton(), True)
main_app.radio_group.buttonToggled.connect(main_app._handle_more_options_toggled)
main_app._update_manga_filename_style_button_text()
main_app._update_skip_scope_button_text()
main_app._update_char_filter_scope_button_text()
main_app._update_multithreading_for_date_mode()
if hasattr(main_app, 'download_thumbnails_checkbox'):
main_app._handle_thumbnail_mode_change(main_app.download_thumbnails_checkbox.isChecked())
if hasattr(main_app, 'favorite_mode_checkbox'):
main_app._handle_favorite_mode_toggle(False)
def get_dark_theme(scale=1):
"""
Generates the stylesheet for the dark theme, scaled by the given factor.
"""
# Define base sizes
font_size_base = 10
font_size_small_base = 9.5
padding_base = 5
padding_small = 4
button_h_padding_base = 12
indicator_size_base = 14
# Apply scaling
font_size = font_size_base * scale
font_size_small = font_size_small_base * scale
line_edit_padding = padding_base * scale
button_padding_v = padding_base * scale
button_padding_h = button_h_padding_base * scale
tooltip_padding = padding_small * scale
return f"""
QWidget {{ background-color: #2E2E2E; color: #E0E0E0; font-family: Segoe UI, Arial, sans-serif; font-size: {font_size}pt; }}
QLineEdit, QListWidget {{ background-color: #3C3F41; border: 1px solid #5A5A5A; padding: {line_edit_padding}px; color: #F0F0F0; border-radius: 4px; }}
QTextEdit {{ background-color: #3C3F41; border: 1px solid #5A5A5A; padding: {line_edit_padding}px;
color: #F0F0F0; border-radius: 4px;
font-family: Consolas, Courier New, monospace; font-size: {font_size_small}pt; }}
QPushButton {{ background-color: #555; color: #F0F0F0; border: 1px solid #6A6A6A; padding: {button_padding_v}px {button_padding_h}px; border-radius: 4px; }}
QPushButton:hover {{ background-color: #656565; border: 1px solid #7A7A7A; }}
QPushButton:pressed {{ background-color: #4A4A4A; }}
QPushButton:disabled {{ background-color: #404040; color: #888; border-color: #555; }}
QLabel {{ font-weight: bold; padding-top: {4 * scale}px; padding-bottom: {2 * scale}px; color: #C0C0C0; }}
QRadioButton, QCheckBox {{ spacing: {5 * scale}px; color: #E0E0E0; padding-top: {4 * scale}px; padding-bottom: {4 * scale}px; }}
QRadioButton::indicator, QCheckBox::indicator {{ width: {indicator_size_base * scale}px; height: {indicator_size_base * scale}px; }}
QListWidget {{ alternate-background-color: #353535; border: 1px solid #5A5A5A; }}
QListWidget::item:selected {{ background-color: #007ACC; color: #FFFFFF; }}
QToolTip {{ background-color: #4A4A4A; color: #F0F0F0; border: 1px solid #6A6A6A; padding: {tooltip_padding}px; border-radius: 3px; }}
QSplitter::handle {{ background-color: #5A5A5A; }}
QSplitter::handle:horizontal {{ width: {5 * scale}px; }}
QSplitter::handle:vertical {{ height: {5 * scale}px; }}
QFrame[frameShape="4"], QFrame[frameShape="5"] {{
border: 1px solid #4A4A4A;
border-radius: 3px;
}}
"""
def apply_theme_to_app(main_app, theme_name, initial_load=False):
"""
Applies the selected theme and scaling to the main application window.
"""
main_app.current_theme = theme_name
if not initial_load:
main_app.settings.setValue(THEME_KEY, theme_name)
main_app.settings.sync()
if theme_name == "dark":
scale = getattr(main_app, 'scale_factor', 1)
main_app.setStyleSheet(get_dark_theme(scale))
if not initial_load:
main_app.log_signal.emit("🎨 Switched to Dark Mode.")
else:
main_app.setStyleSheet("")
if not initial_load:
main_app.log_signal.emit("🎨 Switched to Light Mode.")
main_app.update()