mirror of
https://github.com/Yuvi9587/Kemono-Downloader.git
synced 2025-12-29 16:14:44 +00:00
Commit
This commit is contained in:
177
src/ui/dialogs/ConfirmAddAllDialog.py
Normal file
177
src/ui/dialogs/ConfirmAddAllDialog.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
|
||||
QPushButton, QVBoxLayout
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
# This assumes the new project structure is in place.
|
||||
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
|
||||
|
||||
# --- Constants for Dialog Choices ---
|
||||
# These were moved from main.py to be self-contained within this module's context.
|
||||
CONFIRM_ADD_ALL_ACCEPTED = 1
|
||||
CONFIRM_ADD_ALL_SKIP_ADDING = 2
|
||||
CONFIRM_ADD_ALL_CANCEL_DOWNLOAD = 3
|
||||
|
||||
|
||||
class ConfirmAddAllDialog(QDialog):
|
||||
"""
|
||||
A dialog to confirm adding multiple new character/series names to Known.txt.
|
||||
It appears when the user provides filter names that are not already known,
|
||||
allowing them to persist these names for future use.
|
||||
"""
|
||||
|
||||
def __init__(self, new_filter_objects_list, parent_app, parent=None):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
new_filter_objects_list (list): A list of filter objects (dicts) to propose adding.
|
||||
parent_app (DownloaderApp): A reference to the main application window for theming and translations.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.parent_app = parent_app
|
||||
self.setModal(True)
|
||||
self.new_filter_objects_list = new_filter_objects_list
|
||||
# Default choice if the dialog is closed without a button press
|
||||
self.user_choice = CONFIRM_ADD_ALL_CANCEL_DOWNLOAD
|
||||
|
||||
# --- Basic Window Setup ---
|
||||
app_icon = get_app_icon_object()
|
||||
if app_icon and not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
# Set window size dynamically
|
||||
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
|
||||
scale_factor = screen_height / 768.0
|
||||
base_min_w, base_min_h = 480, 350
|
||||
scaled_min_w = int(base_min_w * scale_factor)
|
||||
scaled_min_h = int(base_min_h * scale_factor)
|
||||
self.setMinimumSize(scaled_min_w, scaled_min_h)
|
||||
|
||||
# --- Initialize UI and Apply Theming ---
|
||||
self._init_ui()
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts for the dialog."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
self.info_label = QLabel()
|
||||
self.info_label.setWordWrap(True)
|
||||
main_layout.addWidget(self.info_label)
|
||||
|
||||
self.names_list_widget = QListWidget()
|
||||
self._populate_list()
|
||||
main_layout.addWidget(self.names_list_widget)
|
||||
|
||||
# --- Selection Buttons ---
|
||||
selection_buttons_layout = QHBoxLayout()
|
||||
self.select_all_button = QPushButton()
|
||||
self.select_all_button.clicked.connect(self._select_all_items)
|
||||
selection_buttons_layout.addWidget(self.select_all_button)
|
||||
|
||||
self.deselect_all_button = QPushButton()
|
||||
self.deselect_all_button.clicked.connect(self._deselect_all_items)
|
||||
selection_buttons_layout.addWidget(self.deselect_all_button)
|
||||
selection_buttons_layout.addStretch()
|
||||
main_layout.addLayout(selection_buttons_layout)
|
||||
|
||||
# --- Action Buttons ---
|
||||
buttons_layout = QHBoxLayout()
|
||||
self.add_selected_button = QPushButton()
|
||||
self.add_selected_button.clicked.connect(self._accept_add_selected)
|
||||
self.add_selected_button.setDefault(True)
|
||||
buttons_layout.addWidget(self.add_selected_button)
|
||||
|
||||
self.skip_adding_button = QPushButton()
|
||||
self.skip_adding_button.clicked.connect(self._reject_skip_adding)
|
||||
buttons_layout.addWidget(self.skip_adding_button)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
self.cancel_download_button = QPushButton()
|
||||
self.cancel_download_button.clicked.connect(self._reject_cancel_download)
|
||||
buttons_layout.addWidget(self.cancel_download_button)
|
||||
|
||||
main_layout.addLayout(buttons_layout)
|
||||
|
||||
def _populate_list(self):
|
||||
"""Populates the list widget with the new names to be confirmed."""
|
||||
for filter_obj in self.new_filter_objects_list:
|
||||
item_text = filter_obj["name"]
|
||||
list_item = QListWidgetItem(item_text)
|
||||
list_item.setFlags(list_item.flags() | Qt.ItemIsUserCheckable)
|
||||
list_item.setCheckState(Qt.Checked)
|
||||
list_item.setData(Qt.UserRole, filter_obj)
|
||||
self.names_list_widget.addItem(list_item)
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper to get translation based on the main application's current language."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
"""Sets the text for all translatable UI elements."""
|
||||
self.setWindowTitle(self._tr("confirm_add_all_dialog_title", "Confirm Adding New Names"))
|
||||
self.info_label.setText(self._tr("confirm_add_all_info_label", "The following new names/groups..."))
|
||||
self.select_all_button.setText(self._tr("confirm_add_all_select_all_button", "Select All"))
|
||||
self.deselect_all_button.setText(self._tr("confirm_add_all_deselect_all_button", "Deselect All"))
|
||||
self.add_selected_button.setText(self._tr("confirm_add_all_add_selected_button", "Add Selected to Known.txt"))
|
||||
self.skip_adding_button.setText(self._tr("confirm_add_all_skip_adding_button", "Skip Adding These"))
|
||||
self.cancel_download_button.setText(self._tr("confirm_add_all_cancel_download_button", "Cancel Download"))
|
||||
|
||||
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())
|
||||
|
||||
def _select_all_items(self):
|
||||
"""Checks all items in the list."""
|
||||
for i in range(self.names_list_widget.count()):
|
||||
self.names_list_widget.item(i).setCheckState(Qt.Checked)
|
||||
|
||||
def _deselect_all_items(self):
|
||||
"""Unchecks all items in the list."""
|
||||
for i in range(self.names_list_widget.count()):
|
||||
self.names_list_widget.item(i).setCheckState(Qt.Unchecked)
|
||||
|
||||
def _accept_add_selected(self):
|
||||
"""Sets the user choice to the list of selected items and accepts the dialog."""
|
||||
selected_objects = []
|
||||
for i in range(self.names_list_widget.count()):
|
||||
item = self.names_list_widget.item(i)
|
||||
if item.checkState() == Qt.Checked:
|
||||
filter_obj = item.data(Qt.UserRole)
|
||||
if filter_obj:
|
||||
selected_objects.append(filter_obj)
|
||||
|
||||
self.user_choice = selected_objects
|
||||
self.accept()
|
||||
|
||||
def _reject_skip_adding(self):
|
||||
"""Sets the user choice to skip adding and rejects the dialog."""
|
||||
self.user_choice = CONFIRM_ADD_ALL_SKIP_ADDING
|
||||
self.reject()
|
||||
|
||||
def _reject_cancel_download(self):
|
||||
"""Sets the user choice to cancel the entire download and rejects the dialog."""
|
||||
self.user_choice = CONFIRM_ADD_ALL_CANCEL_DOWNLOAD
|
||||
self.reject()
|
||||
|
||||
def exec_(self):
|
||||
"""
|
||||
Overrides the default exec_ to handle the return value logic, ensuring a
|
||||
sensible default if no items are selected but the "Add" button is clicked.
|
||||
"""
|
||||
super().exec_()
|
||||
# If the user clicked "Add Selected" but didn't select any items, treat it as skipping.
|
||||
if isinstance(self.user_choice, list) and not self.user_choice:
|
||||
return CONFIRM_ADD_ALL_SKIP_ADDING
|
||||
return self.user_choice
|
||||
135
src/ui/dialogs/CookieHelpDialog.py
Normal file
135
src/ui/dialogs/CookieHelpDialog.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
from ...i18n.translator import get_translation
|
||||
from ..main_window import get_app_icon_object
|
||||
|
||||
|
||||
class CookieHelpDialog(QDialog):
|
||||
"""
|
||||
A dialog to explain how to get a cookies.txt file.
|
||||
It can be displayed as a simple informational popup or as a modal choice
|
||||
when cookies are required but not found.
|
||||
"""
|
||||
# Constants to define the user's choice from the dialog
|
||||
CHOICE_PROCEED_WITHOUT_COOKIES = 1
|
||||
CHOICE_CANCEL_DOWNLOAD = 2
|
||||
CHOICE_OK_INFO_ONLY = 3
|
||||
|
||||
def __init__(self, parent_app, parent=None, offer_download_without_option=False):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
parent_app (DownloaderApp): A reference to the main application window.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
offer_download_without_option (bool): If True, shows buttons to
|
||||
"Download without Cookies" and "Cancel Download". If False,
|
||||
shows only an "OK" button for informational purposes.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.parent_app = parent_app
|
||||
self.setModal(True)
|
||||
self.offer_download_without_option = offer_download_without_option
|
||||
self.user_choice = None
|
||||
|
||||
# --- Basic Window Setup ---
|
||||
app_icon = get_app_icon_object()
|
||||
if app_icon and not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
self.setMinimumWidth(500)
|
||||
|
||||
# --- Initialize UI and Apply Theming ---
|
||||
self._init_ui()
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts for the dialog."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
self.info_label = QLabel()
|
||||
self.info_label.setTextFormat(Qt.RichText)
|
||||
self.info_label.setOpenExternalLinks(True)
|
||||
self.info_label.setWordWrap(True)
|
||||
main_layout.addWidget(self.info_label)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch(1)
|
||||
|
||||
if self.offer_download_without_option:
|
||||
# Add buttons for making a choice
|
||||
self.download_without_button = QPushButton()
|
||||
self.download_without_button.clicked.connect(self._proceed_without_cookies)
|
||||
button_layout.addWidget(self.download_without_button)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
self.cancel_button.clicked.connect(self._cancel_download)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
else:
|
||||
# Add a simple OK button for informational display
|
||||
self.ok_button = QPushButton()
|
||||
self.ok_button.clicked.connect(self._ok_info_only)
|
||||
button_layout.addWidget(self.ok_button)
|
||||
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper to get translation based on the main application's current language."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
"""Sets the text for all translatable UI elements."""
|
||||
self.setWindowTitle(self._tr("cookie_help_dialog_title", "Cookie File Instructions"))
|
||||
|
||||
instruction_html = f"""
|
||||
{self._tr("cookie_help_instruction_intro", "<p>To use cookies...</p>")}
|
||||
{self._tr("cookie_help_how_to_get_title", "<p><b>How to get cookies.txt:</b></p>")}
|
||||
<ol>
|
||||
{self._tr("cookie_help_step1_extension_intro", "<li>Install extension...</li>")}
|
||||
{self._tr("cookie_help_step2_login", "<li>Go to website...</li>")}
|
||||
{self._tr("cookie_help_step3_click_icon", "<li>Click icon...</li>")}
|
||||
{self._tr("cookie_help_step4_export", "<li>Click export...</li>")}
|
||||
{self._tr("cookie_help_step5_save_file", "<li>Save file...</li>")}
|
||||
{self._tr("cookie_help_step6_app_intro", "<li>In this application:<ul>")}
|
||||
{self._tr("cookie_help_step6a_checkbox", "<li>Ensure checkbox...</li>")}
|
||||
{self._tr("cookie_help_step6b_browse", "<li>Click browse...</li>")}
|
||||
{self._tr("cookie_help_step6c_select", "<li>Select file...</li></ul></li>")}
|
||||
</ol>
|
||||
{self._tr("cookie_help_alternative_paste", "<p>Alternatively, paste...</p>")}
|
||||
"""
|
||||
self.info_label.setText(instruction_html)
|
||||
|
||||
if self.offer_download_without_option:
|
||||
self.download_without_button.setText(self._tr("cookie_help_proceed_without_button", "Download without Cookies"))
|
||||
self.cancel_button.setText(self._tr("cookie_help_cancel_download_button", "Cancel Download"))
|
||||
else:
|
||||
self.ok_button.setText(self._tr("ok_button", "OK"))
|
||||
|
||||
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())
|
||||
|
||||
def _proceed_without_cookies(self):
|
||||
"""Handles the user choice to proceed without using cookies."""
|
||||
self.user_choice = self.CHOICE_PROCEED_WITHOUT_COOKIES
|
||||
self.accept()
|
||||
|
||||
def _cancel_download(self):
|
||||
"""Handles the user choice to cancel the download."""
|
||||
self.user_choice = self.CHOICE_CANCEL_DOWNLOAD
|
||||
self.reject()
|
||||
|
||||
def _ok_info_only(self):
|
||||
"""Handles the acknowledgment when the dialog is purely informational."""
|
||||
self.user_choice = self.CHOICE_OK_INFO_ONLY
|
||||
self.accept()
|
||||
183
src/ui/dialogs/DownloadExtractedLinksDialog.py
Normal file
183
src/ui/dialogs/DownloadExtractedLinksDialog.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# --- Standard Library Imports ---
|
||||
from collections import defaultdict
|
||||
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import pyqtSignal, Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
|
||||
QMessageBox, QPushButton, QVBoxLayout, QAbstractItemView
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
# This assumes the new project structure is in place.
|
||||
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
|
||||
|
||||
|
||||
class DownloadExtractedLinksDialog(QDialog):
|
||||
"""
|
||||
A dialog to select and initiate the download for extracted, supported links
|
||||
from external cloud services like Mega, Google Drive, and Dropbox.
|
||||
"""
|
||||
|
||||
# Signal emitted with a list of selected link information dictionaries
|
||||
download_requested = pyqtSignal(list)
|
||||
|
||||
def __init__(self, links_data, parent_app, parent=None):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
links_data (list): A list of dictionaries, each containing info about an extracted link.
|
||||
parent_app (DownloaderApp): A reference to the main application window for theming and translations.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.links_data = links_data
|
||||
self.parent_app = parent_app
|
||||
|
||||
# --- Basic Window Setup ---
|
||||
app_icon = get_app_icon_object()
|
||||
if not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
# Set window size dynamically based on the parent window's size
|
||||
if parent:
|
||||
parent_width = parent.width()
|
||||
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
|
||||
scaled_min_w = int(base_min_w * scale_factor)
|
||||
scaled_min_h = int(base_min_h * scale_factor)
|
||||
|
||||
self.setMinimumSize(scaled_min_w, scaled_min_h)
|
||||
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 ---
|
||||
self._init_ui()
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts for the dialog."""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.main_info_label = QLabel()
|
||||
self.main_info_label.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
|
||||
self.main_info_label.setWordWrap(True)
|
||||
layout.addWidget(self.main_info_label)
|
||||
|
||||
self.links_list_widget = QListWidget()
|
||||
self.links_list_widget.setSelectionMode(QAbstractItemView.NoSelection)
|
||||
self._populate_list()
|
||||
layout.addWidget(self.links_list_widget)
|
||||
|
||||
# --- Control Buttons ---
|
||||
button_layout = QHBoxLayout()
|
||||
self.select_all_button = QPushButton()
|
||||
self.select_all_button.clicked.connect(lambda: self._set_all_items_checked(Qt.Checked))
|
||||
button_layout.addWidget(self.select_all_button)
|
||||
|
||||
self.deselect_all_button = QPushButton()
|
||||
self.deselect_all_button.clicked.connect(lambda: self._set_all_items_checked(Qt.Unchecked))
|
||||
button_layout.addWidget(self.deselect_all_button)
|
||||
button_layout.addStretch()
|
||||
|
||||
self.download_button = QPushButton()
|
||||
self.download_button.clicked.connect(self._handle_download_selected)
|
||||
self.download_button.setDefault(True)
|
||||
button_layout.addWidget(self.download_button)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def _populate_list(self):
|
||||
"""Populates the list widget with the provided links, grouped by post title."""
|
||||
grouped_links = defaultdict(list)
|
||||
for link_info_item in self.links_data:
|
||||
post_title_for_group = link_info_item.get('title', 'Untitled Post')
|
||||
grouped_links[post_title_for_group].append(link_info_item)
|
||||
|
||||
sorted_post_titles = sorted(grouped_links.keys(), key=lambda x: x.lower())
|
||||
|
||||
for post_title_key in sorted_post_titles:
|
||||
# Add a non-selectable header for each post
|
||||
header_item = QListWidgetItem(f"{post_title_key}")
|
||||
header_item.setFlags(Qt.NoItemFlags)
|
||||
font = header_item.font()
|
||||
font.setBold(True)
|
||||
font.setPointSize(font.pointSize() + 1)
|
||||
header_item.setFont(font)
|
||||
self.links_list_widget.addItem(header_item)
|
||||
|
||||
# Add checkable items for each link within that post
|
||||
for link_info_data in grouped_links[post_title_key]:
|
||||
platform_display = link_info_data.get('platform', 'unknown').upper()
|
||||
display_text = f" [{platform_display}] {link_info_data['link_text']} ({link_info_data['url']})"
|
||||
item = QListWidgetItem(display_text)
|
||||
item.setData(Qt.UserRole, link_info_data)
|
||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||
item.setCheckState(Qt.Checked)
|
||||
self.links_list_widget.addItem(item)
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper to get translation based on current app language."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
"""Sets the text for all translatable UI elements."""
|
||||
self.setWindowTitle(self._tr("download_external_links_dialog_title", "Download Selected External Links"))
|
||||
self.main_info_label.setText(self._tr("download_external_links_dialog_main_label", "Found {count} supported link(s)...").format(count=len(self.links_data)))
|
||||
self.select_all_button.setText(self._tr("select_all_button_text", "Select All"))
|
||||
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"
|
||||
|
||||
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
|
||||
if not item.flags() & Qt.ItemIsUserCheckable:
|
||||
item.setForeground(header_color)
|
||||
|
||||
def _set_all_items_checked(self, check_state):
|
||||
"""Sets the checked state for all checkable items in the list."""
|
||||
for i in range(self.links_list_widget.count()):
|
||||
item = self.links_list_widget.item(i)
|
||||
if item.flags() & Qt.ItemIsUserCheckable:
|
||||
item.setCheckState(check_state)
|
||||
|
||||
def _handle_download_selected(self):
|
||||
"""Gathers selected links and emits the download_requested signal."""
|
||||
selected_links = []
|
||||
for i in range(self.links_list_widget.count()):
|
||||
item = self.links_list_widget.item(i)
|
||||
if item.flags() & Qt.ItemIsUserCheckable and item.checkState() == Qt.Checked and item.data(Qt.UserRole) is not None:
|
||||
selected_links.append(item.data(Qt.UserRole))
|
||||
|
||||
if selected_links:
|
||||
self.download_requested.emit(selected_links)
|
||||
self.accept()
|
||||
else:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self._tr("no_selection_title", "No Selection"),
|
||||
self._tr("no_selection_message_links", "Please select at least one link to download.")
|
||||
)
|
||||
219
src/ui/dialogs/DownloadHistoryDialog.py
Normal file
219
src/ui/dialogs/DownloadHistoryDialog.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# --- Standard Library Imports ---
|
||||
import os
|
||||
import time
|
||||
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import Qt, QStandardPaths, QTimer
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QScrollArea,
|
||||
QPushButton, QVBoxLayout, QSplitter, QWidget, QGroupBox,
|
||||
QFileDialog, QMessageBox
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
from ...i18n.translator import get_translation
|
||||
from ..main_window import get_app_icon_object
|
||||
|
||||
|
||||
class DownloadHistoryDialog(QDialog):
|
||||
"""
|
||||
Dialog to display download history, showing the last few downloaded files
|
||||
and the first posts processed in the current session. It also allows
|
||||
exporting this history to a text file.
|
||||
"""
|
||||
|
||||
def __init__(self, last_downloaded_entries, first_processed_entries, parent_app, parent=None):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
last_downloaded_entries (list): A list of dicts for the last few files.
|
||||
first_processed_entries (list): A list of dicts for the first few posts.
|
||||
parent_app (DownloaderApp): A reference to the main application window.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.parent_app = parent_app
|
||||
self.last_3_downloaded_entries = last_downloaded_entries
|
||||
self.first_processed_entries = first_processed_entries
|
||||
self.setModal(True)
|
||||
|
||||
# --- Basic Window Setup ---
|
||||
app_icon = get_app_icon_object()
|
||||
if app_icon and not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
# Set window size dynamically
|
||||
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
|
||||
scale_factor = screen_height / 1080.0
|
||||
base_min_w, base_min_h = 600, 450
|
||||
scaled_min_w = int(base_min_w * 1.5 * scale_factor)
|
||||
scaled_min_h = int(base_min_h * scale_factor)
|
||||
self.setMinimumSize(scaled_min_w, scaled_min_h)
|
||||
self.resize(scaled_min_w, scaled_min_h + 100) # Give it a bit more height
|
||||
|
||||
# --- Initialize UI and Apply Theming ---
|
||||
self._init_ui()
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts for the dialog."""
|
||||
dialog_layout = QVBoxLayout(self)
|
||||
self.setLayout(dialog_layout)
|
||||
|
||||
self.main_splitter = QSplitter(Qt.Horizontal)
|
||||
dialog_layout.addWidget(self.main_splitter)
|
||||
|
||||
# --- Left Pane (Last Downloaded Files) ---
|
||||
left_pane_widget = self._create_history_pane(
|
||||
self.last_3_downloaded_entries,
|
||||
"history_last_downloaded_header", "Last 3 Files Downloaded:",
|
||||
self._format_last_downloaded_entry
|
||||
)
|
||||
self.main_splitter.addWidget(left_pane_widget)
|
||||
|
||||
# --- Right Pane (First Processed Posts) ---
|
||||
right_pane_widget = self._create_history_pane(
|
||||
self.first_processed_entries,
|
||||
"first_files_processed_header", "First {count} Posts Processed This Session:",
|
||||
self._format_first_processed_entry,
|
||||
count=len(self.first_processed_entries)
|
||||
)
|
||||
self.main_splitter.addWidget(right_pane_widget)
|
||||
|
||||
# --- Bottom Buttons ---
|
||||
bottom_button_layout = QHBoxLayout()
|
||||
self.save_history_button = QPushButton()
|
||||
self.save_history_button.clicked.connect(self._save_history_to_txt)
|
||||
bottom_button_layout.addStretch(1)
|
||||
bottom_button_layout.addWidget(self.save_history_button)
|
||||
dialog_layout.addLayout(bottom_button_layout)
|
||||
|
||||
# Set splitter sizes after the dialog is shown to ensure correct proportions
|
||||
QTimer.singleShot(0, lambda: self.main_splitter.setSizes([self.width() // 2, self.width() // 2]))
|
||||
|
||||
def _create_history_pane(self, entries, header_key, header_default, formatter_func, **kwargs):
|
||||
"""Creates a generic pane for displaying a list of history entries."""
|
||||
pane_widget = QWidget()
|
||||
layout = QVBoxLayout(pane_widget)
|
||||
header_text = self._tr(header_key, header_default).format(**kwargs)
|
||||
header_label = QLabel(header_text)
|
||||
header_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(header_label)
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_content_widget = QWidget()
|
||||
scroll_layout = QVBoxLayout(scroll_content_widget)
|
||||
|
||||
if not entries:
|
||||
no_history_label = QLabel(self._tr("no_download_history_header", "No History Yet"))
|
||||
no_history_label.setAlignment(Qt.AlignCenter)
|
||||
scroll_layout.addWidget(no_history_label)
|
||||
else:
|
||||
for entry in entries:
|
||||
group_box, details_label = formatter_func(entry)
|
||||
group_layout = QVBoxLayout(group_box)
|
||||
group_layout.addWidget(details_label)
|
||||
scroll_layout.addWidget(group_box)
|
||||
|
||||
scroll_area.setWidget(scroll_content_widget)
|
||||
layout.addWidget(scroll_area)
|
||||
return pane_widget
|
||||
|
||||
def _format_last_downloaded_entry(self, entry):
|
||||
"""Formats a single entry for the 'Last Downloaded Files' pane."""
|
||||
group_box = QGroupBox(f"{self._tr('history_file_label', 'File:')} {entry.get('disk_filename', 'N/A')}")
|
||||
details_text = (
|
||||
f"<b>{self._tr('history_from_post_label', 'From Post:')}</b> {entry.get('post_title', 'N/A')} (ID: {entry.get('post_id', 'N/A')})<br>"
|
||||
f"<b>{self._tr('history_creator_series_label', 'Creator/Series:')}</b> {entry.get('creator_display_name', 'N/A')}<br>"
|
||||
f"<b>{self._tr('history_post_uploaded_label', 'Post Uploaded:')}</b> {entry.get('upload_date_str', 'N/A')}<br>"
|
||||
f"<b>{self._tr('history_file_downloaded_label', 'File Downloaded:')}</b> {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(entry.get('download_timestamp', 0)))}<br>"
|
||||
f"<b>{self._tr('history_saved_in_folder_label', 'Saved In Folder:')}</b> {entry.get('download_path', 'N/A')}"
|
||||
)
|
||||
details_label = QLabel(details_text)
|
||||
details_label.setWordWrap(True)
|
||||
details_label.setTextFormat(Qt.RichText)
|
||||
return group_box, details_label
|
||||
|
||||
def _format_first_processed_entry(self, entry):
|
||||
"""Formats a single entry for the 'First Processed Posts' pane."""
|
||||
group_box = QGroupBox(f"{self._tr('history_post_label', 'Post:')} {entry.get('post_title', 'N/A')} (ID: {entry.get('post_id', 'N/A')})")
|
||||
details_text = (
|
||||
f"<b>{self._tr('history_creator_label', 'Creator:')}</b> {entry.get('creator_name', 'N/A')}<br>"
|
||||
f"<b>{self._tr('history_top_file_label', 'Top File:')}</b> {entry.get('top_file_name', 'N/A')}<br>"
|
||||
f"<b>{self._tr('history_num_files_label', 'Num Files in Post:')}</b> {entry.get('num_files', 0)}<br>"
|
||||
f"<b>{self._tr('history_post_uploaded_label', 'Post Uploaded:')}</b> {entry.get('upload_date_str', 'N/A')}<br>"
|
||||
f"<b>{self._tr('history_processed_on_label', 'Processed On:')}</b> {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(entry.get('download_date_timestamp', 0)))}<br>"
|
||||
f"<b>{self._tr('history_saved_to_folder_label', 'Saved To Folder:')}</b> {entry.get('download_location', 'N/A')}"
|
||||
)
|
||||
details_label = QLabel(details_text)
|
||||
details_label.setWordWrap(True)
|
||||
details_label.setTextFormat(Qt.RichText)
|
||||
return group_box, details_label
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper to get translation based on the main application's current language."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
"""Sets the text for all translatable UI elements."""
|
||||
self.setWindowTitle(self._tr("download_history_dialog_title_combined", "Download History"))
|
||||
self.save_history_button.setText(self._tr("history_save_button_text", "Save History to .txt"))
|
||||
|
||||
def _apply_theme(self):
|
||||
"""Applies the current theme from the parent application."""
|
||||
if self.parent_app and hasattr(self.parent_app, 'get_dark_theme') and self.parent_app.current_theme == "dark":
|
||||
self.setStyleSheet(self.parent_app.get_dark_theme())
|
||||
|
||||
def _save_history_to_txt(self):
|
||||
"""Saves the displayed history content to a user-selected text file."""
|
||||
if not self.last_3_downloaded_entries and not self.first_processed_entries:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self._tr("no_download_history_header", "No History Yet"),
|
||||
self._tr("history_nothing_to_save_message", "There is no history to save.")
|
||||
)
|
||||
return
|
||||
|
||||
# Suggest saving in the main download directory or Documents as a fallback
|
||||
main_download_dir = self.parent_app.dir_input.text().strip()
|
||||
default_save_dir = ""
|
||||
if main_download_dir and os.path.isdir(main_download_dir):
|
||||
default_save_dir = main_download_dir
|
||||
else:
|
||||
default_save_dir = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) or self.parent_app.app_base_dir
|
||||
|
||||
default_filepath = os.path.join(default_save_dir, "download_history.txt")
|
||||
|
||||
filepath, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
self._tr("history_save_dialog_title", "Save Download History"),
|
||||
default_filepath,
|
||||
"Text Files (*.txt);;All Files (*)"
|
||||
)
|
||||
|
||||
if not filepath:
|
||||
return
|
||||
|
||||
# Build the text content
|
||||
history_content = []
|
||||
# ... logic for formatting the text content would go here ...
|
||||
|
||||
try:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write("\n".join(history_content))
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self._tr("history_export_success_title", "History Export Successful"),
|
||||
self._tr("history_export_success_message", "Successfully exported to:\n{filepath}").format(filepath=filepath)
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
self._tr("history_export_error_title", "History Export Error"),
|
||||
self._tr("history_export_error_message", "Could not export: {error}").format(error=str(e))
|
||||
)
|
||||
1000
src/ui/dialogs/EmptyPopupDialog.py
Normal file
1000
src/ui/dialogs/EmptyPopupDialog.py
Normal file
File diff suppressed because it is too large
Load Diff
230
src/ui/dialogs/ErrorFilesDialog.py
Normal file
230
src/ui/dialogs/ErrorFilesDialog.py
Normal file
@@ -0,0 +1,230 @@
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import pyqtSignal, Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
|
||||
QMessageBox, QPushButton, QVBoxLayout, QAbstractItemView, QFileDialog
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
from ...i18n.translator import get_translation
|
||||
from ..assets import get_app_icon_object
|
||||
# Corrected Import: The filename uses PascalCase.
|
||||
from .ExportOptionsDialog import ExportOptionsDialog
|
||||
|
||||
|
||||
class ErrorFilesDialog(QDialog):
|
||||
"""
|
||||
Dialog to display files that were skipped due to errors and
|
||||
allows the user to retry downloading them or export the list of URLs.
|
||||
"""
|
||||
|
||||
# Signal emitted with a list of file info dictionaries to retry
|
||||
retry_selected_signal = pyqtSignal(list)
|
||||
|
||||
def __init__(self, error_files_info_list, parent_app, parent=None):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
error_files_info_list (list): A list of dictionaries, each containing
|
||||
info about a failed file.
|
||||
parent_app (DownloaderApp): A reference to the main application window
|
||||
for theming and translations.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.parent_app = parent_app
|
||||
self.setModal(True)
|
||||
self.error_files = error_files_info_list
|
||||
|
||||
# --- Basic Window Setup ---
|
||||
app_icon = get_app_icon_object()
|
||||
if app_icon and not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
# Set window size dynamically
|
||||
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
|
||||
scale_factor = screen_height / 1080.0
|
||||
base_min_w, base_min_h = 500, 300
|
||||
scaled_min_w = int(base_min_w * scale_factor)
|
||||
scaled_min_h = int(base_min_h * scale_factor)
|
||||
self.setMinimumSize(scaled_min_w, scaled_min_h)
|
||||
|
||||
# --- Initialize UI and Apply Theming ---
|
||||
self._init_ui()
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts for the dialog."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
self.info_label = QLabel()
|
||||
self.info_label.setWordWrap(True)
|
||||
main_layout.addWidget(self.info_label)
|
||||
|
||||
if self.error_files:
|
||||
self.files_list_widget = QListWidget()
|
||||
self.files_list_widget.setSelectionMode(QAbstractItemView.NoSelection)
|
||||
self._populate_list()
|
||||
main_layout.addWidget(self.files_list_widget)
|
||||
|
||||
# --- Control Buttons ---
|
||||
buttons_layout = QHBoxLayout()
|
||||
self.select_all_button = QPushButton()
|
||||
self.select_all_button.clicked.connect(self._select_all_items)
|
||||
buttons_layout.addWidget(self.select_all_button)
|
||||
|
||||
self.retry_button = QPushButton()
|
||||
self.retry_button.clicked.connect(self._handle_retry_selected)
|
||||
buttons_layout.addWidget(self.retry_button)
|
||||
|
||||
self.export_button = QPushButton()
|
||||
self.export_button.clicked.connect(self._handle_export_errors_to_txt)
|
||||
buttons_layout.addWidget(self.export_button)
|
||||
buttons_layout.addStretch(1)
|
||||
|
||||
self.ok_button = QPushButton()
|
||||
self.ok_button.clicked.connect(self.accept)
|
||||
self.ok_button.setDefault(True)
|
||||
buttons_layout.addWidget(self.ok_button)
|
||||
main_layout.addLayout(buttons_layout)
|
||||
|
||||
# Enable/disable buttons based on whether there are errors
|
||||
has_errors = bool(self.error_files)
|
||||
self.select_all_button.setEnabled(has_errors)
|
||||
self.retry_button.setEnabled(has_errors)
|
||||
self.export_button.setEnabled(has_errors)
|
||||
|
||||
def _populate_list(self):
|
||||
"""Populates the list widget with details of the failed files."""
|
||||
for error_info in self.error_files:
|
||||
filename = error_info.get('forced_filename_override',
|
||||
error_info.get('file_info', {}).get('name', 'Unknown Filename'))
|
||||
post_title = error_info.get('post_title', 'Unknown Post')
|
||||
post_id = error_info.get('original_post_id_for_log', 'N/A')
|
||||
|
||||
item_text = f"File: {filename}\nFrom Post: '{post_title}' (ID: {post_id})"
|
||||
list_item = QListWidgetItem(item_text)
|
||||
list_item.setData(Qt.UserRole, error_info)
|
||||
list_item.setFlags(list_item.flags() | Qt.ItemIsUserCheckable)
|
||||
list_item.setCheckState(Qt.Unchecked)
|
||||
self.files_list_widget.addItem(list_item)
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper to get translation based on the main application's current language."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
"""Sets the text for all translatable UI elements."""
|
||||
self.setWindowTitle(self._tr("error_files_dialog_title", "Files Skipped Due to Errors"))
|
||||
if not self.error_files:
|
||||
self.info_label.setText(self._tr("error_files_no_errors_label", "No files were recorded as skipped..."))
|
||||
else:
|
||||
self.info_label.setText(self._tr("error_files_found_label", "The following {count} file(s)...").format(count=len(self.error_files)))
|
||||
|
||||
self.select_all_button.setText(self._tr("error_files_select_all_button", "Select All"))
|
||||
self.retry_button.setText(self._tr("error_files_retry_selected_button", "Retry Selected"))
|
||||
self.export_button.setText(self._tr("error_files_export_urls_button", "Export URLs to .txt"))
|
||||
self.ok_button.setText(self._tr("ok_button", "OK"))
|
||||
|
||||
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())
|
||||
|
||||
def _select_all_items(self):
|
||||
"""Checks all items in the list."""
|
||||
if hasattr(self, 'files_list_widget'):
|
||||
for i in range(self.files_list_widget.count()):
|
||||
self.files_list_widget.item(i).setCheckState(Qt.Checked)
|
||||
|
||||
def _handle_retry_selected(self):
|
||||
"""Gathers selected files and emits the retry signal."""
|
||||
if not hasattr(self, 'files_list_widget'):
|
||||
return
|
||||
|
||||
selected_files_for_retry = [
|
||||
self.files_list_widget.item(i).data(Qt.UserRole)
|
||||
for i in range(self.files_list_widget.count())
|
||||
if self.files_list_widget.item(i).checkState() == Qt.Checked
|
||||
]
|
||||
|
||||
if selected_files_for_retry:
|
||||
self.retry_selected_signal.emit(selected_files_for_retry)
|
||||
self.accept()
|
||||
else:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self._tr("fav_artists_no_selection_title", "No Selection"),
|
||||
self._tr("error_files_no_selection_retry_message", "Please select at least one file to retry.")
|
||||
)
|
||||
|
||||
def _handle_export_errors_to_txt(self):
|
||||
"""Exports the URLs of failed files to a text file."""
|
||||
if not self.error_files:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self._tr("error_files_no_errors_export_title", "No Errors"),
|
||||
self._tr("error_files_no_errors_export_message", "There are no error file URLs to export.")
|
||||
)
|
||||
return
|
||||
|
||||
options_dialog = ExportOptionsDialog(parent_app=self.parent_app, parent=self)
|
||||
if not options_dialog.exec_() == QDialog.Accepted:
|
||||
return
|
||||
|
||||
export_option = options_dialog.get_selected_option()
|
||||
|
||||
lines_to_export = []
|
||||
for error_item in self.error_files:
|
||||
file_info = error_item.get('file_info', {})
|
||||
url = file_info.get('url')
|
||||
|
||||
if url:
|
||||
if export_option == ExportOptionsDialog.EXPORT_MODE_WITH_DETAILS:
|
||||
original_filename = file_info.get('name', 'Unknown Filename')
|
||||
post_title = error_item.get('post_title', 'Unknown Post')
|
||||
post_id = error_item.get('original_post_id_for_log', 'N/A')
|
||||
details_string = f" [Post: '{post_title}' (ID: {post_id}), File: '{original_filename}']"
|
||||
lines_to_export.append(f"{url}{details_string}")
|
||||
else:
|
||||
lines_to_export.append(url)
|
||||
|
||||
if not lines_to_export:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self._tr("error_files_no_urls_found_export_title", "No URLs Found"),
|
||||
self._tr("error_files_no_urls_found_export_message", "Could not extract any URLs...")
|
||||
)
|
||||
return
|
||||
|
||||
default_filename = "error_file_links.txt"
|
||||
filepath, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
self._tr("error_files_save_dialog_title", "Save Error File URLs"),
|
||||
default_filename,
|
||||
"Text Files (*.txt);;All Files (*)"
|
||||
)
|
||||
|
||||
if filepath:
|
||||
try:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
for line in lines_to_export:
|
||||
f.write(f"{line}\n")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
self._tr("error_files_export_success_title", "Export Successful"),
|
||||
self._tr("error_files_export_success_message", "Successfully exported...").format(
|
||||
count=len(lines_to_export), filepath=filepath
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
self._tr("error_files_export_error_title", "Export Error"),
|
||||
self._tr("error_files_export_error_message", "Could not export...").format(error=str(e))
|
||||
)
|
||||
118
src/ui/dialogs/ExportOptionsDialog.py
Normal file
118
src/ui/dialogs/ExportOptionsDialog.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
|
||||
QRadioButton, QButtonGroup
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
# This assumes the new project structure is in place.
|
||||
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
|
||||
|
||||
|
||||
class ExportOptionsDialog(QDialog):
|
||||
"""
|
||||
Dialog to choose the export format for error file links.
|
||||
It allows the user to select between exporting only the URLs or
|
||||
exporting URLs with additional details.
|
||||
"""
|
||||
# Constants to define the export modes
|
||||
EXPORT_MODE_LINK_ONLY = 1
|
||||
EXPORT_MODE_WITH_DETAILS = 2
|
||||
|
||||
def __init__(self, parent_app, parent=None):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
parent_app (DownloaderApp): A reference to the main application window for theming and translations.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.parent_app = parent_app
|
||||
self.setModal(True)
|
||||
# Default option
|
||||
self.selected_option = self.EXPORT_MODE_LINK_ONLY
|
||||
|
||||
# --- Basic Window Setup ---
|
||||
app_icon = get_app_icon_object()
|
||||
if app_icon and not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
# Set window size dynamically
|
||||
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
|
||||
scale_factor = screen_height / 768.0
|
||||
base_min_w = 350
|
||||
scaled_min_w = int(base_min_w * scale_factor)
|
||||
self.setMinimumWidth(scaled_min_w)
|
||||
|
||||
# --- Initialize UI and Apply Theming ---
|
||||
self._init_ui()
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts for the dialog."""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.description_label = QLabel()
|
||||
layout.addWidget(self.description_label)
|
||||
|
||||
self.radio_group = QButtonGroup(self)
|
||||
|
||||
self.radio_link_only = QRadioButton()
|
||||
self.radio_link_only.setChecked(True)
|
||||
self.radio_group.addButton(self.radio_link_only, self.EXPORT_MODE_LINK_ONLY)
|
||||
layout.addWidget(self.radio_link_only)
|
||||
|
||||
self.radio_with_details = QRadioButton()
|
||||
self.radio_group.addButton(self.radio_with_details, self.EXPORT_MODE_WITH_DETAILS)
|
||||
layout.addWidget(self.radio_with_details)
|
||||
|
||||
# --- Action Buttons ---
|
||||
button_layout = QHBoxLayout()
|
||||
self.export_button = QPushButton()
|
||||
self.export_button.clicked.connect(self._handle_export)
|
||||
self.export_button.setDefault(True)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
|
||||
button_layout.addStretch(1)
|
||||
button_layout.addWidget(self.export_button)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper to get translation based on the main application's current language."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
"""Sets the text for all translatable UI elements."""
|
||||
self.setWindowTitle(self._tr("export_options_dialog_title", "Export Options"))
|
||||
self.description_label.setText(self._tr("export_options_description_label", "Choose the format for exporting error file links:"))
|
||||
self.radio_link_only.setText(self._tr("export_options_radio_link_only", "Link per line (URL only)"))
|
||||
self.radio_link_only.setToolTip(self._tr("export_options_radio_link_only_tooltip", "Exports only the direct download URL..."))
|
||||
self.radio_with_details.setText(self._tr("export_options_radio_with_details", "Export with details (URL [Post, File info])"))
|
||||
self.radio_with_details.setToolTip(self._tr("export_options_radio_with_details_tooltip", "Exports the URL followed by details..."))
|
||||
self.export_button.setText(self._tr("export_options_export_button", "Export"))
|
||||
self.cancel_button.setText(self._tr("fav_posts_cancel_button", "Cancel"))
|
||||
|
||||
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())
|
||||
|
||||
def _handle_export(self):
|
||||
"""Sets the selected export option and accepts the dialog."""
|
||||
self.selected_option = self.radio_group.checkedId()
|
||||
self.accept()
|
||||
|
||||
def get_selected_option(self):
|
||||
"""Returns the export mode chosen by the user."""
|
||||
return self.selected_option
|
||||
288
src/ui/dialogs/FavoriteArtistsDialog.py
Normal file
288
src/ui/dialogs/FavoriteArtistsDialog.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# --- Standard Library Imports ---
|
||||
import html
|
||||
import re
|
||||
|
||||
# --- Third-Party Library Imports ---
|
||||
import requests
|
||||
from PyQt5.QtCore import QCoreApplication, Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget,
|
||||
QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
from ...i18n.translator import get_translation
|
||||
# Corrected Import: Get the icon from the new assets utility module
|
||||
from ..assets import get_app_icon_object
|
||||
from ...utils.network_utils import prepare_cookies_for_request
|
||||
from .CookieHelpDialog import CookieHelpDialog
|
||||
|
||||
|
||||
class FavoriteArtistsDialog (QDialog ):
|
||||
"""Dialog to display and select favorite artists."""
|
||||
def __init__ (self ,parent_app ,cookies_config ):
|
||||
super ().__init__ (parent_app )
|
||||
self .parent_app =parent_app
|
||||
self .cookies_config =cookies_config
|
||||
self .all_fetched_artists =[]
|
||||
|
||||
app_icon =get_app_icon_object ()
|
||||
if not app_icon .isNull ():
|
||||
self .setWindowIcon (app_icon )
|
||||
self .selected_artist_urls =[]
|
||||
|
||||
self .setModal (True )
|
||||
self .setMinimumSize (500 ,500 )
|
||||
|
||||
self ._init_ui ()
|
||||
self ._fetch_favorite_artists ()
|
||||
|
||||
def _get_domain_for_service (self ,service_name ):
|
||||
service_lower =service_name .lower ()
|
||||
coomer_primary_services ={'onlyfans','fansly','manyvids','candfans'}
|
||||
if service_lower in coomer_primary_services :
|
||||
return "coomer.su"
|
||||
else :
|
||||
return "kemono.su"
|
||||
|
||||
def _tr (self ,key ,default_text =""):
|
||||
"""Helper to get translation based on current app language."""
|
||||
if callable (get_translation )and self .parent_app :
|
||||
return get_translation (self .parent_app .current_selected_language ,key ,default_text )
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui (self ):
|
||||
self .setWindowTitle (self ._tr ("fav_artists_dialog_title","Favorite Artists"))
|
||||
self .status_label .setText (self ._tr ("fav_artists_loading_status","Loading favorite artists..."))
|
||||
self .search_input .setPlaceholderText (self ._tr ("fav_artists_search_placeholder","Search artists..."))
|
||||
self .select_all_button .setText (self ._tr ("fav_artists_select_all_button","Select All"))
|
||||
self .deselect_all_button .setText (self ._tr ("fav_artists_deselect_all_button","Deselect All"))
|
||||
self .download_button .setText (self ._tr ("fav_artists_download_selected_button","Download Selected"))
|
||||
self .cancel_button .setText (self ._tr ("fav_artists_cancel_button","Cancel"))
|
||||
|
||||
def _init_ui (self ):
|
||||
main_layout =QVBoxLayout (self )
|
||||
|
||||
self .status_label =QLabel ()
|
||||
self .status_label .setAlignment (Qt .AlignCenter )
|
||||
main_layout .addWidget (self .status_label )
|
||||
|
||||
self .search_input =QLineEdit ()
|
||||
self .search_input .textChanged .connect (self ._filter_artist_list_display )
|
||||
main_layout .addWidget (self .search_input )
|
||||
|
||||
|
||||
self .artist_list_widget =QListWidget ()
|
||||
self .artist_list_widget .setStyleSheet ("""
|
||||
QListWidget::item {
|
||||
border-bottom: 1px solid #4A4A4A; /* Slightly softer line */
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}""")
|
||||
main_layout .addWidget (self .artist_list_widget )
|
||||
self .artist_list_widget .setAlternatingRowColors (True )
|
||||
self .search_input .setVisible (False )
|
||||
self .artist_list_widget .setVisible (False )
|
||||
|
||||
combined_buttons_layout =QHBoxLayout ()
|
||||
|
||||
self .select_all_button =QPushButton ()
|
||||
self .select_all_button .clicked .connect (self ._select_all_items )
|
||||
combined_buttons_layout .addWidget (self .select_all_button )
|
||||
|
||||
self .deselect_all_button =QPushButton ()
|
||||
self .deselect_all_button .clicked .connect (self ._deselect_all_items )
|
||||
combined_buttons_layout .addWidget (self .deselect_all_button )
|
||||
|
||||
|
||||
self .download_button =QPushButton ()
|
||||
self .download_button .clicked .connect (self ._accept_selection_action )
|
||||
self .download_button .setEnabled (False )
|
||||
self .download_button .setDefault (True )
|
||||
combined_buttons_layout .addWidget (self .download_button )
|
||||
|
||||
self .cancel_button =QPushButton ()
|
||||
self .cancel_button .clicked .connect (self .reject )
|
||||
combined_buttons_layout .addWidget (self .cancel_button )
|
||||
|
||||
combined_buttons_layout .addStretch (1 )
|
||||
main_layout .addLayout (combined_buttons_layout )
|
||||
|
||||
self ._retranslate_ui ()
|
||||
if hasattr (self .parent_app ,'get_dark_theme')and self .parent_app .current_theme =="dark":
|
||||
self .setStyleSheet (self .parent_app .get_dark_theme ())
|
||||
|
||||
|
||||
def _logger (self ,message ):
|
||||
"""Helper to log messages, either to parent app or console."""
|
||||
if hasattr (self .parent_app ,'log_signal')and self .parent_app .log_signal :
|
||||
self .parent_app .log_signal .emit (f"[FavArtistsDialog] {message }")
|
||||
else :
|
||||
print (f"[FavArtistsDialog] {message }")
|
||||
|
||||
def _show_content_elements (self ,show ):
|
||||
"""Helper to show/hide content-related widgets."""
|
||||
self .search_input .setVisible (show )
|
||||
self .artist_list_widget .setVisible (show )
|
||||
|
||||
def _fetch_favorite_artists (self ):
|
||||
kemono_fav_url ="https://kemono.su/api/v1/account/favorites?type=artist"
|
||||
coomer_fav_url ="https://coomer.su/api/v1/account/favorites?type=artist"
|
||||
|
||||
self .all_fetched_artists =[]
|
||||
fetched_any_successfully =False
|
||||
errors_occurred =[]
|
||||
any_cookies_loaded_successfully_for_any_source =False
|
||||
|
||||
api_sources =[
|
||||
{"name":"Kemono.su","url":kemono_fav_url ,"domain":"kemono.su"},
|
||||
{"name":"Coomer.su","url":coomer_fav_url ,"domain":"coomer.su"}
|
||||
]
|
||||
|
||||
for source in api_sources :
|
||||
self ._logger (f"Attempting to fetch favorite artists from: {source ['name']} ({source ['url']})")
|
||||
self .status_label .setText (self ._tr ("fav_artists_loading_from_source_status","⏳ Loading favorites from {source_name}...").format (source_name =source ['name']))
|
||||
QCoreApplication .processEvents ()
|
||||
|
||||
cookies_dict_for_source =None
|
||||
if self .cookies_config ['use_cookie']:
|
||||
cookies_dict_for_source =prepare_cookies_for_request (
|
||||
True ,
|
||||
self .cookies_config ['cookie_text'],
|
||||
self .cookies_config ['selected_cookie_file'],
|
||||
self .cookies_config ['app_base_dir'],
|
||||
self ._logger ,
|
||||
target_domain =source ['domain']
|
||||
)
|
||||
if cookies_dict_for_source :
|
||||
any_cookies_loaded_successfully_for_any_source =True
|
||||
else :
|
||||
self ._logger (f"Warning ({source ['name']}): Cookies enabled but could not be loaded for this domain. Fetch might fail if cookies are required.")
|
||||
try :
|
||||
headers ={'User-Agent':'Mozilla/5.0'}
|
||||
response =requests .get (source ['url'],headers =headers ,cookies =cookies_dict_for_source ,timeout =20 )
|
||||
response .raise_for_status ()
|
||||
artists_data_from_api =response .json ()
|
||||
|
||||
if not isinstance (artists_data_from_api ,list ):
|
||||
error_msg =f"Error ({source ['name']}): API did not return a list of artists (got {type (artists_data_from_api )})."
|
||||
self ._logger (error_msg )
|
||||
errors_occurred .append (error_msg )
|
||||
continue
|
||||
|
||||
processed_artists_from_source =0
|
||||
for artist_entry in artists_data_from_api :
|
||||
artist_id =artist_entry .get ("id")
|
||||
artist_name =html .unescape (artist_entry .get ("name","Unknown Artist").strip ())
|
||||
artist_service_platform =artist_entry .get ("service")
|
||||
|
||||
if artist_id and artist_name and artist_service_platform :
|
||||
artist_page_domain =self ._get_domain_for_service (artist_service_platform )
|
||||
full_url =f"https://{artist_page_domain }/{artist_service_platform }/user/{artist_id }"
|
||||
|
||||
self .all_fetched_artists .append ({
|
||||
'name':artist_name ,
|
||||
'url':full_url ,
|
||||
'service':artist_service_platform ,
|
||||
'id':artist_id ,
|
||||
'_source_api':source ['name']
|
||||
})
|
||||
processed_artists_from_source +=1
|
||||
else :
|
||||
self ._logger (f"Warning ({source ['name']}): Skipping favorite artist entry due to missing data: {artist_entry }")
|
||||
|
||||
if processed_artists_from_source >0 :
|
||||
fetched_any_successfully =True
|
||||
self ._logger (f"Fetched {processed_artists_from_source } artists from {source ['name']}.")
|
||||
|
||||
except requests .exceptions .RequestException as e :
|
||||
error_msg =f"Error fetching favorites from {source ['name']}: {e }"
|
||||
self ._logger (error_msg )
|
||||
errors_occurred .append (error_msg )
|
||||
except Exception as e :
|
||||
error_msg =f"An unexpected error occurred with {source ['name']}: {e }"
|
||||
self ._logger (error_msg )
|
||||
errors_occurred .append (error_msg )
|
||||
|
||||
|
||||
if self .cookies_config ['use_cookie']and not any_cookies_loaded_successfully_for_any_source :
|
||||
self .status_label .setText (self ._tr ("fav_artists_cookies_required_status","Error: Cookies enabled but could not be loaded for any source."))
|
||||
self ._logger ("Error: Cookies enabled but no cookies loaded for any source. Showing help dialog.")
|
||||
cookie_help_dialog =CookieHelpDialog (self )
|
||||
cookie_help_dialog .exec_ ()
|
||||
self .download_button .setEnabled (False )
|
||||
if not fetched_any_successfully :
|
||||
errors_occurred .append ("Cookies enabled but could not be loaded for any API source.")
|
||||
|
||||
unique_artists_map ={}
|
||||
for artist in self .all_fetched_artists :
|
||||
key =(artist ['service'].lower (),str (artist ['id']).lower ())
|
||||
if key not in unique_artists_map :
|
||||
unique_artists_map [key ]=artist
|
||||
self .all_fetched_artists =list (unique_artists_map .values ())
|
||||
|
||||
self .all_fetched_artists .sort (key =lambda x :x ['name'].lower ())
|
||||
self ._populate_artist_list_widget ()
|
||||
|
||||
if fetched_any_successfully and self .all_fetched_artists :
|
||||
self .status_label .setText (self ._tr ("fav_artists_found_status","Found {count} total favorite artist(s).").format (count =len (self .all_fetched_artists )))
|
||||
self ._show_content_elements (True )
|
||||
self .download_button .setEnabled (True )
|
||||
elif not fetched_any_successfully and not errors_occurred :
|
||||
self .status_label .setText (self ._tr ("fav_artists_none_found_status","No favorite artists found on Kemono.su or Coomer.su."))
|
||||
self ._show_content_elements (False )
|
||||
self .download_button .setEnabled (False )
|
||||
else :
|
||||
final_error_message =self ._tr ("fav_artists_failed_status","Failed to fetch favorites.")
|
||||
if errors_occurred :
|
||||
final_error_message +=" Errors: "+"; ".join (errors_occurred )
|
||||
self .status_label .setText (final_error_message )
|
||||
self ._show_content_elements (False )
|
||||
self .download_button .setEnabled (False )
|
||||
if fetched_any_successfully and not self .all_fetched_artists :
|
||||
self .status_label .setText (self ._tr ("fav_artists_no_favorites_after_processing","No favorite artists found after processing."))
|
||||
|
||||
def _populate_artist_list_widget (self ,artists_to_display =None ):
|
||||
self .artist_list_widget .clear ()
|
||||
source_list =artists_to_display if artists_to_display is not None else self .all_fetched_artists
|
||||
for artist_data in source_list :
|
||||
item =QListWidgetItem (f"{artist_data ['name']} ({artist_data .get ('service','N/A').capitalize ()})")
|
||||
item .setFlags (item .flags ()|Qt .ItemIsUserCheckable )
|
||||
item .setCheckState (Qt .Unchecked )
|
||||
item .setData (Qt .UserRole ,artist_data )
|
||||
self .artist_list_widget .addItem (item )
|
||||
|
||||
def _filter_artist_list_display (self ):
|
||||
search_text =self .search_input .text ().lower ().strip ()
|
||||
if not search_text :
|
||||
self ._populate_artist_list_widget ()
|
||||
return
|
||||
|
||||
filtered_artists =[
|
||||
artist for artist in self .all_fetched_artists
|
||||
if search_text in artist ['name'].lower ()or search_text in artist ['url'].lower ()
|
||||
]
|
||||
self ._populate_artist_list_widget (filtered_artists )
|
||||
|
||||
def _select_all_items (self ):
|
||||
for i in range (self .artist_list_widget .count ()):
|
||||
self .artist_list_widget .item (i ).setCheckState (Qt .Checked )
|
||||
|
||||
def _deselect_all_items (self ):
|
||||
for i in range (self .artist_list_widget .count ()):
|
||||
self .artist_list_widget .item (i ).setCheckState (Qt .Unchecked )
|
||||
|
||||
def _accept_selection_action (self ):
|
||||
self .selected_artists_data =[]
|
||||
for i in range (self .artist_list_widget .count ()):
|
||||
item =self .artist_list_widget .item (i )
|
||||
if item .checkState ()==Qt .Checked :
|
||||
self .selected_artists_data .append (item .data (Qt .UserRole ))
|
||||
|
||||
if not self .selected_artists_data :
|
||||
QMessageBox .information (self ,"No Selection","Please select at least one artist to download.")
|
||||
return
|
||||
self .accept ()
|
||||
|
||||
def get_selected_artists (self ):
|
||||
return self .selected_artists_data
|
||||
629
src/ui/dialogs/FavoritePostsDialog.py
Normal file
629
src/ui/dialogs/FavoritePostsDialog.py
Normal file
@@ -0,0 +1,629 @@
|
||||
# --- Standard Library Imports ---
|
||||
import html
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
# --- Third-Party Library Imports ---
|
||||
import requests
|
||||
from PyQt5.QtCore import QCoreApplication, Qt, pyqtSignal, QThread
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget,
|
||||
QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout, QProgressBar,
|
||||
QWidget, QCheckBox
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
from ...i18n.translator import get_translation
|
||||
from ..assets import get_app_icon_object
|
||||
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
|
||||
|
||||
|
||||
class FavoritePostsFetcherThread (QThread ):
|
||||
"""Worker thread to fetch favorite posts and creator names."""
|
||||
status_update =pyqtSignal (str )
|
||||
progress_bar_update =pyqtSignal (int ,int )
|
||||
finished =pyqtSignal (list ,str )
|
||||
|
||||
def __init__ (self ,cookies_config ,parent_logger_func ,target_domain_preference =None ):
|
||||
super ().__init__ ()
|
||||
self .cookies_config =cookies_config
|
||||
self .parent_logger_func =parent_logger_func
|
||||
self .target_domain_preference =target_domain_preference
|
||||
self .cancellation_event =threading .Event ()
|
||||
self .error_key_map ={
|
||||
"Kemono.su":"kemono_su",
|
||||
"Coomer.su":"coomer_su"
|
||||
}
|
||||
|
||||
def _logger (self ,message ):
|
||||
self .parent_logger_func (f"[FavPostsFetcherThread] {message }")
|
||||
|
||||
def run (self ):
|
||||
kemono_fav_posts_url ="https://kemono.su/api/v1/account/favorites?type=post"
|
||||
coomer_fav_posts_url ="https://coomer.su/api/v1/account/favorites?type=post"
|
||||
|
||||
all_fetched_posts_temp =[]
|
||||
error_messages_for_summary =[]
|
||||
fetched_any_successfully =False
|
||||
any_cookies_loaded_successfully_for_any_source =False
|
||||
|
||||
self .status_update .emit ("key_fetching_fav_post_list_init")
|
||||
self .progress_bar_update .emit (0 ,0 )
|
||||
|
||||
api_sources =[
|
||||
{"name":"Kemono.su","url":kemono_fav_posts_url ,"domain":"kemono.su"},
|
||||
{"name":"Coomer.su","url":coomer_fav_posts_url ,"domain":"coomer.su"}
|
||||
]
|
||||
|
||||
api_sources_to_try =[]
|
||||
if self .target_domain_preference :
|
||||
self ._logger (f"Targeting specific domain for favorites: {self .target_domain_preference }")
|
||||
for source_def in api_sources :
|
||||
if source_def ["domain"]==self .target_domain_preference :
|
||||
api_sources_to_try .append (source_def )
|
||||
break
|
||||
if not api_sources_to_try :
|
||||
self ._logger (f"Warning: Preferred domain '{self .target_domain_preference }' not a recognized API source. Fetching from all.")
|
||||
api_sources_to_try =api_sources
|
||||
else :
|
||||
self ._logger ("No specific domain preference, or both domains have cookies. Will attempt to fetch from all sources.")
|
||||
api_sources_to_try =api_sources
|
||||
|
||||
for source in api_sources_to_try :
|
||||
if self .cancellation_event .is_set ():
|
||||
self .finished .emit ([],"KEY_FETCH_CANCELLED_DURING")
|
||||
return
|
||||
cookies_dict_for_source =None
|
||||
if self .cookies_config ['use_cookie']:
|
||||
cookies_dict_for_source =prepare_cookies_for_request (
|
||||
True ,
|
||||
self .cookies_config ['cookie_text'],
|
||||
self .cookies_config ['selected_cookie_file'],
|
||||
self .cookies_config ['app_base_dir'],
|
||||
self ._logger ,
|
||||
target_domain =source ['domain']
|
||||
)
|
||||
if cookies_dict_for_source :
|
||||
any_cookies_loaded_successfully_for_any_source =True
|
||||
else :
|
||||
self ._logger (f"Warning ({source ['name']}): Cookies enabled but could not be loaded for this domain. Fetch might fail if cookies are required.")
|
||||
|
||||
self ._logger (f"Attempting to fetch favorite posts from: {source ['name']} ({source ['url']})")
|
||||
source_key_part =self .error_key_map .get (source ['name'],source ['name'].lower ().replace ('.','_'))
|
||||
self .status_update .emit (f"key_fetching_from_source_{source_key_part }")
|
||||
QCoreApplication .processEvents ()
|
||||
|
||||
try :
|
||||
headers ={'User-Agent':'Mozilla/5.0'}
|
||||
response =requests .get (source ['url'],headers =headers ,cookies =cookies_dict_for_source ,timeout =20 )
|
||||
response .raise_for_status ()
|
||||
posts_data_from_api =response .json ()
|
||||
|
||||
if not isinstance (posts_data_from_api ,list ):
|
||||
err_detail =f"Error ({source ['name']}): API did not return a list of posts (got {type (posts_data_from_api )})."
|
||||
self ._logger (err_detail )
|
||||
error_messages_for_summary .append (err_detail )
|
||||
continue
|
||||
|
||||
processed_posts_from_source =0
|
||||
for post_entry in posts_data_from_api :
|
||||
post_id =post_entry .get ("id")
|
||||
post_title =html .unescape (post_entry .get ("title","Untitled Post").strip ())
|
||||
service =post_entry .get ("service")
|
||||
creator_id =post_entry .get ("user")
|
||||
added_date_str =post_entry .get ("added",post_entry .get ("published",""))
|
||||
|
||||
if post_id and post_title and service and creator_id :
|
||||
all_fetched_posts_temp .append ({
|
||||
'post_id':post_id ,'title':post_title ,'service':service ,
|
||||
'creator_id':creator_id ,'added_date':added_date_str ,
|
||||
'_source_api':source ['name']
|
||||
})
|
||||
processed_posts_from_source +=1
|
||||
else :
|
||||
self ._logger (f"Warning ({source ['name']}): Skipping favorite post entry due to missing data: {post_entry }")
|
||||
|
||||
if processed_posts_from_source >0 :
|
||||
fetched_any_successfully =True
|
||||
self ._logger (f"Fetched {processed_posts_from_source } posts from {source ['name']}.")
|
||||
|
||||
except requests .exceptions .RequestException as e :
|
||||
err_detail =f"Error fetching favorite posts from {source ['name']}: {e }"
|
||||
self ._logger (err_detail )
|
||||
error_messages_for_summary .append (err_detail )
|
||||
if e .response is not None and e .response .status_code ==401 :
|
||||
self .finished .emit ([],"KEY_AUTH_FAILED")
|
||||
self ._logger (f"Authorization failed for {source ['name']}, emitting KEY_AUTH_FAILED.")
|
||||
return
|
||||
except Exception as e :
|
||||
err_detail =f"An unexpected error occurred with {source ['name']}: {e }"
|
||||
self ._logger (err_detail )
|
||||
error_messages_for_summary .append (err_detail )
|
||||
|
||||
if self .cancellation_event .is_set ():
|
||||
self .finished .emit ([],"KEY_FETCH_CANCELLED_AFTER")
|
||||
return
|
||||
|
||||
|
||||
if self .cookies_config ['use_cookie']and not any_cookies_loaded_successfully_for_any_source :
|
||||
|
||||
if self .target_domain_preference and not any_cookies_loaded_successfully_for_any_source :
|
||||
|
||||
domain_key_part =self .error_key_map .get (self .target_domain_preference ,self .target_domain_preference .lower ().replace ('.','_'))
|
||||
self .finished .emit ([],f"KEY_COOKIES_REQUIRED_BUT_NOT_FOUND_FOR_DOMAIN_{domain_key_part }")
|
||||
return
|
||||
|
||||
|
||||
self .finished .emit ([],"KEY_COOKIES_REQUIRED_BUT_NOT_FOUND_GENERIC")
|
||||
return
|
||||
|
||||
unique_posts_map ={}
|
||||
for post in all_fetched_posts_temp :
|
||||
key =(post ['service'].lower (),str (post ['creator_id']).lower (),str (post ['post_id']).lower ())
|
||||
if key not in unique_posts_map :
|
||||
unique_posts_map [key ]=post
|
||||
all_fetched_posts_temp =list (unique_posts_map .values ())
|
||||
|
||||
all_fetched_posts_temp .sort (key =lambda x :(x .get ('_source_api','').lower (),x .get ('service','').lower (),str (x .get ('creator_id','')).lower (),(x .get ('added_date')or '')),reverse =False )
|
||||
|
||||
if error_messages_for_summary :
|
||||
error_summary_str ="; ".join (error_messages_for_summary )
|
||||
if not fetched_any_successfully :
|
||||
self .finished .emit ([],f"KEY_FETCH_FAILED_GENERIC_{error_summary_str [:50 ]}")
|
||||
else :
|
||||
self .finished .emit (all_fetched_posts_temp ,f"KEY_FETCH_PARTIAL_SUCCESS_{error_summary_str [:50 ]}")
|
||||
elif not all_fetched_posts_temp and not fetched_any_successfully and not self .target_domain_preference :
|
||||
self .finished .emit ([],"KEY_NO_FAVORITES_FOUND_ALL_PLATFORMS")
|
||||
else :
|
||||
self .finished .emit (all_fetched_posts_temp ,"KEY_FETCH_SUCCESS")
|
||||
|
||||
class PostListItemWidget (QWidget ):
|
||||
"""Custom widget for displaying a single post in the FavoritePostsDialog list."""
|
||||
def __init__ (self ,post_data_dict ,parent_dialog_ref ,parent =None ):
|
||||
super ().__init__ (parent )
|
||||
self .post_data =post_data_dict
|
||||
self .parent_dialog =parent_dialog_ref
|
||||
|
||||
self .layout =QHBoxLayout (self )
|
||||
self .layout .setContentsMargins (5 ,3 ,5 ,3 )
|
||||
self .layout .setSpacing (10 )
|
||||
|
||||
self .checkbox =QCheckBox ()
|
||||
self .layout .addWidget (self .checkbox )
|
||||
|
||||
self .info_label =QLabel ()
|
||||
self .info_label .setWordWrap (True )
|
||||
self .info_label .setTextFormat (Qt .RichText )
|
||||
self .layout .addWidget (self .info_label ,1 )
|
||||
|
||||
self ._setup_display_text ()
|
||||
def _setup_display_text (self ):
|
||||
suffix_plain =self .post_data .get ('suffix_for_display',"")
|
||||
title_plain =self .post_data .get ('title','Untitled Post')
|
||||
escaped_suffix =html .escape (suffix_plain )
|
||||
escaped_title =html .escape (title_plain )
|
||||
p_style_paragraph ="font-size:10.5pt; margin:0; padding:0;"
|
||||
title_span_style ="font-weight:bold; color:#E0E0E0;"
|
||||
suffix_span_style ="color:#999999; font-weight:normal; font-size:9.5pt;"
|
||||
|
||||
if escaped_suffix :
|
||||
display_html_content =f"<p style='{p_style_paragraph }'><span style='{title_span_style }'>{escaped_title }</span><span style='{suffix_span_style }'>{escaped_suffix }</span></p>"
|
||||
else :
|
||||
display_html_content =f"<p style='{p_style_paragraph }'><span style='{title_span_style }'>{escaped_title }</span></p>"
|
||||
|
||||
self .info_label .setText (display_html_content )
|
||||
|
||||
def isChecked (self ):return self .checkbox .isChecked ()
|
||||
def setCheckState (self ,state ):self .checkbox .setCheckState (state )
|
||||
def get_post_data (self ):return self .post_data
|
||||
|
||||
class FavoritePostsDialog (QDialog ):
|
||||
"""Dialog to display and select favorite posts."""
|
||||
def __init__ (self ,parent_app ,cookies_config ,known_names_list_ref ,target_domain_preference =None ):
|
||||
super ().__init__ (parent_app )
|
||||
self .parent_app =parent_app
|
||||
self .cookies_config =cookies_config
|
||||
self .all_fetched_posts =[]
|
||||
self .selected_posts_data =[]
|
||||
self .known_names_list_ref =known_names_list_ref
|
||||
self .target_domain_preference_for_this_fetch =target_domain_preference
|
||||
self .creator_name_cache ={}
|
||||
self .displayable_grouped_posts ={}
|
||||
self .fetcher_thread =None
|
||||
|
||||
app_icon =get_app_icon_object ()
|
||||
if not app_icon .isNull ():
|
||||
self .setWindowIcon (app_icon )
|
||||
|
||||
self .setModal (True )
|
||||
self .setMinimumSize (600 ,600 )
|
||||
if hasattr (self .parent_app ,'get_dark_theme'):
|
||||
self .setStyleSheet (self .parent_app .get_dark_theme ())
|
||||
|
||||
self ._init_ui ()
|
||||
self ._load_creator_names_from_file ()
|
||||
self ._retranslate_ui ()
|
||||
self ._start_fetching_favorite_posts ()
|
||||
|
||||
def _update_status_label_from_key (self ,status_key ):
|
||||
"""Translates a status key and updates the status label."""
|
||||
|
||||
translated_status =self ._tr (status_key .lower (),status_key )
|
||||
self .status_label .setText (translated_status )
|
||||
|
||||
def _init_ui (self ):
|
||||
main_layout =QVBoxLayout (self )
|
||||
|
||||
self .status_label =QLabel ()
|
||||
self .status_label .setAlignment (Qt .AlignCenter )
|
||||
main_layout .addWidget (self .status_label )
|
||||
|
||||
self .progress_bar =QProgressBar ()
|
||||
self .progress_bar .setTextVisible (False )
|
||||
self .progress_bar .setVisible (False )
|
||||
main_layout .addWidget (self .progress_bar )
|
||||
|
||||
self .search_input =QLineEdit ()
|
||||
|
||||
self .search_input .textChanged .connect (self ._filter_post_list_display )
|
||||
main_layout .addWidget (self .search_input )
|
||||
|
||||
self .post_list_widget =QListWidget ()
|
||||
self .post_list_widget .setStyleSheet ("""
|
||||
QListWidget::item {
|
||||
border-bottom: 1px solid #4A4A4A;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}""")
|
||||
self .post_list_widget .setAlternatingRowColors (True )
|
||||
main_layout .addWidget (self .post_list_widget )
|
||||
|
||||
combined_buttons_layout =QHBoxLayout ()
|
||||
self .select_all_button =QPushButton ()
|
||||
self .select_all_button .clicked .connect (self ._select_all_items )
|
||||
combined_buttons_layout .addWidget (self .select_all_button )
|
||||
|
||||
self .deselect_all_button =QPushButton ()
|
||||
self .deselect_all_button .clicked .connect (self ._deselect_all_items )
|
||||
combined_buttons_layout .addWidget (self .deselect_all_button )
|
||||
|
||||
self .download_button =QPushButton ()
|
||||
self .download_button .clicked .connect (self ._accept_selection_action )
|
||||
self .download_button .setEnabled (False )
|
||||
self .download_button .setDefault (True )
|
||||
combined_buttons_layout .addWidget (self .download_button )
|
||||
|
||||
self .cancel_button =QPushButton ()
|
||||
self .cancel_button .clicked .connect (self .reject )
|
||||
combined_buttons_layout .addWidget (self .cancel_button )
|
||||
combined_buttons_layout .addStretch (1 )
|
||||
main_layout .addLayout (combined_buttons_layout )
|
||||
|
||||
def _tr (self ,key ,default_text =""):
|
||||
"""Helper to get translation based on current app language."""
|
||||
if callable (get_translation )and self .parent_app :
|
||||
return get_translation (self .parent_app .current_selected_language ,key ,default_text )
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui (self ):
|
||||
self .setWindowTitle (self ._tr ("fav_posts_dialog_title","Favorite Posts"))
|
||||
self .status_label .setText (self ._tr ("fav_posts_loading_status","Loading favorite posts..."))
|
||||
self .search_input .setPlaceholderText (self ._tr ("fav_posts_search_placeholder","Search posts (title, creator name, ID, service)..."))
|
||||
self .select_all_button .setText (self ._tr ("fav_posts_select_all_button","Select All"))
|
||||
self .deselect_all_button .setText (self ._tr ("fav_posts_deselect_all_button","Deselect All"))
|
||||
self .download_button .setText (self ._tr ("fav_posts_download_selected_button","Download Selected"))
|
||||
self .cancel_button .setText (self ._tr ("fav_posts_cancel_button","Cancel"))
|
||||
|
||||
def _logger (self ,message ):
|
||||
if hasattr (self .parent_app ,'log_signal')and self .parent_app .log_signal :
|
||||
self .parent_app .log_signal .emit (f"[FavPostsDialog] {message }")
|
||||
else :
|
||||
print (f"[FavPostsDialog] {message }")
|
||||
|
||||
def _load_creator_names_from_file (self ):
|
||||
"""Loads creator id-name-service mappings from creators.txt."""
|
||||
self ._logger ("Attempting to load creators.json for Favorite Posts Dialog.")
|
||||
|
||||
if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'):
|
||||
base_path_for_creators =sys ._MEIPASS
|
||||
self ._logger (f" Running bundled. Using _MEIPASS: {base_path_for_creators }")
|
||||
else :
|
||||
base_path_for_creators =self .parent_app .app_base_dir
|
||||
self ._logger (f" Not bundled or _MEIPASS unavailable. Using app_base_dir: {base_path_for_creators }")
|
||||
creators_file_path = os.path.join(base_path_for_creators, "data", "creators.json")
|
||||
self ._logger (f"Full path to creators.json: {creators_file_path }")
|
||||
|
||||
if not os .path .exists (creators_file_path ):
|
||||
self ._logger (f"Warning: 'creators.json' not found at {creators_file_path }. Creator names will not be displayed.")
|
||||
return
|
||||
|
||||
try :
|
||||
with open (creators_file_path ,'r',encoding ='utf-8')as f :
|
||||
loaded_data =json .load (f )
|
||||
|
||||
if isinstance (loaded_data ,list )and len (loaded_data )>0 and isinstance (loaded_data [0 ],list ):
|
||||
creators_list =loaded_data [0 ]
|
||||
elif isinstance (loaded_data ,list )and all (isinstance (item ,dict )for item in loaded_data ):
|
||||
creators_list =loaded_data
|
||||
else :
|
||||
self ._logger (f"Warning: 'creators.json' has an unexpected format. Expected a list of lists or a flat list of creator objects.")
|
||||
return
|
||||
|
||||
for creator_data in creators_list :
|
||||
creator_id =creator_data .get ("id")
|
||||
name =creator_data .get ("name")
|
||||
service =creator_data .get ("service")
|
||||
if creator_id and name and service :
|
||||
self .creator_name_cache [(service .lower (),str (creator_id ))]=name
|
||||
self ._logger (f"Successfully loaded {len (self .creator_name_cache )} creator names from 'creators.json'.")
|
||||
except Exception as e :
|
||||
self ._logger (f"Error loading 'creators.json': {e }")
|
||||
|
||||
def _start_fetching_favorite_posts (self ):
|
||||
self .download_button .setEnabled (False )
|
||||
self .status_label .setText ("Initializing favorite posts fetch...")
|
||||
|
||||
self .fetcher_thread =FavoritePostsFetcherThread (
|
||||
self .cookies_config ,
|
||||
self .parent_app .log_signal .emit ,
|
||||
target_domain_preference =self .target_domain_preference_for_this_fetch
|
||||
)
|
||||
self .fetcher_thread .status_update .connect (self ._update_status_label_from_key )
|
||||
self .fetcher_thread .finished .connect (self ._on_fetch_completed )
|
||||
self .fetcher_thread .progress_bar_update .connect (self ._set_progress_bar_value )
|
||||
self .progress_bar .setVisible (True )
|
||||
self .fetcher_thread .start ()
|
||||
|
||||
def _set_progress_bar_value (self ,value ,maximum ):
|
||||
if maximum ==0 :
|
||||
self .progress_bar .setRange (0 ,0 )
|
||||
self .progress_bar .setValue (0 )
|
||||
else :
|
||||
self .progress_bar .setRange (0 ,maximum )
|
||||
self .progress_bar .setValue (value )
|
||||
|
||||
def _on_fetch_completed (self ,fetched_posts_list ,status_key ):
|
||||
self .progress_bar .setVisible (False )
|
||||
|
||||
proceed_to_display_posts =False
|
||||
show_error_message_box =False
|
||||
message_box_title_key ="fav_posts_fetch_error_title"
|
||||
message_box_text_key ="fav_posts_fetch_error_message"
|
||||
message_box_params ={'domain':self .target_domain_preference_for_this_fetch or "platform",'error_message_part':""}
|
||||
status_label_text_key =None
|
||||
|
||||
if status_key =="KEY_FETCH_SUCCESS":
|
||||
proceed_to_display_posts =True
|
||||
elif status_key and status_key .startswith ("KEY_FETCH_PARTIAL_SUCCESS_")and fetched_posts_list :
|
||||
displayable_detail =status_key .replace ("KEY_FETCH_PARTIAL_SUCCESS_","").replace ("_"," ")
|
||||
self ._logger (f"Partial success with posts: {status_key } -> {displayable_detail }")
|
||||
|
||||
|
||||
proceed_to_display_posts =True
|
||||
elif status_key :
|
||||
specific_domain_msg_part =f" for {self .target_domain_preference_for_this_fetch }"if self .target_domain_preference_for_this_fetch else ""
|
||||
|
||||
if status_key .startswith ("KEY_COOKIES_REQUIRED_BUT_NOT_FOUND_FOR_DOMAIN_")or status_key =="KEY_COOKIES_REQUIRED_BUT_NOT_FOUND_GENERIC":
|
||||
status_label_text_key ="fav_posts_cookies_required_error"
|
||||
self ._logger (f"Cookie error: {status_key }. Showing help dialog.")
|
||||
cookie_help_dialog =CookieHelpDialog (self )
|
||||
cookie_help_dialog .exec_ ()
|
||||
elif status_key =="KEY_AUTH_FAILED":
|
||||
status_label_text_key ="fav_posts_auth_failed_title"
|
||||
self ._logger (f"Auth error: {status_key }. Showing help dialog.")
|
||||
QMessageBox .warning (self ,self ._tr ("fav_posts_auth_failed_title","Authorization Failed (Posts)"),
|
||||
self ._tr ("fav_posts_auth_failed_message_generic","...").format (domain_specific_part =specific_domain_msg_part ))
|
||||
cookie_help_dialog =CookieHelpDialog (self )
|
||||
cookie_help_dialog .exec_ ()
|
||||
elif status_key =="KEY_NO_FAVORITES_FOUND_ALL_PLATFORMS":
|
||||
status_label_text_key ="fav_posts_no_posts_found_status"
|
||||
self ._logger (status_key )
|
||||
elif status_key .startswith ("KEY_FETCH_CANCELLED"):
|
||||
status_label_text_key ="fav_posts_fetch_cancelled_status"
|
||||
self ._logger (status_key )
|
||||
else :
|
||||
displayable_error_detail =status_key
|
||||
if status_key .startswith ("KEY_FETCH_FAILED_GENERIC_"):
|
||||
displayable_error_detail =status_key .replace ("KEY_FETCH_FAILED_GENERIC_","").replace ("_"," ")
|
||||
elif status_key .startswith ("KEY_FETCH_PARTIAL_SUCCESS_"):
|
||||
displayable_error_detail =status_key .replace ("KEY_FETCH_PARTIAL_SUCCESS_","Partial success but no posts: ").replace ("_"," ")
|
||||
|
||||
message_box_params ['error_message_part']=f":\n\n{displayable_error_detail }"if displayable_error_detail else ""
|
||||
status_label_text_key ="fav_posts_fetch_error_message"
|
||||
show_error_message_box =True
|
||||
self ._logger (f"Fetch error: {status_key } -> {displayable_error_detail }")
|
||||
|
||||
if status_label_text_key :
|
||||
self .status_label .setText (self ._tr (status_label_text_key ,status_label_text_key ).format (**message_box_params ))
|
||||
if show_error_message_box :
|
||||
QMessageBox .critical (self ,self ._tr (message_box_title_key ),self ._tr (message_box_text_key ).format (**message_box_params ))
|
||||
|
||||
self .download_button .setEnabled (False )
|
||||
return
|
||||
|
||||
|
||||
if not proceed_to_display_posts :
|
||||
if not status_label_text_key :
|
||||
self .status_label .setText (self ._tr ("fav_posts_cookies_required_error","Error: Cookies are required for favorite posts but could not be loaded."))
|
||||
self .download_button .setEnabled (False )
|
||||
return
|
||||
|
||||
if not self .creator_name_cache :
|
||||
self ._logger ("Warning: Creator name cache is empty. Names will not be resolved from creators.json. Displaying IDs instead.")
|
||||
else :
|
||||
self ._logger (f"Creator name cache has {len (self .creator_name_cache )} entries. Attempting to resolve names...")
|
||||
sample_keys =list (self .creator_name_cache .keys ())[:3 ]
|
||||
if sample_keys :
|
||||
self ._logger (f"Sample keys from creator_name_cache: {sample_keys }")
|
||||
|
||||
|
||||
processed_one_missing_log =False
|
||||
for post_entry in fetched_posts_list :
|
||||
service_from_post =post_entry .get ('service','')
|
||||
creator_id_from_post =post_entry .get ('creator_id','')
|
||||
|
||||
lookup_key_service =service_from_post .lower ()
|
||||
lookup_key_id =str (creator_id_from_post )
|
||||
lookup_key_tuple =(lookup_key_service ,lookup_key_id )
|
||||
|
||||
resolved_name =self .creator_name_cache .get (lookup_key_tuple )
|
||||
|
||||
if resolved_name :
|
||||
post_entry ['creator_name_resolved']=resolved_name
|
||||
else :
|
||||
post_entry ['creator_name_resolved']=str (creator_id_from_post )
|
||||
if not processed_one_missing_log and self .creator_name_cache :
|
||||
self ._logger (f"Debug: Name not found for key {lookup_key_tuple }. Using ID '{creator_id_from_post }'.")
|
||||
processed_one_missing_log =True
|
||||
|
||||
self .all_fetched_posts =fetched_posts_list
|
||||
|
||||
if not self .all_fetched_posts :
|
||||
self .status_label .setText (self ._tr ("fav_posts_no_posts_found_status","No favorite posts found."))
|
||||
self .download_button .setEnabled (False )
|
||||
return
|
||||
|
||||
try :
|
||||
self ._populate_post_list_widget ()
|
||||
self .status_label .setText (self ._tr ("fav_posts_found_status","{count} favorite post(s) found.").format (count =len (self .all_fetched_posts )))
|
||||
self .download_button .setEnabled (True )
|
||||
except Exception as e :
|
||||
self .status_label .setText (self ._tr ("fav_posts_display_error_status","Error displaying posts: {error}").format (error =str (e )))
|
||||
self ._logger (f"Error during _populate_post_list_widget: {e }\n{traceback .format_exc (limit =3 )}")
|
||||
QMessageBox .critical (self ,self ._tr ("fav_posts_ui_error_title","UI Error"),self ._tr ("fav_posts_ui_error_message","Could not display favorite posts: {error}").format (error =str (e )))
|
||||
self .download_button .setEnabled (False )
|
||||
|
||||
|
||||
def _find_best_known_name_match_in_title (self ,title_raw ):
|
||||
if not title_raw or not self .known_names_list_ref :
|
||||
return None
|
||||
|
||||
title_lower =title_raw .lower ()
|
||||
best_match_known_name_primary =None
|
||||
longest_match_len =0
|
||||
|
||||
for known_entry in self .known_names_list_ref :
|
||||
aliases_to_check =set ()
|
||||
for alias_val in known_entry .get ("aliases",[]):
|
||||
aliases_to_check .add (alias_val )
|
||||
if not known_entry .get ("is_group",False ):
|
||||
aliases_to_check .add (known_entry ["name"])
|
||||
sorted_aliases_for_entry =sorted (list (aliases_to_check ),key =len ,reverse =True )
|
||||
|
||||
for alias in sorted_aliases_for_entry :
|
||||
alias_lower =alias .lower ()
|
||||
if not alias_lower :
|
||||
continue
|
||||
if re .search (r'\b'+re .escape (alias_lower )+r'\b',title_lower ):
|
||||
if len (alias_lower )>longest_match_len :
|
||||
longest_match_len =len (alias_lower )
|
||||
best_match_known_name_primary =known_entry ["name"]
|
||||
break
|
||||
return best_match_known_name_primary
|
||||
|
||||
def _populate_post_list_widget (self ,posts_to_display =None ):
|
||||
self .post_list_widget .clear ()
|
||||
|
||||
source_list_for_grouping =posts_to_display if posts_to_display is not None else self .all_fetched_posts
|
||||
grouped_posts ={}
|
||||
for post in source_list_for_grouping :
|
||||
service =post .get ('service','unknown_service')
|
||||
creator_id =post .get ('creator_id','unknown_id')
|
||||
group_key =(service ,creator_id )
|
||||
if group_key not in grouped_posts :
|
||||
grouped_posts [group_key ]=[]
|
||||
grouped_posts [group_key ].append (post )
|
||||
|
||||
sorted_group_keys =sorted (grouped_posts .keys (),key =lambda x :(x [0 ].lower (),x [1 ].lower ()))
|
||||
|
||||
self .displayable_grouped_posts ={
|
||||
key :sorted (grouped_posts [key ],key =lambda p :(p .get ('added_date')or ''),reverse =True )
|
||||
for key in sorted_group_keys
|
||||
}
|
||||
for service ,creator_id_val in sorted_group_keys :
|
||||
creator_name_display =self .creator_name_cache .get (
|
||||
(service .lower (),str (creator_id_val )),
|
||||
str (creator_id_val )
|
||||
)
|
||||
artist_header_display_text =f"{creator_name_display } ({service .capitalize ()} / {creator_id_val })"
|
||||
artist_header_item =QListWidgetItem (f"🎨 {artist_header_display_text }")
|
||||
artist_header_item .setFlags (Qt .NoItemFlags )
|
||||
font =artist_header_item .font ()
|
||||
font .setBold (True )
|
||||
font .setPointSize (font .pointSize ()+1 )
|
||||
artist_header_item .setFont (font )
|
||||
artist_header_item .setForeground (Qt .cyan )
|
||||
self .post_list_widget .addItem (artist_header_item )
|
||||
for post_data in self .displayable_grouped_posts [(service ,creator_id_val )]:
|
||||
post_title_raw =post_data .get ('title','Untitled Post')
|
||||
found_known_name_primary =self ._find_best_known_name_match_in_title (post_title_raw )
|
||||
|
||||
plain_text_title_for_list_item =post_title_raw
|
||||
if found_known_name_primary :
|
||||
suffix_text =f" [Known - {found_known_name_primary }]"
|
||||
post_data ['suffix_for_display']=suffix_text
|
||||
plain_text_title_for_list_item =post_title_raw +suffix_text
|
||||
else :
|
||||
post_data .pop ('suffix_for_display',None )
|
||||
|
||||
list_item =QListWidgetItem (self .post_list_widget )
|
||||
list_item .setText (plain_text_title_for_list_item )
|
||||
list_item .setFlags (list_item .flags ()|Qt .ItemIsUserCheckable )
|
||||
list_item .setCheckState (Qt .Unchecked )
|
||||
list_item .setData (Qt .UserRole ,post_data )
|
||||
self .post_list_widget .addItem (list_item )
|
||||
|
||||
def _filter_post_list_display (self ):
|
||||
search_text =self .search_input .text ().lower ().strip ()
|
||||
if not search_text :
|
||||
self ._populate_post_list_widget (self .all_fetched_posts )
|
||||
return
|
||||
|
||||
filtered_posts_to_group =[]
|
||||
for post in self .all_fetched_posts :
|
||||
matches_post_title =search_text in post .get ('title','').lower ()
|
||||
matches_creator_name =search_text in post .get ('creator_name_resolved','').lower ()
|
||||
matches_creator_id =search_text in post .get ('creator_id','').lower ()
|
||||
matches_service =search_text in post ['service'].lower ()
|
||||
|
||||
if matches_post_title or matches_creator_name or matches_creator_id or matches_service :
|
||||
filtered_posts_to_group .append (post )
|
||||
|
||||
self ._populate_post_list_widget (filtered_posts_to_group )
|
||||
|
||||
def _select_all_items (self ):
|
||||
for i in range (self .post_list_widget .count ()):
|
||||
item =self .post_list_widget .item (i )
|
||||
if item and item .flags ()&Qt .ItemIsUserCheckable :
|
||||
item .setCheckState (Qt .Checked )
|
||||
|
||||
def _deselect_all_items (self ):
|
||||
for i in range (self .post_list_widget .count ()):
|
||||
item =self .post_list_widget .item (i )
|
||||
if item and item .flags ()&Qt .ItemIsUserCheckable :
|
||||
item .setCheckState (Qt .Unchecked )
|
||||
|
||||
def _accept_selection_action (self ):
|
||||
self .selected_posts_data =[]
|
||||
for i in range (self .post_list_widget .count ()):
|
||||
item =self .post_list_widget .item (i )
|
||||
if item and item .checkState ()==Qt .Checked :
|
||||
post_data_for_download =item .data (Qt .UserRole )
|
||||
self .selected_posts_data .append (post_data_for_download )
|
||||
|
||||
if not self .selected_posts_data :
|
||||
QMessageBox .information (self ,self ._tr ("fav_posts_no_selection_title","No Selection"),self ._tr ("fav_posts_no_selection_message","Please select at least one post to download."))
|
||||
return
|
||||
self .accept ()
|
||||
|
||||
def get_selected_posts (self ):
|
||||
return self .selected_posts_data
|
||||
202
src/ui/dialogs/FutureSettingsDialog.py
Normal file
202
src/ui/dialogs/FutureSettingsDialog.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# --- Standard Library Imports ---
|
||||
import os
|
||||
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import Qt, QStandardPaths
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
|
||||
QGroupBox, QComboBox, QMessageBox
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
# This assumes the new project structure is in place.
|
||||
from ...i18n.translator import get_translation
|
||||
from ..main_window import get_app_icon_object
|
||||
from ...config.constants import (
|
||||
THEME_KEY, LANGUAGE_KEY, DOWNLOAD_LOCATION_KEY
|
||||
)
|
||||
|
||||
|
||||
class FutureSettingsDialog(QDialog):
|
||||
"""
|
||||
A dialog for managing application-wide settings like theme, language,
|
||||
and saving the default download path.
|
||||
"""
|
||||
def __init__(self, parent_app_ref, parent=None):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
parent_app_ref (DownloaderApp): A reference to the main application window.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.parent_app = parent_app_ref
|
||||
self.setModal(True)
|
||||
|
||||
# --- Basic Window Setup ---
|
||||
app_icon = get_app_icon_object()
|
||||
if app_icon and not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
# Set window size dynamically
|
||||
screen_height = QApplication.primaryScreen().availableGeometry().height() if QApplication.primaryScreen() else 768
|
||||
scale_factor = screen_height / 768.0
|
||||
base_min_w, base_min_h = 380, 250
|
||||
scaled_min_w = int(base_min_w * scale_factor)
|
||||
scaled_min_h = int(base_min_h * scale_factor)
|
||||
self.setMinimumSize(scaled_min_w, scaled_min_h)
|
||||
|
||||
# --- Initialize UI and Apply Theming ---
|
||||
self._init_ui()
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts for the dialog."""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# --- Appearance Settings ---
|
||||
self.appearance_group_box = QGroupBox()
|
||||
appearance_layout = QVBoxLayout(self.appearance_group_box)
|
||||
self.theme_toggle_button = QPushButton()
|
||||
self.theme_toggle_button.clicked.connect(self._toggle_theme)
|
||||
appearance_layout.addWidget(self.theme_toggle_button)
|
||||
layout.addWidget(self.appearance_group_box)
|
||||
|
||||
# --- Language Settings ---
|
||||
self.language_group_box = QGroupBox()
|
||||
language_group_layout = QVBoxLayout(self.language_group_box)
|
||||
self.language_selection_layout = QHBoxLayout()
|
||||
self.language_label = QLabel()
|
||||
self.language_selection_layout.addWidget(self.language_label)
|
||||
self.language_combo_box = QComboBox()
|
||||
self.language_combo_box.currentIndexChanged.connect(self._language_selection_changed)
|
||||
self.language_selection_layout.addWidget(self.language_combo_box, 1)
|
||||
language_group_layout.addLayout(self.language_selection_layout)
|
||||
layout.addWidget(self.language_group_box)
|
||||
|
||||
# --- Download Settings ---
|
||||
self.download_settings_group_box = QGroupBox()
|
||||
download_settings_layout = QVBoxLayout(self.download_settings_group_box)
|
||||
self.save_path_button = QPushButton()
|
||||
self.save_path_button.clicked.connect(self._save_download_path)
|
||||
download_settings_layout.addWidget(self.save_path_button)
|
||||
layout.addWidget(self.download_settings_group_box)
|
||||
|
||||
layout.addStretch(1)
|
||||
|
||||
# --- OK Button ---
|
||||
self.ok_button = QPushButton()
|
||||
self.ok_button.clicked.connect(self.accept)
|
||||
layout.addWidget(self.ok_button, 0, Qt.AlignRight | Qt.AlignBottom)
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper to get translation based on the main application's current language."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
"""Sets the text for all translatable UI elements."""
|
||||
self.setWindowTitle(self._tr("settings_dialog_title", "Settings"))
|
||||
self.appearance_group_box.setTitle(self._tr("appearance_group_title", "Appearance"))
|
||||
self.language_group_box.setTitle(self._tr("language_group_title", "Language Settings"))
|
||||
self.download_settings_group_box.setTitle(self._tr("settings_download_group_title", "Download Settings"))
|
||||
self.language_label.setText(self._tr("language_label", "Language:"))
|
||||
self._update_theme_toggle_button_text()
|
||||
self._populate_language_combo_box()
|
||||
|
||||
self.save_path_button.setText(self._tr("settings_save_path_button", "Save Current Download Path"))
|
||||
self.save_path_button.setToolTip(self._tr("settings_save_path_tooltip", "Save the current 'Download Location' for future sessions."))
|
||||
self.ok_button.setText(self._tr("ok_button", "OK"))
|
||||
|
||||
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())
|
||||
else:
|
||||
self.setStyleSheet("")
|
||||
|
||||
def _update_theme_toggle_button_text(self):
|
||||
"""Updates the theme button text and tooltip based on the current theme."""
|
||||
if self.parent_app.current_theme == "dark":
|
||||
self.theme_toggle_button.setText(self._tr("theme_toggle_light", "Switch to Light Mode"))
|
||||
self.theme_toggle_button.setToolTip(self._tr("theme_tooltip_light", "Change the application appearance to light."))
|
||||
else:
|
||||
self.theme_toggle_button.setText(self._tr("theme_toggle_dark", "Switch to Dark Mode"))
|
||||
self.theme_toggle_button.setToolTip(self._tr("theme_tooltip_dark", "Change the application appearance to dark."))
|
||||
|
||||
def _toggle_theme(self):
|
||||
"""Toggles the application theme and updates the UI."""
|
||||
new_theme = "light" if self.parent_app.current_theme == "dark" else "dark"
|
||||
self.parent_app.apply_theme(new_theme)
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _populate_language_combo_box(self):
|
||||
"""Populates the language dropdown with available languages."""
|
||||
self.language_combo_box.blockSignals(True)
|
||||
self.language_combo_box.clear()
|
||||
languages = [
|
||||
("en","English"),
|
||||
("ja","日本語 (Japanese)"),
|
||||
("fr","Français (French)"),
|
||||
("de","Deutsch (German)"),
|
||||
("es","Español (Spanish)"),
|
||||
("pt","Português (Portuguese)"),
|
||||
("ru","Русский (Russian)"),
|
||||
("zh_CN","简体中文 (Simplified Chinese)"),
|
||||
("zh_TW","繁體中文 (Traditional Chinese)"),
|
||||
("ko","한국어 (Korean)")
|
||||
]
|
||||
for lang_code, lang_name in languages:
|
||||
self.language_combo_box.addItem(lang_name, lang_code)
|
||||
if self.parent_app.current_selected_language == lang_code:
|
||||
self.language_combo_box.setCurrentIndex(self.language_combo_box.count() - 1)
|
||||
self.language_combo_box.blockSignals(False)
|
||||
|
||||
def _language_selection_changed(self, index):
|
||||
"""Handles the user selecting a new language."""
|
||||
selected_lang_code = self.language_combo_box.itemData(index)
|
||||
if selected_lang_code and selected_lang_code != self.parent_app.current_selected_language:
|
||||
self.parent_app.current_selected_language = selected_lang_code
|
||||
self.parent_app.settings.setValue(LANGUAGE_KEY, selected_lang_code)
|
||||
self.parent_app.settings.sync()
|
||||
|
||||
self._retranslate_ui()
|
||||
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(QMessageBox.Information)
|
||||
msg_box.setWindowTitle(self._tr("language_change_title", "Language Changed"))
|
||||
msg_box.setText(self._tr("language_change_message", "A restart is required..."))
|
||||
msg_box.setInformativeText(self._tr("language_change_informative", "Would you like to restart now?"))
|
||||
restart_button = msg_box.addButton(self._tr("restart_now_button", "Restart Now"), QMessageBox.ApplyRole)
|
||||
ok_button = msg_box.addButton(self._tr("ok_button", "OK"), QMessageBox.AcceptRole)
|
||||
msg_box.setDefaultButton(ok_button)
|
||||
msg_box.exec_()
|
||||
|
||||
if msg_box.clickedButton() == restart_button:
|
||||
self.parent_app._request_restart_application()
|
||||
|
||||
def _save_download_path(self):
|
||||
"""Saves the current download path from the main window to settings."""
|
||||
if hasattr(self.parent_app, 'dir_input') and self.parent_app.dir_input:
|
||||
current_path = self.parent_app.dir_input.text().strip()
|
||||
if current_path:
|
||||
if os.path.isdir(current_path):
|
||||
self.parent_app.settings.setValue(DOWNLOAD_LOCATION_KEY, current_path)
|
||||
self.parent_app.settings.sync()
|
||||
QMessageBox.information(self,
|
||||
self._tr("settings_save_path_success_title", "Path Saved"),
|
||||
self._tr("settings_save_path_success_message", "Download location '{path}' saved.").format(path=current_path))
|
||||
else:
|
||||
QMessageBox.warning(self,
|
||||
self._tr("settings_save_path_invalid_title", "Invalid Path"),
|
||||
self._tr("settings_save_path_invalid_message", "The path '{path}' is not a valid directory.").format(path=current_path))
|
||||
else:
|
||||
QMessageBox.warning(self,
|
||||
self._tr("settings_save_path_empty_title", "Empty Path"),
|
||||
self._tr("settings_save_path_empty_message", "Download location cannot be empty."))
|
||||
else:
|
||||
QMessageBox.critical(self, "Error", "Could not access download path input from main application.")
|
||||
192
src/ui/dialogs/HelpGuideDialog.py
Normal file
192
src/ui/dialogs/HelpGuideDialog.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# --- Standard Library Imports ---
|
||||
import os
|
||||
import sys
|
||||
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import QUrl, QSize, Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
|
||||
QStackedWidget, QScrollArea, QFrame, QWidget
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
from ...i18n.translator import get_translation
|
||||
from ..main_window import get_app_icon_object
|
||||
|
||||
|
||||
class TourStepWidget(QWidget):
|
||||
"""
|
||||
A custom widget representing a single step or page in the feature guide.
|
||||
It neatly formats a title and its corresponding content.
|
||||
"""
|
||||
def __init__(self, title_text, content_text, parent=None):
|
||||
super().__init__(parent)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(10)
|
||||
|
||||
title_label = QLabel(title_text)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setFrameShape(QFrame.NoFrame)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
scroll_area.setStyleSheet("background-color: transparent;")
|
||||
|
||||
content_label = QLabel(content_text)
|
||||
content_label.setWordWrap(True)
|
||||
content_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
content_label.setTextFormat(Qt.RichText)
|
||||
content_label.setOpenExternalLinks(True) # Allow opening links in the content
|
||||
content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;")
|
||||
scroll_area.setWidget(content_label)
|
||||
layout.addWidget(scroll_area, 1)
|
||||
|
||||
|
||||
class HelpGuideDialog (QDialog ):
|
||||
"""A multi-page dialog for displaying the feature guide."""
|
||||
def __init__ (self ,steps_data ,parent_app ,parent =None ):
|
||||
super ().__init__ (parent )
|
||||
self .current_step =0
|
||||
self .steps_data =steps_data
|
||||
self .parent_app =parent_app
|
||||
|
||||
app_icon =get_app_icon_object ()
|
||||
if app_icon and not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
self .setModal (True )
|
||||
self .setFixedSize (650 ,600 )
|
||||
|
||||
|
||||
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 ()
|
||||
if self .parent_app :
|
||||
self .move (self .parent_app .geometry ().center ()-self .rect ().center ())
|
||||
|
||||
def _tr (self ,key ,default_text =""):
|
||||
"""Helper to get translation based on current app language."""
|
||||
if callable (get_translation )and self .parent_app :
|
||||
return get_translation (self .parent_app .current_selected_language ,key ,default_text )
|
||||
return default_text
|
||||
|
||||
|
||||
def _init_ui (self ):
|
||||
main_layout =QVBoxLayout (self )
|
||||
main_layout .setContentsMargins (0 ,0 ,0 ,0 )
|
||||
main_layout .setSpacing (0 )
|
||||
|
||||
self .stacked_widget =QStackedWidget ()
|
||||
main_layout .addWidget (self .stacked_widget ,1 )
|
||||
|
||||
self .tour_steps_widgets =[]
|
||||
for title ,content in self .steps_data :
|
||||
step_widget =TourStepWidget (title ,content )
|
||||
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"))
|
||||
|
||||
buttons_layout =QHBoxLayout ()
|
||||
buttons_layout .setContentsMargins (15 ,10 ,15 ,15 )
|
||||
buttons_layout .setSpacing (10 )
|
||||
|
||||
self .back_button =QPushButton (self ._tr ("tour_dialog_back_button","Back"))
|
||||
self .back_button .clicked .connect (self ._previous_step )
|
||||
self .back_button .setEnabled (False )
|
||||
|
||||
if getattr (sys ,'frozen',False )and hasattr (sys ,'_MEIPASS'):
|
||||
assets_base_dir =sys ._MEIPASS
|
||||
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__), '..', '..', '..'))
|
||||
|
||||
github_icon_path =os .path .join (assets_base_dir ,"assets","github.png")
|
||||
instagram_icon_path =os .path .join (assets_base_dir ,"assets","instagram.png")
|
||||
discord_icon_path =os .path .join (assets_base_dir ,"assets","discord.png")
|
||||
|
||||
self .github_button =QPushButton (QIcon (github_icon_path ),"")
|
||||
self .instagram_button =QPushButton (QIcon (instagram_icon_path ),"")
|
||||
self .Discord_button =QPushButton (QIcon (discord_icon_path ),"")
|
||||
|
||||
icon_size =QSize (24 ,24 )
|
||||
self .github_button .setIconSize (icon_size )
|
||||
self .instagram_button .setIconSize (icon_size )
|
||||
self .Discord_button .setIconSize (icon_size )
|
||||
|
||||
self .next_button =QPushButton (self ._tr ("tour_dialog_next_button","Next"))
|
||||
self .next_button .clicked .connect (self ._next_step_action )
|
||||
self .next_button .setDefault (True )
|
||||
self .github_button .clicked .connect (self ._open_github_link )
|
||||
self .instagram_button .clicked .connect (self ._open_instagram_link )
|
||||
self .Discord_button .clicked .connect (self ._open_Discord_link )
|
||||
self .github_button .setToolTip (self ._tr ("help_guide_github_tooltip","Visit project's GitHub page (Opens in browser)"))
|
||||
self .instagram_button .setToolTip (self ._tr ("help_guide_instagram_tooltip","Visit our Instagram page (Opens in browser)"))
|
||||
self .Discord_button .setToolTip (self ._tr ("help_guide_discord_tooltip","Visit our Discord community (Opens in browser)"))
|
||||
|
||||
|
||||
social_layout =QHBoxLayout ()
|
||||
social_layout .setSpacing (10 )
|
||||
social_layout .addWidget (self .github_button )
|
||||
social_layout .addWidget (self .instagram_button )
|
||||
social_layout .addWidget (self .Discord_button )
|
||||
|
||||
while buttons_layout .count ():
|
||||
item =buttons_layout .takeAt (0 )
|
||||
if item .widget ():
|
||||
item .widget ().setParent (None )
|
||||
elif item .layout ():
|
||||
pass
|
||||
buttons_layout .addLayout (social_layout )
|
||||
buttons_layout .addStretch (1 )
|
||||
buttons_layout .addWidget (self .back_button )
|
||||
buttons_layout .addWidget (self .next_button )
|
||||
main_layout .addLayout (buttons_layout )
|
||||
self ._update_button_states ()
|
||||
|
||||
def _next_step_action (self ):
|
||||
if self .current_step <len (self .tour_steps_widgets )-1 :
|
||||
self .current_step +=1
|
||||
self .stacked_widget .setCurrentIndex (self .current_step )
|
||||
else :
|
||||
self .accept ()
|
||||
self ._update_button_states ()
|
||||
|
||||
def _previous_step (self ):
|
||||
if self .current_step >0 :
|
||||
self .current_step -=1
|
||||
self .stacked_widget .setCurrentIndex (self .current_step )
|
||||
self ._update_button_states ()
|
||||
|
||||
def _update_button_states (self ):
|
||||
if self .current_step ==len (self .tour_steps_widgets )-1 :
|
||||
self .next_button .setText (self ._tr ("tour_dialog_finish_button","Finish"))
|
||||
else :
|
||||
self .next_button .setText (self ._tr ("tour_dialog_next_button","Next"))
|
||||
self .back_button .setEnabled (self .current_step >0 )
|
||||
|
||||
def _open_github_link (self ):
|
||||
QDesktopServices .openUrl (QUrl ("https://github.com/Yuvi9587"))
|
||||
|
||||
def _open_instagram_link (self ):
|
||||
QDesktopServices .openUrl (QUrl ("https://www.instagram.com/uvi.arts/"))
|
||||
|
||||
def _open_Discord_link (self ):
|
||||
QDesktopServices .openUrl (QUrl ("https://discord.gg/BqP64XTdJN"))
|
||||
150
src/ui/dialogs/KnownNamesFilterDialog.py
Normal file
150
src/ui/dialogs/KnownNamesFilterDialog.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget,
|
||||
QListWidgetItem, QPushButton, QVBoxLayout
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
from ...i18n.translator import get_translation
|
||||
from ..main_window import get_app_icon_object
|
||||
|
||||
|
||||
class KnownNamesFilterDialog(QDialog):
|
||||
"""
|
||||
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
|
||||
|
||||
to reuse their saved names and groups for filtering downloads.
|
||||
"""
|
||||
|
||||
def __init__(self, known_names_list, parent_app_ref, parent=None):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
known_names_list (list): A list of known name objects (dicts) from Known.txt.
|
||||
parent_app_ref (DownloaderApp): A reference to the main application window.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.parent_app = parent_app_ref
|
||||
self.setModal(True)
|
||||
self.all_known_name_entries = sorted(known_names_list, key=lambda x: x['name'].lower())
|
||||
self.selected_entries_to_return = []
|
||||
|
||||
# --- Basic Window Setup ---
|
||||
app_icon = get_app_icon_object()
|
||||
if app_icon and not app_icon.isNull():
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
# Set window size dynamically
|
||||
screen_geometry = QApplication.primaryScreen().availableGeometry()
|
||||
base_width, base_height = 460, 450
|
||||
scale_factor_h = screen_geometry.height() / 1080.0
|
||||
effective_scale_factor = max(0.75, min(scale_factor_h, 1.5))
|
||||
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 ---
|
||||
self._init_ui()
|
||||
self._retranslate_ui()
|
||||
self._apply_theme()
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts for the dialog."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.textChanged.connect(self._filter_list_display)
|
||||
main_layout.addWidget(self.search_input)
|
||||
|
||||
self.names_list_widget = QListWidget()
|
||||
self._populate_list_widget()
|
||||
main_layout.addWidget(self.names_list_widget)
|
||||
|
||||
# --- Control Buttons ---
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
self.select_all_button = QPushButton()
|
||||
self.select_all_button.clicked.connect(self._select_all_items)
|
||||
buttons_layout.addWidget(self.select_all_button)
|
||||
|
||||
self.deselect_all_button = QPushButton()
|
||||
self.deselect_all_button.clicked.connect(self._deselect_all_items)
|
||||
buttons_layout.addWidget(self.deselect_all_button)
|
||||
buttons_layout.addStretch(1)
|
||||
|
||||
self.add_button = QPushButton()
|
||||
self.add_button.clicked.connect(self._accept_selection_action)
|
||||
self.add_button.setDefault(True)
|
||||
buttons_layout.addWidget(self.add_button)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
buttons_layout.addWidget(self.cancel_button)
|
||||
main_layout.addLayout(buttons_layout)
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper to get translation based on the main application's current language."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _retranslate_ui(self):
|
||||
"""Sets the text for all translatable UI elements."""
|
||||
self.setWindowTitle(self._tr("known_names_filter_dialog_title", "Add Known Names to Filter"))
|
||||
self.search_input.setPlaceholderText(self._tr("known_names_filter_search_placeholder", "Search names..."))
|
||||
self.select_all_button.setText(self._tr("known_names_filter_select_all_button", "Select All"))
|
||||
self.deselect_all_button.setText(self._tr("known_names_filter_deselect_all_button", "Deselect All"))
|
||||
self.add_button.setText(self._tr("known_names_filter_add_selected_button", "Add Selected"))
|
||||
self.cancel_button.setText(self._tr("fav_posts_cancel_button", "Cancel"))
|
||||
|
||||
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())
|
||||
|
||||
def _populate_list_widget(self):
|
||||
"""Populates the list widget with the known names."""
|
||||
self.names_list_widget.clear()
|
||||
for entry_obj in self.all_known_name_entries:
|
||||
item = QListWidgetItem(entry_obj['name'])
|
||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||
item.setCheckState(Qt.Unchecked)
|
||||
item.setData(Qt.UserRole, entry_obj)
|
||||
self.names_list_widget.addItem(item)
|
||||
|
||||
def _filter_list_display(self):
|
||||
"""Filters the displayed list based on the search input text."""
|
||||
search_text_lower = self.search_input.text().lower()
|
||||
for i in range(self.names_list_widget.count()):
|
||||
item = self.names_list_widget.item(i)
|
||||
entry_obj = item.data(Qt.UserRole)
|
||||
matches_search = not search_text_lower or search_text_lower in entry_obj['name'].lower()
|
||||
item.setHidden(not matches_search)
|
||||
|
||||
def _select_all_items(self):
|
||||
"""Checks all visible items in the list widget."""
|
||||
for i in range(self.names_list_widget.count()):
|
||||
item = self.names_list_widget.item(i)
|
||||
if not item.isHidden():
|
||||
item.setCheckState(Qt.Checked)
|
||||
|
||||
def _deselect_all_items(self):
|
||||
"""Unchecks all items in the list widget."""
|
||||
for i in range(self.names_list_widget.count()):
|
||||
self.names_list_widget.item(i).setCheckState(Qt.Unchecked)
|
||||
|
||||
def _accept_selection_action(self):
|
||||
"""Gathers the selected entries and accepts the dialog."""
|
||||
self.selected_entries_to_return = []
|
||||
for i in range(self.names_list_widget.count()):
|
||||
item = self.names_list_widget.item(i)
|
||||
if item.checkState() == Qt.Checked:
|
||||
self.selected_entries_to_return.append(item.data(Qt.UserRole))
|
||||
self.accept()
|
||||
|
||||
def get_selected_entries(self):
|
||||
"""Returns the list of known name entries selected by the user."""
|
||||
return self.selected_entries_to_return
|
||||
217
src/ui/dialogs/TourDialog.py
Normal file
217
src/ui/dialogs/TourDialog.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# --- Standard Library Imports ---
|
||||
import os
|
||||
import sys
|
||||
|
||||
# --- PyQt5 Imports ---
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QSettings, QCoreApplication
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
|
||||
QStackedWidget, QScrollArea, QFrame, QWidget, QCheckBox
|
||||
)
|
||||
|
||||
# --- Local Application Imports ---
|
||||
from ...i18n.translator import get_translation
|
||||
from ..main_window import get_app_icon_object
|
||||
from ...config.constants import (
|
||||
CONFIG_ORGANIZATION_NAME
|
||||
)
|
||||
|
||||
|
||||
class TourStepWidget(QWidget):
|
||||
"""
|
||||
A custom widget representing a single step or page in the feature tour.
|
||||
It neatly formats a title and its corresponding content.
|
||||
"""
|
||||
def __init__(self, title_text, content_text, parent=None):
|
||||
super().__init__(parent)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(10)
|
||||
|
||||
title_label = QLabel(title_text)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #E0E0E0; padding-bottom: 15px;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setFrameShape(QFrame.NoFrame)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
scroll_area.setStyleSheet("background-color: transparent;")
|
||||
|
||||
content_label = QLabel(content_text)
|
||||
content_label.setWordWrap(True)
|
||||
content_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
content_label.setTextFormat(Qt.RichText)
|
||||
content_label.setOpenExternalLinks(True)
|
||||
content_label.setStyleSheet("font-size: 11pt; color: #C8C8C8; line-height: 1.8;")
|
||||
scroll_area.setWidget(content_label)
|
||||
layout.addWidget(scroll_area, 1)
|
||||
|
||||
|
||||
class TourDialog(QDialog):
|
||||
"""
|
||||
A dialog that shows a multi-page tour to the user on first launch.
|
||||
Includes a "Never show again" checkbox and uses QSettings to remember this preference.
|
||||
"""
|
||||
tour_finished_normally = pyqtSignal()
|
||||
tour_skipped = pyqtSignal()
|
||||
|
||||
# Constants for QSettings
|
||||
CONFIG_APP_NAME_TOUR = "ApplicationTour"
|
||||
TOUR_SHOWN_KEY = "neverShowTourAgainV19"
|
||||
|
||||
def __init__(self, parent_app, parent=None):
|
||||
"""
|
||||
Initializes the dialog.
|
||||
|
||||
Args:
|
||||
parent_app (DownloaderApp): A reference to the main application window.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.settings = QSettings(CONFIG_ORGANIZATION_NAME, self.CONFIG_APP_NAME_TOUR)
|
||||
self.current_step = 0
|
||||
self.parent_app = parent_app
|
||||
|
||||
self.setWindowIcon(get_app_icon_object())
|
||||
self.setModal(True)
|
||||
self.setFixedSize(600, 620)
|
||||
|
||||
self._init_ui()
|
||||
self._apply_theme()
|
||||
self._center_on_screen()
|
||||
|
||||
def _tr(self, key, default_text=""):
|
||||
"""Helper for translation."""
|
||||
if callable(get_translation) and self.parent_app:
|
||||
return get_translation(self.parent_app.current_selected_language, key, default_text)
|
||||
return default_text
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initializes all UI components and layouts."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
self.stacked_widget = QStackedWidget()
|
||||
main_layout.addWidget(self.stacked_widget, 1)
|
||||
|
||||
# Load content for each step
|
||||
steps_content = [
|
||||
("tour_dialog_step1_title", "tour_dialog_step1_content"),
|
||||
("tour_dialog_step2_title", "tour_dialog_step2_content"),
|
||||
("tour_dialog_step3_title", "tour_dialog_step3_content"),
|
||||
("tour_dialog_step4_title", "tour_dialog_step4_content"),
|
||||
("tour_dialog_step5_title", "tour_dialog_step5_content"),
|
||||
("tour_dialog_step6_title", "tour_dialog_step6_content"),
|
||||
("tour_dialog_step7_title", "tour_dialog_step7_content"),
|
||||
("tour_dialog_step8_title", "tour_dialog_step8_content"),
|
||||
]
|
||||
|
||||
self.tour_steps_widgets = []
|
||||
for title_key, content_key in steps_content:
|
||||
title = self._tr(title_key, title_key)
|
||||
content = self._tr(content_key, "Content not found.")
|
||||
step_widget = TourStepWidget(title, content)
|
||||
self.tour_steps_widgets.append(step_widget)
|
||||
self.stacked_widget.addWidget(step_widget)
|
||||
|
||||
self.setWindowTitle(self._tr("tour_dialog_title", "Welcome to Kemono Downloader!"))
|
||||
|
||||
# --- Bottom Controls ---
|
||||
bottom_controls_layout = QVBoxLayout()
|
||||
bottom_controls_layout.setContentsMargins(15, 10, 15, 15)
|
||||
bottom_controls_layout.setSpacing(12)
|
||||
|
||||
self.never_show_again_checkbox = QCheckBox(self._tr("tour_dialog_never_show_checkbox", "Never show this tour again"))
|
||||
bottom_controls_layout.addWidget(self.never_show_again_checkbox, 0, Qt.AlignLeft)
|
||||
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout.setSpacing(10)
|
||||
self.skip_button = QPushButton(self._tr("tour_dialog_skip_button", "Skip Tour"))
|
||||
self.skip_button.clicked.connect(self._skip_tour_action)
|
||||
self.back_button = QPushButton(self._tr("tour_dialog_back_button", "Back"))
|
||||
self.back_button.clicked.connect(self._previous_step)
|
||||
self.next_button = QPushButton(self._tr("tour_dialog_next_button", "Next"))
|
||||
self.next_button.clicked.connect(self._next_step_action)
|
||||
self.next_button.setDefault(True)
|
||||
|
||||
buttons_layout.addWidget(self.skip_button)
|
||||
buttons_layout.addStretch(1)
|
||||
buttons_layout.addWidget(self.back_button)
|
||||
buttons_layout.addWidget(self.next_button)
|
||||
|
||||
bottom_controls_layout.addLayout(buttons_layout)
|
||||
main_layout.addLayout(bottom_controls_layout)
|
||||
|
||||
self._update_button_states()
|
||||
|
||||
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())
|
||||
else:
|
||||
self.setStyleSheet("QDialog { background-color: #f0f0f0; }")
|
||||
|
||||
def _center_on_screen(self):
|
||||
"""Centers the dialog on the screen."""
|
||||
try:
|
||||
screen_geo = QApplication.primaryScreen().availableGeometry()
|
||||
self.move(screen_geo.center() - self.rect().center())
|
||||
except Exception as e:
|
||||
print(f"[TourDialog] Error centering dialog: {e}")
|
||||
|
||||
def _next_step_action(self):
|
||||
"""Moves to the next step or finishes the tour."""
|
||||
if self.current_step < len(self.tour_steps_widgets) - 1:
|
||||
self.current_step += 1
|
||||
self.stacked_widget.setCurrentIndex(self.current_step)
|
||||
else:
|
||||
self._finish_tour_action()
|
||||
self._update_button_states()
|
||||
|
||||
def _previous_step(self):
|
||||
"""Moves to the previous step."""
|
||||
if self.current_step > 0:
|
||||
self.current_step -= 1
|
||||
self.stacked_widget.setCurrentIndex(self.current_step)
|
||||
self._update_button_states()
|
||||
|
||||
def _update_button_states(self):
|
||||
"""Updates the state and text of navigation buttons."""
|
||||
is_last_step = self.current_step == len(self.tour_steps_widgets) - 1
|
||||
self.next_button.setText(self._tr("tour_dialog_finish_button", "Finish") if is_last_step else self._tr("tour_dialog_next_button", "Next"))
|
||||
self.back_button.setEnabled(self.current_step > 0)
|
||||
|
||||
def _skip_tour_action(self):
|
||||
"""Handles the action when the tour is skipped."""
|
||||
self._save_settings_if_checked()
|
||||
self.tour_skipped.emit()
|
||||
self.reject()
|
||||
|
||||
def _finish_tour_action(self):
|
||||
"""Handles the action when the tour is finished normally."""
|
||||
self._save_settings_if_checked()
|
||||
self.tour_finished_normally.emit()
|
||||
self.accept()
|
||||
|
||||
def _save_settings_if_checked(self):
|
||||
"""Saves the 'never show again' preference to QSettings."""
|
||||
self.settings.setValue(self.TOUR_SHOWN_KEY, self.never_show_again_checkbox.isChecked())
|
||||
self.settings.sync()
|
||||
|
||||
@staticmethod
|
||||
def should_show_tour():
|
||||
"""Checks QSettings to see if the tour should be shown on startup."""
|
||||
settings = QSettings(TourDialog.CONFIG_ORGANIZATION_NAME, TourDialog.CONFIG_APP_NAME_TOUR)
|
||||
never_show = settings.value(TourDialog.TOUR_SHOWN_KEY, False, type=bool)
|
||||
return not never_show
|
||||
|
||||
CONFIG_ORGANIZATION_NAME = CONFIG_ORGANIZATION_NAME
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Ensures settings are saved if the dialog is closed via the 'X' button."""
|
||||
self._skip_tour_action()
|
||||
super().closeEvent(event)
|
||||
1
src/ui/dialogs/__init__.py
Normal file
1
src/ui/dialogs/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# ...existing code...
|
||||
Reference in New Issue
Block a user