This commit is contained in:
Yuvi9587
2025-07-18 07:54:11 -07:00
parent 8ba2a572fa
commit 3935cbeea4
11 changed files with 720 additions and 611 deletions

View File

@@ -72,7 +72,7 @@ CONFIRM_ADD_ALL_CANCEL_DOWNLOAD = 3
# --- File Type Extensions --- # --- File Type Extensions ---
IMAGE_EXTENSIONS = { IMAGE_EXTENSIONS = {
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp', '.jpg', '.jpeg', '.jpe', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
'.heic', '.heif', '.svg', '.ico', '.jfif', '.pjpeg', '.pjp', '.avif' '.heic', '.heif', '.svg', '.ico', '.jfif', '.pjpeg', '.pjp', '.avif'
} }
VIDEO_EXTENSIONS = { VIDEO_EXTENSIONS = {

View File

@@ -74,7 +74,7 @@ class PostProcessorWorker:
def __init__(self, post_data, download_root, known_names, def __init__(self, post_data, download_root, known_names,
filter_character_list, emitter, filter_character_list, emitter,
unwanted_keywords, filter_mode, skip_zip, skip_rar, unwanted_keywords, filter_mode, skip_zip,
use_subfolders, use_post_subfolders, target_post_id_from_initial_url, custom_folder_name, use_subfolders, use_post_subfolders, target_post_id_from_initial_url, custom_folder_name,
compress_images, download_thumbnails, service, user_id, pause_event, compress_images, download_thumbnails, service, user_id, pause_event,
api_url_input, cancellation_event, api_url_input, cancellation_event,
@@ -121,7 +121,6 @@ class PostProcessorWorker:
self.unwanted_keywords = unwanted_keywords if unwanted_keywords is not None else set() self.unwanted_keywords = unwanted_keywords if unwanted_keywords is not None else set()
self.filter_mode = filter_mode self.filter_mode = filter_mode
self.skip_zip = skip_zip self.skip_zip = skip_zip
self.skip_rar = skip_rar
self.use_subfolders = use_subfolders self.use_subfolders = use_subfolders
self.use_post_subfolders = use_post_subfolders self.use_post_subfolders = use_post_subfolders
self.target_post_id_from_initial_url = target_post_id_from_initial_url self.target_post_id_from_initial_url = target_post_id_from_initial_url
@@ -394,13 +393,9 @@ class PostProcessorWorker:
if not is_audio_type: if not is_audio_type:
self.logger(f" -> Filter Skip: '{api_original_filename}' (Not Audio).") self.logger(f" -> Filter Skip: '{api_original_filename}' (Not Audio).")
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
if self.skip_zip and is_zip(api_original_filename): if (self.skip_zip) and is_archive(api_original_filename):
self.logger(f" -> Pref Skip: '{api_original_filename}' (ZIP).") self.logger(f" -> Pref Skip: '{api_original_filename}' (Archive).")
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
if self.skip_rar and is_rar(api_original_filename):
self.logger(f" -> Pref Skip: '{api_original_filename}' (RAR).")
return 0, 1, api_original_filename, False, FILE_DOWNLOAD_STATUS_SKIPPED, None
try: try:
os.makedirs(target_folder_path, exist_ok=True) os.makedirs(target_folder_path, exist_ok=True)
except OSError as e: except OSError as e:
@@ -568,7 +563,6 @@ class PostProcessorWorker:
if self._check_pause(f"Post-download hash check for '{api_original_filename}'"): if self._check_pause(f"Post-download hash check for '{api_original_filename}'"):
return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None return 0, 1, filename_to_save_in_main_path, was_original_name_kept_flag, FILE_DOWNLOAD_STATUS_SKIPPED, None
# --- Final Corrected Duplicate Handling Logic ---
should_skip = False should_skip = False
with self.downloaded_hash_counts_lock: with self.downloaded_hash_counts_lock:
current_count = self.downloaded_hash_counts.get(calculated_file_hash, 0) current_count = self.downloaded_hash_counts.get(calculated_file_hash, 0)
@@ -695,6 +689,7 @@ class PostProcessorWorker:
def process(self): def process(self):
result_tuple = (0, 0, [], [], [], None, None) result_tuple = (0, 0, [], [], [], None, None)
try: try:
if self._check_pause(f"Post processing for ID {self.post.get('id', 'N/A')}"): if self._check_pause(f"Post processing for ID {self.post.get('id', 'N/A')}"):
@@ -729,7 +724,8 @@ class PostProcessorWorker:
effective_unwanted_keywords_for_folder_naming.update(self.creator_download_folder_ignore_words) effective_unwanted_keywords_for_folder_naming.update(self.creator_download_folder_ignore_words)
post_content_html = post_data.get('content', '') post_content_html = post_data.get('content', '')
self.logger(f"\n--- Processing Post {post_id} ('{post_title[:50]}...') (Thread: {threading.current_thread().name}) ---") if not self.extract_links_only:
self.logger(f"\n--- Processing Post {post_id} ('{post_title[:50]}...') (Thread: {threading.current_thread().name}) ---")
num_potential_files_in_post = len(post_attachments or []) + (1 if post_main_file_info and post_main_file_info.get('path') else 0) num_potential_files_in_post = len(post_attachments or []) + (1 if post_main_file_info and post_main_file_info.get('path') else 0)
post_is_candidate_by_title_char_match = False post_is_candidate_by_title_char_match = False
@@ -1264,9 +1260,6 @@ class PostProcessorWorker:
else: else:
self.logger(f" ⚠️ Skipping invalid attachment {idx + 1} for post {post_id}: {str(att_info)[:100]}") self.logger(f" ⚠️ Skipping invalid attachment {idx + 1} for post {post_id}: {str(att_info)[:100]}")
# --- START: Conditionally de-duplicate files from API response ---
# Only de-duplicate by URL if we are in the default hash-skipping mode.
# If the user wants to keep everything, we must process all entries from the API.
if self.keep_duplicates_mode == DUPLICATE_HANDLING_HASH: if self.keep_duplicates_mode == DUPLICATE_HANDLING_HASH:
unique_files_by_url = {} unique_files_by_url = {}
for file_info in all_files_from_post_api: for file_info in all_files_from_post_api:
@@ -1281,7 +1274,6 @@ class PostProcessorWorker:
if new_count < original_count: if new_count < original_count:
self.logger(f" De-duplicated file list: Removed {original_count - new_count} redundant entries from the API response.") self.logger(f" De-duplicated file list: Removed {original_count - new_count} redundant entries from the API response.")
# --- END: Conditionally de-duplicate files from API response ---
if self.scan_content_for_images and post_content_html and not self.extract_links_only: if self.scan_content_for_images and post_content_html and not self.extract_links_only:
self.logger(f" Scanning post content for additional image URLs (Post ID: {post_id})...") self.logger(f" Scanning post content for additional image URLs (Post ID: {post_id})...")
@@ -1614,7 +1606,7 @@ class DownloadThread(QThread):
def __init__(self, api_url_input, output_dir, known_names_copy, def __init__(self, api_url_input, output_dir, known_names_copy,
cancellation_event, cancellation_event,
pause_event, filter_character_list=None, dynamic_character_filter_holder=None, pause_event, filter_character_list=None, dynamic_character_filter_holder=None,
filter_mode='all', skip_zip=True, skip_rar=True, filter_mode='all', skip_zip=True,
use_subfolders=True, use_post_subfolders=False, custom_folder_name=None, compress_images=False, use_subfolders=True, use_post_subfolders=False, custom_folder_name=None, compress_images=False,
download_thumbnails=False, service=None, user_id=None, download_thumbnails=False, service=None, user_id=None,
downloaded_files=None, downloaded_file_hashes=None, downloaded_files_lock=None, downloaded_file_hashes_lock=None, downloaded_files=None, downloaded_file_hashes=None, downloaded_files_lock=None, downloaded_file_hashes_lock=None,
@@ -1654,7 +1646,8 @@ class DownloadThread(QThread):
text_export_format='txt', text_export_format='txt',
single_pdf_mode=False, single_pdf_mode=False,
project_root_dir=None, project_root_dir=None,
processed_post_ids=None): processed_post_ids=None,
start_offset=0):
super().__init__() super().__init__()
self.api_url_input = api_url_input self.api_url_input = api_url_input
self.output_dir = output_dir self.output_dir = output_dir
@@ -1667,7 +1660,6 @@ class DownloadThread(QThread):
self.dynamic_filter_holder = dynamic_character_filter_holder self.dynamic_filter_holder = dynamic_character_filter_holder
self.filter_mode = filter_mode self.filter_mode = filter_mode
self.skip_zip = skip_zip self.skip_zip = skip_zip
self.skip_rar = skip_rar
self.use_subfolders = use_subfolders self.use_subfolders = use_subfolders
self.use_post_subfolders = use_post_subfolders self.use_post_subfolders = use_post_subfolders
self.custom_folder_name = custom_folder_name self.custom_folder_name = custom_folder_name
@@ -1717,7 +1709,8 @@ class DownloadThread(QThread):
self.text_export_format = text_export_format self.text_export_format = text_export_format
self.single_pdf_mode = single_pdf_mode self.single_pdf_mode = single_pdf_mode
self.project_root_dir = project_root_dir self.project_root_dir = project_root_dir
self.processed_post_ids = processed_post_ids if processed_post_ids is not None else [] self.processed_post_ids_set = set(processed_post_ids) if processed_post_ids is not None else set()
self.start_offset = start_offset
if self.compress_images and Image is None: if self.compress_images and Image is None:
self.logger("⚠️ Image compression disabled: Pillow library not found (DownloadThread).") self.logger("⚠️ Image compression disabled: Pillow library not found (DownloadThread).")
@@ -1730,7 +1723,9 @@ class DownloadThread(QThread):
def run(self): def run(self):
""" """
The main execution method for the single-threaded download process. The main execution method for the download process.
This version correctly uses the central `download_from_api` function
and explicitly maps all arguments to the PostProcessorWorker to prevent TypeErrors.
""" """
grand_total_downloaded_files = 0 grand_total_downloaded_files = 0
grand_total_skipped_files = 0 grand_total_skipped_files = 0
@@ -1749,6 +1744,7 @@ class DownloadThread(QThread):
worker_signals_obj.worker_finished_signal.connect(lambda result: None) worker_signals_obj.worker_finished_signal.connect(lambda result: None)
self.logger(" Starting post fetch (single-threaded download process)...") self.logger(" Starting post fetch (single-threaded download process)...")
post_generator = download_from_api( post_generator = download_from_api(
self.api_url_input, self.api_url_input,
logger=self.logger, logger=self.logger,
@@ -1762,109 +1758,105 @@ class DownloadThread(QThread):
selected_cookie_file=self.selected_cookie_file, selected_cookie_file=self.selected_cookie_file,
app_base_dir=self.app_base_dir, app_base_dir=self.app_base_dir,
manga_filename_style_for_sort_check=self.manga_filename_style if self.manga_mode_active else None, manga_filename_style_for_sort_check=self.manga_filename_style if self.manga_mode_active else None,
# --- FIX: ADDED A COMMA to the line above --- processed_post_ids=self.processed_post_ids_set
processed_post_ids=self.processed_post_ids
) )
for posts_batch_data in post_generator: for posts_batch_data in post_generator:
if self.isInterruptionRequested(): if self.isInterruptionRequested():
was_process_cancelled = True was_process_cancelled = True
break break
for individual_post_data in posts_batch_data: for individual_post_data in posts_batch_data:
if self.isInterruptionRequested(): if self.isInterruptionRequested():
was_process_cancelled = True was_process_cancelled = True
break break
post_processing_worker = PostProcessorWorker( # --- START OF FIX: Explicitly build the arguments dictionary ---
post_data=individual_post_data, # This robustly maps all thread attributes to the correct worker parameters.
download_root=self.output_dir, worker_args = {
known_names=self.known_names, 'post_data': individual_post_data,
filter_character_list=self.filter_character_list_objects_initial, 'emitter': worker_signals_obj,
dynamic_character_filter_holder=self.dynamic_filter_holder, 'download_root': self.output_dir,
unwanted_keywords=self.unwanted_keywords, 'known_names': self.known_names,
filter_mode=self.filter_mode, 'filter_character_list': self.filter_character_list_objects_initial,
skip_zip=self.skip_zip, skip_rar=self.skip_rar, 'dynamic_character_filter_holder': self.dynamic_filter_holder,
use_subfolders=self.use_subfolders, use_post_subfolders=self.use_post_subfolders, 'target_post_id_from_initial_url': self.initial_target_post_id,
target_post_id_from_initial_url=self.initial_target_post_id, 'num_file_threads': self.num_file_threads_for_worker,
custom_folder_name=self.custom_folder_name, 'processed_post_ids': list(self.processed_post_ids_set),
compress_images=self.compress_images, download_thumbnails=self.download_thumbnails, 'unwanted_keywords': self.unwanted_keywords,
service=self.service, user_id=self.user_id, 'filter_mode': self.filter_mode,
api_url_input=self.api_url_input, 'skip_zip': self.skip_zip,
pause_event=self.pause_event, 'use_subfolders': self.use_subfolders,
cancellation_event=self.cancellation_event, 'use_post_subfolders': self.use_post_subfolders,
emitter=worker_signals_obj, 'custom_folder_name': self.custom_folder_name,
downloaded_files=self.downloaded_files, 'compress_images': self.compress_images,
downloaded_file_hashes=self.downloaded_file_hashes, 'download_thumbnails': self.download_thumbnails,
downloaded_files_lock=self.downloaded_files_lock, 'service': self.service,
downloaded_file_hashes_lock=self.downloaded_file_hashes_lock, 'user_id': self.user_id,
skip_words_list=self.skip_words_list, 'api_url_input': self.api_url_input,
skip_words_scope=self.skip_words_scope, 'pause_event': self.pause_event,
show_external_links=self.show_external_links, 'cancellation_event': self.cancellation_event,
extract_links_only=self.extract_links_only, 'downloaded_files': self.downloaded_files,
num_file_threads=self.num_file_threads_for_worker, 'downloaded_file_hashes': self.downloaded_file_hashes,
skip_current_file_flag=self.skip_current_file_flag, 'downloaded_files_lock': self.downloaded_files_lock,
manga_mode_active=self.manga_mode_active, 'downloaded_file_hashes_lock': self.downloaded_file_hashes_lock,
manga_filename_style=self.manga_filename_style, 'skip_words_list': self.skip_words_list,
manga_date_prefix=self.manga_date_prefix, 'skip_words_scope': self.skip_words_scope,
char_filter_scope=self.char_filter_scope, 'show_external_links': self.show_external_links,
remove_from_filename_words_list=self.remove_from_filename_words_list, 'extract_links_only': self.extract_links_only,
allow_multipart_download=self.allow_multipart_download, 'skip_current_file_flag': self.skip_current_file_flag,
selected_cookie_file=self.selected_cookie_file, 'manga_mode_active': self.manga_mode_active,
app_base_dir=self.app_base_dir, 'manga_filename_style': self.manga_filename_style,
cookie_text=self.cookie_text, 'char_filter_scope': self.char_filter_scope,
override_output_dir=self.override_output_dir, 'remove_from_filename_words_list': self.remove_from_filename_words_list,
manga_global_file_counter_ref=self.manga_global_file_counter_ref, 'allow_multipart_download': self.allow_multipart_download,
use_cookie=self.use_cookie, 'cookie_text': self.cookie_text,
manga_date_file_counter_ref=self.manga_date_file_counter_ref, 'use_cookie': self.use_cookie,
use_date_prefix_for_subfolder=self.use_date_prefix_for_subfolder, 'override_output_dir': self.override_output_dir,
keep_in_post_duplicates=self.keep_in_post_duplicates, 'selected_cookie_file': self.selected_cookie_file,
keep_duplicates_mode=self.keep_duplicates_mode, 'app_base_dir': self.app_base_dir,
keep_duplicates_limit=self.keep_duplicates_limit, 'manga_date_prefix': self.manga_date_prefix,
downloaded_hash_counts=self.downloaded_hash_counts, 'manga_date_file_counter_ref': self.manga_date_file_counter_ref,
downloaded_hash_counts_lock=self.downloaded_hash_counts_lock, 'scan_content_for_images': self.scan_content_for_images,
creator_download_folder_ignore_words=self.creator_download_folder_ignore_words, 'creator_download_folder_ignore_words': self.creator_download_folder_ignore_words,
session_file_path=self.session_file_path, 'manga_global_file_counter_ref': self.manga_global_file_counter_ref,
session_lock=self.session_lock, 'use_date_prefix_for_subfolder': self.use_date_prefix_for_subfolder,
text_only_scope=self.text_only_scope, 'keep_in_post_duplicates': self.keep_in_post_duplicates,
text_export_format=self.text_export_format, 'keep_duplicates_mode': self.keep_duplicates_mode,
single_pdf_mode=self.single_pdf_mode, 'keep_duplicates_limit': self.keep_duplicates_limit,
project_root_dir=self.project_root_dir 'downloaded_hash_counts': self.downloaded_hash_counts,
) 'downloaded_hash_counts_lock': self.downloaded_hash_counts_lock,
try: 'session_file_path': self.session_file_path,
(dl_count, skip_count, kept_originals_this_post, 'session_lock': self.session_lock,
retryable_failures, permanent_failures, 'text_only_scope': self.text_only_scope,
history_data, temp_filepath) = post_processing_worker.process() 'text_export_format': self.text_export_format,
'single_pdf_mode': self.single_pdf_mode,
'project_root_dir': self.project_root_dir,
}
# --- END OF FIX ---
grand_total_downloaded_files += dl_count post_processing_worker = PostProcessorWorker(**worker_args)
grand_total_skipped_files += skip_count
if kept_originals_this_post: (dl_count, skip_count, kept_originals_this_post,
grand_list_of_kept_original_filenames.extend(kept_originals_this_post) retryable_failures, permanent_failures,
if retryable_failures: history_data, temp_filepath) = post_processing_worker.process()
self.retryable_file_failed_signal.emit(retryable_failures)
if history_data:
if len(self.history_candidates_buffer) < 8:
self.post_processed_for_history_signal.emit(history_data)
if permanent_failures:
self.permanent_file_failed_signal.emit(permanent_failures)
if self.single_pdf_mode and temp_filepath: grand_total_downloaded_files += dl_count
self.progress_signal.emit(f"TEMP_FILE_PATH:{temp_filepath}") grand_total_skipped_files += skip_count
if kept_originals_this_post:
grand_list_of_kept_original_filenames.extend(kept_originals_this_post)
if retryable_failures:
self.retryable_file_failed_signal.emit(retryable_failures)
if history_data:
self.post_processed_for_history_signal.emit(history_data)
if permanent_failures:
self.permanent_file_failed_signal.emit(permanent_failures)
if self.single_pdf_mode and temp_filepath:
self.progress_signal.emit(f"TEMP_FILE_PATH:{temp_filepath}")
except Exception as proc_err:
post_id_for_err = individual_post_data.get('id', 'N/A')
self.logger(f"❌ Error processing post {post_id_for_err} in DownloadThread: {proc_err}")
traceback.print_exc()
num_potential_files_est = len(individual_post_data.get('attachments', [])) + (
1 if individual_post_data.get('file') else 0)
grand_total_skipped_files += num_potential_files_est
if self.skip_current_file_flag and self.skip_current_file_flag.is_set():
self.skip_current_file_flag.clear()
self.logger(" Skip current file flag was processed and cleared by DownloadThread.")
self.msleep(10)
if was_process_cancelled: if was_process_cancelled:
break break
if not was_process_cancelled and not self.isInterruptionRequested(): if not was_process_cancelled and not self.isInterruptionRequested():
self.logger("✅ All posts processed or end of content reached by DownloadThread.") self.logger("✅ All posts processed or end of content reached by DownloadThread.")
@@ -1873,7 +1865,6 @@ class DownloadThread(QThread):
traceback.print_exc() traceback.print_exc()
finally: finally:
try: try:
# Disconnect signals
if worker_signals_obj: if worker_signals_obj:
worker_signals_obj.progress_signal.disconnect(self.progress_signal) worker_signals_obj.progress_signal.disconnect(self.progress_signal)
worker_signals_obj.file_download_status_signal.disconnect(self.file_download_status_signal) worker_signals_obj.file_download_status_signal.disconnect(self.file_download_status_signal)
@@ -1883,15 +1874,9 @@ class DownloadThread(QThread):
worker_signals_obj.file_successfully_downloaded_signal.disconnect(self.file_successfully_downloaded_signal) worker_signals_obj.file_successfully_downloaded_signal.disconnect(self.file_successfully_downloaded_signal)
except (TypeError, RuntimeError) as e: except (TypeError, RuntimeError) as e:
self.logger(f" Note during DownloadThread signal disconnection: {e}") self.logger(f" Note during DownloadThread signal disconnection: {e}")
# Emit the final signal with all collected results
self.finished_signal.emit(grand_total_downloaded_files, grand_total_skipped_files, self.isInterruptionRequested(), grand_list_of_kept_original_filenames) self.finished_signal.emit(grand_total_downloaded_files, grand_total_skipped_files, self.isInterruptionRequested(), grand_list_of_kept_original_filenames)
def receive_add_character_result (self ,result ):
with QMutexLocker (self .prompt_mutex ):
self ._add_character_response =result
self .logger (f" (DownloadThread) Received character prompt response: {'Yes (added/confirmed)'if result else 'No (declined/failed)'}")
class InterruptedError(Exception): class InterruptedError(Exception):
"""Custom exception for handling cancellations gracefully.""" """Custom exception for handling cancellations gracefully."""
pass pass

File diff suppressed because one or more lines are too long

View File

@@ -9,9 +9,7 @@ from PyQt5.QtWidgets import (
) )
# --- Local Application Imports --- # --- Local Application Imports ---
# This assumes the new project structure is in place.
from ...i18n.translator import get_translation 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 ..main_window import get_app_icon_object
from ...utils.resolution import get_dark_theme from ...utils.resolution import get_dark_theme
@@ -42,21 +40,15 @@ class DownloadExtractedLinksDialog(QDialog):
if not app_icon.isNull(): if not app_icon.isNull():
self.setWindowIcon(app_icon) self.setWindowIcon(app_icon)
# Set window size dynamically based on the parent window's size # --- START OF FIX ---
if parent: # Get the user-defined scale factor from the parent application.
parent_width = parent.width() scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
parent_height = parent.height()
# Use a scaling factor for different screen resolutions
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
scale_factor = screen_height / 768.0
base_min_w, base_min_h = 500, 400 # Define base dimensions and apply the correct scale factor.
scaled_min_w = int(base_min_w * scale_factor) base_width, base_height = 600, 450
scaled_min_h = int(base_min_h * scale_factor) self.setMinimumSize(int(base_width * scale_factor), int(base_height * scale_factor))
self.resize(int(base_width * scale_factor * 1.1), int(base_height * scale_factor * 1.1))
self.setMinimumSize(scaled_min_w, scaled_min_h) # --- END OF FIX ---
self.resize(max(int(parent_width * 0.6 * scale_factor), scaled_min_w),
max(int(parent_height * 0.7 * scale_factor), scaled_min_h))
# --- Initialize UI and Apply Theming --- # --- Initialize UI and Apply Theming ---
self._init_ui() self._init_ui()
@@ -186,4 +178,4 @@ class DownloadExtractedLinksDialog(QDialog):
self, self,
self._tr("no_selection_title", "No Selection"), self._tr("no_selection_title", "No Selection"),
self._tr("no_selection_message_links", "Please select at least one link to download.") self._tr("no_selection_message_links", "Please select at least one link to download.")
) )

View File

@@ -42,13 +42,11 @@ class ErrorFilesDialog(QDialog):
if app_icon and not app_icon.isNull(): if app_icon and not app_icon.isNull():
self.setWindowIcon(app_icon) self.setWindowIcon(app_icon)
# Set window size dynamically scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
scale_factor = screen_height / 1080.0 base_width, base_height = 550, 400
base_min_w, base_min_h = 500, 300 self.setMinimumSize(int(base_width * scale_factor), int(base_height * scale_factor))
scaled_min_w = int(base_min_w * scale_factor) self.resize(int(base_width * scale_factor * 1.1), int(base_height * scale_factor * 1.1))
scaled_min_h = int(base_min_h * scale_factor)
self.setMinimumSize(scaled_min_w, scaled_min_h)
# --- Initialize UI and Apply Theming --- # --- Initialize UI and Apply Theming ---
self._init_ui() self._init_ui()
@@ -232,4 +230,4 @@ class ErrorFilesDialog(QDialog):
self, self,
self._tr("error_files_export_error_title", "Export Error"), self._tr("error_files_export_error_title", "Export Error"),
self._tr("error_files_export_error_message", "Could not export...").format(error=str(e)) self._tr("error_files_export_error_message", "Could not export...").format(error=str(e))
) )

View File

@@ -20,17 +20,20 @@ class TourStepWidget(QWidget):
A custom widget representing a single step or page in the feature guide. A custom widget representing a single step or page in the feature guide.
It neatly formats a title and its corresponding content. It neatly formats a title and its corresponding content.
""" """
def __init__(self, title_text, content_text, parent=None): def __init__(self, title_text, content_text, parent=None, scale=1.0):
super().__init__(parent) super().__init__(parent)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20) layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(10) layout.setSpacing(10)
title_font_size = int(14 * scale)
content_font_size = int(11 * scale)
title_label = QLabel(title_text) title_label = QLabel(title_text)
title_label.setAlignment(Qt.AlignCenter) title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;") title_label.setStyleSheet(f"font-size: {title_font_size}pt; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
layout.addWidget(title_label) layout.addWidget(title_label)
scroll_area = QScrollArea() scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True) scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QFrame.NoFrame) scroll_area.setFrameShape(QFrame.NoFrame)
@@ -42,8 +45,8 @@ class TourStepWidget(QWidget):
content_label.setWordWrap(True) content_label.setWordWrap(True)
content_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) content_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
content_label.setTextFormat(Qt.RichText) content_label.setTextFormat(Qt.RichText)
content_label.setOpenExternalLinks(True) # Allow opening links in the content content_label.setOpenExternalLinks(True)
content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;") content_label.setStyleSheet(f"font-size: {content_font_size}pt; color: #C8C8C8; line-height: 1.8;")
scroll_area.setWidget(content_label) scroll_area.setWidget(content_label)
layout.addWidget(scroll_area, 1) layout.addWidget(scroll_area, 1)
@@ -56,27 +59,38 @@ class HelpGuideDialog (QDialog ):
self .steps_data =steps_data self .steps_data =steps_data
self .parent_app =parent_app self .parent_app =parent_app
app_icon =get_app_icon_object () scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
app_icon = get_app_icon_object()
if app_icon and not app_icon.isNull(): if app_icon and not app_icon.isNull():
self.setWindowIcon(app_icon) self.setWindowIcon(app_icon)
self .setModal (True ) self.setModal(True)
self .setFixedSize (650 ,600 ) self.resize(int(650 * scale), int(600 * scale))
dialog_font_size = int(11 * scale)
current_theme_style = ""
if hasattr(self.parent_app, 'current_theme') and self.parent_app.current_theme == "dark":
current_theme_style = get_dark_theme(scale)
else:
current_theme_style = f"""
QDialog {{ background-color: #F0F0F0; border: 1px solid #B0B0B0; }}
QLabel {{ color: #1E1E1E; }}
QPushButton {{
background-color: #E1E1E1;
color: #1E1E1E;
border: 1px solid #ADADAD;
padding: {int(8*scale)}px {int(15*scale)}px;
border-radius: 4px;
min-height: {int(25*scale)}px;
font-size: {dialog_font_size}pt;
}}
QPushButton:hover {{ background-color: #CACACA; }}
QPushButton:pressed {{ background-color: #B0B0B0; }}
"""
current_theme_style ="" self.setStyleSheet(current_theme_style)
if hasattr (self .parent_app ,'current_theme')and self .parent_app .current_theme =="dark":
if hasattr (self .parent_app ,'get_dark_theme'):
current_theme_style =self .parent_app .get_dark_theme ()
self .setStyleSheet (current_theme_style if current_theme_style else """
QDialog { background-color: #2E2E2E; border: 1px solid #5A5A5A; }
QLabel { color: #E0E0E0; }
QPushButton { background-color: #555; color: #F0F0F0; border: 1px solid #6A6A6A; padding: 8px 15px; border-radius: 4px; min-height: 25px; font-size: 11pt; }
QPushButton:hover { background-color: #656565; }
QPushButton:pressed { background-color: #4A4A4A; }
""")
self ._init_ui () self ._init_ui ()
if self .parent_app : if self .parent_app :
self .move (self .parent_app .geometry ().center ()-self .rect ().center ()) self .move (self .parent_app .geometry ().center ()-self .rect ().center ())
@@ -97,10 +111,11 @@ class HelpGuideDialog (QDialog ):
main_layout .addWidget (self .stacked_widget ,1 ) main_layout .addWidget (self .stacked_widget ,1 )
self .tour_steps_widgets =[] self .tour_steps_widgets =[]
for title ,content in self .steps_data : scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
step_widget =TourStepWidget (title ,content ) for title, content in self.steps_data:
self .tour_steps_widgets .append (step_widget ) step_widget = TourStepWidget(title, content, scale=scale)
self .stacked_widget .addWidget (step_widget ) self.tour_steps_widgets.append(step_widget)
self.stacked_widget.addWidget(step_widget)
self .setWindowTitle (self ._tr ("help_guide_dialog_title","Kemono Downloader - Feature Guide")) self .setWindowTitle (self ._tr ("help_guide_dialog_title","Kemono Downloader - Feature Guide"))
@@ -115,7 +130,6 @@ class HelpGuideDialog (QDialog ):
if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'): if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'):
assets_base_dir =sys ._MEIPASS assets_base_dir =sys ._MEIPASS
else : else :
# Go up three levels from this file's directory (src/ui/dialogs) to the project root
assets_base_dir =os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) assets_base_dir =os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
github_icon_path =os .path .join (assets_base_dir ,"assets","github.png") github_icon_path =os .path .join (assets_base_dir ,"assets","github.png")
@@ -126,7 +140,9 @@ class HelpGuideDialog (QDialog ):
self .instagram_button =QPushButton (QIcon (instagram_icon_path ),"") self .instagram_button =QPushButton (QIcon (instagram_icon_path ),"")
self .Discord_button =QPushButton (QIcon (discord_icon_path ),"") self .Discord_button =QPushButton (QIcon (discord_icon_path ),"")
icon_size =QSize (24 ,24 ) scale = self.parent_app.scale_factor if hasattr(self.parent_app, 'scale_factor') else 1.0
icon_dim = int(24 * scale)
icon_size = QSize(icon_dim, icon_dim)
self .github_button .setIconSize (icon_size ) self .github_button .setIconSize (icon_size )
self .instagram_button .setIconSize (icon_size ) self .instagram_button .setIconSize (icon_size )
self .Discord_button .setIconSize (icon_size ) self .Discord_button .setIconSize (icon_size )

View File

@@ -14,7 +14,6 @@ class KnownNamesFilterDialog(QDialog):
""" """
A dialog to select names from the Known.txt list to add to the main A dialog to select names from the Known.txt list to add to the main
character filter input field. This provides a convenient way for users character filter input field. This provides a convenient way for users
to reuse their saved names and groups for filtering downloads. to reuse their saved names and groups for filtering downloads.
""" """
@@ -40,11 +39,10 @@ class KnownNamesFilterDialog(QDialog):
# Set window size dynamically # Set window size dynamically
screen_geometry = QApplication.primaryScreen().availableGeometry() screen_geometry = QApplication.primaryScreen().availableGeometry()
scale_factor = getattr(self.parent_app, 'scale_factor', 1.0)
base_width, base_height = 460, 450 base_width, base_height = 460, 450
scale_factor_h = screen_geometry.height() / 1080.0 self.setMinimumSize(int(base_width * scale_factor), int(base_height * scale_factor))
effective_scale_factor = max(0.75, min(scale_factor_h, 1.5)) self.resize(int(base_width * scale_factor * 1.1), int(base_height * scale_factor * 1.1))
self.setMinimumSize(int(base_width * effective_scale_factor), int(base_height * effective_scale_factor))
self.resize(int(base_width * effective_scale_factor * 1.1), int(base_height * effective_scale_factor * 1.1))
# --- Initialize UI and Apply Theming --- # --- Initialize UI and Apply Theming ---
self._init_ui() self._init_ui()
@@ -153,4 +151,4 @@ class KnownNamesFilterDialog(QDialog):
def get_selected_entries(self): def get_selected_entries(self):
"""Returns the list of known name entries selected by the user.""" """Returns the list of known name entries selected by the user."""
return self.selected_entries_to_return return self.selected_entries_to_return

View File

@@ -74,4 +74,4 @@ def create_single_pdf_from_content(posts_data, output_filename, font_path, logge
return True return True
except Exception as e: except Exception as e:
logger(f"❌ A critical error occurred while saving the final PDF: {e}") logger(f"❌ A critical error occurred while saving the final PDF: {e}")
return False return False

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ MAX_FILENAME_COMPONENT_LENGTH = 150
# Sets of file extensions for quick type checking # Sets of file extensions for quick type checking
IMAGE_EXTENSIONS = { IMAGE_EXTENSIONS = {
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp', '.jpg', '.jpeg', '.jpe', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
'.heic', '.heif', '.svg', '.ico', '.jfif', '.pjpeg', '.pjp', '.avif' '.heic', '.heif', '.svg', '.ico', '.jfif', '.pjpeg', '.pjp', '.avif'
} }
VIDEO_EXTENSIONS = { VIDEO_EXTENSIONS = {

View File

@@ -24,19 +24,14 @@ def setup_ui(main_app):
Args: Args:
main_app: The instance of the main DownloaderApp. main_app: The instance of the main DownloaderApp.
""" """
# --- START: Modified Scaling Logic ---
# Force a fixed scale factor to disable UI scaling on high-DPI screens.
scale = float(main_app.settings.value(UI_SCALE_KEY, 1.0)) scale = float(main_app.settings.value(UI_SCALE_KEY, 1.0))
main_app.scale_factor = scale main_app.scale_factor = scale
# --- Set the global font size for the application ---
default_font = QApplication.font() default_font = QApplication.font()
base_font_size = 9 # Use a standard base size base_font_size = 9 # Use a standard base size
default_font.setPointSize(int(base_font_size * scale)) default_font.setPointSize(int(base_font_size * scale))
main_app.setFont(default_font) main_app.setFont(default_font)
# --- END: Modified Scaling Logic ---
# --- Set the global font size for the application ---
default_font = QApplication.font() default_font = QApplication.font()
base_font_size = 9 # Use a standard base size base_font_size = 9 # Use a standard base size
default_font.setPointSize(int(base_font_size * scale)) default_font.setPointSize(int(base_font_size * scale))
@@ -221,12 +216,10 @@ def setup_ui(main_app):
checkboxes_group_layout.setSpacing(10) checkboxes_group_layout.setSpacing(10)
row1_layout = QHBoxLayout() row1_layout = QHBoxLayout()
row1_layout.setSpacing(10) row1_layout.setSpacing(10)
main_app.skip_zip_checkbox = QCheckBox("Skip .zip") main_app.skip_zip_checkbox = QCheckBox("Skip archives")
main_app.skip_zip_checkbox.setToolTip("Skip Common Archives (Eg.. Zip, Rar, 7z)")
main_app.skip_zip_checkbox.setChecked(True) main_app.skip_zip_checkbox.setChecked(True)
row1_layout.addWidget(main_app.skip_zip_checkbox) 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") main_app.download_thumbnails_checkbox = QCheckBox("Download Thumbnails Only")
row1_layout.addWidget(main_app.download_thumbnails_checkbox) 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 = QCheckBox("Scan Content for Images")
@@ -246,7 +239,7 @@ def setup_ui(main_app):
checkboxes_group_layout.addWidget(advanced_settings_label) checkboxes_group_layout.addWidget(advanced_settings_label)
advanced_row1_layout = QHBoxLayout() advanced_row1_layout = QHBoxLayout()
advanced_row1_layout.setSpacing(10) advanced_row1_layout.setSpacing(10)
main_app.use_subfolders_checkbox = QCheckBox("Separate Folders by Name/Title") main_app.use_subfolders_checkbox = QCheckBox("Separate Folders by Known.txt")
main_app.use_subfolders_checkbox.setChecked(True) main_app.use_subfolders_checkbox.setChecked(True)
main_app.use_subfolders_checkbox.toggled.connect(main_app.update_ui_for_subfolders) main_app.use_subfolders_checkbox.toggled.connect(main_app.update_ui_for_subfolders)
advanced_row1_layout.addWidget(main_app.use_subfolders_checkbox) advanced_row1_layout.addWidget(main_app.use_subfolders_checkbox)